diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json index 5d9799de50..6b4e96f4fb 100644 --- a/public/language/en-GB/flags.json +++ b/public/language/en-GB/flags.json @@ -13,6 +13,7 @@ "filter-reset": "Remove Filters", "filters": "Filter Options", "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", "filter-type": "Flag Type", "filter-type-all": "All Content", "filter-type-post": "Post", @@ -21,6 +22,13 @@ "filter-quick-mine": "Assigned to me", "apply-filters": "Apply Filters", + "quick-links": "Quick Links", + "flagged-user": "Flagged User", + "reporter": "Reporting User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "notes": "Flag Notes", "add-note": "Add Note", "no-notes": "No shared notes.", diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js index a5277f98df..cbdaf94f24 100644 --- a/public/src/client/flags/list.js +++ b/public/src/client/flags/list.js @@ -7,6 +7,7 @@ define('forum/flags/list', ['components'], function (components) { Flags.init = function () { Flags.enableFilterForm(); + Flags.enableChatButtons(); }; Flags.enableFilterForm = function () { @@ -29,5 +30,11 @@ define('forum/flags/list', ['components'], function (components) { }); }; + Flags.enableChatButtons = function () { + $('[data-chat]').on('click', function () { + app.newChat(this.getAttribute('data-chat')); + }); + }; + return Flags; }); diff --git a/public/src/client/topic/flag.js b/public/src/client/topic/flag.js index 046585ae68..6b3440da54 100644 --- a/public/src/client/topic/flag.js +++ b/public/src/client/topic/flag.js @@ -48,7 +48,7 @@ define('forum/topic/flag', [], function () { if (!pid || !reason) { return; } - socket.emit('flags.create', {pid: pid, reason: reason}, function (err) { + socket.emit('flags.create', {type: 'post', id: pid, reason: reason}, function (err) { if (err) { return app.alertError(err.message); } diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 512ad32222..788294a01e 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -26,7 +26,7 @@ modsController.flags.list = function (req, res, next) { } // Parse query string params for filters - var valid = ['assignee', 'state', 'reporterId', 'type', 'quick']; + var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'quick']; var filters = valid.reduce(function (memo, cur) { if (req.query.hasOwnProperty(cur)) { memo[cur] = req.query[cur]; diff --git a/src/flags.js b/src/flags.js index a8bca052d1..45aae2502e 100644 --- a/src/flags.js +++ b/src/flags.js @@ -1,23 +1,22 @@ - - '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 = { - _defaults: { - state: 'open', - assignee: null - } -}; +var Flags = {}; Flags.get = function (flagId, callback) { async.waterfall([ @@ -72,6 +71,10 @@ Flags.list = function (filters, uid, callback) { case 'assignee': sets.push('flags:byAssignee:' + filters[type]); break; + + case 'targetUid': + sets.push('flags:byTargetUid:' + filters[type]); + break; case 'quick': switch (filters.quick) { @@ -145,6 +148,43 @@ Flags.list = function (filters, uid, 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 (data.reporter.banned) { + return callback(new Error('[[error:user-banned]]')); + } + + switch (payload.type) { + case 'post': + async.parallel({ + privileges: async.apply(privileges.posts.get, [payload.id], payload.uid) + }, function (err, subdata) { + if (err) { + return callback(err); + } + + var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1; + if (!subdata.privileges[0].isAdminOrMod && parseInt(data.reporter.reputation, 10) < minimumReputation) { + return callback(new Error('[[error:not-enough-reputation-to-flag]]')); + } + + callback(); + }); + break; + } + }); +}; + Flags.getTarget = function (type, id, uid, callback) { switch (type) { case 'post': @@ -204,17 +244,22 @@ Flags.getNotes = function (flagId, callback) { }; Flags.create = function (type, id, uid, reason, callback) { + var targetUid; + async.waterfall([ function (next) { // Sanity checks async.parallel([ async.apply(Flags.exists, type, id, uid), - async.apply(Flags.targetExists, type, id) + async.apply(Flags.targetExists, type, id), + async.apply(Flags.getTargetUid, type, id) ], function (err, checks) { if (err) { return next(err); } + targetUid = checks[2] || null; + if (checks[0]) { return next(new Error('[[error:already-flagged]]')); } else if (!checks[1]) { @@ -226,25 +271,31 @@ Flags.create = function (type, id, uid, reason, callback) { }, async.apply(db.incrObjectField, 'global', 'nextFlagId'), function (flagId, next) { - async.parallel([ - async.apply(db.setObject.bind(db), 'flag:' + flagId, Object.assign({}, Flags._defaults, { + var tasks = [ + async.apply(db.setObject.bind(db), 'flag:' + flagId, { flagId: flagId, type: type, targetId: id, description: reason, uid: uid, datetime: Date.now() - })), + }), async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId), // by time, the default async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, Date.now(), flagId), // by reporter async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, Date.now(), flagId), // by flag type async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId) // save hash for existence checking - ], function (err, data) { + ]; + + if (targetUid) { + tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, Date.now(), flagId)); // by target uid + } + + async.parallel(tasks, function (err, data) { if (err) { return next(err); } - Flags.appendHistory(flagId, uid, ['created']); + Flags.update(flagId, uid, { "state": "open" }); next(null, flagId); }); }, @@ -318,7 +369,7 @@ Flags.exists = function (type, id, uid, callback) { Flags.targetExists = function (type, id, callback) { switch (type) { - case 'topic': + case 'topic': // just an example... topics.exists(id, callback); break; @@ -328,6 +379,14 @@ Flags.targetExists = function (type, id, callback) { } }; +Flags.getTargetUid = function (type, id, callback) { + switch (type) { + case 'post': + posts.getPostField(id, 'uid', callback); + break; + } +}; + Flags.update = function (flagId, uid, changeset, callback) { // Retrieve existing flag data to compare for history-saving purposes var fields = ['state', 'assignee']; @@ -369,7 +428,7 @@ Flags.update = function (flagId, uid, changeset, callback) { // 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, history)) + tasks.push(async.apply(Flags.appendHistory, flagId, uid, history)); async.parallel(tasks, function (err, data) { return next(err); @@ -462,4 +521,56 @@ Flags.appendNote = function (flagId, uid, note, callback) { ], callback); }; +Flags.notify = function (flagObj, uid, callback) { + // Notify administrators, mods, and other associated people + 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: results.post.content, + 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:post.flag', {post: results.post, reason: flagObj.description, flaggingUser: flagObj.reporter}); + notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), callback); + }); + }); + break; + } +}; + module.exports = Flags; \ No newline at end of file diff --git a/src/socket.io/flags.js b/src/socket.io/flags.js index e6fb0be116..ce148faa87 100644 --- a/src/socket.io/flags.js +++ b/src/socket.io/flags.js @@ -21,89 +21,22 @@ SocketFlags.create = function (socket, data, callback) { return callback(new Error('[[error:not-logged-in]]')); } - if (!data || !data.pid || !data.reason) { + if (!data || !data.type || !data.id || !data.reason) { return callback(new Error('[[error:invalid-data]]')); } - var flaggingUser = {}; - var post; - async.waterfall([ + async.apply(flags.validate, { + uid: socket.uid, + type: data.type, + id: data.id + }), 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; - - flags.create('post', post.pid, socket.uid, data.reason, next); + // If we got here, then no errors occurred + flags.create(data.type, data.id, socket.uid, data.reason, next); }, function (flagObj, 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); - }); + flags.notify(flagObj, socket.uid, next); } ], callback); };