forked from eloquence/lib.reviews
-
Notifications
You must be signed in to change notification settings - Fork 0
/
process-uploads.js
205 lines (174 loc) · 6.78 KB
/
process-uploads.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
'use strict';
// External dependencies
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const checkCSRF = require('csurf')();
const fileType = require('file-type');
const readChunk = require('read-chunk');
const fs = require('fs');
const isSVG = require('is-svg');
const config = require('config');
// Internal dependencies
const Thing = require('../models/thing');
const File = require('../models/file');
const getResourceErrorHandler = require('./handlers/resource-error-handler');
const render = require('./helpers/render');
const debug = require('../util/debug');
const ErrorMessage = require('../util/error');
const flashError = require('./helpers/flash-error');
const allowedTypes = ['image/png', 'image/gif', 'image/svg+xml', 'image/jpeg', 'video/webm', 'audio/ogg', 'video/ogg', 'audio/mpeg', 'image/webp'];
// Uploading is a two step process. In the first step, the user simply posts the
// file or files. In the second step, they provide information such as the
// license and description. This first step has to be handled separately
// because of the requirement of managing upload streams and multipart forms.
//
// Whether or not an upload is finished, as long as we have a valid file, we
// keep it on disk, initially in a temporary directory. We also create a
// record in the "files" table for it that can be completed later.
router.post('/thing/:id/upload', function(req, res, next) {
// req.body won't be populated if this is a multipart request, and we pass
// it along to the middleware for step 2, which can be found in things.js
if (typeof req.body == "object" && Object.keys(req.body).length)
return next();
let id = req.params.id.trim();
Thing.getNotStaleOrDeleted(id)
.then(thing => {
thing.populateUserInfo(req.user);
if (!thing.userCanUpload)
return render.permissionError(req, res, {
titleKey: 'add media'
});
let storage = multer.diskStorage({
destination: config.uploadTempDir,
filename(req, file, done) {
let p = path.parse(file.originalname);
let name = `${p.name}-${Date.now()}${p.ext}`;
name.replace(/<>&/g, '');
done(null, name);
}
});
let upload = multer({
limits: {
fileSize: 1024 * 1024 * 100 // 100 MB
},
fileFilter,
storage
}).array('media');
// Execute the actual upload middleware
upload(req, res, error => {
// An error at this stage most likely means an unsupported file type was among the batch.
// We reject the whole batch and report the bad apple.
if (error) {
cleanupFiles(req);
flashError(req, error);
return res.redirect(`/thing/${thing.id}`);
}
if (req.files.length) {
let validators = [];
req.files.forEach(file => {
// SVG files need full examination
if (file.mimetype != 'image/svg+xml')
validators.push(validateFile(file.path, file.mimetype));
else
validators.push(validateSVG(file.path));
});
// Validate all files
Promise
.all(validators)
.then(() => {
let fileRevPromises = [];
req.files.forEach(() => fileRevPromises.push(File.createFirstRevision(req.user)));
Promise
.all(fileRevPromises)
.then(fileRevs => {
req.files.forEach((file, index) => {
fileRevs[index].name = file.filename;
fileRevs[index].uploadedBy = req.user.id;
fileRevs[index].uploadedOn = new Date();
thing.addFile(fileRevs[index]);
});
thing
.saveAll() // saves joined files
.then(thing => render.template(req, res, 'thing-upload-step-2', {
titleKey: 'add media',
thing
}))
.catch(error => next(error)); // Problem saving file metadata
})
.catch(error => next(error)); // Problem starting file revisions
})
.catch(error => { // One of the files couldn't be validated
cleanupFiles(req);
flashError(req, error);
res.redirect(`/thing/${thing.id}`);
});
} else {
req.flash('pageErrors', req.__('no file received'));
res.redirect(`/thing/${thing.id}`);
}
});
// Note that at the time the filter runs, we won't have the complete file yet,
// so we may temporarily store files and delete them later if, after
// investigation, they turn out to contain unacceptable content.
function fileFilter(req, file, done) {
checkCSRF(req, res, error => {
if (error)
return done(error); // Bad CSRF token, reject upload
if (allowedTypes.indexOf(file.mimetype) == -1) {
done(new ErrorMessage('unsupported file type', [file.originalname, file.mimetype]), false);
} else
done(null, true); // Accept file for furhter investigation
});
}
})
.catch(getResourceErrorHandler(req, res, next, 'thing', id));
});
function cleanupFiles(req) {
if (!Array.isArray(req.files))
return;
req.files.forEach(file => {
fs.unlink(file.path, error => {
if (error)
debug.error({
context: 'upload',
error,
req
});
});
});
}
// Verify that a file's contents match its claimed MIME type. This is shallow,
// fast validation. If files are manipulated, we need to pay further attention
// to any possible exploits.
function validateFile(filePath, claimedType) {
return new Promise((resolve, reject) => {
readChunk(filePath, 0, 262)
.then(buffer => {
let type = fileType(buffer);
if (!type)
return reject(new ErrorMessage('unrecognized file type', [path.basename(filePath)]));
if (type.mime === claimedType)
return resolve();
if (type.mime !== claimedType)
return reject(new ErrorMessage('mime mismatch', [path.basename(filePath), claimedType, type.mime]));
})
.catch(error => reject(error));
});
}
// SVGs can't be validated by magic number check. This, too, is a relatively
// shallow validation, not a full XML parse.
function validateSVG(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, (error, data) => {
if (error)
return reject(error);
if (isSVG(data))
return resolve();
else
return reject(new ErrorMessage('not valid svg', [path.basename(filePath)]));
});
});
}
module.exports = router;