diff --git a/lib/getFilterInfosAndTargetContentTypeFromQueryString.js b/lib/getFilterInfosAndTargetContentTypeFromQueryString.js index 3822847..c512fdf 100644 --- a/lib/getFilterInfosAndTargetContentTypeFromQueryString.js +++ b/lib/getFilterInfosAndTargetContentTypeFromQueryString.js @@ -67,6 +67,299 @@ Object.keys(isOperationByEngineNameAndName).forEach(function (engineName) { }); }); +function isNumberWithin(num, min, max) { + return typeof num === 'number' && num >= min && num <= max; +} + +function isValidOperation(name, args) { + switch (name) { + case 'crop': + return args.length === 1 && /^(?:east|west|center|north(?:|west|east)|south(?:|west|east))/.test(args[0]); + case 'rotate': + return args.length === 0 || (args.length === 1 && (args[0] === 0 || args[0] === 90 || args[0] === 180 || args[0] === 270)); + case 'resize': + return args.length === 2 && isNumberWithin(args[0], 1, 16384) && isNumberWithin(args[1], 1, 16384); + case 'extract': + return args.length === 4 && isNumberWithin(args[0], 1, 16384) && isNumberWithin(args[1], 1, 16384) && isNumberWithin(args[2], 1, 16384) && isNumberWithin(args[3], 1, 16384); + case 'interpolateWith': + return args.length === 1 && /^(?:nearest|bilinear|vertexSplitQuadraticBasisSpline|bicubic|locallyBoundedBicubic|nohalo)$/.test(args[0]); + case 'background': + return args.length === 1 && /^#[0-9a-f]{6}$/.test(args[0]); + case 'blur': + return args.length === 0 || (args.length === 1 && isNumberWithin(args[0], 0.3, 1000)); + case 'sharpen': + return args.length <= 3 && + (typeof args[0] === 'undefined' || typeof args[0] === 'number') && + (typeof args[1] === 'undefined' || typeof args[1] === 'number') && + (typeof args[2] === 'undefined' || typeof args[2] === 'number'); + case 'threshold': + return args.length === 0 || (args.length === 1 && isNumberWithin(args[0], 0, 255)); + case 'gamma': + return args.length === 0 || (args.length === 1 && isNumberWithin(args[0], 1, 3)); + case 'quality': + return args.length === 1 && isNumberWithin(args[0], 1, 100); + case 'tile': + return args.length <= 2 && + (typeof args[0] === 'undefined' || isNumberWithin(args[0], 1, 8192)) && + (typeof args[1] === 'undefined' || isNumberWithin(args[0], 0, 8192)); + case 'compressionLevel': + return args.length === 1 && isNumberWithin(args[0], 0, 9); + case 'png': + case 'jpeg': + case 'gif': + case 'webp': + case 'withoutEnlargement': + case 'progressive': + case 'metadata': + case 'ignoreAspectRatio': + case 'embed': + case 'max': + case 'min': + case 'negate': + case 'flatten': + case 'flip': + case 'flop': + case 'grayscale': + case 'greyscale': + case 'normalize': + case 'withMetadata': + case 'withoutChromaSubsampling': + case 'withoutAdaptiveFiltering': + case 'trellisQuantization': + case 'trellisQuantisation': + case 'overshootDeringing': + case 'optimizeScans': + case 'optimiseScans': + return args.length === 0; + // Not supported: overlayWith + + // Engines: + case 'sharp': + case 'gm': + return args.length === 0; + + // FIXME: Add validation code for all the below. + // https://github.com/papandreou/express-processimage/issues/4 + // Other engines: + case 'pngcrush': + case 'pngquant': + case 'jpegtran': + case 'optipng': + case 'svgfilter': + case 'inkscape': + return true; + + // Graphicsmagick specific operations: + // FIXME: Add validation code for all the below. + // https://github.com/papandreou/express-processimage/issues/4 + case 'setFormat': + case 'identify': + case 'selectFrame': + case 'subCommand': + case 'adjoin': + case 'affine': + case 'alpha': + case 'append': + case 'authenticate': + case 'average': + case 'backdrop': + case 'blackThreshold': + case 'bluePrimary': + case 'border': + case 'borderColor': + case 'box': + case 'channel': + case 'chop': + case 'clip': + case 'coalesce': + case 'colorize': + case 'colorMap': + case 'compose': + case 'compress': + case 'convolve': + case 'createDirectories': + case 'deconstruct': + case 'define': + case 'delay': + case 'displace': + case 'display': + case 'dispose': + case 'dissolve': + case 'encoding': + case 'endian': + case 'file': + case 'flatten': + case 'foreground': + case 'frame': + case 'fuzz': + case 'gaussian': + case 'geometry': + case 'greenPrimary': + case 'highlightColor': + case 'highlightStyle': + case 'iconGeometry': + case 'intent': + case 'lat': + case 'level': + case 'list': + case 'log': + case 'loop': + case 'map': + case 'mask': + case 'matte': + case 'matteColor': + case 'maximumError': + case 'mode': + case 'monitor': + case 'mosaic': + case 'motionBlur': + case 'name': + case 'noop': + case 'normalize': + case 'opaque': + case 'operator': + case 'orderedDither': + case 'outputDirectory': + case 'page': + case 'pause': + case 'pen': + case 'ping': + case 'pointSize': + case 'preview': + case 'process': + case 'profile': + case 'progress': + case 'randomThreshold': + case 'recolor': + case 'redPrimary': + case 'remote': + case 'render': + case 'repage': + case 'sample': + case 'samplingFactor': + case 'scene': + case 'scenes': + case 'screen': + case 'set': + case 'segment': + case 'shade': + case 'shadow': + case 'sharedMemory': + case 'shave': + case 'shear': + case 'silent': + case 'rawSize': + case 'snaps': + case 'stegano': + case 'stereo': + case 'textFont': + case 'texture': + case 'threshold': + case 'thumbnail': + case 'tile': + case 'title': + case 'transform': + case 'transparent': + case 'treeDepth': + case 'update': + case 'units': + case 'unsharp': + case 'usePixmap': + case 'view': + case 'virtualPixel': + case 'visual': + case 'watermark': + case 'wave': + case 'whitePoint': + case 'whiteThreshold': + case 'window': + case 'windowGroup': + case 'strip': + case 'interlace': + case 'setFormat': + case 'resizeExact': + case 'scale': + case 'filter': + case 'density': + case 'noProfile': + case 'resample': + case 'rotate': + case 'magnify': + case 'minify': + case 'quality': + case 'charcoal': + case 'modulate': + case 'antialias': + case 'bitdepth': + case 'colors': + case 'colorspace': + case 'comment': + case 'contrast': + case 'cycle': + case 'despeckle': + case 'dither': + case 'monochrome': + case 'edge': + case 'emboss': + case 'enhance': + case 'equalize': + case 'gamma': + case 'implode': + case 'label': + case 'limit': + case 'median': + case 'negative': + case 'noise': + case 'paint': + case 'raise': + case 'lower': + case 'region': + case 'roll': + case 'sharpen': + case 'solarize': + case 'spread': + case 'swirl': + case 'type': + case 'trim': + case 'extent': + case 'gravity': + case 'background': + case 'fill': + case 'stroke': + case 'strokeWidth': + case 'font': + case 'fontSize': + case 'draw': + case 'drawPoint': + case 'drawLine': + case 'drawRectangle': + case 'drawArc': + case 'drawEllipse': + case 'drawCircle': + case 'drawPolyline': + case 'drawPolygon': + case 'drawBezier': + case 'drawText': + case 'setDraw': + case 'thumb': + case 'thumbExact': + case 'morph': + case 'sepia': + case 'autoOrient': + case 'in': + case 'out': + case 'preprocessor': + case 'addSrcFormatter': + case 'inputIs': + case 'compare': + case 'composite': + case 'montage': + return true; + default: + return false; + } +} + module.exports = function getFilterInfosAndTargetContentTypeFromQueryString(queryString, options) { options = options || {}; var filters = options.filters || {}, @@ -299,7 +592,7 @@ module.exports = function getFilterInfosAndTargetContentTypeFromQueryString(quer } }) : []; - if (typeof options.allowOperation === 'function' && !options.allowOperation(operationName, operationArgs)) { + if (!isValidOperation(operationName, operationArgs) || (typeof options.allowOperation === 'function' && !options.allowOperation(operationName, operationArgs))) { leftOverQueryStringFragments.push(keyValuePair); } else { var filterInfo; diff --git a/test/processImage.js b/test/processImage.js index 9022089..b7f4ba9 100644 --- a/test/processImage.js +++ b/test/processImage.js @@ -311,14 +311,14 @@ describe('express-processimage', function () { }); it('should allow an operation for which allowOperation returns true', function () { - return expect('GET /turtle.jpg?resize=87', 'to yield response', { + return expect('GET /turtle.jpg?resize=87,100', 'to yield response', { headers: { 'Content-Type': 'image/jpeg' }, body: expect.it('to have metadata satisfying', { size: { width: 87 } }) }).then(function () { expect(config.allowOperation, 'to have calls satisfying', function () { - config.allowOperation('resize', [87]); + config.allowOperation('resize', [ 87, 100 ]); }); }); }); @@ -357,7 +357,7 @@ describe('express-processimage', function () { }); it('should allow retrieving the image metadata for the result of an operation', function () { - return expect('GET /turtle.jpg?png&greyscale&resize=10&metadata', 'to yield response', { + return expect('GET /turtle.jpg?png&greyscale&resize=10,9&metadata', 'to yield response', { body: { width: 10, height: 9, @@ -459,7 +459,7 @@ describe('express-processimage', function () { it('should use sharp when a gif is converted to png', function () { config.debug = true; - return expect('GET /animated.gif?resize=40&png', 'to yield response', { + return expect('GET /animated.gif?resize=40,100&png', 'to yield response', { headers: { 'X-Express-Processimage': 'sharp' }, @@ -485,9 +485,14 @@ describe('express-processimage', function () { }); }); - it('should return an error when an invalid syntax is used for a parameter value', function () { - return expect('GET /turtle.jpg?resize=100%22', 'to yield response', { - errorPassedToNext: { statusCode: 400 } + it('should ignore invalid operations', function () { + return expect('GET /turtle.jpg?resize=10%22', 'to yield response', { + body: expect.it('to have metadata satisfying', { + size: { + width: 481, + height: 424 + } + }) }); }); }); @@ -663,4 +668,23 @@ describe('express-processimage', function () { }); }); }); + + describe('with invalid parameters', function () { + [ + 'resize=foo,100', 'resize=', 'resize=100,200,300', 'resize=0,0', 'resize=-1,-1', 'resize=32000,32000', + 'crop=foo', 'crop=', 'crop=north,south', + 'extract=', 'extract=1,2,3,4,5', 'extract=32000,32000,32000,32000', + 'rotate=95', 'rotate=90,270', + 'png=hey', + 'interpolateWith=something' + ].forEach(function (invalidOperation) { + it('disallows an operation of ' + invalidOperation, function () { + return expect('GET /testImage.png?' + invalidOperation, 'to yield response', { + body: expect.it('to have metadata satisfying', { + size: { width: 12, height: 5 } + }) + }); + }); + }); + }); }); diff --git a/testdata/testImage.png b/testdata/testImage.png new file mode 100644 index 0000000..7586ff3 Binary files /dev/null and b/testdata/testImage.png differ