Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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  }