diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index ef85db041d..d70808a8b7 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -7,10 +7,10 @@ define('admin/manage/users', ['translator', 'benchpress'], function (translator, Users.init = function () { var navPills = $('.nav-pills li'); var pathname = window.location.pathname; - if (!navPills.find('a[href="' + pathname + '"]').length) { + if (!navPills.find('a[href^="' + pathname + '"]').length) { pathname = config.relative_path + '/admin/manage/users/latest'; } - navPills.removeClass('active').find('a[href="' + pathname + '"]').parent().addClass('active'); + navPills.removeClass('active').find('a[href^="' + pathname + '"]').parent().addClass('active'); $('#results-per-page').val(ajaxify.data.resultsPerPage).on('change', function () { var query = utils.params(); diff --git a/src/controllers/admin/rewards.js b/src/controllers/admin/rewards.js index 37b49929bb..062bbdcf9a 100644 --- a/src/controllers/admin/rewards.js +++ b/src/controllers/admin/rewards.js @@ -1,19 +1,10 @@ 'use strict'; -var async = require('async'); +const admin = require('../../rewards/admin'); -var rewardsController = module.exports; +const rewardsController = module.exports; -rewardsController.get = function (req, res, next) { - async.waterfall([ - function (next) { - require('../../rewards/admin').get(next); - }, - function (data) { - res.render('admin/extend/rewards', data); - }, - ], next); +rewardsController.get = async function (req, res) { + const data = await admin.get(); + res.render('admin/extend/rewards', data); }; - - -module.exports = rewardsController; diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index 7242cbab1f..e80619a72e 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -1,64 +1,47 @@ 'use strict'; -var async = require('async'); +const meta = require('../../meta'); +const emailer = require('../../emailer'); +const notifications = require('../../notifications'); -var meta = require('../../meta'); -var emailer = require('../../emailer'); -var notifications = require('../../notifications'); +const settingsController = module.exports; -var settingsController = module.exports; +settingsController.get = async function (req, res, next) { + const term = req.params.term ? req.params.term : 'general'; -settingsController.get = function (req, res, next) { - var term = req.params.term ? req.params.term : 'general'; - - switch (req.params.term) { - case 'email': - renderEmail(req, res, next); - break; - case 'user': - renderUser(req, res, next); - break; - default: + if (term === 'email') { + await renderEmail(req, res, next); + } else if (term === 'user') { + await renderUser(req, res, next); + } else { res.render('admin/settings/' + term); } }; -function renderEmail(req, res, next) { - async.waterfall([ - function (next) { - async.parallel({ - emails: async.apply(emailer.getTemplates, meta.config), - services: emailer.listServices, - }, next); - }, - function (results) { - res.render('admin/settings/email', { - emails: results.emails, - sendable: results.emails.filter(function (email) { - return !email.path.includes('_plaintext') && !email.path.includes('partials'); - }), - services: results.services, - }); - }, - ], next); +async function renderEmail(req, res) { + const [emails, services] = await Promise.all([ + emailer.getTemplates(meta.config), + emailer.listServices(), + ]); + res.render('admin/settings/email', { + emails: emails, + sendable: emails.filter(function (email) { + return !email.path.includes('_plaintext') && !email.path.includes('partials'); + }), + services: services, + }); } -function renderUser(req, res, next) { - async.waterfall([ - function (next) { - notifications.getAllNotificationTypes(next); - }, - function (notificationTypes) { - var notificationSettings = notificationTypes.map(function (type) { - return { - name: type, - label: '[[notifications:' + type + ']]', - }; - }); - res.render('admin/settings/user', { - notificationSettings: notificationSettings, - }); - }, - ], next); +async function renderUser(req, res) { + const notificationTypes = await notifications.getAllNotificationTypes(); + const notificationSettings = notificationTypes.map(function (type) { + return { + name: type, + label: '[[notifications:' + type + ']]', + }; + }); + res.render('admin/settings/user', { + notificationSettings: notificationSettings, + }); } diff --git a/src/controllers/admin/social.js b/src/controllers/admin/social.js index 75bdfd5967..d08bf67f7d 100644 --- a/src/controllers/admin/social.js +++ b/src/controllers/admin/social.js @@ -1,20 +1,12 @@ 'use strict'; -var async = require('async'); +const social = require('../../social'); -var social = require('../../social'); +const socialController = module.exports; -var socialController = module.exports; - -socialController.get = function (req, res, next) { - async.waterfall([ - function (next) { - social.getPostSharing(next); - }, - function (posts) { - res.render('admin/general/social', { - posts: posts, - }); - }, - ], next); +socialController.get = async function (req, res) { + const posts = await social.getPostSharing(); + res.render('admin/general/social', { + posts: posts, + }); }; diff --git a/src/controllers/admin/sounds.js b/src/controllers/admin/sounds.js index c0ad374a55..ab82009596 100644 --- a/src/controllers/admin/sounds.js +++ b/src/controllers/admin/sounds.js @@ -1,48 +1,38 @@ 'use strict'; -var async = require('async'); +const plugins = require('../../plugins'); +const meta = require('../../meta'); -var plugins = require('../../plugins'); -var meta = require('../../meta'); +const soundsController = module.exports; -var soundsController = module.exports; - -soundsController.get = function (req, res, next) { - var types = [ +soundsController.get = async function (req, res) { + const types = [ 'notification', 'chat-incoming', 'chat-outgoing', ]; - async.waterfall([ - function (next) { - meta.configs.getFields(types, next); - }, - function (settings) { - settings = settings || {}; - - var output = {}; - - types.forEach(function (type) { - var soundpacks = plugins.soundpacks.map(function (pack) { - var sounds = Object.keys(pack.sounds).map(function (soundName) { - var value = pack.name + ' | ' + soundName; - return { - name: soundName, - value: value, - selected: value === settings[type], - }; - }); + const settings = await meta.configs.getFields(types) || {}; + var output = {}; + + types.forEach(function (type) { + var soundpacks = plugins.soundpacks.map(function (pack) { + var sounds = Object.keys(pack.sounds).map(function (soundName) { + var value = pack.name + ' | ' + soundName; + return { + name: soundName, + value: value, + selected: value === settings[type], + }; + }); - return { - name: pack.name, - sounds: sounds, - }; - }); + return { + name: pack.name, + sounds: sounds, + }; + }); - output[type + '-sound'] = soundpacks; - }); + output[type + '-sound'] = soundpacks; + }); - res.render('admin/general/sounds', output); - }, - ], next); + res.render('admin/general/sounds', output); }; diff --git a/src/controllers/admin/tags.js b/src/controllers/admin/tags.js index 3fec3915ec..eff1ae714c 100644 --- a/src/controllers/admin/tags.js +++ b/src/controllers/admin/tags.js @@ -1,18 +1,10 @@ 'use strict'; -var async = require('async'); +const topics = require('../../topics'); -var topics = require('../../topics'); +const tagsController = module.exports; -var tagsController = module.exports; - -tagsController.get = function (req, res, next) { - async.waterfall([ - function (next) { - topics.getTags(0, 199, next); - }, - function (tags) { - res.render('admin/manage/tags', { tags: tags }); - }, - ], next); +tagsController.get = async function (req, res) { + const tags = await topics.getTags(0, 199); + res.render('admin/manage/tags', { tags: tags }); }; diff --git a/src/controllers/admin/themes.js b/src/controllers/admin/themes.js index 99a1e12c98..59be636331 100644 --- a/src/controllers/admin/themes.js +++ b/src/controllers/admin/themes.js @@ -1,48 +1,32 @@ 'use strict'; -var path = require('path'); -var fs = require('fs'); -var async = require('async'); - -var file = require('../../file'); - -var themesController = module.exports; - -var defaultScreenshotPath = path.join(__dirname, '../../../public/images/themes/default.png'); - -themesController.get = function (req, res, next) { - var themeDir = path.join(__dirname, '../../../node_modules', req.params.theme); - var themeConfigPath = path.join(themeDir, 'theme.json'); - var screenshotPath; - async.waterfall([ - function (next) { - fs.readFile(themeConfigPath, 'utf8', function (err, config) { - if (err) { - if (err.code === 'ENOENT') { - return next(Error('invalid-data')); - } - - return next(err); - } - - return next(null, config); - }); - }, - function (themeConfig, next) { - try { - themeConfig = JSON.parse(themeConfig); - } catch (e) { - return next(e); - } - - next(null, themeConfig.screenshot ? path.join(themeDir, themeConfig.screenshot) : defaultScreenshotPath); - }, - function (_screenshotPath, next) { - screenshotPath = _screenshotPath; - file.exists(screenshotPath, next); - }, - function (exists) { - res.sendFile(exists ? screenshotPath : defaultScreenshotPath); - }, - ], next); +const path = require('path'); +const fs = require('fs'); +const util = require('util'); +const readFileAsync = util.promisify(fs.readFile); + +const file = require('../../file'); + +const themesController = module.exports; + +const defaultScreenshotPath = path.join(__dirname, '../../../public/images/themes/default.png'); + +themesController.get = async function (req, res, next) { + const themeDir = path.join(__dirname, '../../../node_modules', req.params.theme); + const themeConfigPath = path.join(themeDir, 'theme.json'); + + let themeConfig; + try { + themeConfig = await readFileAsync(themeConfigPath, 'utf8'); + themeConfig = JSON.parse(themeConfig); + } catch (err) { + if (err.code === 'ENOENT') { + return next(Error('invalid-data')); + } + return next(err); + } + + const screenshotPath = themeConfig.screenshot ? path.join(themeDir, themeConfig.screenshot) : defaultScreenshotPath; + const exists = await file.exists(screenshotPath); + res.sendFile(exists ? screenshotPath : defaultScreenshotPath); }; diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index a0c27ae20c..41648e906d 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -1,83 +1,71 @@ 'use strict'; -var path = require('path'); -var async = require('async'); -var nconf = require('nconf'); -var mime = require('mime'); -var fs = require('fs'); - -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 || ''); +const path = require('path'); +const nconf = require('nconf'); +const mime = require('mime'); +const fs = require('fs'); +const util = require('util'); +const readdirAsync = util.promisify(fs.readdir); +const statAsync = util.promisify(fs.stat); + +const meta = require('../../meta'); +const posts = require('../../posts'); +const file = require('../../file'); +const image = require('../../image'); +const plugins = require('../../plugins'); +const pagination = require('../../pagination'); + +const allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; + +const uploadsController = module.exports; + +uploadsController.get = async function (req, res, next) { + const 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) { - // Float directories to the top - files.sort(function (a, b) { - if (a.isDirectory && !b.isDirectory) { - return -1; - } else if (!a.isDirectory && b.isDirectory) { - return 1; - } else if (!a.isDirectory && !b.isDirectory) { - return a.mtime < b.mtime ? -1 : 1; - } - - return 0; - }); + const itemsPerPage = 20; + const page = parseInt(req.query.page, 10) || 1; + try { + let files = await readdirAsync(currentFolder); + files = files.filter(filename => filename !== '.gitignore'); + const itemCount = files.length; + var start = Math.max(0, (page - 1) * itemsPerPage); + var stop = start + itemsPerPage; + files = files.slice(start, stop); + + files = await filesToData(currentFolder, files); + + // Float directories to the top + files.sort(function (a, b) { + if (a.isDirectory && !b.isDirectory) { + return -1; + } else if (!a.isDirectory && b.isDirectory) { + return 1; + } else if (!a.isDirectory && !b.isDirectory) { + return a.mtime < b.mtime ? -1 : 1; + } - // 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)); - }); + return 0; + }); - next(err, files); - }); - } else { - setImmediate(next, null, files); - } - }, - function (files) { - res.render('admin/manage/uploads', { - currentFolder: currentFolder.replace(nconf.get('upload_path'), ''), - showPids: files.length && files[0].hasOwnProperty('inPids'), - files: files, - breadcrumbs: buildBreadcrumbs(currentFolder), - pagination: pagination.create(page, Math.ceil(itemCount / itemsPerPage), req.query), + // Add post usage info if in /files + if (req.query.dir === '/files') { + const usage = await posts.uploads.getUsage(files); + files.forEach(function (file, idx) { + file.inPids = usage[idx].map(pid => parseInt(pid, 10)); }); - }, - ], next); + } + res.render('admin/manage/uploads', { + currentFolder: currentFolder.replace(nconf.get('upload_path'), ''), + showPids: files.length && files[0].hasOwnProperty('inPids'), + files: files, + breadcrumbs: buildBreadcrumbs(currentFolder), + pagination: pagination.create(page, Math.ceil(itemCount / itemsPerPage), req.query), + }); + } catch (err) { + next(err); + } }; function buildBreadcrumbs(currentFolder) { @@ -98,42 +86,33 @@ function buildBreadcrumbs(currentFolder) { 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(), - mtime: stat.mtimeMs, - }); - }, - ], next); - }, callback); +async function filesToData(currentDir, files) { + return await Promise.all(files.map(file => getFileData(currentDir, file))); } -uploadsController.uploadCategoryPicture = function (req, res, next) { - var uploadedFile = req.files.files[0]; - var params = null; +async function getFileData(currentDir, file) { + const stat = await statAsync(path.join(currentDir, file)); + let filesInDir = []; + if (stat.isDirectory()) { + filesInDir = await readdirAsync(path.join(currentDir, file)); + } + const url = nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '') + '/' + file; + return { + 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(), + mtime: stat.mtimeMs, + }; +} + +uploadsController.uploadCategoryPicture = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + let params = null; try { params = JSON.parse(req.body.params); @@ -142,91 +121,80 @@ uploadsController.uploadCategoryPicture = function (req, res, next) { 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); + if (validateUpload(res, uploadedFile, allowedImageTypes)) { + const filename = 'category-' + params.cid + path.extname(uploadedFile.name); + await 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) { +uploadsController.uploadFavicon = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; + + if (validateUpload(res, uploadedFile, allowedTypes)) { + try { + const imageObj = await file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path); + res.json([{ name: uploadedFile.name, url: imageObj.url }]); + } catch (err) { + next(err); + } finally { 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); - } +uploadsController.uploadTouchIcon = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + const allowedTypes = ['image/png']; + const sizes = [36, 48, 72, 96, 144, 192]; + if (validateUpload(res, uploadedFile, allowedTypes)) { + try { + const imageObj = await file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path); // Resize the image into squares for use as touch icons at various DPIs - async.eachSeries(sizes, function (size, next) { - image.resizeImage({ + for (const size of sizes) { + /* eslint-disable no-await-in-loop */ + await image.resizeImage({ path: uploadedFile.path, target: path.join(nconf.get('upload_path'), 'system', 'touchicon-' + size + '.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 }]); - }); - }); + }); + } + res.json([{ name: uploadedFile.name, url: imageObj.url }]); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } } }; -uploadsController.uploadLogo = function (req, res, next) { - upload('site-logo', req, res, next); +uploadsController.uploadLogo = async function (req, res, next) { + await upload('site-logo', req, res, next); }; -uploadsController.uploadSound = function (req, res, next) { - var uploadedFile = req.files.files[0]; +uploadsController.uploadSound = async function (req, res, next) { + const uploadedFile = req.files.files[0]; - var mimeType = mime.getType(uploadedFile.name); + const 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); - } + try { + await file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path); + await meta.sounds.build(); res.json([{}]); - }); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } }; -uploadsController.uploadFile = function (req, res, next) { - var uploadedFile = req.files.files[0]; - var params; +uploadsController.uploadFile = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + let params; try { params = JSON.parse(req.body.params); } catch (e) { @@ -234,33 +202,34 @@ uploadsController.uploadFile = function (req, res, next) { 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); - } + try { + const data = await file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path); res.json([{ url: data.url }]); - }); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } }; -uploadsController.uploadDefaultAvatar = function (req, res, next) { - upload('avatar-default', req, res, next); +uploadsController.uploadDefaultAvatar = async function (req, res, next) { + await upload('avatar-default', req, res, next); }; -uploadsController.uploadOgImage = function (req, res, next) { - upload('og:image', req, res, next); +uploadsController.uploadOgImage = async function (req, res, next) { + await upload('og:image', req, res, next); }; -function upload(name, req, res, next) { - var uploadedFile = req.files.files[0]; +async function upload(name, req, res, next) { + const 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); + if (validateUpload(res, uploadedFile, allowedImageTypes)) { + const filename = name + path.extname(uploadedFile.name); + await uploadImage(filename, 'system', uploadedFile, req, res, next); } } -function validateUpload(req, res, next, uploadedFile, allowedTypes) { +function validateUpload(res, uploadedFile, allowedTypes) { if (!allowedTypes.includes(uploadedFile.type)) { file.delete(uploadedFile.path); res.json({ error: '[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]' }); @@ -270,64 +239,39 @@ function validateUpload(req, res, next, uploadedFile, allowedTypes) { 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, - height: 50, - }), - async.apply(meta.configs.set, 'brand:emailLogo', path.join(nconf.get('upload_url'), 'system/site-logo-x50.png')), - function (next) { - image.size(uploadedFile.path, function (err, size) { - if (err) { - return next(err); - } - - meta.configs.setMultiple({ - 'brand:logo:width': size.width, - 'brand:logo:height': size.height, - }, function (err) { - next(err); - }); - }); - }, - ], function (err) { - next(err, imageData); - }); - } else if (path.basename(filename, path.extname(filename)) === 'og:image' && folder === 'system') { - image.size(uploadedFile.path, function (err, size) { - if (err) { - next(err); - } - meta.configs.setMultiple({ - 'og:image:width': size.width, - 'og:image:height': size.height, - }, function (err) { - next(err, imageData); - }); - }); - } else { - setImmediate(next, null, imageData); - } - }, - ], function (err, image) { - file.delete(uploadedFile.path); - if (err) { - return next(err); +async function uploadImage(filename, folder, uploadedFile, req, res, next) { + let imageData; + try { + if (plugins.hasListeners('filter:uploadImage')) { + imageData = await plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.uid }); + } else { + imageData = await file.saveFileToLocal(filename, folder, uploadedFile.path); } - res.json([{ name: uploadedFile.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url }]); - }); + + if (path.basename(filename, path.extname(filename)) === 'site-logo' && folder === 'system') { + const uploadPath = path.join(nconf.get('upload_path'), folder, 'site-logo-x50.png'); + await image.resizeImage({ + path: uploadedFile.path, + target: uploadPath, + height: 50, + }); + await meta.configs.set('brand:emailLogo', path.join(nconf.get('upload_url'), 'system/site-logo-x50.png')); + const size = await image.size(uploadedFile.path); + await meta.configs.setMultiple({ + 'brand:logo:width': size.width, + 'brand:logo:height': size.height, + }); + } else if (path.basename(filename, path.extname(filename)) === 'og:image' && folder === 'system') { + const size = await image.size(uploadedFile.path); + await meta.configs.setMultiple({ + 'og:image:width': size.width, + 'og:image:height': size.height, + }); + } + res.json([{ name: uploadedFile.name, url: imageData.url.startsWith('http') ? imageData.url : nconf.get('relative_path') + imageData.url }]); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } } diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index cb4f2b7420..c6c6673fc0 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -1,18 +1,18 @@ 'use strict'; -var async = require('async'); -var nconf = require('nconf'); +const nconf = require('nconf'); -var user = require('../../user'); -var meta = require('../../meta'); -var db = require('../../database'); -var pagination = require('../../pagination'); -var events = require('../../events'); -var plugins = require('../../plugins'); +const user = require('../../user'); +const meta = require('../../meta'); +const db = require('../../database'); +const pagination = require('../../pagination'); +const events = require('../../events'); +const plugins = require('../../plugins'); +const utils = require('../../utils'); -var usersController = module.exports; +const usersController = module.exports; -var userFields = ['uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned', +const userFields = ['uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned', 'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed']; usersController.search = function (req, res) { @@ -22,154 +22,129 @@ usersController.search = function (req, res) { }); }; -usersController.sortByJoinDate = function (req, res, next) { - getUsers('users:joindate', 'latest', undefined, undefined, req, res, next); +usersController.sortByJoinDate = async function (req, res) { + await getUsers('users:joindate', 'latest', undefined, undefined, req, res); }; -usersController.notValidated = function (req, res, next) { - getUsers('users:notvalidated', 'notvalidated', undefined, undefined, req, res, next); +usersController.notValidated = async function (req, res) { + await getUsers('users:notvalidated', 'notvalidated', undefined, undefined, req, res); }; -usersController.noPosts = function (req, res, next) { - getUsers('users:postcount', 'noposts', '-inf', 0, req, res, next); +usersController.noPosts = async function (req, res) { + await getUsers('users:postcount', 'noposts', '-inf', 0, req, res); }; -usersController.topPosters = function (req, res, next) { - getUsers('users:postcount', 'topposts', 0, '+inf', req, res, next); +usersController.topPosters = async function (req, res) { + await getUsers('users:postcount', 'topposts', 0, '+inf', req, res); }; -usersController.mostReputaion = function (req, res, next) { - getUsers('users:reputation', 'mostreputation', 0, '+inf', req, res, next); +usersController.mostReputaion = async function (req, res) { + await getUsers('users:reputation', 'mostreputation', 0, '+inf', req, res); }; -usersController.flagged = function (req, res, next) { - getUsers('users:flags', 'mostflags', 1, '+inf', req, res, next); +usersController.flagged = async function (req, res) { + await getUsers('users:flags', 'mostflags', 1, '+inf', req, res); }; -usersController.inactive = function (req, res, next) { - var timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3); - var cutoff = Date.now() - timeRange; - getUsers('users:online', 'inactive', '-inf', cutoff, req, res, next); +usersController.inactive = async function (req, res) { + const timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3); + const cutoff = Date.now() - timeRange; + await getUsers('users:online', 'inactive', '-inf', cutoff, req, res); }; -usersController.banned = function (req, res, next) { - getUsers('users:banned', 'banned', undefined, undefined, req, res, next); +usersController.banned = async function (req, res) { + await getUsers('users:banned', 'banned', undefined, undefined, req, res); }; -usersController.registrationQueue = function (req, res, next) { - var page = parseInt(req.query.page, 10) || 1; - var itemsPerPage = 20; - var start = (page - 1) * 20; - var stop = start + itemsPerPage - 1; - var invitations; - - async.waterfall([ - function (next) { - async.parallel({ - registrationQueueCount: function (next) { - db.sortedSetCard('registration:queue', next); - }, - users: function (next) { - user.getRegistrationQueue(start, stop, next); - }, - customHeaders: function (next) { - plugins.fireHook('filter:admin.registrationQueue.customHeaders', { headers: [] }, next); - }, - invites: function (next) { - async.waterfall([ - function (next) { - user.getAllInvites(next); - }, - function (_invitations, next) { - invitations = _invitations; - async.map(invitations, function (invites, next) { - user.getUserField(invites.uid, 'username', next); - }, next); - }, - function (usernames, next) { - invitations.forEach(function (invites, index) { - invites.username = usernames[index]; - }); - async.map(invitations, function (invites, next) { - async.map(invites.invitations, user.getUsernameByEmail, next); - }, next); - }, - function (usernames, next) { - invitations.forEach(function (invites, index) { - invites.invitations = invites.invitations.map(function (email, i) { - return { - email: email, - username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i], - }; - }); - }); - next(null, invitations); - }, - ], next); - }, - }, next); - }, - function (data) { - var pageCount = Math.max(1, Math.ceil(data.registrationQueueCount / itemsPerPage)); - data.pagination = pagination.create(page, pageCount); - data.customHeaders = data.customHeaders.headers; - res.render('admin/manage/registration', data); - }, - ], next); +usersController.registrationQueue = async function (req, res) { + const page = parseInt(req.query.page, 10) || 1; + const itemsPerPage = 20; + const start = (page - 1) * 20; + const stop = start + itemsPerPage - 1; + + const data = await utils.promiseParallel({ + registrationQueueCount: db.sortedSetCard('registration:queue'), + users: user.getRegistrationQueue(start, stop), + customHeaders: plugins.fireHook('filter:admin.registrationQueue.customHeaders', { headers: [] }), + invites: getInvites(), + }); + var pageCount = Math.max(1, Math.ceil(data.registrationQueueCount / itemsPerPage)); + data.pagination = pagination.create(page, pageCount); + data.customHeaders = data.customHeaders.headers; + res.render('admin/manage/registration', data); }; -function getUsers(set, section, min, max, req, res, next) { - var page = parseInt(req.query.page, 10) || 1; - var resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50; +async function getInvites() { + const invitations = await user.getAllInvites(); + const uids = invitations.map(invite => invite.uid); + let usernames = await user.getUsersFields(uids, ['username']); + usernames = usernames.map(user => user.username); + + invitations.forEach(function (invites, index) { + invites.username = usernames[index]; + }); + + async function getUsernamesByEmails(emails) { + const uids = await db.sortedSetScore('email:uid', emails.map(email => String(email).toLowerCase())); + const usernames = await user.getUsersFields(uids, ['username']); + return usernames.map(user => user.username); + } + + usernames = await Promise.all(invitations.map(invites => getUsernamesByEmails(invites.invitations))); + + invitations.forEach(function (invites, index) { + invites.invitations = invites.invitations.map(function (email, i) { + return { + email: email, + username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i], + }; + }); + }); + return invitations; +} + +async function getUsers(set, section, min, max, req, res) { + const page = parseInt(req.query.page, 10) || 1; + let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50; if (![50, 100, 250, 500].includes(resultsPerPage)) { resultsPerPage = 50; } - var start = Math.max(0, page - 1) * resultsPerPage; - var stop = start + resultsPerPage - 1; - var byScore = min !== undefined && max !== undefined; - - async.waterfall([ - function (next) { - async.parallel({ - count: function (next) { - if (byScore) { - db.sortedSetCount(set, min, max, next); - } else if (set === 'users:banned' || set === 'users:notvalidated') { - db.sortedSetCard(set, next); - } else { - db.getObjectField('global', 'userCount', next); - } - }, - users: function (next) { - async.waterfall([ - function (next) { - if (byScore) { - db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min, next); - } else { - user.getUidsFromSet(set, start, stop, next); - } - }, - function (uids, next) { - user.getUsersWithFields(uids, userFields, req.uid, next); - }, - ], next); - }, - }, next); - }, - function (results) { - results.users = results.users.filter(function (user) { - return user && parseInt(user.uid, 10); - }); - var data = { - users: results.users, - page: page, - pageCount: Math.max(1, Math.ceil(results.count / resultsPerPage)), - resultsPerPage: resultsPerPage, - }; - data[section] = true; - render(req, res, data); - }, - ], next); + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + const byScore = min !== undefined && max !== undefined; + + async function getCount() { + if (byScore) { + return await db.sortedSetCount(set, min, max); + } else if (set === 'users:banned' || set === 'users:notvalidated') { + return await db.sortedSetCard(set); + } + return await db.getObjectField('global', 'userCount'); + } + + async function getUsersWithFields() { + let uids; + if (byScore) { + uids = await db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min); + } else { + uids = await user.getUidsFromSet(set, start, stop); + } + return await user.getUsersWithFields(uids, userFields, req.uid); + } + + const [count, users] = await Promise.all([ + getCount(), + getUsersWithFields(), + ]); + + const data = { + users: users.filter(user => user && parseInt(user.uid, 10)), + page: page, + pageCount: Math.max(1, Math.ceil(count / resultsPerPage)), + resultsPerPage: resultsPerPage, + }; + data[section] = true; + render(req, res, data); } function render(req, res, data) { @@ -185,7 +160,7 @@ function render(req, res, data) { res.render('admin/manage/users', data); } -usersController.getCSV = function (req, res, next) { +usersController.getCSV = async function (req, res) { var referer = req.headers.referer; if (!referer || !referer.replace(nconf.get('url'), '').startsWith('/admin/manage/users')) { @@ -196,14 +171,8 @@ usersController.getCSV = function (req, res, next) { uid: req.uid, ip: req.ip, }); - async.waterfall([ - function (next) { - user.getUsersCSV(next); - }, - function (data) { - res.attachment('users.csv'); - res.setHeader('Content-Type', 'text/csv'); - res.end(data); - }, - ], next); + const data = await user.getUsersCSV(); + res.attachment('users.csv'); + res.setHeader('Content-Type', 'text/csv'); + res.end(data); }; diff --git a/src/controllers/admin/widgets.js b/src/controllers/admin/widgets.js index 15e75a0f68..04ac72fd23 100644 --- a/src/controllers/admin/widgets.js +++ b/src/controllers/admin/widgets.js @@ -1,16 +1,9 @@ 'use strict'; -var async = require('async'); +const widgetsController = module.exports; +const admin = require('../../widgets/admin'); -var widgetsController = module.exports; - -widgetsController.get = function (req, res, next) { - async.waterfall([ - function (next) { - require('../../widgets/admin').get(next); - }, - function (data) { - res.render('admin/extend/widgets', data); - }, - ], next); +widgetsController.get = async function (req, res) { + const data = await admin.get(); + res.render('admin/extend/widgets', data); }; diff --git a/src/rewards/admin.js b/src/rewards/admin.js index 60f335ef86..b7d0d7a235 100644 --- a/src/rewards/admin.js +++ b/src/rewards/admin.js @@ -139,3 +139,5 @@ function getActiveRewards(callback) { }); }); } + +require('../promisify')(rewards);