Skip to content

Commit 8f599b4

Browse files
Merge pull request #27 from pronein/feature/allowExtensionPreservationWithSafeFileNames
Feature - Allow extension preservation with safe file names
2 parents 63c759a + bace2a1 commit 8f599b4

9 files changed

+332
-115
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ Pass in non-Busboy options directly to the middleware. These are express-fileupl
121121
Option | Acceptable Values | Details
122122
--- | --- | ---
123123
safeFileNames | <ul><li><code>false</code>&nbsp;**(default)**</li><li><code>true</code></li><li>regex</li></ul> | 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.<br /><br />**Example #1 (strip slashes from file names):** `app.use(fileUpload({ safeFileNames: /\\/g }))`<br />**Example #2:** `app.use(fileUpload({ safeFileNames: true }))`
124+
preserveExtension | <ul><li><code>false</code>&nbsp;**(default)**</li><li><code>true</code></li><li><code>*Number*</code></li></ul> | Preserves filename extension when using <code>safeFileNames</code> option. If set to <code>true</code>, will default to an extension length of 3. If set to <code>*Number*</code>, 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.<br /><br />**Example #1 (true):**<br /><code>app.use(fileUpload({ safeFileNames: true, preserveExtension: true }));</code><br />*myFileName.ext* --> *myFileName.ext*<br /><br />**Example #2 (max extension length 2, extension shifted):**<br /><code>app.use(fileUpload({ safeFileNames: true, preserveExtension: 2 }));</code><br />*myFileName.ext* --> *myFileNamee.xt*
124125

125126
# Help Wanted
126127
Pull Requests are welcomed!

lib/index.js

+31-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = function(options) {
1515

1616
return function(req, res, next) {
1717
if (!hasBody(req) || !hasAcceptableMethod(req) || !hasAcceptableMime(req))
18-
return next();
18+
return next();
1919

2020
processMultipart(options, req, res, next);
2121
};
@@ -85,14 +85,40 @@ function processMultipart(options, req, res, next) {
8585
// see: https://github.com/richardgirges/express-fileupload/issues/14
8686
// firefox uploads empty file in case of cache miss when f5ing page.
8787
// resulting in unexpected behavior. if there is no file data, the file is invalid.
88-
if(!buf.length)
88+
if (!buf.length)
8989
return;
9090

9191
if (options.safeFileNames) {
92+
let maxExtensionLength = 3;
93+
let extension = '';
94+
9295
if (typeof options.safeFileNames === 'object')
9396
safeFileNameRegex = options.safeFileNames;
9497

95-
filename = filename.replace(safeFileNameRegex, '');
98+
maxExtensionLength = parseInt(options.preserveExtension);
99+
if (options.preserveExtension || maxExtensionLength === 0) {
100+
if (isNaN(maxExtensionLength))
101+
maxExtensionLength = 3;
102+
else
103+
maxExtensionLength = Math.abs(maxExtensionLength);
104+
105+
let filenameParts = filename.split('.');
106+
let filenamePartsLen = filenameParts.length;
107+
if (filenamePartsLen > 1) {
108+
extension = filenameParts.pop();
109+
110+
if (extension.length > maxExtensionLength && maxExtensionLength > 0) {
111+
filenameParts[filenameParts.length - 1] +=
112+
'.' + extension.substr(0, extension.length - maxExtensionLength);
113+
extension = extension.substr(-maxExtensionLength);
114+
}
115+
116+
extension = maxExtensionLength ? '.' + extension.replace(safeFileNameRegex, '') : '';
117+
filename = filenameParts.join('.');
118+
}
119+
}
120+
121+
filename = filename.replace(safeFileNameRegex, '').concat(extension);
96122
}
97123

98124
let newFile = {
@@ -123,9 +149,9 @@ function processMultipart(options, req, res, next) {
123149
} else {
124150
// Array fields
125151
if (req.files[fieldname] instanceof Array)
126-
req.files[fieldname].push(newFile);
152+
req.files[fieldname].push(newFile);
127153
else
128-
req.files[fieldname] = [req.files[fieldname], newFile];
154+
req.files[fieldname] = [req.files[fieldname], newFile];
129155
}
130156
});
131157
});

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "express-fileupload",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"author": "Richard Girges <[email protected]>",
55
"description": "Simple express file upload middleware that wraps around Busboy",
66
"main": "./lib/index",

test/files/basket.ball.bp

151 KB
Binary file not shown.

test/files/my$Invalid#fileName.png123

263 KB
Binary file not shown.

test/multipartFields.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const request = require('supertest');
44
const server = require('./server');
5-
const app = server.app;
5+
const app = server.setup();
66

77
let mockUser = {
88
firstName: 'Joe',

test/multipartUploads.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const fs = require('fs');
44
const path = require('path');
55
const request = require('supertest');
66
const server = require('./server');
7-
const app = server.app;
7+
const app = server.setup();
88
const clearUploadsDir = server.clearUploadsDir;
99
const fileDir = server.fileDir;
1010
const uploadDir = server.uploadDir;

test/options.spec.js

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const request = require('supertest');
4+
const server = require('./server');
5+
const clearUploadsDir = server.clearUploadsDir;
6+
const fileDir = server.fileDir;
7+
const uploadDir = server.uploadDir;
8+
9+
describe('File Upload Options Tests', function() {
10+
afterEach(function(done) {
11+
clearUploadsDir();
12+
done();
13+
});
14+
15+
/**
16+
* Upload the file for testing and verify the expected filename.
17+
* @param {object} options The expressFileUpload options.
18+
* @param {string} actualFileNameToUpload The name of the file to upload.
19+
* @param {string} expectedFileNameOnFileSystem The name of the file after upload.
20+
* @param {function} done The mocha continuation function.
21+
*/
22+
function executeFileUploadTestWalk(options,
23+
actualFileNameToUpload,
24+
expectedFileNameOnFileSystem,
25+
done) {
26+
request(server.setup(options))
27+
.post('/upload/single')
28+
.attach('testFile', path.join(fileDir, actualFileNameToUpload))
29+
.expect(200)
30+
.end(function(err) {
31+
if (err)
32+
return done(err);
33+
34+
const uploadedFilePath = path.join(uploadDir, expectedFileNameOnFileSystem);
35+
36+
fs.stat(uploadedFilePath, done);
37+
});
38+
}
39+
40+
describe('Testing [safeFileNames] option to ensure:', function() {
41+
it('Does nothing to your filename when disabled.',
42+
function(done) {
43+
const fileUploadOptions = {safeFileNames: false};
44+
const actualFileName = 'my$Invalid#fileName.png123';
45+
const expectedFileName = 'my$Invalid#fileName.png123';
46+
47+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
48+
});
49+
50+
it('Is disabled by default.',
51+
function(done) {
52+
const fileUploadOptions = null;
53+
const actualFileName = 'my$Invalid#fileName.png123';
54+
const expectedFileName = 'my$Invalid#fileName.png123';
55+
56+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
57+
});
58+
59+
it('Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.',
60+
function(done) {
61+
const fileUploadOptions = {safeFileNames: true};
62+
const actualFileName = 'my$Invalid#fileName.png123';
63+
const expectedFileName = 'myInvalidfileNamepng123';
64+
65+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
66+
});
67+
68+
it('Accepts a regex for stripping (decidedly) "invalid" characters from filename.',
69+
function(done) {
70+
const fileUploadOptions = {safeFileNames: /[\$#]/g};
71+
const actualFileName = 'my$Invalid#fileName.png123';
72+
const expectedFileName = 'myInvalidfileName.png123';
73+
74+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
75+
});
76+
});
77+
78+
describe('Testing [preserveExtension] option to ensure:', function() {
79+
it('Does not preserve the extension of your filename when disabled.',
80+
function(done) {
81+
const fileUploadOptions = {safeFileNames: true, preserveExtension: false};
82+
const actualFileName = 'my$Invalid#fileName.png123';
83+
const expectedFileName = 'myInvalidfileNamepng123';
84+
85+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
86+
});
87+
88+
it('Is disabled by default.',
89+
function(done) {
90+
const fileUploadOptions = {safeFileNames: true};
91+
const actualFileName = 'my$Invalid#fileName.png123';
92+
const expectedFileName = 'myInvalidfileNamepng123';
93+
94+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
95+
});
96+
97+
it('Shortens your extension to the default(3) when enabled, if the extension found is larger.',
98+
function(done) {
99+
const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
100+
const actualFileName = 'my$Invalid#fileName.png123';
101+
const expectedFileName = 'myInvalidfileNamepng.123';
102+
103+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
104+
});
105+
106+
it('Leaves your extension alone when enabled, if the extension found is <= default(3) length',
107+
function(done) {
108+
const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
109+
const actualFileName = 'car.png';
110+
const expectedFileName = 'car.png';
111+
112+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
113+
});
114+
115+
it('Can be configured for an extension length > default(3).',
116+
function(done) {
117+
const fileUploadOptions = {safeFileNames: true, preserveExtension: 7};
118+
const actualFileName = 'my$Invalid#fileName.png123';
119+
const expectedFileName = 'myInvalidfileName.png123';
120+
121+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
122+
});
123+
124+
it('Can be configured for an extension length < default(3).',
125+
function(done) {
126+
const fileUploadOptions = {safeFileNames: true, preserveExtension: 2};
127+
const actualFileName = 'my$Invalid#fileName.png123';
128+
const expectedFileName = 'myInvalidfileNamepng1.23';
129+
130+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
131+
});
132+
133+
it('Will use the absolute value of your extension length when negative.',
134+
function(done) {
135+
const fileUploadOptions = {safeFileNames: true, preserveExtension: -5};
136+
const actualFileName = 'my$Invalid#fileName.png123';
137+
const expectedFileName = 'myInvalidfileNamep.ng123';
138+
139+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
140+
});
141+
142+
it('Will leave no extension when the extension length == 0.',
143+
function(done) {
144+
const fileUploadOptions = {safeFileNames: true, preserveExtension: 0};
145+
const actualFileName = 'car.png';
146+
const expectedFileName = 'car';
147+
148+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
149+
});
150+
151+
it('Will accept numbers as strings, if they can be resolved with parseInt.',
152+
function(done) {
153+
const fileUploadOptions = {safeFileNames: true, preserveExtension: '3'};
154+
const actualFileName = 'my$Invalid#fileName.png123';
155+
const expectedFileName = 'myInvalidfileNamepng.123';
156+
157+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
158+
});
159+
160+
it('Will be evaluated for truthy-ness if it cannot be parsed as an int.',
161+
function(done) {
162+
const fileUploadOptions = {safeFileNames: true, preserveExtension: 'not-a-#-but-truthy'};
163+
const actualFileName = 'my$Invalid#fileName.png123';
164+
const expectedFileName = 'myInvalidfileNamepng.123';
165+
166+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
167+
});
168+
169+
it('Will ignore any decimal amount when evaluating for extension length.',
170+
function(done) {
171+
const fileUploadOptions = {safeFileNames: true, preserveExtension: 4.98};
172+
const actualFileName = 'my$Invalid#fileName.png123';
173+
const expectedFileName = 'myInvalidfileNamepn.g123';
174+
175+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
176+
});
177+
178+
it('Only considers the last dotted part as the extension.',
179+
function(done) {
180+
const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
181+
const actualFileName = 'basket.ball.bp';
182+
const expectedFileName = 'basketball.bp';
183+
184+
executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
185+
});
186+
});
187+
});

0 commit comments

Comments
 (0)