diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json
index 6b4e96f4fb..a9272897ca 100644
--- a/public/language/en-GB/flags.json
+++ b/public/language/en-GB/flags.json
@@ -29,6 +29,9 @@
"start-new-chat": "Start New Chat",
"go-to-target": "View Flag Target",
+ "user-view": "View Profile",
+ "user-edit": "Edit Profile",
+
"notes": "Flag Notes",
"add-note": "Add Note",
"no-notes": "No shared notes.",
@@ -43,5 +46,13 @@
"state-resolved": "Resolved",
"state-rejected": "Rejected",
"no-assignee": "Not Assigned",
- "note-added": "Note Added"
+ "note-added": "Note Added",
+
+ "modal-title": "Report Inappropriate Content",
+ "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.",
+ "modal-reason-spam": "Spam",
+ "modal-reason-offensive": "Offensive",
+ "modal-reason-custom": "Reason for reporting this content...",
+ "modal-submit": "Submit Report",
+ "modal-submit-success": "Content has been flagged for moderation."
}
\ No newline at end of file
diff --git a/public/language/en-GB/notifications.json b/public/language/en-GB/notifications.json
index 5a2ed58908..0838ca17eb 100644
--- a/public/language/en-GB/notifications.json
+++ b/public/language/en-GB/notifications.json
@@ -21,6 +21,9 @@
"user_flagged_post_in": "%1 flagged a post in %2",
"user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
"user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
+ "user_flagged_user": "%1 flagged a user profile (%2)",
+ "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)",
+ "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)",
"user_posted_to" : "%1 has posted a reply to: %2",
"user_posted_to_dual" : "%1 and %2 have posted replies to: %3",
"user_posted_to_multiple" : "%1 and %2 others have posted replies to: %3",
diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json
index 4ae208076e..5571292dda 100644
--- a/public/language/en-GB/topic.json
+++ b/public/language/en-GB/topic.json
@@ -28,7 +28,6 @@
"link": "Link",
"share": "Share",
"tools": "Tools",
- "flag": "Flag",
"locked": "Locked",
"pinned": "Pinned",
"moved": "Moved",
@@ -36,7 +35,6 @@
"bookmark_instructions" : "Click here to return to the last read post in this thread.",
"flag_title": "Flag this post for moderation",
- "flag_success": "This post has been flagged for moderation.",
"deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.",
@@ -138,10 +136,5 @@
"stale.create": "Create a new topic",
"stale.reply_anyway": "Reply to this topic anyway",
- "link_back": "Re: [%1](%2)\n\n",
-
- "spam": "Spam",
- "offensive": "Offensive",
- "custom-flag-reason": "Enter a flagging reason"
-
+ "link_back": "Re: [%1](%2)\n\n"
}
diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json
index f0cb35f615..0725f208a3 100644
--- a/public/language/en-GB/user.json
+++ b/public/language/en-GB/user.json
@@ -35,6 +35,7 @@
"chat": "Chat",
"chat_with": "Continue chat with %1",
"new_chat_with": "Start new chat with %1",
+ "flag-profile": "Flag Profile",
"follow": "Follow",
"unfollow": "Unfollow",
"more": "More",
diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js
index d225e2cae1..b53cbb2be4 100644
--- a/public/src/client/account/header.js
+++ b/public/src/client/account/header.js
@@ -49,6 +49,7 @@ define('forum/account/header', [
components.get('account/ban').on('click', banAccount);
components.get('account/unban').on('click', unbanAccount);
components.get('account/delete').on('click', deleteAccount);
+ components.get('account/flag').on('click', flagAccount);
};
function hidePrivateLinks() {
@@ -167,6 +168,15 @@ define('forum/account/header', [
});
}
+ function flagAccount() {
+ require(['flags'], function (flags) {
+ flags.showFlagModal({
+ type: 'user',
+ id: ajaxify.data.uid
+ });
+ });
+ }
+
function removeCover() {
socket.emit('user.removeCover', {
uid: ajaxify.data.uid
diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js
index cbdaf94f24..bf8b4bc1a8 100644
--- a/public/src/client/flags/list.js
+++ b/public/src/client/flags/list.js
@@ -23,6 +23,8 @@ define('forum/flags/list', ['components'], function (components) {
var qs = payload.map(function (filter) {
if (filter.value) {
return filter.name + '=' + filter.value;
+ } else {
+ return;
}
}).filter(Boolean).join('&');
diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js
index faa0567878..5618e692db 100644
--- a/public/src/client/topic/postTools.js
+++ b/public/src/client/topic/postTools.js
@@ -167,10 +167,11 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
postContainer.on('click', '[component="post/flag"]', function () {
var pid = getData($(this), 'data-pid');
- var username = getData($(this), 'data-username');
- var userslug = getData($(this), 'data-userslug');
- require(['forum/topic/flag'], function (flag) {
- flag.showFlagModal(pid, username, userslug);
+ require(['flags'], function (flags) {
+ flags.showFlagModal({
+ type: 'post',
+ id: pid
+ });
});
});
diff --git a/public/src/client/topic/flag.js b/public/src/modules/flags.js
similarity index 62%
rename from public/src/client/topic/flag.js
rename to public/src/modules/flags.js
index 6b3440da54..cc9fd5103a 100644
--- a/public/src/client/topic/flag.js
+++ b/public/src/modules/flags.js
@@ -2,18 +2,13 @@
/* globals define, app, socket, templates */
-define('forum/topic/flag', [], function () {
-
+define('flags', [], function () {
var Flag = {},
flagModal,
flagCommit;
- Flag.showFlagModal = function (pid, username, userslug) {
- parseModal({
- pid: pid,
- username: username,
- userslug: userslug
- }, function (html) {
+ Flag.showFlagModal = function (data) {
+ parseModal(data, function (html) {
flagModal = $(html);
flagModal.on('hidden.bs.modal', function () {
@@ -23,11 +18,11 @@ define('forum/topic/flag', [], function () {
flagCommit = flagModal.find('#flag-post-commit');
flagModal.on('click', '.flag-reason', function () {
- flagPost(pid, $(this).text());
+ createFlag(data.type, data.id, $(this).text());
});
flagCommit.on('click', function () {
- flagPost(pid, flagModal.find('#flag-reason-custom').val());
+ createFlag(data.type, data.id, flagModal.find('#flag-reason-custom').val());
});
flagModal.modal('show');
@@ -37,24 +32,24 @@ define('forum/topic/flag', [], function () {
};
function parseModal(tplData, callback) {
- templates.parse('partials/modals/flag_post_modal', tplData, function (html) {
+ templates.parse('partials/modals/flag_modal', tplData, function (html) {
require(['translator'], function (translator) {
translator.translate(html, callback);
});
});
}
- function flagPost(pid, reason) {
- if (!pid || !reason) {
+ function createFlag(type, id, reason) {
+ if (!type || !id || !reason) {
return;
}
- socket.emit('flags.create', {type: 'post', id: pid, reason: reason}, function (err) {
+ socket.emit('flags.create', {type: type, id: id, reason: reason}, function (err) {
if (err) {
return app.alertError(err.message);
}
flagModal.modal('hide');
- app.alertSuccess('[[topic:flag_success]]');
+ app.alertSuccess('[[flags:modal-submit-success]]');
});
}
diff --git a/src/controllers/mods.js b/src/controllers/mods.js
index 788294a01e..656605f277 100644
--- a/src/controllers/mods.js
+++ b/src/controllers/mods.js
@@ -63,7 +63,11 @@ modsController.flags.detail = function (req, res, next) {
}
res.render('flags/detail', Object.assign(results.flagData, {
- assignees: results.assignees
+ assignees: results.assignees,
+ type_bool: ['post', 'user'].reduce(function (memo, cur) {
+ memo[cur] = results.flagData.type === cur;
+ return memo;
+ }, {})
}));
});
};
diff --git a/src/flags.js b/src/flags.js
index 45aae2502e..9f121f81bf 100644
--- a/src/flags.js
+++ b/src/flags.js
@@ -167,20 +167,44 @@ Flags.validate = function (payload, callback) {
switch (payload.type) {
case 'post':
async.parallel({
- privileges: async.apply(privileges.posts.get, [payload.id], payload.uid)
+ editable: async.apply(privileges.posts.canEdit, 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) {
+ // Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
+ if (!subdata.editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
}
callback();
});
break;
+
+ case 'user':
+ async.parallel({
+ editable: async.apply(privileges.users.canEdit, payload.uid, payload.id)
+ }, function (err, subdata) {
+ 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 (!subdata.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;
}
});
};
@@ -369,13 +393,17 @@ Flags.exists = function (type, id, uid, callback) {
Flags.targetExists = function (type, id, callback) {
switch (type) {
- case 'topic': // just an example...
- topics.exists(id, callback);
- break;
-
case 'post':
posts.exists(id, callback);
break;
+
+ case 'user':
+ user.exists(id, callback);
+ break;
+
+ default:
+ callback(new Error('[[error:invalid-data]]'));
+ break;
}
};
@@ -384,6 +412,10 @@ Flags.getTargetUid = function (type, id, callback) {
case 'post':
posts.getPostField(id, 'uid', callback);
break;
+
+ case 'user':
+ setImmediate(callback, null, id);
+ break;
}
};
@@ -553,7 +585,7 @@ Flags.notify = function (flagObj, uid, callback) {
notifications.create({
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
- bodyLong: results.post.content,
+ bodyLong: flagObj.description,
pid: flagObj.targetId,
path: '/post/' + flagObj.targetId,
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
@@ -570,6 +602,36 @@ Flags.notify = function (flagObj, uid, 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);
+ }
+
+ notifications.push(notification, results.admins.concat(results.globalMods), callback);
+ });
+ });
+ break;
+
+ default:
+ callback(new Error('[[error:invalid-data]]'));
+ break;
}
};
diff --git a/src/meta/js.js b/src/meta/js.js
index 626fa0ecd8..947550f37e 100644
--- a/src/meta/js.js
+++ b/src/meta/js.js
@@ -49,7 +49,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',
@@ -72,7 +71,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
diff --git a/src/notifications.js b/src/notifications.js
index b99700be01..0fb1e1ace3 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -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) {
diff --git a/test/flags.js b/test/flags.js
index ee735afd60..14bb1e65c6 100644
--- a/test/flags.js
+++ b/test/flags.js
@@ -32,6 +32,11 @@ describe('Flags', function () {
content: 'This is flaggable content'
}, next);
});
+ },
+ function (topicData, next) {
+ User.create({
+ username: 'testUser2', password: 'abcdef', email: 'c@d.com'
+ }, next);
}
], done);
});
@@ -212,7 +217,7 @@ describe('Flags', function () {
Flags.validate({
type: 'post',
id: 1,
- uid: 1
+ uid: 2
}, function (err) {
assert.ok(err);
assert.strictEqual('[[error:not-enough-reputation-to-flag]]', err.message);
@@ -305,6 +310,10 @@ describe('Flags', function () {
assert.ifError(err);
Flags.getHistory(1, function (err, history) {
+ if (err) {
+ throw err;
+ }
+
assert.strictEqual(entries + 1, history.length);
done();
});
@@ -315,6 +324,7 @@ describe('Flags', function () {
describe('.getHistory()', function () {
it('should retrieve a flag\'s history', function (done) {
Flags.getHistory(1, function (err, history) {
+ assert.ifError(err);
assert.strictEqual(history[0].fields[0].value, '[[flags:state-rejected]]');
done();
});