mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 12:05:57 +01:00
Merge branch 'master' into user-blocking
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -61,7 +61,6 @@ tx.exe
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
build
|
||||
*.log
|
||||
test/files/normalise.jpg.png
|
||||
test/files/normalise-resized.jpg
|
||||
|
||||
4
build/.gitignore
vendored
Normal file
4
build/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
*/
|
||||
!export
|
||||
!.gitignore
|
||||
3
build/export/.gitignore
vendored
Normal file
3
build/export/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.
|
||||
!.gitignore
|
||||
!README
|
||||
5
build/export/README
Normal file
5
build/export/README
Normal file
@@ -0,0 +1,5 @@
|
||||
This directory contains archives of user uploads that are prepared on-demand
|
||||
when a user wants to retrieve a copy of their uploaded content.
|
||||
|
||||
You can delete the files in here at will. They will just be regenerated if
|
||||
requested again.
|
||||
@@ -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",
|
||||
@@ -65,7 +66,7 @@
|
||||
"nconf": "^0.9.1",
|
||||
"nodebb-plugin-composer-default": "6.0.22",
|
||||
"nodebb-plugin-dbsearch": "2.0.16",
|
||||
"nodebb-plugin-emoji": "^2.2.0",
|
||||
"nodebb-plugin-emoji": "^2.2.2",
|
||||
"nodebb-plugin-emoji-android": "2.0.0",
|
||||
"nodebb-plugin-markdown": "8.4.2",
|
||||
"nodebb-plugin-mentions": "2.2.6",
|
||||
@@ -73,9 +74,9 @@
|
||||
"nodebb-plugin-spam-be-gone": "0.5.3",
|
||||
"nodebb-rewards-essentials": "0.0.11",
|
||||
"nodebb-theme-lavender": "5.0.4",
|
||||
"nodebb-theme-persona": "8.0.11",
|
||||
"nodebb-theme-persona": "9.0.0",
|
||||
"nodebb-theme-slick": "1.2.1",
|
||||
"nodebb-theme-vanilla": "9.0.8",
|
||||
"nodebb-theme-vanilla": "10.0.0",
|
||||
"nodebb-widget-essentials": "4.0.2",
|
||||
"nodemailer": "4.4.1",
|
||||
"passport": "^0.4.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"upload-file": "Upload File",
|
||||
"filename": "Filename",
|
||||
"usage": "Post Usage",
|
||||
"orphaned": "Orphaned",
|
||||
"size/filecount": "Size / Filecount",
|
||||
"confirm-delete": "Do you really want to delete this file?",
|
||||
"filecount": "%1 files"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"private-groups.warning": "<strong>Beware!</strong> 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 <em>(Default: disabled)</em>",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"account/downvoted": "Posts downvoted by %1",
|
||||
"account/best": "Best posts made by %1",
|
||||
"account/blocks": "Blocked users for %1",
|
||||
"account/uploads": "Uploads by %1",
|
||||
|
||||
"confirm": "Email Confirmed",
|
||||
|
||||
|
||||
@@ -19,5 +19,9 @@
|
||||
"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_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."
|
||||
}
|
||||
@@ -2,5 +2,8 @@
|
||||
"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",
|
||||
"public-uploads-info": "Uploads are public, all visitors can see them.",
|
||||
"private-uploads-info": "Uploads are private, only logged in users can see them."
|
||||
}
|
||||
@@ -163,5 +163,28 @@
|
||||
"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.<br /><br />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.<br /><br />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.",
|
||||
|
||||
"consent.export_profile": "Export Profile (.csv)",
|
||||
"consent.export_uploads": "Export Uploaded Content (.zip)",
|
||||
"consent.export_posts": "Export Posts (.csv)"
|
||||
}
|
||||
|
||||
22
public/src/client/account/consent.js
Normal file
22
public/src/client/account/consent.js
Normal file
@@ -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;
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
24
public/src/client/account/uploads.js
Normal file
24
public/src/client/account/uploads.js
Normal file
@@ -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-name]');
|
||||
var name = el.attr('data-name');
|
||||
|
||||
socket.emit('user.deleteUpload', { name: name, uid: ajaxify.data.uid }, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
el.remove();
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
return AccountUploads;
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -17,6 +17,11 @@ app.isConnected = false;
|
||||
|
||||
socket = io(config.websocketAddress, ioParams);
|
||||
|
||||
if (parseInt(app.user.uid, 10) >= 0) {
|
||||
addHandlers();
|
||||
}
|
||||
|
||||
function addHandlers() {
|
||||
socket.on('connect', onConnect);
|
||||
|
||||
socket.on('reconnecting', onReconnecting);
|
||||
@@ -41,6 +46,7 @@ app.isConnected = false;
|
||||
socket.on('event:banned', onEventBanned);
|
||||
|
||||
socket.on('event:alert', app.alert);
|
||||
}
|
||||
|
||||
function onConnect() {
|
||||
app.isConnected = true;
|
||||
|
||||
@@ -12,6 +12,8 @@ var accountsController = {
|
||||
chats: require('./accounts/chats'),
|
||||
session: require('./accounts/session'),
|
||||
blocks: require('./accounts/blocks'),
|
||||
uploads: require('./accounts/uploads'),
|
||||
consent: require('./accounts/consent'),
|
||||
};
|
||||
|
||||
module.exports = accountsController;
|
||||
|
||||
53
src/controllers/accounts/consent.js
Normal file
53
src/controllers/accounts/consent.js
Normal file
@@ -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;
|
||||
@@ -17,9 +17,17 @@ var editController = module.exports;
|
||||
editController.get = function (req, res, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
userData: function (next) {
|
||||
accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
|
||||
},
|
||||
function (userData, next) {
|
||||
canUseSignature: function (next) {
|
||||
privileges.global.can('signature', req.uid, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
var userData = results.userData;
|
||||
if (!userData) {
|
||||
return callback();
|
||||
}
|
||||
@@ -27,18 +35,23 @@ 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);
|
||||
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();
|
||||
|
||||
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 + ']]';
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -122,8 +122,9 @@ profileController.get = function (req, res, callback) {
|
||||
}
|
||||
);
|
||||
}
|
||||
userData.selectedGroup = userData.groups.find(function (group) {
|
||||
return group && group.name === userData.groupTitle;
|
||||
|
||||
userData.selectedGroup = userData.groups.filter(function (group) {
|
||||
return group && userData.groupTitleArray.includes(group.name);
|
||||
});
|
||||
|
||||
plugins.fireHook('filter:user.account', { userData: userData, uid: req.uid }, next);
|
||||
|
||||
57
src/controllers/accounts/uploads.js
Normal file
57
src/controllers/accounts/uploads.js
Normal file
@@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var db = require('../../database');
|
||||
var helpers = require('../helpers');
|
||||
var meta = require('../../meta');
|
||||
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);
|
||||
},
|
||||
uploadNames: function (next) {
|
||||
db.getSortedSetRevRange('uid:' + userData.uid + ':uploads', start, stop, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (results) {
|
||||
userData.uploads = results.uploadNames.map(function (uploadName) {
|
||||
return {
|
||||
name: uploadName,
|
||||
url: nconf.get('upload_url') + uploadName,
|
||||
};
|
||||
});
|
||||
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);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ var fs = require('fs');
|
||||
var jimp = require('jimp');
|
||||
|
||||
var meta = require('../../meta');
|
||||
var posts = require('../../posts');
|
||||
var file = require('../../file');
|
||||
var image = require('../../image');
|
||||
var plugins = require('../../plugins');
|
||||
@@ -41,23 +42,46 @@ uploadsController.get = function (req, res, next) {
|
||||
|
||||
filesToData(currentFolder, files, next);
|
||||
},
|
||||
function (files) {
|
||||
function (files, next) {
|
||||
// Float directories to the top
|
||||
files.sort(function (a, b) {
|
||||
if (a.isDirectory && !b.isDirectory) {
|
||||
return -1;
|
||||
} else if (!a.isDirectory && b.isDirectory) {
|
||||
return 1;
|
||||
} else if (!a.isDirectory && !b.isDirectory) {
|
||||
return a.mtime < b.mtime ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Add post usage info if in /files
|
||||
if (req.query.dir === '/files') {
|
||||
posts.uploads.getUsage(files, function (err, usage) {
|
||||
files.forEach(function (file, idx) {
|
||||
file.inPids = usage[idx].map(pid => parseInt(pid, 10));
|
||||
});
|
||||
|
||||
next(err, files);
|
||||
});
|
||||
} else {
|
||||
setImmediate(next, null, files);
|
||||
}
|
||||
},
|
||||
], function (err, files) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.render('admin/manage/uploads', {
|
||||
currentFolder: currentFolder.replace(nconf.get('upload_path'), ''),
|
||||
showPids: files[0].hasOwnProperty('inPids'),
|
||||
files: files,
|
||||
breadcrumbs: buildBreadcrumbs(currentFolder),
|
||||
pagination: pagination.create(page, Math.ceil(itemCount / itemsPerPage), req.query),
|
||||
});
|
||||
},
|
||||
], next);
|
||||
});
|
||||
};
|
||||
|
||||
function buildBreadcrumbs(currentFolder) {
|
||||
@@ -104,6 +128,7 @@ function filesToData(currentDir, files, callback) {
|
||||
sizeHumanReadable: (stat.size / 1024).toFixed(1) + 'KiB',
|
||||
isDirectory: stat.isDirectory(),
|
||||
isFile: stat.isFile(),
|
||||
mtime: stat.mtimeMs,
|
||||
});
|
||||
},
|
||||
], next);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,31 @@ 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);
|
||||
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);
|
||||
},
|
||||
function (data, next) {
|
||||
next(null, data.storedFile);
|
||||
@@ -254,5 +259,3 @@ function deleteTempFiles(files) {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = uploadsController;
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var winston = require('winston');
|
||||
var converter = require('json-2-csv');
|
||||
var archiver = require('archiver');
|
||||
|
||||
var db = require('../database');
|
||||
var user = require('../user');
|
||||
var meta = require('../meta');
|
||||
var posts = require('../posts');
|
||||
var batch = require('../batch');
|
||||
var events = require('../events');
|
||||
var accountHelpers = require('./accounts/helpers');
|
||||
|
||||
var userController = module.exports;
|
||||
@@ -97,3 +106,135 @@ 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);
|
||||
});
|
||||
};
|
||||
|
||||
userController.exportUploads = function (req, res, next) {
|
||||
const archivePath = path.join(__dirname, '../../build/export', req.params.uid + '_uploads.zip');
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 }, // Sets the compression level.
|
||||
});
|
||||
const maxAge = 1000 * 60 * 60 * 24; // 1 day
|
||||
|
||||
const rootDirectory = path.join(__dirname, '../../public/uploads/');
|
||||
const trimPath = function (path) {
|
||||
return path.replace(rootDirectory, '');
|
||||
};
|
||||
let isFresh = false;
|
||||
const sendFile = function () {
|
||||
events.log({
|
||||
type: 'export:uploads',
|
||||
uid: req.uid,
|
||||
targetUid: req.params.uid,
|
||||
ip: req.ip,
|
||||
fresh: isFresh,
|
||||
});
|
||||
|
||||
res.sendFile(req.params.uid + '_uploads.zip', {
|
||||
root: path.join(__dirname, '../../build/export'),
|
||||
headers: {
|
||||
'Content-Disposition': 'attachment; filename=' + req.params.uid + '_uploads.zip',
|
||||
maxAge: maxAge,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Check for existing file, if exists and is < 1 day in age, send this instead
|
||||
try {
|
||||
fs.accessSync(archivePath, fs.constants.F_OK | fs.constants.R_OK);
|
||||
isFresh = (Date.now() - fs.statSync(archivePath).mtimeMs) < maxAge;
|
||||
if (isFresh) {
|
||||
return sendFile();
|
||||
}
|
||||
} catch (err) {
|
||||
// File doesn't exist, continue
|
||||
}
|
||||
|
||||
const output = fs.createWriteStream(archivePath);
|
||||
output.on('close', sendFile);
|
||||
|
||||
archive.on('warning', function (err) {
|
||||
switch (err.code) {
|
||||
case 'ENOENT':
|
||||
winston.warn('[user/export/uploads] File not found: ' + trimPath(err.path));
|
||||
break;
|
||||
|
||||
default:
|
||||
winston.warn('[user/export/uploads] Unexpected warning: ' + err.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
archive.on('error', function (err) {
|
||||
switch (err.code) {
|
||||
case 'EACCES':
|
||||
winston.error('[user/export/uploads] File inaccessible: ' + trimPath(err.path));
|
||||
break;
|
||||
|
||||
default:
|
||||
winston.error('[user/export/uploads] Unable to construct archive: ' + err.message);
|
||||
break;
|
||||
}
|
||||
|
||||
res.sendStatus(500);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
winston.info('[user/export/uploads] Collating uploads for uid ' + req.params.uid);
|
||||
user.collateUploads(req.params.uid, archive, function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
};
|
||||
|
||||
userController.exportProfile = function (req, res, next) {
|
||||
async.waterfall([
|
||||
async.apply(db.getObjects.bind(db), ['user:1', 'user:1:settings']),
|
||||
function (objects, next) {
|
||||
Object.assign(objects[0], objects[1]);
|
||||
delete objects[0].password;
|
||||
|
||||
converter.json2csv(objects[0], next);
|
||||
},
|
||||
], function (err, csv) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'text/csv').set('Content-Disposition', 'attachment; filename="' + req.params.uid + '_profile.csv"').send(csv);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,6 +12,10 @@ var utils = require('./utils');
|
||||
|
||||
var events = module.exports;
|
||||
|
||||
/**
|
||||
* Useful options in data: type, uid, ip, targetUid
|
||||
* Everything else gets stringified and shown as pretty JSON string
|
||||
*/
|
||||
events.log = function (data, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
|
||||
12
src/file.js
12
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) {
|
||||
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) {
|
||||
|
||||
26
src/flags.js
26
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,7 +123,10 @@ Flags.list = function (filters, uid, callback) {
|
||||
var sets = [];
|
||||
var orSets = [];
|
||||
|
||||
if (Object.keys(filters).length > 0) {
|
||||
// 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)) {
|
||||
@@ -131,7 +136,6 @@ Flags.list = function (filters, uid, callback) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default
|
||||
|
||||
async.waterfall([
|
||||
@@ -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: utils.toISOString(flagObj.datetime),
|
||||
}));
|
||||
});
|
||||
}, 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);
|
||||
|
||||
@@ -361,7 +361,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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -26,6 +26,7 @@ require('./posts/votes')(Posts);
|
||||
require('./posts/bookmarks')(Posts);
|
||||
require('./posts/queue')(Posts);
|
||||
require('./posts/diffs')(Posts);
|
||||
require('./posts/uploads')(Posts);
|
||||
|
||||
Posts.exists = function (pid, callback) {
|
||||
db.isSortedSetMember('posts:pid', pid, callback);
|
||||
|
||||
@@ -101,6 +101,7 @@ module.exports = function (Posts) {
|
||||
function (next) {
|
||||
db.incrObjectField('global', 'postCount', next);
|
||||
},
|
||||
async.apply(Posts.uploads.sync, postData.pid),
|
||||
], function (err) {
|
||||
next(err);
|
||||
});
|
||||
|
||||
@@ -73,6 +73,7 @@ module.exports = function (Posts) {
|
||||
|
||||
Posts.diffs.save(data.pid, oldContent, data.content, next);
|
||||
},
|
||||
async.apply(Posts.uploads.sync, data.pid),
|
||||
function (next) {
|
||||
postData.cid = results.topic.cid;
|
||||
postData.topic = results.topic;
|
||||
|
||||
110
src/posts/uploads.js
Normal file
110
src/posts/uploads.js
Normal file
@@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var crypto = require('crypto');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
var db = require('../database');
|
||||
|
||||
module.exports = function (Posts) {
|
||||
Posts.uploads = {};
|
||||
|
||||
const md5 = filename => crypto.createHash('md5').update(filename).digest('hex');
|
||||
const pathPrefix = path.join(__dirname, '../../public/uploads/files');
|
||||
|
||||
Posts.uploads.sync = function (pid, callback) {
|
||||
// Scans a post and updates sorted set of uploads
|
||||
const searchRegex = /\/assets\/uploads\/files\/([^\s")]+\.?[\w]*)/g;
|
||||
|
||||
async.parallel({
|
||||
content: async.apply(Posts.getPostField, pid, 'content'),
|
||||
uploads: async.apply(Posts.uploads.list, pid),
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Extract upload file paths from post content
|
||||
let match = searchRegex.exec(data.content);
|
||||
const uploads = [];
|
||||
while (match) {
|
||||
uploads.push(match[1].replace('-resized', ''));
|
||||
match = searchRegex.exec(data.content);
|
||||
}
|
||||
|
||||
// Create add/remove sets
|
||||
const add = uploads.filter(path => !data.uploads.includes(path));
|
||||
const remove = data.uploads.filter(path => !uploads.includes(path));
|
||||
|
||||
async.parallel([
|
||||
async.apply(Posts.uploads.associate, pid, add),
|
||||
async.apply(Posts.uploads.dissociate, pid, remove),
|
||||
], function (err) {
|
||||
// Strictly return only err
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Posts.uploads.list = function (pid, callback) {
|
||||
// Returns array of this post's uploads
|
||||
db.getSortedSetRange('post:' + pid + ':uploads', 0, -1, callback);
|
||||
};
|
||||
|
||||
Posts.uploads.isOrphan = function (filePath, callback) {
|
||||
// Returns bool indicating whether a file is still CURRENTLY included in any posts
|
||||
db.sortedSetCard('upload:' + md5(filePath) + ':pids', function (err, length) {
|
||||
callback(err, length === 0);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.uploads.getUsage = function (filePaths, callback) {
|
||||
// Given an array of file names, determines which pids they are used in
|
||||
if (!Array.isArray(filePaths)) {
|
||||
filePaths = [filePaths];
|
||||
}
|
||||
|
||||
const keys = filePaths.map(fileObj => 'upload:' + md5(fileObj.name.replace('-resized', '')) + ':pids');
|
||||
async.map(keys, function (key, next) {
|
||||
db.getSortedSetRange(key, 0, -1, next);
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Posts.uploads.associate = function (pid, filePaths, callback) {
|
||||
// Adds an upload to a post's sorted set of uploads
|
||||
const now = Date.now();
|
||||
filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths;
|
||||
const scores = filePaths.map(() => now);
|
||||
|
||||
async.filter(filePaths, function (filePath, next) {
|
||||
// Only process files that exist
|
||||
fs.access(path.join(pathPrefix, filePath), fs.constants.F_OK | fs.constants.R_OK, function (err) {
|
||||
next(null, !err);
|
||||
});
|
||||
}, function (err, filePaths) {
|
||||
let methods = [async.apply(db.sortedSetAdd.bind(db), 'post:' + pid + ':uploads', scores, filePaths)];
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
methods = methods.concat(filePaths.map(path => async.apply(db.sortedSetAdd.bind(db), 'upload:' + md5(path) + ':pids', now, pid)));
|
||||
async.parallel(methods, function (err) {
|
||||
// Strictly return only err
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Posts.uploads.dissociate = function (pid, filePaths, callback) {
|
||||
// Removes an upload from a post's sorted set of uploads
|
||||
filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths;
|
||||
let methods = [async.apply(db.sortedSetRemove.bind(db), 'post:' + pid + ':uploads', filePaths)];
|
||||
methods = methods.concat(filePaths.map(path => async.apply(db.sortedSetRemove.bind(db), 'upload:' + md5(path) + ':pids', pid)));
|
||||
|
||||
async.parallel(methods, function (err) {
|
||||
// Strictly return only err
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -2,36 +2,48 @@
|
||||
|
||||
var async = require('async');
|
||||
var validator = require('validator');
|
||||
var _ = require('lodash');
|
||||
|
||||
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({
|
||||
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);
|
||||
},
|
||||
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) {
|
||||
return groupTitle && array.indexOf(groupTitle) === index;
|
||||
return userData && userData.groupTitleArray;
|
||||
});
|
||||
groupTitles = _.uniq(_.flatten(groupTitles));
|
||||
|
||||
groups.getGroupsData(groupTitles, next);
|
||||
},
|
||||
function (groupsData, next) {
|
||||
@@ -58,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;
|
||||
}
|
||||
@@ -67,14 +81,14 @@ 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 || parseInt(meta.config.disableSignatures, 10) === 1) {
|
||||
if (!userData.signature || !canUseSignature || parseInt(meta.config.disableSignatures, 10) === 1) {
|
||||
userData.signature = '';
|
||||
return next();
|
||||
}
|
||||
@@ -86,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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -30,6 +30,8 @@ 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);
|
||||
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);
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ 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('/user/uid/:uid/export/uploads', middleware.checkAccountPermissions, controllers.user.exportUploads);
|
||||
router.get('/user/uid/:uid/export/profile', middleware.checkAccountPermissions, controllers.user.exportProfile);
|
||||
|
||||
router.get('/:type/pid/:id', controllers.api.getObject);
|
||||
router.get('/:type/tid/:id', controllers.api.getObject);
|
||||
router.get('/:type/cid/:id', controllers.api.getObject);
|
||||
|
||||
@@ -340,3 +340,16 @@ SocketUser.setModerationNote = function (socket, data, callback) {
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketUser.deleteUpload = function (socket, data, callback) {
|
||||
if (!data || !data.name || !data.uid) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
user.deleteUpload(socket.uid, data.uid, data.name, callback);
|
||||
};
|
||||
|
||||
SocketUser.gdpr = {};
|
||||
|
||||
SocketUser.gdpr.consent = function (socket, data, callback) {
|
||||
user.setUserField(socket.uid, 'gdpr_consent', 1, callback);
|
||||
};
|
||||
|
||||
11
src/upgrades/1.8.0/give_signature_privileges.js
Normal file
11
src/upgrades/1.8.0/give_signature_privileges.js
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
21
src/upgrades/1.9.0/refresh_post_upload_associations.js
Normal file
21
src/upgrades/1.9.0/refresh_post_upload_associations.js
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var posts = require('../../posts');
|
||||
|
||||
module.exports = {
|
||||
name: 'Refresh post-upload associations',
|
||||
timestamp: Date.UTC(2018, 3, 16),
|
||||
method: function (callback) {
|
||||
var progress = this.progress;
|
||||
|
||||
require('../../batch').processSortedSet('posts:pid', function (pids, next) {
|
||||
async.each(pids, function (pid, next) {
|
||||
posts.uploads.sync(pid, next);
|
||||
progress.incr();
|
||||
}, next);
|
||||
}, {
|
||||
progress: this.progress,
|
||||
}, callback);
|
||||
},
|
||||
};
|
||||
30
src/user.js
30
src/user.js
@@ -37,6 +37,7 @@ require('./user/password')(User);
|
||||
require('./user/info')(User);
|
||||
require('./user/online')(User);
|
||||
require('./user/blocks')(User);
|
||||
require('./user/uploads')(User);
|
||||
|
||||
User.getUidsFromSet = function (set, start, stop, callback) {
|
||||
if (set === 'users:online') {
|
||||
@@ -349,7 +350,31 @@ User.getModeratedCids = function (uid, callback) {
|
||||
User.addInterstitials = function (callback) {
|
||||
plugins.registerHook('core', {
|
||||
hook: 'filter:register.interstitial',
|
||||
method: function (data, callback) {
|
||||
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.gdpr_consent ? null : new Error('[[register:gdpr_consent_denied]]'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(callback, null, data);
|
||||
},
|
||||
|
||||
// Forum Terms of Use
|
||||
function (data, callback) {
|
||||
if (meta.config.termsOfUse && !data.userData.acceptTos) {
|
||||
data.interstitials.push({
|
||||
template: 'partials/acceptTos',
|
||||
@@ -366,8 +391,9 @@ User.addInterstitials = function (callback) {
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, data);
|
||||
setImmediate(callback, null, data);
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
callback();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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,21 @@ module.exports = function (User) {
|
||||
}, { alwaysStartAt: 0 }, callback);
|
||||
}
|
||||
|
||||
function deleteUploads(uid, callback) {
|
||||
batch.processSortedSet('uid:' + uid + ':uploads', function (uploadNames, next) {
|
||||
async.waterfall([
|
||||
function (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', uploadNames, next);
|
||||
},
|
||||
], next);
|
||||
}, { alwaysStartAt: 0 }, callback);
|
||||
}
|
||||
|
||||
User.deleteAccount = function (uid, callback) {
|
||||
var userData;
|
||||
async.waterfall([
|
||||
|
||||
50
src/user/uploads.js
Normal file
50
src/user/uploads.js
Normal file
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var path = require('path');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var db = require('../database');
|
||||
var file = require('../file');
|
||||
var batch = require('../batch');
|
||||
|
||||
module.exports = function (User) {
|
||||
User.deleteUpload = function (callerUid, uid, uploadName, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
isUsersUpload: function (next) {
|
||||
db.isSortedSetMember('uid:' + callerUid + ':uploads', uploadName, next);
|
||||
},
|
||||
isAdminOrGlobalMod: function (next) {
|
||||
User.isAdminOrGlobalMod(callerUid, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
if (!results.isAdminOrGlobalMod && !results.isUsersUpload) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
file.delete(path.join(nconf.get('upload_path'), uploadName), next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetRemove('uid:' + uid + ':uploads', uploadName, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
User.collateUploads = function (uid, archive, callback) {
|
||||
batch.processSortedSet('uid:' + uid + ':uploads', function (files, next) {
|
||||
files.forEach(function (file) {
|
||||
archive.file(path.join(nconf.get('upload_path'), file), {
|
||||
name: path.basename(file),
|
||||
});
|
||||
});
|
||||
|
||||
setImmediate(next);
|
||||
}, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>[[admin/manage/uploads:filename]]</th>
|
||||
<!-- IF showPids --><th class="text-right">[[admin/manage/uploads:usage]]</th><!-- END -->
|
||||
<th class="text-right">[[admin/manage/uploads:size/filecount]]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -16,17 +17,28 @@
|
||||
<!-- BEGIN files -->
|
||||
<tr data-path="{files.path}">
|
||||
<!-- IF files.isDirectory -->
|
||||
<td class="col-md-9" role="button">
|
||||
<td class="col-md-6" role="button">
|
||||
<i class="fa fa-fw fa-folder-o"></i> <a href="{config.relative_path}/admin/manage/uploads?dir={files.path}">{files.name}</a>
|
||||
</td>
|
||||
<!-- ENDIF files.isDirectory -->
|
||||
|
||||
<!-- IF files.isFile -->
|
||||
<td class="col-md-9">
|
||||
<td class="col-md-6">
|
||||
<i class="fa fa-fw fa-file-text-o"></i> <a href="{config.relative_path}{files.url}" target="_blank">{files.name}</a>
|
||||
</td>
|
||||
<!-- ENDIF files.isFile -->
|
||||
|
||||
<!-- IF showPids -->
|
||||
<td class="col-md-3 text-right">
|
||||
<!-- BEGIN ../inPids -->
|
||||
<a target="_blank" href="{config.relative_path}/post/@value"><span class="label label-default">@value</span></a>
|
||||
<!-- END -->
|
||||
<!-- IF !../inPids.length -->
|
||||
<span class="label label-danger">[[admin/manage/uploads:orphaned]]</span>
|
||||
<!-- END -->
|
||||
</td>
|
||||
<!-- END -->
|
||||
|
||||
<td class="col-md-2 text-right"><!-- IF files.isFile -->{files.sizeHumanReadable}<!-- ELSE -->[[admin/manage/uploads:filecount, {files.fileCount}]]<!-- ENDIF files.isFile --></td>
|
||||
|
||||
<td role="button" class="col-md-1 text-right"><i class="delete fa fa-fw fa-trash-o <!-- IF !files.isFile --> hidden<!-- ENDIF !files.isFile -->"></i></td>
|
||||
|
||||
@@ -29,6 +29,17 @@
|
||||
[[admin/settings/group:allow-creation-help]]
|
||||
</p>
|
||||
|
||||
<div class="checkbox">
|
||||
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
||||
<input class="mdl-switch__input" type="checkbox" data-field="allowMultipleBadges">
|
||||
<span class="mdl-switch__label"><strong>Allow Multiple Badges</strong></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="help-block">
|
||||
[[admin/settings/group:allow-multiple-badges-help]]
|
||||
</p>
|
||||
|
||||
<label>[[admin/settings/group:max-name-length]]</label>
|
||||
<input class="form-control" type="text" placeholder="255" data-field="maximumGroupNameLength" />
|
||||
</form>
|
||||
|
||||
23
src/views/partials/gdpr_consent.tpl
Normal file
23
src/views/partials/gdpr_consent.tpl
Normal file
@@ -0,0 +1,23 @@
|
||||
<div class="form-group">
|
||||
<p class="lead">[[user:consent.lead]]</p>
|
||||
<p>[[user:consent.intro]]</p>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="gdpr_agree_data" id="gdpr_agree_data"> <strong>[[register:gdpr_agree_data]]</strong>
|
||||
</label>
|
||||
</div>
|
||||
<p>
|
||||
[[user:consent.email_intro]]
|
||||
<!-- IF digestEnabled -->
|
||||
[[user:consent.digest_frequency, {digestFrequency}]]
|
||||
<!-- ELSE -->
|
||||
[[user:consent.digest_off]]
|
||||
<!-- END -->
|
||||
</p>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="gdpr_agree_email" id="gdpr_agree_email"> <strong>[[register:gdpr_agree_email]]</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1336,17 +1336,21 @@ describe('Controllers', function () {
|
||||
name: 'selectedGroup',
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
groups.join('selectedGroup', fooUid, function (err) {
|
||||
user.create({ username: 'groupie' }, function (err, uid) {
|
||||
assert.ifError(err);
|
||||
request(nconf.get('url') + '/api/user/foo', { json: true }, function (err, res, body) {
|
||||
groups.join('selectedGroup', uid, function (err) {
|
||||
assert.ifError(err);
|
||||
request(nconf.get('url') + '/api/user/groupie', { json: true }, function (err, res, body) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert(body.selectedGroup.name, 'selectedGroup');
|
||||
assert(Array.isArray(body.selectedGroup));
|
||||
assert.equal(body.selectedGroup[0].name, 'selectedGroup');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should 404 if user does not exist', function (done) {
|
||||
groups.join('administrators', fooUid, function (err) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,7 +205,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) {
|
||||
|
||||
224
test/posts.js
224
test/posts.js
@@ -5,6 +5,9 @@ var assert = require('assert');
|
||||
var async = require('async');
|
||||
var request = require('request');
|
||||
var nconf = require('nconf');
|
||||
var crypto = require('crypto');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
var db = require('./mocks/databasemock');
|
||||
var topics = require('../src/topics');
|
||||
@@ -877,4 +880,225 @@ describe('Post\'s', function () {
|
||||
], done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload methods', function () {
|
||||
var pid;
|
||||
|
||||
before(function (done) {
|
||||
// Create stub files for testing
|
||||
['abracadabra.png', 'shazam.jpg', 'whoa.gif', 'amazeballs.jpg', 'wut.txt', 'test.bmp']
|
||||
.forEach(filename => fs.closeSync(fs.openSync(path.join(__dirname, '../public/uploads/files', filename), 'w')));
|
||||
|
||||
topics.post({
|
||||
uid: 1,
|
||||
cid: 1,
|
||||
title: 'topic with some images',
|
||||
content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png) and another [alt text](/assets/uploads/files/shazam.jpg)',
|
||||
}, function (err, topicPostData) {
|
||||
assert.ifError(err);
|
||||
pid = topicPostData.postData.pid;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('.sync()', function () {
|
||||
it('should properly add new images to the post\'s zset', function (done) {
|
||||
posts.uploads.sync(pid, function (err) {
|
||||
assert.ifError(err);
|
||||
|
||||
db.sortedSetCard('post:' + pid + ':uploads', function (err, length) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(2, length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove an image if it is edited out of the post', function (done) {
|
||||
async.series([
|
||||
function (next) {
|
||||
posts.edit({
|
||||
pid: pid,
|
||||
uid: 1,
|
||||
content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png)... AND NO MORE!',
|
||||
}, next);
|
||||
},
|
||||
async.apply(posts.uploads.sync, pid),
|
||||
], function (err) {
|
||||
assert.ifError(err);
|
||||
db.sortedSetCard('post:' + pid + ':uploads', function (err, length) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(1, length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.list()', function () {
|
||||
it('should display the uploaded files for a specific post', function (done) {
|
||||
posts.uploads.list(pid, function (err, uploads) {
|
||||
assert.ifError(err);
|
||||
assert.equal(true, Array.isArray(uploads));
|
||||
assert.strictEqual(1, uploads.length);
|
||||
assert.equal('string', typeof uploads[0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isOrphan()', function () {
|
||||
it('should return false if upload is not an orphan', function (done) {
|
||||
posts.uploads.isOrphan('abracadabra.png', function (err, isOrphan) {
|
||||
assert.ifError(err);
|
||||
assert.equal(false, isOrphan);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if upload is an orphan', function (done) {
|
||||
posts.uploads.isOrphan('shazam.jpg', function (err, isOrphan) {
|
||||
assert.ifError(err);
|
||||
assert.equal(true, isOrphan);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.associate()', function () {
|
||||
it('should add an image to the post\'s maintained list of uploads', function (done) {
|
||||
async.waterfall([
|
||||
async.apply(posts.uploads.associate, pid, 'whoa.gif'),
|
||||
async.apply(posts.uploads.list, pid),
|
||||
], function (err, uploads) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(2, uploads.length);
|
||||
assert.strictEqual(true, uploads.includes('whoa.gif'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow arrays to be passed in', function (done) {
|
||||
async.waterfall([
|
||||
async.apply(posts.uploads.associate, pid, ['amazeballs.jpg', 'wut.txt']),
|
||||
async.apply(posts.uploads.list, pid),
|
||||
], function (err, uploads) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(4, uploads.length);
|
||||
assert.strictEqual(true, uploads.includes('amazeballs.jpg'));
|
||||
assert.strictEqual(true, uploads.includes('wut.txt'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should save a reverse association of md5sum to pid', function (done) {
|
||||
const md5 = filename => crypto.createHash('md5').update(filename).digest('hex');
|
||||
|
||||
async.waterfall([
|
||||
async.apply(posts.uploads.associate, pid, ['test.bmp']),
|
||||
function (next) {
|
||||
db.getSortedSetRange('upload:' + md5('test.bmp') + ':pids', 0, -1, next);
|
||||
},
|
||||
], function (err, pids) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(true, Array.isArray(pids));
|
||||
assert.strictEqual(true, pids.length > 0);
|
||||
assert.equal(pid, pids[0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not associate a file that does not exist on the local disk', function (done) {
|
||||
async.waterfall([
|
||||
async.apply(posts.uploads.associate, pid, ['nonexistant.xls']),
|
||||
async.apply(posts.uploads.list, pid),
|
||||
], function (err, uploads) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(uploads.length, 5);
|
||||
assert.strictEqual(false, uploads.includes('nonexistant.xls'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.dissociate()', function () {
|
||||
it('should remove an image from the post\'s maintained list of uploads', function (done) {
|
||||
async.waterfall([
|
||||
async.apply(posts.uploads.dissociate, pid, 'whoa.gif'),
|
||||
async.apply(posts.uploads.list, pid),
|
||||
], function (err, uploads) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(4, uploads.length);
|
||||
assert.strictEqual(false, uploads.includes('whoa.gif'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow arrays to be passed in', function (done) {
|
||||
async.waterfall([
|
||||
async.apply(posts.uploads.dissociate, pid, ['amazeballs.jpg', 'wut.txt']),
|
||||
async.apply(posts.uploads.list, pid),
|
||||
], function (err, uploads) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(2, uploads.length);
|
||||
assert.strictEqual(false, uploads.includes('amazeballs.jpg'));
|
||||
assert.strictEqual(false, uploads.includes('wut.txt'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('post uploads management', function () {
|
||||
let topic;
|
||||
let reply;
|
||||
before(function (done) {
|
||||
topics.post({
|
||||
uid: 1,
|
||||
cid: cid,
|
||||
title: 'topic to test uploads with',
|
||||
content: '[abcdef](/assets/uploads/files/abracadabra.png)',
|
||||
}, function (err, topicPostData) {
|
||||
assert.ifError(err);
|
||||
topics.reply({
|
||||
uid: 1,
|
||||
tid: topicPostData.topicData.tid,
|
||||
timestamp: Date.now(),
|
||||
content: '[abcdef](/assets/uploads/files/shazam.jpg)',
|
||||
}, function (err, replyData) {
|
||||
assert.ifError(err);
|
||||
topic = topicPostData;
|
||||
reply = replyData;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should automatically sync uploads on topic create and reply', function (done) {
|
||||
db.sortedSetsCard(['post:' + topic.topicData.mainPid + ':uploads', 'post:' + reply.pid + ':uploads'], function (err, lengths) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(1, lengths[0]);
|
||||
assert.strictEqual(1, lengths[1]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should automatically sync uploads on post edit', function (done) {
|
||||
async.waterfall([
|
||||
async.apply(posts.edit, {
|
||||
pid: reply.pid,
|
||||
uid: 1,
|
||||
content: 'no uploads',
|
||||
}),
|
||||
function (postData, next) {
|
||||
posts.uploads.list(reply.pid, next);
|
||||
},
|
||||
], function (err, uploads) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(true, Array.isArray(uploads));
|
||||
assert.strictEqual(0, uploads.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -1434,6 +1434,7 @@ describe('User', function () {
|
||||
password: '123456',
|
||||
'password-confirm': '123456',
|
||||
email: '<script>alert("ok")<script>reject@me.com',
|
||||
gdpr_consent: true,
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
helpers.loginUser('admin', '123456', function (err, jar) {
|
||||
@@ -1465,6 +1466,7 @@ describe('User', function () {
|
||||
password: '123456',
|
||||
'password-confirm': '123456',
|
||||
email: 'accept@me.com',
|
||||
gdpr_consent: true,
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
socketAdmin.user.acceptRegistration({ uid: adminUid }, { username: 'acceptme' }, function (err, uid) {
|
||||
|
||||
Reference in New Issue
Block a user