Commit 00e0488a authored by Ruslan Kabalin's avatar Ruslan Kabalin Committed by Robert Lyon

Add forum objectionable content reporing functionality (bug #1024872).

The feature allows to report objectionable content in the forum posts and
topics. When post is reported, the notification is sent to site admins,
group admins and forum moderators. Any of above can take action and either
mark the post as not objectionable or delete it. In both cases the
notification about action will be sent to users who were originally
notified about objectionable content, so that they will be aware on other
person action and outcomes. Site admin normally can't access the content of
the forum he/she is not a member of, however in the case of objectionable
content, site admin will be able to have temporary rights similar to group
admins, making possible to delete or edit any post in the given forum. Once
the issues is resolved (forum no longer contains objectionable content),
admin will not longer be able to access forum in the group he/she is not a
member.

Change-Id: I12e459e2f754fcc7f5eeb0bad2f646927ad03cf8
Signed-off-by: default avatarRuslan Kabalin <r.kabalin@lancaster.ac.uk>
Signed-off-by: Robert Lyon's avatarRobert Lyon <robertl@catalyst.net.nz>
parent b19a318b
......@@ -39,6 +39,8 @@
<FIELD NAME="body" TYPE="text" NOTNULL="true" />
<FIELD NAME="ctime" TYPE="datetime" NOTNULL="true" />
<FIELD NAME="deleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" />
<FIELD NAME="reported" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" />
<FIELD NAME="reportedreason" TYPE="text" NOTNULL="false" />
<FIELD NAME="sent" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" />
<FIELD NAME="path" TYPE="char" LENGTH="2048" NOTNULL="false" />
</FIELDS>
......
......@@ -127,5 +127,21 @@ function xmldb_interaction_forum_upgrade($oldversion=0) {
}
}
if ($oldversion < 2014050800) {
// Add new columns 'reported' and 'reportedreason' to table
// interaction_forum_post used for objectionable content reporting.
$table = new XMLDBTable('interaction_forum_post');
$field = new XMLDBField('reported');
$field->setAttributes(XMLDB_TYPE_INTEGER, 1, null, XMLDB_NOTNULL, null, null, null, 0);
add_field($table, $field);
$field = new XMLDBField('reportedreason');
$field->setAttributes(XMLDB_TYPE_TEXT);
add_field($table, $field);
// Subscribe admins to new activity.
$adminusers = get_column('usr', 'id', 'admin', 1, 'deleted', 0);
activity_add_admin_defaults($adminusers);
}
return true;
}
......@@ -92,7 +92,20 @@ $form = pieform(array(
));
function deletepost_submit(Pieform $form, $values) {
global $SESSION;
global $SESSION, $USER;
$post = get_record('interaction_forum_post', 'id', $values['post']);
if ($post->reported) {
// Trigger activity.
$data = new StdClass;
$data->postid = $values['post'];
$data->message = '';
$data->reporter = $USER->get('id');
$data->ctime = time();
$data->event = DELETE_OBJECTIONABLE_POST;
activity_occurred('reportpost', $data, 'interaction', 'forum');
}
update_record(
'interaction_forum_post',
array('deleted' => 1),
......
......@@ -83,8 +83,19 @@ $form = pieform(array(
));
function deletetopic_submit(Pieform $form, $values) {
global $SESSION;
$topicid = param_integer('id');
global $SESSION, $USER, $topicid;
$post = get_record_sql('SELECT * FROM {interaction_forum_post} WHERE topic = ? AND parent IS NULL', $topicid);
if ($post->reported) {
// Trigger activity.
$data = new StdClass;
$data->postid = $post->id;
$data->message = '';
$data->reporter = $USER->get('id');
$data->ctime = time();
$data->event = DELETE_OBJECTIONABLE_TOPIC;
activity_occurred('reportpost', $data, 'interaction', 'forum');
}
// mark topic as deleted
update_record(
'interaction_forum_topic',
......
......@@ -29,6 +29,7 @@ $string['cantedittopic'] = 'You are not allowed to edit this topic';
$string['cantfindforum'] = 'Could not find forum with id %s';
$string['cantfindpost'] = 'Could not find post with id %s';
$string['cantfindtopic'] = 'Could not find topic with id %s';
$string['cantmakenonobjectionable'] = 'You are not allowed to mark this post as not objectionable.';
$string['cantviewforums'] = 'You are not allowed to view forums in this group';
$string['cantviewtopic'] = 'You are not allowed to view topics in this forum';
$string['chooseanaction'] = 'Choose an action';
......@@ -36,6 +37,7 @@ $string['clicksetsubject'] = 'Click to set a subject';
$string['Closed'] = 'Closed';
$string['Close'] = 'Close';
$string['closeddescription'] = 'Closed topics can only be replied to by moderators and the group administrators';
$string['complaint'] = 'Complaint';
$string['Count'] = 'Count';
$string['createtopicusersdescription'] = 'If set to "All group members", anyone can create new topics and reply to existing topics. If set to "Moderators and group administrators", only moderators and group administrators can start new topics, but once topics exist, all users can post replies to them.';
$string['currentmoderators'] = 'Current moderators';
......@@ -98,6 +100,17 @@ $string['newtopic'] = 'New topic';
$string['noforumpostsyet'] = 'There are no posts in this group yet';
$string['noforums'] = 'There are no forums in this group';
$string['notopics'] = 'There are no topics in this forum';
$string['notifyadministrator'] = 'Notify administrator';
$string['objectionablepostdeletedsubject'] = 'Objectionable post in forum topic "%s" was deleted by %s.';
$string['objectionablepostdeletedbody'] = '%s has looked at post by %s previously reported as objectionable and deleted it.
The objectionable post content was:
%s';
$string['objectionabletopicdeletedsubject'] = 'Objectionable forum topic "%s" was deleted by %s.';
$string['objectionabletopicdeletedbody'] = '%s has looked at topic by %s previously reported as objectionable and deleted it.
The objectionable topic content was:
%s';
$string['Open'] = 'Open';
$string['Order'] = 'Order';
$string['orderdescription'] = 'Choose where you want the forum to be ordered compared to the other forums';
......@@ -109,6 +122,12 @@ $string['postdelay'] = 'Post delay';
$string['postdelaydescription'] = 'The minimum time (in minutes) that must pass before a new post can be mailed out to forum subscribers. The author of a post may make edits during this time.';
$string['postedin'] = '%s posted in %s';
$string['Poster'] = 'Poster';
$string['postobjectionable'] = 'This post has been reported by you as containing objectionable content.';
$string['postnotobjectionable'] = 'This post has been reported as containing objectionable content. If this is not the case, you can click the button to remove this notice and notify the other administrators.';
$string['postnotobjectionablebody'] = '%s has looked at post by %s and marked it as no longer containing objectionable material.';
$string['postnotobjectionablesubject'] = 'Post in forum topic "%s" was marked as not objectionable by %s.';
$string['postnotobjectionablesuccess'] = 'Post was marked as not objectionable.';
$string['postnotobjectionablesubmit'] = 'Not objectionable';
$string['postreply'] = 'Post reply';
$string['Posts'] = 'Posts';
$string['allposts'] = 'All posts';
......@@ -120,6 +139,11 @@ $string['Reply'] = 'Reply';
$string['replyforumpostnotificationsubjectline'] = 'Re: %s';
$string['Re:'] = 'Re: ';
$string['replyto'] = 'Reply to: ';
$string['reporteddetails'] = 'Reported details';
$string['reportedpostdetails'] = '<b>Reported by %s on %s:</b><p>%s</p>';
$string['reportobjectionablematerial'] = 'Report';
$string['reportpost'] = 'Report post';
$string['reportpostsuccess'] = 'Post reported successfully';
$string['sendnow'] = 'Send message now';
$string['sendnowdescription'] = 'Send message immediately instead of waiting at least %s minutes for it to be sent.';
$string['Sticky'] = 'Sticky';
......@@ -144,6 +168,7 @@ $string['topicunstickysuccess'] = 'Topic unset as sticky successfully';
$string['topicunsubscribesuccess'] = 'Topics unsubscribed successfully';
$string['topicupdatefailed'] = 'Topics update failed';
$string['typenewpost'] = 'New forum post';
$string['typereportpost'] = 'Objectionable content in forum';
$string['Unsticky'] = 'Unsticky';
$string['Unsubscribe'] = 'Unsubscribe';
$string['unsubscribefromforum'] = 'Unsubscribe from forum';
......@@ -173,3 +198,38 @@ $string['closetopicsdescription'] = 'If checked, all new topics in this forum wi
$string['activetopicsdescription'] = 'Recently updated topics in your groups.';
$string['timeleftnotice'] = 'You have %s minutes left to finish editing.';
$string['objectionablecontentpost'] = 'Objectionable content on forum topic "%s" reported by %s';
$string['objectionablecontentposthtml'] = '<div style="padding: 0.5em 0; border-bottom: 1px solid #999;">Objectionable content on forum topic "%s" reported by %s
<br>%s</div>
<div style="margin: 1em 0;">%s</div>
<div style="padding: 0.5em 0; border-bottom: 1px solid #999;">The objectionable post content is:
<br>%s</div>
<div style="margin: 1em 0;">%s</div>
<div style="font-size: smaller; border-top: 1px solid #999;">
<p>Complaint relates to: <a href="%s">%s</a></p>
<p>Reported by: <a href="%s">%s</a></p>
</div>';
$string['objectionablecontentposttext'] = 'Objectionable content on forum topic "%s" reported by %s
%s
------------------------------------------------------------------------
%s
------------------------------------------------------------------------
The objectionable post content is:
%s
------------------------------------------------------------------------
%s
-----------------------------------------------------------------------
To see the post, follow this link:
%s
To see the reporter\'s profile, follow this link:
%s';
......@@ -11,6 +11,12 @@
require_once('activity.php');
// Contstants for objectionable content reporting events.
define('REPORT_OBJECTIONABLE', 1);
define('MAKE_NOT_OBJECTIONABLE', 2);
define('DELETE_OBJECTIONABLE_POST', 3);
define('DELETE_OBJECTIONABLE_TOPIC', 4);
class PluginInteractionForum extends PluginInteraction {
public static function instance_config_form($group, $instance=null) {
......@@ -23,12 +29,7 @@ class PluginInteractionForum extends PluginInteraction {
$indentmode = isset($instanceconfig['indentmode']) ? $instanceconfig['indentmode']->value : null;
$maxindent = isset($instanceconfig['maxindent']) ? $instanceconfig['maxindent']->value : null;
$moderators = get_column_sql(
'SELECT fm.user FROM {interaction_forum_moderator} fm
JOIN {usr} u ON (fm.user = u.id AND u.deleted = 0)
WHERE fm.forum = ?',
array($instance->get('id'))
);
$moderators = get_forum_moderators($instance->get('id'));
}
if ($instance === null) {
......@@ -322,7 +323,14 @@ EOF;
'delay' => 1,
'allownonemethod' => 1,
'defaultmethod' => 'email',
)
),
(object)array(
'name' => 'reportpost',
'admin' => 1,
'delay' => 0,
'allownonemethod' => 1,
'defaultmethod' => 'email',
),
);
}
......@@ -377,9 +385,22 @@ EOF;
}
public static function group_menu_items($group) {
global $USER;
$role = group_user_access($group->id);
$hasobjectionable = false;
if (!$role && $USER->get('admin')) {
// No role, but site admin - see if there is objectionable content,
// so that menu item should be displayed.
foreach (self::get_instance_ids($group->id) as $instanceid) {
$instance = new InteractionForumInstance($instanceid);
if ($instance->has_objectionable()) {
$hasobjectionable = true;
break;
}
}
}
$menu = array();
if ($group->public || $role) {
if ($group->public || $role || ($hasobjectionable && $USER->get('admin'))) {
$menu['forums'] = array(// @todo: make forums an artefact plugin
'path' => 'groups/forums',
'url' => 'interaction/forum/index.php?group=' . $group->id,
......@@ -778,6 +799,19 @@ EOF;
}
return null;
}
/**
* Return IDs of plugin instances
*
* @param int $groupid optional group ID number
* @return array list of the instance IDs
*/
public static function get_instance_ids($groupid = null) {
if (isset($groupid) && $groupid > 0) {
return get_column('interaction_instance', 'id', 'plugin', 'forum', 'group', $groupid, 'deleted', 0);
}
return get_column('interaction_instance', 'id', 'plugin', 'forum', 'deleted', 0);
}
}
class InteractionForumInstance extends InteractionInstance {
......@@ -795,6 +829,19 @@ class InteractionForumInstance extends InteractionInstance {
);
}
/**
* Check if forum instance contains reported content.
*
* @returns bool $reported whether forum contains reported content.
*/
public function has_objectionable() {
$reported = count_records_sql(
'SELECT count(fp.id) FROM {interaction_forum_topic} ft
JOIN {interaction_forum_post} fp ON (ft.id = fp.topic)
WHERE fp.deleted = 0 AND fp.reported = 1 AND ft.forum = ?', array($this->id)
);
return (bool) $reported;
}
}
class ActivityTypeInteractionForumNewPost extends ActivityTypePlugin {
......@@ -951,6 +998,161 @@ class ActivityTypeInteractionForumNewPost extends ActivityTypePlugin {
}
}
class ActivityTypeInteractionForumReportPost extends ActivityTypePlugin {
protected $postid;
protected $message;
protected $reporter;
protected $ctime;
protected $event;
protected $temp;
public function __construct($data, $cron = false) {
parent::__construct($data, $cron);
$post = get_record_sql('
SELECT
p.subject, p.body, p.poster, p.parent, ' . db_format_tsfield('p.ctime', 'ctime') . ',
t.id AS topicid, fp.subject AS topicsubject, f.title AS forumtitle, g.id AS groupid, g.name AS groupname, f.id AS forumid
FROM {interaction_forum_post} p
INNER JOIN {interaction_forum_topic} t ON (t.id = p.topic AND t.deleted = 0)
INNER JOIN {interaction_forum_post} fp ON (fp.parent IS NULL AND fp.topic = t.id)
INNER JOIN {interaction_instance} f ON (t.forum = f.id AND f.deleted = 0)
INNER JOIN {group} g ON (f.group = g.id AND g.deleted = 0)
WHERE p.id = ? AND p.deleted = 0',
array($this->postid)
);
// The post may have been deleted during the activity delay
if (!$post) {
$this->users = array();
return;
}
// Set notification to site admins.
$siteadmins = activity_get_users($this->get_id(), null, null, true);
// Get forum moderators and admins.
$forumadminsandmoderators = activity_get_users(
$this->get_id(),
array_merge(get_forum_moderators($post->forumid),
group_get_admin_ids($post->groupid)));
// Populate users to notify list and get rid of duplicates.
foreach (array_merge($siteadmins, $forumadminsandmoderators) as $user) {
if (!isset($this->users[$user->id])) {
$this->users[$user->id] = $user;
}
}
// Record who reported it.
$this->fromuser = $this->reporter;
$post->posttime = strftime(get_string('strftimedaydatetime'), $post->ctime);
$post->textbody = trim(html2text($post->body));
$post->htmlbody = clean_html($post->body);
$this->url = 'interaction/forum/topic.php?id=' . $post->topicid . '&post=' . $this->postid;
$this->add_urltext(array(
'key' => 'Topic',
'section' => 'interaction.forum'
));
if ($this->event === REPORT_OBJECTIONABLE) {
$this->overridemessagecontents = true;
$this->strings->subject = (object) array(
'key' => 'objectionablecontentpost',
'section' => 'interaction.forum',
'args' => array($post->topicsubject, display_default_name($this->reporter)),
);
}
else if ($this->event === MAKE_NOT_OBJECTIONABLE) {
$this->strings = (object) array(
'subject' => (object) array(
'key' => 'postnotobjectionablesubject',
'section' => 'interaction.forum',
'args' => array($post->topicsubject, display_default_name($this->reporter)),
),
'message' => (object) array(
'key' => 'postnotobjectionablebody',
'section' => 'interaction.forum',
'args' => array(display_default_name($this->reporter), display_default_name($post->poster)),
),
);
}
else if ($this->event === DELETE_OBJECTIONABLE_POST) {
$this->url = '';
$this->strings = (object) array(
'subject' => (object) array(
'key' => 'objectionablepostdeletedsubject',
'section' => 'interaction.forum',
'args' => array($post->topicsubject, display_default_name($this->reporter)),
),
'message' => (object) array(
'key' => 'objectionablepostdeletedbody',
'section' => 'interaction.forum',
'args' => array(display_default_name($this->reporter), display_default_name($post->poster), $post->textbody),
),
);
}
else if ($this->event === DELETE_OBJECTIONABLE_TOPIC) {
$this->url = '';
$this->strings = (object) array(
'subject' => (object) array(
'key' => 'objectionabletopicdeletedsubject',
'section' => 'interaction.forum',
'args' => array($post->topicsubject, display_default_name($this->reporter)),
),
'message' => (object) array(
'key' => 'objectionabletopicdeletedbody',
'section' => 'interaction.forum',
'args' => array(display_default_name($this->reporter), display_default_name($post->poster), $post->textbody),
),
);
}
else {
throw new SystemException();
}
$this->temp = (object) array('post' => $post);
}
public function get_emailmessage($user) {
$post = $this->temp->post;
$reporterurl = profile_url($this->reporter);
$ctime = strftime(get_string_from_language($user->lang, 'strftimedaydatetime'), $this->ctime);
return get_string_from_language(
$user->lang, 'objectionablecontentviewtext', 'interaction.forum',
$post->topicsubject, display_default_name($this->reporter), $ctime,
$this->message, $post->posttime, $post->textbody, get_config('wwwroot') . $this->url, $reporterurl
);
}
public function get_htmlmessage($user) {
$post = $this->temp->post;
$reportername = hsc(display_default_name($this->reporter));
$reporterurl = profile_url($this->reporter);
$ctime = strftime(get_string_from_language($user->lang, 'strftimedaydatetime'), $this->ctime);
return get_string_from_language(
$user->lang, 'objectionablecontentposthtml', 'interaction.forum',
hsc($post->topicsubject), $reportername, $ctime,
$this->message, $post->posttime, $post->htmlbody, get_config('wwwroot') . $this->url, hsc($post->topicsubject),
$reporterurl, $reportername
);
}
public function get_plugintype(){
return 'interaction';
}
public function get_pluginname(){
return 'forum';
}
public function get_required_parameters() {
return array('postid', 'message', 'reporter', 'ctime', 'event');
}
}
// constants for forum membership types
define('INTERACTION_FORUM_ADMIN', 1);
define('INTERACTION_FORUM_MOD', 2);
......@@ -965,12 +1167,11 @@ define('INTERACTION_FORUM_MEMBER', 4);
* @returns constant access level or false
*/
function user_can_access_forum($forumid, $userid=null) {
if (empty($userid)) {
global $USER;
$userid = $USER->get('id');
}
else if (!is_int($userid)) {
throw new InvalidArgumentException("non integer user id given to user_can_access_forum: $userid");
global $USER;
$forumuser = $USER;
if (!empty($userid)) {
$forumuser = new User;
$forumuser->find_by_id($userid);
}
if (!is_int($forumid)) {
throw new InvalidArgumentException("non integer forum id given to user_can_access_forum: $forumid");
......@@ -978,8 +1179,14 @@ function user_can_access_forum($forumid, $userid=null) {
$membership = 0;
// Allow site admins accessing the forum directly if it has objectionable content.
$instance = new InteractionForumInstance($forumid);
if ($instance->has_objectionable() && $forumuser->get('admin')) {
return $membership | INTERACTION_FORUM_ADMIN | INTERACTION_FORUM_MOD;
}
$groupid = get_field('interaction_instance', '"group"', 'id', $forumid);
$groupmembership = group_user_access((int)$groupid, (int)$userid);
$groupmembership = group_user_access((int)$groupid, $forumuser->get('id'));
if (!$groupmembership) {
return $membership;
......@@ -988,12 +1195,28 @@ function user_can_access_forum($forumid, $userid=null) {
if ($groupmembership == 'admin') {
$membership = $membership | INTERACTION_FORUM_ADMIN | INTERACTION_FORUM_MOD;
}
if (record_exists('interaction_forum_moderator', 'forum', $forumid, 'user', $userid)) {
if (record_exists('interaction_forum_moderator', 'forum', $forumid, 'user', $forumuser->get('id'))) {
$membership = $membership | INTERACTION_FORUM_MOD;
}
return $membership;
}
/**
* Get list of moderators for a given forum.
*
* @param int $forumid id of forum
*
* @returns array $moderators list of forum moderators.
*/
function get_forum_moderators($forumid) {
$moderators = get_column_sql(
'SELECT fm.user FROM {interaction_forum_moderator} fm
JOIN {usr} u ON (fm.user = u.id AND u.deleted = 0)
WHERE fm.forum = ?', array($forumid)
);
return (array) $moderators;
}
/**
* Is a user allowed to edit a post
*
......@@ -1005,7 +1228,7 @@ function user_can_access_forum($forumid, $userid=null) {
* @returns boolean
*/
function user_can_edit_post($poster, $posttime, $userid=null, $verifydelay=true) {
if (empty($userid)) {
if (empty($userid)) {
global $USER;
$userid = $USER->get('id');
}
......
<?php
/**
*
* @package mahara
* @subpackage interaction-forum
* @author Lancaster University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
*
*/
define('INTERNAL', 1);
define('MENUITEM', 'groups/forums');
define('SECTION_PLUGINTYPE', 'interaction');
define('SECTION_PLUGINNAME', 'forum');
define('SECTION_PAGE', 'reportpost');
require(dirname(dirname(dirname(__FILE__))) . '/init.php');
safe_require('interaction' ,'forum');
require_once('group.php');
require_once(get_config('docroot') . 'interaction/lib.php');
require_once('pieforms/pieform.php');
$postid = param_integer('id');
$post = get_record_sql(
'SELECT p.subject, p.body, p.topic, p.parent, p.poster, ' . db_format_tsfield('p.ctime', 'ctime') . ', p.reported, p.reportedreason, m.user AS moderator, t.forum, p2.subject AS topicsubject, f.group, f.title AS forumtitle, g.name AS groupname
FROM {interaction_forum_post} p
INNER JOIN {interaction_forum_topic} t ON (p.topic = t.id AND t.deleted != 1)
INNER JOIN {interaction_forum_post} p2 ON (p2.topic = t.id AND p2.parent IS NULL)
INNER JOIN {interaction_instance} f ON (t.forum = f.id AND f.deleted != 1)
INNER JOIN {group} g ON (g.id = f.group AND g.deleted = ?)
LEFT JOIN (
SELECT m.forum, m.user
FROM {interaction_forum_moderator} m
INNER JOIN {usr} u ON (m.user = u.id AND u.deleted = 0)
) m ON (m.forum = f.id AND m.user = p.poster)
WHERE p.id = ?
AND p.deleted != 1',
array(0, $postid)
);
if (!$post) {
throw new NotFoundException(get_string('cantfindpost', 'interaction.forum', $postid));
}
$membership = user_can_access_forum((int)$post->forum);
$moderator = (bool)($membership & INTERACTION_FORUM_MOD);
if (!$membership && !get_field('group', 'public', 'id', $post->group)) {
throw new GroupAccessDeniedException(get_string('cantviewtopic', 'interaction.forum'));
}
define('GROUP', $post->group);
define('TITLE', $post->topicsubject . ' - ' . get_string('reportpost', 'interaction.forum'));
$post->ctime = relative_date(get_string('strftimerecentfullrelative', 'interaction.forum'), get_string('strftimerecentfull'), $post->ctime);
$form = pieform(array(
'name' => 'reportpost',
'autofocus' => false,
'elements' => array(
'message' => array(
'type' => 'textarea',
'title' => get_string('complaint', 'interaction.forum'),
'rows' => 5,
'cols' => 80,
),
'submit' => array(
'type' => 'submitcancel',
'value' => array(get_string('notifyadministrator', 'interaction.forum'), get_string('cancel')),
'goto' => get_config('wwwroot') . 'interaction/forum/topic.php?id=' . $post->topic . '&post=' . $postid
),
'topic' => array(
'type' => 'hidden',