mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-29 18:59:58 +01:00
Merge branch 'develop' into threads-enhancement
This commit is contained in:
@@ -4,6 +4,7 @@ var fs = require('fs');
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var sanitizeHTML = require('sanitize-html');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var utils = require('../../public/src/utils');
|
||||
var Translator = require('../../public/src/modules/translator').Translator;
|
||||
@@ -23,7 +24,7 @@ function filterDirectories(directories) {
|
||||
}
|
||||
|
||||
function getAdminNamespaces(callback) {
|
||||
utils.walk(path.resolve(__dirname, '../../public/templates/admin'), function (err, directories) {
|
||||
utils.walk(path.resolve(nconf.get('views_dir'), 'admin'), function (err, directories) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -60,7 +61,7 @@ var fallbackCacheInProgress = {};
|
||||
var fallbackCache = {};
|
||||
|
||||
function initFallback(namespace, callback) {
|
||||
fs.readFile(path.resolve(__dirname, '../../public/templates/', namespace + '.tpl'), function (err, file) {
|
||||
fs.readFile(path.resolve(nconf.get('views_dir'), namespace + '.tpl'), function (err, file) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
@@ -16,17 +16,21 @@ var uniquevisitors = 0;
|
||||
|
||||
var isCategory = /^(?:\/api)?\/category\/(\d+)/;
|
||||
|
||||
new cronJob('*/10 * * * *', function () {
|
||||
new cronJob('*/10 * * * * *', function () {
|
||||
Analytics.writeData();
|
||||
}, null, true);
|
||||
|
||||
Analytics.increment = function (keys) {
|
||||
Analytics.increment = function (keys, callback) {
|
||||
keys = Array.isArray(keys) ? keys : [keys];
|
||||
|
||||
keys.forEach(function (key) {
|
||||
counters[key] = counters[key] || 0;
|
||||
++counters[key];
|
||||
});
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
Analytics.pageView = function (payload) {
|
||||
|
||||
@@ -73,7 +73,7 @@ module.exports = function (Categories) {
|
||||
next(null, category);
|
||||
},
|
||||
function (category, next) {
|
||||
plugins.fireHook('action:category.create', category);
|
||||
plugins.fireHook('action:category.create', {category: category});
|
||||
next(null, category);
|
||||
}
|
||||
], callback);
|
||||
@@ -139,9 +139,20 @@ module.exports = function (Categories) {
|
||||
};
|
||||
|
||||
Categories.copyPrivilegesFrom = function (fromCid, toCid, callback) {
|
||||
async.each(privileges.privilegeList, function (privilege, next) {
|
||||
copyPrivilege(privilege, fromCid, toCid, next);
|
||||
}, callback);
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
plugins.fireHook('filter:categories.copyPrivilegesFrom', {
|
||||
privileges: privileges.privilegeList,
|
||||
fromCid: fromCid,
|
||||
toCid: toCid
|
||||
}, next);
|
||||
},
|
||||
function (data, next) {
|
||||
async.each(data.privileges, function (privilege, next) {
|
||||
copyPrivilege(privilege, data.fromCid, data.toCid, next);
|
||||
}, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
function copyPrivilege(privilege, fromCid, toCid, callback) {
|
||||
|
||||
@@ -31,7 +31,7 @@ module.exports = function (Categories) {
|
||||
purgeCategory(cid, next);
|
||||
},
|
||||
function (next) {
|
||||
plugins.fireHook('action:category.delete', cid);
|
||||
plugins.fireHook('action:category.delete', {cid: cid, uid: uid});
|
||||
next();
|
||||
}
|
||||
], callback);
|
||||
|
||||
@@ -119,7 +119,13 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
|
||||
userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
|
||||
userData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
|
||||
userData['email:confirmed'] = !!parseInt(userData['email:confirmed'], 10);
|
||||
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), isSelf);
|
||||
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), {
|
||||
self: isSelf,
|
||||
other: !isSelf,
|
||||
moderator: isModerator,
|
||||
globalMod: isGlobalModerator,
|
||||
admin: isAdmin
|
||||
});
|
||||
|
||||
userData.sso = results.sso.associations;
|
||||
userData.status = user.getStatus(userData);
|
||||
@@ -154,9 +160,30 @@ helpers.getBaseUser = function (userslug, callerUID, callback) {
|
||||
helpers.getUserDataByUserSlug(userslug, callerUID, callback);
|
||||
};
|
||||
|
||||
function filterLinks(links, self) {
|
||||
return links.filter(function (link) {
|
||||
return link && (link.public || self);
|
||||
function filterLinks(links, states) {
|
||||
return links.filter(function (link, index) {
|
||||
// "public" is the old property, if visibility is defined, discard `public`
|
||||
if (link.hasOwnProperty('public') && !link.hasOwnProperty('visibility')) {
|
||||
winston.warn('[account/profileMenu (' + link.id + ')] Use of the `.public` property is deprecated, use `visibility` now');
|
||||
return link && (link.public || states.self);
|
||||
}
|
||||
|
||||
// Default visibility
|
||||
link.visibility = Object.assign({
|
||||
self: true,
|
||||
other: true,
|
||||
moderator: true,
|
||||
globalMod: true,
|
||||
admin: true
|
||||
}, link.visibility);
|
||||
|
||||
// Iterate through states and permit if every test passes (or is not defined)
|
||||
var permit = Object.keys(states).some(function (state) {
|
||||
return states[state] === link.visibility[state];
|
||||
});
|
||||
|
||||
links[index].public = permit;
|
||||
return permit;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ var adminController = {
|
||||
dashboard: require('./admin/dashboard'),
|
||||
categories: require('./admin/categories'),
|
||||
tags: require('./admin/tags'),
|
||||
flags: require('./admin/flags'),
|
||||
blacklist: require('./admin/blacklist'),
|
||||
groups: require('./admin/groups'),
|
||||
appearance: require('./admin/appearance'),
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
var async = require('async');
|
||||
var validator = require('validator');
|
||||
|
||||
var posts = require('../../posts');
|
||||
var user = require('../../user');
|
||||
var categories = require('../../categories');
|
||||
var analytics = require('../../analytics');
|
||||
var pagination = require('../../pagination');
|
||||
|
||||
var flagsController = {};
|
||||
|
||||
var itemsPerPage = 20;
|
||||
|
||||
flagsController.get = function (req, res, next) {
|
||||
var byUsername = req.query.byUsername || '';
|
||||
var cid = req.query.cid || 0;
|
||||
var sortBy = req.query.sortBy || 'count';
|
||||
var page = parseInt(req.query.page, 10) || 1;
|
||||
|
||||
async.parallel({
|
||||
categories: function (next) {
|
||||
categories.buildForSelect(req.uid, next);
|
||||
},
|
||||
flagData: function (next) {
|
||||
getFlagData(req, res, next);
|
||||
},
|
||||
analytics: function (next) {
|
||||
analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
|
||||
},
|
||||
assignees: async.apply(user.getAdminsandGlobalModsandModerators)
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Minimise data set for assignees so tjs does less work
|
||||
results.assignees = results.assignees.map(function (userObj) {
|
||||
return {
|
||||
uid: userObj.uid,
|
||||
username: userObj.username
|
||||
};
|
||||
});
|
||||
|
||||
// If res.locals.cids is populated, then slim down the categories list
|
||||
if (res.locals.cids) {
|
||||
results.categories = results.categories.filter(function (category) {
|
||||
return res.locals.cids.indexOf(String(category.cid)) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage));
|
||||
|
||||
results.categories.forEach(function (category) {
|
||||
category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
|
||||
});
|
||||
|
||||
var data = {
|
||||
posts: results.flagData.posts,
|
||||
assignees: results.assignees,
|
||||
analytics: results.analytics,
|
||||
categories: results.categories,
|
||||
byUsername: validator.escape(String(byUsername)),
|
||||
sortByCount: sortBy === 'count',
|
||||
sortByTime: sortBy === 'time',
|
||||
pagination: pagination.create(page, pageCount, req.query),
|
||||
};
|
||||
res.render('admin/manage/flags', data);
|
||||
});
|
||||
};
|
||||
|
||||
function getFlagData(req, res, callback) {
|
||||
var sortBy = req.query.sortBy || 'count';
|
||||
var byUsername = req.query.byUsername || '';
|
||||
var cid = req.query.cid || res.locals.cids || 0;
|
||||
var page = parseInt(req.query.page, 10) || 1;
|
||||
var start = (page - 1) * itemsPerPage;
|
||||
var stop = start + itemsPerPage - 1;
|
||||
|
||||
var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'];
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (byUsername) {
|
||||
user.getUidByUsername(byUsername, next);
|
||||
} else {
|
||||
process.nextTick(next, null, 0);
|
||||
}
|
||||
},
|
||||
function (uid, next) {
|
||||
if (uid) {
|
||||
sets.push('uid:' + uid + ':flag:pids');
|
||||
}
|
||||
|
||||
posts.getFlags(sets, cid, req.uid, start, stop, next);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
|
||||
module.exports = flagsController;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
var meta = require('../../meta');
|
||||
|
||||
var settingsController = module.exports;
|
||||
@@ -25,7 +26,7 @@ function renderEmail(req, res, next) {
|
||||
var path = require('path');
|
||||
var utils = require('../../../public/src/utils');
|
||||
|
||||
var emailsPath = path.join(__dirname, '../../../public/templates/emails');
|
||||
var emailsPath = path.join(nconf.get('views_dir'), 'emails');
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
|
||||
@@ -7,9 +7,9 @@ var themesController = {};
|
||||
|
||||
themesController.get = function (req, res, next) {
|
||||
var themeDir = path.join(__dirname, '../../../node_modules/' + req.params.theme);
|
||||
file.exists(themeDir, function (exists) {
|
||||
if (!exists) {
|
||||
return next();
|
||||
file.exists(themeDir, function (err, exists) {
|
||||
if (err || !exists) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var themeConfig = require(path.join(themeDir, 'theme.json')),
|
||||
|
||||
@@ -105,14 +105,10 @@ uploadsController.uploadSound = function (req, res, next) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var soundsPath = path.join(__dirname, '../../../public/sounds'),
|
||||
var soundsPath = path.join(__dirname, '../../../build/public/sounds'),
|
||||
filePath = path.join(__dirname, '../../../public/uploads/sounds', uploadedFile.name);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
fs.link(filePath, path.join(soundsPath, path.basename(filePath)));
|
||||
} else {
|
||||
fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file');
|
||||
}
|
||||
file.link(filePath, path.join(soundsPath, path.basename(filePath)));
|
||||
|
||||
fs.unlink(uploadedFile.path, function (err) {
|
||||
if (err) {
|
||||
|
||||
@@ -128,7 +128,11 @@ function getUsers(set, section, min, max, req, res, next) {
|
||||
if (byScore) {
|
||||
db.sortedSetCount(set, min, max, next);
|
||||
} else {
|
||||
db.sortedSetCard(set, next);
|
||||
if (set === 'users:banned' || set === 'users:notvalidated') {
|
||||
db.sortedSetCard(set, next);
|
||||
} else {
|
||||
db.getObjectField('global', 'userCount', next);
|
||||
}
|
||||
}
|
||||
},
|
||||
users: function (next) {
|
||||
|
||||
@@ -332,7 +332,7 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) {
|
||||
// Force session check for all connected socket.io clients with the same session id
|
||||
sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
|
||||
|
||||
plugins.fireHook('action:user.loggedIn', uid);
|
||||
plugins.fireHook('action:user.loggedIn', {uid: uid, req: req});
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,24 +3,127 @@
|
||||
var async = require('async');
|
||||
|
||||
var user = require('../user');
|
||||
var adminFlagsController = require('./admin/flags');
|
||||
var categories = require('../categories');
|
||||
var flags = require('../flags');
|
||||
var analytics = require('../analytics');
|
||||
|
||||
var modsController = {};
|
||||
var modsController = {
|
||||
flags: {}
|
||||
};
|
||||
|
||||
modsController.flagged = function (req, res, next) {
|
||||
modsController.flags.list = function (req, res, next) {
|
||||
async.parallel({
|
||||
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
|
||||
moderatedCids: async.apply(user.getModeratedCids, req.uid)
|
||||
}, function (err, results) {
|
||||
if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
if (!results.isAdminOrGlobalMod && results.moderatedCids.length) {
|
||||
res.locals.cids = results.moderatedCids;
|
||||
}
|
||||
|
||||
adminFlagsController.get(req, res, next);
|
||||
// Parse query string params for filters
|
||||
var hasFilter = false;
|
||||
var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick'];
|
||||
var filters = valid.reduce(function (memo, cur) {
|
||||
if (req.query.hasOwnProperty(cur)) {
|
||||
memo[cur] = req.query[cur];
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
hasFilter = !!Object.keys(filters).length;
|
||||
|
||||
if (res.locals.cids) {
|
||||
if (!filters.cid) {
|
||||
// If mod and no cid filter, add filter for their modded categories
|
||||
filters.cid = res.locals.cids;
|
||||
} else {
|
||||
// Remove cids they do not moderate
|
||||
if (Array.isArray(filters.cid)) {
|
||||
filters.cid = filters.cid.filter(function (cid) {
|
||||
return res.locals.cids.indexOf(String(cid)) !== -1;
|
||||
});
|
||||
} else if (res.locals.cids.indexOf(String(filters.cid)) === -1) {
|
||||
filters.cid = res.locals.cids;
|
||||
hasFilter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async.parallel({
|
||||
flags: async.apply(flags.list, filters, req.uid),
|
||||
analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30),
|
||||
categories: async.apply(categories.buildForSelect, req.uid)
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// If res.locals.cids is populated, then slim down the categories list
|
||||
if (res.locals.cids) {
|
||||
data.categories = data.categories.filter(function (category) {
|
||||
return res.locals.cids.indexOf(String(category.cid)) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
// Minimal returned set for templates.js
|
||||
data.categories = data.categories.reduce(function (memo, cur) {
|
||||
if (!res.locals.cids) {
|
||||
memo[cur.cid] = cur.name;
|
||||
return memo;
|
||||
}
|
||||
|
||||
// If mod, remove categories they can't moderate
|
||||
if (res.locals.cids.indexOf(String(cur.cid)) !== -1) {
|
||||
memo[cur.cid] = cur.name;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
res.render('flags/list', {
|
||||
flags: data.flags,
|
||||
analytics: data.analytics,
|
||||
categories: data.categories,
|
||||
hasFilter: hasFilter,
|
||||
filters: filters,
|
||||
title: '[[pages:flags]]'
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
modsController.flags.detail = function (req, res, next) {
|
||||
async.parallel({
|
||||
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
|
||||
moderatedCids: async.apply(user.getModeratedCids, req.uid),
|
||||
flagData: async.apply(flags.get, req.params.flagId),
|
||||
assignees: async.apply(user.getAdminsandGlobalModsandModerators)
|
||||
}, function (err, results) {
|
||||
if (err || !results.flagData) {
|
||||
return next(err || new Error('[[error:invalid-data]]'));
|
||||
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
res.render('flags/detail', Object.assign(results.flagData, {
|
||||
assignees: results.assignees,
|
||||
type_bool: ['post', 'user', 'empty'].reduce(function (memo, cur) {
|
||||
if (cur !== 'empty') {
|
||||
memo[cur] = results.flagData.type === cur && !!Object.keys(results.flagData.target).length;
|
||||
} else {
|
||||
memo[cur] = !Object.keys(results.flagData.target).length;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {}),
|
||||
title: '[[pages:flag-details, ' + req.params.flagId + ']]'
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ searchController.search = function (req, res, next) {
|
||||
repliesFilter: req.query.repliesFilter,
|
||||
timeRange: req.query.timeRange,
|
||||
timeFilter: req.query.timeFilter,
|
||||
sortBy: req.query.sortBy,
|
||||
sortBy: req.query.sortBy || meta.config.searchDefaultSortBy || '',
|
||||
sortDirection: req.query.sortDirection,
|
||||
page: page,
|
||||
uid: req.uid,
|
||||
@@ -67,6 +67,7 @@ searchController.search = function (req, res, next) {
|
||||
searchData.title = '[[global:header.search]]';
|
||||
searchData.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]);
|
||||
searchData.expandSearch = !req.query.term;
|
||||
searchData.searchDefaultSortBy = meta.config.searchDefaultSortBy || '';
|
||||
|
||||
res.render('search', searchData);
|
||||
});
|
||||
|
||||
@@ -40,7 +40,15 @@ unreadController.get = function (req, res, next) {
|
||||
settings = results.settings;
|
||||
var start = Math.max(0, (page - 1) * settings.topicsPerPage);
|
||||
var stop = start + settings.topicsPerPage - 1;
|
||||
topics.getUnreadTopics(cid, req.uid, start, stop, filter, next);
|
||||
var cutoff = req.session.unreadCutoff ? req.session.unreadCutoff : topics.unreadCutoff();
|
||||
topics.getUnreadTopics({
|
||||
cid: cid,
|
||||
uid: req.uid,
|
||||
start: start,
|
||||
stop: stop,
|
||||
filter: filter,
|
||||
cutoff: cutoff
|
||||
}, next);
|
||||
}
|
||||
], function (err, data) {
|
||||
if (err) {
|
||||
|
||||
@@ -26,7 +26,7 @@ function getCover(type, id) {
|
||||
return covers[id];
|
||||
}
|
||||
|
||||
return nconf.get('relative_path') + '/images/cover-default.png';
|
||||
return nconf.get('relative_path') + '/assets/images/cover-default.png';
|
||||
}
|
||||
|
||||
module.exports = coverPhoto;
|
||||
|
||||
33
src/file.js
33
src/file.js
@@ -87,19 +87,40 @@ file.allowedExtensions = function () {
|
||||
|
||||
file.exists = function (path, callback) {
|
||||
fs.stat(path, function (err, stat) {
|
||||
callback(!err && stat);
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
file.existsSync = function (path) {
|
||||
var exists = false;
|
||||
try {
|
||||
exists = fs.statSync(path);
|
||||
} catch(err) {
|
||||
exists = false;
|
||||
fs.statSync(path);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return !!exists;
|
||||
return true;
|
||||
};
|
||||
|
||||
file.link = function link(filePath, destPath, cb) {
|
||||
if (process.platform === 'win32') {
|
||||
fs.link(filePath, destPath, cb);
|
||||
} else {
|
||||
fs.symlink(filePath, destPath, 'file', cb);
|
||||
}
|
||||
};
|
||||
|
||||
file.linkDirs = function linkDirs(sourceDir, destDir, callback) {
|
||||
var type = (process.platform === 'win32') ? 'junction' : 'dir';
|
||||
fs.symlink(sourceDir, destDir, type, callback);
|
||||
};
|
||||
|
||||
module.exports = file;
|
||||
|
||||
663
src/flags.js
Normal file
663
src/flags.js
Normal file
@@ -0,0 +1,663 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var db = require('./database');
|
||||
var user = require('./user');
|
||||
var groups = require('./groups');
|
||||
var meta = require('./meta');
|
||||
var notifications = require('./notifications');
|
||||
var analytics = require('./analytics');
|
||||
var topics = require('./topics');
|
||||
var posts = require('./posts');
|
||||
var privileges = require('./privileges');
|
||||
var plugins = require('./plugins');
|
||||
var utils = require('../public/src/utils');
|
||||
var _ = require('underscore');
|
||||
var S = require('string');
|
||||
|
||||
var Flags = {};
|
||||
|
||||
Flags.get = function (flagId, callback) {
|
||||
async.waterfall([
|
||||
// First stage
|
||||
async.apply(async.parallel, {
|
||||
base: async.apply(db.getObject.bind(db), 'flag:' + flagId),
|
||||
history: async.apply(Flags.getHistory, flagId),
|
||||
notes: async.apply(Flags.getNotes, flagId)
|
||||
}),
|
||||
function (data, next) {
|
||||
// Second stage
|
||||
async.parallel({
|
||||
userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture']),
|
||||
targetObj: async.apply(Flags.getTarget, data.base.type, data.base.targetId, data.base.uid)
|
||||
}, function (err, payload) {
|
||||
// Final object return construction
|
||||
next(err, Object.assign(data.base, {
|
||||
datetimeISO: new Date(parseInt(data.base.datetime, 10)).toISOString(),
|
||||
target_readable: data.base.type.charAt(0).toUpperCase() + data.base.type.slice(1) + ' ' + data.base.targetId,
|
||||
target: payload.targetObj,
|
||||
history: data.history,
|
||||
notes: data.notes,
|
||||
reporter: payload.userObj
|
||||
}));
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.list = function (filters, uid, callback) {
|
||||
if (typeof filters === 'function' && !uid && !callback) {
|
||||
callback = filters;
|
||||
filters = {};
|
||||
}
|
||||
|
||||
var sets = [];
|
||||
var orSets = [];
|
||||
var prepareSets = function (setPrefix, value) {
|
||||
if (!Array.isArray(value)) {
|
||||
sets.push(setPrefix + value);
|
||||
} else if (value.length) {
|
||||
value.forEach(function (x) {
|
||||
orSets.push(setPrefix + x);
|
||||
});
|
||||
} else {
|
||||
// Empty array, do nothing
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (Object.keys(filters).length > 0) {
|
||||
for (var type in filters) {
|
||||
switch (type) {
|
||||
case 'type':
|
||||
prepareSets('flags:byType:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'state':
|
||||
prepareSets('flags:byState:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'reporterId':
|
||||
prepareSets('flags:byReporter:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
prepareSets('flags:byAssignee:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'targetUid':
|
||||
prepareSets('flags:byTargetUid:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'cid':
|
||||
prepareSets('flags:byCid:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'quick':
|
||||
switch (filters.quick) {
|
||||
case 'mine':
|
||||
sets.push('flags:byAssignee:' + uid);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (sets.length === 1) {
|
||||
db.getSortedSetRevRange(sets[0], 0, -1, next);
|
||||
} else if (sets.length > 1) {
|
||||
db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }, next);
|
||||
} else {
|
||||
next(null, []);
|
||||
}
|
||||
},
|
||||
function (flagIds, next) {
|
||||
// Find flags according to "or" rules, if any
|
||||
if (orSets.length) {
|
||||
db.getSortedSetRevUnion({ sets: orSets, start: 0, stop: -1, aggregate: 'MAX' }, function (err, _flagIds) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (sets.length) {
|
||||
// If flag ids are already present, return a subset of flags that are in both sets
|
||||
next(null, _.intersection(flagIds, _flagIds));
|
||||
} else {
|
||||
// Otherwise, return all flags returned via orSets
|
||||
next(null, _.union(flagIds, _flagIds));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setImmediate(next, null, flagIds);
|
||||
}
|
||||
},
|
||||
function (flagIds, next) {
|
||||
async.map(flagIds, function (flagId, next) {
|
||||
async.waterfall([
|
||||
async.apply(db.getObject, 'flag:' + flagId),
|
||||
function (flagObj, next) {
|
||||
user.getUserFields(flagObj.uid, ['username', 'picture'], function (err, userObj) {
|
||||
next(err, Object.assign(flagObj, {
|
||||
reporter: {
|
||||
username: userObj.username,
|
||||
picture: userObj.picture,
|
||||
'icon:bgColor': userObj['icon:bgColor'],
|
||||
'icon:text': userObj['icon:text']
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
], function (err, flagObj) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
switch(flagObj.state) {
|
||||
case 'open':
|
||||
flagObj.labelClass = 'info';
|
||||
break;
|
||||
case 'wip':
|
||||
flagObj.labelClass = 'warning';
|
||||
break;
|
||||
case 'resolved':
|
||||
flagObj.labelClass = 'success';
|
||||
break;
|
||||
case 'rejected':
|
||||
flagObj.labelClass = 'danger';
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, Object.assign(flagObj, {
|
||||
target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId,
|
||||
datetimeISO: new Date(parseInt(flagObj.datetime, 10)).toISOString()
|
||||
}));
|
||||
});
|
||||
}, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.validate = function (payload, callback) {
|
||||
async.parallel({
|
||||
targetExists: async.apply(Flags.targetExists, payload.type, payload.id),
|
||||
target: async.apply(Flags.getTarget, payload.type, payload.id, payload.uid),
|
||||
reporter: async.apply(user.getUserData, payload.uid)
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (data.target.deleted) {
|
||||
return callback(new Error('[[error:post-deleted]]'));
|
||||
} else if (parseInt(data.reporter.banned, 10)) {
|
||||
return callback(new Error('[[error:user-banned]]'));
|
||||
}
|
||||
|
||||
switch (payload.type) {
|
||||
case 'post':
|
||||
privileges.posts.canEdit(payload.id, payload.uid, function (err, editable) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
|
||||
if (!editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
|
||||
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
privileges.users.canEdit(payload.uid, payload.id, function (err, editable) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
|
||||
if (!editable && parseInt(data.reporter.reputation, 10) < minimumReputation) {
|
||||
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Flags.getNotes = function (flagId, callback) {
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':notes', 0, -1),
|
||||
function (notes, next) {
|
||||
var uids = [];
|
||||
var noteObj;
|
||||
notes = notes.map(function (note) {
|
||||
try {
|
||||
noteObj = JSON.parse(note.value);
|
||||
uids.push(noteObj[0]);
|
||||
return {
|
||||
uid: noteObj[0],
|
||||
content: noteObj[1],
|
||||
datetime: note.score,
|
||||
datetimeISO: new Date(parseInt(note.score, 10)).toISOString()
|
||||
};
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
next(null, notes, uids);
|
||||
},
|
||||
function (notes, uids, next) {
|
||||
user.getUsersFields(uids, ['username', 'userslug', 'picture'], function (err, users) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, notes.map(function (note, idx) {
|
||||
note.user = users[idx];
|
||||
return note;
|
||||
}));
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.create = function (type, id, uid, reason, timestamp, callback) {
|
||||
var targetUid;
|
||||
var targetCid;
|
||||
var doHistoryAppend = false;
|
||||
|
||||
// timestamp is optional
|
||||
if (typeof timestamp === 'function' && !callback) {
|
||||
callback = timestamp;
|
||||
timestamp = Date.now();
|
||||
doHistoryAppend = true;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
// Sanity checks
|
||||
async.apply(Flags.exists, type, id, uid),
|
||||
async.apply(Flags.targetExists, type, id),
|
||||
|
||||
// Extra data for zset insertion
|
||||
async.apply(Flags.getTargetUid, type, id),
|
||||
async.apply(Flags.getTargetCid, type, id)
|
||||
], function (err, checks) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
targetUid = checks[2] || null;
|
||||
targetCid = checks[3] || null;
|
||||
|
||||
if (checks[0]) {
|
||||
return next(new Error('[[error:already-flagged]]'));
|
||||
} else if (!checks[1]) {
|
||||
return next(new Error('[[error:invalid-data]]'));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
async.apply(db.incrObjectField, 'global', 'nextFlagId'),
|
||||
function (flagId, next) {
|
||||
var tasks = [
|
||||
async.apply(db.setObject.bind(db), 'flag:' + flagId, {
|
||||
flagId: flagId,
|
||||
type: type,
|
||||
targetId: id,
|
||||
description: reason,
|
||||
uid: uid,
|
||||
datetime: timestamp
|
||||
}),
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', timestamp, flagId), // by time, the default
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, timestamp, flagId), // by reporter
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, timestamp, flagId), // by flag type
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:hash', flagId, [type, id, uid].join(':')), // save zset for duplicate checking
|
||||
async.apply(analytics.increment, 'flags') // some fancy analytics
|
||||
];
|
||||
|
||||
if (targetUid) {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid
|
||||
}
|
||||
if (targetCid) {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byCid:' + targetCid, timestamp, flagId)); // by target cid
|
||||
}
|
||||
if (type === 'post') {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byPid:' + id, timestamp, flagId)); // by target pid
|
||||
}
|
||||
|
||||
async.parallel(tasks, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (doHistoryAppend) {
|
||||
Flags.update(flagId, uid, { "state": "open" });
|
||||
}
|
||||
|
||||
next(null, flagId);
|
||||
});
|
||||
},
|
||||
async.apply(Flags.get)
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.exists = function (type, id, uid, callback) {
|
||||
db.isSortedSetMember('flags:hash', [type, id, uid].join(':'), callback);
|
||||
};
|
||||
|
||||
Flags.getTarget = function (type, id, uid, callback) {
|
||||
async.waterfall([
|
||||
async.apply(Flags.targetExists, type, id),
|
||||
function (exists, next) {
|
||||
if (exists) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
async.waterfall([
|
||||
async.apply(posts.getPostsByPids, [id], uid),
|
||||
function (posts, next) {
|
||||
topics.addPostData(posts, uid, next);
|
||||
}
|
||||
], function (err, posts) {
|
||||
next(err, posts[0]);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user.getUsersData([id], function (err, users) {
|
||||
next(err, users ? users[0] : undefined);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
next(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Target used to exist (otherwise flag creation'd fail), but no longer
|
||||
next(null, {});
|
||||
}
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.targetExists = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.exists(id, callback);
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user.exists(id, callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.getTargetUid = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.getPostField(id, 'uid', callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
setImmediate(callback, null, id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.getTargetCid = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.getCidByPid(id, callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
setImmediate(callback, null, id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.update = function (flagId, uid, changeset, callback) {
|
||||
// Retrieve existing flag data to compare for history-saving purposes
|
||||
var fields = ['state', 'assignee'];
|
||||
var tasks = [];
|
||||
var now = changeset.datetime || Date.now();
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields),
|
||||
function (current, next) {
|
||||
for (var prop in changeset) {
|
||||
if (changeset.hasOwnProperty(prop)) {
|
||||
if (current[prop] === changeset[prop]) {
|
||||
delete changeset[prop];
|
||||
} else {
|
||||
// Add tasks as necessary
|
||||
switch (prop) {
|
||||
case 'state':
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byState:' + changeset[prop], now, flagId));
|
||||
tasks.push(async.apply(db.sortedSetRemove.bind(db), 'flags:byState:' + current[prop], flagId));
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byAssignee:' + changeset[prop], now, flagId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(changeset).length) {
|
||||
// No changes
|
||||
return next();
|
||||
}
|
||||
|
||||
// Save new object to db (upsert)
|
||||
tasks.push(async.apply(db.setObject, 'flag:' + flagId, changeset));
|
||||
|
||||
// Append history
|
||||
tasks.push(async.apply(Flags.appendHistory, flagId, uid, changeset));
|
||||
|
||||
// Fire plugin hook
|
||||
tasks.push(async.apply(plugins.fireHook, 'action:flag.update', { flagId: flagId, changeset: changeset, uid: uid }));
|
||||
|
||||
async.parallel(tasks, function (err, data) {
|
||||
return next(err);
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.getHistory = function (flagId, callback) {
|
||||
var history;
|
||||
var uids = [];
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':history', 0, -1),
|
||||
function (_history, next) {
|
||||
history = _history.map(function (entry) {
|
||||
try {
|
||||
entry.value = JSON.parse(entry.value);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
uids.push(entry.value[0]);
|
||||
|
||||
// Deserialise changeset
|
||||
var changeset = entry.value[1];
|
||||
if (changeset.hasOwnProperty('state')) {
|
||||
changeset.state = changeset.state === undefined ? '' : '[[flags:state-' + changeset.state + ']]';
|
||||
}
|
||||
|
||||
return {
|
||||
uid: entry.value[0],
|
||||
fields: changeset,
|
||||
datetime: entry.score,
|
||||
datetimeISO: new Date(parseInt(entry.score, 10)).toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
user.getUsersFields(uids, ['username', 'userslug', 'picture'], next);
|
||||
}
|
||||
], function (err, users) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Append user data to each history event
|
||||
history = history.map(function (event, idx) {
|
||||
event.user = users[idx];
|
||||
return event;
|
||||
});
|
||||
|
||||
callback(null, history);
|
||||
});
|
||||
};
|
||||
|
||||
Flags.appendHistory = function (flagId, uid, changeset, callback) {
|
||||
var payload;
|
||||
var datetime = changeset.datetime || Date.now();
|
||||
delete changeset.datetime;
|
||||
|
||||
try {
|
||||
payload = JSON.stringify([uid, changeset, datetime]);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
db.sortedSetAdd('flag:' + flagId + ':history', datetime, payload, callback);
|
||||
};
|
||||
|
||||
Flags.appendNote = function (flagId, uid, note, datetime, callback) {
|
||||
if (typeof datetime === 'function' && !callback) {
|
||||
callback = datetime;
|
||||
datetime = Date.now();
|
||||
}
|
||||
|
||||
var payload;
|
||||
try {
|
||||
payload = JSON.stringify([uid, note]);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', datetime, payload),
|
||||
async.apply(Flags.appendHistory, flagId, uid, {
|
||||
notes: null,
|
||||
datetime: datetime
|
||||
})
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.notify = function (flagObj, uid, callback) {
|
||||
// Notify administrators, mods, and other associated people
|
||||
if (!callback) {
|
||||
callback = function () {};
|
||||
}
|
||||
|
||||
switch (flagObj.type) {
|
||||
case 'post':
|
||||
async.parallel({
|
||||
post: function (next) {
|
||||
async.waterfall([
|
||||
async.apply(posts.getPostData, flagObj.targetId),
|
||||
async.apply(posts.parsePost)
|
||||
], next);
|
||||
},
|
||||
title: async.apply(topics.getTitleByPid, flagObj.targetId),
|
||||
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
|
||||
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
|
||||
moderators: function (next) {
|
||||
async.waterfall([
|
||||
async.apply(posts.getCidByPid, flagObj.targetId),
|
||||
function (cid, next) {
|
||||
groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, next);
|
||||
}
|
||||
], next);
|
||||
}
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var title = S(results.title).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: flagObj.description,
|
||||
pid: flagObj.targetId,
|
||||
path: '/post/' + flagObj.targetId,
|
||||
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
|
||||
from: uid,
|
||||
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
|
||||
topicTitle: results.title
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:flag.create', {
|
||||
flag: flagObj
|
||||
});
|
||||
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), callback);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
async.parallel({
|
||||
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
|
||||
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reporter.username + ', ' + flagObj.target.username + ']]',
|
||||
bodyLong: flagObj.description,
|
||||
path: '/uid/' + flagObj.targetId,
|
||||
nid: 'flag:user:' + flagObj.targetId + ':uid:' + uid,
|
||||
from: uid,
|
||||
mergeId: 'notifications:user_flagged_user|' + flagObj.targetId
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:flag.create', {
|
||||
flag: flagObj
|
||||
});
|
||||
notifications.push(notification, results.admins.concat(results.globalMods), callback);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Flags;
|
||||
@@ -432,12 +432,8 @@ var utils = require('../public/src/utils');
|
||||
db.getSortedSetRevRange(set, 0, -1, next);
|
||||
},
|
||||
function (groupNames, next) {
|
||||
var groupSets = groupNames.map(function (name) {
|
||||
return 'group:' + name + ':members';
|
||||
});
|
||||
|
||||
async.map(uids, function (uid, next) {
|
||||
db.isMemberOfSortedSets(groupSets, uid, function (err, isMembers) {
|
||||
Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ module.exports = function (Groups) {
|
||||
async.series(tasks, next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:group.create', groupData);
|
||||
plugins.fireHook('action:group.create', {group: groupData});
|
||||
next(null, groupData);
|
||||
}
|
||||
], callback);
|
||||
|
||||
@@ -17,8 +17,6 @@ module.exports = function (Groups) {
|
||||
}
|
||||
var groupObj = groupsData[0];
|
||||
|
||||
plugins.fireHook('action:group.destroy', groupObj);
|
||||
|
||||
async.parallel([
|
||||
async.apply(db.delete, 'group:' + groupName),
|
||||
async.apply(db.sortedSetRemove, 'groups:createtime', groupName),
|
||||
@@ -45,6 +43,7 @@ module.exports = function (Groups) {
|
||||
return callback(err);
|
||||
}
|
||||
Groups.resetCache();
|
||||
plugins.fireHook('action:group.destroy', {group: groupObj});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,9 @@ image.resizeImage = function (data, callback) {
|
||||
}
|
||||
},
|
||||
function (image, next) {
|
||||
if (data.write === false) {
|
||||
return next();
|
||||
}
|
||||
image.write(data.target || data.path, next);
|
||||
}
|
||||
], function (err) {
|
||||
|
||||
@@ -15,7 +15,14 @@ module.exports = function (Messaging) {
|
||||
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
|
||||
|
||||
Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) {
|
||||
Messaging.getUidsInRoom(roomId, 0, -1, function (err, uids) {
|
||||
async.parallel({
|
||||
uids: function (next) {
|
||||
Messaging.getUidsInRoom(roomId, 0, -1, next);
|
||||
},
|
||||
roomData: function (next) {
|
||||
Messaging.getRoomData(roomId, next);
|
||||
}
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
@@ -23,9 +30,10 @@ module.exports = function (Messaging) {
|
||||
var data = {
|
||||
roomId: roomId,
|
||||
fromUid: fromUid,
|
||||
message: messageObj
|
||||
message: messageObj,
|
||||
roomName: results.roomData.roomName
|
||||
};
|
||||
uids.forEach(function (uid) {
|
||||
results.uids.forEach(function (uid) {
|
||||
data.self = parseInt(uid, 10) === parseInt(fromUid) ? 1 : 0;
|
||||
Messaging.pushUnreadCount(uid);
|
||||
sockets.in('uid_' + uid).emit('event:chats.receive', data);
|
||||
@@ -43,7 +51,7 @@ module.exports = function (Messaging) {
|
||||
}
|
||||
|
||||
queueObj.timeout = setTimeout(function () {
|
||||
sendNotifications(fromUid, uids, roomId, queueObj.message, function (err) {
|
||||
sendNotifications(fromUid, results.uids, roomId, queueObj.message, function (err) {
|
||||
if (!err) {
|
||||
delete Messaging.notifyQueue[fromUid + ':' + roomId];
|
||||
}
|
||||
|
||||
@@ -43,20 +43,36 @@ exports.build = function build(targets, callback) {
|
||||
};
|
||||
|
||||
exports.buildTargets = function (targets, callback) {
|
||||
var cacheBuster = require('./cacheBuster');
|
||||
var meta = require('../meta');
|
||||
var numCpus = require('os').cpus().length;
|
||||
var strategy = (targets.length > 1 && numCpus > 1);
|
||||
|
||||
buildStart = buildStart || Date.now();
|
||||
|
||||
var step = function (startTime, target, next) {
|
||||
var step = function (startTime, target, next, err) {
|
||||
if (err) {
|
||||
winston.error('Build failed: ' + err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's');
|
||||
next();
|
||||
};
|
||||
|
||||
async.parallel([
|
||||
if (strategy) {
|
||||
winston.verbose('[build] Utilising multiple cores/processes');
|
||||
} else {
|
||||
winston.verbose('[build] Utilising single-core');
|
||||
}
|
||||
|
||||
async[strategy ? 'parallel' : 'series']([
|
||||
function (next) {
|
||||
if (targets.indexOf('js') !== -1) {
|
||||
winston.info('[build] Building javascript');
|
||||
var startTime = Date.now();
|
||||
async.series([
|
||||
meta.js.linkModules,
|
||||
meta.js.linkStatics,
|
||||
async.apply(meta.js.minify, 'nodebb.min.js'),
|
||||
async.apply(meta.js.minify, 'acp.min.js')
|
||||
], step.bind(this, startTime, 'js', next));
|
||||
@@ -74,13 +90,13 @@ exports.buildTargets = function (targets, callback) {
|
||||
case 'clientCSS':
|
||||
winston.info('[build] Building client-side CSS');
|
||||
startTime = Date.now();
|
||||
meta.css.minify('stylesheet.css', step.bind(this, startTime, target, next));
|
||||
meta.css.minify('client', step.bind(this, startTime, target, next));
|
||||
break;
|
||||
|
||||
case 'acpCSS':
|
||||
winston.info('[build] Building admin control panel CSS');
|
||||
startTime = Date.now();
|
||||
meta.css.minify('admin.css', step.bind(this, startTime, target, next));
|
||||
meta.css.minify('admin', step.bind(this, startTime, target, next));
|
||||
break;
|
||||
|
||||
case 'tpl':
|
||||
@@ -108,14 +124,21 @@ exports.buildTargets = function (targets, callback) {
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
var time = (Date.now() - buildStart) / 1000;
|
||||
cacheBuster.write(function (err) {
|
||||
if (err) {
|
||||
winston.error('[build] Failed to write `cache-buster.conf`: ' + err.message);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
winston.info('[build] Asset compilation successful. Completed in ' + time + 's.');
|
||||
var time = (Date.now() - buildStart) / 1000;
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
winston.info('[build] Asset compilation successful. Completed in ' + time + 's.');
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
46
src/meta/cacheBuster.js
Normal file
46
src/meta/cacheBuster.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var mkdirp = require('mkdirp');
|
||||
var winston = require('winston');
|
||||
|
||||
var filePath = path.join(__dirname, '../../build/cache-buster');
|
||||
|
||||
var cached;
|
||||
|
||||
// cache buster is an 11-character, lowercase, alphanumeric string
|
||||
function generate() {
|
||||
return (Math.random() * 1e18).toString(32).slice(0, 11);
|
||||
}
|
||||
|
||||
exports.write = function write(callback) {
|
||||
mkdirp(path.dirname(filePath), function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fs.writeFile(filePath, generate(), callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.read = function read(callback) {
|
||||
if (cached) {
|
||||
return callback(null, cached);
|
||||
}
|
||||
|
||||
fs.readFile(filePath, function (err, buffer) {
|
||||
if (err) {
|
||||
winston.warn('[cache-buster] could not read cache buster: ' + err.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
buffer = buffer.toString();
|
||||
if (buffer) {
|
||||
cached = buffer;
|
||||
return callback(null, cached);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
@@ -6,7 +6,7 @@ var nconf = require('nconf');
|
||||
|
||||
var db = require('../database');
|
||||
var pubsub = require('../pubsub');
|
||||
var utils = require('../../public/src/utils');
|
||||
var cacheBuster = require('./cacheBuster');
|
||||
|
||||
module.exports = function (Meta) {
|
||||
|
||||
@@ -21,10 +21,16 @@ module.exports = function (Meta) {
|
||||
Meta.configs.list(next);
|
||||
},
|
||||
function (config, next) {
|
||||
config['cache-buster'] = 'v=' + utils.generateUUID();
|
||||
cacheBuster.read(function (err, buster) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
config['cache-buster'] = 'v=' + (buster || Date.now());
|
||||
|
||||
Meta.config = config;
|
||||
setImmediate(next);
|
||||
Meta.config = config;
|
||||
next();
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
126
src/meta/css.js
126
src/meta/css.js
@@ -18,8 +18,33 @@ var utils = require('../../public/src/utils');
|
||||
module.exports = function (Meta) {
|
||||
|
||||
Meta.css = {};
|
||||
Meta.css.cache = undefined;
|
||||
Meta.css.acpCache = undefined;
|
||||
|
||||
var buildImports = {
|
||||
client: function (source) {
|
||||
return '@import "./theme";\n' + source + '\n' + [
|
||||
'@import "font-awesome";',
|
||||
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
|
||||
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
|
||||
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
|
||||
'@import (inline) "../node_modules/cropperjs/dist/cropper.css";',
|
||||
'@import "../../public/less/flags.less";',
|
||||
'@import "../../public/less/blacklist.less";',
|
||||
'@import "../../public/less/generics.less";',
|
||||
'@import "../../public/less/mixins.less";',
|
||||
'@import "../../public/less/global.less";',
|
||||
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
|
||||
},
|
||||
admin: function (source) {
|
||||
return source + '\n' + [
|
||||
'@import "font-awesome";',
|
||||
'@import "../public/less/admin/admin";',
|
||||
'@import "../public/less/generics.less";',
|
||||
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
|
||||
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
|
||||
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
|
||||
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
|
||||
},
|
||||
};
|
||||
|
||||
Meta.css.minify = function (target, callback) {
|
||||
callback = callback || function () {};
|
||||
@@ -30,25 +55,25 @@ module.exports = function (Meta) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var themeId = (themeData['theme:id'] || 'nodebb-theme-persona'),
|
||||
baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')),
|
||||
paths = [
|
||||
baseThemePath,
|
||||
path.join(__dirname, '../../node_modules'),
|
||||
path.join(__dirname, '../../public/vendor/fontawesome/less')
|
||||
],
|
||||
source = '@import "font-awesome";';
|
||||
var themeId = (themeData['theme:id'] || 'nodebb-theme-persona');
|
||||
var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla'));
|
||||
var paths = [
|
||||
baseThemePath,
|
||||
path.join(__dirname, '../../node_modules'),
|
||||
path.join(__dirname, '../../public/vendor/fontawesome/less')
|
||||
];
|
||||
var source = '';
|
||||
|
||||
plugins.lessFiles = filterMissingFiles(plugins.lessFiles);
|
||||
plugins.cssFiles = filterMissingFiles(plugins.cssFiles);
|
||||
var lessFiles = filterMissingFiles(plugins.lessFiles);
|
||||
var cssFiles = filterMissingFiles(plugins.cssFiles);
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
getStyleSource(plugins.cssFiles, '\n@import (inline) ".', '.css', next);
|
||||
getStyleSource(cssFiles, '\n@import (inline) ".', '.css', next);
|
||||
},
|
||||
function (src, next) {
|
||||
source += src;
|
||||
getStyleSource(plugins.lessFiles, '\n@import ".', '.less', next);
|
||||
getStyleSource(lessFiles, '\n@import ".', '.less', next);
|
||||
},
|
||||
function (src, next) {
|
||||
source += src;
|
||||
@@ -59,43 +84,11 @@ module.exports = function (Meta) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var acpSource = source;
|
||||
|
||||
if (target !== 'admin.css') {
|
||||
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
|
||||
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
|
||||
source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
|
||||
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";';
|
||||
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";';
|
||||
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";';
|
||||
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";';
|
||||
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";';
|
||||
source += '\n@import (inline) "..' + path.sep + 'node_modules/cropperjs/dist/cropper.css";';
|
||||
source = '@import "./theme";\n' + source;
|
||||
|
||||
minify(source, paths, 'cache', callback);
|
||||
} else {
|
||||
acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n';
|
||||
acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n';
|
||||
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n';
|
||||
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
|
||||
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
|
||||
|
||||
minify(acpSource, paths, 'acpCache', callback);
|
||||
}
|
||||
minify(buildImports[target](source), paths, target, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Meta.css.getFromFile = function (callback) {
|
||||
async.series([
|
||||
async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/stylesheet.css'), 'cache'),
|
||||
async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/admin.css'), 'acpCache')
|
||||
], function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
function getStyleSource(files, prefix, extension, callback) {
|
||||
var pluginDirectories = [],
|
||||
source = '';
|
||||
@@ -125,35 +118,22 @@ module.exports = function (Meta) {
|
||||
});
|
||||
}
|
||||
|
||||
Meta.css.commitToFile = function (filename, callback) {
|
||||
var file = (filename === 'acpCache' ? 'admin' : 'stylesheet') + '.css';
|
||||
Meta.css.commitToFile = function (target, source, callback) {
|
||||
var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css';
|
||||
|
||||
fs.writeFile(path.join(__dirname, '../../public/' + file), Meta.css[filename], function (err) {
|
||||
fs.writeFile(path.join(__dirname, '../../build/public/' + filename), source, function (err) {
|
||||
if (!err) {
|
||||
winston.verbose('[meta/css] ' + file + ' committed to disk.');
|
||||
winston.verbose('[meta/css] ' + target + ' CSS committed to disk.');
|
||||
} else {
|
||||
winston.error('[meta/css] ' + err.message);
|
||||
process.exit(0);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Meta.css.loadFile = function (filePath, filename, callback) {
|
||||
winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file');
|
||||
|
||||
fs.readFile(filePath, function (err, file) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
Meta.css[filename] = file;
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
function minify(source, paths, destination, callback) {
|
||||
function minify(source, paths, target, callback) {
|
||||
callback = callback || function () {};
|
||||
less.render(source, {
|
||||
paths: paths
|
||||
@@ -163,20 +143,14 @@ module.exports = function (Meta) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
postcss([ autoprefixer, clean() ]).process(lessOutput.css).then(function (result) {
|
||||
postcss(global.env === 'development' ? [ autoprefixer ] : [ autoprefixer, clean() ]).process(lessOutput.css).then(function (result) {
|
||||
result.warnings().forEach(function (warn) {
|
||||
winston.verbose(warn.toString());
|
||||
});
|
||||
Meta.css[destination] = result.css;
|
||||
|
||||
// Save the compiled CSS in public/ so things like nginx can serve it
|
||||
if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) {
|
||||
return Meta.css.commitToFile(destination, function () {
|
||||
callback(null, result.css);
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, result.css);
|
||||
return Meta.css.commitToFile(target, result.css, function () {
|
||||
callback(null, result.css);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,14 +19,17 @@ module.exports = function (Meta) {
|
||||
|
||||
winston.verbose('Checking dependencies for outdated modules');
|
||||
|
||||
async.every(modules, function (module, next) {
|
||||
async.each(modules, function (module, next) {
|
||||
fs.readFile(path.join(__dirname, '../../node_modules/', module, 'package.json'), {
|
||||
encoding: 'utf-8'
|
||||
}, function (err, pkgData) {
|
||||
// If a bundled plugin/theme is not present, skip the dep check (#3384)
|
||||
if (err && err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
|
||||
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
|
||||
return next(true);
|
||||
if (err) {
|
||||
// If a bundled plugin/theme is not present, skip the dep check (#3384)
|
||||
if (err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
|
||||
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
|
||||
return next();
|
||||
}
|
||||
return next(err);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -34,20 +37,24 @@ module.exports = function (Meta) {
|
||||
} catch(e) {
|
||||
process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n');
|
||||
depsMissing = true;
|
||||
return next(true);
|
||||
return next();
|
||||
}
|
||||
|
||||
var ok = !semver.validRange(pkg.dependencies[module]) || semver.satisfies(pkgData.version, pkg.dependencies[module]);
|
||||
|
||||
if (ok || (pkgData._resolved && pkgData._resolved.indexOf('//github.com') !== -1)) {
|
||||
next(true);
|
||||
next();
|
||||
} else {
|
||||
process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n');
|
||||
depsOutdated = true;
|
||||
next(true);
|
||||
next();
|
||||
}
|
||||
});
|
||||
}, function (ok) {
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (depsMissing) {
|
||||
callback(new Error('dependencies-missing'));
|
||||
} else if (depsOutdated) {
|
||||
|
||||
114
src/meta/js.js
114
src/meta/js.js
@@ -4,8 +4,11 @@ var winston = require('winston');
|
||||
var fork = require('child_process').fork;
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
var fs = require('fs');
|
||||
var mkdirp = require('mkdirp');
|
||||
var rimraf = require('rimraf');
|
||||
|
||||
var file = require('../file');
|
||||
var plugins = require('../plugins');
|
||||
var utils = require('../../public/src/utils');
|
||||
|
||||
@@ -50,7 +53,6 @@ module.exports = function (Meta) {
|
||||
'public/src/client/unread.js',
|
||||
'public/src/client/topic.js',
|
||||
'public/src/client/topic/events.js',
|
||||
'public/src/client/topic/flag.js',
|
||||
'public/src/client/topic/fork.js',
|
||||
'public/src/client/topic/move.js',
|
||||
'public/src/client/topic/posts.js',
|
||||
@@ -73,7 +75,8 @@ module.exports = function (Meta) {
|
||||
'public/src/modules/taskbar.js',
|
||||
'public/src/modules/helpers.js',
|
||||
'public/src/modules/sounds.js',
|
||||
'public/src/modules/string.js'
|
||||
'public/src/modules/string.js',
|
||||
'public/src/modules/flags.js'
|
||||
],
|
||||
|
||||
// modules listed below are routed through express (/src/modules) so they can be defined anonymously
|
||||
@@ -86,30 +89,45 @@ module.exports = function (Meta) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Meta.js.bridgeModules = function (app, callback) {
|
||||
// Add routes for AMD-type modules to serve those files
|
||||
function addRoute(relPath) {
|
||||
var relativePath = nconf.get('relative_path');
|
||||
|
||||
app.get(relativePath + '/src/modules/' + relPath, function (req, res) {
|
||||
return res.sendFile(path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), {
|
||||
maxAge: app.enabled('cache') ? 5184000000 : 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var numBridged = 0;
|
||||
|
||||
for(var relPath in Meta.js.scripts.modules) {
|
||||
if (Meta.js.scripts.modules.hasOwnProperty(relPath)) {
|
||||
addRoute(relPath);
|
||||
++numBridged;
|
||||
|
||||
Meta.js.linkModules = function (callback) {
|
||||
rimraf(path.join(__dirname, '../../build/public/src/modules'), function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
async.eachLimit(Object.keys(Meta.js.scripts.modules), 1000, function (relPath, next) {
|
||||
var filePath = path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]);
|
||||
var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
|
||||
|
||||
winston.verbose('[meta/js] ' + numBridged + ' of ' + Object.keys(Meta.js.scripts.modules).length + ' modules bridged');
|
||||
callback();
|
||||
mkdirp(path.dirname(destPath), function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
file.link(filePath, destPath, next);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Meta.js.linkStatics = function (callback) {
|
||||
rimraf(path.join(__dirname, '../../build/public/plugins'), function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.eachLimit(Object.keys(plugins.staticDirs), 1000, function (mappedPath, next) {
|
||||
var sourceDir = plugins.staticDirs[mappedPath];
|
||||
var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
|
||||
|
||||
mkdirp(path.dirname(destDir), function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
file.linkDirs(sourceDir, destDir, next);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Meta.js.minify = function (target, callback) {
|
||||
@@ -139,12 +157,7 @@ module.exports = function (Meta) {
|
||||
winston.verbose('[meta/js] ' + target + ' minification complete');
|
||||
minifier.kill();
|
||||
|
||||
if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) {
|
||||
return Meta.js.commitToFile(target, callback);
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
|
||||
Meta.js.commitToFile(target, callback);
|
||||
break;
|
||||
case 'error':
|
||||
winston.error('[meta/js] Could not compile ' + target + ': ' + message.message);
|
||||
@@ -203,48 +216,11 @@ module.exports = function (Meta) {
|
||||
};
|
||||
|
||||
Meta.js.commitToFile = function (target, callback) {
|
||||
fs.writeFile(path.join(__dirname, '../../public/' + target), Meta.js.target[target].cache, function (err) {
|
||||
fs.writeFile(path.join(__dirname, '../../build/public/' + target), Meta.js.target[target].cache, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
Meta.js.getFromFile = function (target, callback) {
|
||||
function readFile(filePath, next) {
|
||||
fs.readFile(filePath, function (err, contents) {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
if (!filePath.endsWith('.map')) {
|
||||
winston.warn('[meta/js] ' + filePath + ' not found on disk, did you run ./nodebb build?');
|
||||
}
|
||||
return next(null, '');
|
||||
}
|
||||
}
|
||||
next(err, contents);
|
||||
});
|
||||
}
|
||||
|
||||
var scriptPath = path.join(nconf.get('base_dir'), 'public/' + target);
|
||||
var mapPath = path.join(nconf.get('base_dir'), 'public/' + target + '.map');
|
||||
|
||||
async.parallel({
|
||||
script: function (next) {
|
||||
readFile(scriptPath, next);
|
||||
},
|
||||
map: function (next) {
|
||||
readFile(mapPath, next);
|
||||
}
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
Meta.js.target[target] = {
|
||||
cache: results.script,
|
||||
map: results.map
|
||||
};
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
function setupDebugging() {
|
||||
/**
|
||||
* Check if the parent process is running with the debug option --debug (or --debug-brk)
|
||||
|
||||
@@ -28,9 +28,7 @@ function getTranslationTree(callback) {
|
||||
});
|
||||
|
||||
// Filter out plugins with invalid paths
|
||||
async.filter(paths, file.exists, function (paths) {
|
||||
next(null, paths);
|
||||
});
|
||||
async.filter(paths, file.exists, next);
|
||||
},
|
||||
function (paths, next) {
|
||||
async.map(paths, Plugins.loadPluginInfo, next);
|
||||
|
||||
@@ -8,6 +8,7 @@ var rimraf = require('rimraf');
|
||||
var mkdirp = require('mkdirp');
|
||||
var async = require('async');
|
||||
|
||||
var file = require('../file');
|
||||
var plugins = require('../plugins');
|
||||
var db = require('../database');
|
||||
|
||||
@@ -28,7 +29,7 @@ module.exports = function (Meta) {
|
||||
Meta.sounds.getFiles = function (callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
fs.readdir(path.join(__dirname, '../../public/sounds'), next);
|
||||
fs.readdir(path.join(__dirname, '../../build/public/sounds'), next);
|
||||
},
|
||||
function (sounds, next) {
|
||||
fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function (err, uploaded) {
|
||||
@@ -88,7 +89,7 @@ module.exports = function (Meta) {
|
||||
};
|
||||
|
||||
function setupSounds(callback) {
|
||||
var soundsPath = path.join(__dirname, '../../public/sounds');
|
||||
var soundsPath = path.join(__dirname, '../../build/public/sounds');
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
@@ -107,21 +108,6 @@ module.exports = function (Meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nconf.get('local-assets') === false) {
|
||||
// Don't regenerate the public/sounds/ directory. Instead, create a mapping for the router to use
|
||||
Meta.sounds._filePathHash = filePaths.reduce(function (hash, filePath) {
|
||||
hash[path.basename(filePath)] = filePath;
|
||||
return hash;
|
||||
}, {});
|
||||
|
||||
winston.verbose('[sounds] Sounds OK');
|
||||
if (typeof next === 'function') {
|
||||
return next();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the sounds directory
|
||||
async.series([
|
||||
function (next) {
|
||||
@@ -138,11 +124,7 @@ module.exports = function (Meta) {
|
||||
|
||||
// Link paths
|
||||
async.each(filePaths, function (filePath, next) {
|
||||
if (process.platform === 'win32') {
|
||||
fs.link(filePath, path.join(soundsPath, path.basename(filePath)), next);
|
||||
} else {
|
||||
fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file', next);
|
||||
}
|
||||
file.link(filePath, path.join(soundsPath, path.basename(filePath)), next);
|
||||
}, function (err) {
|
||||
if (!err) {
|
||||
winston.verbose('[sounds] Sounds OK');
|
||||
|
||||
@@ -27,18 +27,28 @@ module.exports = function (Meta) {
|
||||
async.filter(files, function (file, next) {
|
||||
fs.stat(path.join(themePath, file), function (err, fileStat) {
|
||||
if (err) {
|
||||
return next(false);
|
||||
if (err.code === 'ENOENT') {
|
||||
return next(null, false);
|
||||
}
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next((fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
|
||||
next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
|
||||
});
|
||||
}, function (themes) {
|
||||
}, function (err, themes) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.map(themes, function (theme, next) {
|
||||
var config = path.join(themePath, theme, 'theme.json');
|
||||
|
||||
fs.readFile(config, function (err, file) {
|
||||
if (err) {
|
||||
return next();
|
||||
if (err.code === 'ENOENT') {
|
||||
return next(null, null);
|
||||
}
|
||||
return next(err);
|
||||
}
|
||||
try {
|
||||
var configObj = JSON.parse(file.toString());
|
||||
@@ -48,7 +58,7 @@ module.exports = function (Meta) {
|
||||
if (configObj.screenshot) {
|
||||
configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id;
|
||||
} else {
|
||||
configObj.screenshot_url = nconf.get('relative_path') + '/images/themes/default.png';
|
||||
configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png';
|
||||
}
|
||||
next(null, configObj);
|
||||
} catch (err) {
|
||||
|
||||
@@ -101,7 +101,7 @@ module.exports = function (middleware) {
|
||||
plugins: results.custom_header.plugins,
|
||||
authentication: results.custom_header.authentication,
|
||||
scripts: results.scripts,
|
||||
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
|
||||
'cache-buster': meta.config['cache-buster'] || '',
|
||||
env: process.env.NODE_ENV ? true : false,
|
||||
title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel',
|
||||
bodyClass: data.bodyClass
|
||||
|
||||
@@ -46,7 +46,7 @@ module.exports = function (middleware) {
|
||||
bootswatchCSS: meta.config['theme:src'],
|
||||
title: meta.config.title || '',
|
||||
description: meta.config.description || '',
|
||||
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
|
||||
'cache-buster': meta.config['cache-buster'] || '',
|
||||
'brand:logo': meta.config['brand:logo'] || '',
|
||||
'brand:logo:url': meta.config['brand:logo:url'] || '',
|
||||
'brand:logo:alt': meta.config['brand:logo:alt'] || '',
|
||||
|
||||
@@ -73,6 +73,30 @@ middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) {
|
||||
}
|
||||
};
|
||||
|
||||
middleware.ensureSelfOrPrivileged = function (req, res, next) {
|
||||
/*
|
||||
The "self" part of this middleware hinges on you having used
|
||||
middleware.exposeUid prior to invoking this middleware.
|
||||
*/
|
||||
if (req.user) {
|
||||
if (parseInt(req.user.uid, 10) === parseInt(res.locals.uid, 10)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
user.isPrivileged(req.uid, function (err, ok) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
} else if (ok) {
|
||||
return next();
|
||||
} else {
|
||||
controllers.helpers.notAllowed(req, res);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
controllers.helpers.notAllowed(req, res);
|
||||
}
|
||||
};
|
||||
|
||||
middleware.pageView = function (req, res, next) {
|
||||
analytics.pageView({
|
||||
ip: req.ip,
|
||||
|
||||
@@ -415,6 +415,7 @@ var utils = require('../public/src/utils');
|
||||
'notifications:user_started_following_you',
|
||||
'notifications:user_posted_to',
|
||||
'notifications:user_flagged_post_in',
|
||||
'notifications:user_flagged_user',
|
||||
'new_register'
|
||||
],
|
||||
isolated, differentiators, differentiator, modifyIndex, set;
|
||||
@@ -462,6 +463,7 @@ var utils = require('../public/src/utils');
|
||||
case 'notifications:user_started_following_you':
|
||||
case 'notifications:user_posted_to':
|
||||
case 'notifications:user_flagged_post_in':
|
||||
case 'notifications:user_flagged_user':
|
||||
var usernames = set.map(function (notifObj) {
|
||||
return notifObj && notifObj.user && notifObj.user.username;
|
||||
}).filter(function (username, idx, array) {
|
||||
|
||||
@@ -159,9 +159,7 @@ var middleware;
|
||||
});
|
||||
|
||||
// Filter out plugins with invalid paths
|
||||
async.filter(paths, file.exists, function (paths) {
|
||||
next(null, paths);
|
||||
});
|
||||
async.filter(paths, file.exists, next);
|
||||
},
|
||||
function (paths, next) {
|
||||
async.map(paths, Plugins.loadPluginInfo, next);
|
||||
@@ -344,11 +342,15 @@ var middleware;
|
||||
|
||||
async.filter(dirs, function (dir, callback) {
|
||||
fs.stat(dir, function (err, stats) {
|
||||
callback(!err && stats.isDirectory());
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, stats.isDirectory());
|
||||
});
|
||||
}, function (plugins) {
|
||||
next(null, plugins);
|
||||
});
|
||||
}, next);
|
||||
},
|
||||
|
||||
function (files, next) {
|
||||
|
||||
@@ -7,7 +7,10 @@ module.exports = function (Plugins) {
|
||||
Plugins.deprecatedHooks = {
|
||||
'filter:user.custom_fields': null, // remove in v1.1.0
|
||||
'filter:post.save': 'filter:post.create',
|
||||
'filter:user.profileLinks': 'filter:user.profileMenu'
|
||||
'filter:user.profileLinks': 'filter:user.profileMenu',
|
||||
'action:post.flag': 'action:flag.create',
|
||||
'action:plugin.activate': null,
|
||||
'action:plugin.install': null
|
||||
};
|
||||
/*
|
||||
`data` is an object consisting of (* is required):
|
||||
|
||||
@@ -50,8 +50,11 @@ module.exports = function (Plugins) {
|
||||
},
|
||||
function (next) {
|
||||
meta.reloadRequired = true;
|
||||
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', id);
|
||||
next();
|
||||
if (isActive) {
|
||||
Plugins.fireHook('action:plugin.deactivate', {id: id});
|
||||
}
|
||||
|
||||
setImmediate(next);
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
@@ -68,7 +71,6 @@ module.exports = function (Plugins) {
|
||||
};
|
||||
|
||||
function toggleInstall(id, version, callback) {
|
||||
var type;
|
||||
var installed;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
@@ -76,7 +78,6 @@ module.exports = function (Plugins) {
|
||||
},
|
||||
function (_installed, next) {
|
||||
installed = _installed;
|
||||
type = installed ? 'uninstall' : 'install';
|
||||
Plugins.isActive(id, next);
|
||||
},
|
||||
function (active, next) {
|
||||
@@ -86,23 +87,26 @@ module.exports = function (Plugins) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
next();
|
||||
setImmediate(next);
|
||||
},
|
||||
function (next) {
|
||||
runNpmCommand(type, id, version || 'latest', next);
|
||||
runNpmCommand(installed ? 'uninstall' : 'install', id, version || 'latest', next);
|
||||
},
|
||||
function (next) {
|
||||
Plugins.get(id, next);
|
||||
},
|
||||
function (pluginData, next) {
|
||||
Plugins.fireHook('action:plugin.' + type, id);
|
||||
next(null, pluginData);
|
||||
if (installed) {
|
||||
Plugins.fireHook('action:plugin.uninstall', {id: id, version: version});
|
||||
}
|
||||
|
||||
setImmediate(next, null, pluginData);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
function runNpmCommand(command, pkgName, version, callback) {
|
||||
require('child_process').execFile('npm', [command, pkgName + (command === 'install' ? '@' + version : '')], function (err, stdout) {
|
||||
require('child_process').execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', [command, pkgName + (command === 'install' ? '@' + version : '')], function (err, stdout) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ module.exports = function (Plugins) {
|
||||
return path.join(__dirname, '../../node_modules/', plugin);
|
||||
});
|
||||
|
||||
async.filter(plugins, file.exists, function (plugins) {
|
||||
next(null, plugins);
|
||||
});
|
||||
async.filter(plugins, file.exists, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
@@ -54,7 +52,9 @@ module.exports = function (Plugins) {
|
||||
async.parallel([
|
||||
async.apply(mapFiles, pluginData, 'css', 'cssFiles'),
|
||||
async.apply(mapFiles, pluginData, 'less', 'lessFiles'),
|
||||
async.apply(mapClientSideScripts, pluginData)
|
||||
async.apply(mapClientSideScripts, pluginData),
|
||||
async.apply(mapClientModules, pluginData),
|
||||
async.apply(mapStaticDirectories, pluginData, pluginData.path),
|
||||
], next);
|
||||
}, next);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ module.exports = function (Plugins) {
|
||||
},
|
||||
function (next) {
|
||||
mapClientModules(pluginData, next);
|
||||
},
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
|
||||
@@ -157,13 +157,13 @@ module.exports = function (Plugins) {
|
||||
var realPath = pluginData.staticDirs[mappedPath];
|
||||
var staticDir = path.join(pluginPath, realPath);
|
||||
|
||||
file.exists(staticDir, function (exists) {
|
||||
file.exists(staticDir, function (err, exists) {
|
||||
if (exists) {
|
||||
Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir;
|
||||
} else {
|
||||
winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.');
|
||||
}
|
||||
callback();
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -298,6 +298,7 @@ module.exports = function (Plugins) {
|
||||
pluginData.version = packageData.version;
|
||||
pluginData.repository = packageData.repository;
|
||||
pluginData.nbbpm = packageData.nbbpm;
|
||||
pluginData.path = pluginPath;
|
||||
} catch(err) {
|
||||
var pluginDir = pluginPath.split(path.sep);
|
||||
pluginDir = pluginDir[pluginDir.length - 1];
|
||||
|
||||
@@ -21,7 +21,6 @@ var plugins = require('./plugins');
|
||||
require('./posts/category')(Posts);
|
||||
require('./posts/summary')(Posts);
|
||||
require('./posts/recent')(Posts);
|
||||
require('./posts/flags')(Posts);
|
||||
require('./posts/tools')(Posts);
|
||||
require('./posts/votes')(Posts);
|
||||
require('./posts/bookmarks')(Posts);
|
||||
@@ -157,7 +156,7 @@ var plugins = require('./plugins');
|
||||
pid: pid
|
||||
};
|
||||
data[field] = value;
|
||||
plugins.fireHook('action:post.setFields', data);
|
||||
plugins.fireHook('action:post.setFields', {data: data});
|
||||
callback();
|
||||
});
|
||||
};
|
||||
@@ -168,7 +167,7 @@ var plugins = require('./plugins');
|
||||
return callback(err);
|
||||
}
|
||||
data.pid = pid;
|
||||
plugins.fireHook('action:post.setFields', data);
|
||||
plugins.fireHook('action:post.setFields', {data: data});
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ module.exports = function (Posts) {
|
||||
},
|
||||
function (postData, next) {
|
||||
postData.isMain = isMain;
|
||||
plugins.fireHook('action:post.save', _.clone(postData));
|
||||
plugins.fireHook('action:post.save', {post: _.clone(postData)});
|
||||
next(null, postData);
|
||||
}
|
||||
], callback);
|
||||
|
||||
@@ -8,6 +8,7 @@ var topics = require('../topics');
|
||||
var user = require('../user');
|
||||
var notifications = require('../notifications');
|
||||
var plugins = require('../plugins');
|
||||
var flags = require('../flags');
|
||||
|
||||
module.exports = function (Posts) {
|
||||
|
||||
@@ -28,6 +29,7 @@ module.exports = function (Posts) {
|
||||
topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next);
|
||||
},
|
||||
function (topicData, next) {
|
||||
postData.cid = topicData.cid;
|
||||
async.parallel([
|
||||
function (next) {
|
||||
updateTopicTimestamp(topicData, next);
|
||||
@@ -41,7 +43,7 @@ module.exports = function (Posts) {
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:post.delete', pid);
|
||||
plugins.fireHook('action:post.delete', {post: _.clone(postData), uid: uid});
|
||||
next(null, postData);
|
||||
}
|
||||
], callback);
|
||||
@@ -78,7 +80,7 @@ module.exports = function (Posts) {
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:post.restore', _.clone(postData));
|
||||
plugins.fireHook('action:post.restore', {post: _.clone(postData), uid: uid});
|
||||
next(null, postData);
|
||||
}
|
||||
], callback);
|
||||
@@ -143,17 +145,17 @@ module.exports = function (Posts) {
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
|
||||
},
|
||||
function (next) {
|
||||
Posts.dismissFlag(pid, next);
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
plugins.fireHook('action:post.purge', pid);
|
||||
db.delete('post:' + pid, next);
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
Posts.getPostData(pid, next);
|
||||
},
|
||||
function (postData, next) {
|
||||
plugins.fireHook('action:post.purge', {post: postData, uid: uid});
|
||||
db.delete('post:' + pid, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ module.exports = function (Posts) {
|
||||
|
||||
postData.cid = results.topic.cid;
|
||||
postData.topic = results.topic;
|
||||
plugins.fireHook('action:post.edit', _.clone(postData));
|
||||
plugins.fireHook('action:post.edit', {post: _.clone(postData), uid: data.uid});
|
||||
|
||||
cache.del(String(postData.pid));
|
||||
pubsub.publish('post:edit', String(postData.pid));
|
||||
@@ -137,7 +137,7 @@ module.exports = function (Posts) {
|
||||
function (tags, next) {
|
||||
topicData.tags = data.tags;
|
||||
topicData.oldTitle = results.topic.title;
|
||||
plugins.fireHook('action:topic.edit', topicData);
|
||||
plugins.fireHook('action:topic.edit', {topic: topicData, uid: data.uid});
|
||||
next(null, {
|
||||
tid: tid,
|
||||
cid: results.topic.cid,
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var db = require('../database');
|
||||
var user = require('../user');
|
||||
var analytics = require('../analytics');
|
||||
|
||||
module.exports = function (Posts) {
|
||||
|
||||
Posts.flag = function (post, uid, reason, callback) {
|
||||
if (!parseInt(uid, 10) || !reason) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
hasFlagged: async.apply(Posts.isFlaggedByUser, post.pid, uid),
|
||||
exists: async.apply(Posts.exists, post.pid)
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
if (!results.exists) {
|
||||
return next(new Error('[[error:no-post]]'));
|
||||
}
|
||||
|
||||
if (results.hasFlagged) {
|
||||
return next(new Error('[[error:already-flagged]]'));
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('posts:flagged', now, post.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.incrObjectField('post:' + post.pid, 'flags', next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
|
||||
},
|
||||
function (next) {
|
||||
if (parseInt(post.uid, 10)) {
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
|
||||
async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
|
||||
async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid)
|
||||
], next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
], next);
|
||||
},
|
||||
function (data, next) {
|
||||
openNewFlag(post.pid, uid, next);
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
analytics.increment('flags');
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
function openNewFlag(pid, uid, callback) {
|
||||
db.sortedSetScore('posts:flags:count', pid, function (err, count) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (count === 1) { // Only update state on new flag
|
||||
Posts.updateFlagData(uid, pid, {
|
||||
state: 'open'
|
||||
}, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Posts.isFlaggedByUser = function (pid, uid, callback) {
|
||||
db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback);
|
||||
};
|
||||
|
||||
Posts.dismissFlag = function (pid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
|
||||
},
|
||||
function (postData, next) {
|
||||
if (!postData.pid) {
|
||||
return callback();
|
||||
}
|
||||
async.parallel([
|
||||
function (next) {
|
||||
if (parseInt(postData.uid, 10)) {
|
||||
if (parseInt(postData.flags, 10) > 0) {
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
|
||||
async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags)
|
||||
], next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove([
|
||||
'posts:flagged',
|
||||
'posts:flags:count',
|
||||
'uid:' + postData.uid + ':flag:pids'
|
||||
], pid, next);
|
||||
},
|
||||
function (next) {
|
||||
async.series([
|
||||
function (next) {
|
||||
db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function (err, uids) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
async.each(uids, function (uid, next) {
|
||||
var nid = 'post_flag:' + pid + ':uid:' + uid;
|
||||
async.parallel([
|
||||
async.apply(db.delete, 'notifications:' + nid),
|
||||
async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
|
||||
], next);
|
||||
}, next);
|
||||
});
|
||||
},
|
||||
async.apply(db.delete, 'pid:' + pid + ':flag:uids')
|
||||
], next);
|
||||
},
|
||||
async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
|
||||
async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason'),
|
||||
async.apply(db.deleteObjectFields, 'post:' + pid, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history'])
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.dismissAllFlags = function (callback) {
|
||||
db.getSortedSetRange('posts:flagged', 0, -1, function (err, pids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.eachSeries(pids, Posts.dismissFlag, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.dismissUserFlags = function (uid, callback) {
|
||||
db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function (err, pids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.eachSeries(pids, Posts.dismissFlag, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.getFlags = function (set, cid, uid, start, stop, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (Array.isArray(set)) {
|
||||
db.getSortedSetRevIntersect({sets: set, start: start, stop: -1, aggregate: 'MAX'}, next);
|
||||
} else {
|
||||
db.getSortedSetRevRange(set, start, -1, next);
|
||||
}
|
||||
},
|
||||
function (pids, next) {
|
||||
if (cid) {
|
||||
Posts.filterPidsByCid(pids, cid, next);
|
||||
} else {
|
||||
process.nextTick(next, null, pids);
|
||||
}
|
||||
},
|
||||
function (pids, next) {
|
||||
getFlaggedPostsWithReasons(pids, uid, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
var count = posts.length;
|
||||
var end = stop - start + 1;
|
||||
next(null, {posts: posts.slice(0, stop === -1 ? undefined : end), count: count});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
function getFlaggedPostsWithReasons(pids, uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
uidsReasons: function (next) {
|
||||
async.map(pids, function (pid, next) {
|
||||
db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next);
|
||||
}, next);
|
||||
},
|
||||
posts: function (next) {
|
||||
Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history']}, next);
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
async.map(results.uidsReasons, function (uidReasons, next) {
|
||||
async.map(uidReasons, function (uidReason, next) {
|
||||
var uid = uidReason.split(':')[0];
|
||||
var reason = uidReason.substr(uidReason.indexOf(':') + 1);
|
||||
user.getUserFields(uid, ['username', 'userslug', 'picture'], function (err, userData) {
|
||||
next(err, {user: userData, reason: reason});
|
||||
});
|
||||
}, next);
|
||||
}, function (err, reasons) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
results.posts.forEach(function (post, index) {
|
||||
if (post) {
|
||||
post.flagReasons = reasons[index];
|
||||
}
|
||||
});
|
||||
|
||||
next(null, results.posts);
|
||||
});
|
||||
},
|
||||
async.apply(Posts.expandFlagHistory),
|
||||
function (posts, next) {
|
||||
// Parse out flag data into its own object inside each post hash
|
||||
async.map(posts, function (postObj, next) {
|
||||
for(var prop in postObj) {
|
||||
postObj.flagData = postObj.flagData || {};
|
||||
|
||||
if (postObj.hasOwnProperty(prop) && prop.startsWith('flag:')) {
|
||||
postObj.flagData[prop.slice(5)] = postObj[prop];
|
||||
|
||||
if (prop === 'flag:state') {
|
||||
switch(postObj[prop]) {
|
||||
case 'open':
|
||||
postObj.flagData.labelClass = 'info';
|
||||
break;
|
||||
case 'wip':
|
||||
postObj.flagData.labelClass = 'warning';
|
||||
break;
|
||||
case 'resolved':
|
||||
postObj.flagData.labelClass = 'success';
|
||||
break;
|
||||
case 'rejected':
|
||||
postObj.flagData.labelClass = 'danger';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delete postObj[prop];
|
||||
}
|
||||
}
|
||||
|
||||
if (postObj.flagData.assignee) {
|
||||
user.getUserFields(parseInt(postObj.flagData.assignee, 10), ['username', 'picture'], function (err, userData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
postObj.flagData.assigneeUser = userData;
|
||||
next(null, postObj);
|
||||
});
|
||||
} else {
|
||||
setImmediate(next.bind(null, null, postObj));
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
Posts.updateFlagData = function (uid, pid, flagObj, callback) {
|
||||
// Retrieve existing flag data to compare for history-saving purposes
|
||||
var changes = [];
|
||||
var changeset = {};
|
||||
var prop;
|
||||
|
||||
Posts.getPostData(pid, function (err, postData) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Track new additions
|
||||
for(prop in flagObj) {
|
||||
if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) {
|
||||
changes.push(prop);
|
||||
}
|
||||
}
|
||||
|
||||
// Track changed items
|
||||
for(prop in postData) {
|
||||
if (
|
||||
postData.hasOwnProperty(prop) && prop.startsWith('flag:') &&
|
||||
flagObj.hasOwnProperty(prop.slice(5)) &&
|
||||
postData[prop] !== flagObj[prop.slice(5)]
|
||||
) {
|
||||
changes.push(prop.slice(5));
|
||||
}
|
||||
}
|
||||
|
||||
changeset = changes.reduce(function (memo, prop) {
|
||||
memo['flag:' + prop] = flagObj[prop];
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
// Append changes to history string
|
||||
if (changes.length) {
|
||||
try {
|
||||
var history = JSON.parse(postData['flag:history'] || '[]');
|
||||
|
||||
changes.forEach(function (property) {
|
||||
switch(property) {
|
||||
case 'assignee': // intentional fall-through
|
||||
case 'state':
|
||||
history.unshift({
|
||||
uid: uid,
|
||||
type: property,
|
||||
value: flagObj[property],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'notes':
|
||||
history.unshift({
|
||||
uid: uid,
|
||||
type: property,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
changeset['flag:history'] = JSON.stringify(history);
|
||||
} catch (e) {
|
||||
winston.warn('[posts/updateFlagData] Unable to deserialise post flag history, likely malformed data');
|
||||
}
|
||||
}
|
||||
|
||||
// Save flag data into post hash
|
||||
if (changes.length) {
|
||||
Posts.setPostFields(pid, changeset, callback);
|
||||
} else {
|
||||
setImmediate(callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Posts.expandFlagHistory = function (posts, callback) {
|
||||
// Expand flag history
|
||||
async.map(posts, function (post, next) {
|
||||
var history;
|
||||
try {
|
||||
history = JSON.parse(post['flag:history'] || '[]');
|
||||
} catch (e) {
|
||||
winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data');
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
async.map(history, function (event, next) {
|
||||
event.timestampISO = new Date(event.timestamp).toISOString();
|
||||
|
||||
async.parallel([
|
||||
function (next) {
|
||||
user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
event.user = userData;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
if (event.type === 'assignee') {
|
||||
user.getUserField(parseInt(event.value, 10), 'username', function (err, username) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
event.label = username || 'Unknown user';
|
||||
next(null);
|
||||
});
|
||||
} else if (event.type === 'state') {
|
||||
event.label = '[[topic:flag_manage_state_' + event.value + ']]';
|
||||
setImmediate(next);
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
}
|
||||
], function (err) {
|
||||
next(err, event);
|
||||
});
|
||||
}, function (err, history) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
post['flag:history'] = history;
|
||||
next(null, post);
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
};
|
||||
@@ -8,12 +8,8 @@ var helpers = {};
|
||||
|
||||
helpers.some = function (tasks, callback) {
|
||||
async.some(tasks, function (task, next) {
|
||||
task(function (err, result) {
|
||||
next(!err && result);
|
||||
});
|
||||
}, function (result) {
|
||||
callback(null, result);
|
||||
});
|
||||
task(next);
|
||||
}, callback);
|
||||
};
|
||||
|
||||
helpers.isUserAllowedTo = function (privilege, uid, cid, callback) {
|
||||
|
||||
@@ -37,12 +37,14 @@ rewards.checkConditionAndRewardUser = function (uid, condition, method, callback
|
||||
function (rewards, next) {
|
||||
async.filter(rewards, function (reward, next) {
|
||||
if (!reward) {
|
||||
return next(false);
|
||||
return next(null, false);
|
||||
}
|
||||
|
||||
checkCondition(reward, method, next);
|
||||
}, function (eligible) {
|
||||
if (!eligible) {
|
||||
checkCondition(reward, method, function (result) {
|
||||
next(null, result);
|
||||
});
|
||||
}, function (err, eligible) {
|
||||
if (err || !eligible) {
|
||||
return next(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ function addRoutes(router, middleware, controllers) {
|
||||
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
|
||||
|
||||
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
|
||||
router.get('/manage/flags', middlewares, controllers.admin.flags.get);
|
||||
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
|
||||
|
||||
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
var helpers = {};
|
||||
|
||||
helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) {
|
||||
middlewares = middlewares.concat([middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]);
|
||||
middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares);
|
||||
|
||||
router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller);
|
||||
router.get('/api' + name, middlewares, controller);
|
||||
};
|
||||
|
||||
helpers.setupAdminPageRoute = function (router, name, middleware, middlewares, controller) {
|
||||
router.get(name, middleware.admin.buildHeader, middlewares, controller);
|
||||
router.get('/api' + name, middlewares, controller);
|
||||
};
|
||||
|
||||
|
||||
module.exports = helpers;
|
||||
@@ -15,7 +15,6 @@ var metaRoutes = require('./meta');
|
||||
var apiRoutes = require('./api');
|
||||
var adminRoutes = require('./admin');
|
||||
var feedRoutes = require('./feeds');
|
||||
var pluginRoutes = require('./plugins');
|
||||
var authRoutes = require('./authentication');
|
||||
var helpers = require('./helpers');
|
||||
|
||||
@@ -41,7 +40,8 @@ function mainRoutes(app, middleware, controllers) {
|
||||
}
|
||||
|
||||
function modRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/posts/flags', middleware, [], controllers.mods.flagged);
|
||||
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
|
||||
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail);
|
||||
}
|
||||
|
||||
function globalModRoutes(app, middleware, controllers) {
|
||||
@@ -121,7 +121,6 @@ module.exports = function (app, middleware, hotswapIds) {
|
||||
metaRoutes(router, middleware, controllers);
|
||||
apiRoutes(router, middleware, controllers);
|
||||
feedRoutes(router, middleware, controllers);
|
||||
pluginRoutes(router, middleware, controllers);
|
||||
|
||||
mainRoutes(router, middleware, controllers);
|
||||
topicRoutes(router, middleware, controllers);
|
||||
@@ -143,10 +142,44 @@ module.exports = function (app, middleware, hotswapIds) {
|
||||
}
|
||||
|
||||
app.use(middleware.privateUploads);
|
||||
app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../', 'build/public'), {
|
||||
|
||||
app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../build/public'), {
|
||||
maxAge: app.enabled('cache') ? 5184000000 : 0
|
||||
}));
|
||||
app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../public'), {
|
||||
maxAge: app.enabled('cache') ? 5184000000 : 0
|
||||
}));
|
||||
// TODO: deprecate?
|
||||
app.use(relativePath + '/plugins', express.static(path.join(__dirname, '../../build/public/plugins'), {
|
||||
maxAge: app.enabled('cache') ? 5184000000 : 0
|
||||
}));
|
||||
|
||||
// DEPRECATED
|
||||
var deprecatedPaths = [
|
||||
'/nodebb.min.js',
|
||||
'/acp.min.js',
|
||||
'/stylesheet.css',
|
||||
'/js-enabled.css',
|
||||
'/admin.css',
|
||||
'/logo.png',
|
||||
'/favicon.ico',
|
||||
'/vendor/',
|
||||
'/uploads/',
|
||||
'/templates/',
|
||||
'/src/',
|
||||
'/images/',
|
||||
'/language/',
|
||||
'/sounds/',
|
||||
];
|
||||
app.use(relativePath, function (req, res, next) {
|
||||
if (deprecatedPaths.some(function (path) { return req.path.startsWith(path); })) {
|
||||
winston.warn('[deprecated] Accessing `' + req.path.slice(1) + '` from `/` is deprecated. ' +
|
||||
'Use `/assets' + req.path + '` to access this file.');
|
||||
res.redirect(relativePath + '/assets' + req.path + '?' + meta.config['cache-buster']);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
// DEPRECATED
|
||||
app.use(relativePath + '/api/language', function (req, res) {
|
||||
winston.warn('[deprecated] Accessing language files from `/api/language` is deprecated. ' +
|
||||
@@ -154,10 +187,7 @@ module.exports = function (app, middleware, hotswapIds) {
|
||||
res.redirect(relativePath + '/assets/language' + req.path + '.json?' + meta.config['cache-buster']);
|
||||
});
|
||||
|
||||
app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), {
|
||||
maxAge: app.enabled('cache') ? 5184000000 : 0
|
||||
}));
|
||||
app.use(relativePath + '/vendor/jquery/timeago/locales', middleware.processTimeagoLocales);
|
||||
app.use(relativePath + '/assets/vendor/jquery/timeago/locales', middleware.processTimeagoLocales);
|
||||
app.use(controllers.handle404);
|
||||
app.use(controllers.handleURIErrors);
|
||||
app.use(controllers.handleErrors);
|
||||
|
||||
@@ -1,51 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
var path = require('path');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var meta = require('../meta');
|
||||
|
||||
|
||||
function sendMinifiedJS(req, res) {
|
||||
var target = path.basename(req.path);
|
||||
var cache = meta.js.target[target] ? meta.js.target[target].cache : '';
|
||||
res.type('text/javascript').send(cache);
|
||||
}
|
||||
|
||||
// The portions of code involving the source map are commented out as they're broken in UglifyJS2
|
||||
// Follow along here: https://github.com/mishoo/UglifyJS2/issues/700
|
||||
// function sendJSSourceMap(req, res) {
|
||||
// if (meta.js.hasOwnProperty('map')) {
|
||||
// res.type('application/json').send(meta.js.map);
|
||||
// } else {
|
||||
// res.redirect(404);
|
||||
// }
|
||||
// };
|
||||
|
||||
function sendStylesheet(req, res) {
|
||||
res.type('text/css').status(200).send(meta.css.cache);
|
||||
}
|
||||
|
||||
function sendACPStylesheet(req, res) {
|
||||
res.type('text/css').status(200).send(meta.css.acpCache);
|
||||
}
|
||||
|
||||
function sendSoundFile(req, res, next) {
|
||||
var resolved = meta.sounds._filePathHash[path.basename(req.path)];
|
||||
|
||||
if (resolved) {
|
||||
res.status(200).sendFile(resolved);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (app, middleware, controllers) {
|
||||
app.get('/stylesheet.css', middleware.addExpiresHeaders, sendStylesheet);
|
||||
app.get('/admin.css', middleware.addExpiresHeaders, sendACPStylesheet);
|
||||
app.get('/nodebb.min.js', middleware.addExpiresHeaders, sendMinifiedJS);
|
||||
app.get('/acp.min.js', middleware.addExpiresHeaders, sendMinifiedJS);
|
||||
// app.get('/nodebb.min.js.map', middleware.addExpiresHeaders, sendJSSourceMap);
|
||||
app.get('/sitemap.xml', controllers.sitemap.render);
|
||||
app.get('/sitemap/pages.xml', controllers.sitemap.getPages);
|
||||
app.get('/sitemap/categories.xml', controllers.sitemap.getCategories);
|
||||
@@ -53,8 +8,4 @@ module.exports = function (app, middleware, controllers) {
|
||||
app.get('/robots.txt', controllers.robots);
|
||||
app.get('/manifest.json', controllers.manifest);
|
||||
app.get('/css/previews/:theme', controllers.admin.themes.get);
|
||||
|
||||
if (nconf.get('local-assets') === false) {
|
||||
app.get('/sounds/*', middleware.addExpiresHeaders, sendSoundFile);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
var _ = require('underscore');
|
||||
var path = require('path');
|
||||
|
||||
var plugins = require('../plugins');
|
||||
|
||||
module.exports = function (app, middleware, controllers) {
|
||||
// Static Assets
|
||||
app.get('/plugins/:id/*', middleware.addExpiresHeaders, function (req, res, next) {
|
||||
|
||||
var relPath = req._parsedUrl.pathname.replace('/plugins/', '');
|
||||
|
||||
var matches = _.map(plugins.staticDirs, function (realPath, mappedPath) {
|
||||
if (relPath.match(mappedPath)) {
|
||||
var pathToFile = path.join(plugins.staticDirs[mappedPath], decodeURIComponent(relPath.slice(mappedPath.length)));
|
||||
if (pathToFile.startsWith(plugins.staticDirs[mappedPath])) {
|
||||
return pathToFile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (!matches || !matches.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.sendFile(matches[0], {}, function (err) {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
// File doesn't exist, this isn't an error, to send to 404 handler
|
||||
return next();
|
||||
} else {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -147,7 +147,7 @@ function getMatchedPosts(pids, data, callback) {
|
||||
topicFields.push('postcount');
|
||||
}
|
||||
|
||||
if (data.sortBy) {
|
||||
if (data.sortBy && data.sortBy !== 'relevance') {
|
||||
if (data.sortBy.startsWith('category')) {
|
||||
topicFields.push('cid');
|
||||
} else if (data.sortBy.startsWith('topic.')) {
|
||||
@@ -328,7 +328,7 @@ function filterByTags(posts, hasTags) {
|
||||
}
|
||||
|
||||
function sortPosts(posts, data) {
|
||||
if (!posts.length || !data.sortBy) {
|
||||
if (!posts.length || !data.sortBy || data.sortBy === 'relevance') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -168,10 +168,11 @@ SocketAdmin.config.setMultiple = function (socket, data, callback) {
|
||||
key: field,
|
||||
value: data[field]
|
||||
};
|
||||
plugins.fireHook('action:config.set', setting);
|
||||
|
||||
logger.monitorConfig({io: index.server}, setting);
|
||||
}
|
||||
}
|
||||
plugins.fireHook('action:config.set', {settings: data});
|
||||
setImmediate(next);
|
||||
}
|
||||
], callback);
|
||||
|
||||
@@ -69,14 +69,6 @@ User.resetLockouts = function (socket, uids, callback) {
|
||||
async.each(uids, user.auth.resetLockout, callback);
|
||||
};
|
||||
|
||||
User.resetFlags = function (socket, uids, callback) {
|
||||
if (!Array.isArray(uids)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
user.resetFlags(uids, callback);
|
||||
};
|
||||
|
||||
User.validateEmail = function (socket, uids, callback) {
|
||||
if (!Array.isArray(uids)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
|
||||
111
src/socket.io/flags.js
Normal file
111
src/socket.io/flags.js
Normal file
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var S = require('string');
|
||||
|
||||
var user = require('../user');
|
||||
var groups = require('../groups');
|
||||
var posts = require('../posts');
|
||||
var topics = require('../topics');
|
||||
var privileges = require('../privileges');
|
||||
var notifications = require('../notifications');
|
||||
var plugins = require('../plugins');
|
||||
var meta = require('../meta');
|
||||
var utils = require('../../public/src/utils');
|
||||
var flags = require('../flags');
|
||||
|
||||
var SocketFlags = {};
|
||||
|
||||
SocketFlags.create = function (socket, data, callback) {
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:not-logged-in]]'));
|
||||
}
|
||||
|
||||
if (!data || !data.type || !data.id || !data.reason) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
async.apply(flags.validate, {
|
||||
uid: socket.uid,
|
||||
type: data.type,
|
||||
id: data.id
|
||||
}),
|
||||
function (next) {
|
||||
// If we got here, then no errors occurred
|
||||
flags.create(data.type, data.id, socket.uid, data.reason, next);
|
||||
}
|
||||
], function (err, flagObj) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
flags.notify(flagObj, socket.uid);
|
||||
callback(null, flagObj);
|
||||
});
|
||||
};
|
||||
|
||||
SocketFlags.update = function (socket, data, callback) {
|
||||
if (!data || !(data.flagId && data.data)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid)
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
// Translate form data into object
|
||||
payload = data.data.reduce(function (memo, cur) {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, payload);
|
||||
|
||||
flags.update(data.flagId, socket.uid, payload, next);
|
||||
},
|
||||
async.apply(flags.getHistory, data.flagId)
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketFlags.appendNote = function (socket, data, callback) {
|
||||
if (!data || !(data.flagId && data.note)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid)
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
flags.appendNote(data.flagId, socket.uid, data.note, next);
|
||||
},
|
||||
function (next) {
|
||||
async.parallel({
|
||||
"notes": async.apply(flags.getNotes, data.flagId),
|
||||
"history": async.apply(flags.getHistory, data.flagId)
|
||||
}, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
module.exports = SocketFlags;
|
||||
@@ -130,7 +130,7 @@ function onMessage(socket, payload) {
|
||||
|
||||
function requireModules() {
|
||||
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist', 'flags'
|
||||
];
|
||||
|
||||
modules.forEach(function (module) {
|
||||
|
||||
@@ -20,7 +20,6 @@ require('./posts/move')(SocketPosts);
|
||||
require('./posts/votes')(SocketPosts);
|
||||
require('./posts/bookmarks')(SocketPosts);
|
||||
require('./posts/tools')(SocketPosts);
|
||||
require('./posts/flag')(SocketPosts);
|
||||
|
||||
SocketPosts.reply = function (socket, data, callback) {
|
||||
if (!data || !data.tid || !data.content) {
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var S = require('string');
|
||||
|
||||
var user = require('../../user');
|
||||
var groups = require('../../groups');
|
||||
var posts = require('../../posts');
|
||||
var topics = require('../../topics');
|
||||
var privileges = require('../../privileges');
|
||||
var notifications = require('../../notifications');
|
||||
var plugins = require('../../plugins');
|
||||
var meta = require('../../meta');
|
||||
var utils = require('../../../public/src/utils');
|
||||
|
||||
module.exports = function (SocketPosts) {
|
||||
|
||||
SocketPosts.flag = function (socket, data, callback) {
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:not-logged-in]]'));
|
||||
}
|
||||
|
||||
if (!data || !data.pid || !data.reason) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var flaggingUser = {};
|
||||
var post;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next);
|
||||
},
|
||||
function (postData, next) {
|
||||
if (parseInt(postData.deleted, 10) === 1) {
|
||||
return next(new Error('[[error:post-deleted]]'));
|
||||
}
|
||||
|
||||
post = postData;
|
||||
topics.getTopicFields(post.tid, ['title', 'cid'], next);
|
||||
},
|
||||
function (topicData, next) {
|
||||
post.topic = topicData;
|
||||
|
||||
async.parallel({
|
||||
isAdminOrMod: function (next) {
|
||||
privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next);
|
||||
},
|
||||
userData: function (next) {
|
||||
user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next);
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
function (user, next) {
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) {
|
||||
return next(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
if (parseInt(user.banned, 10) === 1) {
|
||||
return next(new Error('[[error:user-banned]]'));
|
||||
}
|
||||
|
||||
flaggingUser = user.userData;
|
||||
flaggingUser.uid = socket.uid;
|
||||
|
||||
posts.flag(post, socket.uid, data.reason, next);
|
||||
},
|
||||
function (next) {
|
||||
async.parallel({
|
||||
post: function (next) {
|
||||
posts.parsePost(post, next);
|
||||
},
|
||||
admins: function (next) {
|
||||
groups.getMembers('administrators', 0, -1, next);
|
||||
},
|
||||
globalMods: function (next) {
|
||||
groups.getMembers('Global Moderators', 0, -1, next);
|
||||
},
|
||||
moderators: function (next) {
|
||||
groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next);
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
var title = S(post.topic.title).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: post.content,
|
||||
pid: data.pid,
|
||||
path: '/post/' + data.pid,
|
||||
nid: 'post_flag:' + data.pid + ':uid:' + socket.uid,
|
||||
from: socket.uid,
|
||||
mergeId: 'notifications:user_flagged_post_in|' + data.pid,
|
||||
topicTitle: post.topic.title
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:post.flag', {post: post, reason: data.reason, flaggingUser: flaggingUser});
|
||||
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next);
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.dismissFlag = function (socket, pid, callback) {
|
||||
if (!pid || !socket.uid) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(socket.uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalModerator, next) {
|
||||
if (!isAdminOrGlobalModerator) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
posts.dismissFlag(pid, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.dismissAllFlags = function (socket, data, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(socket.uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalModerator, next) {
|
||||
if (!isAdminOrGlobalModerator) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
posts.dismissAllFlags(next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.updateFlag = function (socket, data, callback) {
|
||||
if (!data || !(data.pid && data.data)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid)
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
// Translate form data into object
|
||||
payload = data.data.reduce(function (memo, cur) {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, payload);
|
||||
|
||||
posts.updateFlagData(socket.uid, data.pid, payload, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
};
|
||||
@@ -98,7 +98,7 @@ module.exports = function (SocketTopics) {
|
||||
var start = parseInt(data.after, 10);
|
||||
var stop = start + 9;
|
||||
|
||||
topics.getUnreadTopics(data.cid, socket.uid, start, stop, data.filter, callback);
|
||||
topics.getUnreadTopics({cid: data.cid, uid: socket.uid, start: start, stop: stop, filter: data.filter}, callback);
|
||||
};
|
||||
|
||||
SocketTopics.loadMoreRecentTopics = function (socket, data, callback) {
|
||||
|
||||
@@ -51,7 +51,7 @@ module.exports = function (SocketTopics) {
|
||||
SocketTopics.markCategoryTopicsRead = function (socket, cid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
topics.getUnreadTids(cid, socket.uid, '', next);
|
||||
topics.getUnreadTids({cid: cid, uid: socket.uid, filter: ''}, next);
|
||||
},
|
||||
function (tids, next) {
|
||||
SocketTopics.markAsRead(socket, tids, next);
|
||||
|
||||
@@ -99,6 +99,9 @@ module.exports = function (SocketUser) {
|
||||
uploadedpicture: '',
|
||||
picture: userData.uploadedpicture === userData.picture ? '' : userData.picture // if current picture is uploaded picture, reset to user icon
|
||||
}, next);
|
||||
},
|
||||
function (next) {
|
||||
plugins.fireHook('action:user.removeUploadedPicture', {callerUid: socket.uid, uid: data.uid}, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -162,7 +162,7 @@ module.exports = function (SocketUser) {
|
||||
data.email = oldUserData.email;
|
||||
}
|
||||
|
||||
user.updateProfile(data.uid, data, next);
|
||||
user.updateProfile(socket.uid, data, next);
|
||||
},
|
||||
function (userData, next) {
|
||||
function log(type, eventData) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var _ = require('underscore');
|
||||
var validator = require('validator');
|
||||
var S = require('string');
|
||||
var db = require('../database');
|
||||
@@ -82,7 +83,7 @@ module.exports = function (Topics) {
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:topic.save', topicData);
|
||||
plugins.fireHook('action:topic.save', {topic: _.clone(topicData)});
|
||||
next(null, topicData.tid);
|
||||
}
|
||||
], callback);
|
||||
@@ -174,7 +175,7 @@ module.exports = function (Topics) {
|
||||
data.postData.index = 0;
|
||||
|
||||
analytics.increment(['topics', 'topics:byCid:' + data.topicData.cid]);
|
||||
plugins.fireHook('action:topic.post', data.topicData);
|
||||
plugins.fireHook('action:topic.post', {topic: data.topicData, post: data.postData});
|
||||
|
||||
if (parseInt(uid, 10)) {
|
||||
user.notifications.sendTopicNotificationToFollowers(uid, data.topicData, data.postData);
|
||||
@@ -269,7 +270,7 @@ module.exports = function (Topics) {
|
||||
|
||||
Topics.notifyFollowers(postData, uid);
|
||||
analytics.increment(['posts', 'posts:byCid:' + cid]);
|
||||
plugins.fireHook('action:topic.reply', postData);
|
||||
plugins.fireHook('action:topic.reply', {post: _.clone(postData)});
|
||||
|
||||
next(null, postData);
|
||||
}
|
||||
|
||||
@@ -137,15 +137,18 @@ module.exports = function (Topics) {
|
||||
function (next) {
|
||||
reduceCounters(tid, next);
|
||||
}
|
||||
], next);
|
||||
], function (err) {
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
Topics.getTopicData(tid, next);
|
||||
},
|
||||
function (topicData, next) {
|
||||
plugins.fireHook('action:topic.purge', {topic: topicData, uid: uid});
|
||||
db.delete('topic:' + tid, next);
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
plugins.fireHook('action:topic.purge', tid);
|
||||
db.delete('topic:' + tid, callback);
|
||||
});
|
||||
], callback);
|
||||
};
|
||||
|
||||
function deleteFromFollowersIgnorers(tid, callback) {
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
|
||||
var db = require('../database');
|
||||
var user = require('../user');
|
||||
var posts = require('../posts');
|
||||
var privileges = require('../privileges');
|
||||
var plugins = require('../plugins');
|
||||
var meta = require('../meta');
|
||||
|
||||
|
||||
module.exports = function (Topics) {
|
||||
|
||||
Topics.createTopicFromPosts = function (uid, title, pids, fromTid, callback) {
|
||||
@@ -55,11 +53,14 @@ module.exports = function (Topics) {
|
||||
}
|
||||
Topics.create({uid: results.postData.uid, title: title, cid: cid}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
Topics.updateTopicBookmarks(fromTid, pids, function () { next( null, results );} );
|
||||
function (_tid, next) {
|
||||
Topics.updateTopicBookmarks(fromTid, pids, function (err) {
|
||||
next(err, _tid);
|
||||
});
|
||||
},
|
||||
function (_tid, next) {
|
||||
function move(pid, next) {
|
||||
tid = _tid;
|
||||
async.eachSeries(pids, function (pid, next) {
|
||||
privileges.posts.canEdit(pid, uid, function (err, canEdit) {
|
||||
if (err || !canEdit.flag) {
|
||||
return next(err || new Error(canEdit.message));
|
||||
@@ -67,14 +68,13 @@ module.exports = function (Topics) {
|
||||
|
||||
Topics.movePostToTopic(pid, tid, next);
|
||||
});
|
||||
}
|
||||
tid = _tid;
|
||||
async.eachSeries(pids, move, next);
|
||||
}, next);
|
||||
},
|
||||
function (next) {
|
||||
Topics.updateTimestamp(tid, Date.now(), next);
|
||||
},
|
||||
function (next) {
|
||||
plugins.fireHook('action:topic.fork', {tid: tid, fromTid: fromTid, uid: uid});
|
||||
Topics.getTopicData(tid, next);
|
||||
}
|
||||
], callback);
|
||||
|
||||
@@ -56,9 +56,9 @@ module.exports = function (Topics) {
|
||||
topicData.deleted = isDelete ? 1 : 0;
|
||||
|
||||
if (isDelete) {
|
||||
plugins.fireHook('action:topic.delete', topicData);
|
||||
plugins.fireHook('action:topic.delete', {topic: topicData, uid: uid});
|
||||
} else {
|
||||
plugins.fireHook('action:topic.restore', topicData);
|
||||
plugins.fireHook('action:topic.restore', {topic: topicData, uid: uid});
|
||||
}
|
||||
|
||||
var data = {
|
||||
@@ -114,18 +114,18 @@ module.exports = function (Topics) {
|
||||
function toggleLock(tid, uid, lock, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
var cid;
|
||||
var topicData;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Topics.getTopicField(tid, 'cid', next);
|
||||
Topics.getTopicFields(tid, ['tid', 'uid', 'cid'], next);
|
||||
},
|
||||
function (_cid, next) {
|
||||
cid = _cid;
|
||||
if (!cid) {
|
||||
function (_topicData, next) {
|
||||
topicData = _topicData;
|
||||
if (!topicData || !topicData.cid) {
|
||||
return next(new Error('[[error:no-topic]]'));
|
||||
}
|
||||
privileges.categories.isAdminOrMod(cid, uid, next);
|
||||
privileges.categories.isAdminOrMod(topicData.cid, uid, next);
|
||||
},
|
||||
function (isAdminOrMod, next) {
|
||||
if (!isAdminOrMod) {
|
||||
@@ -135,16 +135,11 @@ module.exports = function (Topics) {
|
||||
Topics.setTopicField(tid, 'locked', lock ? 1 : 0, next);
|
||||
},
|
||||
function (next) {
|
||||
var data = {
|
||||
tid: tid,
|
||||
isLocked: lock,
|
||||
uid: uid,
|
||||
cid: cid
|
||||
};
|
||||
topicData.isLocked = lock;
|
||||
|
||||
plugins.fireHook('action:topic.lock', data);
|
||||
plugins.fireHook('action:topic.lock', {topic: _.clone(topicData), uid: uid});
|
||||
|
||||
next(null, data);
|
||||
next(null, topicData);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
@@ -167,7 +162,7 @@ module.exports = function (Topics) {
|
||||
if (!exists) {
|
||||
return callback(new Error('[[error:no-topic]]'));
|
||||
}
|
||||
Topics.getTopicFields(tid, ['cid', 'lastposttime', 'postcount'], next);
|
||||
Topics.getTopicFields(tid, ['uid', 'tid', 'cid', 'lastposttime', 'postcount'], next);
|
||||
},
|
||||
function (_topicData, next) {
|
||||
topicData = _topicData;
|
||||
@@ -198,16 +193,12 @@ module.exports = function (Topics) {
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
var data = {
|
||||
tid: tid,
|
||||
isPinned: pin,
|
||||
uid: uid,
|
||||
cid: topicData.cid
|
||||
};
|
||||
|
||||
plugins.fireHook('action:topic.pin', data);
|
||||
topicData.isPinned = pin;
|
||||
|
||||
next(null, data);
|
||||
plugins.fireHook('action:topic.pin', {topic: _.clone(topicData), uid: uid});
|
||||
|
||||
next(null, topicData);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
@@ -225,7 +216,7 @@ module.exports = function (Topics) {
|
||||
var uniqueCids = _.unique(topicData.map(function (topicData) {
|
||||
return topicData && parseInt(topicData.cid, 10);
|
||||
}));
|
||||
|
||||
|
||||
if (uniqueCids.length > 1 || !uniqueCids.length || !uniqueCids[0]) {
|
||||
return next(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
@@ -249,7 +240,7 @@ module.exports = function (Topics) {
|
||||
setImmediate(next);
|
||||
}
|
||||
}
|
||||
], next);
|
||||
], next);
|
||||
}, next);
|
||||
}
|
||||
], callback);
|
||||
|
||||
@@ -18,14 +18,13 @@ module.exports = function (Topics) {
|
||||
callback = filter;
|
||||
filter = '';
|
||||
}
|
||||
Topics.getUnreadTids(0, uid, filter, function (err, tids) {
|
||||
Topics.getUnreadTids({cid: 0, uid: uid, filter: filter}, function (err, tids) {
|
||||
callback(err, Array.isArray(tids) ? tids.length : 0);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Topics.getUnreadTopics = function (cid, uid, start, stop, filter, callback) {
|
||||
|
||||
Topics.getUnreadTopics = function (params, callback) {
|
||||
var unreadTopics = {
|
||||
showSelect: true,
|
||||
nextStart : 0,
|
||||
@@ -34,7 +33,7 @@ module.exports = function (Topics) {
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Topics.getUnreadTids(cid, uid, filter, next);
|
||||
Topics.getUnreadTids(params, next);
|
||||
},
|
||||
function (tids, next) {
|
||||
unreadTopics.topicCount = tids.length;
|
||||
@@ -43,13 +42,13 @@ module.exports = function (Topics) {
|
||||
return next(null, []);
|
||||
}
|
||||
|
||||
if (stop === -1) {
|
||||
tids = tids.slice(start);
|
||||
if (params.stop === -1) {
|
||||
tids = tids.slice(params.start);
|
||||
} else {
|
||||
tids = tids.slice(start, stop + 1);
|
||||
tids = tids.slice(params.start, params.stop + 1);
|
||||
}
|
||||
|
||||
Topics.getTopicsByTids(tids, uid, next);
|
||||
Topics.getTopicsByTids(tids, params.uid, next);
|
||||
},
|
||||
function (topicData, next) {
|
||||
if (!Array.isArray(topicData) || !topicData.length) {
|
||||
@@ -57,7 +56,7 @@ module.exports = function (Topics) {
|
||||
}
|
||||
|
||||
unreadTopics.topics = topicData;
|
||||
unreadTopics.nextStart = stop + 1;
|
||||
unreadTopics.nextStart = params.stop + 1;
|
||||
next(null, unreadTopics);
|
||||
}
|
||||
], callback);
|
||||
@@ -67,21 +66,19 @@ module.exports = function (Topics) {
|
||||
return Date.now() - (parseInt(meta.config.unreadCutoff, 10) || 2) * 86400000;
|
||||
};
|
||||
|
||||
Topics.getUnreadTids = function (cid, uid, filter, callback) {
|
||||
uid = parseInt(uid, 10);
|
||||
Topics.getUnreadTids = function (params, callback) {
|
||||
var uid = parseInt(params.uid, 10);
|
||||
if (uid === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var cutoff = Topics.unreadCutoff();
|
||||
|
||||
var cutoff = params.cutoff || Topics.unreadCutoff();
|
||||
var ignoredCids;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
ignoredCids: function (next) {
|
||||
if (filter === 'watched') {
|
||||
if (params.filter === 'watched') {
|
||||
return next(null, []);
|
||||
}
|
||||
user.getIgnoredCategories(uid, next);
|
||||
@@ -121,7 +118,7 @@ module.exports = function (Topics) {
|
||||
if (results.ignoredTids.indexOf(recentTopic.value.toString()) !== -1) {
|
||||
return false;
|
||||
}
|
||||
switch (filter) {
|
||||
switch (params.filter) {
|
||||
case 'new':
|
||||
return !userRead[recentTopic.value];
|
||||
default:
|
||||
@@ -133,7 +130,7 @@ module.exports = function (Topics) {
|
||||
return array.indexOf(tid) === index;
|
||||
});
|
||||
|
||||
if (filter === 'watched') {
|
||||
if (params.filter === 'watched') {
|
||||
Topics.filterWatchedTids(tids, uid, next);
|
||||
} else {
|
||||
next(null, tids);
|
||||
@@ -143,7 +140,7 @@ module.exports = function (Topics) {
|
||||
|
||||
tids = tids.slice(0, 200);
|
||||
|
||||
filterTopics(uid, tids, cid, ignoredCids, filter, next);
|
||||
filterTopics(uid, tids, params.cid, ignoredCids, params.filter, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ var schemaDate;
|
||||
var thisSchemaDate;
|
||||
|
||||
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
|
||||
var latestSchema = Date.UTC(2016, 10, 22);
|
||||
var latestSchema = Date.UTC(2016, 11, 7);
|
||||
|
||||
Upgrade.check = function (callback) {
|
||||
db.get('schemaDate', function (err, value) {
|
||||
@@ -277,7 +277,7 @@ Upgrade.upgrade = function (callback) {
|
||||
|
||||
if (schemaDate < thisSchemaDate) {
|
||||
updatesMade = true;
|
||||
winston.info('[2016/11/25] Creating sorted sets for pinned topcis');
|
||||
winston.info('[2016/11/25] Creating sorted sets for pinned topics');
|
||||
|
||||
var topics = require('./topics');
|
||||
var batch = require('./batch');
|
||||
@@ -314,6 +314,101 @@ Upgrade.upgrade = function (callback) {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
thisSchemaDate = Date.UTC(2016, 11, 7);
|
||||
|
||||
if (schemaDate < thisSchemaDate) {
|
||||
updatesMade = true;
|
||||
winston.info('[2016/12/07] Migrating flags to new schema (#5232)');
|
||||
|
||||
var batch = require('./batch');
|
||||
var posts = require('./posts');
|
||||
var flags = require('./flags');
|
||||
var migrated = 0;
|
||||
|
||||
batch.processSortedSet('posts:pid', function (ids, next) {
|
||||
posts.getPostsByPids(ids, 1, function (err, posts) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
posts = posts.filter(function (post) {
|
||||
return post.hasOwnProperty('flags');
|
||||
});
|
||||
|
||||
async.each(posts, function (post, next) {
|
||||
async.parallel({
|
||||
uids: async.apply(db.getSortedSetRangeWithScores, 'pid:' + post.pid + ':flag:uids', 0, -1),
|
||||
reasons: async.apply(db.getSortedSetRange, 'pid:' + post.pid + ':flag:uid:reason', 0, -1)
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Adding in another check here in case a post was improperly dismissed (flag count > 1 but no flags in db)
|
||||
if (!data.uids.length || !data.reasons.length) {
|
||||
return setImmediate(next);
|
||||
}
|
||||
|
||||
// Just take the first entry
|
||||
var datetime = data.uids[0].score;
|
||||
var reason = data.reasons[0].split(':')[1];
|
||||
var flagObj;
|
||||
|
||||
async.waterfall([
|
||||
async.apply(flags.create, 'post', post.pid, data.uids[0].value, reason, datetime),
|
||||
function (_flagObj, next) {
|
||||
flagObj = _flagObj;
|
||||
if (post['flag:state'] || post['flag:assignee']) {
|
||||
flags.update(flagObj.flagId, 1, {
|
||||
state: post['flag:state'],
|
||||
assignee: post['flag:assignee'],
|
||||
datetime: datetime
|
||||
}, next);
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
if (post.hasOwnProperty('flag:notes') && post['flag:notes'].length) {
|
||||
try {
|
||||
var history = JSON.parse(post['flag:history']);
|
||||
history = history.filter(function (event) {
|
||||
return event.type === 'notes';
|
||||
})[0];
|
||||
|
||||
flags.appendNote(flagObj.flagId, history.uid, post['flag:notes'], history.timestamp, next);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
}
|
||||
], function (err) {
|
||||
if (err && err.message === '[[error:already-flagged]]') {
|
||||
// Already flagged, no need to parse, but not an error
|
||||
next();
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, next);
|
||||
});
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - done');
|
||||
Upgrade.update(thisSchemaDate, next);
|
||||
});
|
||||
} else {
|
||||
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - skipped!');
|
||||
next();
|
||||
}
|
||||
}
|
||||
// Add new schema updates here
|
||||
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
|
||||
], function (err) {
|
||||
|
||||
10
src/user.js
10
src/user.js
@@ -256,6 +256,16 @@ var meta = require('./meta');
|
||||
privileges.users.isGlobalModerator(uid, callback);
|
||||
};
|
||||
|
||||
User.isPrivileged = function (uid, callback) {
|
||||
async.parallel([
|
||||
async.apply(User.isAdministrator, uid),
|
||||
async.apply(User.isGlobalModerator, uid),
|
||||
async.apply(User.isModeratorOfAnyCategory, uid)
|
||||
], function (err, results) {
|
||||
callback(err, results ? results.some(Boolean) : false);
|
||||
});
|
||||
};
|
||||
|
||||
User.isAdminOrGlobalMod = function (uid, callback) {
|
||||
async.parallel({
|
||||
isAdmin: async.apply(User.isAdministrator, uid),
|
||||
|
||||
@@ -6,6 +6,7 @@ var db = require('../database');
|
||||
var posts = require('../posts');
|
||||
var plugins = require('../plugins');
|
||||
var winston = require('winston');
|
||||
var flags = require('../flags');
|
||||
|
||||
module.exports = function (User) {
|
||||
|
||||
@@ -55,14 +56,4 @@ module.exports = function (User) {
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
User.resetFlags = function (uids, callback) {
|
||||
if (!Array.isArray(uids) || !uids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
async.eachSeries(uids, function (uid, next) {
|
||||
posts.dismissUserFlags(uid, next);
|
||||
}, callback);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -141,7 +141,7 @@ module.exports = function (User) {
|
||||
if (userNameChanged) {
|
||||
User.notifications.sendNameChangeNotification(userData.uid, userData.username);
|
||||
}
|
||||
plugins.fireHook('action:user.create', userData);
|
||||
plugins.fireHook('action:user.create', {user: userData});
|
||||
next(null, userData.uid);
|
||||
}
|
||||
], callback);
|
||||
|
||||
@@ -34,11 +34,20 @@ module.exports = function (User) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(uids) || !uids.length) {
|
||||
// Eliminate duplicates and build ref table
|
||||
var uniqueUids = uids.filter(function (uid, index) {
|
||||
return index === uids.indexOf(uid);
|
||||
});
|
||||
var ref = uniqueUids.reduce(function (memo, cur, idx) {
|
||||
memo[cur] = idx;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
if (!Array.isArray(uniqueUids) || !uniqueUids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var keys = uids.map(function (uid) {
|
||||
var keys = uniqueUids.map(function (uid) {
|
||||
return 'user:' + uid;
|
||||
});
|
||||
|
||||
@@ -60,6 +69,10 @@ module.exports = function (User) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
users = uids.map(function (uid) {
|
||||
return users[ref[uid]];
|
||||
});
|
||||
|
||||
modifyUserData(users, fieldsToRemove, callback);
|
||||
});
|
||||
};
|
||||
@@ -80,7 +93,16 @@ module.exports = function (User) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var keys = uids.map(function (uid) {
|
||||
// Eliminate duplicates and build ref table
|
||||
var uniqueUids = uids.filter(function (uid, index) {
|
||||
return index === uids.indexOf(uid);
|
||||
});
|
||||
var ref = uniqueUids.reduce(function (memo, cur, idx) {
|
||||
memo[cur] = idx;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
var keys = uniqueUids.map(function (uid) {
|
||||
return 'user:' + uid;
|
||||
});
|
||||
|
||||
@@ -89,6 +111,10 @@ module.exports = function (User) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
users = uids.map(function (uid) {
|
||||
return users[ref[uid]];
|
||||
});
|
||||
|
||||
modifyUserData(users, [], callback);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -57,7 +57,8 @@ module.exports = function (User) {
|
||||
path: picture.path,
|
||||
extension: extension,
|
||||
width: imageDimension,
|
||||
height: imageDimension
|
||||
height: imageDimension,
|
||||
write: false,
|
||||
}, next);
|
||||
},
|
||||
function (next) {
|
||||
|
||||
@@ -16,6 +16,9 @@ module.exports = function (User) {
|
||||
var fields = ['username', 'email', 'fullname', 'website', 'location',
|
||||
'groupTitle', 'birthday', 'signature', 'aboutme'];
|
||||
|
||||
var updateUid = data.uid;
|
||||
var oldData;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
plugins.fireHook('filter:user.updateProfile', {uid: uid, data: data, fields: fields}, next);
|
||||
@@ -27,14 +30,18 @@ module.exports = function (User) {
|
||||
async.series([
|
||||
async.apply(isAboutMeValid, data),
|
||||
async.apply(isSignatureValid, data),
|
||||
async.apply(isEmailAvailable, data, uid),
|
||||
async.apply(isUsernameAvailable, data, uid),
|
||||
async.apply(isEmailAvailable, data, updateUid),
|
||||
async.apply(isUsernameAvailable, data, updateUid),
|
||||
async.apply(isGroupTitleValid, data)
|
||||
], function (err) {
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
User.getUserFields(updateUid, fields, next);
|
||||
},
|
||||
function (_oldData, next) {
|
||||
oldData = _oldData;
|
||||
async.each(fields, function (field, next) {
|
||||
if (!(data[field] !== undefined && typeof data[field] === 'string')) {
|
||||
return next();
|
||||
@@ -43,21 +50,21 @@ module.exports = function (User) {
|
||||
data[field] = data[field].trim();
|
||||
|
||||
if (field === 'email') {
|
||||
return updateEmail(uid, data.email, next);
|
||||
return updateEmail(updateUid, data.email, next);
|
||||
} else if (field === 'username') {
|
||||
return updateUsername(uid, data.username, next);
|
||||
return updateUsername(updateUid, data.username, next);
|
||||
} else if (field === 'fullname') {
|
||||
return updateFullname(uid, data.fullname, next);
|
||||
return updateFullname(updateUid, data.fullname, next);
|
||||
} else if (field === 'signature') {
|
||||
data[field] = S(data[field]).stripTags().s;
|
||||
}
|
||||
|
||||
User.setUserField(uid, field, data[field], next);
|
||||
User.setUserField(updateUid, field, data[field], next);
|
||||
}, next);
|
||||
},
|
||||
function (next) {
|
||||
plugins.fireHook('action:user.updateProfile', {data: data, uid: uid});
|
||||
User.getUserFields(uid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], next);
|
||||
plugins.fireHook('action:user.updateProfile', {uid: uid, data: data, fields: fields, oldData: oldData});
|
||||
User.getUserFields(updateUid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<title>{title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{relative_path}/admin.css?{cache-buster}" />
|
||||
<link rel="stylesheet" type="text/css" href="{relative_path}/vendor/mdl/mdl.min.css?{cache-buster}" />
|
||||
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/admin.css?{cache-buster}" />
|
||||
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/vendor/mdl/mdl.min.css?{cache-buster}" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
|
||||
<script>
|
||||
@@ -21,17 +21,17 @@
|
||||
</script>
|
||||
|
||||
<script src="https://storage.googleapis.com/code.getmdl.io/1.0.3/material.min.js"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/acp.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/colorpicker/colorpicker.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/src/admin/admin.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/ace/ace.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/semver/semver.browser.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/jquery/deserialize/jquery.deserialize.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/snackbar/snackbar.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/slideout/slideout.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/vendor/nprogress.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/acp.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/colorpicker/colorpicker.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/src/admin/admin.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/ace/ace.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/semver/semver.browser.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/deserialize/jquery.deserialize.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/snackbar/snackbar.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/slideout/slideout.min.js?{cache-buster}"></script>
|
||||
<script type="text/javascript" src="{relative_path}/assets/vendor/nprogress.min.js?{cache-buster}"></script>
|
||||
|
||||
<!-- BEGIN scripts -->
|
||||
<script type="text/javascript" src="{scripts.src}"></script>
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
<div class="flags">
|
||||
|
||||
<div class="col-lg-12">
|
||||
|
||||
<div class="text-center">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div><canvas id="flags:daily" height="250"></canvas></div>
|
||||
<p>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-footer"><small>[[admin/manage/flags:daily]]</small></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="flag-search" method="GET" action="flags">
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<div>
|
||||
<label>[[admin/manage/flags:by-user]]</label>
|
||||
<input type="text" class="form-control" id="byUsername" placeholder="[[admin/manage/flags:by-user-search]]" name="byUsername" value="{byUsername}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<div>
|
||||
<label>[[admin/manage/flags:category]]</label>
|
||||
<select class="form-control" id="category-selector" name="cid">
|
||||
<option value="">[[unread:all_categories]]</option>
|
||||
<!-- BEGIN categories -->
|
||||
<option value="{categories.cid}" <!-- IF categories.selected -->selected<!-- ENDIF categories.selected -->>{categories.text}</option>
|
||||
<!-- END categories -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>[[admin/manage/flags:sort-by]]</label>
|
||||
<div>
|
||||
<div>
|
||||
<select id="flag-sort-by" class="form-control" name="sortBy">
|
||||
<option value="count" <!-- IF sortByCount -->selected<!-- ENDIF sortByCount -->>[[admin/manage/flags:sort-by.most-flags]]</option>
|
||||
<option value="time" <!-- IF sortByTime -->selected<!-- ENDIF sortByTime -->>[[admin/manage/flags:sort-by.most-recent]]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">[[admin/manage/flags:search]]</button>
|
||||
<button class="btn btn-primary" id="dismissAll">[[admin/manage/flags:dismiss-all]]</button>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div data-next="{next}">
|
||||
|
||||
<div component="posts/flags" class="panel-group post-container" id="accordion" role="tablist" aria-multiselectable="true" data-next="{next}">
|
||||
<!-- IF !posts.length -->
|
||||
<div class="alert alert-success">
|
||||
[[admin/manage/flags:none-flagged]]
|
||||
</div>
|
||||
<!-- ENDIF !posts.length -->
|
||||
|
||||
<!-- BEGIN posts -->
|
||||
<div class="panel panel-default" component="posts/flag" data-pid="{../pid}">
|
||||
<div class="panel-heading" role="tab">
|
||||
<h4 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#flag-pid-{posts.pid}" aria-expanded="true" aria-controls="flag-pid-{posts.pid}">
|
||||
<!-- IF ../flagData.assignee -->
|
||||
<div class="pull-right">
|
||||
<!-- IF ../flagData.assigneeUser.picture -->
|
||||
<img class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" src="{../flagData.assigneeUser.picture}">
|
||||
<!-- ELSE -->
|
||||
<div class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" style="background-color: {../flagData.assigneeUser.icon:bgColor};">{../flagData.assigneeUser.icon:text}</div>
|
||||
<!-- ENDIF ../flagData.assigneeUser.picture -->
|
||||
</div>
|
||||
<!-- ENDIF ../flagData.assignee -->
|
||||
<span class="label <!-- IF ../flagData.labelClass -->label-{../flagData.labelClass}<!-- ELSE -->label-info<!-- ENDIF ../flagData.labelClass -->">[[topic:flag_manage_state_<!-- IF ../flagData.state -->{../flagData.state}<!-- ELSE -->open<!-- ENDIF ../flagData.state -->]]</span>
|
||||
[[topic:flag_manage_title, {posts.category.name}]]
|
||||
<small><span class="timeago" title="{posts.timestampISO}"></span></small>
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="flag-pid-{posts.pid}" class="panel-collapse collapse" role="tabpanel">
|
||||
<div class="panel-body">
|
||||
<div class="row" data-pid="{posts.pid}" data-tid="{posts.topic.tid}">
|
||||
<div class="col-sm-8">
|
||||
<div class="well flag-post-body">
|
||||
<a href="{config.relative_path}/user/{../user.userslug}">
|
||||
<!-- IF ../user.picture -->
|
||||
<img title="{posts.user.username}" src="{../user.picture}">
|
||||
<!-- ELSE -->
|
||||
<div title="{posts.user.username}" class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
|
||||
<!-- ENDIF ../user.picture -->
|
||||
</a>
|
||||
|
||||
<a href="{config.relative_path}/user/{../user.userslug}">
|
||||
<strong><span>{../user.username}</span></strong>
|
||||
</a>
|
||||
<div class="content">
|
||||
<p>{posts.content}</p>
|
||||
</div>
|
||||
<small>
|
||||
<span class="pull-right">
|
||||
[[posted-in, <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>]],
|
||||
<span class="timeago" title="{posts.timestampISO}"></span> •
|
||||
<a href="{config.relative_path}/post/{posts.pid}" target="_blank">[[admin/manage/flags:read-more]]</a>
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<i class="fa fa-flag"></i>
|
||||
[[admin/manage/flags:flagged-x-times, {posts.flags}]]
|
||||
<blockquote class="flag-reporters">
|
||||
<ul>
|
||||
<!-- BEGIN posts.flagReasons -->
|
||||
<li>
|
||||
<a target="_blank" href="{config.relative_path}/user/{../user.userslug}">
|
||||
<!-- IF ../user.picture -->
|
||||
<img src="{../user.picture}" />
|
||||
<!-- ELSE -->
|
||||
<div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
|
||||
<!-- ENDIF ../user.picture -->
|
||||
{../user.username}
|
||||
</a>: "{posts.flagReasons.reason}"
|
||||
</li>
|
||||
<!-- END posts.flagReasons -->
|
||||
</ul>
|
||||
</blockquote>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-success dismiss">
|
||||
[[admin/manage/flags:dismiss]]
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger delete">
|
||||
[[admin/manage/flags:delete-post]]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<form role="form">
|
||||
<div class="form-group">
|
||||
<label for="{posts.pid}-assignee">
|
||||
[[topic:flag_manage_assignee]]
|
||||
</label>
|
||||
<select class="form-control" id="{posts.pid}-assignee" name="assignee">
|
||||
<!-- BEGIN assignees -->
|
||||
<option value="{assignees.uid}">{assignees.username}</option>
|
||||
<!-- END assignees -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{posts.pid}-state">
|
||||
[[topic:flag_manage_state]]
|
||||
</label>
|
||||
<select class="form-control" id="{posts.pid}-state" name="state">
|
||||
<option value="open">
|
||||
[[topic:flag_manage_state_open]]
|
||||
</option>
|
||||
<option value="wip">
|
||||
[[topic:flag_manage_state_wip]]
|
||||
</option>
|
||||
<option value="resolved">
|
||||
[[topic:flag_manage_state_resolved]]
|
||||
</option>
|
||||
<option value="rejected">
|
||||
[[topic:flag_manage_state_rejected]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{posts.pid}-notes">
|
||||
[[topic:flag_manage_notes]]
|
||||
</label>
|
||||
<textarea class="form-control" id="{posts.pid}-notes" name="notes"></textarea>
|
||||
</div>
|
||||
<button type="button" component="posts/flag/update" class="btn btn-sm btn-primary btn-block">
|
||||
[[topic:flag_manage_update]]
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h5>[[topic:flag_manage_history]]</h5>
|
||||
<!-- IF !posts.flagData.history.length -->
|
||||
<div class="alert alert-info">
|
||||
[[topic:flag_manage_no_history]]
|
||||
</div>
|
||||
<!-- ELSE -->
|
||||
<ul class="list-group" component="posts/flag/history">
|
||||
<!-- BEGIN posts.flagData.history -->
|
||||
<li class="list-group-item">
|
||||
<div class="pull-right"><small><span class="timeago" title="{posts.flagData.history.timestampISO}"></span></small></div>
|
||||
<!-- IF ../user.picture -->
|
||||
<img class="avatar avatar-sm avatar-rounded" src="{../user.picture}" title="{../user.username}" />
|
||||
<!-- ELSE -->
|
||||
<div class="avatar avatar-sm avatar-rounded" style="background-color: {../user.icon:bgColor};" title="{../user.username}">{../user.icon:text}</div>
|
||||
<!-- ENDIF ../user.picture -->
|
||||
[[topic:flag_manage_history_{posts.flagData.history.type}, {posts.flagData.history.label}]]
|
||||
</li>
|
||||
<!-- END posts.flagData.history -->
|
||||
</ul>
|
||||
<!-- ENDIF !posts.flagData.history.length -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END posts -->
|
||||
<!-- IMPORT partials/paginator.tpl -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +19,6 @@
|
||||
<li><a href="#" class="ban-user-temporary"><i class="fa fa-fw fa-clock-o"></i>[[admin/manage/users:temp-ban]]</a></li>
|
||||
<li><a href="#" class="unban-user"><i class="fa fa-fw fa-comment-o"></i> [[admin/manage/users:unban]]</a></li>
|
||||
<li><a href="#" class="reset-lockout"><i class="fa fa-fw fa-unlock"></i> [[admin/manage/users:reset-lockout]]</a></li>
|
||||
<li><a href="#" class="reset-flags"><i class="fa fa-fw fa-flag"></i> [[admin/manage/users:reset-flags]]</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" class="delete-user"><i class="fa fa-fw fa-trash-o"></i> [[admin/manage/users:delete]]</a></li>
|
||||
<li><a href="#" class="delete-user-and-content"><i class="fa fa-fw fa-trash-o"></i> [[admin/manage/users:purge]]</a></li>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/flags">[[admin/menu:manage/flags]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -192,7 +191,6 @@
|
||||
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/flags">[[admin/menu:manage/flags]]</a></li>
|
||||
<li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -113,6 +113,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/general:search-default-sort-by]]</div>
|
||||
<div class="col-sm-10 col-xs-12">
|
||||
<select id="post-sort-by" class="form-control" data-field="searchDefaultSortBy">
|
||||
<option value="relevance">[[search:relevance]]</option>
|
||||
<option value="timestamp">[[search:post-time]]</option>
|
||||
<option value="teaser.timestamp">[[search:last-reply-time]]</option>
|
||||
<option value="topic.title">[[search:topic-title]]</option>
|
||||
<option value="topic.postcount">[[search:number-of-replies]]</option>
|
||||
<option value="topic.viewcount">[[search:number-of-views]]</option>
|
||||
<option value="topic.timestamp">[[search:topic-start-date]]</option>
|
||||
<option value="user.username">[[search:username]]</option>
|
||||
<option value="category.name">[[search:category]]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/general:outgoing-links]]</div>
|
||||
<div class="col-sm-10 col-xs-12">
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<p class="help-block">
|
||||
[[admin/settings/group:default-cover-help]]
|
||||
</p>
|
||||
<input type="text" class="form-control input-lg" id="groups:defaultCovers" data-field="groups:defaultCovers" value="{config.relative_path}/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" /><br />
|
||||
<input type="text" class="form-control input-lg" id="groups:defaultCovers" data-field="groups:defaultCovers" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" /><br />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<p class="help-block">
|
||||
[[admin/settings/uploads:default-covers-help]]
|
||||
</p>
|
||||
<input type="text" class="form-control input-lg" id="profile:defaultCovers" data-field="profile:defaultCovers" value="{config.relative_path}/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" />
|
||||
<input type="text" class="form-control input-lg" id="profile:defaultCovers" data-field="profile:defaultCovers" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ server.on('error', function (err) {
|
||||
winston.error(err);
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
winston.error('NodeBB address in use, exiting...');
|
||||
process.exit(0);
|
||||
process.exit(1);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
@@ -88,7 +88,6 @@ function initializeNodeBB(callback) {
|
||||
plugins.init(app, middleware, next);
|
||||
},
|
||||
async.apply(plugins.fireHook, 'static:assets.prepare', {}),
|
||||
async.apply(meta.js.bridgeModules, app),
|
||||
function (next) {
|
||||
plugins.fireHook('static:app.preload', {
|
||||
app: app,
|
||||
@@ -104,9 +103,6 @@ function initializeNodeBB(callback) {
|
||||
},
|
||||
function (next) {
|
||||
async.series([
|
||||
async.apply(meta.js.getFromFile, 'nodebb.min.js'),
|
||||
async.apply(meta.js.getFromFile, 'acp.min.js'),
|
||||
async.apply(meta.css.getFromFile),
|
||||
async.apply(meta.sounds.init),
|
||||
async.apply(languages.init),
|
||||
async.apply(meta.blacklist.load)
|
||||
@@ -266,11 +262,11 @@ module.exports.testSocket = function (socketPath, callback) {
|
||||
var file = require('./file');
|
||||
async.series([
|
||||
function (next) {
|
||||
file.exists(socketPath, function (exists) {
|
||||
file.exists(socketPath, function (err, exists) {
|
||||
if (exists) {
|
||||
next();
|
||||
} else {
|
||||
callback();
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
var plugins = require('../plugins');
|
||||
|
||||
var admin = {};
|
||||
@@ -25,7 +26,7 @@ admin.get = function (callback) {
|
||||
plugins.fireHook('filter:widgets.getWidgets', [], next);
|
||||
},
|
||||
adminTemplate: function (next) {
|
||||
fs.readFile(path.resolve(__dirname, '../../public/templates/admin/partials/widget-settings.tpl'), 'utf8', next);
|
||||
fs.readFile(path.resolve(nconf.get('views_dir'), 'admin/partials/widget-settings.tpl'), 'utf8', next);
|
||||
}
|
||||
}, function (err, widgetData) {
|
||||
if (err) {
|
||||
|
||||
Reference in New Issue
Block a user