diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json index 08e5701f40..d11670719c 100644 --- a/public/language/en-GB/admin/manage/users.json +++ b/public/language/en-GB/admin/manage/users.json @@ -121,6 +121,28 @@ "alerts.email-sent-to": "An invitation email has been sent to %1", "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", "alerts.select-a-single-user-to-change-email": "Select a single user to change email", + "export": "Export", + "export-users-fields-title": "Select CSV Fields", + "export-field-email": "Email", + "export-field-username": "Username", + "export-field-uid": "UID", + "export-field-ip": "IP", + "export-field-joindate": "Join date", + "export-field-lastonline": "Last Online", + "export-field-lastposttime": "Last Post Time", + "export-field-reputation": "Reputation", + "export-field-postcount": "Post Count", + "export-field-topiccount": "Topic Count", + "export-field-profileviews": "Profile Views", + "export-field-followercount": "Follower Count", + "export-field-followingcount": "Following Count", + "export-field-fullname": "Full Name", + "export-field-website": "Website", + "export-field-location": "Location", + "export-field-birthday": "Birthday", + "export-field-signature": "Signature", + "export-field-aboutme": "About Me", + "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.", "email": "Email", diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index 16a4918d63..740a8478dc 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -27,15 +27,60 @@ define('admin/manage/users', [ timeout: 0, }); }); - socket.emit('admin.user.exportUsersCSV', {}, function (err) { - if (err) { - return alerts.error(err); - } - alerts.alert({ - alert_id: 'export-users-start', - message: '[[admin/manage/users:export-users-started]]', - timeout: (ajaxify.data.userCount / 5000) * 500, - }); + + const defaultFields = [ + { label: '[[admin/manage/users:export-field-email]]', field: 'email', selected: true }, + { label: '[[admin/manage/users:export-field-username]]', field: 'username', selected: true }, + { label: '[[admin/manage/users:export-field-uid]]', field: 'uid', selected: true }, + { label: '[[admin/manage/users:export-field-ip]]', field: 'ip', selected: true }, + { label: '[[admin/manage/users:export-field-joindate]]', field: 'joindate', selected: false }, + { label: '[[admin/manage/users:export-field-lastonline]]', field: 'lastonline', selected: false }, + { label: '[[admin/manage/users:export-field-lastposttime]]', field: 'lastposttime', selected: false }, + { label: '[[admin/manage/users:export-field-reputation]]', field: 'reputation', selected: false }, + { label: '[[admin/manage/users:export-field-postcount]]', field: 'postcount', selected: false }, + { label: '[[admin/manage/users:export-field-topiccount]]', field: 'topiccount', selected: false }, + { label: '[[admin/manage/users:export-field-profileviews]]', field: 'profileviews', selected: false }, + { label: '[[admin/manage/users:export-field-followercount]]', field: 'followerCount', selected: false }, + { label: '[[admin/manage/users:export-field-followingcount]]', field: 'followingCount', selected: false }, + { label: '[[admin/manage/users:export-field-fullname]]', field: 'fullname', selected: false }, + { label: '[[admin/manage/users:export-field-website]]', field: 'website', selected: false }, + { label: '[[admin/manage/users:export-field-location]]', field: 'location', selected: false }, + { label: '[[admin/manage/users:export-field-birthday]]', field: 'birthday', selected: false }, + { label: '[[admin/manage/users:export-field-signature]]', field: 'signature', selected: false }, + { label: '[[admin/manage/users:export-field-aboutme]]', field: 'aboutme', selected: false }, + ]; + const options = defaultFields.map((field, i) => (` +
+ + +
` + )).join(''); + + const modal = bootbox.dialog({ + message: options, + title: '[[admin/manage/users:export-users-fields-title]]', + buttons: { + submit: { + label: '[[admin/manage/users:export]]', + callback: function () { + const fields = modal.find('[data-field]').filter( + (index, el) => $(el).is(':checked') + ).map((index, el) => $(el).attr('data-field')).get(); + socket.emit('admin.user.exportUsersCSV', { fields }, function (err) { + if (err) { + return alerts.error(err); + } + alerts.alert({ + alert_id: 'export-users-start', + message: '[[admin/manage/users:export-users-started]]', + timeout: Math.max(5000, (ajaxify.data.userCount / 5000) * 500), + }); + }); + }, + }, + }, }); return false; diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 6c5e0c5b28..45e91caaa6 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -162,7 +162,7 @@ User.setReputation = async function (socket, data) { ]); }; -User.exportUsersCSV = async function (socket) { +User.exportUsersCSV = async function (socket, data) { await events.log({ type: 'exportUsersCSV', uid: socket.uid, @@ -170,7 +170,7 @@ User.exportUsersCSV = async function (socket) { }); setTimeout(async () => { try { - await user.exportUsersCSV(); + await user.exportUsersCSV(data.fields); if (socket.emit) { socket.emit('event:export-users-csv'); } diff --git a/src/user/admin.js b/src/user/admin.js index b29adcf86f..369aafee50 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -5,6 +5,7 @@ const fs = require('fs'); const path = require('path'); const winston = require('winston'); const validator = require('validator'); +const json2csvAsync = require('json2csv').parseAsync; const { baseDir } = require('../constants').paths; const db = require('../database'); @@ -47,41 +48,39 @@ module.exports = function (User) { return csvContent; }; - User.exportUsersCSV = async function () { + User.exportUsersCSV = async function (fieldsToExport = ['email', 'username', 'uid', 'ip']) { winston.verbose('[user/exportUsersCSV] Exporting User CSV data'); const { fields, showIps } = await plugins.hooks.fire('filter:user.csvFields', { - fields: ['email', 'username', 'uid'], - showIps: true, + fields: fieldsToExport, + showIps: fieldsToExport.includes('ip'), }); + + if (!showIps && fields.includes('ip')) { + fields.splice(fields.indexOf('ip'), 1); + } const fd = await fs.promises.open( path.join(baseDir, 'build/export', 'users.csv'), 'w' ); - fs.promises.appendFile(fd, `${fields.join(',')}${showIps ? ',ip' : ''}\n`); - await batch.processSortedSet('users:joindate', async (uids) => { - const usersData = await User.getUsersFields(uids, fields.slice()); - let userIPs = ''; - let ips = []; - + fs.promises.appendFile(fd, `${fields.map(f => `"${f}"`).join(',')}\n`); + await batch.processSortedSet('group:administrators:members', async (uids) => { + const userFieldsToLoad = fields.filter(field => field !== 'ip' && field !== 'password'); + const usersData = await User.getUsersFields(uids, userFieldsToLoad); + let userIps = []; if (showIps) { - ips = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:ip`)); + userIps = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:ip`)); } - let line = ''; usersData.forEach((user, index) => { - line += `${fields - .map(field => (isFinite(user[field]) ? `'${user[field]}'` : user[field])) - .join(',')}`; - if (showIps) { - userIPs = ips[index] ? ips[index].join(',') : ''; - line += `,"${userIPs}"\n`; - } else { - line += '\n'; + if (Array.isArray(userIps[index])) { + user.ip = userIps[index].join(','); } }); - await fs.promises.appendFile(fd, line); + const opts = { fields, header: false }; + const csv = await json2csvAsync(usersData, opts); + await fs.promises.appendFile(fd, csv); }, { batch: 5000, interval: 250,