MDL-61937 mod_data: Correct rating export args in privacy
[moodle.git] / mod / data / classes / privacy / provider.php
CommitLineData
ab803d69
MG
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/>.
16
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 */
24
25namespace mod_data\privacy;
26
27use core_privacy\local\metadata\collection;
28use core_privacy\local\request\approved_contextlist;
29use core_privacy\local\request\contextlist;
30use core_privacy\local\request\helper;
31use core_privacy\local\request\transform;
32use core_privacy\local\request\writer;
33use core_privacy\manager;
34
35defined('MOODLE_INTERNAL') || die();
36
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 */
44class provider implements
45 // This plugin stores personal data.
46 \core_privacy\local\metadata\provider,
47
48 // This plugin is a core_user_data_provider.
49 \core_privacy\local\request\plugin\provider {
50
51 /** @var array stores list of records marked for deletion */
52 protected static $deletedrecords = [];
53
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 );
84
85 // Link to subplugins.
86 $collection->add_plugintype_link('datafield', [], 'privacy:metadata:datafieldnpluginsummary');
87
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');
93
94 return $collection;
95 }
96
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";
114
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);
127
128 return $contextlist;
129 }
130
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 }
153
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 }
189
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 }
206
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;
214
215 if (!$contextlist->count()) {
216 return;
217 }
218
219 $user = $contextlist->get_user();
220
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']);
240
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 }
251
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 }
267
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.
ea80f967 297 \core_rating\privacy\provider::export_area_ratings($user->id, $context, [$recordobj->id], 'mod_data', 'entry',
ab803d69
MG
298 $recordobj->id, $recordobj->userid != $user->id);
299 }
300
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 }
315
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;
323
324 if (!$context instanceof \context_module) {
325 return;
326 }
327
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();
342
343 self::delete_data_records($context);
344 }
345
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;
353
354 if (empty($contextlist->count())) {
355 return;
356 }
357
358 $user = $contextlist->get_user();
359
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 }
379
380 // Additionally remove comments this user made on other entries.
381 \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_data', 'entry');
382
383 // We do not delete ratings made by this user on other records because it may change grades.
384 }
385
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]);
400
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 }
408
409 self::$deletedrecords[$recordobj->id] = $recordobj->id;
410 }
411
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 }
426
427 list($sql, $params) = $DB->get_in_or_equal(self::$deletedrecords, SQL_PARAMS_NAMED);
428
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);
442
443 self::$deletedrecords = [];
444 }
445}