'use strict'; var path = require('path'); var async = require('async'); var nconf = require('nconf'); var mime = require('mime'); var fs = require('fs'); var jimp = require('jimp'); var meta = require('../../meta'); var posts = require('../../posts'); var file = require('../../file'); var image = require('../../image'); var plugins = require('../../plugins'); var pagination = require('../../pagination'); var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; var uploadsController = module.exports; uploadsController.get = function (req, res, next) { var currentFolder = path.join(nconf.get('upload_path'), req.query.dir || ''); if (!currentFolder.startsWith(nconf.get('upload_path'))) { return next(new Error('[[error:invalid-path]]')); } var itemsPerPage = 20; var itemCount = 0; var page = parseInt(req.query.page, 10) || 1; async.waterfall([ function (next) { fs.readdir(currentFolder, next); }, function (files, next) { files = files.filter(function (filename) { return filename !== '.gitignore'; }); itemCount = files.length; var start = Math.max(0, (page - 1) * itemsPerPage); var stop = start + itemsPerPage; files = files.slice(start, stop); filesToData(currentFolder, files, next); }, function (files, next) { files.sort(function (a, b) { if (a.isDirectory && !b.isDirectory) { return -1; } else if (!a.isDirectory && b.isDirectory) { return 1; } return 0; }); // Add post usage info if in /files if (req.query.dir === '/files') { posts.uploads.getUsage(files, function (err, usage) { files.forEach(function (file, idx) { file.inPids = usage[idx].map(pid => parseInt(pid, 10)); }); next(err, files); }); } else { setImmediate(next, null, files); } }, ], function (err, files) { if (err) { return next(err); } res.render('admin/manage/uploads', { currentFolder: currentFolder.replace(nconf.get('upload_path'), ''), showPids: files[0].hasOwnProperty('inPids'), files: files, breadcrumbs: buildBreadcrumbs(currentFolder), pagination: pagination.create(page, Math.ceil(itemCount / itemsPerPage), req.query), }); }); }; function buildBreadcrumbs(currentFolder) { var crumbs = []; var parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep); var currentPath = ''; parts.forEach(function (part) { var dir = path.join(currentPath, part); crumbs.push({ text: part || 'Uploads', url: part ? '/admin/manage/uploads?dir=' + dir : '/admin/manage/uploads', }); currentPath = dir; }); return crumbs; } function filesToData(currentDir, files, callback) { async.map(files, function (file, next) { var stat; async.waterfall([ function (next) { fs.stat(path.join(currentDir, file), next); }, function (_stat, next) { stat = _stat; if (stat.isDirectory()) { fs.readdir(path.join(currentDir, file), next); } else { next(null, []); } }, function (filesInDir, next) { var url = nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '') + '/' + file; next(null, { name: file, path: path.join(currentDir, file).replace(nconf.get('upload_path'), ''), url: url, fileCount: Math.max(0, filesInDir.length - 1), // ignore .gitignore size: stat.size, sizeHumanReadable: (stat.size / 1024).toFixed(1) + 'KiB', isDirectory: stat.isDirectory(), isFile: stat.isFile(), }); }, ], next); }, callback); } uploadsController.uploadCategoryPicture = function (req, res, next) { var uploadedFile = req.files.files[0]; var params = null; try { params = JSON.parse(req.body.params); } catch (e) { file.delete(uploadedFile.path); return next(new Error('[[error:invalid-json]]')); } if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) { var filename = 'category-' + params.cid + path.extname(uploadedFile.name); uploadImage(filename, 'category', uploadedFile, req, res, next); } }; uploadsController.uploadFavicon = function (req, res, next) { var uploadedFile = req.files.files[0]; var allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function (err, image) { file.delete(uploadedFile.path); if (err) { return next(err); } res.json([{ name: uploadedFile.name, url: image.url }]); }); } }; uploadsController.uploadTouchIcon = function (req, res, next) { var uploadedFile = req.files.files[0]; var allowedTypes = ['image/png']; var sizes = [36, 48, 72, 96, 144, 192]; if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function (err, imageObj) { if (err) { return next(err); } // Resize the image into squares for use as touch icons at various DPIs async.each(sizes, function (size, next) { async.series([ async.apply(file.saveFileToLocal, 'touchicon-' + size + '.png', 'system', uploadedFile.path), async.apply(image.resizeImage, { path: path.join(nconf.get('upload_path'), 'system', 'touchicon-' + size + '.png'), extension: 'png', width: size, height: size, }), ], next); }, function (err) { file.delete(uploadedFile.path); if (err) { return next(err); } res.json([{ name: uploadedFile.name, url: imageObj.url }]); }); }); } }; uploadsController.uploadLogo = function (req, res, next) { upload('site-logo', req, res, next); }; uploadsController.uploadSound = function (req, res, next) { var uploadedFile = req.files.files[0]; var mimeType = mime.getType(uploadedFile.name); if (!/^audio\//.test(mimeType)) { return next(Error('[[error:invalid-data]]')); } async.waterfall([ function (next) { file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, next); }, function (uploadedSound, next) { meta.sounds.build(next); }, ], function (err) { file.delete(uploadedFile.path); if (err) { return next(err); } res.json([{}]); }); }; uploadsController.uploadFile = function (req, res, next) { var uploadedFile = req.files.files[0]; var params; try { params = JSON.parse(req.body.params); } catch (e) { file.delete(uploadedFile.path); return next(new Error('[[error:invalid-json]]')); } file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path, function (err, data) { file.delete(uploadedFile.path); if (err) { return next(err); } res.json([{ url: data.url }]); }); }; uploadsController.uploadDefaultAvatar = function (req, res, next) { upload('avatar-default', req, res, next); }; uploadsController.uploadOgImage = function (req, res, next) { upload('og:image', req, res, next); }; function upload(name, req, res, next) { var uploadedFile = req.files.files[0]; if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) { var filename = name + path.extname(uploadedFile.name); uploadImage(filename, 'system', uploadedFile, req, res, next); } } function validateUpload(req, res, next, uploadedFile, allowedTypes) { if (allowedTypes.indexOf(uploadedFile.type) === -1) { file.delete(uploadedFile.path); res.json({ error: '[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]' }); return false; } return true; } function uploadImage(filename, folder, uploadedFile, req, res, next) { async.waterfall([ function (next) { if (plugins.hasListeners('filter:uploadImage')) { plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.uid }, next); } else { file.saveFileToLocal(filename, folder, uploadedFile.path, next); } }, function (imageData, next) { // Post-processing for site-logo if (path.basename(filename, path.extname(filename)) === 'site-logo' && folder === 'system') { var uploadPath = path.join(nconf.get('upload_path'), folder, 'site-logo-x50.png'); async.series([ async.apply(image.resizeImage, { path: uploadedFile.path, target: uploadPath, extension: 'png', height: 50, }), async.apply(meta.configs.set, 'brand:emailLogo', path.join(nconf.get('upload_url'), 'system/site-logo-x50.png')), ], function (err) { next(err, imageData); }); } else if (path.basename(filename, path.extname(filename)) === 'og:image' && folder === 'system') { jimp.read(imageData.path).then(function (image) { meta.configs.setMultiple({ 'og:image:height': image.bitmap.height, 'og:image:width': image.bitmap.width, }, function (err) { next(err, imageData); }); }).catch(function (err) { next(err); }); } else { setImmediate(next, null, imageData); } }, ], function (err, image) { file.delete(uploadedFile.path); if (err) { return next(err); } res.json([{ name: uploadedFile.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url }]); }); }