diff --git a/README.md b/README.md index 5b1a098..e87fbbe 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Pass in non-Busboy options directly to the middleware. These are express-fileupl Option | Acceptable Values | Details --- | --- | --- safeFileNames | | Strips characters from the upload's filename. You can use custom regex to determine what to strip. If set to `true`, non-alphanumeric characters _except_ dashes and underscores will be stripped. This option is off by default.

**Example #1 (strip slashes from file names):** `app.use(fileUpload({ safeFileNames: /\\/g }))`
**Example #2:** `app.use(fileUpload({ safeFileNames: true }))` +preserveExtension | | Preserves filename extension when using safeFileNames option. If set to true, will default to an extension length of 3. If set to *Number*, this will be the max allowable extension length. If an extension is smaller than the extension length, it remains untouched. If the extension is longer, it is shifted.

**Example #1 (true):**
app.use(fileUpload({ safeFileNames: true, preserveExtension: true }));
*myFileName.ext* --> *myFileName.ext*

**Example #2 (max extension length 2, extension shifted):**
app.use(fileUpload({ safeFileNames: true, preserveExtension: 2 }));
*myFileName.ext* --> *myFileNamee.xt* # Help Wanted Pull Requests are welcomed! diff --git a/lib/index.js b/lib/index.js index c34d443..851a8f4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,7 +15,7 @@ module.exports = function(options) { return function(req, res, next) { if (!hasBody(req) || !hasAcceptableMethod(req) || !hasAcceptableMime(req)) - return next(); + return next(); processMultipart(options, req, res, next); }; @@ -85,14 +85,40 @@ function processMultipart(options, req, res, next) { // see: https://github.com/richardgirges/express-fileupload/issues/14 // firefox uploads empty file in case of cache miss when f5ing page. // resulting in unexpected behavior. if there is no file data, the file is invalid. - if(!buf.length) + if (!buf.length) return; if (options.safeFileNames) { + let maxExtensionLength = 3; + let extension = ''; + if (typeof options.safeFileNames === 'object') safeFileNameRegex = options.safeFileNames; - filename = filename.replace(safeFileNameRegex, ''); + maxExtensionLength = parseInt(options.preserveExtension); + if (options.preserveExtension || maxExtensionLength === 0) { + if (isNaN(maxExtensionLength)) + maxExtensionLength = 3; + else + maxExtensionLength = Math.abs(maxExtensionLength); + + let filenameParts = filename.split('.'); + let filenamePartsLen = filenameParts.length; + if (filenamePartsLen > 1) { + extension = filenameParts.pop(); + + if (extension.length > maxExtensionLength && maxExtensionLength > 0) { + filenameParts[filenameParts.length - 1] += + '.' + extension.substr(0, extension.length - maxExtensionLength); + extension = extension.substr(-maxExtensionLength); + } + + extension = maxExtensionLength ? '.' + extension.replace(safeFileNameRegex, '') : ''; + filename = filenameParts.join('.'); + } + } + + filename = filename.replace(safeFileNameRegex, '').concat(extension); } let newFile = { @@ -123,9 +149,9 @@ function processMultipart(options, req, res, next) { } else { // Array fields if (req.files[fieldname] instanceof Array) - req.files[fieldname].push(newFile); + req.files[fieldname].push(newFile); else - req.files[fieldname] = [req.files[fieldname], newFile]; + req.files[fieldname] = [req.files[fieldname], newFile]; } }); }); diff --git a/package.json b/package.json index cb8731a..a620ca3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-fileupload", - "version": "0.1.2", + "version": "0.1.3", "author": "Richard Girges ", "description": "Simple express file upload middleware that wraps around Busboy", "main": "./lib/index", diff --git a/test/files/basket.ball.bp b/test/files/basket.ball.bp new file mode 100644 index 0000000..a2a1571 Binary files /dev/null and b/test/files/basket.ball.bp differ diff --git a/test/files/my$Invalid#fileName.png123 b/test/files/my$Invalid#fileName.png123 new file mode 100644 index 0000000..510b859 Binary files /dev/null and b/test/files/my$Invalid#fileName.png123 differ diff --git a/test/multipartFields.spec.js b/test/multipartFields.spec.js index 034164e..bbddfe6 100644 --- a/test/multipartFields.spec.js +++ b/test/multipartFields.spec.js @@ -2,7 +2,7 @@ const request = require('supertest'); const server = require('./server'); -const app = server.app; +const app = server.setup(); let mockUser = { firstName: 'Joe', diff --git a/test/multipartUploads.spec.js b/test/multipartUploads.spec.js index 0dc1e1c..f4be574 100644 --- a/test/multipartUploads.spec.js +++ b/test/multipartUploads.spec.js @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const request = require('supertest'); const server = require('./server'); -const app = server.app; +const app = server.setup(); const clearUploadsDir = server.clearUploadsDir; const fileDir = server.fileDir; const uploadDir = server.uploadDir; diff --git a/test/options.spec.js b/test/options.spec.js new file mode 100644 index 0000000..b1af76c --- /dev/null +++ b/test/options.spec.js @@ -0,0 +1,187 @@ +const fs = require('fs'); +const path = require('path'); +const request = require('supertest'); +const server = require('./server'); +const clearUploadsDir = server.clearUploadsDir; +const fileDir = server.fileDir; +const uploadDir = server.uploadDir; + +describe('File Upload Options Tests', function() { + afterEach(function(done) { + clearUploadsDir(); + done(); + }); + + /** + * Upload the file for testing and verify the expected filename. + * @param {object} options The expressFileUpload options. + * @param {string} actualFileNameToUpload The name of the file to upload. + * @param {string} expectedFileNameOnFileSystem The name of the file after upload. + * @param {function} done The mocha continuation function. + */ + function executeFileUploadTestWalk(options, + actualFileNameToUpload, + expectedFileNameOnFileSystem, + done) { + request(server.setup(options)) + .post('/upload/single') + .attach('testFile', path.join(fileDir, actualFileNameToUpload)) + .expect(200) + .end(function(err) { + if (err) + return done(err); + + const uploadedFilePath = path.join(uploadDir, expectedFileNameOnFileSystem); + + fs.stat(uploadedFilePath, done); + }); + } + + describe('Testing [safeFileNames] option to ensure:', function() { + it('Does nothing to your filename when disabled.', + function(done) { + const fileUploadOptions = {safeFileNames: false}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'my$Invalid#fileName.png123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Is disabled by default.', + function(done) { + const fileUploadOptions = null; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'my$Invalid#fileName.png123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.', + function(done) { + const fileUploadOptions = {safeFileNames: true}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepng123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Accepts a regex for stripping (decidedly) "invalid" characters from filename.', + function(done) { + const fileUploadOptions = {safeFileNames: /[\$#]/g}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileName.png123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + }); + + describe('Testing [preserveExtension] option to ensure:', function() { + it('Does not preserve the extension of your filename when disabled.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: false}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepng123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Is disabled by default.', + function(done) { + const fileUploadOptions = {safeFileNames: true}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepng123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Shortens your extension to the default(3) when enabled, if the extension found is larger.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: true}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepng.123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Leaves your extension alone when enabled, if the extension found is <= default(3) length', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: true}; + const actualFileName = 'car.png'; + const expectedFileName = 'car.png'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Can be configured for an extension length > default(3).', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: 7}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileName.png123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Can be configured for an extension length < default(3).', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: 2}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepng1.23'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Will use the absolute value of your extension length when negative.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: -5}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamep.ng123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Will leave no extension when the extension length == 0.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: 0}; + const actualFileName = 'car.png'; + const expectedFileName = 'car'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Will accept numbers as strings, if they can be resolved with parseInt.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: '3'}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepng.123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Will be evaluated for truthy-ness if it cannot be parsed as an int.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: 'not-a-#-but-truthy'}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepng.123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Will ignore any decimal amount when evaluating for extension length.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: 4.98}; + const actualFileName = 'my$Invalid#fileName.png123'; + const expectedFileName = 'myInvalidfileNamepn.g123'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + + it('Only considers the last dotted part as the extension.', + function(done) { + const fileUploadOptions = {safeFileNames: true, preserveExtension: true}; + const actualFileName = 'basket.ball.bp'; + const expectedFileName = 'basketball.bp'; + + executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done); + }); + }); +}); diff --git a/test/server.js b/test/server.js index 41a80b7..3f4bdeb 100644 --- a/test/server.js +++ b/test/server.js @@ -1,14 +1,8 @@ 'use strict'; - -const fs = require('fs-extra'); const path = require('path'); -const express = require('express'); -const expressFileupload = require('../lib/index'); - const fileDir = path.join(__dirname, 'files'); const uploadDir = path.join(__dirname, 'uploads'); - -const app = express(); +const fs = require('fs-extra'); const clearUploadsDir = function() { if (!fs.existsSync(uploadDir)) { @@ -18,157 +12,166 @@ const clearUploadsDir = function() { } }; -app.use(expressFileupload()); +const setup = function(fileUploadOptions) { + const express = require('express'); + const expressFileupload = require('../lib/index'); -app.all('/upload/single', function(req, res) { - if (!req.files) - return res.status(400).send('No files were uploaded.'); + const app = express(); - let testFile = req.files.testFile; - let uploadPath = path.join(uploadDir, testFile.name); + fileUploadOptions = fileUploadOptions || {}; + app.use(expressFileupload(fileUploadOptions)); - testFile.mv(uploadPath, function(err) { - if (err) - return res.status(500).send(err); + app.all('/upload/single', function(req, res) { + if (!req.files) + return res.status(400).send('No files were uploaded.'); + + let testFile = req.files.testFile; + let uploadPath = path.join(uploadDir, testFile.name); + + testFile.mv(uploadPath, function(err) { + if (err) + return res.status(500).send(err); - res.send('File uploaded to ' + uploadPath); + res.send('File uploaded to ' + uploadPath); + }); }); -}); -app.all('/upload/single/withfields', function(req, res) { - if (!req.files) - return res.status(400).send('No files were uploaded.'); + app.all('/upload/single/withfields', function(req, res) { + if (!req.files) + return res.status(400).send('No files were uploaded.'); - if (!req.body) - return res.status(400).send('No request body found'); + if (!req.body) + return res.status(400).send('No request body found'); - if (!req.body.firstName || !req.body.firstName.trim()) - return res.status(400).send('Invalid first name'); + if (!req.body.firstName || !req.body.firstName.trim()) + return res.status(400).send('Invalid first name'); - if (!req.body.lastName || !req.body.lastName.trim()) - return res.status(400).send('Invalid last name'); + if (!req.body.lastName || !req.body.lastName.trim()) + return res.status(400).send('Invalid last name'); - if (!req.body.email || !req.body.email.trim()) - return res.status(400).send('Invalid email'); + if (!req.body.email || !req.body.email.trim()) + return res.status(400).send('Invalid email'); - let testFile = req.files.testFile; - let uploadPath = path.join(uploadDir, testFile.name); + let testFile = req.files.testFile; + let uploadPath = path.join(uploadDir, testFile.name); - testFile.mv(uploadPath, function(err) { - if (err) - return res.status(500).send(err); + testFile.mv(uploadPath, function(err) { + if (err) + return res.status(500).send(err); - res.json({ - firstName: req.body.firstName, - lastName: req.body.lastName, - email: req.body.email + res.json({ + firstName: req.body.firstName, + lastName: req.body.lastName, + email: req.body.email + }); }); }); -}); -app.all('/upload/multiple', function(req, res) { - if (!req.files) - return res.status(400).send('No files were uploaded.'); + app.all('/upload/multiple', function(req, res) { + if (!req.files) + return res.status(400).send('No files were uploaded.'); - let testFile1 = req.files.testFile1; - let testFile2 = req.files.testFile2; - let testFile3 = req.files.testFile3; - let uploadPath1 = path.join(uploadDir, testFile1.name); - let uploadPath2 = path.join(uploadDir, testFile2.name); - let uploadPath3 = path.join(uploadDir, testFile3.name); + let testFile1 = req.files.testFile1; + let testFile2 = req.files.testFile2; + let testFile3 = req.files.testFile3; + let uploadPath1 = path.join(uploadDir, testFile1.name); + let uploadPath2 = path.join(uploadDir, testFile2.name); + let uploadPath3 = path.join(uploadDir, testFile3.name); - if (!testFile1) - return res.status(400).send('testFile1 was not uploaded'); + if (!testFile1) + return res.status(400).send('testFile1 was not uploaded'); - if (!testFile2) - return res.status(400).send('testFile2 was not uploaded'); + if (!testFile2) + return res.status(400).send('testFile2 was not uploaded'); - if (!testFile3) - return res.status(400).send('testFile3 was not uploaded'); + if (!testFile3) + return res.status(400).send('testFile3 was not uploaded'); - testFile1.mv(uploadPath1, function(err) { - if (err) - return res.status(500).send(err); - - testFile2.mv(uploadPath2, function(err) { + testFile1.mv(uploadPath1, function(err) { if (err) return res.status(500).send(err); - testFile3.mv(uploadPath3, function(err) { + testFile2.mv(uploadPath2, function(err) { if (err) return res.status(500).send(err); - res.send('Files uploaded to ' + uploadDir); + testFile3.mv(uploadPath3, function(err) { + if (err) + return res.status(500).send(err); + + res.send('Files uploaded to ' + uploadDir); + }); }); }); }); -}); -app.all('/upload/array', function(req, res) { - if (!req.files) - return res.status(400).send('No files were uploaded.'); + app.all('/upload/array', function(req, res) { + if (!req.files) + return res.status(400).send('No files were uploaded.'); - let testFiles = req.files.testFiles; + let testFiles = req.files.testFiles; - if (!testFiles) - return res.status(400).send('No files were uploaded'); + if (!testFiles) + return res.status(400).send('No files were uploaded'); - if (!Array.isArray(testFiles)) - return res.status(400).send('Files were not uploaded as an array'); + if (!Array.isArray(testFiles)) + return res.status(400).send('Files were not uploaded as an array'); - if (!testFiles.length) - return res.status(400).send('Files array is empty'); + if (!testFiles.length) + return res.status(400).send('Files array is empty'); - let filesUploaded = 0; - for (let i = 0; i < testFiles.length; i++) { - let uploadPath = path.join(uploadDir, testFiles[i].name); + let filesUploaded = 0; + for (let i = 0; i < testFiles.length; i++) { + let uploadPath = path.join(uploadDir, testFiles[i].name); - testFiles[i].mv(uploadPath, function(err) { - if (err) - return res.status(500).send(err); + testFiles[i].mv(uploadPath, function(err) { + if (err) + return res.status(500).send(err); - if (++filesUploaded === testFiles.length) - res.send('File uploaded to ' + uploadPath); - }); - } -}); + if (++filesUploaded === testFiles.length) + res.send('File uploaded to ' + uploadPath); + }); + } + }); -app.all('/fields/user', function(req, res) { - if (!req.body) - return res.status(400).send('No request body found'); + app.all('/fields/user', function(req, res) { + if (!req.body) + return res.status(400).send('No request body found'); - if (!req.body.firstName || !req.body.firstName.trim()) - return res.status(400).send('Invalid first name'); + if (!req.body.firstName || !req.body.firstName.trim()) + return res.status(400).send('Invalid first name'); - if (!req.body.lastName || !req.body.lastName.trim()) - return res.status(400).send('Invalid last name'); + if (!req.body.lastName || !req.body.lastName.trim()) + return res.status(400).send('Invalid last name'); - if (!req.body.email || !req.body.email.trim()) - return res.status(400).send('Invalid email'); + if (!req.body.email || !req.body.email.trim()) + return res.status(400).send('Invalid email'); - res.json({ - firstName: req.body.firstName, - lastName: req.body.lastName, - email: req.body.email + res.json({ + firstName: req.body.firstName, + lastName: req.body.lastName, + email: req.body.email + }); }); -}); -app.all('/fields/array', function(req, res) { - if (!req.body) - return res.status(400).send('No request body found'); + app.all('/fields/array', function(req, res) { + if (!req.body) + return res.status(400).send('No request body found'); - if (!req.body.testField) - return res.status(400).send('Invalid field'); + if (!req.body.testField) + return res.status(400).send('Invalid field'); - if (!Array.isArray(req.body.testField)) - return res.status(400).send('Field is not an array'); + if (!Array.isArray(req.body.testField)) + return res.status(400).send('Field is not an array'); - res.json(req.body.testField); -}); + res.json(req.body.testField); + }); + return app; +}; module.exports = { - app, + setup, fileDir, uploadDir, clearUploadsDir