From 245e1a9159a8e66a00cc5a14c6bff69463a36b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 28 Feb 2018 16:32:39 -0500 Subject: [PATCH 01/42] closes #6334 --- install/package.json | 4 ++-- public/src/sockets.js | 40 +++++++++++++++++++++++----------------- src/middleware/header.js | 2 +- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/install/package.json b/install/package.json index b719934cc8..d6441ffc07 100644 --- a/install/package.json +++ b/install/package.json @@ -73,9 +73,9 @@ "nodebb-plugin-spam-be-gone": "0.5.3", "nodebb-rewards-essentials": "0.0.11", "nodebb-theme-lavender": "5.0.3", - "nodebb-theme-persona": "7.2.25", + "nodebb-theme-persona": "7.2.26", "nodebb-theme-slick": "1.1.4", - "nodebb-theme-vanilla": "8.1.10", + "nodebb-theme-vanilla": "8.1.11", "nodebb-widget-essentials": "4.0.2", "nodemailer": "4.4.1", "passport": "^0.4.0", diff --git a/public/src/sockets.js b/public/src/sockets.js index 1aaf02ca53..0a0c295217 100644 --- a/public/src/sockets.js +++ b/public/src/sockets.js @@ -17,30 +17,36 @@ app.isConnected = false; socket = io(config.websocketAddress, ioParams); - socket.on('connect', onConnect); + if (parseInt(app.user.uid, 10) >= 0) { + addHandlers(); + } - socket.on('reconnecting', onReconnecting); + function addHandlers() { + socket.on('connect', onConnect); - socket.on('disconnect', onDisconnect); + socket.on('reconnecting', onReconnecting); - socket.on('reconnect_failed', function () { - // Wait ten times the reconnection delay and then start over - setTimeout(socket.connect.bind(socket), parseInt(config.reconnectionDelay, 10) * 10); - }); + socket.on('disconnect', onDisconnect); - socket.on('checkSession', function (uid) { - if (parseInt(uid, 10) !== parseInt(app.user.uid, 10)) { - app.handleInvalidSession(); - } - }); + socket.on('reconnect_failed', function () { + // Wait ten times the reconnection delay and then start over + setTimeout(socket.connect.bind(socket), parseInt(config.reconnectionDelay, 10) * 10); + }); - socket.on('setHostname', function (hostname) { - app.upstreamHost = hostname; - }); + socket.on('checkSession', function (uid) { + if (parseInt(uid, 10) !== parseInt(app.user.uid, 10)) { + app.handleInvalidSession(); + } + }); - socket.on('event:banned', onEventBanned); + socket.on('setHostname', function (hostname) { + app.upstreamHost = hostname; + }); - socket.on('event:alert', app.alert); + socket.on('event:banned', onEventBanned); + + socket.on('event:alert', app.alert); + } function onConnect() { app.isConnected = true; diff --git a/src/middleware/header.js b/src/middleware/header.js index c769991753..00f01b1aba 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -251,7 +251,7 @@ module.exports = function (middleware) { data.templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1; data.templateValues.customJS = data.templateValues.useCustomJS ? meta.config.customJS : ''; - + data.templateValues.isSpider = req.isSpider(); req.app.render('footer', data.templateValues, next); }, ], callback); From e1c6761087c3d48f2bb0bf8e30f07a90e73996fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 28 Feb 2018 17:38:31 -0500 Subject: [PATCH 02/42] closes #6258 --- install/package.json | 2 +- src/controllers/accounts/edit.js | 14 +++++++++++--- src/install.js | 2 +- src/posts/user.js | 8 +++++++- src/privileges/global.js | 2 ++ src/upgrades/1.8.0/give_signature_privileges.js | 11 +++++++++++ test/categories.js | 2 ++ test/mocks/databasemock.js | 2 +- 8 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 src/upgrades/1.8.0/give_signature_privileges.js diff --git a/install/package.json b/install/package.json index d6441ffc07..d2739fe6ad 100644 --- a/install/package.json +++ b/install/package.json @@ -73,7 +73,7 @@ "nodebb-plugin-spam-be-gone": "0.5.3", "nodebb-rewards-essentials": "0.0.11", "nodebb-theme-lavender": "5.0.3", - "nodebb-theme-persona": "7.2.26", + "nodebb-theme-persona": "7.2.27", "nodebb-theme-slick": "1.1.4", "nodebb-theme-vanilla": "8.1.11", "nodebb-widget-essentials": "4.0.2", diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 08f5edfaf3..3402c3052a 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -17,9 +17,17 @@ var editController = module.exports; editController.get = function (req, res, callback) { async.waterfall([ function (next) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); + async.parallel({ + userData: function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); + }, + canUseSignature: function (next) { + privileges.global.can('signature', req.uid, next); + }, + }, next); }, - function (userData, next) { + function (results, next) { + var userData = results.userData; if (!userData) { return callback(); } @@ -30,7 +38,7 @@ editController.get = function (req, res, callback) { userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1; userData.allowWebsite = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:website'], 10) || 0); userData.allowAboutMe = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:aboutme'], 10) || 0); - userData.allowSignature = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:signature'], 10) || 0); + userData.allowSignature = results.canUseSignature && (!userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:signature'], 10) || 0)); userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 200; userData.defaultAvatar = user.getDefaultAvatar(); diff --git a/src/install.js b/src/install.js index 33bd5a58d1..9113103c52 100644 --- a/src/install.js +++ b/src/install.js @@ -356,7 +356,7 @@ function createGlobalModeratorsGroup(next) { function giveGlobalPrivileges(next) { var privileges = require('./privileges'); - privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next); + privileges.global.give(['chat', 'upload:post:image', 'signature'], 'registered-users', next); } function createCategories(next) { diff --git a/src/posts/user.js b/src/posts/user.js index 2ada0b2dd3..8fd1e08bb0 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -7,12 +7,14 @@ var user = require('../user'); var groups = require('../groups'); var meta = require('../meta'); var plugins = require('../plugins'); +var privileges = require('../privileges'); module.exports = function (Posts) { Posts.getUserInfoForPosts = function (uids, uid, callback) { var groupsMap = {}; var userData; var userSettings; + var canUseSignature; async.waterfall([ function (next) { async.parallel({ @@ -22,11 +24,15 @@ module.exports = function (Posts) { userSettings: function (next) { user.getMultipleUserSettings(uids, next); }, + canUseSignature: function (next) { + privileges.global.can('signature', uid, next); + }, }, next); }, function (results, next) { userData = results.userData; userSettings = results.userSettings; + canUseSignature = results.canUseSignature; var groupTitles = userData.map(function (userData) { return userData && userData.groupTitle; }).filter(function (groupTitle, index, array) { @@ -74,7 +80,7 @@ module.exports = function (Posts) { groups.isMember(userData.uid, userData.groupTitle, next); }, signature: function (next) { - if (!userData.signature || parseInt(meta.config.disableSignatures, 10) === 1) { + if (!userData.signature || !canUseSignature || parseInt(meta.config.disableSignatures, 10) === 1) { userData.signature = ''; return next(); } diff --git a/src/privileges/global.js b/src/privileges/global.js index f1f88c4fff..3f2955d5be 100644 --- a/src/privileges/global.js +++ b/src/privileges/global.js @@ -16,12 +16,14 @@ module.exports = function (privileges) { { name: 'Chat' }, { name: 'Upload Images' }, { name: 'Upload Files' }, + { name: 'Signature' }, ]; privileges.global.userPrivilegeList = [ 'chat', 'upload:post:image', 'upload:post:file', + 'signature', ]; privileges.global.groupPrivilegeList = privileges.global.userPrivilegeList.map(function (privilege) { diff --git a/src/upgrades/1.8.0/give_signature_privileges.js b/src/upgrades/1.8.0/give_signature_privileges.js new file mode 100644 index 0000000000..87d93605f5 --- /dev/null +++ b/src/upgrades/1.8.0/give_signature_privileges.js @@ -0,0 +1,11 @@ +'use strict'; + +var privileges = require('../../privileges'); + +module.exports = { + name: 'Give registered users signature privilege', + timestamp: Date.UTC(2018, 1, 28), + method: function (callback) { + privileges.global.give(['signature'], 'registered-users', callback); + }, +}; diff --git a/test/categories.js b/test/categories.js index 90198f91b4..d0d446942b 100644 --- a/test/categories.js +++ b/test/categories.js @@ -668,6 +668,7 @@ describe('Categories', function () { chat: false, 'upload:post:image': false, 'upload:post:file': false, + signature: false, }); done(); @@ -704,6 +705,7 @@ describe('Categories', function () { 'groups:chat': true, 'groups:upload:post:image': true, 'groups:upload:post:file': false, + 'groups:signature': true, }); done(); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 0bef41c466..6553363dc4 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -202,7 +202,7 @@ function setupDefaultConfigs(meta, next) { function giveDefaultGlobalPrivileges(next) { var privileges = require('../../src/privileges'); - privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next); + privileges.global.give(['chat', 'upload:post:image', 'signature'], 'registered-users', next); } function enableDefaultPlugins(callback) { From db2cd527a19ec2869f9b93aefea11cd5fadd0323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 1 Mar 2018 09:38:16 -0500 Subject: [PATCH 03/42] up vanilla --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d2739fe6ad..587f4c8485 100644 --- a/install/package.json +++ b/install/package.json @@ -75,7 +75,7 @@ "nodebb-theme-lavender": "5.0.3", "nodebb-theme-persona": "7.2.27", "nodebb-theme-slick": "1.1.4", - "nodebb-theme-vanilla": "8.1.11", + "nodebb-theme-vanilla": "8.1.12", "nodebb-widget-essentials": "4.0.2", "nodemailer": "4.4.1", "passport": "^0.4.0", From a126f07471c881beddbafe0e49ebcf1ae820a5b5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 9 Mar 2018 12:57:52 -0500 Subject: [PATCH 04/42] closes #6043 --- public/language/en-GB/flags.json | 1 + src/controllers/mods.js | 14 +++++++++--- src/flags.js | 38 +++++++++++++++++++++++--------- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json index d05a5b25a8..35fc87011a 100644 --- a/public/language/en-GB/flags.json +++ b/public/language/en-GB/flags.json @@ -18,6 +18,7 @@ "filter-type": "Flag Type", "filter-type-all": "All Content", "filter-type-post": "Post", + "filter-type-user": "User", "filter-state": "State", "filter-assignee": "Assignee UID", "filter-cid": "Category", diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 69bfd7fbbf..52403a5950 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -7,15 +7,17 @@ var categories = require('../categories'); var flags = require('../flags'); var analytics = require('../analytics'); var plugins = require('../plugins'); -var adminPostQueueController = require('./admin/postqueue'); +var pagination = require('../pagination'); +var adminPostQueueController = require('./admin/postqueue'); var modsController = module.exports; modsController.flags = {}; modsController.flags.list = function (req, res, next) { var filters; var hasFilter; - var validFilters = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick']; + var validFilters = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick', 'page', 'perPage']; + async.waterfall([ function (next) { async.parallel({ @@ -62,6 +64,11 @@ modsController.flags.list = function (req, res, next) { } } + // Pagination doesn't count as a filter + if (Object.keys(filters).length === 2 && filters.hasOwnProperty('page') && filters.hasOwnProperty('perPage')) { + hasFilter = false; + } + async.parallel({ flags: async.apply(flags.list, filters, req.uid), analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30), @@ -92,12 +99,13 @@ modsController.flags.list = function (req, res, next) { }, {}); res.render('flags/list', { - flags: data.flags, + flags: data.flags.flags, analytics: data.analytics, categories: data.categories, hasFilter: hasFilter, filters: filters, title: '[[pages:flags]]', + pagination: pagination.create(data.flags.page, data.flags.pageCount, req.query), }); }, ], next); diff --git a/src/flags.js b/src/flags.js index 8ea298b8e8..d508568329 100644 --- a/src/flags.js +++ b/src/flags.js @@ -51,6 +51,8 @@ Flags.init = function (callback) { cid: function (sets, orSets, key) { prepareSets(sets, orSets, 'flags:byCid:', key); }, + page: function () { /* noop */ }, + perPage: function () { /* noop */ }, quick: function (sets, orSets, key, uid) { switch (key) { case 'mine': @@ -121,14 +123,16 @@ Flags.list = function (filters, uid, callback) { var sets = []; var orSets = []; - if (Object.keys(filters).length > 0) { - for (var type in filters) { - if (filters.hasOwnProperty(type)) { - if (Flags._filters.hasOwnProperty(type)) { - Flags._filters[type](sets, orSets, filters[type], uid); - } else { - winston.warn('[flags/list] No flag filter type found: ' + type); - } + // Default filter + filters.page = filters.hasOwnProperty('page') ? Math.abs(parseInt(filters.page, 10) || 1) : 1; + filters.perPage = filters.hasOwnProperty('perPage') ? Math.abs(parseInt(filters.perPage, 10) || 20) : 20; + + for (var type in filters) { + if (filters.hasOwnProperty(type)) { + if (Flags._filters.hasOwnProperty(type)) { + Flags._filters[type](sets, orSets, filters[type], uid); + } else { + winston.warn('[flags/list] No flag filter type found: ' + type); } } } @@ -165,6 +169,11 @@ Flags.list = function (filters, uid, callback) { } }, function (flagIds, next) { + // Create subset for parsing based on page number (n=20) + const flagsPerPage = Math.abs(parseInt(filters.perPage, 10) || 1); + const pageCount = Math.ceil(flagIds.length / flagsPerPage); + flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage); + async.map(flagIds, function (flagId, next) { async.waterfall([ async.apply(db.getObject, 'flag:' + flagId), @@ -206,13 +215,20 @@ Flags.list = function (filters, uid, callback) { datetimeISO: new Date(parseInt(flagObj.datetime, 10)).toISOString(), })); }); - }, next); + }, function (err, flags) { + next(err, flags, pageCount); + }); }, - function (flags, next) { + function (flags, pageCount, next) { plugins.fireHook('filter:flags.list', { flags: flags, + page: filters.page, }, function (err, data) { - next(err, data.flags); + next(err, { + flags: data.flags, + page: data.page, + pageCount: pageCount, + }); }); }, ], callback); From 9ea6dbc0e798d65bfccc67ffaca0487065c18e2c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 9 Mar 2018 14:31:59 -0500 Subject: [PATCH 05/42] fix flag tests --- test/flags.js | 78 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/test/flags.js b/test/flags.js index 6dbcd5e08c..31672844bf 100644 --- a/test/flags.js +++ b/test/flags.js @@ -155,15 +155,18 @@ describe('Flags', function () { describe('.list()', function () { it('should show a list of flags (with one item)', function (done) { - Flags.list({}, 1, function (err, flags) { + Flags.list({}, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.equal(flags.length, 1); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.equal(payload.flags.length, 1); - Flags.get(flags[0].flagId, function (err, flagData) { + Flags.get(payload.flags[0].flagId, function (err, flagData) { assert.ifError(err); - assert.equal(flags[0].flagId, flagData.flagId); - assert.equal(flags[0].description, flagData.description); + assert.equal(payload.flags[0].flagId, flagData.flagId); + assert.equal(payload.flags[0].description, flagData.description); done(); }); }); @@ -173,10 +176,13 @@ describe('Flags', function () { it('should return a filtered list of flags if said filters are passed in', function (done) { Flags.list({ state: 'open', - }, 1, function (err, flags) { + }, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.strictEqual(1, parseInt(flags[0].flagId, 10)); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.strictEqual(1, parseInt(payload.flags[0].flagId, 10)); done(); }); }); @@ -184,10 +190,13 @@ describe('Flags', function () { it('should return no flags if a filter with no matching flags is used', function (done) { Flags.list({ state: 'rejected', - }, 1, function (err, flags) { + }, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.strictEqual(0, flags.length); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.strictEqual(0, payload.flags.length); done(); }); }); @@ -195,10 +204,13 @@ describe('Flags', function () { it('should return a flag when filtered by cid 1', function (done) { Flags.list({ cid: 1, - }, 1, function (err, flags) { + }, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.strictEqual(1, flags.length); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.strictEqual(1, payload.flags.length); done(); }); }); @@ -206,10 +218,13 @@ describe('Flags', function () { it('shouldn\'t return a flag when filtered by cid 2', function (done) { Flags.list({ cid: 2, - }, 1, function (err, flags) { + }, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.strictEqual(0, flags.length); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.strictEqual(0, payload.flags.length); done(); }); }); @@ -217,10 +232,13 @@ describe('Flags', function () { it('should return a flag when filtered by both cid 1 and 2', function (done) { Flags.list({ cid: [1, 2], - }, 1, function (err, flags) { + }, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.strictEqual(1, flags.length); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.strictEqual(1, payload.flags.length); done(); }); }); @@ -229,10 +247,13 @@ describe('Flags', function () { Flags.list({ cid: [1, 2], state: 'open', - }, 1, function (err, flags) { + }, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.strictEqual(1, flags.length); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.strictEqual(1, payload.flags.length); done(); }); }); @@ -241,10 +262,13 @@ describe('Flags', function () { Flags.list({ cid: [1, 2], state: 'resolved', - }, 1, function (err, flags) { + }, 1, function (err, payload) { assert.ifError(err); - assert.ok(Array.isArray(flags)); - assert.strictEqual(0, flags.length); + assert.ok(payload.hasOwnProperty('flags')); + assert.ok(payload.hasOwnProperty('page')); + assert.ok(payload.hasOwnProperty('pageCount')); + assert.ok(Array.isArray(payload.flags)); + assert.strictEqual(0, payload.flags.length); done(); }); }); From 4d9e2b11603f8234f17fc0a120dad5e0a59a5e09 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 Mar 2018 12:50:12 -0400 Subject: [PATCH 06/42] fixes #6354 --- public/src/modules/chat.js | 5 +++++ public/src/modules/notifications.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 5791a7828f..7dc3015c05 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -18,6 +18,7 @@ define('chat', [ module.prepareDOM = function () { var chatsToggleEl = components.get('chat/dropdown'); var chatsListEl = components.get('chat/list'); + var chatsDropdownWrapper = chatsToggleEl.parents('.dropdown'); chatsToggleEl.on('click', function () { if (chatsToggleEl.parent().hasClass('open')) { @@ -27,6 +28,10 @@ define('chat', [ module.loadChatsDropdown(chatsListEl); }); + if (chatsDropdownWrapper.hasClass('open')) { + module.loadChatsDropdown(chatsListEl); + } + chatsListEl.on('click', '[data-roomid]', function (ev) { if ($(ev.target).parents('.user-link').length) { return; diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 92574f3eb1..937f625738 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -10,6 +10,7 @@ define('notifications', ['sounds', 'translator', 'components', 'navigator', 'ben var notifContainer = components.get('notifications'); var notifTrigger = notifContainer.children('a'); var notifList = components.get('notifications/list'); + var notifDropdownWrapper = notifTrigger.parents('.dropdown'); notifTrigger.on('click', function (e) { e.preventDefault(); @@ -20,6 +21,10 @@ define('notifications', ['sounds', 'translator', 'components', 'navigator', 'ben Notifications.loadNotifications(notifList); }); + if (notifDropdownWrapper.hasClass('open')) { + Notifications.loadNotifications(notifList); + } + notifList.on('click', '[data-nid]', function (ev) { var notifEl = $(this); if (scrollToPostIndexIfOnPage(notifEl)) { From 3b57d3eadb1888e347ec153aaf85210ba72eefa6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 5 Apr 2018 14:35:49 -0400 Subject: [PATCH 07/42] closes #6434 --- src/plugins/hooks.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js index c555424377..9133378ace 100644 --- a/src/plugins/hooks.js +++ b/src/plugins/hooks.js @@ -12,20 +12,24 @@ module.exports = function (Plugins) { 'action:flag.create': 'action:flags.create', 'action:flag.update': 'action:flags.update', }; - /* - `data` is an object consisting of (* is required): - `data.hook`*, the name of the NodeBB hook - `data.method`*, the method called in that plugin - `data.priority`, the relative priority of the method when it is eventually called (default: 10) - */ - Plugins.registerHook = function (id, data, callback) { - callback = callback || function () {}; - function register() { + + Plugins.internals = { + _register: function (data, callback) { Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || []; Plugins.loadedHooks[data.hook].push(data); callback(); - } + }, + }; + + /* + `data` is an object consisting of (* is required): + `data.hook`*, the name of the NodeBB hook + `data.method`*, the method called in that plugin (can be an array of functions) + `data.priority`, the relative priority of the method when it is eventually called (default: 10) + */ + Plugins.registerHook = function (id, data, callback) { + callback = callback || function () {}; if (!data.hook) { winston.warn('[plugins/' + id + '] registerHook called with invalid data.hook', data); @@ -48,7 +52,13 @@ module.exports = function (Plugins) { data.priority = 10; } - if (typeof data.method === 'string' && data.method.length > 0) { + if (Array.isArray(data.method) && data.method.every(method => typeof method === 'function' || typeof method === 'string')) { + // Go go gadget recursion! + async.eachSeries(data.method, function (method, next) { + const singularData = Object.assign({}, data, { method: method }); + Plugins.registerHook(id, singularData, next); + }, callback); + } else if (typeof data.method === 'string' && data.method.length > 0) { method = data.method.split('.').reduce(function (memo, prop) { if (memo && memo[prop]) { return memo[prop]; @@ -60,9 +70,9 @@ module.exports = function (Plugins) { // Write the actual method reference to the hookObj data.method = method; - register(); + Plugins.internals._register(data, callback); } else if (typeof data.method === 'function') { - register(); + Plugins.internals._register(data, callback); } else { winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method); return callback(); From 47606c8df55f04c03820a2d2846359b47e09bb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 5 Apr 2018 16:46:32 -0400 Subject: [PATCH 08/42] merge --- .../language/en-GB/admin/settings/group.json | 1 + public/src/client/account/edit.js | 2 ++ src/controllers/accounts/edit.js | 7 ++++- src/controllers/topics.js | 1 + src/posts/user.js | 30 +++++++++++++------ src/user/data.js | 18 ++++++++++- src/views/admin/settings/group.tpl | 11 +++++++ 7 files changed, 59 insertions(+), 11 deletions(-) diff --git a/public/language/en-GB/admin/settings/group.json b/public/language/en-GB/admin/settings/group.json index 1ae88c9cf5..fe3e39915b 100644 --- a/public/language/en-GB/admin/settings/group.json +++ b/public/language/en-GB/admin/settings/group.json @@ -5,6 +5,7 @@ "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", "allow-creation": "Allow Group Creation", "allow-creation-help": "If enabled, users can create groups (Default: disabled)", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", "max-name-length": "Maximum Group Name Length", "cover-image": "Group Cover Image", "default-cover": "Default Cover Images", diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index 8b270a8fd2..9956c659c6 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -37,6 +37,8 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components' aboutme: $('#inputAboutMe').val(), }; + userData.groupTitle = JSON.stringify(Array.isArray(userData.groupTitle) ? userData.groupTitle : [userData.groupTitle]); + $(window).trigger('action:profile.update', userData); socket.emit('user.updateProfile', userData, function (err, data) { diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 3402c3052a..af92cdc7bc 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -35,6 +35,7 @@ editController.get = function (req, res, callback) { userData.maximumAboutMeLength = parseInt(meta.config.maximumAboutMeLength, 10) || 1000; userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10); userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads, 10) === 1; + userData.allowMultipleBadges = parseInt(meta.config.allowMultipleBadges, 10) === 1; userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1; userData.allowWebsite = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:website'], 10) || 0); userData.allowAboutMe = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:aboutme'], 10) || 0); @@ -45,8 +46,12 @@ editController.get = function (req, res, callback) { userData.groups = userData.groups.filter(function (group) { return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users'; }); + + if (!userData.allowMultipleBadges) { + userData.groupTitle = userData.groupTitleArray[0]; + } userData.groups.forEach(function (group) { - group.selected = group.name === userData.groupTitle; + group.selected = userData.groupTitleArray.includes(group.name); }); userData.title = '[[pages:account/edit, ' + userData.username + ']]'; diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 9e89a1e929..40299ceb9b 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -145,6 +145,7 @@ topicsController.get = function (req, res, callback) { topicData.postEditDuration = parseInt(meta.config.postEditDuration, 10) || 0; topicData.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0; topicData.scrollToMyPost = settings.scrollToMyPost; + topicData.allowMultipleBadges = parseInt(meta.config.allowMultipleBadges, 10) === 1; topicData.rssFeedUrl = nconf.get('relative_path') + '/topic/' + topicData.tid + '.rss'; if (req.loggedIn) { topicData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; diff --git a/src/posts/user.js b/src/posts/user.js index 8fd1e08bb0..e0df76c2a3 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -2,6 +2,7 @@ var async = require('async'); var validator = require('validator'); +var _ = require('lodash'); var user = require('../user'); var groups = require('../groups'); @@ -15,11 +16,16 @@ module.exports = function (Posts) { var userData; var userSettings; var canUseSignature; + var allowMultipleBadges = parseInt(meta.config.allowMultipleBadges, 10) === 1; async.waterfall([ function (next) { async.parallel({ userData: function (next) { - user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status', 'lastonline', 'groupTitle'], next); + user.getUsersFields(uids, [ + 'uid', 'username', 'fullname', 'userslug', + 'reputation', 'postcount', 'picture', 'signature', + 'banned', 'status', 'lastonline', 'groupTitle', + ], next); }, userSettings: function (next) { user.getMultipleUserSettings(uids, next); @@ -34,10 +40,10 @@ module.exports = function (Posts) { userSettings = results.userSettings; canUseSignature = results.canUseSignature; var groupTitles = userData.map(function (userData) { - return userData && userData.groupTitle; - }).filter(function (groupTitle, index, array) { - return groupTitle && array.indexOf(groupTitle) === index; + return userData && userData.groupTitleArray; }); + groupTitles = _.uniq(_.flatten(groupTitles)); + groups.getGroupsData(groupTitles, next); }, function (groupsData, next) { @@ -64,6 +70,8 @@ module.exports = function (Posts) { userData.status = user.getStatus(userData); userData.signature = validator.escape(String(userData.signature || '')); userData.fullname = userSettings[index].showfullname ? validator.escape(String(userData.fullname || '')) : undefined; + userData.selectedGroups = []; + if (parseInt(meta.config.hideFullname, 10) === 1) { userData.fullname = undefined; } @@ -73,11 +81,11 @@ module.exports = function (Posts) { async.waterfall([ function (next) { async.parallel({ - isMemberOfGroup: function (next) { - if (!userData.groupTitle || !groupsMap[userData.groupTitle]) { + isMemberOfGroups: function (next) { + if (!Array.isArray(userData.groupTitleArray) || !userData.groupTitleArray.length) { return next(); } - groups.isMember(userData.uid, userData.groupTitle, next); + groups.isMemberOfGroups(userData.uid, userData.groupTitleArray, next); }, signature: function (next) { if (!userData.signature || !canUseSignature || parseInt(meta.config.disableSignatures, 10) === 1) { @@ -92,8 +100,12 @@ module.exports = function (Posts) { }, next); }, function (results, next) { - if (results.isMemberOfGroup && userData.groupTitle && groupsMap[userData.groupTitle]) { - userData.selectedGroup = groupsMap[userData.groupTitle]; + if (results.isMemberOfGroups && userData.groupTitleArray) { + userData.groupTitleArray.forEach(function (userGroup, index) { + if (results.isMemberOfGroups[index] && groupsMap[userGroup]) { + userData.selectedGroups.push(groupsMap[userGroup]); + } + }); } userData.custom_profile_info = results.customProfileInfo.profile; diff --git a/src/user/data.js b/src/user/data.js index db41509aa0..bec2f2a216 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -135,7 +135,9 @@ module.exports = function (User) { if (!user) { return; } - + if (user.hasOwnProperty('groupTitle')) { + parseGroupTitle(user); + } if (user.hasOwnProperty('username')) { user.username = validator.escape(user.username ? user.username.toString() : ''); } @@ -192,6 +194,20 @@ module.exports = function (User) { plugins.fireHook('filter:users.get', users, callback); } + function parseGroupTitle(user) { + try { + user.groupTitleArray = JSON.parse(user.groupTitle); + } catch (err) { + user.groupTitleArray = [user.groupTitle]; + } + if (!Array.isArray(user.groupTitleArray)) { + user.groupTitleArray = [user.groupTitleArray]; + } + if (parseInt(meta.config.allowMultipleBadges, 10) !== 1) { + user.groupTitleArray = [user.groupTitleArray[0]]; + } + } + User.getDefaultAvatar = function () { if (!meta.config.defaultAvatar) { return ''; diff --git a/src/views/admin/settings/group.tpl b/src/views/admin/settings/group.tpl index fd696cb5ad..6f267baf57 100644 --- a/src/views/admin/settings/group.tpl +++ b/src/views/admin/settings/group.tpl @@ -29,6 +29,17 @@ [[admin/settings/group:allow-creation-help]]

+
+ +
+ +

+ [[admin/settings/group:allow-multiple-badges-help]] +

+ From 13a3f44ed306024876ae402564da4588fba8ca99 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 5 Apr 2018 16:53:05 -0400 Subject: [PATCH 09/42] closes #6432 --- public/language/en-GB/register.json | 11 +++++- src/user.js | 59 ++++++++++++++++++++--------- src/user/create.js | 1 + src/views/partials/gdpr_consent.tpl | 23 +++++++++++ 4 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 src/views/partials/gdpr_consent.tpl diff --git a/public/language/en-GB/register.json b/public/language/en-GB/register.json index 81b20421d4..0611f4b878 100644 --- a/public/language/en-GB/register.json +++ b/public/language/en-GB/register.json @@ -19,5 +19,14 @@ "terms_of_use_error": "You must agree to the Terms of Use", "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", "interstitial.intro": "We require some additional information before we can create your account.", - "interstitial.errors-found": "We could not complete your registration:" + "interstitial.errors-found": "We could not complete your registration:", + + "gdpr_lead": "This community forum collects and processes your personal information.", + "gdpr_intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your user settings page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "gdpr_email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email.", + "gdpr_digest_frequency": "By default, this community delivers email digests every %1.", + "gdpr_digest_off": "Currently, this community does not send out email digests", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails." } \ No newline at end of file diff --git a/src/user.js b/src/user.js index caf47fdcb9..f81c08f280 100644 --- a/src/user.js +++ b/src/user.js @@ -348,25 +348,50 @@ User.getModeratedCids = function (uid, callback) { User.addInterstitials = function (callback) { plugins.registerHook('core', { hook: 'filter:register.interstitial', - method: function (data, callback) { - if (meta.config.termsOfUse && !data.userData.acceptTos) { - data.interstitials.push({ - template: 'partials/acceptTos', - data: { - termsOfUse: meta.config.termsOfUse, - }, - callback: function (userData, formData, next) { - if (formData['agree-terms'] === 'on') { - userData.acceptTos = true; - } + method: [ + // GDPR information collection/processing consent + email consent + function (data, callback) { + if (!data.userData.gdpr_consent) { + data.interstitials.push({ + template: 'partials/gdpr_consent', + data: { + digestFrequency: meta.config.dailyDigestFreq, + digestEnabled: meta.config.dailyDigestFreq !== 'off', + }, + callback: function (userData, formData, next) { + if (formData.gdpr_agree_data === 'on' && formData.gdpr_agree_email === 'on') { + userData.gdpr_consent = true; + } - next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]')); - }, - }); - } + next(userData.gdpr_consent ? null : new Error('[[register:gdpr_consent_denied]]')); + }, + }); + } - callback(null, data); - }, + setImmediate(callback, null, data); + }, + + // Forum Terms of Use + function (data, callback) { + if (meta.config.termsOfUse && !data.userData.acceptTos) { + data.interstitials.push({ + template: 'partials/acceptTos', + data: { + termsOfUse: meta.config.termsOfUse, + }, + callback: function (userData, formData, next) { + if (formData['agree-terms'] === 'on') { + userData.acceptTos = true; + } + + next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]')); + }, + }); + } + + setImmediate(callback, null, data); + }, + ], }); callback(); diff --git a/src/user/create.js b/src/user/create.js index f2a9b87f55..d6718e8a4d 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -46,6 +46,7 @@ module.exports = function (User) { lastposttime: 0, banned: 0, status: 'online', + gdpr_consent: data.gdpr_consent === true ? 1 : 0, }; User.uniqueUsername(userData, next); diff --git a/src/views/partials/gdpr_consent.tpl b/src/views/partials/gdpr_consent.tpl new file mode 100644 index 0000000000..420f4f3adb --- /dev/null +++ b/src/views/partials/gdpr_consent.tpl @@ -0,0 +1,23 @@ +
+

[[register:gdpr_lead]]

+

[[register:gdpr_intro]]

+
+ +
+

+ [[register:gdpr_email_intro]] + + [[register:gdpr_digest_frequency, {digestFrequency}]] + + [[register:gdpr_digest_off]] + +

+ +
+ +
+
\ No newline at end of file From aef788f3eec151a37a508b65effddaeeb096e71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 5 Apr 2018 17:09:10 -0400 Subject: [PATCH 10/42] remove unused var --- src/posts/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posts/user.js b/src/posts/user.js index e0df76c2a3..edc1259a44 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -16,7 +16,7 @@ module.exports = function (Posts) { var userData; var userSettings; var canUseSignature; - var allowMultipleBadges = parseInt(meta.config.allowMultipleBadges, 10) === 1; + async.waterfall([ function (next) { async.parallel({ From e9ed7f0bb3555f80aaa916881cde97e973ed69f5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 6 Apr 2018 10:16:26 -0400 Subject: [PATCH 11/42] closes #6435 --- src/controllers/authentication.js | 15 +++++++++++---- src/controllers/index.js | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 88d0ec5b49..f8537da085 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -152,7 +152,12 @@ authenticationController.registerComplete = function (req, res, next) { var callbacks = data.interstitials.reduce(function (memo, cur) { if (cur.hasOwnProperty('callback') && typeof cur.callback === 'function') { - memo.push(async.apply(cur.callback, req.session.registration, req.body)); + memo.push(function (next) { + cur.callback(req.session.registration, req.body, function (err) { + // Pass error as second argument so all callbacks are executed + next(null, err); + }); + }); } return memo; @@ -170,9 +175,11 @@ authenticationController.registerComplete = function (req, res, next) { } }; - async.parallel(callbacks, function (err) { - if (err) { - req.flash('error', err.message); + async.parallel(callbacks, function (_blank, err) { + if (err.length) { + req.flash('errors', err.filter(Boolean).map(function (err) { + return err.message; + })); return res.redirect(nconf.get('relative_path') + '/register/complete'); } diff --git a/src/controllers/index.js b/src/controllers/index.js index 550130ce92..292d892767 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -207,7 +207,7 @@ Controllers.registerInterstitial = function (req, res, next) { async.parallel(renders, next); }, function (sections) { - var errors = req.flash('error'); + var errors = req.flash('errors'); res.render('registerComplete', { title: '[[pages:registration-complete]]', errors: errors, From 04979f86a82cd0404d4028f6fe2f94065c7292b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 6 Apr 2018 15:16:28 -0400 Subject: [PATCH 12/42] delete users uploads on account delete store uid::uploads --- src/controllers/uploads.js | 20 +++++++++++--------- src/file.js | 20 ++++++++++++-------- src/user/delete.js | 22 ++++++++++++++++++++++ test/uploads.js | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 3d225c22fe..60366be026 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -5,13 +5,14 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); +var db = require('../database'); var meta = require('../meta'); var file = require('../file'); var plugins = require('../plugins'); var image = require('../image'); var privileges = require('../privileges'); -var uploadsController = {}; +var uploadsController = module.exports; uploadsController.upload = function (req, res, filesIterator) { var files = req.files.files; @@ -192,7 +193,7 @@ uploadsController.uploadGroupCover = function (uid, uploadedFile, callback) { file.isFileTypeAllowed(uploadedFile.path, next); }, function (next) { - saveFileToLocal(uploadedFile, next); + saveFileToLocal(uid, uploadedFile, next); }, ], callback); }; @@ -220,27 +221,30 @@ uploadsController.uploadFile = function (uid, uploadedFile, callback) { return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]')); } - saveFileToLocal(uploadedFile, callback); + saveFileToLocal(uid, uploadedFile, callback); }; -function saveFileToLocal(uploadedFile, callback) { +function saveFileToLocal(uid, uploadedFile, callback) { var filename = uploadedFile.name || 'upload'; var extension = path.extname(filename) || ''; filename = Date.now() + '-' + validator.escape(filename.substr(0, filename.length - extension.length)).substr(0, 255) + extension; - + var storedFile; async.waterfall([ function (next) { file.saveFileToLocal(filename, 'files', uploadedFile.path, next); }, function (upload, next) { - var storedFile = { + storedFile = { url: nconf.get('relative_path') + upload.url, path: upload.path, name: uploadedFile.name, }; - plugins.fireHook('filter:uploadStored', { uploadedFile: uploadedFile, storedFile: storedFile }, next); + db.sortedSetAdd('uid:' + uid + ':uploads', Date.now(), upload.url, next); + }, + function (next) { + plugins.fireHook('filter:uploadStored', { uid: uid, uploadedFile: uploadedFile, storedFile: storedFile }, next); }, function (data, next) { next(null, data.storedFile); @@ -254,5 +258,3 @@ function deleteTempFiles(files) { next(); }); } - -module.exports = uploadsController; diff --git a/src/file.js b/src/file.js index e31ae18399..2fbd15dbdf 100644 --- a/src/file.js +++ b/src/file.js @@ -141,8 +141,9 @@ file.exists = function (path, callback) { if (err.code === 'ENOENT') { return callback(null, false); } + return callback(err); } - callback(err, true); + callback(null, true); }); }; @@ -159,14 +160,17 @@ file.existsSync = function (path) { return true; }; -file.delete = function (path) { - if (path) { - fs.unlink(path, function (err) { - if (err) { - winston.error(err); - } - }); +file.delete = function (path, callback) { + callback = callback || function () {}; + if (!path) { + return callback(); } + fs.unlink(path, function (err) { + if (err) { + winston.error(err); + } + callback(); + }); }; file.link = function link(filePath, destPath, relative, callback) { diff --git a/src/user/delete.js b/src/user/delete.js index ace9dd969c..dd8e617903 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -2,6 +2,8 @@ var async = require('async'); var _ = require('lodash'); +var path = require('path'); +var nconf = require('nconf'); var db = require('../database'); var posts = require('../posts'); @@ -10,6 +12,7 @@ var groups = require('../groups'); var messaging = require('../messaging'); var plugins = require('../plugins'); var batch = require('../batch'); +var file = require('../file'); module.exports = function (User) { User.delete = function (callerUid, uid, callback) { @@ -24,6 +27,9 @@ module.exports = function (User) { function (next) { deleteTopics(callerUid, uid, next); }, + function (next) { + deleteUploads(uid, next); + }, function (next) { User.deleteAccount(uid, next); }, @@ -46,6 +52,22 @@ module.exports = function (User) { }, { alwaysStartAt: 0 }, callback); } + function deleteUploads(uid, callback) { + batch.processSortedSet('uid:' + uid + ':uploads', function (urls, next) { + async.waterfall([ + function (next) { + async.each(urls, function (url, next) { + var filePath = path.join(nconf.get('upload_path'), url.replace(nconf.get('upload_url'), '')); + file.delete(filePath, next); + }, next); + }, + function (next) { + db.sortedSetRemove('uid:' + uid + ':uploads', urls, next); + }, + ], next); + }, { alwaysStartAt: 0 }, callback); + } + User.deleteAccount = function (uid, callback) { var userData; async.waterfall([ diff --git a/test/uploads.js b/test/uploads.js index db8305d592..77fa0832ee 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -159,6 +159,41 @@ describe('Upload Controllers', function () { done(); }); }); + + it('should delete users uploads if account is deleted', function (done) { + var jar; + var uid; + var url; + var file = require('../src/file'); + + async.waterfall([ + function (next) { + user.create({ username: 'uploader', password: 'barbar' }, next); + }, + function (_uid, next) { + uid = _uid; + helpers.loginUser('uploader', 'barbar', next); + }, + function (jar, csrf_token, next) { + helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, next); + }, + function (res, body, next) { + assert(body); + assert(body[0].url); + url = body[0].url; + + user.delete(1, uid, next); + }, + function (next) { + var filePath = path.join(nconf.get('upload_path'), url.replace('/assets/uploads', '')); + file.exists(filePath, next); + }, + function (exists, next) { + assert(!exists); + done(); + }, + ], done); + }); }); describe('admin uploads', function () { From 7cd004ca23c3db61b31ed54160702c18a5c46c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 6 Apr 2018 15:42:53 -0400 Subject: [PATCH 13/42] user uploads route --- public/language/en-GB/global.json | 1 + public/language/en-GB/pages.json | 1 + public/language/en-GB/uploads.json | 3 +- src/controllers/accounts.js | 1 + src/controllers/accounts/uploads.js | 53 +++++++++++++++++++++++++++++ src/routes/accounts.js | 1 + 6 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/controllers/accounts/uploads.js diff --git a/public/language/en-GB/global.json b/public/language/en-GB/global.json index 1195d792a0..14d9b13421 100644 --- a/public/language/en-GB/global.json +++ b/public/language/en-GB/global.json @@ -122,6 +122,7 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", + "uploads": "Uploads", "allowed-file-types": "Allowed file types are %1", "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", diff --git a/public/language/en-GB/pages.json b/public/language/en-GB/pages.json index 70f6cc24a3..1a7f23eace 100644 --- a/public/language/en-GB/pages.json +++ b/public/language/en-GB/pages.json @@ -55,6 +55,7 @@ "account/upvoted": "Posts upvoted by %1", "account/downvoted": "Posts downvoted by %1", "account/best": "Best posts made by %1", + "account/uploads": "Uploads by %1", "confirm": "Email Confirmed", diff --git a/public/language/en-GB/uploads.json b/public/language/en-GB/uploads.json index 8cf9487901..bf916b8a42 100644 --- a/public/language/en-GB/uploads.json +++ b/public/language/en-GB/uploads.json @@ -2,5 +2,6 @@ "uploading-file" : "Uploading the file...", "select-file-to-upload": "Select a file to upload!", "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found" } \ No newline at end of file diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js index a7aa1716b4..465d387eb7 100644 --- a/src/controllers/accounts.js +++ b/src/controllers/accounts.js @@ -11,6 +11,7 @@ var accountsController = { notifications: require('./accounts/notifications'), chats: require('./accounts/chats'), session: require('./accounts/session'), + uploads: require('./accounts/uploads'), }; module.exports = accountsController; diff --git a/src/controllers/accounts/uploads.js b/src/controllers/accounts/uploads.js new file mode 100644 index 0000000000..ee0825c623 --- /dev/null +++ b/src/controllers/accounts/uploads.js @@ -0,0 +1,53 @@ +'use strict'; + + +var async = require('async'); + +var db = require('../../database'); +var helpers = require('../helpers'); +var pagination = require('../../pagination'); +var accountHelpers = require('./helpers'); + +var uploadsController = module.exports; + +uploadsController.get = function (req, res, callback) { + var userData; + + var page = Math.max(1, parseInt(req.query.page, 10) || 1); + var itemsPerPage = 25; + + async.waterfall([ + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); + }, + function (_userData, next) { + userData = _userData; + if (!userData) { + return callback(); + } + + var start = (page - 1) * itemsPerPage; + var stop = start + itemsPerPage - 1; + async.parallel({ + itemCount: function (next) { + db.sortedSetCard('uid:' + userData.uid + ':uploads', next); + }, + uploadUrls: function (next) { + db.getSortedSetRevRange('uid:' + userData.uid + ':uploads', start, stop, next); + }, + }, next); + }, + function (results) { + userData.uploads = results.uploadUrls.map(function (url) { + return { + url: url, + }; + }); + var pageCount = Math.ceil(results.itemCount / itemsPerPage); + userData.pagination = pagination.create(page, pageCount, req.query); + userData.title = '[[pages:account/uploads, ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[global:uploads]]' }]); + res.render('account/uploads', userData); + }, + ], callback); +}; diff --git a/src/routes/accounts.js b/src/routes/accounts.js index 5c040b6af1..7d4bd7a578 100644 --- a/src/routes/accounts.js +++ b/src/routes/accounts.js @@ -29,6 +29,7 @@ module.exports = function (app, middleware, controllers) { setupPageRoute(app, '/user/:userslug/edit/password', middleware, accountMiddlewares, controllers.accounts.edit.password); setupPageRoute(app, '/user/:userslug/info', middleware, accountMiddlewares, controllers.accounts.info.get); setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get); + setupPageRoute(app, '/user/:userslug/uploads', middleware, accountMiddlewares, controllers.accounts.uploads.get); app.delete('/api/user/:userslug/session/:uuid', [middleware.exposeUid, middleware.ensureSelfOrGlobalPrivilege], controllers.accounts.session.revoke); From 8e822c7772bd5828b809654e539eaefc371b05ea Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 9 Apr 2018 12:22:44 -0400 Subject: [PATCH 14/42] Added user consent pages (#6430) - "Your Rights & Consent" user settings page --- public/language/en-GB/register.json | 5 --- public/language/en-GB/user.json | 21 ++++++++++- public/src/client/account/consent.js | 22 ++++++++++++ src/controllers/accounts.js | 1 + src/controllers/accounts/consent.js | 53 ++++++++++++++++++++++++++++ src/controllers/accounts/helpers.js | 11 ++++++ src/routes/accounts.js | 1 + src/socket.io/user.js | 6 ++++ src/user/data.js | 2 +- src/views/partials/gdpr_consent.tpl | 10 +++--- 10 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 public/src/client/account/consent.js create mode 100644 src/controllers/accounts/consent.js diff --git a/public/language/en-GB/register.json b/public/language/en-GB/register.json index 0611f4b878..461295ef5f 100644 --- a/public/language/en-GB/register.json +++ b/public/language/en-GB/register.json @@ -21,11 +21,6 @@ "interstitial.intro": "We require some additional information before we can create your account.", "interstitial.errors-found": "We could not complete your registration:", - "gdpr_lead": "This community forum collects and processes your personal information.", - "gdpr_intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your user settings page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", - "gdpr_email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email.", - "gdpr_digest_frequency": "By default, this community delivers email digests every %1.", - "gdpr_digest_off": "Currently, this community does not send out email digests", "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails." diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 3dcf2521b4..073dd3b38f 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -160,5 +160,24 @@ "info.email-history": "Email History", "info.moderation-note": "Moderation Note", "info.moderation-note.success": "Moderation note saved", - "info.moderation-note.add": "Add note" + "info.moderation-note.add": "Add note", + + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "By default, this community delivers email digests every %1.", + "consent.digest_off": "Currently, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + + "consent.right_of_access": "You have the Right of Access", + "consent.right_of_access_description": "You have the right to access any data collected by this website upon request. You can retrieve a copy of this data by clicking the appropriate button below.", + "consent.right_to_rectification": "You have the Right to Rectification", + "consent.right_to_rectification_description": "You have the right to change or update any inaccurate data provided to us. Your profile can be updated by editing your profile, and post content can always be edited. If this is not the case, please contact this site's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "consent.right_to_erasure_description": "At any time, you are able to revoke your consent to data collection and/or processing by deleting your account.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "consent.right_to_data_portability_description": "You may request from us a machine-readable export of any collected data about you and your account. You can do so by clicking the appropriate button below." } diff --git a/public/src/client/account/consent.js b/public/src/client/account/consent.js new file mode 100644 index 0000000000..f038c7acea --- /dev/null +++ b/public/src/client/account/consent.js @@ -0,0 +1,22 @@ +'use strict'; + + +define('forum/account/consent', ['forum/account/header'], function (header) { + var Consent = {}; + + Consent.init = function () { + header.init(); + + $('[data-action="consent"]').on('click', function () { + socket.emit('user.gdpr.consent', {}, function (err) { + if (err) { + return app.alertError(err.message); + } + + ajaxify.refresh(); + }); + }); + }; + + return Consent; +}); diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js index 465d387eb7..94543e033f 100644 --- a/src/controllers/accounts.js +++ b/src/controllers/accounts.js @@ -12,6 +12,7 @@ var accountsController = { chats: require('./accounts/chats'), session: require('./accounts/session'), uploads: require('./accounts/uploads'), + consent: require('./accounts/consent'), }; module.exports = accountsController; diff --git a/src/controllers/accounts/consent.js b/src/controllers/accounts/consent.js new file mode 100644 index 0000000000..e5204ce448 --- /dev/null +++ b/src/controllers/accounts/consent.js @@ -0,0 +1,53 @@ +'use strict'; + +var async = require('async'); + +var db = require('../../database'); +var meta = require('../../meta'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); + +var consentController = {}; + +consentController.get = function (req, res, next) { + var userData; + + async.waterfall([ + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); + }, + function (_userData, next) { + userData = _userData; + if (!userData) { + return next(); + } + + // Direct database call is used here because `gdpr_consent` is a protected user field and is automatically scrubbed from standard user data retrieval calls + db.getObjectField('user:' + userData.uid, 'gdpr_consent', function (err, consented) { + if (err) { + return next(err); + } + + userData.gdpr_consent = !!parseInt(consented, 10); + + next(null, userData); + }); + }, + ], function (err, userData) { + if (err) { + return next(err); + } + + userData.digest = { + frequency: meta.config.dailyDigestFreq, + enabled: meta.config.dailyDigestFreq !== 'off', + }; + + userData.title = '[[user:consent.title]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:consent.title]]' }]); + + res.render('account/consent', userData); + }); +}; + +module.exports = consentController; diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index bc43213de0..60935f91f7 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -68,6 +68,17 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { globalMod: true, admin: true, }, + }, { + id: 'consent', + route: 'consent', + name: '[[user:consent.title]]', + visibility: { + self: true, + other: false, + moderator: false, + globalMod: false, + admin: false, + }, }], }, next); }, diff --git a/src/routes/accounts.js b/src/routes/accounts.js index 7d4bd7a578..3eac849bbd 100644 --- a/src/routes/accounts.js +++ b/src/routes/accounts.js @@ -30,6 +30,7 @@ module.exports = function (app, middleware, controllers) { setupPageRoute(app, '/user/:userslug/info', middleware, accountMiddlewares, controllers.accounts.info.get); setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get); setupPageRoute(app, '/user/:userslug/uploads', middleware, accountMiddlewares, controllers.accounts.uploads.get); + setupPageRoute(app, '/user/:userslug/consent', middleware, accountMiddlewares, controllers.accounts.consent.get); app.delete('/api/user/:userslug/session/:uuid', [middleware.exposeUid, middleware.ensureSelfOrGlobalPrivilege], controllers.accounts.session.revoke); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 36026a7f28..60846491ae 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -340,3 +340,9 @@ SocketUser.setModerationNote = function (socket, data, callback) { }, ], callback); }; + +SocketUser.gdpr = {}; + +SocketUser.gdpr.consent = function (socket, data, callback) { + user.setUserField(socket.uid, 'gdpr_consent', 1, callback); +}; diff --git a/src/user/data.js b/src/user/data.js index bec2f2a216..05ebc2f9b2 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -80,7 +80,7 @@ module.exports = function (User) { fields = fields.filter(function (field) { var isFieldWhitelisted = field && results.whitelist.includes(field); if (!isFieldWhitelisted) { - winston.verbose('[user/getUsersFields] ' + field + ' removed because it is not whitelisted, see `filter:user.whietlistFields`'); + winston.verbose('[user/getUsersFields] ' + field + ' removed because it is not whitelisted, see `filter:user.whitelistFields`'); } return isFieldWhitelisted; }); diff --git a/src/views/partials/gdpr_consent.tpl b/src/views/partials/gdpr_consent.tpl index 420f4f3adb..7ee5b7db4d 100644 --- a/src/views/partials/gdpr_consent.tpl +++ b/src/views/partials/gdpr_consent.tpl @@ -1,17 +1,17 @@
-

[[register:gdpr_lead]]

-

[[register:gdpr_intro]]

+

[[user:consent.lead]]

+

[[user:consent.intro]]

- [[register:gdpr_email_intro]] + [[user:consent.email_intro]] - [[register:gdpr_digest_frequency, {digestFrequency}]] + [[user:consent.digest_frequency, {digestFrequency}]] - [[register:gdpr_digest_off]] + [[user:consent.digest_off]]

From b35bfb81ce1d8279cbad4d144d89647a2a9de314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 9 Apr 2018 20:03:33 -0400 Subject: [PATCH 15/42] ability to delete uploads from account page #6431 --- public/src/client/account/uploads.js | 24 ++++++++++++++++++++++ src/file.js | 7 +++++++ src/socket.io/user.js | 7 +++++++ src/user.js | 1 + src/user/delete.js | 6 ++---- src/user/uploads.js | 30 ++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 public/src/client/account/uploads.js create mode 100644 src/user/uploads.js diff --git a/public/src/client/account/uploads.js b/public/src/client/account/uploads.js new file mode 100644 index 0000000000..cc8bdd43f2 --- /dev/null +++ b/public/src/client/account/uploads.js @@ -0,0 +1,24 @@ +'use strict'; + +define('forum/account/uploads', ['forum/account/header'], function (header) { + var AccountUploads = {}; + + AccountUploads.init = function () { + header.init(); + + $('[data-action="delete"]').on('click', function () { + var el = $(this).parents('[data-url]'); + var url = el.attr('data-url'); + + socket.emit('user.deleteUpload', url, function (err) { + if (err) { + return app.alertError(err.message); + } + el.remove(); + }); + return false; + }); + }; + + return AccountUploads; +}); diff --git a/src/file.js b/src/file.js index 2fbd15dbdf..2c3f8d71ab 100644 --- a/src/file.js +++ b/src/file.js @@ -88,6 +88,13 @@ file.saveFileToLocal = function (filename, folder, tempPath, callback) { }); }; +file.uploadUrlToPath = function (url) { + if (typeof url !== 'string') { + return ''; + } + return path.join(nconf.get('upload_path'), url.replace(nconf.get('upload_url'), '')); +}; + file.base64ToLocal = function (imageData, uploadPath, callback) { var buffer = Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64'); uploadPath = path.join(nconf.get('upload_path'), uploadPath); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 60846491ae..4f2acde057 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -341,6 +341,13 @@ SocketUser.setModerationNote = function (socket, data, callback) { ], callback); }; +SocketUser.deleteUpload = function (socket, url, callback) { + if (!url) { + return callback(new Error('[[error:invalid-data]]')); + } + user.deleteUpload(socket.uid, url, callback); +}; + SocketUser.gdpr = {}; SocketUser.gdpr.consent = function (socket, data, callback) { diff --git a/src/user.js b/src/user.js index f81c08f280..8ab4d1c441 100644 --- a/src/user.js +++ b/src/user.js @@ -36,6 +36,7 @@ require('./user/invite')(User); require('./user/password')(User); require('./user/info')(User); require('./user/online')(User); +require('./user/uploads')(User); User.getUidsFromSet = function (set, start, stop, callback) { if (set === 'users:online') { diff --git a/src/user/delete.js b/src/user/delete.js index dd8e617903..d56c9749ac 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -2,8 +2,7 @@ var async = require('async'); var _ = require('lodash'); -var path = require('path'); -var nconf = require('nconf'); + var db = require('../database'); var posts = require('../posts'); @@ -57,8 +56,7 @@ module.exports = function (User) { async.waterfall([ function (next) { async.each(urls, function (url, next) { - var filePath = path.join(nconf.get('upload_path'), url.replace(nconf.get('upload_url'), '')); - file.delete(filePath, next); + file.delete(file.uploadUrlToPath(url), next); }, next); }, function (next) { diff --git a/src/user/uploads.js b/src/user/uploads.js new file mode 100644 index 0000000000..60a84d8fe8 --- /dev/null +++ b/src/user/uploads.js @@ -0,0 +1,30 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var file = require('../file'); + +module.exports = function (User) { + User.deleteUpload = function (uid, url, callback) { + async.waterfall([ + function (next) { + async.parallel({ + isUsersUpload: function (next) { + db.isSortedSetMember('uid:' + uid + ':uploads', url, next); + }, + isAdminOrGlobalMod: function (next) { + User.isAdminOrGlobalMod(uid, next); + }, + }, next); + }, + function (results, next) { + if (!results.isAdminOrGlobalMod && !results.isUsersUpload) { + return next(new Error('[[error:no-privileges]]')); + } + + file.delete(file.uploadUrlToPath(url), next); + }, + ], callback); + }; +}; From 927537c759a571fd84fce52add9f3483edb36763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 9 Apr 2018 20:22:36 -0400 Subject: [PATCH 16/42] #6431 add private upload info --- public/language/en-GB/uploads.json | 4 +++- public/src/client/account/uploads.js | 2 +- src/controllers/accounts/uploads.js | 2 ++ src/socket.io/user.js | 6 +++--- src/user/uploads.js | 9 ++++++--- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/public/language/en-GB/uploads.json b/public/language/en-GB/uploads.json index bf916b8a42..e58955543a 100644 --- a/public/language/en-GB/uploads.json +++ b/public/language/en-GB/uploads.json @@ -3,5 +3,7 @@ "select-file-to-upload": "Select a file to upload!", "upload-success": "File uploaded successfully!", "maximum-file-size": "Maximum %1 kb", - "no-uploads-found": "No uploads found" + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads": "Uploads are private, only logged in users can see them." } \ No newline at end of file diff --git a/public/src/client/account/uploads.js b/public/src/client/account/uploads.js index cc8bdd43f2..96754db74c 100644 --- a/public/src/client/account/uploads.js +++ b/public/src/client/account/uploads.js @@ -10,7 +10,7 @@ define('forum/account/uploads', ['forum/account/header'], function (header) { var el = $(this).parents('[data-url]'); var url = el.attr('data-url'); - socket.emit('user.deleteUpload', url, function (err) { + socket.emit('user.deleteUpload', { url: url, uid: ajaxify.data.uid }, function (err) { if (err) { return app.alertError(err.message); } diff --git a/src/controllers/accounts/uploads.js b/src/controllers/accounts/uploads.js index ee0825c623..6f0ff29fff 100644 --- a/src/controllers/accounts/uploads.js +++ b/src/controllers/accounts/uploads.js @@ -5,6 +5,7 @@ var async = require('async'); var db = require('../../database'); var helpers = require('../helpers'); +var meta = require('../../meta'); var pagination = require('../../pagination'); var accountHelpers = require('./helpers'); @@ -45,6 +46,7 @@ uploadsController.get = function (req, res, callback) { }); var pageCount = Math.ceil(results.itemCount / itemsPerPage); userData.pagination = pagination.create(page, pageCount, req.query); + userData.privateUploads = parseInt(meta.config.privateUploads, 10) === 1; userData.title = '[[pages:account/uploads, ' + userData.username + ']]'; userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[global:uploads]]' }]); res.render('account/uploads', userData); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 4f2acde057..25df8f7931 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -341,11 +341,11 @@ SocketUser.setModerationNote = function (socket, data, callback) { ], callback); }; -SocketUser.deleteUpload = function (socket, url, callback) { - if (!url) { +SocketUser.deleteUpload = function (socket, data, callback) { + if (!data || !data.url || !data.uid) { return callback(new Error('[[error:invalid-data]]')); } - user.deleteUpload(socket.uid, url, callback); + user.deleteUpload(socket.uid, data.uid, data.url, callback); }; SocketUser.gdpr = {}; diff --git a/src/user/uploads.js b/src/user/uploads.js index 60a84d8fe8..f5e840a9f2 100644 --- a/src/user/uploads.js +++ b/src/user/uploads.js @@ -6,15 +6,15 @@ var db = require('../database'); var file = require('../file'); module.exports = function (User) { - User.deleteUpload = function (uid, url, callback) { + User.deleteUpload = function (callerUid, uid, url, callback) { async.waterfall([ function (next) { async.parallel({ isUsersUpload: function (next) { - db.isSortedSetMember('uid:' + uid + ':uploads', url, next); + db.isSortedSetMember('uid:' + callerUid + ':uploads', url, next); }, isAdminOrGlobalMod: function (next) { - User.isAdminOrGlobalMod(uid, next); + User.isAdminOrGlobalMod(callerUid, next); }, }, next); }, @@ -25,6 +25,9 @@ module.exports = function (User) { file.delete(file.uploadUrlToPath(url), next); }, + function (next) { + db.sortedSetRemove('uid:' + uid + ':uploads', url, next); + }, ], callback); }; }; From ef99ef86a4e727654de0f29d2092a92fbd1143d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 9 Apr 2018 20:23:36 -0400 Subject: [PATCH 17/42] fix lang-key --- public/language/en-GB/uploads.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/en-GB/uploads.json b/public/language/en-GB/uploads.json index e58955543a..4aca2bce1e 100644 --- a/public/language/en-GB/uploads.json +++ b/public/language/en-GB/uploads.json @@ -5,5 +5,5 @@ "maximum-file-size": "Maximum %1 kb", "no-uploads-found": "No uploads found", "public-uploads-info": "Uploads are public, all visitors can see them.", - "private-uploads": "Uploads are private, only logged in users can see them." + "private-uploads-info": "Uploads are private, only logged in users can see them." } \ No newline at end of file From 2e6e7f7484df685fd9457f83f5a81f885a03f030 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 10 Apr 2018 15:59:24 -0400 Subject: [PATCH 18/42] posts export endpoint for GDPR, re: #6441 --- src/controllers/user.js | 36 ++++++++++++++++++++++++++++++++++++ src/routes/api.js | 2 ++ 2 files changed, 38 insertions(+) diff --git a/src/controllers/user.js b/src/controllers/user.js index 3405c1ea1d..edb7202b39 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,9 +1,12 @@ 'use strict'; var async = require('async'); +var converter = require('json-2-csv'); var user = require('../user'); var meta = require('../meta'); +var posts = require('../posts'); +var batch = require('../batch'); var accountHelpers = require('./accounts/helpers'); var userController = module.exports; @@ -97,3 +100,36 @@ userController.getUserDataByUID = function (callerUid, uid, callback) { callback(null, results.userData); }); }; + +userController.exportPosts = function (req, res, next) { + async.waterfall([ + function (next) { + var payload = []; + batch.processSortedSet('uid:' + req.params.uid + ':posts', function (pids, next) { + async.map(pids, posts.getPostData, function (err, posts) { + if (err) { + return next(err); + } + + // Convert newlines in content + posts = posts.map(function (post) { + post.content = '"' + post.content.replace(/\n/g, '\\n').replace(/"/g, '\\"') + '"'; + return post; + }); + + payload = payload.concat(posts); + next(); + }); + }, function (err) { + next(err, payload); + }); + }, + async.apply(converter.json2csv), + ], function (err, csv) { + if (err) { + return next(err); + } + + res.set('Content-Type', 'text/csv').set('Content-Disposition', 'attachment; filename="' + req.params.uid + '_posts.csv"').send(csv); + }); +}; diff --git a/src/routes/api.js b/src/routes/api.js index 34f14f5a8c..0e531b7236 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -15,6 +15,8 @@ module.exports = function (app, middleware, controllers) { router.get('/user/username/:username', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUsername); router.get('/user/email/:email', middleware.checkGlobalPrivacySettings, controllers.user.getUserByEmail); + router.get('/user/uid/:uid/export/posts', middleware.checkAccountPermissions, controllers.user.exportPosts); + router.get('/:type/pid/:id', controllers.api.getObject); router.get('/:type/tid/:id', controllers.api.getObject); router.get('/:type/cid/:id', controllers.api.getObject); From ee78e874dc354a8e935d0a0297c48b73d3882d3a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Apr 2018 10:53:55 -0400 Subject: [PATCH 19/42] add archiver dependency --- install/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/install/package.json b/install/package.json index aa09ad2a0f..f49e523967 100644 --- a/install/package.json +++ b/install/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "ace-builds": "^1.2.9", + "archiver": "^2.1.1", "async": "2.6.0", "autoprefixer": "7.2.4", "bcryptjs": "2.4.3", From 0de40188f1c19274f98e9e816d0b372b3c554496 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Apr 2018 11:09:30 -0400 Subject: [PATCH 20/42] Add build/export to repo --- .gitignore | 1 - build/.gitignore | 4 ++++ build/export/.gitignore | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 build/.gitignore create mode 100644 build/export/.gitignore diff --git a/.gitignore b/.gitignore index 2ae33250b3..72399b5f92 100644 --- a/.gitignore +++ b/.gitignore @@ -61,7 +61,6 @@ tx.exe coverage .nyc_output -build *.log test/files/normalise.jpg.png test/files/normalise-resized.jpg diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000000..9addbff7eb --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +* +*/ +!export +!.gitignore diff --git a/build/export/.gitignore b/build/export/.gitignore new file mode 100644 index 0000000000..49ca10c54e --- /dev/null +++ b/build/export/.gitignore @@ -0,0 +1,2 @@ +. +!.gitignore From 5d2a7106f60a1294fc6cc9c5fc58137a5ef5738a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Apr 2018 12:35:05 -0400 Subject: [PATCH 21/42] change upload storage --- public/src/client/account/uploads.js | 6 +++--- src/controllers/accounts/uploads.js | 8 +++++--- src/controllers/uploads.js | 3 ++- src/file.js | 7 ------- src/socket.io/user.js | 4 ++-- src/user/delete.js | 11 ++++++----- src/user/uploads.js | 10 ++++++---- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/public/src/client/account/uploads.js b/public/src/client/account/uploads.js index 96754db74c..222e30e9a0 100644 --- a/public/src/client/account/uploads.js +++ b/public/src/client/account/uploads.js @@ -7,10 +7,10 @@ define('forum/account/uploads', ['forum/account/header'], function (header) { header.init(); $('[data-action="delete"]').on('click', function () { - var el = $(this).parents('[data-url]'); - var url = el.attr('data-url'); + var el = $(this).parents('[data-name]'); + var name = el.attr('data-name'); - socket.emit('user.deleteUpload', { url: url, uid: ajaxify.data.uid }, function (err) { + socket.emit('user.deleteUpload', { name: name, uid: ajaxify.data.uid }, function (err) { if (err) { return app.alertError(err.message); } diff --git a/src/controllers/accounts/uploads.js b/src/controllers/accounts/uploads.js index 6f0ff29fff..21a5287550 100644 --- a/src/controllers/accounts/uploads.js +++ b/src/controllers/accounts/uploads.js @@ -2,6 +2,7 @@ var async = require('async'); +var nconf = require('nconf'); var db = require('../../database'); var helpers = require('../helpers'); @@ -33,15 +34,16 @@ uploadsController.get = function (req, res, callback) { itemCount: function (next) { db.sortedSetCard('uid:' + userData.uid + ':uploads', next); }, - uploadUrls: function (next) { + uploadNames: function (next) { db.getSortedSetRevRange('uid:' + userData.uid + ':uploads', start, stop, next); }, }, next); }, function (results) { - userData.uploads = results.uploadUrls.map(function (url) { + userData.uploads = results.uploadNames.map(function (uploadName) { return { - url: url, + name: uploadName, + url: nconf.get('upload_url') + uploadName, }; }); var pageCount = Math.ceil(results.itemCount / itemsPerPage); diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 60366be026..487332542e 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -241,7 +241,8 @@ function saveFileToLocal(uid, uploadedFile, callback) { name: uploadedFile.name, }; - db.sortedSetAdd('uid:' + uid + ':uploads', Date.now(), upload.url, next); + var fileKey = upload.url.replace(nconf.get('upload_url'), ''); + db.sortedSetAdd('uid:' + uid + ':uploads', Date.now(), fileKey, next); }, function (next) { plugins.fireHook('filter:uploadStored', { uid: uid, uploadedFile: uploadedFile, storedFile: storedFile }, next); diff --git a/src/file.js b/src/file.js index 2c3f8d71ab..2fbd15dbdf 100644 --- a/src/file.js +++ b/src/file.js @@ -88,13 +88,6 @@ file.saveFileToLocal = function (filename, folder, tempPath, callback) { }); }; -file.uploadUrlToPath = function (url) { - if (typeof url !== 'string') { - return ''; - } - return path.join(nconf.get('upload_path'), url.replace(nconf.get('upload_url'), '')); -}; - file.base64ToLocal = function (imageData, uploadPath, callback) { var buffer = Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64'); uploadPath = path.join(nconf.get('upload_path'), uploadPath); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 25df8f7931..4f16e82eda 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -342,10 +342,10 @@ SocketUser.setModerationNote = function (socket, data, callback) { }; SocketUser.deleteUpload = function (socket, data, callback) { - if (!data || !data.url || !data.uid) { + if (!data || !data.name || !data.uid) { return callback(new Error('[[error:invalid-data]]')); } - user.deleteUpload(socket.uid, data.uid, data.url, callback); + user.deleteUpload(socket.uid, data.uid, data.name, callback); }; SocketUser.gdpr = {}; diff --git a/src/user/delete.js b/src/user/delete.js index d56c9749ac..6712569634 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -2,7 +2,8 @@ var async = require('async'); var _ = require('lodash'); - +var path = require('path'); +var nconf = require('nconf'); var db = require('../database'); var posts = require('../posts'); @@ -52,15 +53,15 @@ module.exports = function (User) { } function deleteUploads(uid, callback) { - batch.processSortedSet('uid:' + uid + ':uploads', function (urls, next) { + batch.processSortedSet('uid:' + uid + ':uploads', function (uploadNames, next) { async.waterfall([ function (next) { - async.each(urls, function (url, next) { - file.delete(file.uploadUrlToPath(url), next); + async.each(uploadNames, function (uploadName, next) { + file.delete(path.join(nconf.get('upload_path'), uploadName), next); }, next); }, function (next) { - db.sortedSetRemove('uid:' + uid + ':uploads', urls, next); + db.sortedSetRemove('uid:' + uid + ':uploads', uploadNames, next); }, ], next); }, { alwaysStartAt: 0 }, callback); diff --git a/src/user/uploads.js b/src/user/uploads.js index f5e840a9f2..faec37fce8 100644 --- a/src/user/uploads.js +++ b/src/user/uploads.js @@ -1,17 +1,19 @@ 'use strict'; var async = require('async'); +var path = require('path'); +var nconf = require('nconf'); var db = require('../database'); var file = require('../file'); module.exports = function (User) { - User.deleteUpload = function (callerUid, uid, url, callback) { + User.deleteUpload = function (callerUid, uid, uploadName, callback) { async.waterfall([ function (next) { async.parallel({ isUsersUpload: function (next) { - db.isSortedSetMember('uid:' + callerUid + ':uploads', url, next); + db.isSortedSetMember('uid:' + callerUid + ':uploads', uploadName, next); }, isAdminOrGlobalMod: function (next) { User.isAdminOrGlobalMod(callerUid, next); @@ -23,10 +25,10 @@ module.exports = function (User) { return next(new Error('[[error:no-privileges]]')); } - file.delete(file.uploadUrlToPath(url), next); + file.delete(path.join(nconf.get('upload_path'), uploadName), next); }, function (next) { - db.sortedSetRemove('uid:' + uid + ':uploads', url, next); + db.sortedSetRemove('uid:' + uid + ':uploads', uploadName, next); }, ], callback); }; From 65c845789805560b2fbb247f57b1b94eb146388f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Apr 2018 12:51:34 -0400 Subject: [PATCH 22/42] fixing tests that broke due to gdpr requirement --- test/authentication.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/authentication.js b/test/authentication.js index 99512c7af4..b90d585971 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -56,6 +56,7 @@ describe('authentication', function () { username: username, password: password, 'password-confirm': password, + gdpr_consent: true, }, json: true, jar: jar, @@ -150,6 +151,7 @@ describe('authentication', function () { password: 'adminpwd', 'password-confirm': 'adminpwd', userLang: 'it', + gdpr_consent: true, }, json: true, jar: jar, From ed289ebeab9159aab586dc054de2a2ecde1a10d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Apr 2018 13:28:14 -0400 Subject: [PATCH 23/42] two more text fixes --- test/user.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/user.js b/test/user.js index f96205a5e4..c88edef39d 100644 --- a/test/user.js +++ b/test/user.js @@ -1434,6 +1434,7 @@ describe('User', function () { password: '123456', 'password-confirm': '123456', email: '