b2c36a8c3a386c00acb7ca7e9433078bacf8dd0f
[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     /**
52      * Return the fields which contain personal data.
53      *
54      * @param collection $collection a reference to the collection to use to store the metadata.
55      * @return collection the updated collection of metadata items.
56      */
57     public static function get_metadata(collection $collection) : collection {
58         $collection->add_database_table(
59             'data_records',
60             [
61                 'userid' => 'privacy:metadata:data_records:userid',
62                 'groupid' => 'privacy:metadata:data_records:groupid',
63                 'timecreated' => 'privacy:metadata:data_records:timecreated',
64                 'timemodified' => 'privacy:metadata:data_records:timemodified',
65                 'approved' => 'privacy:metadata:data_records:approved',
66             ],
67             'privacy:metadata:data_records'
68         );
69         $collection->add_database_table(
70             'data_content',
71             [
72                 'fieldid' => 'privacy:metadata:data_content:fieldid',
73                 'content' => 'privacy:metadata:data_content:content',
74                 'content1' => 'privacy:metadata:data_content:content1',
75                 'content2' => 'privacy:metadata:data_content:content2',
76                 'content3' => 'privacy:metadata:data_content:content3',
77                 'content4' => 'privacy:metadata:data_content:content4',
78             ],
79             'privacy:metadata:data_content'
80         );
82         // Link to subplugins.
83         $collection->add_plugintype_link('datafield', [], 'privacy:metadata:datafieldnpluginsummary');
85         // Subsystems used.
86         $collection->link_subsystem('core_comment', 'privacy:metadata:commentpurpose');
87         $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
88         $collection->link_subsystem('core_tag', 'privacy:metadata:tagpurpose');
89         $collection->link_subsystem('core_rating', 'privacy:metadata:ratingpurpose');
91         return $collection;
92     }
94     /**
95      * Get the list of contexts that contain user information for the specified user.
96      *
97      * @param int $userid the userid.
98      * @return contextlist the list of contexts containing user info for the user.
99      */
100     public static function get_contexts_for_userid(int $userid) : contextlist {
101         // Fetch all data records.
102         $sql = "SELECT c.id
103                   FROM {context} c
104             INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
105             INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
106             INNER JOIN {data} d ON d.id = cm.instance
107             INNER JOIN {data_records} dr ON dr.dataid = d.id
108              LEFT JOIN {comments} com ON com.commentarea=:commentarea and com.itemid = dr.id AND com.userid = :userid1
109              LEFT JOIN {rating} r ON r.contextid = c.id AND r.itemid  = dr.id AND r.component = :moddata
110                        AND r.ratingarea = :ratingarea AND r.userid = :userid2
111                  WHERE dr.userid = :userid OR com.id IS NOT NULL OR r.id IS NOT NULL";
113         $params = [
114             'modname'       => 'data',
115             'contextlevel'  => CONTEXT_MODULE,
116             'userid'        => $userid,
117             'userid1'       => $userid,
118             'userid2'       => $userid,
119             'commentarea'   => 'database_entry',
120             'moddata'       => 'mod_data',
121             'ratingarea'    => 'entry',
122         ];
123         $contextlist = new contextlist();
124         $contextlist->add_from_sql($sql, $params);
126         return $contextlist;
127     }
129     /**
130      * Creates an object from all fields in the $record where key starts with $prefix
131      *
132      * @param \stdClass $record
133      * @param string $prefix
134      * @param array $additionalfields
135      * @return \stdClass
136      */
137     protected static function extract_object_from_record($record, $prefix, $additionalfields = []) {
138         $object = new \stdClass();
139         foreach ($record as $key => $value) {
140             if (preg_match('/^'.preg_quote($prefix, '/').'(.*)/', $key, $matches)) {
141                 $object->{$matches[1]} = $value;
142             }
143         }
144         if ($additionalfields) {
145             foreach ($additionalfields as $key => $value) {
146                 $object->$key = $value;
147             }
148         }
149         return $object;
150     }
152     /**
153      * Export one field answer in a record in database activity module
154      *
155      * @param \context $context
156      * @param \stdClass $recordobj record from DB table {data_records}
157      * @param \stdClass $fieldobj record from DB table {data_fields}
158      * @param \stdClass $contentobj record from DB table {data_content}
159      */
160     protected static function export_data_content($context, $recordobj, $fieldobj, $contentobj) {
161         $value = (object)[
162             'field' => [
163                 // Name and description are displayed in mod_data without applying format_string().
164                 'name' => $fieldobj->name,
165                 'description' => $fieldobj->description,
166                 'type' => $fieldobj->type,
167                 'required' => transform::yesno($fieldobj->required),
168             ],
169             'content' => $contentobj->content
170         ];
171         foreach (['content1', 'content2', 'content3', 'content4'] as $key) {
172             if ($contentobj->$key !== null) {
173                 $value->$key = $contentobj->$key;
174             }
175         }
176         $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
177         if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
178             component_class_callback($classname, 'export_data_content',
179                 [$context, $recordobj, $fieldobj, $contentobj, $value]);
180         } else {
181             // Data field plugin does not implement datafield_provider, just export default value.
182             writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $value);
183         }
184         writer::with_context($context)->export_area_files([$recordobj->id, $contentobj->id], 'mod_data',
185             'content', $contentobj->id);
186     }
188     /**
189      * SQL query that returns all fields from {data_content}, {data_fields} and {data_records} tables
190      *
191      * @return string
192      */
193     protected static function sql_fields() {
194         return 'd.id AS dataid, dc.id AS contentid, dc.fieldid, df.type AS fieldtype, df.name AS fieldname,
195                   df.description AS fielddescription, df.required AS fieldrequired,
196                   df.param1 AS fieldparam1, df.param2 AS fieldparam2, df.param3 AS fieldparam3, df.param4 AS fieldparam4,
197                   df.param5 AS fieldparam5, df.param6 AS fieldparam6, df.param7 AS fieldparam7, df.param8 AS fieldparam8,
198                   df.param9 AS fieldparam9, df.param10 AS fieldparam10,
199                   dc.content AS contentcontent, dc.content1 AS contentcontent1, dc.content2 AS contentcontent2,
200                   dc.content3 AS contentcontent3, dc.content4 AS contentcontent4,
201                   dc.recordid, dr.timecreated AS recordtimecreated, dr.timemodified AS recordtimemodified,
202                   dr.approved AS recordapproved, dr.groupid AS recordgroupid, dr.userid AS recorduserid';
203     }
205     /**
206      * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
207      *
208      * @param approved_contextlist $contextlist a list of contexts approved for export.
209      */
210     public static function export_user_data(approved_contextlist $contextlist) {
211         global $DB;
213         if (!$contextlist->count()) {
214             return;
215         }
217         $user = $contextlist->get_user();
219         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
220         $sql = "SELECT cm.id AS cmid, d.name AS dataname, cm.course AS courseid, " . self::sql_fields() . "
221                 FROM {context} ctx
222                 JOIN {course_modules} cm ON cm.id = ctx.instanceid
223                 JOIN {modules} m ON m.id = cm.module AND m.name = :modname
224                 JOIN {data} d ON d.id = cm.instance
225                 JOIN {data_records} dr ON dr.dataid = d.id
226                 JOIN {data_content} dc ON dc.recordid = dr.id
227                 JOIN {data_fields} df ON df.id = dc.fieldid
228                 WHERE ctx.id {$contextsql} AND ctx.contextlevel = :contextlevel
229                 AND dr.userid = :userid OR
230                   EXISTS (SELECT 1 FROM {comments} com WHERE com.commentarea=:commentarea
231                     AND com.itemid = dr.id AND com.userid = :userid1) OR
232                   EXISTS (SELECT 1 FROM {rating} r WHERE r.contextid = ctx.id AND r.itemid  = dr.id AND r.component = :moddata
233                     AND r.ratingarea = :ratingarea AND r.userid = :userid2)
234                 ORDER BY cm.id, dr.id, dc.fieldid";
235         $rs = $DB->get_recordset_sql($sql, $contextparams + ['contextlevel' => CONTEXT_MODULE,
236                 'modname' => 'data', 'userid' => $user->id, 'userid1' => $user->id, 'commentarea' => 'database_entry',
237                 'userid2' => $user->id, 'ratingarea' => 'entry', 'moddata' => 'mod_data']);
239         $context = null;
240         $recordobj = null;
241         foreach ($rs as $row) {
242             if (!$context || $context->instanceid != $row->cmid) {
243                 // This row belongs to the different data module than the previous row.
244                 // Export the data for the previous module.
245                 self::export_data($context, $user);
246                 // Start new data module.
247                 $context = \context_module::instance($row->cmid);
248             }
250             if (!$recordobj || $row->recordid != $recordobj->id) {
251                 // Export previous data record.
252                 self::export_data_record($context, $user, $recordobj);
253                 // Prepare for exporting new data record.
254                 $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
255             }
256             $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
257             $contentobj = self::extract_object_from_record($row, 'content',
258                 ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
259             self::export_data_content($context, $recordobj, $fieldobj, $contentobj);
260         }
261         $rs->close();
262         self::export_data_record($context, $user, $recordobj);
263         self::export_data($context, $user);
264     }
266     /**
267      * Export one entry in the database activity module (one record in {data_records} table)
268      *
269      * @param \context $context
270      * @param \stdClass $user
271      * @param \stdClass $recordobj
272      */
273     protected static function export_data_record($context, $user, $recordobj) {
274         if (!$recordobj) {
275             return;
276         }
277         $data = [
278             'userid' => transform::user($user->id),
279             'groupid' => $recordobj->groupid,
280             'timecreated' => transform::datetime($recordobj->timecreated),
281             'timemodified' => transform::datetime($recordobj->timemodified),
282             'approved' => transform::yesno($recordobj->approved),
283         ];
284         // Data about the record.
285         writer::with_context($context)->export_data([$recordobj->id], (object)$data);
286         // Related tags.
287         \core_tag\privacy\provider::export_item_tags($user->id, $context, [$recordobj->id],
288             'mod_data', 'data_records', $recordobj->id);
289         // Export comments. For records that were not made by this user export only this user's comments, for own records
290         // export comments made by everybody.
291         \core_comment\privacy\provider::export_comments($context, 'mod_data', 'database_entry', $recordobj->id,
292             [$recordobj->id], $recordobj->userid != $user->id);
293         // Export ratings. For records that were not made by this user export only this user's ratings, for own records
294         // export ratings from everybody.
295         \core_rating\privacy\provider::export_area_ratings($user->id, $context, [$recordobj->id], 'mod_data', 'entry',
296             $recordobj->id, $recordobj->userid != $user->id);
297     }
299     /**
300      * Export basic info about database activity module
301      *
302      * @param \context $context
303      * @param \stdClass $user
304      */
305     protected static function export_data($context, $user) {
306         if (!$context) {
307             return;
308         }
309         $contextdata = helper::get_context_data($context, $user);
310         helper::export_context_files($context, $user);
311         writer::with_context($context)->export_data([], $contextdata);
312     }
314     /**
315      * Delete all data for all users in the specified context.
316      *
317      * @param \context $context the context to delete in.
318      */
319     public static function delete_data_for_all_users_in_context(\context $context) {
320         global $DB;
322         if (!$context instanceof \context_module) {
323             return;
324         }
325         $recordstobedeleted = [];
327         $sql = "SELECT " . self::sql_fields() . "
328                 FROM {course_modules} cm
329                 JOIN {modules} m ON m.id = cm.module AND m.name = :modname
330                 JOIN {data} d ON d.id = cm.instance
331                 JOIN {data_records} dr ON dr.dataid = d.id
332                 LEFT JOIN {data_content} dc ON dc.recordid = dr.id
333                 LEFT JOIN {data_fields} df ON df.id = dc.fieldid
334                 WHERE cm.id = :cmid
335                 ORDER BY dr.id";
336         $rs = $DB->get_recordset_sql($sql, ['cmid' => $context->instanceid, 'modname' => 'data']);
337         foreach ($rs as $row) {
338             self::mark_data_content_for_deletion($context, $row);
339             $recordstobedeleted[$row->recordid] = $row->recordid;
340         }
341         $rs->close();
343         self::delete_data_records($context, $recordstobedeleted);
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();
359         $recordstobedeleted = [];
361         foreach ($contextlist->get_contexts() as $context) {
362             $sql = "SELECT " . self::sql_fields() . "
363                 FROM {context} ctx
364                 JOIN {course_modules} cm ON cm.id = ctx.instanceid
365                 JOIN {modules} m ON m.id = cm.module AND m.name = :modname
366                 JOIN {data} d ON d.id = cm.instance
367                 JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid = :userid
368                 LEFT JOIN {data_content} dc ON dc.recordid = dr.id
369                 LEFT JOIN {data_fields} df ON df.id = dc.fieldid
370                 WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel
371                 ORDER BY dr.id";
372             $rs = $DB->get_recordset_sql($sql, ['ctxid' => $context->id, 'contextlevel' => CONTEXT_MODULE,
373                 'modname' => 'data', 'userid' => $user->id]);
374             foreach ($rs as $row) {
375                 self::mark_data_content_for_deletion($context, $row);
376                 $recordstobedeleted[$row->recordid] = $row->recordid;
377             }
378             $rs->close();
379             self::delete_data_records($context, $recordstobedeleted);
380         }
382         // Additionally remove comments this user made on other entries.
383         \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_data', 'database_entry');
385         // We do not delete ratings made by this user on other records because it may change grades.
386     }
388     /**
389      * Marks a data_record/data_content for deletion
390      *
391      * Also invokes callback from datafield plugin in case it stores additional data that needs to be deleted
392      *
393      * @param \context $context
394      * @param \stdClass $row result of SQL query - tables data_content, data_record, data_fields join together
395      */
396     protected static function mark_data_content_for_deletion($context, $row) {
397         $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
398         if ($row->contentid && $row->fieldid) {
399             $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
400             $contentobj = self::extract_object_from_record($row, 'content',
401                 ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
403             // Allow datafield plugin to implement their own deletion.
404             $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
405             if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
406                 component_class_callback($classname, 'delete_data_content',
407                     [$context, $recordobj, $fieldobj, $contentobj]);
408             }
409         }
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      * @param array $recordstobedeleted list of ids of the data records that need to be deleted
421      */
422     protected static function delete_data_records($context, $recordstobedeleted) {
423         global $DB;
424         if (empty($recordstobedeleted)) {
425             return;
426         }
428         list($sql, $params) = $DB->get_in_or_equal($recordstobedeleted, SQL_PARAMS_NAMED);
430         // Delete files.
431         get_file_storage()->delete_area_files_select($context->id, 'mod_data', 'data_records',
432             "IN (SELECT dc.id FROM {data_content} dc WHERE dc.recordid $sql)", $params);
433         // Delete from data_content.
434         $DB->delete_records_select('data_content', 'recordid ' . $sql, $params);
435         // Delete from data_records.
436         $DB->delete_records_select('data_records', 'id ' . $sql, $params);
437         // Delete tags.
438         \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_data', 'data_records', $sql, $params);
439         // Delete comments.
440         \core_comment\privacy\provider::delete_comments_for_all_users_select($context, 'mod_data', 'database_entry', $sql, $params);
441         // Delete ratings.
442         \core_rating\privacy\provider::delete_ratings_select($context, 'mod_data', 'entry', $sql, $params);
443     }