+
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 @@
+
\ 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 @@