/*
* Copyright 2013 david gonzalez.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.activecq.tools.quickimage.impl;
import com.activecq.tools.quickimage.QuickImageResource;
import com.day.cq.commons.PathInfo;
import com.day.cq.commons.jcr.JcrConstants;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceDecorator;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.Constants;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* User: david
*
* Order or parameter application:
*
* Crop (x,y,width,height)
* Resize (width, height)
* Rotate (degrees)
*
*/
@Component(
label = "ActiveCQ - QuickImage Resource Decorator",
description = "QuickImage Resource Decorator that allows image resources to be rendered using the parameterization of the Adobe CQ Image Component (width, height, rotation and crop) via constructed URIs.",
metatype = true,
immediate = false,
configurationFactory = true)
@Properties({
@Property(
label="Vendor",
name= Constants.SERVICE_VENDOR,
value="ActiveCQ",
propertyPrivate=true
)
})
@Service
public class QuickImageResourceDecorator implements ResourceDecorator {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private static final int SYSTEM_MAX_DIMENSION = 10000;
/**
* OSGi Properties *
*/
private static final String DEFAULT_RESOURCE_TYPE = "foundation/components/parbase";
private String resourceType = DEFAULT_RESOURCE_TYPE;
@Property(label = ".img Resource Type", description = "Resource type to apply to QuickImage sling:resourceType property. This resourceType must implement the .img selector script. [Default: foundation/components/parbase]", value = DEFAULT_RESOURCE_TYPE)
private static final String PROP_RESOURCE_TYPE = "prop.resource-type";
private static final int DEFAULT_MAX_WIDTH = 2048;
private int maxWidth = DEFAULT_MAX_WIDTH;
@Property(label = "Max Image Width", description = "Maximum width dimension in pixels allowable for resizing. [Default: 2048]", intValue = DEFAULT_MAX_WIDTH)
private static final String PROP_MAX_WIDTH = "prop.image.max-width";
private static final int DEFAULT_MAX_HEIGHT = 2048;
private int maxHeight = DEFAULT_MAX_HEIGHT;
@Property(label = "Max Image Height", description = "Maximum height dimension in pixels allowable for resizing. [Default: 2048]", intValue = DEFAULT_MAX_HEIGHT)
private static final String PROP_MAX_HEIGHT = "prop.image.max-height";
private static final String[] DEFAULT_EXTENSION_WHITELIST = new String[] {"png", "jpg", "gif", "jpeg", "bmp", "tif", "tiff", "pic", "pict"};
private String[] extensionWhiteList = DEFAULT_EXTENSION_WHITELIST;
@Property(label = "Extension White List", description = "List of resource type extensions this decorator can decorate. [Default: \"png\", \"jpg\", \"gif\", \"jpeg\", \"bmp\", \"tif\", \"tiff\", \"pic\", \"pict\"]", value = { "png", "jpg", "gif", "jpeg", "bmp", "tif", "tiff", "pic", "pict" })
private static final String PROP_EXTENSION_WHITELIST = "prop.white-list.extensions";
private static final String[] DEFAULT_PATH_WHITELIST = new String[] { "^/content/.*", "^/etc/designs/.*", "^/apps/.*", "^/libs/.*" };
private String[] pathWhiteList = DEFAULT_PATH_WHITELIST;
@Property(label = "Path White List", description = "Regex for paths this decorator can decorate. [Default: \"^/content/.*\", \"^/etc/designs/.*\"]", value = { "^/content/.*", "^/etc/designs/.*", "^/apps/.*", "^/libs/.*" })
private static final String PROP_PATH_WHITELIST = "prop.white-list.paths";
private static final String[] DEFAULT_WIDTH_WHITELIST = new String[] { };
private String[] widthWhiteList = DEFAULT_WIDTH_WHITELIST;
@Property(label = "Width White List", description = "List of widths in pixels that are allowable for re-sizing. Blank allows all. System maximum of 10000. [Default: All]", value = { })
private static final String PROP_WIDTH_WHITELIST = "prop.white-list.widths";
private static final String[] DEFAULT_HEIGHT_WHITELIST = new String[] { };
private String[] heightWhiteList = DEFAULT_HEIGHT_WHITELIST;
@Property(label = "Height White List", description = "List of heights in pixels that are allowable for re-sizing. Blank allows all. System maximum of 10000. [Default: All]", value = { })
private static final String PROP_HEIGHT_WHITELIST = "prop.white-list.heights";
private static final String[] DEFAULT_ROTATE_WHITELIST = new String[] { };
private String[] rotateWhiteList = DEFAULT_ROTATE_WHITELIST;
@Property(label = "Rotation White List", description = "List of widths in pixels that are allowable for rotation. Blank allows all. [Default: All]", value = { })
private static final String PROP_ROTATE_WHITELIST = "prop.white-list.rotations";
/**
* Default cstor, will return the usual Resource object as Request is null
*
* @param resource
* @return
*/
public Resource decorate(Resource resource) {
return this.decorate(resource, null);
}
/**
* Returns a QuickImage resource
*
* @param resource
* @param request
* @return
*/
public Resource decorate(Resource resource, HttpServletRequest request) {
// Basic condition checking
if(!accepts(resource, request)) {
return resource;
}
final String suffix = getSuffix(request.getRequestURI());
// Get suffix values from the Request URI
String width = parseSuffix(suffix, com.activecq.tools.quickimage.Constants.KEY_WIDTH);
String height = parseSuffix(suffix, com.activecq.tools.quickimage.Constants.KEY_HEIGHT);
String rotate = parseSuffix(suffix, com.activecq.tools.quickimage.Constants.KEY_ROTATE);
String crop = parseSuffix(suffix, com.activecq.tools.quickimage.Constants.KEY_CROP, 4, ",");
// Scrub suffix param data
width = scrubDimension(width, maxWidth, widthWhiteList);
height = scrubDimension(height, maxHeight, heightWhiteList);
rotate = scrubRotate(rotate, rotateWhiteList);
// Create QuickImageResource
final QuickImageResource imageResource = new QuickImageResource(resource, resourceType);
imageResource.setCrop(crop);
imageResource.setRotate(rotate);
imageResource.setWidth(width);
imageResource.setHeight(height);
return imageResource;
}
/**
* Method used to determine if the request can be handled by the QuickImage Resource Decorator
*
* @param resource
* @param request
* @com.activecq.tools.quickimage.Constants
*/
private boolean accepts(Resource resource, HttpServletRequest request) {
if(resource == null || request == null) {
return false;
}
final RequestPathInfo pathInfo = new PathInfo(request.getRequestURI());
final String suffix = getSuffix(request.getRequestURI());
final String extension = pathInfo.getExtension();
final String quick = parseSuffix(suffix, com.activecq.tools.quickimage.Constants.KEY_QUICK, 0, null);
if(StringUtils.isBlank(extension) ||
StringUtils.isBlank(suffix) ||
!ResourceUtil.isA(resource, JcrConstants.NT_FILE) ||
!isWhiteListedMatch(resource.getPath(), pathWhiteList) ||
!isWhiteListed(extension, extensionWhiteList) ||
!StringUtils.equals(quick, String.valueOf(true))) {
return false;
}
return true;
}
/**
* Wrapper method for parseSuffix(String suffix, String key, int size, String delimiter)
*
* Passes
* size: 0
* delimiter: null
*
* @param suffix
* @param key
* @return
*/
private String parseSuffix(String suffix, String key) {
return parseSuffix(suffix, key, 1, null);
}
/**
* Get key/values from the suffix string
*
* @param suffix Suffix string (foo/bar/pets/cat/dog/nuts.bolts)
* @param key Suffix segment to treat as the key
* @param size Number of suffix value segments after the key segment to return
* @param delimiter Delimiter used to join the value segments
* @return "true" if key exists and size = 0, segment value is key exists and size = 1, joined segment values using delimiter if key exists and size > 1
*/
private String parseSuffix(String suffix, String key, int size, String delimiter) {
final String[] suffixes = StringUtils.split(suffix, "/");
final int index = ArrayUtils.indexOf(suffixes, key);
if(index < 0 || (index + size) >= suffixes.length) { return null; }
if(size < 1) {
return String.valueOf(true);
}
final List<String> result = new ArrayList<String>();
for(int i = index + 1; i <= index + size; i++) {
result.add(suffixes[i]);
}
delimiter = StringUtils.isBlank(delimiter) ? "" : delimiter;
return StringUtils.join(result, delimiter);
}
/**
* Returns the Suffix string from the RequestURI due to bug in CQ Common PathInfo parsing algorithm
*
* @param uri
* @return
*/
private String getSuffix(String uri) {
if(!StringUtils.contains(uri, '.')) {
return null;
}
String suffix = StringUtils.substringAfter(uri, ".");
suffix = StringUtils.substringAfter(suffix, "/");
if(StringUtils.isNotBlank(suffix)) {
return suffix;
}
return null;
}
/**
* Scrub width and height image resize parameters based on QuickImage configurations
*
* @param dimension
* @param max
* @param whitelist
* @return
*/
private String scrubDimension(String dimension, int max, String[] whitelist) {
final String DEFAULT_DIMENSION = "0";
try {
if(StringUtils.isBlank(dimension)) {
return DEFAULT_DIMENSION;
}
final int d = Integer.parseInt(dimension);
if(d < 0 || d > max || d > SYSTEM_MAX_DIMENSION) {
// falls outside of normal bounds, return zero
return DEFAULT_DIMENSION;
}
if(isWhiteListed(dimension, whitelist)) {
return dimension;
}
// Dimension is not a white listed value
return DEFAULT_DIMENSION;
} catch (Exception ex) {
// Error occurred parsing dimension value, use the DEFAULT_DIMENSION
// which forces the image to render using native dimension
return DEFAULT_DIMENSION;
}
}
/**
* Validates and normalizaed rotation parameter.
*
* This normalized values to exist between -360 and 360.
*
* @param rotate
* @param whiteList
* @return
*/
private String scrubRotate(String rotate, String[] whiteList) {
final String DEFAULT_ROTATE = "0";
try {
if(StringUtils.isBlank(rotate)) {
return DEFAULT_ROTATE;
}
final long r = Long.parseLong(rotate) % 360;
if(isWhiteListed(rotate, whiteList)) {
return String.valueOf(r);
}
// Rotate is not a white listed value
return DEFAULT_ROTATE;
} catch (Exception ex) {
// Error occurred parsing rotate value, use the DEFAULT_ROTATE
// which forces the image to render using 0 rotation
return DEFAULT_ROTATE;
}
}
/**
* Checks if the value exists in the parameter whitelist
*
* @param value
* @param whitelist
* @return true if the value is in the whitelist (case sensitive)
*/
private boolean isWhiteListed(String value, String[] whitelist) {
if(whitelist == null || whitelist.length == 0) {
return true;
}
return ArrayUtils.contains(whitelist, value);
}
/**
* Checks if the value matches an item in the parameter whiteList
*
* @param path
* @param whiteList
* @return true if the value matches a value in the whiteList (case sensitive)
*/
private boolean isWhiteListedMatch(String path, String[] whiteList) {
if(whiteList == null || whiteList.length == 0) {
return true;
} else if(StringUtils.isBlank(path)) {
return false;
}
for(String regex : whiteList) {
if(!StringUtils.isBlank(regex)) {
if(path.matches(regex)) {
return true;
}
}
}
return false;
}
/**
* OSGi Component Methods *
*/
@Activate
protected void activate(final ComponentContext componentContext) throws Exception {
configure(componentContext);
}
@Deactivate
protected void deactivate(ComponentContext ctx) {
}
private void configure(final ComponentContext componentContext) {
final Map<String, String> properties = (Map<String, String>) componentContext.getProperties();
this.resourceType = PropertiesUtil.toString(properties.get(PROP_RESOURCE_TYPE), DEFAULT_RESOURCE_TYPE);
this.maxWidth = PropertiesUtil.toInteger(properties.get(PROP_MAX_WIDTH), DEFAULT_MAX_WIDTH);
this.maxHeight = PropertiesUtil.toInteger(properties.get(PROP_MAX_HEIGHT), DEFAULT_MAX_HEIGHT);
this.extensionWhiteList = PropertiesUtil.toStringArray(properties.get(PROP_EXTENSION_WHITELIST), DEFAULT_EXTENSION_WHITELIST);
this.pathWhiteList = PropertiesUtil.toStringArray(properties.get(PROP_PATH_WHITELIST), DEFAULT_PATH_WHITELIST);
this.widthWhiteList = PropertiesUtil.toStringArray(properties.get(PROP_WIDTH_WHITELIST), DEFAULT_WIDTH_WHITELIST);
this.heightWhiteList = PropertiesUtil.toStringArray(properties.get(PROP_HEIGHT_WHITELIST), DEFAULT_HEIGHT_WHITELIST);
this.rotateWhiteList = PropertiesUtil.toStringArray(properties.get(PROP_ROTATE_WHITELIST), DEFAULT_ROTATE_WHITELIST);
}
}