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.commons.lang3.Strings; 034import org.apache.excalibur.source.Source; 035import org.apache.excalibur.source.SourceNotFoundException; 036import org.apache.excalibur.source.SourceResolver; 037 038import org.ametys.core.util.ImageHelper; 039import org.ametys.core.util.POIHolder; 040import org.ametys.core.util.POIHolder.Point; 041 042import io.github.borewit.sanitize.SVGSanitizer; 043 044/** 045 * Resource handler for images 046 */ 047public class ImageResourceHandler extends SimpleResourceHandler 048{ 049 /** url for safe svg files */ 050 protected static final Pattern SAFE_SVG_PATTERN = Pattern.compile("^plugin(|:[^:]+)://resources/.+"); 051 052 private static final Pattern _SIZE_PATTERN = Pattern.compile("^(.+)_(max|crop|)(\\d+)x(\\d+)(\\.[^./]+)?$"); 053 054 private static final Collection<String> __ALLOWED_OUTPUT_FORMATS = Set.of("png", "gif", "jpg", "jpeg"); 055 private static final Collection<String> __UNRESIZABLE_FORMATS = Set.of("svg"); 056 057 private int _height; 058 private int _width; 059 private int _maxHeight; 060 private int _maxWidth; 061 private int _cropHeight; 062 private int _cropWidth; 063 private Point _poi; 064 private String _location; 065 066 private boolean _download; 067 068 069 record SizedSource(Source source, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth, Point poi) { /* empty */ } 070 071 /** 072 * Default constructor 073 */ 074 public ImageResourceHandler() 075 { 076 super(); 077 } 078 079 /** 080 * If the {@link Source} is already resolved by the {@link ResourceHandlerProvider}, 081 * it may provide it through the constructor to avoid resolving it again. 082 * @param sizedSource the source. 083 */ 084 public ImageResourceHandler(SizedSource sizedSource) 085 { 086 super(sizedSource.source); 087 _toFields(sizedSource); 088 } 089 090 private void _toFields(SizedSource sizedSource) 091 { 092 _source = sizedSource.source; 093 _height = sizedSource.height; 094 _width = sizedSource.width; 095 _maxHeight = sizedSource.maxHeight; 096 _maxWidth = sizedSource.maxWidth; 097 _cropHeight = sizedSource.cropHeight; 098 _cropWidth = sizedSource.cropWidth; 099 _poi = sizedSource.poi; 100 } 101 102 @Override 103 public Source setup(String location, Map objectModel, Parameters par, boolean readForDownload) throws IOException, ProcessingException 104 { 105 if (_source == null) 106 { 107 // If the source has not been resolved by the provider, do it now 108 SizedSource sizedSource = _resolveSource(location, _resolver); 109 if (sizedSource != null) 110 { 111 _toFields(sizedSource); 112 } 113 } 114 115 if (_source != null) 116 { 117 _location = location; 118 _download = readForDownload; 119 120 return _source; 121 } 122 else 123 { 124 throw new ResourceNotFoundException("Resource not found for URI : " + location); 125 } 126 } 127 128 /** 129 * Is the SVG file denoted by the uri potentially unsafe? 130 * @param sourceURI The svg file uri 131 * @return true if the svg is not from a trusted source 132 */ 133 protected boolean isUnsafeSVG(String sourceURI) 134 { 135 return !SAFE_SVG_PATTERN.matcher(sourceURI).matches(); 136 } 137 138 @Override 139 public void generate(OutputStream out) throws IOException, ProcessingException 140 { 141 String sourceURI = _source.getURI(); 142 String fileExtension = StringUtils.substringAfterLast(sourceURI, ".").toLowerCase(); 143 144 try (InputStream is = _source.getInputStream()) 145 { 146 if (_processImage(fileExtension)) 147 { 148 String outputFormat = __ALLOWED_OUTPUT_FORMATS.contains(fileExtension) ? fileExtension : "png"; 149 ImageHelper.generateThumbnail(is, out, outputFormat, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth, _poi); 150 } 151 else if (Strings.CI.equals(fileExtension, "svg") && isUnsafeSVG(_location)) 152 { 153 try 154 { 155 SVGSanitizer.sanitize(is, out); 156 } 157 catch (Exception e) 158 { 159 throw new ProcessingException("Cannot sanitize the SVG " + sourceURI, e); 160 } 161 } 162 else 163 { 164 // Copy data in response 165 IOUtils.copy(is, out); 166 } 167 } 168 } 169 170 private boolean _processImage(String fileExtension) 171 { 172 if (__UNRESIZABLE_FORMATS.contains(fileExtension)) 173 { 174 return false; 175 } 176 else if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0) 177 { 178 // resize or crop is required, assume this is an image 179 return true; 180 } 181 else if (!_download) 182 { 183 String mimeType = _source.getMimeType(); 184 185 // only process image if it is for rendering purposes 186 return mimeType != null && mimeType.startsWith("image/"); 187 } 188 else 189 { 190 return false; 191 } 192 } 193 194 @Override 195 public Serializable getKey() 196 { 197 return _source.getURI() + "###" + _width + "x" + _height + "x" + _maxWidth + "x" + _maxHeight + "x" + _cropWidth + "x" + _cropHeight; 198 } 199 200 private static SizedSource _getBasicSizedSource(String location, SourceResolver resolver) throws IOException 201 { 202 Source source = null; 203 try 204 { 205 source = resolver.resolveURI(location); 206 if (source != null && source.exists()) 207 { 208 return new SizedSource(source, 0, 0, 0, 0, 0, 0, null); 209 } 210 else 211 { 212 throw new SourceNotFoundException(location); 213 } 214 } 215 catch (SourceNotFoundException e) 216 { 217 resolver.release(source); 218 return null; 219 } 220 } 221 222 /** 223 * Resolve the source at the given location 224 * @param location the location of the source to resolve 225 * @param resolver the source resolver 226 * @return the resolved source or null 227 */ 228 static SizedSource _resolveSource(String location, SourceResolver resolver) 229 { 230 try 231 { 232 SizedSource sizedSource = _getBasicSizedSource(location, resolver); 233 if (sizedSource != null) 234 { 235 return sizedSource; 236 } 237 238 Source source = null; 239 240 Matcher sizeMatcher = _SIZE_PATTERN.matcher(location); 241 if (sizeMatcher.matches()) 242 { 243 String computedLocation = sizeMatcher.group(1); 244 String suffix = sizeMatcher.group(5); 245 if (suffix != null) 246 { 247 computedLocation += suffix; 248 } 249 250 try 251 { 252 source = resolver.resolveURI(computedLocation); 253 if (!source.exists()) 254 { 255 throw new SourceNotFoundException(computedLocation); 256 } 257 } 258 catch (SourceNotFoundException e) 259 { 260 resolver.release(source); 261 return null; 262 } 263 264 // type is either empty (resize), max or crop. 265 String type = sizeMatcher.group(2); 266 267 int pHeight = Integer.parseInt(sizeMatcher.group(3)); 268 int pWidth = Integer.parseInt(sizeMatcher.group(4)); 269 270 int height = "".equals(type) ? pHeight : 0; 271 int width = "".equals(type) ? pWidth : 0; 272 int maxHeight = "max".equals(type) ? pHeight : 0; 273 int maxWidth = "max".equals(type) ? pWidth : 0; 274 int cropHeight = "crop".equals(type) ? pHeight : 0; 275 int cropWidth = "crop".equals(type) ? pWidth : 0; 276 277 Point poi = null; 278 if (source instanceof POIHolder) 279 { 280 poi = ((POIHolder) source).getPOI(); 281 } 282 283 return new SizedSource(source, height, width, maxHeight, maxWidth, cropHeight, cropWidth, poi); 284 } 285 else 286 { 287 return null; 288 } 289 } 290 catch (IOException e) 291 { 292 return null; 293 } 294 } 295}