Merge branch 'wip-MDL-61937-master' of git://github.com/marinaglancy/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 4 May 2018 03:29:24 +0000 (11:29 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Fri, 4 May 2018 03:29:24 +0000 (11:29 +0800)
38 files changed:
comment/classes/privacy/provider.php
comment/tests/privacy_test.php
mod/data/classes/privacy/datafield_provider.php [new file with mode: 0644]
mod/data/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/checkbox/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/checkbox/lang/en/datafield_checkbox.php
mod/data/field/date/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/date/lang/en/datafield_date.php
mod/data/field/file/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/file/field.class.php
mod/data/field/file/lang/en/datafield_file.php
mod/data/field/latlong/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/latlong/lang/en/datafield_latlong.php
mod/data/field/menu/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/menu/lang/en/datafield_menu.php
mod/data/field/multimenu/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/multimenu/lang/en/datafield_multimenu.php
mod/data/field/number/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/number/lang/en/datafield_number.php
mod/data/field/picture/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/picture/field.class.php
mod/data/field/picture/lang/en/datafield_picture.php
mod/data/field/radiobutton/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/radiobutton/lang/en/datafield_radiobutton.php
mod/data/field/text/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/text/lang/en/datafield_text.php
mod/data/field/textarea/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/textarea/lang/en/datafield_textarea.php
mod/data/field/url/classes/privacy/provider.php [new file with mode: 0644]
mod/data/field/url/lang/en/datafield_url.php
mod/data/lang/en/data.php
mod/data/tests/generator/lib.php
mod/data/tests/generator_test.php
mod/data/tests/privacy_provider_test.php [new file with mode: 0644]
rating/classes/privacy/provider.php
rating/tests/privacy_provider_test.php
tag/classes/privacy/provider.php
tag/tests/privacy_test.php

index 68ae8a2..df60226 100644 (file)
@@ -29,8 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 use \core_privacy\local\metadata\collection;
 use \core_privacy\local\request\transform;
 
-require_once($CFG->dirroot . '/comment/lib.php');
-
 /**
  * Privacy class for requesting user data.
  *
@@ -67,33 +65,46 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
      * @param  bool   $onlyforthisuser  Only return the comments this user made.
      */
     public static function export_comments(\context $context, string $component, string $commentarea, int $itemid,
-            array $subcontext, bool $onlyforthisuser = true) {
-        $data = new \stdClass;
-        $data->context   = $context;
-        $data->area      = $commentarea;
-        $data->itemid    = $itemid;
-        $data->component = $component;
-
-        $commentobject = new \comment($data);
-        $commentobject->set_view_permission(true);
-        $comments = $commentobject->get_comments(0);
-        $subcontext[] = get_string('commentsubcontext', 'core_comment');
-
-        $comments = array_filter($comments, function($comment) use ($onlyforthisuser) {
-            global $USER;
-
-            return (!$onlyforthisuser || $comment->userid == $USER->id);
-        });
-
-        $comments = array_map(function($comment) {
-            return (object) [
-                'content' => $comment->content,
-                'time' => transform::datetime($comment->timecreated),
-                'userid' => transform::user($comment->userid),
+                                           array $subcontext, bool $onlyforthisuser = true) {
+        global $USER, $DB;
+        $params = [
+            'contextid' => $context->id,
+            'component' => $component,
+            'commentarea' => $commentarea,
+            'itemid' => $itemid
+        ];
+        $sql = "SELECT c.id, c.content, c.format, c.timecreated, c.userid
+                  FROM {comments} c
+                 WHERE c.contextid = :contextid AND
+                       c.commentarea = :commentarea AND
+                       c.itemid = :itemid AND
+                       (c.component IS NULL OR c.component = :component)";
+        if ($onlyforthisuser) {
+            $sql .= " AND c.userid = :userid";
+            $params['userid'] = $USER->id;
+        }
+        $sql .= " ORDER BY c.timecreated DESC";
+
+        $rs = $DB->get_recordset_sql($sql, $params);
+        $comments = [];
+        foreach ($rs as $record) {
+            if ($record->userid != $USER->id) {
+                // Clean HTML in comments that were added by other users.
+                $comment = ['content' => format_text($record->content, $record->format, ['context' => $context])];
+            } else {
+                // Export comments made by this user as they are stored.
+                $comment = ['content' => $record->content, 'contentformat' => $record->format];
+            }
+            $comment += [
+                'time' => transform::datetime($record->timecreated),
+                'userid' => transform::user($record->userid),
             ];
-        }, $comments);
+            $comments[] = (object)$comment;
+        }
+        $rs->close();
 
         if (!empty($comments)) {
+            $subcontext[] = get_string('commentsubcontext', 'core_comment');
             \core_privacy\local\request\writer::with_context($context)
                 ->export_data($subcontext, (object) [
                     'comments' => $comments,
@@ -125,6 +136,26 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
         $DB->delete_records('comments', $params);
     }
 
+    /**
+     * Deletes all comments for a specified context, component, and commentarea.
+     *
+     * @param  \context $context Details about which context to delete comments for.
+     * @param  string $component Component to delete.
+     * @param  string $commentarea Comment area to delete.
+     * @param  string $itemidstest an SQL fragment that the itemid must match. Used
+     *      in the query like WHERE itemid $itemidstest. Must use named parameters,
+     *      and may not use named parameters called contextid, component or commentarea.
+     * @param array $params any query params used by $itemidstest.
+     */
+    public static function delete_comments_for_all_users_select(\context $context, string $component, string $commentarea,
+            $itemidstest, $params = []) {
+        global $DB;
+        $params += ['contextid' => $context->id, 'component' => $component, 'commentarea' => $commentarea];
+        $DB->delete_records_select('comments',
+            'contextid = :contextid AND component = :component AND commentarea = :commentarea AND itemid ' . $itemidstest,
+            $params);
+    }
+
     /**
      * Deletes all records for a user from a list of approved contexts.
      *
index ab02ab7..e7c6d0e 100644 (file)
@@ -160,6 +160,76 @@ class core_comment_privacy_testcase extends provider_testcase {
         $this->assertEquals('tool_dataprivacy', $data->component);
     }
 
+    /**
+     * Tests the deletion of all comments in a context.
+     */
+    public function test_delete_comments_for_all_users_select() {
+        global $DB;
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+
+        $coursecontext1 = context_course::instance($course1->id);
+        $coursecontext2 = context_course::instance($course2->id);
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $comment1 = $this->get_comment_object($coursecontext1, $course1);
+        $comment2 = $this->get_comment_object($coursecontext2, $course2);
+
+        $this->setUser($user1);
+        $comment1->add('First comment for user 1 on comment 1');
+        $comment2->add('First comment for user 1 on comment 2');
+        $this->setUser($user2);
+        $comment1->add('First comment for user 2 on comment 1');
+        $comment2->add('First comment for user 2 on comment 2');
+
+        // Because of the way things are set up with validation, creating an entry with the same context in a different component
+        // or comment area is a huge pain. We're just going to jam entries into the table instead.
+        $record = (object) [
+            'contextid' => $coursecontext1->id,
+            'component' => 'block_comments',
+            'commentarea' => 'other_comments',
+            'itemid' => 2,
+            'content' => 'Comment user 1 different comment area',
+            'format' => 0,
+            'userid' => $user1->id,
+            'timecreated' => time()
+        ];
+        $DB->insert_record('comments', $record);
+        $record = (object) [
+            'contextid' => $coursecontext1->id,
+            'component' => 'tool_dataprivacy',
+            'commentarea' => 'page_comments',
+            'itemid' => 2,
+            'content' => 'Comment user 1 different component',
+            'format' => 0,
+            'userid' => $user1->id,
+            'timecreated' => time()
+        ];
+        $DB->insert_record('comments', $record);
+
+        // Delete only for the first context. All records in the comments table for this context should be removed.
+        list($sql, $params) = $DB->get_in_or_equal([0, 1, 2, 3], SQL_PARAMS_NAMED);
+        \core_comment\privacy\provider::delete_comments_for_all_users_select($coursecontext1,
+            'block_comments', 'page_comments', $sql, $params);
+        // No records left here.
+        $this->assertCount(0, $comment1->get_comments());
+        // All of the records are left intact here.
+        $this->assertCount(2, $comment2->get_comments());
+        // Check the other comment area.
+        $result = $DB->get_records('comments', ['commentarea' => 'other_comments']);
+        $this->assertCount(1, $result);
+        $data = array_shift($result);
+        $this->assertEquals('other_comments', $data->commentarea);
+        // Check the different component, same commentarea.
+        $result = $DB->get_records('comments', ['component' => 'tool_dataprivacy']);
+        $this->assertCount(1, $result);
+        $data = array_shift($result);
+        $this->assertEquals('tool_dataprivacy', $data->component);
+    }
+
     /**
      * Tests deletion of comments for a specified user and contexts.
      */
@@ -229,7 +299,7 @@ class core_comment_privacy_testcase extends provider_testcase {
         // Nothing changed here as user 1 did not leave a comment.
         $comment3comments = $comment3->get_comments();
         $this->assertCount(1, $comment3comments);
-        $data = array_shift(($comment3comments));
+        $data = array_shift($comment3comments);
         $this->assertEquals($user2->id, $data->userid);
         // Check the other comment area.
         $result = $DB->get_records('comments', ['commentarea' => 'other_comments']);
diff --git a/mod/data/classes/privacy/datafield_provider.php b/mod/data/classes/privacy/datafield_provider.php
new file mode 100644 (file)
index 0000000..bc4f407
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains interface datafield_provider
+ *
+ * @package mod_data
+ * @copyright 2018 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Interface datafield_provider, all datafield plugins need to implement it
+ *
+ * @package mod_data
+ * @copyright 2018 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface datafield_provider extends \core_privacy\local\request\plugin\subplugin_provider {
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * Datafield plugins providers should implement this method to:
+     * - preprocess references to files in the response (examples - textarea, picture, file)
+     * - make content more human-readable (example - replace values separators in multimenu, format date in date)
+     * - add more information about the field itself (example - list all options for menu, multimenu, radio)
+     *
+     * Sample implementation (from datafield_textarea):
+     *
+     *    $defaultvalue->content = writer::with_context($context)
+     *        ->rewrite_pluginfile_urls([$recordobj->id, $contentobj->id], 'mod_data', 'content', $contentobj->id,
+     *        $defaultvalue->content);
+     *    writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue);
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * Usually datafield plugins do not store anything and this method will be empty.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj);
+}
diff --git a/mod/data/classes/privacy/provider.php b/mod/data/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..5500806
--- /dev/null
@@ -0,0 +1,445 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy Subsystem implementation for mod_data.
+ *
+ * @package    mod_data
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\helper;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use core_privacy\manager;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Implementation of the privacy subsystem plugin provider for the database activity module.
+ *
+ * @package    mod_data
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+        // This plugin stores personal data.
+        \core_privacy\local\metadata\provider,
+
+        // This plugin is a core_user_data_provider.
+        \core_privacy\local\request\plugin\provider {
+
+    /** @var array stores list of records marked for deletion */
+    protected static $deletedrecords = [];
+
+    /**
+     * Return the fields which contain personal data.
+     *
+     * @param collection $collection a reference to the collection to use to store the metadata.
+     * @return collection the updated collection of metadata items.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_database_table(
+            'data_records',
+            [
+                'userid' => 'privacy:metadata:data_records:userid',
+                'groupid' => 'privacy:metadata:data_records:groupid',
+                'timecreated' => 'privacy:metadata:data_records:timecreated',
+                'timemodified' => 'privacy:metadata:data_records:timemodified',
+                'approved' => 'privacy:metadata:data_records:approved',
+            ],
+            'privacy:metadata:data_records'
+        );
+        $collection->add_database_table(
+            'data_content',
+            [
+                'fieldid' => 'privacy:metadata:data_content:fieldid',
+                'content' => 'privacy:metadata:data_content:content',
+                'content1' => 'privacy:metadata:data_content:content1',
+                'content2' => 'privacy:metadata:data_content:content2',
+                'content3' => 'privacy:metadata:data_content:content3',
+                'content4' => 'privacy:metadata:data_content:content4',
+            ],
+            'privacy:metadata:data_content'
+        );
+
+        // Link to subplugins.
+        $collection->add_plugintype_link('datafield', [], 'privacy:metadata:datafieldnpluginsummary');
+
+        // Subsystems used.
+        $collection->link_subsystem('core_comment', 'privacy:metadata:commentpurpose');
+        $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
+        $collection->link_subsystem('core_tag', 'privacy:metadata:tagpurpose');
+        $collection->link_subsystem('core_rating', 'privacy:metadata:ratingpurpose');
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid the userid.
+     * @return contextlist the list of contexts containing user info for the user.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        // Fetch all data records.
+        $sql = "SELECT c.id
+                  FROM {context} c
+            INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
+            INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
+            INNER JOIN {data} d ON d.id = cm.instance
+            INNER JOIN {data_records} dr ON dr.dataid = d.id
+            LEFT JOIN {comments} com ON com.commentarea=:commentarea and com.itemid = dr.id
+            LEFT JOIN {rating} r ON r.contextid = c.id AND r.itemid  = dr.id AND r.component = :moddata AND r.ratingarea = :ratingarea
+                 WHERE dr.userid = :userid OR com.userid = :userid1 OR r.userid = :userid2";
+
+        $params = [
+            'modname'       => 'data',
+            'contextlevel'  => CONTEXT_MODULE,
+            'userid'        => $userid,
+            'userid1'       => $userid,
+            'userid2'       => $userid,
+            'commentarea'   => 'database_entry',
+            'moddata'       => 'mod_data',
+            'ratingarea'    => 'entry',
+        ];
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Creates an object from all fields in the $record where key starts with $prefix
+     *
+     * @param \stdClass $record
+     * @param string $prefix
+     * @param array $additionalfields
+     * @return \stdClass
+     */
+    protected static function extract_object_from_record($record, $prefix, $additionalfields = []) {
+        $object = new \stdClass();
+        foreach ($record as $key => $value) {
+            if (preg_match('/^'.preg_quote($prefix, '/').'(.*)/', $key, $matches)) {
+                $object->{$matches[1]} = $value;
+            }
+        }
+        if ($additionalfields) {
+            foreach ($additionalfields as $key => $value) {
+                $object->$key = $value;
+            }
+        }
+        return $object;
+    }
+
+    /**
+     * Export one field answer in a record in database activity module
+     *
+     * @param \context $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    protected static function export_data_content($context, $recordobj, $fieldobj, $contentobj) {
+        $value = (object)[
+            'field' => [
+                // Name and description are displayed in mod_data without applying format_string().
+                'name' => $fieldobj->name,
+                'description' => $fieldobj->description,
+                'type' => $fieldobj->type,
+                'required' => transform::yesno($fieldobj->required),
+            ],
+            'content' => $contentobj->content
+        ];
+        foreach (['content1', 'content2', 'content3', 'content4'] as $key) {
+            if ($contentobj->$key !== null) {
+                $value->$key = $contentobj->$key;
+            }
+        }
+        $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
+        if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
+            component_class_callback($classname, 'export_data_content',
+                [$context, $recordobj, $fieldobj, $contentobj, $value]);
+        } else {
+            // Data field plugin does not implement datafield_provider, just export default value.
+            writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $value);
+        }
+        writer::with_context($context)->export_area_files([$recordobj->id, $contentobj->id], 'mod_data',
+            'content', $contentobj->id);
+    }
+
+    /**
+     * SQL query that returns all fields from {data_content}, {data_fields} and {data_records} tables
+     *
+     * @return string
+     */
+    protected static function sql_fields() {
+        return 'd.id AS dataid, dc.id AS contentid, dc.fieldid, df.type AS fieldtype, df.name AS fieldname,
+                  df.description AS fielddescription, df.required AS fieldrequired,
+                  df.param1 AS fieldparam1, df.param2 AS fieldparam2, df.param3 AS fieldparam3, df.param4 AS fieldparam4,
+                  df.param5 AS fieldparam5, df.param6 AS fieldparam6, df.param7 AS fieldparam7, df.param8 AS fieldparam8,
+                  df.param9 AS fieldparam9, df.param10 AS fieldparam10,
+                  dc.content AS contentcontent, dc.content1 AS contentcontent1, dc.content2 AS contentcontent2,
+                  dc.content3 AS contentcontent3, dc.content4 AS contentcontent4,
+                  dc.recordid, dr.timecreated AS recordtimecreated, dr.timemodified AS recordtimemodified,
+                  dr.approved AS recordapproved, dr.groupid AS recordgroupid, dr.userid AS recorduserid';
+    }
+
+    /**
+     * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
+     *
+     * @param approved_contextlist $contextlist a list of contexts approved for export.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        if (!$contextlist->count()) {
+            return;
+        }
+
+        $user = $contextlist->get_user();
+
+        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
+        $sql = "SELECT cm.id AS cmid, d.name AS dataname, cm.course AS courseid, " . self::sql_fields() . "
+                FROM {context} ctx
+                JOIN {course_modules} cm ON cm.id = ctx.instanceid
+                JOIN {modules} m ON m.id = cm.module AND m.name = :modname
+                JOIN {data} d ON d.id = cm.instance
+                JOIN {data_records} dr ON dr.dataid = d.id
+                JOIN {data_content} dc ON dc.recordid = dr.id
+                JOIN {data_fields} df ON df.id = dc.fieldid
+                WHERE ctx.id {$contextsql} AND ctx.contextlevel = :contextlevel
+                AND dr.userid = :userid OR
+                  EXISTS (SELECT 1 FROM {comments} com WHERE com.commentarea=:commentarea
+                    AND com.itemid = dr.id AND com.userid = :userid1) OR
+                  EXISTS (SELECT 1 FROM {rating} r WHERE r.contextid = ctx.id AND r.itemid  = dr.id AND r.component = :moddata
+                    AND r.ratingarea = :ratingarea AND r.userid = :userid2)
+                ORDER BY cm.id, dr.id, dc.fieldid";
+        $rs = $DB->get_recordset_sql($sql, $contextparams + ['contextlevel' => CONTEXT_MODULE,
+                'modname' => 'data', 'userid' => $user->id, 'userid1' => $user->id, 'commentarea' => 'database_entry',
+                'userid2' => $user->id, 'ratingarea' => 'entry', 'moddata' => 'mod_data']);
+
+        $context = null;
+        $recordobj = null;
+        foreach ($rs as $row) {
+            if (!$context || $context->instanceid != $row->cmid) {
+                // This row belongs to the different data module than the previous row.
+                // Export the data for the previous module.
+                self::export_data($context, $user);
+                // Start new data module.
+                $context = \context_module::instance($row->cmid);
+            }
+
+            if (!$recordobj || $row->recordid != $recordobj->id) {
+                // Export previous data record.
+                self::export_data_record($context, $user, $recordobj);
+                // Prepare for exporting new data record.
+                $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
+            }
+            $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
+            $contentobj = self::extract_object_from_record($row, 'content',
+                ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
+            self::export_data_content($context, $recordobj, $fieldobj, $contentobj);
+        }
+        $rs->close();
+        self::export_data_record($context, $user, $recordobj);
+        self::export_data($context, $user);
+    }
+
+    /**
+     * Export one entry in the database activity module (one record in {data_records} table)
+     *
+     * @param \context $context
+     * @param \stdClass $user
+     * @param \stdClass $recordobj
+     */
+    protected static function export_data_record($context, $user, $recordobj) {
+        if (!$recordobj) {
+            return;
+        }
+        $data = [
+            'userid' => transform::user($user->id),
+            'groupid' => $recordobj->groupid,
+            'timecreated' => transform::datetime($recordobj->timecreated),
+            'timemodified' => transform::datetime($recordobj->timemodified),
+            'approved' => transform::yesno($recordobj->approved),
+        ];
+        // Data about the record.
+        writer::with_context($context)->export_data([$recordobj->id], (object)$data);
+        // Related tags.
+        \core_tag\privacy\provider::export_item_tags($user->id, $context, [$recordobj->id],
+            'mod_data', 'data_records', $recordobj->id);
+        // Export comments. For records that were not made by this user export only this user's comments, for own records
+        // export comments made by everybody.
+        \core_comment\privacy\provider::export_comments($context, 'mod_data', 'database_entry', $recordobj->id,
+            [$recordobj->id], $recordobj->userid != $user->id);
+        // Export ratings. For records that were not made by this user export only this user's ratings, for own records
+        // export ratings from everybody.
+        \core_rating\privacy\provider::export_area_ratings($user->id, $context, [$recordobj->id], 'entry',
+            $recordobj->id, $recordobj->userid != $user->id);
+    }
+
+    /**
+     * Export basic info about database activity module
+     *
+     * @param \context $context
+     * @param \stdClass $user
+     */
+    protected static function export_data($context, $user) {
+        if (!$context) {
+            return;
+        }
+        $contextdata = helper::get_context_data($context, $user);
+        helper::export_context_files($context, $user);
+        writer::with_context($context)->export_data([], $contextdata);
+    }
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param \context $context the context to delete in.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        global $DB;
+
+        if (!$context instanceof \context_module) {
+            return;
+        }
+
+        $sql = "SELECT " . self::sql_fields() . "
+                FROM {course_modules} cm
+                JOIN {modules} m ON m.id = cm.module AND m.name = :modname
+                JOIN {data} d ON d.id = cm.instance
+                JOIN {data_records} dr ON dr.dataid = d.id
+                LEFT JOIN {data_content} dc ON dc.recordid = dr.id
+                LEFT JOIN {data_fields} df ON df.id = dc.fieldid
+                WHERE cm.id = :cmid
+                ORDER BY dr.id";
+        $rs = $DB->get_recordset_sql($sql, ['cmid' => $context->instanceid, 'modname' => 'data']);
+        foreach ($rs as $row) {
+            self::mark_data_content_for_deletion($context, $row);
+        }
+        $rs->close();
+
+        self::delete_data_records($context);
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist a list of contexts approved for deletion.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        global $DB;
+
+        if (empty($contextlist->count())) {
+            return;
+        }
+
+        $user = $contextlist->get_user();
+
+        foreach ($contextlist->get_contexts() as $context) {
+            $sql = "SELECT " . self::sql_fields() . "
+                FROM {context} ctx
+                JOIN {course_modules} cm ON cm.id = ctx.instanceid
+                JOIN {modules} m ON m.id = cm.module AND m.name = :modname
+                JOIN {data} d ON d.id = cm.instance
+                JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid = :userid
+                LEFT JOIN {data_content} dc ON dc.recordid = dr.id
+                LEFT JOIN {data_fields} df ON df.id = dc.fieldid
+                WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel
+                ORDER BY dr.id";
+            $rs = $DB->get_recordset_sql($sql, ['ctxid' => $context->id, 'contextlevel' => CONTEXT_MODULE,
+                'modname' => 'data', 'userid' => $user->id]);
+            foreach ($rs as $row) {
+                self::mark_data_content_for_deletion($context, $row);
+            }
+            $rs->close();
+            self::delete_data_records($context);
+        }
+
+        // Additionally remove comments this user made on other entries.
+        \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_data', 'entry');
+
+        // We do not delete ratings made by this user on other records because it may change grades.
+    }
+
+    /**
+     * Marks a data_record/data_content for deletion
+     *
+     * Also invokes callback from datafield plugin in case it stores additional data that needs to be deleted
+     *
+     * @param \context $context
+     * @param \stdClass $row result of SQL query - tables data_content, data_record, data_fields join together
+     */
+    protected static function mark_data_content_for_deletion($context, $row) {
+        $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
+        if ($row->contentid && $row->fieldid) {
+            $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
+            $contentobj = self::extract_object_from_record($row, 'content',
+                ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
+
+            // Allow datafield plugin to implement their own deletion.
+            $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
+            if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
+                component_class_callback($classname, 'delete_data_content',
+                    [$context, $recordobj, $fieldobj, $contentobj]);
+            }
+        }
+
+        self::$deletedrecords[$recordobj->id] = $recordobj->id;
+    }
+
+    /**
+     * Deletes records marked for deletion and all associated data
+     *
+     * Should be executed after all records were marked by {@link mark_data_content_for_deletion()}
+     *
+     * Deletes records from data_content and data_records tables, associated files, tags, comments and ratings.
+     *
+     * @param \context $context
+     */
+    protected static function delete_data_records($context) {
+        global $DB;
+        if (empty(self::$deletedrecords)) {
+            return;
+        }
+
+        list($sql, $params) = $DB->get_in_or_equal(self::$deletedrecords, SQL_PARAMS_NAMED);
+
+        // Delete files.
+        get_file_storage()->delete_area_files_select($context->id, 'mod_data', 'data_records',
+            "IN (SELECT dc.id FROM {data_content} dc WHERE dc.recordid $sql)", $params);
+        // Delete from data_content.
+        $DB->delete_records_select('data_content', 'recordid ' . $sql, $params);
+        // Delete from data_records.
+        $DB->delete_records_select('data_records', 'id ' . $sql, $params);
+        // Delete tags.
+        \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_data', 'data_records', $sql, $params);
+        // Delete comments.
+        \core_comment\privacy\provider::delete_comments_for_all_users_select($context, 'mod_data', 'entry', $sql, $params);
+        // Delete ratings.
+        \core_rating\privacy\provider::delete_ratings_select($context, 'mod_data', 'entry', $sql, $params);
+
+        self::$deletedrecords = [];
+    }
+}
diff --git a/mod/data/field/checkbox/classes/privacy/provider.php b/mod/data/field/checkbox/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..3da14eb
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_checkbox.
+ *
+ * @package    datafield_checkbox
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_checkbox\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_checkbox implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY);
+        $defaultvalue->content = explode('##', $defaultvalue->content);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index b2a3df4..6c2d8bf 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Checkbox';
 $string['fieldtypelabel'] = 'Checkbox field';
+$string['privacy:metadata'] = 'The Checkbox field component does not store any personal data itself, it uses tables defined in mod_data.';
diff --git a/mod/data/field/date/classes/privacy/provider.php b/mod/data/field/date/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..478ac0d
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_date.
+ *
+ * @package    datafield_date
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_date\privacy;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_date implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $defaultvalue->content = transform::date($defaultvalue->content);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 6638b33..304ac62 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Date';
 $string['fieldtypelabel'] = 'Date field';
+$string['privacy:metadata'] = 'The Date field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/file/classes/privacy/provider.php b/mod/data/field/file/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..ebb757d
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_file.
+ *
+ * @package    datafield_file
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_file\privacy;
+use mod_data\privacy\datafield_provider;
+use core_privacy\local\request\writer;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_file implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        if ($fieldobj->param3) {
+            $defaultvalue->field['maxbytes'] = $fieldobj->param3;
+        }
+        // Change file name to file path.
+        $defaultvalue->file = writer::with_context($context)
+            ->rewrite_pluginfile_urls([$recordobj->id, $contentobj->id], 'mod_data', 'content', $contentobj->id,
+                '@@PLUGINFILE@@/' . $defaultvalue->content);
+        unset($defaultvalue->content);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 0e2c464..508613c 100644 (file)
@@ -156,6 +156,10 @@ class data_field_file extends data_field_base {
 
         // Should always be available since it is set by display_add_field before initializing the draft area.
         $content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid));
+        if (!$content) {
+            $content = (object)array('fieldid' => $this->field->id, 'recordid' => $recordid);
+            $content->id = $DB->insert_record('data_content', $content);
+        }
 
         file_save_draft_area_files($value, $this->context->id, 'mod_data', 'content', $content->id);
 
index 2e97c99..4aaa5ba 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'File';
 $string['fieldtypelabel'] = 'File field';
+$string['privacy:metadata'] = 'The File field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/latlong/classes/privacy/provider.php b/mod/data/field/latlong/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..9c7013e
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_latlong.
+ *
+ * @package    datafield_latlong
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_latlong\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_latlong implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $defaultvalue->field['services'] = explode(',', $fieldobj->param1);
+        if ($fieldobj->param2 > 0) {
+            $defaultvalue->field['label'] = 'Content of field '.$fieldobj->param2;
+        } else if ($fieldobj->param2 == -2) {
+            $defaultvalue->field['label'] = 'lattitude/longitude';
+        } else {
+            $defaultvalue->field['label'] = 'item #';
+        }
+        $defaultvalue->lattitude = $contentobj->content;
+        $defaultvalue->longitude = $contentobj->content1;
+        unset($defaultvalue->content);
+        unset($defaultvalue->content1);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index b13c381..a3a7a33 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Latlong';
 $string['fieldtypelabel'] = 'Latitude/longitude field';
+$string['privacy:metadata'] = 'The Latitude/longitude field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/menu/classes/privacy/provider.php b/mod/data/field/menu/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..08c419a
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_menu.
+ *
+ * @package    datafield_menu
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_menu\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_menu implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 49da27d..c6ebec5 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Menu';
 $string['fieldtypelabel'] = 'Menu field';
+$string['privacy:metadata'] = 'The Menu field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/multimenu/classes/privacy/provider.php b/mod/data/field/multimenu/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..42f109e
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_multimenu.
+ *
+ * @package    datafield_multimenu
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_multimenu\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_multimenu implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY);
+        for ($i = 1; $i <= 10; $i++) {
+            unset($defaultvalue->field['param' . $i]);
+        }
+        $defaultvalue->content = explode('##', $defaultvalue->content);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 765519c..847a49e 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Multimenu';
 $string['fieldtypelabel'] = 'Multiple-selection menu field';
+$string['privacy:metadata'] = 'The Multiple-selection menu field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/number/classes/privacy/provider.php b/mod/data/field/number/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..c7a2a73
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_number.
+ *
+ * @package    datafield_number
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_number\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_number implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        if (preg_match("/^\d+$/", trim($fieldobj->param1))) {
+            $defaultvalue->field['decimals'] = trim($fieldobj->param1);
+        }
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 4ed6ce7..27659f3 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Number';
 $string['fieldtypelabel'] = 'Number field';
+$string['privacy:metadata'] = 'The Number field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/picture/classes/privacy/provider.php b/mod/data/field/picture/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..d28fcdb
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_picture.
+ *
+ * @package    datafield_picture
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_picture\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_picture implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        if ($fieldobj->param1) {
+            $defaultvalue->field['width'] = $fieldobj->param1;
+        }
+        if ($fieldobj->param2) {
+            $defaultvalue->field['height'] = $fieldobj->param2;
+        }
+        if ($fieldobj->param3) {
+            $defaultvalue->field['maxbytes'] = $fieldobj->param3;
+        }
+
+        // Change file name to file path.
+        $defaultvalue->file = writer::with_context($context)
+            ->rewrite_pluginfile_urls([$recordobj->id, $contentobj->id], 'mod_data', 'content', $contentobj->id,
+                '@@PLUGINFILE@@/' . $defaultvalue->content);
+        if (isset($defaultvalue->content1)) {
+            $defaultvalue->alttext = $defaultvalue->content1;
+        }
+        unset($defaultvalue->content);
+        unset($defaultvalue->content1);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 7354415..a132afc 100644 (file)
@@ -225,6 +225,10 @@ class data_field_picture extends data_field_base {
 
         // Should always be available since it is set by display_add_field before initializing the draft area.
         $content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid));
+        if (!$content) {
+            $content = (object)array('fieldid' => $this->field->id, 'recordid' => $recordid);
+            $content->id = $DB->insert_record('data_content', $content);
+        }
 
         $names = explode('_', $name);
         switch ($names[2]) {
index 84f71ff..ea29945 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Picture';
 $string['fieldtypelabel'] = 'Picture field';
+$string['privacy:metadata'] = 'The Picture field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/radiobutton/classes/privacy/provider.php b/mod/data/field/radiobutton/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..25926ba
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_radiobutton.
+ *
+ * @package    datafield_radiobutton
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_radiobutton\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_radiobutton implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index acb098c..bd264e3 100644 (file)
@@ -26,3 +26,5 @@
 
 $string['pluginname'] = 'Radio button';
 $string['fieldtypelabel'] = 'Radio button field';
+$string['privacy:metadata'] = 'The Radio button field component does not store any personal data, it uses tables defined in mod_data.';
+
diff --git a/mod/data/field/text/classes/privacy/provider.php b/mod/data/field/text/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..923e517
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_text.
+ *
+ * @package    datafield_text
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_text\privacy;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_text implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 5b87802..b4e3c2d 100644 (file)
@@ -26,3 +26,4 @@
 
 $string['pluginname'] = 'Text input';
 $string['fieldtypelabel'] = 'Text field';
+$string['privacy:metadata'] = 'The Text field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/textarea/classes/privacy/provider.php b/mod/data/field/textarea/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..4ad607f
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_textarea.
+ *
+ * @package    datafield_textarea
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_textarea\privacy;
+
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_textarea implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $subcontext = [$recordobj->id, $contentobj->id];
+        $defaultvalue->content = writer::with_context($context)
+            ->rewrite_pluginfile_urls($subcontext, 'mod_data', 'content', $contentobj->id,
+            $defaultvalue->content);
+        $defaultvalue->contentformat = $defaultvalue->content1;
+        unset($defaultvalue->content1);
+
+        $defaultvalue->field['autolink'] = transform::yesno($fieldobj->param1);
+        $defaultvalue->field['rows'] = $fieldobj->param3;
+        $defaultvalue->field['cols'] = $fieldobj->param2;
+        if ($fieldobj->param5) {
+            $defaultvalue->field['maxbytes'] = $fieldobj->param5;
+        }
+        writer::with_context($context)->export_data($subcontext, $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index c5359b0..00a3785 100644 (file)
@@ -28,3 +28,4 @@ $string['maxbytes'] = 'Maximum embedded file size (bytes)';
 $string['maxbytes_desc'] = 'If set to zero will be unlimited by default';
 $string['pluginname'] = 'Text area';
 $string['fieldtypelabel'] = 'Textarea field';
+$string['privacy:metadata'] = 'The Textarea field component does not store any personal data, it uses tables defined in mod_data.';
diff --git a/mod/data/field/url/classes/privacy/provider.php b/mod/data/field/url/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..75aeb56
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Privacy Subsystem implementation for datafield_url.
+ *
+ * @package    datafield_url
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace datafield_url\privacy;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use mod_data\privacy\datafield_provider;
+
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for datafield_url implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider,
+        datafield_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+
+    /**
+     * Exports data about one record in {data_content} table.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use
+     */
+    public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) {
+        $defaultvalue->field['autolink'] = transform::yesno($fieldobj->param1);
+        if (!empty($fieldobj->param2)) {
+            $defaultvalue->field['forcetext'] = $fieldobj->param2;
+        }
+        $defaultvalue->field['blanktarget'] = transform::yesno($fieldobj->param3);
+        $defaultvalue->url = $contentobj->content;
+        $defaultvalue->text = $contentobj->content1;
+        unset($defaultvalue->content);
+        unset($defaultvalue->content1);
+        writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue);
+    }
+
+    /**
+     * Allows plugins to delete locally stored data.
+     *
+     * @param \context_module $context
+     * @param \stdClass $recordobj record from DB table {data_records}
+     * @param \stdClass $fieldobj record from DB table {data_fields}
+     * @param \stdClass $contentobj record from DB table {data_content}
+     */
+    public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) {
+
+    }
+}
\ No newline at end of file
index 4df275d..b17b690 100644 (file)
@@ -26,4 +26,5 @@
 
 $string['pluginname'] = 'URL';
 $string['openlinkinnewwindow'] = 'Open link in new window';
-$string['fieldtypelabel'] = 'URL field';
\ No newline at end of file
+$string['fieldtypelabel'] = 'URL field';
+$string['privacy:metadata'] = 'The URL field component does not store any personal data, it uses tables defined in mod_data.';
index 30db083..227a9cd 100644 (file)
@@ -302,6 +302,24 @@ $string['pluginname'] = 'Database';
 $string['portfolionotfile'] = 'Export to a portfolio rather than a file (csv and leap2a only)';
 $string['presetinfo'] = 'Saving as a preset will publish this template. Other users may be able to use it in their databases.';
 $string['presets'] = 'Presets';
+$string['privacy:metadata:commentpurpose'] = 'Comments on database records';
+$string['privacy:metadata:data_content'] = 'Represents one answer to one field in database activity module';
+$string['privacy:metadata:data_content:fieldid'] = 'Field definition id';
+$string['privacy:metadata:data_content:content'] = 'Content';
+$string['privacy:metadata:data_content:content1'] = 'Additional content 1';
+$string['privacy:metadata:data_content:content2'] = 'Additional content 2';
+$string['privacy:metadata:data_content:content3'] = 'Additional content 3';
+$string['privacy:metadata:data_content:content4'] = 'Additional content 4';
+$string['privacy:metadata:data_records'] = 'Represent records in database acitivity module';
+$string['privacy:metadata:data_records:userid'] = 'User who created the record';
+$string['privacy:metadata:data_records:groupid'] = 'Group';
+$string['privacy:metadata:data_records:timecreated'] = 'Time when record was created';
+$string['privacy:metadata:data_records:timemodified'] = 'Time when record was last modified';
+$string['privacy:metadata:data_records:approved'] = 'Approval status';
+$string['privacy:metadata:datafieldnpluginsummary'] = 'Fields for database activity module';
+$string['privacy:metadata:filepurpose'] = 'File attached to the database record';
+$string['privacy:metadata:tagpurpose'] = 'Tags on database records';
+$string['privacy:metadata:ratingpurpose'] = 'Ratings on database records';
 $string['radiobutton'] = 'Radio buttons';
 $string['recordapproved'] = 'Entry approved';
 $string['recorddeleted'] = 'Entry deleted';
index 02a4809..3295cca 100644 (file)
@@ -29,8 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * Data generator class for mod_data.
  *
- * Currently, the field types in the ignoredfieldtypes array aren't supported.
- *
  * @package    mod_data
  * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
@@ -48,12 +46,6 @@ class mod_data_generator extends testing_module_generator {
      */
     protected $databaserecordcount = 0;
 
-    /**
-     * @var The field types which not handled by the generator as of now.
-     */
-    protected $ignoredfieldtypes = array('latlong', 'file', 'picture');
-
-
     /**
      * To be called from data reset code only,
      * do not use in tests.
@@ -89,7 +81,6 @@ class mod_data_generator extends testing_module_generator {
 
     /**
      * Creates a field for a mod_data instance.
-     * Currently, the field types in the ignoredfieldtypes array aren't supported.
      *
      * @param StdClass $record
      * @param mod_data $data
@@ -98,12 +89,6 @@ class mod_data_generator extends testing_module_generator {
     public function create_field(stdClass $record = null, $data = null) {
         $record = (array) $record;
 
-        if (in_array($record['type'], $this->ignoredfieldtypes)) {
-            throw new coding_exception('$record\'s type value must not be same as values in ignoredfieldtypes
-                    in phpunit_util::create_field()');
-            return false;
-        }
-
         $this->databasefieldcount++;
 
         if (!isset($data->course)) {
@@ -143,6 +128,8 @@ class mod_data_generator extends testing_module_generator {
                 $record['param1'] = implode("\n", array('multimenu1', 'multimenu2', 'multimenu3', 'multimenu4'));
             } else if (($record['type'] === 'text') || ($record['type'] === 'url')) {
                 $record['param1'] = 1;
+            } else if ($record['type'] == 'latlong') {
+                $record['param1'] = 'Google Maps';
             } else {
                 $record['param1'] = '';
             }
@@ -152,6 +139,8 @@ class mod_data_generator extends testing_module_generator {
 
             if ($record['type'] === 'textarea') {
                 $record['param2'] = 60;
+            } else if ($record['type'] == 'latlong') {
+                $record['param2'] = -1;
             } else {
                 $record['param2'] = '';
             }
@@ -161,6 +150,8 @@ class mod_data_generator extends testing_module_generator {
 
             if (($record['type'] === 'textarea')) {
                 $record['param3'] = 35;
+            } else if ($record['type'] == 'picture' || $record['type'] == 'file') {
+                $record['param3'] = 0;
             } else {
                 $record['param3'] = '';
             }
@@ -193,7 +184,6 @@ class mod_data_generator extends testing_module_generator {
      * Creates a field for a mod_data instance.
      * Keep in mind the default data field params created in create_field() function!
      * ...if you haven't provided your own custom data field parameters there.
-     * Currently, the field types in the ignoredfieldtypes array aren't supported.
      * The developers using the generator must adhere to the following format :
      *
      *   Syntax : $contents[ fieldid ] = fieldvalue
@@ -206,16 +196,19 @@ class mod_data_generator extends testing_module_generator {
      *   $contents['text'] = 'text'
      *   $contents['textarea'] = 'text'
      *   $contents['url'] = 'example.url' or array('example.url', 'urlname')
+     *   $contents['latlong'] = array('value for lattitude', 'value for longitude')
+     *   $contents['file'] = 'filename or draftitemid'
+     *   $contents['picture'] = array('filename or draftitemid', 'alternative text')
      *
-     * @param mod_data $data
+     * @param stdClass $data record from table {data}
      * @param array $contents
      * @param int $groupid
      * @param array $tags
      * @param array $options
-     * @return data_field_{type}
+     * @return int id of the generated record in table {data_records}
      */
     public function create_entry($data, array $contents, $groupid = 0, $tags = [], array $options = null) {
-        global $DB;
+        global $DB, $USER, $CFG;
 
         $this->databaserecordcount++;
 
@@ -233,10 +226,6 @@ class mod_data_generator extends testing_module_generator {
         foreach ($fields as $field) {
             $fieldhascontent = true;
 
-            if (in_array($field->type, $this->ignoredfieldtypes)) {
-                continue;
-            }
-
             $field = data_get_field($field, $data);
 
             $fieldid = $field->field->id;
@@ -293,6 +282,52 @@ class mod_data_generator extends testing_module_generator {
                     $fieldhascontent = false;
                 }
 
+            } else if ($field->type === 'latlong') {
+                $values = array();
+
+                foreach ($contents[$fieldid] as $key => $value) {
+                    $values['field_' . $fieldid . '_' . $key] = $value;
+                }
+
+                $contents[$fieldid] = $values;
+                $fieldname = 'field_' . $fieldid . '_0';
+                if (!$field->notemptyfield($values[$fieldname], $fieldname)) {
+                    $fieldhascontent = false;
+                }
+
+            } else if ($field->type === 'file' || $field->type === 'picture') {
+                if (is_array($contents[$fieldid])) {
+                    list($itemid, $alttext) = $contents[$fieldid];
+                } else {
+                    $itemid = $contents[$fieldid];
+                    $alttext = '';
+                }
+
+                if (strlen($itemid) && !is_numeric($itemid)) {
+                    // We expect draftarea item id here but it can also be a filename, in this case provider will generate file.
+                    $filename = $itemid;
+                    $usercontext = context_user::instance($USER->id);
+                    $itemid = file_get_unused_draft_itemid();
+                    get_file_storage()->create_file_from_string(['component' => 'user', 'filearea' => 'draft',
+                        'contextid' => $usercontext->id, 'itemid' => $itemid, 'filepath' => '/',
+                        'filename' => $filename],
+                        file_get_contents($CFG->dirroot.'/mod/data/pix/icon.png'));
+                }
+
+                $fieldname = 'field_' . $fieldid . '_file';
+                if ($field->type === 'file') {
+                    $contents[$fieldid] = $itemid;
+                } else {
+                    $contents[$fieldid] = [
+                        $fieldname => $itemid,
+                        'field_' . $fieldid . '_alttext' => $alttext
+                    ];
+                }
+
+                if (!$field->notemptyfield($itemid, $fieldname)) {
+                    $fieldhascontent = false;
+                }
+
             } else {
                 if ($field->notemptyfield($contents[$fieldid], 'field_' . $fieldid . '_0')) {
                     continue;
@@ -307,7 +342,7 @@ class mod_data_generator extends testing_module_generator {
         foreach ($contents as $fieldid => $content) {
             $field = data_get_field_from_id($fieldid, $data);
 
-            if (is_array($content) and in_array($field->type, array('date', 'textarea', 'url'))) {
+            if (is_array($content) and in_array($field->type, array('date', 'textarea', 'url', 'picture', 'latlong'))) {
 
                 foreach ($content as $fieldname => $value) {
                     $field->update_content($recordid, $value, $fieldname);
index bb4945e..d89c772 100644 (file)
@@ -159,7 +159,8 @@ class mod_data_generator_testcase extends advanced_testcase {
         $context = context_module::instance($cm->id);
         $this->assertEquals($data->cmid, $context->instanceid);
 
-        $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url');
+        $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url',
+            'latlong', 'file', 'picture');
 
         $count = 1;
 
@@ -192,6 +193,9 @@ class mod_data_generator_testcase extends advanced_testcase {
         $contents[] = 'text for testing';
         $contents[] = '<p>text area testing<br /></p>';
         $contents[] = array('example.url', 'sampleurl');
+        $contents[] = [-31.9489873, 115.8382036]; // Latlong.
+        $contents[] = 'Filename.pdf'; // File - filename.
+        $contents[] = array('Cat1234.jpg', 'Cat'); // Picture - filename with alt text.
         $count = 0;
         $fieldcontents = array();
         foreach ($fields as $fieldrecord) {
@@ -200,6 +204,7 @@ class mod_data_generator_testcase extends advanced_testcase {
 
         $tags = ['Cats', 'mice'];
 
+        $this->setUser($user1);
         $datarecordid = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($data,
             $fieldcontents, $groupa->id, $tags);
 
diff --git a/mod/data/tests/privacy_provider_test.php b/mod/data/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..c85c994
--- /dev/null
@@ -0,0 +1,243 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy provider tests.
+ *
+ * @package    mod_data
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use core_privacy\local\metadata\collection;
+use mod_data\privacy\provider;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy provider tests class.
+ *
+ * @package    mod_data
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
+    /** @var stdClass The student object. */
+    protected $student;
+    /** @var stdClass The student object. */
+    protected $student2;
+
+    /** @var stdClass The data object. */
+    protected $datamodule;
+
+    /** @var stdClass The course object. */
+    protected $course;
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function setUp() {
+        $this->resetAfterTest();
+
+        global $DB;
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $params = [
+            'course' => $course->id,
+            'name' => 'Database module',
+            'comments' => 1,
+            'assessed' => 1,
+        ];
+
+        // The database activity.
+        $datamodule = $this->get_generator()->create_instance($params);
+
+        $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url',
+            'latlong', 'file', 'picture');
+        // Creating test Fields with default parameter values.
+        foreach ($fieldtypes as $count => $fieldtype) {
+            // Creating variables dynamically.
+            $fieldname = 'field' . $count;
+            $record = new \stdClass();
+            $record->name = $fieldname;
+            $record->description = $fieldname . ' descr';
+            $record->type = $fieldtype;
+
+            ${$fieldname} = $this->get_generator()->create_field($record, $datamodule);
+        }
+
+        $cm = get_coursemodule_from_instance('data', $datamodule->id);
+
+        // Create a student.
+        $student1 = $generator->create_user();
+        $student2 = $generator->create_user();
+        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+        $generator->enrol_user($student1->id,  $course->id, $studentrole->id);
+        $generator->enrol_user($student2->id,  $course->id, $studentrole->id);
+
+        // Add records.
+        $this->setUser($student1);
+        $record1id = $this->generate_data_record($datamodule);
+        $this->generate_data_record($datamodule);
+
+        $this->setUser($student2);
+        $this->generate_data_record($datamodule);
+        $this->generate_data_record($datamodule);
+        $this->generate_data_record($datamodule);
+
+        $this->student = $student1;
+        $this->student2 = $student2;
+        $this->datamodule = $datamodule;
+        $this->course = $course;
+    }
+
+    /**
+     * Get mod_data generator
+     *
+     * @return mod_data_generator
+     */
+    protected function get_generator() {
+        return $this->getDataGenerator()->get_plugin_generator('mod_data');
+    }
+
+    /**
+     * Generates one record in the database module as the current student
+     *
+     * @param stdClass $datamodule
+     * @return mixed
+     */
+    protected function generate_data_record($datamodule) {
+        global $DB;
+
+        static $counter = 0;
+        $counter++;
+
+        $contents = array();
+        $contents[] = array('opt1', 'opt2', 'opt3', 'opt4');
+        $contents[] = sprintf("%02f", $counter) . '-01-2000';
+        $contents[] = 'menu1';
+        $contents[] = array('multimenu1', 'multimenu2', 'multimenu3', 'multimenu4');
+        $contents[] = 5 * $counter;
+        $contents[] = 'radioopt1';
+        $contents[] = 'text for testing' . $counter;
+        $contents[] = "<p>text area testing $counter<br /></p>";
+        $contents[] = array('example.url', 'sampleurl' . $counter);
+        $contents[] = [-31.9489873, 115.8382036]; // Latlong.
+        $contents[] = "Filename{$counter}.pdf"; // File - filename.
+        $contents[] = array("Cat{$counter}.jpg", 'Cat' . $counter); // Picture - filename with alt text.
+        $count = 0;
+        $fieldcontents = array();
+        $fields = $DB->get_records('data_fields', array('dataid' => $datamodule->id), 'id');
+        foreach ($fields as $fieldrecord) {
+            $fieldcontents[$fieldrecord->id] = $contents[$count++];
+        }
+        $tags = ['Cats', 'mice' . $counter];
+        return $this->get_generator()->create_entry($datamodule, $fieldcontents, 0, $tags);
+    }
+
+    /**
+     * Test for provider::get_metadata().
+     */
+    public function test_get_metadata() {
+        $collection = new collection('mod_data');
+        $newcollection = provider::get_metadata($collection);
+        $itemcollection = $newcollection->get_collection();
+        $this->assertCount(7, $itemcollection);
+
+        $table = reset($itemcollection);
+        $this->assertEquals('data_records', $table->get_name());
+
+        $table = next($itemcollection);
+        $this->assertEquals('data_content', $table->get_name());
+    }
+
+    /**
+     * Test for provider::get_contexts_for_userid().
+     */
+    public function test_get_contexts_for_userid() {
+        $cm = get_coursemodule_from_instance('data', $this->datamodule->id);
+
+        $contextlist = provider::get_contexts_for_userid($this->student->id);
+        $this->assertCount(1, $contextlist);
+        $contextforuser = $contextlist->current();
+        $cmcontext = context_module::instance($cm->id);
+        $this->assertEquals($cmcontext->id, $contextforuser->id);
+    }
+
+    /**
+     * Get test privacy writer
+     *
+     * @param context $context
+     * @return \core_privacy\tests\request\content_writer
+     */
+    protected function get_writer($context) {
+        return \core_privacy\local\request\writer::with_context($context);
+    }
+
+    /**
+     * Test for provider::export_user_data().
+     */
+    public function test_export_for_context() {
+        global $DB;
+        $cm = get_coursemodule_from_instance('data', $this->datamodule->id);
+        $cmcontext = context_module::instance($cm->id);
+        $records = $DB->get_records_select('data_records', 'userid = :userid ORDER BY id', ['userid' => $this->student->id]);
+        $record = reset($records);
+        $contents = $DB->get_records('data_content', ['recordid' => $record->id]);
+
+        // Export all of the data for the context.
+        $this->export_context_data_for_user($this->student->id, $cmcontext, 'mod_data');
+        $writer = $this->get_writer($cmcontext);
+        $data = $writer->get_data([$record->id]);
+        $this->assertNotEmpty($data);
+        foreach ($contents as $content) {
+            $data = $writer->get_data([$record->id, $content->id]);
+            $this->assertNotEmpty($data);
+            $hasfile = in_array($data->field['type'], ['file', 'picture']);
+            $this->assertEquals($hasfile, !empty($writer->get_files([$record->id, $content->id])));
+        }
+        $tags = $writer->get_related_data([$record->id], 'tags');
+        $this->assertNotEmpty($tags);
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        $cm = get_coursemodule_from_instance('data', $this->datamodule->id);
+        $cmcontext = context_module::instance($cm->id);
+
+        provider::delete_data_for_all_users_in_context($cmcontext);
+
+        $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]);
+        provider::export_user_data($appctxt);
+        $this->assertFalse($this->get_writer($cmcontext)->has_any_data());
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        $cm = get_coursemodule_from_instance('data', $this->datamodule->id);
+        $cmcontext = context_module::instance($cm->id);
+
+        $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]);
+        provider::delete_data_for_user($appctxt);
+
+        provider::export_user_data($appctxt);
+        $this->assertFalse($this->get_writer($cmcontext)->has_any_data());
+    }
+}
index 2bb3c03..1bab73b 100644 (file)
@@ -159,4 +159,58 @@ class provider implements
         ];
         return $return;
     }
+
+    /**
+     * Deletes all ratings for a specified context, component, ratingarea and itemid.
+     *
+     * Only delete ratings when the item itself was deleted.
+     *
+     * We never delete ratings for one user but not others - this may affect grades, therefore ratings
+     * made by particular user are not considered personal information.
+     *
+     * @param  \context $context Details about which context to delete ratings for.
+     * @param  string $component Component to delete.
+     * @param  string $ratingarea Rating area to delete.
+     * @param  int $itemid The item ID for use with deletion.
+     */
+    public static function delete_ratings(\context $context, string $component = null,
+            string $ratingarea = null, int $itemid = null) {
+        global $DB;
+
+        $options = ['contextid' => $context->id];
+        if ($component) {
+            $options['component'] = $component;
+        }
+        if ($ratingarea) {
+            $options['ratingarea'] = $ratingarea;
+        }
+        if ($itemid) {
+            $options['itemid'] = $itemid;
+        }
+
+        $DB->delete_records('rating', $options);
+    }
+
+    /**
+     * Deletes all tag instances for given context, component, itemtype using subquery for itemids
+     *
+     * In most situations you will want to specify $userid as null. Per-user tag instances
+     * are possible in Tags API, however there are no components or standard plugins that actually use them.
+     *
+     * @param  \context $context Details about which context to delete ratings for.
+     * @param  string $component Component to delete.
+     * @param  string $ratingarea Rating area to delete.
+     * @param  string $itemidstest an SQL fragment that the itemid must match. Used
+     *      in the query like WHERE itemid $itemidstest. Must use named parameters,
+     *      and may not use named parameters called contextid, component or ratingarea.
+     * @param array $params any query params used by $itemidstest.
+     */
+    public static function delete_ratings_select(\context $context, string $component,
+             string $ratingarea, $itemidstest, $params = []) {
+        global $DB;
+        $params += ['contextid' => $context->id, 'component' => $component, 'ratingarea' => $ratingarea];
+        $DB->delete_records_select('rating',
+            'contextid = :contextid AND component = :component AND ratingarea = :ratingarea AND itemid ' . $itemidstest,
+            $params);
+    }
 }
index e68e855..7f0fb17 100644 (file)
@@ -246,6 +246,88 @@ class core_rating_privacy_testcase extends \core_privacy\tests\provider_testcase
         $this->assert_has_rating($u2, 20, $result);
     }
 
+    /**
+     * Test delete_ratings() method.
+     */
+    public function test_delete_ratings() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+
+        // Rate all courses as u1, and something else in the same context.
+        $this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25);
+        $this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50);
+        $this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75);
+        $this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99);
+        $this->rate_as_user($u1->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 10);
+
+        // Rate course2 as u2, and something else in a different context/component..
+        $this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90);
+        $this->rate_as_user($u2->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 20);
+
+        // Delete all ratings in course1.
+        $expectedratingscount = $DB->count_records('rating');
+        core_rating\privacy\provider::delete_ratings(\context_course::instance($course1->id));
+        $expectedratingscount -= 1;
+        $this->assertEquals($expectedratingscount, $DB->count_records('rating'));
+
+        // Delete ratings in course2 specifying wrong component.
+        core_rating\privacy\provider::delete_ratings(\context_course::instance($course2->id), 'other_component');
+        $this->assertEquals($expectedratingscount, $DB->count_records('rating'));
+
+        // Delete ratings in course2 specifying correct component.
+        core_rating\privacy\provider::delete_ratings(\context_course::instance($course2->id), 'core_course');
+        $expectedratingscount -= 2;
+        $this->assertEquals($expectedratingscount, $DB->count_records('rating'));
+
+        // Delete user ratings specifyng all attributes.
+        core_rating\privacy\provider::delete_ratings(\context_user::instance($u3->id), 'core_user', 'user', $u3->id);
+        $expectedratingscount -= 2;
+        $this->assertEquals($expectedratingscount, $DB->count_records('rating'));
+    }
+
+    /**
+     * Test delete_ratings_select() method.
+     */
+    public function test_delete_ratings_select() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+
+        // Rate all courses as u1, and something else in the same context.
+        $this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25);
+        $this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50);
+        $this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75);
+        $this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99);
+        $this->rate_as_user($u1->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 10);
+
+        // Rate course2 as u2, and something else in a different context/component..
+        $this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90);
+        $this->rate_as_user($u2->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 20);
+
+        // Delete ratings in course1.
+        list($sql, $params) = $DB->get_in_or_equal([$course1->id, $course2->id], SQL_PARAMS_NAMED);
+        $expectedratingscount = $DB->count_records('rating');
+        core_rating\privacy\provider::delete_ratings_select(\context_course::instance($course1->id),
+            'core_course', 'course', $sql, $params);
+        $expectedratingscount -= 1;
+        $this->assertEquals($expectedratingscount, $DB->count_records('rating'));
+    }
+
     /**
      * Assert that a user has the correct rating.
      *
index 0bd437f..3b399b1 100644 (file)
@@ -146,4 +146,52 @@ class provider implements
                 ->export_related_data($subcontext, 'tags', $tags);
         }
     }
+
+    /**
+     * Deletes all tag instances for given context, component, itemtype, itemid
+     *
+     * In most situations you will want to specify $userid as null. Per-user tag instances
+     * are possible in Tags API, however there are no components or standard plugins that actually use them.
+     *
+     * @param   \context    $context The context to export for
+     * @param   string      $component Tagarea component
+     * @param   string      $itemtype Tagarea item type
+     * @param   int         $itemid The itemid within that component and itemtype (optional)
+     * @param   int         $userid Only delete tag instances made by this user, per-user tags must be enabled for the tagarea
+     */
+    public static function delete_item_tags(\context $context, $component, $itemtype,
+            $itemid = null, $userid = null) {
+        global $DB;
+        $params = ['contextid' => $context->id, 'component' => $component, 'itemtype' => $itemtype];
+        if ($itemid) {
+            $params['itemid'] = $itemid;
+        }
+        if ($userid) {
+            $params['userid'] = $userid;
+        }
+        $DB->delete_records('tag_instance', $params);
+    }
+
+    /**
+     * Deletes all tag instances for given context, component, itemtype using subquery for itemids
+     *
+     * In most situations you will want to specify $userid as null. Per-user tag instances
+     * are possible in Tags API, however there are no components or standard plugins that actually use them.
+     *
+     * @param   \context    $context The context to export for
+     * @param   string      $component Tagarea component
+     * @param   string      $itemtype Tagarea item type
+     * @param   string      $itemidstest an SQL fragment that the itemid must match. Used
+     *      in the query like WHERE itemid $itemidstest. Must use named parameters,
+     *      and may not use named parameters called contextid, component or itemtype.
+     * @param array $params any query params used by $itemidstest.
+     */
+    public static function delete_item_tags_select(\context $context, $component, $itemtype,
+                                            $itemidstest, $params = []) {
+        global $DB;
+        $params += ['contextid' => $context->id, 'component' => $component, 'itemtype' => $itemtype];
+        $DB->delete_records_select('tag_instance',
+            'contextid = :contextid AND component = :component AND itemtype = :itemtype AND itemid ' . $itemidstest,
+            $params);
+    }
 }
index db48c38..05ed607 100644 (file)
@@ -86,4 +86,69 @@ class core_tag_privacy_testcase extends provider_testcase {
             $this->assertContains($exportedtag->rawname, $dummytags);
         }
     }
+
+    /**
+     * Test method delete_item_tags().
+     */
+    public function test_delete_item_tags() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Create a course to tag.
+        $course1 = $this->getDataGenerator()->create_course();
+        $context1 = context_course::instance($course1->id);
+        $course2 = $this->getDataGenerator()->create_course();
+        $context2 = context_course::instance($course2->id);
+
+        // Tag courses.
+        core_tag_tag::set_item_tags('core_course', 'course', $course1->id, $context1, ['Tag 1', 'Tag 2', 'Tag 3']);
+        core_tag_tag::set_item_tags('core_course', 'course', $course2->id, $context2, ['Tag 1', 'Tag 2']);
+
+        $expectedtagcount = $DB->count_records('tag_instance');
+        // Delete tags for course1.
+        core_tag\privacy\provider::delete_item_tags($context1, 'core_course', 'course');
+        $expectedtagcount -= 3;
+        $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
+
+        // Delete tags for course2. Use wrong itemid.
+        core_tag\privacy\provider::delete_item_tags($context2, 'core_course', 'course', $course1->id);
+        $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
+
+        // Use correct itemid.
+        core_tag\privacy\provider::delete_item_tags($context2, 'core_course', 'course', $course2->id);
+        $expectedtagcount -= 2;
+        $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
+    }
+
+    /**
+     * Test method delete_item_tags_select().
+     */
+    public function test_delete_item_tags_select() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Create a course to tag.
+        $course1 = $this->getDataGenerator()->create_course();
+        $context1 = context_course::instance($course1->id);
+        $course2 = $this->getDataGenerator()->create_course();
+        $context2 = context_course::instance($course2->id);
+
+        // Tag courses.
+        core_tag_tag::set_item_tags('core_course', 'course', $course1->id, $context1, ['Tag 1', 'Tag 2', 'Tag 3']);
+        core_tag_tag::set_item_tags('core_course', 'course', $course2->id, $context2, ['Tag 1', 'Tag 2']);
+
+        $expectedtagcount = $DB->count_records('tag_instance');
+        // Delete tags for course1.
+        list($sql, $params) = $DB->get_in_or_equal([$course1->id, $course2->id], SQL_PARAMS_NAMED);
+        core_tag\privacy\provider::delete_item_tags_select($context1, 'core_course', 'course', $sql, $params);
+        $expectedtagcount -= 3;
+        $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
+
+        // Delete tags for course2.
+        core_tag\privacy\provider::delete_item_tags_select($context2, 'core_course', 'course', $sql, $params);
+        $expectedtagcount -= 2;
+        $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
+    }
 }