MDL-61309 mod_forum: Implement privacy deletion
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 24 Apr 2018 00:07:06 +0000 (08:07 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 9 May 2018 06:04:13 +0000 (14:04 +0800)
12 files changed:
mod/forum/classes/message/inbound/reply_handler.php
mod/forum/classes/privacy/provider.php
mod/forum/db/install.xml
mod/forum/db/upgrade.php
mod/forum/discuss.php
mod/forum/externallib.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/rsslib.php
mod/forum/tests/externallib_test.php
mod/forum/tests/privacy_provider_test.php
mod/forum/version.php

index cd369e5..039f42b 100644 (file)
@@ -162,6 +162,7 @@ class reply_handler extends \core\message\inbound\handler {
         $addpost->subject      = $subject;
         $addpost->parent       = $post->id;
         $addpost->itemid       = file_get_unused_draft_itemid();
+        $addpost->deleted      = 0;
 
         list ($message, $format) = self::remove_quoted_text($messagedata);
         $addpost->message = $message;
index f8aa3bc..f0a7964 100644 (file)
@@ -305,8 +305,6 @@ class provider implements
             static::export_digest_data($userid, $forum);
             static::export_subscription_data($userid, $forum);
             static::export_tracking_data($userid, $forum);
-
-
         }
         $forums->close();
 
@@ -373,6 +371,7 @@ class provider implements
                 'pinned' => transform::yesno((bool) $discussion->pinned),
                 'timemodified' => transform::datetime($discussion->timemodified),
                 'usermodified' => transform::datetime($discussion->usermodified),
+                'creator_was_you' => transform::yesno($discussion->userid == $userid),
             ];
 
             // Store the discussion content.
@@ -546,7 +545,7 @@ class provider implements
             'subject' => format_string($post->subject, true),
             'created' => transform::datetime($post->created),
             'modified' => transform::datetime($post->modified),
-            'author' => transform::user($post->userid),
+            'author_was_you' => transform::yesno($post->userid == $userid),
         ];
 
         $postdata->message = writer::with_context($context)
@@ -787,13 +786,10 @@ class provider implements
         $fs->delete_area_files($context->id, 'mod_forum', 'post');
 
         // Delete all ratings in the context.
-        $rm = new \rating_manager();
-        $rm->delete_ratings((object) [
-            'contextid' => $context->id,
-        ]);
+        \core_rating\privacy\provider::delete_ratings($context, 'mod_forum', 'post');
 
         // Delete all Tags.
-        \core_tag_tag::delete_instances('mod_forum', 'post', $context->id);
+        \core_tag\privacy\provider::delete_item_tags($context, 'mod_forum', 'forum_posts');
     }
 
     /**
@@ -803,6 +799,8 @@ class provider implements
      */
     public static function delete_data_for_user(approved_contextlist $contextlist) {
         global $DB;
+        $user = $contextlist->get_user();
+        $userid = $user->id;
         foreach ($contextlist as $context) {
             // Get the course module.
             $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
@@ -824,7 +822,7 @@ class provider implements
             // Delete all discussion items.
             $DB->delete_records_select(
                 'forum_queue',
-                "userid AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
+                "userid = :userid AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
                 [
                     'userid' => $userid,
                     'forum' => $forum->id,
@@ -845,6 +843,7 @@ class provider implements
                 // Do not delete discussion or forum posts.
                 // Instead update them to reflect that the content has been deleted.
                 $postsql = "userid = :userid AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)";
+                $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}";
                 $postparams = [
                     'forum' => $forum->id,
                     'userid' => $userid,
@@ -852,29 +851,23 @@ class provider implements
 
                 // Update the subject.
                 $DB->set_field_select('forum_posts', 'subject', '', $postsql, $postparams);
-                    'subject',
-                    get_string('privacy:request:delete:post:subject', 'mod_forum'),
-                    $postsql,
-                    $postparams);
 
                 // Update the subject and its format.
                 $DB->set_field_select('forum_posts', 'message', '', $postsql, $postparams);
-                    'forum_posts',
-                    'message',
-                    get_string('privacy:request:delete:post:message', 'mod_forum'),
-                    $postsql,
-                    $postparams);
                 $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $postparams);
 
-                $discussion->name = get_string('privacy:request:delete:discussion:name', 'mod_forum', null, $lang);
-                $DB->update_record('forum_discussions', $discussion);
+                // Mark the post as deleted.
+                $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $postparams);
 
-                // Note: Do _not_ delete ratings.
+                // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts.
                 // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating
                 // of any post.
+                \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post',
+                        "IN ($postidsql)", $postparams);
 
                 // Delete all Tags.
-                \core_tag_tag::delete_instances('mod_forum', 'post', $context->id);
+                \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts',
+                        "IN ($postidsql)", $postparams);
             }
 
             $uniquediscussions->close();
index cbd1906..9c0c99b 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/forum/db" VERSION="20171012" COMMENT="XMLDB file for Moodle mod/forum"
+<XMLDB PATH="mod/forum/db" VERSION="20180329" COMMENT="XMLDB file for Moodle mod/forum"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -80,6 +80,7 @@
         <FIELD NAME="attachment" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="totalscore" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="mailnow" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="deleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index f523a38..d8961fd 100644 (file)
@@ -101,5 +101,20 @@ function xmldb_forum_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2018032900) {
+
+        // Define field deleted to be added to forum_posts.
+        $table = new xmldb_table('forum_posts');
+        $field = new xmldb_field('deleted', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'mailnow');
+
+        // Conditionally launch add field deleted.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Forum savepoint reached.
+        upgrade_mod_savepoint(true, 2018032900, 'forum');
+    }
+
     return true;
 }
index 2366e45..f7207a5 100644 (file)
@@ -219,7 +219,7 @@ if (! $post = forum_get_post_full($parent)) {
     print_error("notexists", 'forum', "$CFG->wwwroot/mod/forum/view.php?f=$forum->id");
 }
 
-if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
+if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
     print_error('noviewdiscussionspermission', 'forum', "$CFG->wwwroot/mod/forum/view.php?id=$forum->id");
 }
 
index 9bdb2d4..a92d3bd 100644 (file)
@@ -249,8 +249,7 @@ class mod_forum_external extends external_api {
         $allposts = forum_get_all_discussion_posts($discussion->id, $sort, $forumtracked);
 
         foreach ($allposts as $post) {
-
-            if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
+            if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
                 $warning = array();
                 $warning['item'] = 'post';
                 $warning['itemid'] = $post->id;
@@ -275,6 +274,23 @@ class mod_forum_external extends external_api {
                 $post->children = array();
             }
 
+            if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
+                // The post is available, but has been marked as deleted.
+                // It will still be available but filled with a placeholder.
+                $post->userid = null;
+                $post->userfullname = null;
+                $post->userpictureurl = null;
+
+                $post->subject = get_string('privacy:request:delete:post:subject', 'mod_forum');
+                $post->message = get_string('privacy:request:delete:post:message', 'mod_forum');
+
+                $post->deleted = true;
+                $posts[] = $post;
+
+                continue;
+            }
+            $post->deleted = false;
+
             if (forum_is_author_hidden($post, $forum)) {
                 $post->userid = null;
                 $post->userfullname = null;
@@ -346,7 +362,8 @@ class mod_forum_external extends external_api {
                                 'canreply' => new external_value(PARAM_BOOL, 'The user can reply to posts?'),
                                 'postread' => new external_value(PARAM_BOOL, 'The post was read'),
                                 'userfullname' => new external_value(PARAM_TEXT, 'Post author full name'),
-                                'userpictureurl' => new external_value(PARAM_URL, 'Post author picture.', VALUE_OPTIONAL)
+                                'userpictureurl' => new external_value(PARAM_URL, 'Post author picture.', VALUE_OPTIONAL),
+                                'deleted' => new external_value(PARAM_BOOL, 'This post has been removed.'),
                             ), 'post'
                         )
                     ),
@@ -850,7 +867,8 @@ class mod_forum_external extends external_api {
         $post->messageformat = FORMAT_HTML;   // Force formatting for now.
         $post->messagetrust = trusttext_trusted($context);
         $post->itemid = $options['inlineattachmentsid'];
-        $post->attachments   = $options['attachmentsid'];
+        $post->attachments = $options['attachmentsid'];
+        $post->deleted = 0;
         $fakemform = $post->attachments;
         if ($postid = forum_add_new_post($post, $fakemform)) {
 
index 43f5ca4..8b90059 100644 (file)
@@ -1442,6 +1442,7 @@ function forum_print_overview($courses,&$htmlarray) {
                 .'FROM {forum_discussions} d '
                 .'JOIN {forum_posts} p ON p.discussion = d.id '
                 ."WHERE ($coursessql) "
+                .'AND p.deleted <> 1 '
                 .'AND p.userid != ? '
                 .'AND (d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?)) '
                 .'GROUP BY d.id, d.forum, d.course, d.groupid '
@@ -1469,7 +1470,7 @@ function forum_print_overview($courses,&$htmlarray) {
         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
             ' FROM {forum_posts} p '.
             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
-            ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
+            ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE p.deleted <> 1 AND (';
         $params = array($USER->id);
 
         foreach ($trackingforums as $track) {
@@ -1573,7 +1574,7 @@ function forum_print_recent_activity($course, $viewfullnames, $timestart) {
                                               JOIN {forum_discussions} d ON d.id = p.discussion
                                               JOIN {forum} f             ON f.id = d.forum
                                               JOIN {user} u              ON u.id = p.userid
-                                        WHERE p.created > ? AND f.course = ?
+                                        WHERE p.created > ? AND f.course = ? AND p.deleted <> 1
                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
          return false;
     }
@@ -3182,7 +3183,8 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
         $postisread = forum_tp_is_post_read($USER->id, $post);
     }
 
-    if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
+    if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
+        // Do _not_ check the deleted flag - we need to display a different UI.
         $output = '';
         if (!$dummyifcantsee) {
             if ($return) {
@@ -3221,6 +3223,62 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
         return;
     }
 
+    if (!empty($post->deleted)) {
+        // Note: Posts marked as deleted are still returned by the above forum_user_can_post because it is required for
+        // nesting of posts.
+        $output = '';
+        if (!$dummyifcantsee) {
+            if ($return) {
+                return $output;
+            }
+            echo $output;
+            return;
+        }
+        $output .= html_writer::tag('a', '', [
+                'id' => "p{$post->id}",
+            ]);
+        $output .= html_writer::start_tag('div', [
+                'class' => 'forumpost clearfix',
+                'role' => 'region',
+                'aria-label' => get_string('forumbodydeleted', 'forum'),
+            ]);
+
+        $output .= html_writer::start_tag('div', array('class' => 'row header'));
+        $output .= html_writer::tag('div', '', array('class' => 'left picture'));
+
+        $classes = ['topic'];
+        if (!empty($post->parent)) {
+            $classes[] = 'starter';
+        }
+        $output .= html_writer::start_tag('div', ['class' => implode(' ', $classes)]);
+
+        // Subject.
+        $output .= html_writer::tag('div', get_string('forumsubjectdeleted', 'forum'), [
+                'class' => 'subject',
+                'role' => 'header',
+            ]);
+
+        // Author.
+        $output .= html_writer::tag('div', '', [
+                'class' => 'author',
+                'role' => 'header',
+            ]);
+
+        $output .= html_writer::end_tag('div');
+        $output .= html_writer::end_tag('div'); // End row.
+        $output .= html_writer::start_tag('div', ['class' => 'row']);
+        $output .= html_writer::tag('div', '&nbsp;', ['class' => 'left side']); // Groups.
+        $output .= html_writer::tag('div', get_string('forumbodydeleted', 'forum'), ['class' => 'content']); // Content.
+        $output .= html_writer::end_tag('div'); // End row.
+        $output .= html_writer::end_tag('div'); // End forumpost.
+
+        if ($return) {
+            return $output;
+        }
+        echo $output;
+        return;
+    }
+
     if (empty($str)) {
         $str = new stdClass;
         $str->edit         = get_string('edit', 'forum');
@@ -3487,7 +3545,7 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
     }
 
     // Output ratings
-    if (!empty($post->rating)) {
+    if (!empty($post->deleted) && !empty($post->rating)) {
         $output .= html_writer::tag('div', $OUTPUT->render($post->rating), array('class'=>'forum-post-rating'));
     }
 
@@ -3714,6 +3772,7 @@ function mod_forum_rating_can_see_item_ratings($params) {
     if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
         return false;
     }
+
     return true;
 }
 
@@ -4735,6 +4794,7 @@ function forum_delete_post($post, $children, $course, $cm, $forum, $skipcompleti
                 'forumtype' => $forum->type,
             )
         );
+        $post->deleted = 1;
         if ($post->userid !== $USER->id) {
             $params['relateduserid'] = $post->userid;
         }
@@ -5251,21 +5311,19 @@ function forum_user_can_see_discussion($forum, $discussion, $context, $user=NULL
 }
 
 /**
- * @global object
- * @global object
- * @param object $forum
- * @param object $discussion
- * @param object $post
- * @param object $user
- * @param object $cm
- * @return bool
+ * Check whether a user can see the specified post.
+ *
+ * @param   \stdClass $forum The forum to chcek
+ * @param   \stdClass $discussion The discussion the post is in
+ * @param   \stdClass $post The post in question
+ * @param   \stdClass $user The user to test - if not specified, the current user is checked.
+ * @param   \stdClass $cm The Course Module that the forum is in (required).
+ * @param   bool      $checkdeleted Whether to check the deleted flag on the post.
+ * @return  bool
  */
-function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NULL) {
+function forum_user_can_see_post($forum, $discussion, $post, $user = null, $cm = null, $checkdeleted = true) {
     global $CFG, $USER, $DB;
 
-    // Context used throughout function.
-    $modcontext = context_module::instance($cm->id);
-
     // retrieve objects (yuk)
     if (is_numeric($forum)) {
         debugging('missing full forum', DEBUG_DEVELOPER);
@@ -5291,6 +5349,10 @@ function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NUL
         $post->id = $post->parent;
     }
 
+    if ($checkdeleted && !empty($post->deleted)) {
+        return false;
+    }
+
     if (!$cm) {
         debugging('missing cm', DEBUG_DEVELOPER);
         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
@@ -5298,6 +5360,9 @@ function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NUL
         }
     }
 
+    // Context used throughout function.
+    $modcontext = context_module::instance($cm->id);
+
     if (empty($user) || empty($user->id)) {
         $user = $USER;
     }
@@ -5824,13 +5889,23 @@ function forum_print_posts_threaded($course, &$cm, $forum, $discussion, $parent,
                 forum_print_post($post, $discussion, $forum, $cm, $course, $ownpost, $reply, $link,
                                      '', '', $postread, true, $forumtracked);
             } else {
-                if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
-                    echo "</div>\n";
-                    continue;
+                if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, true)) {
+                    if (forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
+                        // This post has been deleted but still exists and may have children.
+                        $subject = get_string('privacy:request:delete:post:subject', 'mod_forum');
+                        $byline = '';
+                    } else {
+                        // The user can't see this post at all.
+                        echo "</div>\n";
+                        continue;
+                    }
+                } else {
+                    $by = new stdClass();
+                    $by->name = fullname($post, $canviewfullnames);
+                    $by->date = userdate($post->modified);
+                    $byline = ' ' . get_string("bynameondate", "forum", $by);
+                    $subject = format_string($post->subject, true);
                 }
-                $by = new stdClass();
-                $by->name = fullname($post, $canviewfullnames);
-                $by->date = userdate($post->modified);
 
                 if ($forumtracked) {
                     if (!empty($post->postread)) {
@@ -5841,9 +5916,14 @@ function forum_print_posts_threaded($course, &$cm, $forum, $discussion, $parent,
                 } else {
                     $style = '<span class="forumthread">';
                 }
-                echo $style."<a name=\"$post->id\"></a>".
-                     "<a href=\"discuss.php?d=$post->discussion&amp;parent=$post->id\">".format_string($post->subject,true)."</a> ";
-                print_string("bynameondate", "forum", $by);
+
+                echo $style;
+                echo "<a name='{$post->id}'></a>";
+                echo html_writer::link(new moodle_url('/mod/forum/discuss.php', [
+                        'd' => $post->discussion,
+                        'parent' => $post->id,
+                    ]), $subject);
+                echo $byline;
                 echo "</span>";
             }
 
index 88acf30..df0a726 100644 (file)
@@ -791,6 +791,7 @@ if ($mform_post->is_cancelled()) {
         $addpost = $fromform;
         $addpost->forum=$forum->id;
         if ($fromform->id = forum_add_new_post($addpost, $mform_post)) {
+            $fromform->deleted = 0;
             $subscribemessage = forum_post_subscription($fromform, $forum, $discussion);
 
             if (!empty($fromform->mailnow)) {
index 4f41397..f360627 100644 (file)
@@ -189,7 +189,7 @@ function forum_rss_feed_discussions_sql($forum, $cm, $newsince=0) {
               FROM {forum_discussions} d
                    JOIN {forum_posts} p ON p.discussion = d.id
                    JOIN {user} u ON p.userid = u.id
-             WHERE d.forum = {$forum->id} AND p.parent = 0
+             WHERE d.forum = {$forum->id} AND p.parent = 0 AND p.deleted <> 0
                    $timelimit $groupselect $newsince
           ORDER BY $forumsort";
     return array($sql, $params);
@@ -243,7 +243,7 @@ function forum_rss_feed_posts_sql($forum, $cm, $newsince=0) {
                {forum_posts} p,
                {user} u
             WHERE d.forum = {$forum->id} AND
-                p.discussion = d.id AND
+                p.discussion = d.id AND p.deleted <> 0 AND
                 u.id = p.userid $newsince
                 $groupselect
             ORDER BY p.created desc";
@@ -339,10 +339,17 @@ function forum_rss_feed_contents($forum, $sql, $params, $context) {
                 $message = get_string('forumbodyhidden', 'forum');
                 $item->author = get_string('forumauthorhidden', 'forum');
             } else if (!$isdiscussion && !forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
-                // This is a post which the user has no permission to view
-                $item->title = get_string('forumsubjecthidden', 'forum');
-                $message = get_string('forumbodyhidden', 'forum');
-                $item->author = get_string('forumauthorhidden', 'forum');
+                if (forum_user_can_see_post($forum, $discussion, $post, $USER, $cm, false)) {
+                    // This is a post which the user has no permission to view.
+                    $item->title = get_string('forumsubjecthidden', 'forum');
+                    $message = get_string('forumbodyhidden', 'forum');
+                    $item->author = get_string('forumauthorhidden', 'forum');
+                } else {
+                    // This is a post which has been deleted.
+                    $item->title = get_string('privacy:request:delete:post:subject', 'mod_forum');
+                    $message = get_string('privacy:request:delete:post:subject', 'mod_forum');
+                    $item->author = get_string('forumauthorhidden', 'forum');
+                }
             } else {
                 // The user must have permission to view
                 if ($isdiscussion && !empty($rec->discussionname)) {
index cfe301f..c095db7 100644 (file)
@@ -308,7 +308,8 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'canreply' => true,
             'postread' => false,
             'userfullname' => fullname($user3),
-            'userpictureurl' => ''
+            'userpictureurl' => '',
+            'deleted' => false,
         );
 
         $expectedposts['posts'][] = array(
@@ -343,7 +344,8 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'canreply' => true,
             'postread' => false,
             'userfullname' => fullname($user2),
-            'userpictureurl' => ''
+            'userpictureurl' => '',
+            'deleted' => false,
         );
 
         // Test a discussion with two additional posts (total 3 posts).
@@ -416,6 +418,86 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test get forum posts
+     */
+    public function test_mod_forum_get_forum_discussion_posts_deleted() {
+        global $CFG, $PAGE;
+
+        $this->resetAfterTest(true);
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
+
+        // Create a course and enrol some users in it.
+        $course1 = self::getDataGenerator()->create_course();
+
+        // Create users.
+        $user1 = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $user2 = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+
+        // Set the first created user to the test user.
+        self::setUser($user1);
+
+        // Create test data.
+        $forum1 = self::getDataGenerator()->create_module('forum', (object) [
+                'course' => $course1->id,
+            ]);
+        $forum1context = context_module::instance($forum1->cmid);
+
+        // Add discussions to the forum.
+        $discussion = $generator->create_discussion((object) [
+                'course' => $course1->id,
+                'userid' => $user1->id,
+                'forum' => $forum1->id,
+            ]);
+
+        $discussion2 = $generator->create_discussion((object) [
+                'course' => $course1->id,
+                'userid' => $user2->id,
+                'forum' => $forum1->id,
+            ]);
+
+        // Add replies to the discussion.
+        $discussionreply1 = $generator->create_post((object) [
+                'discussion' => $discussion->id,
+                'parent' => $discussion->firstpost,
+                'userid' => $user2->id,
+            ]);
+        $discussionreply2 = $generator->create_post((object) [
+                'discussion' => $discussion->id,
+                'parent' => $discussionreply1->id,
+                'userid' => $user2->id,
+                'subject' => '',
+                'message' => '',
+                'messageformat' => FORMAT_PLAIN,
+                'deleted' => 1,
+            ]);
+        $discussionreply3 = $generator->create_post((object) [
+                'discussion' => $discussion->id,
+                'parent' => $discussion->firstpost,
+                'userid' => $user2->id,
+            ]);
+
+        // Test where some posts have been marked as deleted.
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion->id, 'modified', 'DESC');
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        $deletedsubject = get_string('privacy:request:delete:post:subject', 'mod_forum');
+        $deletedmessage = get_string('privacy:request:delete:post:message', 'mod_forum');
+
+        foreach ($posts['posts'] as $post) {
+            if ($post['id'] == $discussionreply2->id) {
+                $this->assertTrue($post['deleted']);
+                $this->assertEquals($deletedsubject, $post['subject']);
+                $this->assertEquals($deletedmessage, $post['message']);
+            } else {
+                $this->assertFalse($post['deleted']);
+                $this->assertNotEquals($deletedsubject, $post['subject']);
+                $this->assertNotEquals($deletedmessage, $post['message']);
+            }
+        }
+    }
+
     /**
      * Test get forum posts (qanda forum)
      */
index 85d24ab..577a94e 100644 (file)
@@ -29,6 +29,8 @@ global $CFG;
 require_once(__DIR__ . '/helper.php');
 require_once($CFG->dirroot . '/rating/lib.php');
 
+use \mod_forum\privacy\provider;
+
 /**
  * Tests for the forum implementation of the Privacy Provider API.
  *
@@ -888,22 +890,22 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
             }
         }
 
-        // Mark all posts as read by user1.
-        $user1 = reset($users);
+        // Mark all posts as read by user.
+        $user = reset($users);
         $ratedposts = [];
         foreach ($posts as $post) {
             $discussion = $discussions[$post->discussion];
             $forum = $forums[$discussion->forum];
             $context = $contexts[$forum->id];
 
-            // Mark the post as being read by user1.
-            forum_tp_add_read_record($user1->id, $post->id);
+            // Mark the post as being read by user.
+            forum_tp_add_read_record($user->id, $post->id);
 
             // Tag the post.
             \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
 
             // Rate the other users content.
-            if ($post->userid != $user1->id) {
+            if ($post->userid != $user->id) {
                 $ratedposts[$post->id] = $post;
                 $rm = new rating_manager();
                 $ratingoptions = (object) [
@@ -918,7 +920,6 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
                 $rating = new \rating($ratingoptions);
                 $rating->update_rating(75);
             }
-            $posttags = \core_tag_tag::get_item_tags('mod_forum', 'forum_posts', $post->id);
         }
 
         // Run as the user under test.
@@ -941,7 +942,7 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
         // Delete for the first forum.
         $forum = reset($forums);
         $context = $contexts[$forum->id];
-        $this->delete_data_for_all_users_in_context('mod_forum', $context);
+        provider::delete_data_for_all_users_in_context($context);
 
         // Determine what should have been deleted.
         $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
@@ -976,7 +977,7 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
         }
 
         // All tags should have been deleted.
-        $posttags = \core_tag_tag::get_items_tags('mod_forum', 'post', array_keys($postsinforum));
+        $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
         foreach ($posttags as $tags) {
             $this->assertEmpty($tags);
         }
@@ -1028,4 +1029,196 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
             $this->assertNotEmpty($tags);
         }
     }
+
+    /**
+     * Ensure that all user data is deleted for a specific context.
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $fs = get_file_storage();
+        $course = $this->getDataGenerator()->create_course();
+        $users = $this->helper_create_users($course, 5);
+
+        $forums = [];
+        $contexts = [];
+        for ($i = 0; $i < 2; $i++) {
+            $forum = $this->getDataGenerator()->create_module('forum', [
+                'course' => $course->id,
+                'scale' => 100,
+            ]);
+            $cm = get_coursemodule_from_instance('forum', $forum->id);
+            $context = \context_module::instance($cm->id);
+            $forums[$forum->id] = $forum;
+            $contexts[$forum->id] = $context;
+        }
+
+        $discussions = [];
+        $posts = [];
+        $postsbyforum = [];
+        foreach ($users as $user) {
+            $postsbyforum[$user->id] = [];
+            foreach ($forums as $forum) {
+                $context = $contexts[$forum->id];
+
+                // Create a new discussion + post in the forum.
+                list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
+                $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
+                $discussions[$discussion->id] = $discussion;
+                $postsbyforum[$user->id][$context->id] = [];
+
+                // Add a number of replies.
+                $posts[$post->id] = $post;
+                $thisforumposts[$post->id] = $post;
+                $postsbyforum[$user->id][$context->id][$post->id] = $post;
+
+                $reply = $this->helper_reply_to_post($post, $user);
+                $posts[$reply->id] = $reply;
+                $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
+
+                $reply = $this->helper_reply_to_post($post, $user);
+                $posts[$reply->id] = $reply;
+                $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
+
+                $reply = $this->helper_reply_to_post($reply, $user);
+                $posts[$reply->id] = $reply;
+                $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
+
+                // Add a fake inline image to the original post.
+                $fs->create_file_from_string([
+                        'contextid' => $context->id,
+                        'component' => 'mod_forum',
+                        'filearea'  => 'post',
+                        'itemid'    => $post->id,
+                        'filepath'  => '/',
+                        'filename'  => 'example.jpg',
+                    ], 'image contents (not really)');
+            }
+        }
+
+        // Mark all posts as read by user1.
+        $user1 = reset($users);
+        foreach ($posts as $post) {
+            $discussion = $discussions[$post->discussion];
+            $forum = $forums[$discussion->forum];
+            $context = $contexts[$forum->id];
+
+            // Mark the post as being read by user1.
+            forum_tp_add_read_record($user1->id, $post->id);
+        }
+
+        // Rate and tag all posts.
+        $ratedposts = [];
+        foreach ($users as $user) {
+            foreach ($posts as $post) {
+                $discussion = $discussions[$post->discussion];
+                $forum = $forums[$discussion->forum];
+                $context = $contexts[$forum->id];
+
+                // Tag the post.
+                \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
+
+                // Rate the other users content.
+                if ($post->userid != $user->id) {
+                    $ratedposts[$post->id] = $post;
+                    $rm = new rating_manager();
+                    $ratingoptions = (object) [
+                        'context' => $context,
+                        'component' => 'mod_forum',
+                        'ratingarea' => 'post',
+                        'itemid' => $post->id,
+                        'scaleid' => $forum->scale,
+                        'userid' => $user->id,
+                    ];
+
+                    $rating = new \rating($ratingoptions);
+                    $rating->update_rating(75);
+                }
+            }
+        }
+
+        // Delete for one of the forums for the first user.
+        $firstcontext = reset($contexts);
+        list($postinsql, $postinparams) = $DB->get_in_or_equal(
+                array_keys($postsbyforum[$user1->id][$firstcontext->id]), SQL_PARAMS_NAMED);
+
+        $othercontext = next($contexts);
+        list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal(
+                array_keys($postsbyforum[$user1->id][$othercontext->id]), SQL_PARAMS_NAMED);
+
+        $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
+            \core_user::get_user($user1->id),
+            'mod_forum',
+            [$firstcontext->id]
+        );
+        provider::delete_data_for_user($approvedcontextlist);
+
+        // All posts should remain.
+        $this->assertCount(40, $DB->get_records('forum_posts'));
+
+        // There should be 8 posts belonging to user1.
+        $this->assertCount(8, $DB->get_records('forum_posts', [
+                'userid' => $user1->id,
+            ]));
+
+        // Four of those posts should have been marked as deleted.
+        // That means that the deleted flag is set, and both the subject and message are empty.
+        $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
+                    . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
+                    . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
+                , [
+                    'userid' => $user1->id,
+                    'deleted' => 1,
+                    'subject' => '',
+                    'message' => '',
+                ]));
+
+        // Only user1's posts should have been marked this way.
+        $this->assertCount(4, $DB->get_records('forum_posts', [
+                'deleted' => 1,
+            ]));
+        $this->assertCount(4, $DB->get_records_select('forum_posts',
+            $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
+                'subject' => '',
+            ]));
+        $this->assertCount(4, $DB->get_records_select('forum_posts',
+            $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
+                'message' => '',
+            ]));
+
+        // Only the posts in the first discussion should have been marked this way.
+        $this->assertCount(4, $DB->get_records_select('forum_posts',
+            "deleted = :deleted AND id {$postinsql}",
+                array_merge($postinparams, [
+                    'deleted' => 1,
+                ])
+            ));
+
+        // Ratings should have been removed from the affected posts.
+        $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
+
+        // Ratings should remain on posts in the other context.
+        $this->assertCount(16, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
+
+        // Ratings should remain where the user has rated another person's post.
+        $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
+
+        // Tags for the affected posts should be removed.
+        $this->assertCount(8, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
+
+        // Tags should remain for the other posts by this user.
+        $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
+
+        // Tags should remain for others.
+        // Original total: 5 users * 2 forums * 4 posts * 2 tags
+        // Deleted posts: 8
+        // New total: 72.
+        $this->assertCount(72, $DB->get_records('tag_instance'));
+
+        // Files for the affected posts should be removed.
+        $this->assertCount(0, $DB->get_records_select('files', "itemid {$postinsql}", $postinparams));
+
+        // Files for the other posts should remain.
+        $this->assertCount(2, $DB->get_records_select('files', "itemid {$otherpostinsql}", $otherpostinparams));
+    }
 }
index 973acbe..eba4f0b 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017111300;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2018032900;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2017110800;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)