diff --git a/package.json b/package.json
index b6a70902e4..8561984d0d 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,9 @@
"heapdump": "^0.3.0",
"less": "^2.0.0",
"logrotate-stream": "^0.2.3",
+ "mime": "^1.3.4",
"mkdirp": "~0.5.0",
+ "mmmagic": "^0.3.13",
"morgan": "^1.3.2",
"nconf": "~0.7.1",
"nodebb-plugin-dbsearch": "^0.1.0",
diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json
index bf4e7229fd..8c2fadce8d 100644
--- a/public/language/en_GB/error.json
+++ b/public/language/en_GB/error.json
@@ -64,6 +64,7 @@
"invalid-image-type": "Invalid image type. Allowed types are: %1",
"invalid-image-extension": "Invalid image extension",
+ "invalid-file-type": "Invalid file type. Allowed types are: %1",
"group-name-too-short": "Group name too short",
"group-already-exists": "Group already exists",
@@ -80,7 +81,6 @@
"topic-thumbnails-are-disabled": "Topic thumbnails are disabled.",
"invalid-file": "Invalid File",
"uploads-are-disabled": "Uploads are disabled",
- "upload-error": "Upload Error : %1",
"signature-too-long" : "Sorry, your signature cannot be longer than %1 characters.",
diff --git a/public/src/modules/composer/uploads.js b/public/src/modules/composer/uploads.js
index e7bc473cb5..2a6833c604 100644
--- a/public/src/modules/composer/uploads.js
+++ b/public/src/modules/composer/uploads.js
@@ -316,8 +316,7 @@ define('composer/uploads', ['composer/preview', 'csrf'], function(preview, csrf)
function onUploadError(xhr) {
xhr = maybeParse(xhr);
-
- app.alertError('[[error:upload-error, ' + xhr.responseText + ']]');
+ app.alertError(xhr.responseText);
}
return uploads;
diff --git a/public/src/utils.js b/public/src/utils.js
index e0fef6151b..9c46e3fb6a 100644
--- a/public/src/utils.js
+++ b/public/src/utils.js
@@ -130,38 +130,39 @@
return ('' + path).split('.').pop();
},
- fileMimeType: (function () {
- // we only care about images, for now
- var map = {
- "bmp": "image/bmp",
- "cmx": "image/x-cmx",
- "cod": "image/cis-cod",
- "gif": "image/gif",
- "ico": "image/x-icon",
- "ief": "image/ief",
- "jfif": "image/pipeg",
- "jpe": "image/jpeg",
- "jpeg": "image/jpeg",
- "jpg": "image/jpeg",
- "pbm": "image/x-portable-bitmap",
- "pgm": "image/x-portable-graymap",
- "pnm": "image/x-portable-anymap",
- "ppm": "image/x-portable-pixmap",
- "ras": "image/x-cmu-raster",
- "rgb": "image/x-rgb",
- "svg": "image/svg+xml",
- "tif": "image/tiff",
- "tiff": "image/tiff",
- "xbm": "image/x-xbitmap",
- "xpm": "image/x-xpixmap",
- "xwd": "image/x-xwindowdump"
- };
+ extensionMimeTypeMap: {
+ "bmp": "image/bmp",
+ "cmx": "image/x-cmx",
+ "cod": "image/cis-cod",
+ "gif": "image/gif",
+ "ico": "image/x-icon",
+ "ief": "image/ief",
+ "jfif": "image/pipeg",
+ "jpe": "image/jpeg",
+ "jpeg": "image/jpeg",
+ "jpg": "image/jpeg",
+ "png": "image/png",
+ "pbm": "image/x-portable-bitmap",
+ "pgm": "image/x-portable-graymap",
+ "pnm": "image/x-portable-anymap",
+ "ppm": "image/x-portable-pixmap",
+ "ras": "image/x-cmu-raster",
+ "rgb": "image/x-rgb",
+ "svg": "image/svg+xml",
+ "tif": "image/tiff",
+ "tiff": "image/tiff",
+ "xbm": "image/x-xbitmap",
+ "xpm": "image/x-xpixmap",
+ "xwd": "image/x-xwindowdump"
+ },
- return function (path) {
- var extension = utils.fileExtension(path);
- return map[extension] || '*';
- };
- })(),
+ fileMimeType: function (path) {
+ utils.extensionToMimeType(utils.fileExtension(path));
+ },
+
+ extensionToMimeType: function(extension) {
+ return utils.extensionMimeTypeMap[extension] || '*';
+ },
isRelativeUrl: function(url) {
var firstChar = url.slice(0, 1);
diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js
index 13055ca852..600c6a2347 100644
--- a/src/controllers/accounts.js
+++ b/src/controllers/accounts.js
@@ -376,33 +376,26 @@ accountsController.accountSettings = function(req, res, next) {
accountsController.uploadPicture = function (req, res, next) {
var userPhoto = req.files.files[0];
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
-
- if (userPhoto.size > uploadSize * 1024) {
- fs.unlink(userPhoto.path);
- return next(new Error('[[error:file-too-big, ' + uploadSize + ']]'));
- }
-
- var allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
- if (allowedTypes.indexOf(userPhoto.type) === -1) {
- fs.unlink(userPhoto.path);
- return next(new Error('[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]'));
- }
-
var extension = path.extname(userPhoto.name);
- if (!extension) {
- fs.unlink(userPhoto.path);
- return next(new Error('[[error:invalid-image-extension]]'));
- }
-
var updateUid = req.user ? req.user.uid : 0;
var imageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
+ var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
async.waterfall([
+ function(next) {
+ next(userPhoto.size > uploadSize * 1024 ? new Error('[[error:file-too-big, ' + uploadSize + ']]') : null);
+ },
+ function(next) {
+ next(!extension ? new Error('[[error:invalid-image-extension]]') : null);
+ },
+ function(next) {
+ file.isFileTypeAllowed(userPhoto.path, ['png', 'jpeg', 'jpg', 'gif'], next);
+ },
function(next) {
image.resizeImage(userPhoto.path, extension, imageDimension, imageDimension, next);
},
function(next) {
- if (parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1) {
+ if (convertToPNG) {
image.convertImageToPng(userPhoto.path, extension, next);
} else {
next();
@@ -412,7 +405,7 @@ accountsController.uploadPicture = function (req, res, next) {
user.getUidByUserslug(req.params.userslug, next);
},
function(uid, next) {
- if(parseInt(updateUid, 10) === parseInt(uid, 10)) {
+ if (parseInt(updateUid, 10) === parseInt(uid, 10)) {
return next();
}
@@ -450,7 +443,6 @@ accountsController.uploadPicture = function (req, res, next) {
return plugins.fireHook('filter:uploadImage', {image: userPhoto, uid: updateUid}, done);
}
- var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1;
var filename = updateUid + '-profileimg' + (convertToPNG ? '.png' : extension);
user.getUserField(updateUid, 'uploadedpicture', function (err, oldpicture) {
@@ -524,7 +516,7 @@ accountsController.getChats = function(req, res, next) {
if (!toUid || parseInt(toUid, 10) === parseInt(req.user.uid, 10)) {
return helpers.notFound(req, res);
}
-
+
async.parallel({
toUser: async.apply(user.getUserFields, toUid, ['uid', 'username']),
messages: async.apply(messaging.getMessages, req.user.uid, toUid, 'recent', false),
diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js
index b7e590b6bf..dc0db0591d 100644
--- a/src/controllers/uploads.js
+++ b/src/controllers/uploads.js
@@ -7,6 +7,7 @@ var uploadsController = {},
async = require('async'),
meta = require('../meta'),
+ file = require('../file'),
plugins = require('../plugins'),
utils = require('../../public/src/utils'),
image = require('../image');
@@ -42,12 +43,18 @@ uploadsController.upload = function(req, res, filesIterator, next) {
};
uploadsController.uploadPost = function(req, res, next) {
- uploadsController.upload(req, res, function(file, next) {
- if (file.type.match(/image./)) {
- uploadImage(req.user.uid, file, next);
- } else {
- uploadFile(req.user.uid, file, next);
- }
+ uploadsController.upload(req, res, function(uploadedFile, next) {
+ file.isFileTypeAllowed(uploadedFile.path, file.allowedExtensions(), function(err) {
+ if (err) {
+ return next(err);
+ }
+
+ if (uploadedFile.type.match(/image./)) {
+ uploadImage(req.user.uid, uploadedFile, next);
+ } else {
+ uploadFile(req.user.uid, uploadedFile, next);
+ }
+ });
}, next);
};
@@ -57,18 +64,24 @@ uploadsController.uploadThumb = function(req, res, next) {
return next(new Error('[[error:topic-thumbnails-are-disabled]]'));
}
- uploadsController.upload(req, res, function(file, next) {
- if(file.type.match(/image./)) {
- var size = meta.config.topicThumbSize || 120;
- image.resizeImage(file.path, path.extname(file.name), size, size, function(err) {
- if (err) {
- return next(err);
- }
- uploadImage(req.user.uid, file, next);
- });
- } else {
- next(new Error('[[error:invalid-file]]'));
- }
+ uploadsController.upload(req, res, function(uploadedFile, next) {
+ file.isFileTypeAllowed(uploadedFile.path, file.allowedExtensions(), function(err) {
+ if (err) {
+ return next(err);
+ }
+
+ if (uploadedFile.type.match(/image./)) {
+ var size = meta.config.topicThumbSize || 120;
+ image.resizeImage(uploadedFile.path, path.extname(uploadedFile.name), size, size, function(err) {
+ if (err) {
+ return next(err);
+ }
+ uploadImage(req.user.uid, uploadedFile, next);
+ });
+ } else {
+ next(new Error('[[error:invalid-file]]'));
+ }
+ });
}, next);
};
@@ -88,32 +101,32 @@ function uploadImage(uid, image, callback) {
}
}
-function uploadFile(uid, file, callback) {
+function uploadFile(uid, uploadedFile, callback) {
if (plugins.hasListeners('filter:uploadFile')) {
- return plugins.fireHook('filter:uploadFile', {file: file, uid: uid}, callback);
+ return plugins.fireHook('filter:uploadFile', {file: uploadedFile, uid: uid}, callback);
}
if (parseInt(meta.config.allowFileUploads, 10) !== 1) {
return callback(new Error('[[error:uploads-are-disabled]]'));
}
- if (!file) {
+ if (!uploadedFile) {
return callback(new Error('[[error:invalid-file]]'));
}
- if (file.size > parseInt(meta.config.maximumFileSize, 10) * 1024) {
+ if (uploadedFile.size > parseInt(meta.config.maximumFileSize, 10) * 1024) {
return callback(new Error('[[error:file-too-big, ' + meta.config.maximumFileSize + ']]'));
}
- var filename = 'upload-' + utils.generateUUID() + path.extname(file.name);
- require('../file').saveFileToLocal(filename, 'files', file.path, function(err, upload) {
+ var filename = 'upload-' + utils.generateUUID() + path.extname(uploadedFile.name);
+ file.saveFileToLocal(filename, 'files', uploadedFile.path, function(err, upload) {
if (err) {
return callback(err);
}
callback(null, {
url: upload.url,
- name: file.name
+ name: uploadedFile.name
});
});
}
diff --git a/src/file.js b/src/file.js
index 9433dfb40b..a3b594f326 100644
--- a/src/file.js
+++ b/src/file.js
@@ -3,7 +3,12 @@
var fs = require('fs'),
nconf = require('nconf'),
path = require('path'),
- winston = require('winston');
+ winston = require('winston'),
+ mmmagic = require('mmmagic'),
+ Magic = mmmagic.Magic,
+ mime = require('mime'),
+
+ meta= require('./meta');
var file = {};
@@ -11,7 +16,7 @@ file.saveFileToLocal = function(filename, folder, tempPath, callback) {
var uploadPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), folder, filename);
- winston.info('Saving file '+ filename +' to : ' + uploadPath);
+ winston.verbose('Saving file '+ filename +' to : ' + uploadPath);
var is = fs.createReadStream(tempPath);
var os = fs.createWriteStream(uploadPath);
@@ -30,4 +35,38 @@ file.saveFileToLocal = function(filename, folder, tempPath, callback) {
is.pipe(os);
};
+file.isFileTypeAllowed = function(path, allowedExtensions, callback) {
+ if (!Array.isArray(allowedExtensions) || !allowedExtensions.length) {
+ return callback();
+ }
+
+ allowedExtensions = allowedExtensions.filter(Boolean).map(function(extension) {
+ return extension.trim();
+ });
+
+ var magic = new Magic(mmmagic.MAGIC_MIME_TYPE);
+ magic.detectFile(path, function(err, mimeType) {
+ if (err) {
+ return callback(err);
+ }
+
+ var uploadedFileExtension = mime.extension(mimeType);
+
+ if (allowedExtensions.indexOf(uploadedFileExtension) === -1) {
+ return callback(new Error('[[error:invalid-file-type, ' + allowedExtensions.join('-') + ']]'));
+ }
+
+ callback();
+ });
+};
+
+file.allowedExtensions = function() {
+ var allowedExtensions = (meta.config.allowedFileExtensions || '').trim();
+ if (!allowedExtensions) {
+ return [];
+ }
+ allowedExtensions = allowedExtensions.split(',');
+ return allowedExtensions;
+};
+
module.exports = file;
\ No newline at end of file
diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl
index 3cb55eac0e..98b99f3973 100644
--- a/src/views/admin/settings/post.tpl
+++ b/src/views/admin/settings/post.tpl
@@ -119,7 +119,10 @@
Allow users to upload topic thumbnails
- Topic Thumb Size
+ Topic Thumb Size
+
+ Allowed file types, (ie png, pdf, zip). Leave empty to allow all.
+