Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
25
namespace mod_data\privacy;
26
 
27
use core_privacy\local\metadata\collection;
28
use core_privacy\local\request\approved_contextlist;
29
use core_privacy\local\request\approved_userlist;
30
use core_privacy\local\request\contextlist;
31
use core_privacy\local\request\helper;
32
use core_privacy\local\request\transform;
33
use core_privacy\local\request\userlist;
34
use core_privacy\local\request\writer;
35
use core_privacy\manager;
36
 
37
defined('MOODLE_INTERNAL') || die();
38
 
39
/**
40
 * Implementation of the privacy subsystem plugin provider for the database activity module.
41
 *
42
 * @package    mod_data
43
 * @copyright  2018 Marina Glancy
44
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45
 */
46
class provider implements
47
        // This plugin stores personal data.
48
        \core_privacy\local\metadata\provider,
49
 
50
        // This plugin is capable of determining which users have data within it.
51
        \core_privacy\local\request\core_userlist_provider,
52
 
53
        // This plugin is a core_user_data_provider.
54
        \core_privacy\local\request\plugin\provider {
55
 
56
    /**
57
     * Return the fields which contain personal data.
58
     *
59
     * @param collection $collection a reference to the collection to use to store the metadata.
60
     * @return collection the updated collection of metadata items.
61
     */
62
    public static function get_metadata(collection $collection): collection {
63
        $collection->add_database_table(
64
            'data_records',
65
            [
66
                'userid' => 'privacy:metadata:data_records:userid',
67
                'groupid' => 'privacy:metadata:data_records:groupid',
68
                'timecreated' => 'privacy:metadata:data_records:timecreated',
69
                'timemodified' => 'privacy:metadata:data_records:timemodified',
70
                'approved' => 'privacy:metadata:data_records:approved',
71
            ],
72
            'privacy:metadata:data_records'
73
        );
74
        $collection->add_database_table(
75
            'data_content',
76
            [
77
                'fieldid' => 'privacy:metadata:data_content:fieldid',
78
                'content' => 'privacy:metadata:data_content:content',
79
                'content1' => 'privacy:metadata:data_content:content1',
80
                'content2' => 'privacy:metadata:data_content:content2',
81
                'content3' => 'privacy:metadata:data_content:content3',
82
                'content4' => 'privacy:metadata:data_content:content4',
83
            ],
84
            'privacy:metadata:data_content'
85
        );
86
 
87
        // Link to subplugins.
88
        $collection->add_plugintype_link('datafield', [], 'privacy:metadata:datafieldnpluginsummary');
89
 
90
        // Subsystems used.
91
        $collection->link_subsystem('core_comment', 'privacy:metadata:commentpurpose');
92
        $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
93
        $collection->link_subsystem('core_tag', 'privacy:metadata:tagpurpose');
94
        $collection->link_subsystem('core_rating', 'privacy:metadata:ratingpurpose');
95
 
96
        return $collection;
97
    }
98
 
99
    /**
100
     * Get the list of contexts that contain user information for the specified user.
101
     *
102
     * @param int $userid the userid.
103
     * @return contextlist the list of contexts containing user info for the user.
104
     */
105
    public static function get_contexts_for_userid(int $userid): contextlist {
106
        $contextlist = new contextlist();
107
 
108
        // Fetch all data records that the user rote.
109
        $sql = "SELECT c.id
110
                  FROM {context} c
111
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
112
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
113
                  JOIN {data} d ON d.id = cm.instance
114
                  JOIN {data_records} dr ON dr.dataid = d.id
115
                 WHERE dr.userid = :userid";
116
 
117
        $params = [
118
            'contextlevel'  => CONTEXT_MODULE,
119
            'modname'       => 'data',
120
            'userid'        => $userid,
121
        ];
122
        $contextlist->add_from_sql($sql, $params);
123
 
124
        // Fetch contexts where the user commented.
125
        $sql = "SELECT c.id
126
                  FROM {context} c
127
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
128
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
129
                  JOIN {data} d ON d.id = cm.instance
130
                  JOIN {data_records} dr ON dr.dataid = d.id
131
                  JOIN {comments} com ON com.commentarea = :commentarea and com.itemid = dr.id
132
                 WHERE com.userid = :userid";
133
 
134
        $params = [
135
            'contextlevel'  => CONTEXT_MODULE,
136
            'modname'       => 'data',
137
            'commentarea'   => 'database_entry',
138
            'userid'        => $userid,
139
        ];
140
        $contextlist->add_from_sql($sql, $params);
141
 
142
        // Fetch all data records.
143
        $ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_data', 'entry', 'dr.id', $userid, true);
144
        $sql = "SELECT c.id
145
                  FROM {context} c
146
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
147
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
148
                  JOIN {data} d ON d.id = cm.instance
149
                  JOIN {data_records} dr ON dr.dataid = d.id
150
            {$ratingquery->join}
151
                 WHERE {$ratingquery->userwhere}";
152
 
153
        $params = [
154
            'contextlevel'  => CONTEXT_MODULE,
155
            'modname'       => 'data',
156
        ] + $ratingquery->params;
157
        $contextlist->add_from_sql($sql, $params);
158
 
159
        return $contextlist;
160
    }
161
 
162
    /**
163
     * Get the list of users who have data within a context.
164
     *
165
     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
166
     *
167
     */
168
    public static function get_users_in_context(userlist $userlist) {
169
        $context = $userlist->get_context();
170
 
171
        if (!is_a($context, \context_module::class)) {
172
            return;
173
        }
174
 
175
        // Find users with data records.
176
        $sql = "SELECT dr.userid
177
                  FROM {context} c
178
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
179
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
180
                  JOIN {data} d ON d.id = cm.instance
181
                  JOIN {data_records} dr ON dr.dataid = d.id
182
                 WHERE c.id = :contextid";
183
 
184
        $params = [
185
            'modname'       => 'data',
186
            'contextid'     => $context->id,
187
            'contextlevel'  => CONTEXT_MODULE,
188
        ];
189
 
190
        $userlist->add_from_sql('userid', $sql, $params);
191
 
192
        // Find users with comments.
193
        \core_comment\privacy\provider::get_users_in_context_from_sql($userlist, 'com', 'mod_data', 'database_entry', $context->id);
194
 
195
        // Find users with ratings.
196
        $sql = "SELECT dr.id
197
                  FROM {context} c
198
                  JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
199
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
200
                  JOIN {data} d ON d.id = cm.instance
201
                  JOIN {data_records} dr ON dr.dataid = d.id
202
                 WHERE c.id = :contextid";
203
 
204
        $params = [
205
            'modname'       => 'data',
206
            'contextid'     => $context->id,
207
            'contextlevel'  => CONTEXT_MODULE,
208
        ];
209
 
210
        \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_data', 'entry', $sql, $params);
211
    }
212
 
213
    /**
214
     * Creates an object from all fields in the $record where key starts with $prefix
215
     *
216
     * @param \stdClass $record
217
     * @param string $prefix
218
     * @param array $additionalfields
219
     * @return \stdClass
220
     */
221
    protected static function extract_object_from_record($record, $prefix, $additionalfields = []) {
222
        $object = new \stdClass();
223
        foreach ($record as $key => $value) {
224
            if (preg_match('/^'.preg_quote($prefix, '/').'(.*)/', $key, $matches)) {
225
                $object->{$matches[1]} = $value;
226
            }
227
        }
228
        if ($additionalfields) {
229
            foreach ($additionalfields as $key => $value) {
230
                $object->$key = $value;
231
            }
232
        }
233
        return $object;
234
    }
235
 
236
    /**
237
     * Export one field answer in a record in database activity module
238
     *
239
     * @param \context $context
240
     * @param \stdClass $recordobj record from DB table {data_records}
241
     * @param \stdClass $fieldobj record from DB table {data_fields}
242
     * @param \stdClass $contentobj record from DB table {data_content}
243
     */
244
    protected static function export_data_content($context, $recordobj, $fieldobj, $contentobj) {
245
        $value = (object)[
246
            'field' => [
247
                // Name and description are displayed in mod_data without applying format_string().
248
                'name' => $fieldobj->name,
249
                'description' => $fieldobj->description,
250
                'type' => $fieldobj->type,
251
                'required' => transform::yesno($fieldobj->required),
252
            ],
253
            'content' => $contentobj->content
254
        ];
255
        foreach (['content1', 'content2', 'content3', 'content4'] as $key) {
256
            if ($contentobj->$key !== null) {
257
                $value->$key = $contentobj->$key;
258
            }
259
        }
260
        $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
261
        if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
262
            component_class_callback($classname, 'export_data_content',
263
                [$context, $recordobj, $fieldobj, $contentobj, $value]);
264
        } else {
265
            // Data field plugin does not implement datafield_provider, just export default value.
266
            writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $value);
267
        }
268
        writer::with_context($context)->export_area_files([$recordobj->id, $contentobj->id], 'mod_data',
269
            'content', $contentobj->id);
270
    }
271
 
272
    /**
273
     * SQL query that returns all fields from {data_content}, {data_fields} and {data_records} tables
274
     *
275
     * @return string
276
     */
277
    protected static function sql_fields() {
278
        return 'd.id AS dataid, dc.id AS contentid, dc.fieldid, df.type AS fieldtype, df.name AS fieldname,
279
                  df.description AS fielddescription, df.required AS fieldrequired,
280
                  df.param1 AS fieldparam1, df.param2 AS fieldparam2, df.param3 AS fieldparam3, df.param4 AS fieldparam4,
281
                  df.param5 AS fieldparam5, df.param6 AS fieldparam6, df.param7 AS fieldparam7, df.param8 AS fieldparam8,
282
                  df.param9 AS fieldparam9, df.param10 AS fieldparam10,
283
                  dc.content AS contentcontent, dc.content1 AS contentcontent1, dc.content2 AS contentcontent2,
284
                  dc.content3 AS contentcontent3, dc.content4 AS contentcontent4,
285
                  dc.recordid, dr.timecreated AS recordtimecreated, dr.timemodified AS recordtimemodified,
286
                  dr.approved AS recordapproved, dr.groupid AS recordgroupid, dr.userid AS recorduserid';
287
    }
288
 
289
    /**
290
     * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
291
     *
292
     * @param approved_contextlist $contextlist a list of contexts approved for export.
293
     */
294
    public static function export_user_data(approved_contextlist $contextlist) {
295
        global $DB;
296
 
297
        if (!$contextlist->count()) {
298
            return;
299
        }
300
 
301
        $user = $contextlist->get_user();
302
 
303
        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
304
        $sql = "SELECT cm.id AS cmid, d.name AS dataname, cm.course AS courseid, " . self::sql_fields() . "
305
                FROM {context} ctx
306
                JOIN {course_modules} cm ON cm.id = ctx.instanceid
307
                JOIN {modules} m ON m.id = cm.module AND m.name = :modname
308
                JOIN {data} d ON d.id = cm.instance
309
                JOIN {data_records} dr ON dr.dataid = d.id
310
                JOIN {data_content} dc ON dc.recordid = dr.id
311
                JOIN {data_fields} df ON df.id = dc.fieldid
312
                WHERE ctx.id {$contextsql} AND ctx.contextlevel = :contextlevel
313
                AND dr.userid = :userid OR
314
                  EXISTS (SELECT 1 FROM {comments} com WHERE com.commentarea=:commentarea
315
                    AND com.itemid = dr.id AND com.userid = :userid1) OR
316
                  EXISTS (SELECT 1 FROM {rating} r WHERE r.contextid = ctx.id AND r.itemid  = dr.id AND r.component = :moddata
317
                    AND r.ratingarea = :ratingarea AND r.userid = :userid2)
318
                ORDER BY cm.id, dr.id, dc.fieldid";
319
        $rs = $DB->get_recordset_sql($sql, $contextparams + ['contextlevel' => CONTEXT_MODULE,
320
                'modname' => 'data', 'userid' => $user->id, 'userid1' => $user->id, 'commentarea' => 'database_entry',
321
                'userid2' => $user->id, 'ratingarea' => 'entry', 'moddata' => 'mod_data']);
322
 
323
        $context = null;
324
        $recordobj = null;
325
        foreach ($rs as $row) {
326
            if (!$context || $context->instanceid != $row->cmid) {
327
                // This row belongs to the different data module than the previous row.
328
                // Export the data for the previous module.
329
                self::export_data($context, $user);
330
                // Start new data module.
331
                $context = \context_module::instance($row->cmid);
332
            }
333
 
334
            if (!$recordobj || $row->recordid != $recordobj->id) {
335
                // Export previous data record.
336
                self::export_data_record($context, $user, $recordobj);
337
                // Prepare for exporting new data record.
338
                $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
339
            }
340
            $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
341
            $contentobj = self::extract_object_from_record($row, 'content',
342
                ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
343
            self::export_data_content($context, $recordobj, $fieldobj, $contentobj);
344
        }
345
        $rs->close();
346
        self::export_data_record($context, $user, $recordobj);
347
        self::export_data($context, $user);
348
    }
349
 
350
    /**
351
     * Export one entry in the database activity module (one record in {data_records} table)
352
     *
353
     * @param \context $context
354
     * @param \stdClass $user
355
     * @param \stdClass $recordobj
356
     */
357
    protected static function export_data_record($context, $user, $recordobj) {
358
        if (!$recordobj) {
359
            return;
360
        }
361
        $data = [
362
            'userid' => transform::user($user->id),
363
            'groupid' => $recordobj->groupid,
364
            'timecreated' => transform::datetime($recordobj->timecreated),
365
            'timemodified' => transform::datetime($recordobj->timemodified),
366
            'approved' => transform::yesno($recordobj->approved),
367
        ];
368
        // Data about the record.
369
        writer::with_context($context)->export_data([$recordobj->id], (object)$data);
370
        // Related tags.
371
        \core_tag\privacy\provider::export_item_tags($user->id, $context, [$recordobj->id],
372
            'mod_data', 'data_records', $recordobj->id);
373
        // Export comments. For records that were not made by this user export only this user's comments, for own records
374
        // export comments made by everybody.
375
        \core_comment\privacy\provider::export_comments($context, 'mod_data', 'database_entry', $recordobj->id,
376
            [$recordobj->id], $recordobj->userid != $user->id);
377
        // Export ratings. For records that were not made by this user export only this user's ratings, for own records
378
        // export ratings from everybody.
379
        \core_rating\privacy\provider::export_area_ratings($user->id, $context, [$recordobj->id], 'mod_data', 'entry',
380
            $recordobj->id, $recordobj->userid != $user->id);
381
    }
382
 
383
    /**
384
     * Export basic info about database activity module
385
     *
386
     * @param \context $context
387
     * @param \stdClass $user
388
     */
389
    protected static function export_data($context, $user) {
390
        if (!$context) {
391
            return;
392
        }
393
        $contextdata = helper::get_context_data($context, $user);
394
        helper::export_context_files($context, $user);
395
        writer::with_context($context)->export_data([], $contextdata);
396
    }
397
 
398
    /**
399
     * Delete all data for all users in the specified context.
400
     *
401
     * @param \context $context the context to delete in.
402
     */
403
    public static function delete_data_for_all_users_in_context(\context $context) {
404
        global $DB;
405
 
406
        if (!$context instanceof \context_module) {
407
            return;
408
        }
409
        $recordstobedeleted = [];
410
 
411
        $sql = "SELECT " . self::sql_fields() . "
412
                FROM {course_modules} cm
413
                JOIN {modules} m ON m.id = cm.module AND m.name = :modname
414
                JOIN {data} d ON d.id = cm.instance
415
                JOIN {data_records} dr ON dr.dataid = d.id
416
                LEFT JOIN {data_content} dc ON dc.recordid = dr.id
417
                LEFT JOIN {data_fields} df ON df.id = dc.fieldid
418
                WHERE cm.id = :cmid
419
                ORDER BY dr.id";
420
        $rs = $DB->get_recordset_sql($sql, ['cmid' => $context->instanceid, 'modname' => 'data']);
421
        foreach ($rs as $row) {
422
            self::mark_data_content_for_deletion($context, $row);
423
            $recordstobedeleted[$row->recordid] = $row->recordid;
424
        }
425
        $rs->close();
426
 
427
        self::delete_data_records($context, $recordstobedeleted);
428
    }
429
 
430
    /**
431
     * Delete all user data for the specified user, in the specified contexts.
432
     *
433
     * @param approved_contextlist $contextlist a list of contexts approved for deletion.
434
     */
435
    public static function delete_data_for_user(approved_contextlist $contextlist) {
436
        global $DB;
437
 
438
        if (empty($contextlist->count())) {
439
            return;
440
        }
441
 
442
        $user = $contextlist->get_user();
443
        $recordstobedeleted = [];
444
 
445
        foreach ($contextlist->get_contexts() as $context) {
446
            $sql = "SELECT " . self::sql_fields() . "
447
                FROM {context} ctx
448
                JOIN {course_modules} cm ON cm.id = ctx.instanceid
449
                JOIN {modules} m ON m.id = cm.module AND m.name = :modname
450
                JOIN {data} d ON d.id = cm.instance
451
                JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid = :userid
452
                LEFT JOIN {data_content} dc ON dc.recordid = dr.id
453
                LEFT JOIN {data_fields} df ON df.id = dc.fieldid
454
                WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel
455
                ORDER BY dr.id";
456
            $rs = $DB->get_recordset_sql($sql, ['ctxid' => $context->id, 'contextlevel' => CONTEXT_MODULE,
457
                'modname' => 'data', 'userid' => $user->id]);
458
            foreach ($rs as $row) {
459
                self::mark_data_content_for_deletion($context, $row);
460
                $recordstobedeleted[$row->recordid] = $row->recordid;
461
            }
462
            $rs->close();
463
            self::delete_data_records($context, $recordstobedeleted);
464
        }
465
 
466
        // Additionally remove comments this user made on other entries.
467
        \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_data', 'database_entry');
468
 
469
        // We do not delete ratings made by this user on other records because it may change grades.
470
    }
471
 
472
    /**
473
     * Delete multiple users within a single context.
474
     *
475
     * @param   approved_userlist    $userlist The approved context and user information to delete information for.
476
     */
477
    public static function delete_data_for_users(approved_userlist $userlist) {
478
        global $DB;
479
 
480
        $context = $userlist->get_context();
481
        $recordstobedeleted = [];
482
        list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
483
 
484
        $sql = "SELECT " . self::sql_fields() . "
485
                  FROM {context} ctx
486
                  JOIN {course_modules} cm ON cm.id = ctx.instanceid
487
                  JOIN {modules} m ON m.id = cm.module AND m.name = :modname
488
                  JOIN {data} d ON d.id = cm.instance
489
                  JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid {$userinsql}
490
             LEFT JOIN {data_content} dc ON dc.recordid = dr.id
491
             LEFT JOIN {data_fields} df ON df.id = dc.fieldid
492
                 WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel
493
              ORDER BY dr.id";
494
 
495
        $params = [
496
            'ctxid' => $context->id,
497
            'contextlevel' => CONTEXT_MODULE,
498
            'modname' => 'data',
499
        ];
500
        $params += $userinparams;
501
 
502
        $rs = $DB->get_recordset_sql($sql, $params);
503
        foreach ($rs as $row) {
504
            self::mark_data_content_for_deletion($context, $row);
505
            $recordstobedeleted[$row->recordid] = $row->recordid;
506
        }
507
        $rs->close();
508
 
509
        self::delete_data_records($context, $recordstobedeleted);
510
 
511
        // Additionally remove comments these users made on other entries.
512
        \core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_data', 'database_entry');
513
 
514
        // We do not delete ratings made by users on other records because it may change grades.
515
    }
516
 
517
    /**
518
     * Marks a data_record/data_content for deletion
519
     *
520
     * Also invokes callback from datafield plugin in case it stores additional data that needs to be deleted
521
     *
522
     * @param \context $context
523
     * @param \stdClass $row result of SQL query - tables data_content, data_record, data_fields join together
524
     */
525
    protected static function mark_data_content_for_deletion($context, $row) {
526
        $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]);
527
        if ($row->contentid && $row->fieldid) {
528
            $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]);
529
            $contentobj = self::extract_object_from_record($row, 'content',
530
                ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]);
531
 
532
            // Allow datafield plugin to implement their own deletion.
533
            $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type);
534
            if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) {
535
                component_class_callback($classname, 'delete_data_content',
536
                    [$context, $recordobj, $fieldobj, $contentobj]);
537
            }
538
        }
539
    }
540
 
541
    /**
542
     * Deletes records marked for deletion and all associated data
543
     *
544
     * Should be executed after all records were marked by {@link mark_data_content_for_deletion()}
545
     *
546
     * Deletes records from data_content and data_records tables, associated files, tags, comments and ratings.
547
     *
548
     * @param \context $context
549
     * @param array $recordstobedeleted list of ids of the data records that need to be deleted
550
     */
551
    protected static function delete_data_records($context, $recordstobedeleted) {
552
        global $DB;
553
        if (empty($recordstobedeleted)) {
554
            return;
555
        }
556
 
557
        list($sql, $params) = $DB->get_in_or_equal($recordstobedeleted, SQL_PARAMS_NAMED);
558
 
559
        // Delete files.
560
        get_file_storage()->delete_area_files_select($context->id, 'mod_data', 'data_records',
561
            "IN (SELECT dc.id FROM {data_content} dc WHERE dc.recordid $sql)", $params);
562
        // Delete from data_content.
563
        $DB->delete_records_select('data_content', 'recordid ' . $sql, $params);
564
        // Delete from data_records.
565
        $DB->delete_records_select('data_records', 'id ' . $sql, $params);
566
        // Delete tags.
567
        \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_data', 'data_records', $sql, $params);
568
        // Delete comments.
569
        \core_comment\privacy\provider::delete_comments_for_all_users_select($context, 'mod_data', 'database_entry', $sql, $params);
570
        // Delete ratings.
571
        \core_rating\privacy\provider::delete_ratings_select($context, 'mod_data', 'entry', $sql, $params);
572
    }
573
}