diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json index d0d95b380f..76488baeb7 100644 --- a/public/language/en-GB/admin/manage/users.json +++ b/public/language/en-GB/admin/manage/users.json @@ -102,5 +102,7 @@ "alerts.prompt-email": "Emails: ", "alerts.email-sent-to": "An invitation email has been sent to %1", - "alerts.x-users-found": "%1 user(s) found, (%2 seconds)" + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." } \ No newline at end of file diff --git a/public/language/en-GB/notifications.json b/public/language/en-GB/notifications.json index 17d7a23bdf..92811490ff 100644 --- a/public/language/en-GB/notifications.json +++ b/public/language/en-GB/notifications.json @@ -50,6 +50,7 @@ "profile-exported": "%1 profile exported, click to download", "posts-exported": "%1 posts exported, click to download", "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index bba44bf21d..a6f9c887d3 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -13,6 +13,34 @@ define('admin/manage/users', [ ajaxify.go(window.location.pathname + '?' + qs); }); + $('.export-csv').on('click', function () { + socket.once('event:export-users-csv', function () { + app.removeAlert('export-users-start'); + app.alert({ + alert_id: 'export-users', + type: 'success', + title: '[[global:alert.success]]', + message: '[[admin/manage/users:export-users-completed]]', + clickfn: function () { + window.location.href = config.relative_path + '/api/admin/users/csv'; + }, + timeout: 0, + }); + }); + socket.emit('admin.user.exportUsersCSV', {}, function (err) { + if (err) { + return app.alertError(err); + } + app.alert({ + alert_id: 'export-users-start', + message: '[[admin/manage/users:export-users-started]]', + timeout: (ajaxify.data.userCount / 5000) * 500, + }); + }); + + return false; + }); + function getSelectedUids() { var uids = []; diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index ccb7fa1e42..2e5cd2438a 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -1,6 +1,5 @@ 'use strict'; -const nconf = require('nconf'); const validator = require('validator'); const user = require('../../user'); @@ -242,7 +241,7 @@ async function render(req, res, data) { filterBy.forEach(function (filter) { data['filterBy_' + validator.escape(String(filter))] = true; }); - + data.userCount = await db.getObjectField('global', 'userCount'); if (data.adminInviteOnly) { data.showInviteButton = await privileges.users.isAdministrator(req.uid); } else { @@ -252,19 +251,27 @@ async function render(req, res, data) { res.render('admin/manage/users', data); } -usersController.getCSV = async function (req, res) { - var referer = req.headers.referer; - - if (!referer || !referer.replace(nconf.get('url'), '').startsWith('/admin/manage/users')) { - return res.status(403).send('[[error:invalid-origin]]'); - } - events.log({ +usersController.getCSV = async function (req, res, next) { + await events.log({ type: 'getUsersCSV', uid: req.uid, ip: req.ip, }); - const data = await user.getUsersCSV(); - res.attachment('users.csv'); - res.setHeader('Content-Type', 'text/csv'); - res.end(data); + const path = require('path'); + const { baseDir } = require('../../constants').paths; + res.sendFile('users.csv', { + root: path.join(baseDir, 'build/export'), + headers: { + 'Content-Type': 'text/csv', + 'Content-Disposition': 'attachment; filename=users.csv', + }, + }, function (err) { + if (err) { + if (err.code === 'ENOENT') { + res.locals.isAPI = false; + return next(); + } + return next(err); + } + }); }; diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 91dfb49646..bf0325e484 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -1,6 +1,7 @@ 'use strict'; const async = require('async'); +const winston = require('winston'); const db = require('../../database'); const api = require('../../api'); @@ -157,3 +158,27 @@ User.loadGroups = async function (socket, uids) { }); return { users: userData }; }; + +User.exportUsersCSV = async function (socket) { + await events.log({ + type: 'exportUsersCSV', + uid: socket.uid, + ip: socket.ip, + }); + setTimeout(async function () { + try { + await user.exportUsersCSV(); + socket.emit('event:export-users-csv'); + const notifications = require('../../notifications'); + const n = await notifications.create({ + bodyShort: '[[notifications:users-csv-exported]]', + path: '/api/admin/users/csv', + nid: 'users:csv:export', + from: socket.uid, + }); + await notifications.push(n, [socket.uid]); + } catch (err) { + winston.error(err); + } + }, 0); +}; diff --git a/src/user/admin.js b/src/user/admin.js index 981fd921b4..45226bfe85 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -1,9 +1,12 @@ 'use strict'; +const fs = require('fs'); +const path = require('path'); const winston = require('winston'); const validator = require('validator'); +const { baseDir } = require('../constants').paths; const db = require('../database'); const plugins = require('../plugins'); const batch = require('../batch'); @@ -36,11 +39,35 @@ module.exports = function (User) { await batch.processSortedSet('users:joindate', async (uids) => { const usersData = await User.getUsersFields(uids, data.fields); csvContent += usersData.reduce((memo, user) => { - memo += user.email + ',' + user.username + ',' + user.uid + '\n'; + memo += data.fields.map(field => user[field]).join(',') + '\n'; return memo; }, ''); }, {}); return csvContent; }; + + User.exportUsersCSV = async function () { + winston.verbose('[user/exportUsersCSV] Exporting User CSV data'); + + const data = await plugins.hooks.fire('filter:user.csvFields', { fields: ['email', 'username', 'uid'] }); + const fd = await fs.promises.open( + path.join(baseDir, 'build/export', 'users.csv'), + 'w' + ); + fs.promises.appendFile(fd, data.fields.join(',') + '\n'); + await batch.processSortedSet('users:joindate', async (uids) => { + const usersData = await User.getUsersFields(uids, data.fields.slice()); + let line = ''; + usersData.forEach(function (user) { + line += data.fields.map(field => user[field]).join(',') + '\n'; + }); + + await fs.promises.appendFile(fd, line); + }, { + batch: 5000, + interval: 250, + }); + await fd.close(); + }; }; diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index 826ed34411..0cdd099ae2 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -6,7 +6,7 @@ - [[admin/manage/users:download-csv]] + [[admin/manage/users:download-csv]]