001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.resources; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.OutputStream; 021import java.io.Serializable; 022import java.util.Collection; 023import java.util.Map; 024import java.util.Set; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import org.apache.avalon.framework.parameters.Parameters; 029import org.apache.cocoon.ProcessingException; 030import org.apache.cocoon.ResourceNotFoundException; 031import org.apache.commons.io.IOUtils; 032import org.apache.commons.lang3.StringUtils; 033import org.apache.excalibur.source.Source; 034import org.apache.excalibur.source.SourceNotFoundException; 035import org.apache.excalibur.source.SourceResolver; 036 037import org.ametys.core.util.ImageHelper; 038 039import io.github.borewit.sanitize.SVGSanitizer; 040 041/** 042 * Resource handler for images 043 */ 044public class ImageResourceHandler extends SimpleResourceHandler 045{ 046 /** url for safe svg files */ 047 protected static final Pattern SAFE_SVG_PATTERN = Pattern.compile("^plugin(|:[^:]+)://resources/.+"); 048 049 private static final Pattern _SIZE_PATTERN = Pattern.compile("^(.+)_(max|crop|)(\\d+)x(\\d+)(\\.[^./]+)?$"); 050 051 private static final Collection<String> __ALLOWED_OUTPUT_FORMATS = Set.of("png", "gif", "jpg", "jpeg"); 052 private static final Collection<String> __UNRESIZABLE_FORMATS = Set.of("svg"); 053 054 private int _height; 055 private int _width; 056 private int _maxHeight; 057 private int _maxWidth; 058 private int _cropHeight; 059 private int _cropWidth; 060 private String _location; 061 062 private boolean _download; 063 064 record SizedSource(Source source, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) { /* empty */ } 065 066 /** 067 * Default constructor 068 */ 069 public ImageResourceHandler() 070 { 071 super(); 072 } 073 074 /** 075 * If the {@link Source} is already resolved by the {@link ResourceHandlerProvider}, 076 * it may provide it through the constructor to avoid resolving it again. 077 * @param sizedSource the source. 078 */ 079 public ImageResourceHandler(SizedSource sizedSource) 080 { 081 super(sizedSource.source); 082 _toFields(sizedSource); 083 } 084 085 private void _toFields(SizedSource sizedSource) 086 { 087 _source = sizedSource.source; 088 _height = sizedSource.height; 089 _width = sizedSource.width; 090 _maxHeight = sizedSource.maxHeight; 091 _maxWidth = sizedSource.maxWidth; 092 _cropHeight = sizedSource.cropHeight; 093 _cropWidth = sizedSource.cropWidth; 094 } 095 096 @Override 097 public Source setup(String location, Map objectModel, Parameters par, boolean readForDownload) throws IOException, ProcessingException 098 { 099 if (_source == null) 100 { 101 // If the source has not been resolved by the provider, do it now 102 SizedSource sizedSource = _resolveSource(location, _resolver); 103 if (sizedSource != null) 104 { 105 _toFields(sizedSource); 106 } 107 } 108 109 if (_source != null) 110 { 111 _location = location; 112 _download = readForDownload; 113 114 return _source; 115 } 116 else 117 { 118 throw new ResourceNotFoundException("Resource not found for URI : " + location); 119 } 120 } 121 122 /** 123 * Is the SVG file denoted by the uri potentially unsafe? 124 * @param sourceURI The svg file uri 125 * @return true if the svg is not from a trusted source 126 */ 127 protected boolean isUnsafeSVG(String sourceURI) 128 { 129 return !SAFE_SVG_PATTERN.matcher(sourceURI).matches(); 130 } 131 132 @Override 133 public void generate(OutputStream out) throws IOException, ProcessingException 134 { 135 String sourceURI = _source.getURI(); 136 String fileExtension = StringUtils.substringAfterLast(sourceURI, ".").toLowerCase(); 137 138 try (InputStream is = _source.getInputStream()) 139 { 140 if (_processImage(fileExtension)) 141 { 142 String outputFormat = __ALLOWED_OUTPUT_FORMATS.contains(fileExtension) ? fileExtension : "png"; 143 ImageHelper.generateThumbnail(is, out, outputFormat, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth); 144 } 145 else if (StringUtils.equalsIgnoreCase(fileExtension, "svg") && isUnsafeSVG(_location)) 146 { 147 try 148 { 149 SVGSanitizer.sanitize(is, out); 150 } 151 catch (Exception e) 152 { 153 throw new ProcessingException("Cannot sanitize the SVG " + sourceURI, e); 154 } 155 } 156 else 157 { 158 // Copy data in response 159 IOUtils.copy(is, out); 160 } 161 } 162 } 163 164 private boolean _processImage(String fileExtension) 165 { 166 if (__UNRESIZABLE_FORMATS.contains(fileExtension)) 167 { 168 return false; 169 } 170 else if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0) 171 { 172 // resize or crop is required, assume this is an image 173 return true; 174 } 175 else if (!_download) 176 { 177 String mimeType = _source.getMimeType(); 178 179 // only process image if it is for rendering purposes 180 return mimeType != null && mimeType.startsWith("image/"); 181 } 182 else 183 { 184 return false; 185 } 186 } 187 188 @Override 189 public Serializable getKey() 190 { 191 return _source.getURI() + "###" + _width + "x" + _height + "x" + _maxWidth + "x" + _maxHeight + "x" + _cropWidth + "x" + _cropHeight; 192 } 193 194 /** 195 * Resolve the source at the given location 196 * @param location the location of the source to resolve 197 * @param resolver the source resolver 198 * @return the resolved source or null 199 */ 200 static SizedSource _resolveSource(String location, SourceResolver resolver) 201 { 202 try 203 { 204 Source source = null; 205 try 206 { 207 source = resolver.resolveURI(location); 208 if (source != null && source.exists()) 209 { 210 return new SizedSource(source, 0, 0, 0, 0, 0, 0); 211 } 212 else 213 { 214 throw new SourceNotFoundException(location); 215 } 216 } 217 catch (SourceNotFoundException e) 218 { 219 resolver.release(source); 220 } 221 222 223 Matcher sizeMatcher = _SIZE_PATTERN.matcher(location); 224 if (sizeMatcher.matches()) 225 { 226 String computedLocation = sizeMatcher.group(1); 227 String suffix = sizeMatcher.group(5); 228 if (suffix != null) 229 { 230 computedLocation += suffix; 231 } 232 233 try 234 { 235 source = resolver.resolveURI(computedLocation); 236 if (!source.exists()) 237 { 238 throw new SourceNotFoundException(computedLocation); 239 } 240 } 241 catch (SourceNotFoundException e) 242 { 243 resolver.release(source); 244 return null; 245 } 246 247 // type is either empty (resize), max or crop. 248 String type = sizeMatcher.group(2); 249 250 int pHeight = Integer.parseInt(sizeMatcher.group(3)); 251 int pWidth = Integer.parseInt(sizeMatcher.group(4)); 252 253 int height = "".equals(type) ? pHeight : 0; 254 int width = "".equals(type) ? pWidth : 0; 255 int maxHeight = "max".equals(type) ? pHeight : 0; 256 int maxWidth = "max".equals(type) ? pWidth : 0; 257 int cropHeight = "crop".equals(type) ? pHeight : 0; 258 int cropWidth = "crop".equals(type) ? pWidth : 0; 259 260 return new SizedSource(source, height, width, maxHeight, maxWidth, cropHeight, cropWidth); 261 } 262 else 263 { 264 return null; 265 } 266 } 267 catch (IOException e) 268 { 269 return null; 270 } 271 } 272}