fix: #8969, export csv to file

v1.18.x
Barış Soner Uşaklı 4 years ago
parent 007a3258a0
commit 6e6a7a8f8a

@ -102,5 +102,7 @@
"alerts.prompt-email": "Emails: ", "alerts.prompt-email": "Emails: ",
"alerts.email-sent-to": "An invitation email has been sent to %1", "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."
} }

@ -50,6 +50,7 @@
"profile-exported": "<strong>%1</strong> profile exported, click to download", "profile-exported": "<strong>%1</strong> profile exported, click to download",
"posts-exported": "<strong>%1</strong> posts exported, click to download", "posts-exported": "<strong>%1</strong> posts exported, click to download",
"uploads-exported": "<strong>%1</strong> uploads exported, click to download", "uploads-exported": "<strong>%1</strong> uploads exported, click to download",
"users-csv-exported": "Users csv exported, click to download",
"email-confirmed": "Email Confirmed", "email-confirmed": "Email Confirmed",
"email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",

@ -13,6 +13,34 @@ define('admin/manage/users', [
ajaxify.go(window.location.pathname + '?' + qs); 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() { function getSelectedUids() {
var uids = []; var uids = [];

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const nconf = require('nconf');
const validator = require('validator'); const validator = require('validator');
const user = require('../../user'); const user = require('../../user');
@ -242,7 +241,7 @@ async function render(req, res, data) {
filterBy.forEach(function (filter) { filterBy.forEach(function (filter) {
data['filterBy_' + validator.escape(String(filter))] = true; data['filterBy_' + validator.escape(String(filter))] = true;
}); });
data.userCount = await db.getObjectField('global', 'userCount');
if (data.adminInviteOnly) { if (data.adminInviteOnly) {
data.showInviteButton = await privileges.users.isAdministrator(req.uid); data.showInviteButton = await privileges.users.isAdministrator(req.uid);
} else { } else {
@ -252,19 +251,27 @@ async function render(req, res, data) {
res.render('admin/manage/users', data); res.render('admin/manage/users', data);
} }
usersController.getCSV = async function (req, res) { usersController.getCSV = async function (req, res, next) {
var referer = req.headers.referer; await events.log({
if (!referer || !referer.replace(nconf.get('url'), '').startsWith('/admin/manage/users')) {
return res.status(403).send('[[error:invalid-origin]]');
}
events.log({
type: 'getUsersCSV', type: 'getUsersCSV',
uid: req.uid, uid: req.uid,
ip: req.ip, ip: req.ip,
}); });
const data = await user.getUsersCSV(); const path = require('path');
res.attachment('users.csv'); const { baseDir } = require('../../constants').paths;
res.setHeader('Content-Type', 'text/csv'); res.sendFile('users.csv', {
res.end(data); 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);
}
});
}; };

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const async = require('async'); const async = require('async');
const winston = require('winston');
const db = require('../../database'); const db = require('../../database');
const api = require('../../api'); const api = require('../../api');
@ -157,3 +158,27 @@ User.loadGroups = async function (socket, uids) {
}); });
return { users: userData }; 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);
};

@ -1,9 +1,12 @@
'use strict'; 'use strict';
const fs = require('fs');
const path = require('path');
const winston = require('winston'); const winston = require('winston');
const validator = require('validator'); const validator = require('validator');
const { baseDir } = require('../constants').paths;
const db = require('../database'); const db = require('../database');
const plugins = require('../plugins'); const plugins = require('../plugins');
const batch = require('../batch'); const batch = require('../batch');
@ -36,11 +39,35 @@ module.exports = function (User) {
await batch.processSortedSet('users:joindate', async (uids) => { await batch.processSortedSet('users:joindate', async (uids) => {
const usersData = await User.getUsersFields(uids, data.fields); const usersData = await User.getUsersFields(uids, data.fields);
csvContent += usersData.reduce((memo, user) => { 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 memo;
}, ''); }, '');
}, {}); }, {});
return csvContent; 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();
};
}; };

@ -6,7 +6,7 @@
<!-- IF showInviteButton --> <!-- IF showInviteButton -->
<button component="user/invite" class="btn btn-success"><i class="fa fa-users"></i> [[admin/manage/users:invite]]</button> <button component="user/invite" class="btn btn-success"><i class="fa fa-users"></i> [[admin/manage/users:invite]]</button>
<!-- ENDIF showInviteButton --> <!-- ENDIF showInviteButton -->
<a target="_blank" href="{config.relative_path}/api/admin/users/csv" class="btn btn-primary">[[admin/manage/users:download-csv]]</a> <a target="_blank" href="#" class="btn btn-primary export-csv">[[admin/manage/users:download-csv]]</a>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">

Loading…
Cancel
Save