MDL-61937 mod_data: Correct rating export args in privacy
[moodle.git] / mod / data / classes / privacy / provider.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Privacy Subsystem implementation for mod_data.
19  *
20  * @package    mod_data
21  * @copyright  2018 Marina Glancy
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace mod_data\privacy;
27 use core_privacy\local\metadata\collection;
28 use core_privacy\local\request\approved_contextlist;
29 use core_privacy\local\request\contextlist;
30 use core_privacy\local\request\helper;
31 use core_privacy\local\request\transform;
32 use core_privacy\local\request\writer;
33 use core_privacy\manager;
35 defined('MOODLE_INTERNAL') || die();
37 /**
38  * Implementation of the privacy subsystem plugin provider for the database activity module.
39  *
40  * @package    mod_data
41  * @copyright  2018 Marina Glancy
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class provider implements
45         // This plugin stores personal data.
46         \core_privacy\local\metadata\provider,
48         // This plugin is a core_user_data_provider.
49         \core_privacy\local\request\plugin\provider {
51     /** @var array stores list of records marked for deletion */
52     protected static $deletedrecords = [];
54     /**
55      * Return the fields which contain personal data.
56      *
57      * @param collection $collection a reference to the collection to use to store the metadata.
58      * @return collection the updated collection of metadata items.
59      */
60     public static function get_metadata(collection $collection) : collection {
61         $collection->add_database_table(
62             'data_records',
63             [
64                 'userid' => 'privacy:metadata:data_records:userid',
65                 'groupid' => 'privacy:metadata:data_records:groupid',
66                 'timecreated' => 'privacy:metadata:data_records:timecreated',
67                 'timemodified' => 'privacy:metadata:data_records:timemodified',
68                 'approved' => 'privacy:metadata:data_records:approved',
69             ],
70             'privacy:metadata:data_records'
71         );
72         $collection->add_database_table(
73             'data_content',
74             [
75                 'fieldid' => 'privacy:metadata:data_content:fieldid',
76                 'content' => 'privacy:metadata:data_content:content',
77                 'content1' => 'privacy:metadata:data_content:content1',
78                 'content2' => 'privacy:metadata:data_content:content2',
79                 'content3' => 'privacy:metadata:data_content:content3',
80                 'content4' => 'privacy:metadata:data_content:content4',
81             ],
82             'privacy:metadata:data_content'
83         );
85         // Link to subplugins.
86         $collection->add_plugintype_link('datafield', [], 'privacy:metadata:datafieldnpluginsummary');
88         // Subsystems used.
89         $collection->link_subsystem('core_comment', 'privacy:metadata:commentpurpose');
90         $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
91         $collection->link_subsystem('core_tag', 'privacy:metadata:tagpurpose');
92         $collection->link_subsystem('core_rating', 'privacy:metadata:ratingpurpose');
94         return $collection;
95     }
97     /**
98      * Get the list of contexts that contain user information for the specified user.
99      *
100      * @param int $userid the userid.
101      * @return contextlist the list of contexts containing user info for the user.
102      */
103     public static function get_contexts_for_userid(int $userid) : contextlist {
104         // Fetch all data records.
105         $sql = "SELECT c.id
106                   FROM {context} c
107             INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
108             INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
109             INNER JOIN {data} d ON d.id = cm.instance
110             INNER JOIN {data_records} dr ON dr.dataid = d.id
111             LEFT JOIN {comments} com ON com.commentarea=:commentarea and com.itemid = dr.id
112             LEFT JOIN {rating} r ON r.contextid = c.id AND r.itemid  = dr.id AND r.component = :moddata AND r.ratingarea = :ratingarea
113                  WHERE dr.userid = :userid OR com.userid = :userid1 OR r.userid = :userid2";
115         $params = [
116             'modname'       => 'data',
117             'contextlevel'  => CONTEXT_MODULE,
118             'userid'        => $userid,
119             'userid1'       => $userid,
120             'userid2'       => $userid,
121             'commentarea'   => 'database_entry',
122             'moddata'       => 'mod_data',
123             'ratingarea'    => 'entry',
124         ];
125         $contextlist = new contextlist();
126         $contextlist->add_from_sql($sql, $params);
128         return $contextlist;
129     }
131     /**
132      * Creates an object from all fields in the $record where key starts with $prefix
133      *
134      * @param \stdClass $record
135      * @param string $prefix
136      * @param array $additionalfields
137      * @return \stdClass
138      */
139     protected static function extract_object_from_record($record, $prefix, $additionalfields = []) {
140         $object = new \stdClass();
141         foreach ($record as $key => $value) {
142             if (preg_match('/^'.preg_quote($prefix, '/').'(.*)/', $key, $matches)) {
143                 $object->{$matches[1]} = $value;
144             }
145         }
146         if ($additionalfields) {
147             foreach ($additionalfields as $key => $value) {
148                 $object->$key = $value;
149             }
150         }
151         return $object;
152     }
154     /**
155      * Export one field answer in a record in database activity module
156      *
157      * @param \context $context
158      * @param \stdClass $recordobj record from DB table {data_records}
159      * @param \stdClass $fieldobj record from DB table {data_fields}
160      * @param \stdClass $contentobj record from DB table {data_content}
161      */
162     protected static function export_data_content($context, $recordobj, $fieldobj, $contentobj) {
163         $value = (object)[
164             'field' => [
165                 // Name and description are displayed in mod_data without applying format_string().
166                 'name' => $fieldobj->name,
167                 'description' => $fieldobj->description,
168                 'type' => $fieldobj->type,
169                 'required' => transform::yesno($fieldobj->required),
170             ],
171             'content' => $contentobj->content
172         ];
173         foreach (['content1', 'content2', 'content3', 'content4'] as $key) {
174             if ($contentobj->$key !== null) {
175                 $value->$key = $contentobj->$key;
176             }
177         }
178         $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
179         if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
180             component_class_callback($classname, 'export_data_content',
181                 [$context, $recordobj, $fieldobj, $contentobj, $value]);
182         } else {
183             // Data field plugin does not implement datafield_provider, just export default value.
184             writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $value);
185         }
186         writer::with_context($context)->export_area_files([$recordobj->id, $contentobj->id], 'mod_data',
187             'content', $contentobj->id);
188     }
190     /**
191      * SQL query that returns all fields from {data_content}, {data_fields} and {data_records} tables
192      *
193      * @return string
194      */
195     protected static function sql_fields() {
196         return 'd.id AS dataid, dc.id AS contentid, dc.fieldid, df.type AS fieldtype, df.name AS fieldname,
197                   df.description AS fielddescription, df.required AS fieldrequired,
198                   df.param1 AS fieldparam1, df.param2 AS fieldparam2, df.param3 AS fieldparam3, df.param4 AS fieldparam4,
199                   df.param5 AS fieldparam5, df.param6 AS fieldparam6, df.param7 AS fieldparam7, df.param8 AS fieldparam8,
200                   df.param9 AS fieldparam9, df.param10 AS fieldparam10,
201                   dc.content AS contentcontent, dc.content1 AS contentcontent1, dc.content2 AS contentcontent2,
202                   dc.content3 AS contentcontent3, dc.content4 AS contentcontent4,
203                   dc.recordid, dr.timecreated AS recordtimecreated, dr.timemodified AS recordtimemodified,
204                   dr.approved AS recordapproved, dr.groupid AS recordgroupid, dr.userid AS recorduserid';
205     }
207     /**
208      * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
209      *
210      * @param approved_contextlist $contextlist a list of contexts approved for export.
211      */
212     public static function export_user_data(approved_contextlist $contextlist) {
213         global $DB;
215         if (!$contextlist->count()) {
216             return;
217         }
219         $user = $contextlist->get_user();
221         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
222         $sql = "SELECT cm.id AS cmid, d.name AS dataname, cm.course AS courseid, " . self::sql_fields() . "
223                 FROM {context} ctx
224                 JOIN {course_modules} cm ON cm.id = ctx.instanceid
225                 JOIN {modules} m ON m.id = cm.module AND m.name = :modname
226                 JOIN {data} d ON d.id = cm.instance
227                 JOIN {data_records} dr ON dr.dataid = d.id
228                 JOIN {data_content} dc ON dc.recordid = dr.id
229                 JOIN {data_fields} df ON df.id = dc.fieldid
230                 WHERE ctx.id {$contextsql} AND ctx.contextlevel = :contextlevel
231                 AND dr.userid = :userid OR
232                   EXISTS (SELECT 1 FROM {comments} com WHERE com.commentarea=:commentarea
233                     AND com.itemid = dr.id AND com.userid = :userid1) OR
234                   EXISTS (SELECT 1 FROM {rating} r WHERE r.contextid = ctx.id AND r.itemid  = dr.id AND r.component = :moddata
235                     AND r.ratingarea = :ratingarea AND r.userid = :userid2)
236                 ORDER BY cm.id, dr.id, dc.fieldid";
237         $rs = $DB->get_recordset_sql($sql, $contextparams + ['contextlevel' => CONTEXT_MODULE,
238                 'modname' => 'data', 'userid' => $user->id, 'userid1' => $user->id, 'commentarea' => 'database_entry',
239                 'userid2' => $user->id, 'ratingarea' => 'entry', 'moddata' => 'mod_data']);
241         $context = null;
242         $recordobj = null;
243         foreach ($rs as $row) {
244             if (!$context || $context->instanceid != $row->cmid) {
245                 // This row belongs to the different data module than the previous row.
246                 // Export the data for the previous module.
247                 self::export_data($context, $user);
248                 // Start new data module.
249                 $context = \context_module::instance($row->cmid);
250             }
252             if (!$recordobj || $row->recordid != $recordobj->id) {
253                 // Export previous data record.
254                 self::export_data_record($context, $user, $recordobj);
255                 // Prepare for exporting new data record.
256                 $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
257             }
258             $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
259             $contentobj = self::extract_object_from_record($row, 'content',
260                 ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
261             self::export_data_content($context, $recordobj, $fieldobj, $contentobj);
262         }
263         $rs->close();
264         self::export_data_record($context, $user, $recordobj);
265         self::export_data($context, $user);
266     }
268     /**
269      * Export one entry in the database activity module (one record in {data_records} table)
270      *
271      * @param \context $context
272      * @param \stdClass $user
273      * @param \stdClass $recordobj
274      */
275     protected static function export_data_record($context, $user, $recordobj) {
276         if (!$recordobj) {
277             return;
278         }
279         $data = [
280             'userid' => transform::user($user->id),
281             'groupid' => $recordobj->groupid,
282             'timecreated' => transform::datetime($recordobj->timecreated),
283             'timemodified' => transform::datetime($recordobj->timemodified),
284             'approved' => transform::yesno($recordobj->approved),
285         ];
286         // Data about the record.
287         writer::with_context($context)->export_data([$recordobj->id], (object)$data);
288         // Related tags.
289         \core_tag\privacy\provider::export_item_tags($user->id, $context, [$recordobj->id],
290             'mod_data', 'data_records', $recordobj->id);
291         // Export comments. For records that were not made by this user export only this user's comments, for own records
292         // export comments made by everybody.
293         \core_comment\privacy\provider::export_comments($context, 'mod_data', 'database_entry', $recordobj->id,
294             [$recordobj->id], $recordobj->userid != $user->id);
295         // Export ratings. For records that were not made by this user export only this user's ratings, for own records
296         // export ratings from everybody.
297         \core_rating\privacy\provider::export_area_ratings($user->id, $context, [$recordobj->id], 'mod_data', 'entry',
298             $recordobj->id, $recordobj->userid != $user->id);
299     }
301     /**
302      * Export basic info about database activity module
303      *
304      * @param \context $context
305      * @param \stdClass $user
306      */
307     protected static function export_data($context, $user) {
308         if (!$context) {
309             return;
310         }
311         $contextdata = helper::get_context_data($context, $user);
312         helper::export_context_files($context, $user);
313         writer::with_context($context)->export_data([], $contextdata);
314     }
316     /**
317      * Delete all data for all users in the specified context.
318      *
319      * @param \context $context the context to delete in.
320      */
321     public static function delete_data_for_all_users_in_context(\context $context) {
322         global $DB;
324         if (!$context instanceof \context_module) {
325             return;
326         }
328         $sql = "SELECT " . self::sql_fields() . "
329                 FROM {course_modules} cm
330                 JOIN {modules} m ON m.id = cm.module AND m.name = :modname
331                 JOIN {data} d ON d.id = cm.instance
332                 JOIN {data_records} dr ON dr.dataid = d.id
333                 LEFT JOIN {data_content} dc ON dc.recordid = dr.id
334                 LEFT JOIN {data_fields} df ON df.id = dc.fieldid
335                 WHERE cm.id = :cmid
336                 ORDER BY dr.id";
337         $rs = $DB->get_recordset_sql($sql, ['cmid' => $context->instanceid, 'modname' => 'data']);
338         foreach ($rs as $row) {
339             self::mark_data_content_for_deletion($context, $row);
340         }
341         $rs->close();
343         self::delete_data_records($context);
344     }
346     /**
347      * Delete all user data for the specified user, in the specified contexts.
348      *
349      * @param approved_contextlist $contextlist a list of contexts approved for deletion.
350      */
351     public static function delete_data_for_user(approved_contextlist $contextlist) {
352         global $DB;
354         if (empty($contextlist->count())) {
355             return;
356         }
358         $user = $contextlist->get_user();
360         foreach ($contextlist->get_contexts() as $context) {
361             $sql = "SELECT " . self::sql_fields() . "
362                 FROM {context} ctx
363                 JOIN {course_modules} cm ON cm.id = ctx.instanceid
364                 JOIN {modules} m ON m.id = cm.module AND m.name = :modname
365                 JOIN {data} d ON d.id = cm.instance
366                 JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid = :userid
367                 LEFT JOIN {data_content} dc ON dc.recordid = dr.id
368                 LEFT JOIN {data_fields} df ON df.id = dc.fieldid
369                 WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel
370                 ORDER BY dr.id";
371             $rs = $DB->get_recordset_sql($sql, ['ctxid' => $context->id, 'contextlevel' => CONTEXT_MODULE,
372                 'modname' => 'data', 'userid' => $user->id]);
373             foreach ($rs as $row) {
374                 self::mark_data_content_for_deletion($context, $row);
375             }
376             $rs->close();
377             self::delete_data_records($context);
378         }
380         // Additionally remove comments this user made on other entries.
381         \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_data', 'entry');
383         // We do not delete ratings made by this user on other records because it may change grades.
384     }
386     /**
387      * Marks a data_record/data_content for deletion
388      *
389      * Also invokes callback from datafield plugin in case it stores additional data that needs to be deleted
390      *
391      * @param \context $context
392      * @param \stdClass $row result of SQL query - tables data_content, data_record, data_fields join together
393      */
394     protected static function mark_data_content_for_deletion($context, $row) {
395         $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
396         if ($row->contentid && $row->fieldid) {
397             $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
398             $contentobj = self::extract_object_from_record($row, 'content',
399                 ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
401             // Allow datafield plugin to implement their own deletion.
402             $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
403             if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
404                 component_class_callback($classname, 'delete_data_content',
405                     [$context, $recordobj, $fieldobj, $contentobj]);
406             }
407         }
409         self::$deletedrecords[$recordobj->id] = $recordobj->id;
410     }
412     /**
413      * Deletes records marked for deletion and all associated data
414      *
415      * Should be executed after all records were marked by {@link mark_data_content_for_deletion()}
416      *
417      * Deletes records from data_content and data_records tables, associated files, tags, comments and ratings.
418      *
419      * @param \context $context
420      */
421     protected static function delete_data_records($context) {
422         global $DB;
423         if (empty(self::$deletedrecords)) {
424             return;
425         }
427         list($sql, $params) = $DB->get_in_or_equal(self::$deletedrecords, SQL_PARAMS_NAMED);
429         // Delete files.
430         get_file_storage()->delete_area_files_select($context->id, 'mod_data', 'data_records',
431             "IN (SELECT dc.id FROM {data_content} dc WHERE dc.recordid $sql)", $params);
432         // Delete from data_content.
433         $DB->delete_records_select('data_content', 'recordid ' . $sql, $params);
434         // Delete from data_records.
435         $DB->delete_records_select('data_records', 'id ' . $sql, $params);
436         // Delete tags.
437         \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_data', 'data_records', $sql, $params);
438         // Delete comments.
439         \core_comment\privacy\provider::delete_comments_for_all_users_select($context, 'mod_data', 'entry', $sql, $params);
440         // Delete ratings.
441         \core_rating\privacy\provider::delete_ratings_select($context, 'mod_data', 'entry', $sql, $params);
443         self::$deletedrecords = [];
444     }