Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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_assignment.
  19   *
  20   * @package    mod_assignment
  21   * @copyright  2018 Zig Tan <zig@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace mod_assignment\privacy;
  26  
  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\transform;
  31  use core_privacy\local\request\writer;
  32  use core_privacy\local\request\helper;
  33  use core_privacy\local\request\approved_userlist;
  34  use core_privacy\local\request\userlist;
  35  
  36  defined('MOODLE_INTERNAL') || die();
  37  
  38  global $CFG;
  39  require_once($CFG->dirroot . '/mod/assignment/lib.php');
  40  
  41  /**
  42   * Implementation of the privacy subsystem plugin provider for mod_assignment.
  43   *
  44   * @copyright  2018 Zig Tan <zig@moodle.com>
  45   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46   */
  47  class provider implements
  48      \core_privacy\local\metadata\provider,
  49      \core_privacy\local\request\plugin\provider,
  50      \core_privacy\local\request\user_preference_provider,
  51      \core_privacy\local\request\core_userlist_provider {
  52  
  53      /**
  54       * Return the fields which contain personal data.
  55       *
  56       * @param collection $collection a reference to the collection to use to store the metadata.
  57       * @return collection the updated collection of metadata items.
  58       */
  59      public static function get_metadata(collection $collection) : collection {
  60          $collection->add_database_table(
  61              'assignment_submissions',
  62              [
  63                  'userid' => 'privacy:metadata:assignment_submissions:userid',
  64                  'timecreated' => 'privacy:metadata:assignment_submissions:timecreated',
  65                  'timemodified' => 'privacy:metadata:assignment_submissions:timemodified',
  66                  'numfiles' => 'privacy:metadata:assignment_submissions:numfiles',
  67                  'data1' => 'privacy:metadata:assignment_submissions:data1',
  68                  'data2' => 'privacy:metadata:assignment_submissions:data2',
  69                  'grade' => 'privacy:metadata:assignment_submissions:grade',
  70                  'submissioncomment' => 'privacy:metadata:assignment_submissions:submissioncomment',
  71                  'teacher' => 'privacy:metadata:assignment_submissions:teacher',
  72                  'timemarked' => 'privacy:metadata:assignment_submissions:timemarked',
  73                  'mailed' => 'privacy:metadata:assignment_submissions:mailed'
  74              ],
  75              'privacy:metadata:assignment_submissions'
  76          );
  77  
  78          // Legacy mod_assignment preferences from Moodle 2.X.
  79          $collection->add_user_preference('assignment_filter', 'privacy:metadata:assignmentfilter');
  80          $collection->add_user_preference('assignment_mailinfo', 'privacy:metadata:assignmentmailinfo');
  81          $collection->add_user_preference('assignment_perpage', 'privacy:metadata:assignmentperpage');
  82          $collection->add_user_preference('assignment_quickgrade', 'privacy:metadata:assignmentquickgrade');
  83  
  84          return $collection;
  85      }
  86  
  87      /**
  88       * Get the list of contexts that contain user information for the specified user.
  89       *
  90       * @param int $userid the userid.
  91       * @return contextlist the list of contexts containing user info for the user.
  92       */
  93      public static function get_contexts_for_userid(int $userid) : contextlist {
  94          $contextlist = new contextlist();
  95  
  96          $sql = "SELECT DISTINCT
  97                         ctx.id
  98                    FROM {context} ctx
  99                    JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmodule
 100                    JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
 101                    JOIN {assignment} a ON cm.instance = a.id
 102                    JOIN {assignment_submissions} s ON s.assignment = a.id
 103                   WHERE s.userid = :userid
 104                      OR s.teacher = :teacher";
 105  
 106          $params = [
 107              'contextmodule'  => CONTEXT_MODULE,
 108              'modulename'    => 'assignment',
 109              'userid'        => $userid,
 110              'teacher'       => $userid
 111          ];
 112  
 113          $contextlist->add_from_sql($sql, $params);
 114  
 115          return $contextlist;
 116      }
 117  
 118      /**
 119       * Get the list of users who have data within a context.
 120       *
 121       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 122       */
 123      public static function get_users_in_context(userlist $userlist) {
 124          $context = $userlist->get_context();
 125          if ($context->contextlevel != CONTEXT_MODULE) {
 126              return;
 127          }
 128  
 129          $params = [
 130              'modulename' => 'assignment',
 131              'contextlevel' => CONTEXT_MODULE,
 132              'contextid' => $context->id
 133          ];
 134          $sql = "SELECT s.userid
 135                    FROM {assignment_submissions} s
 136                    JOIN {assignment} a ON s.assignment = a.id
 137                    JOIN {modules} m ON m.name = :modulename
 138                    JOIN {course_modules} cm ON a.id = cm.instance AND cm.module = m.id
 139                    JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
 140                   WHERE ctx.id = :contextid
 141          ";
 142          $userlist->add_from_sql('userid', $sql, $params);
 143  
 144          $sql = "SELECT s.teacher
 145                    FROM {assignment_submissions} s
 146                    JOIN {assignment} a ON s.assignment = a.id
 147                    JOIN {modules} m ON m.name = :modulename
 148                    JOIN {course_modules} cm ON a.id = cm.instance AND cm.module = m.id
 149                    JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
 150                   WHERE ctx.id = :contextid
 151          ";
 152          $userlist->add_from_sql('teacher', $sql, $params);
 153      }
 154  
 155      /**
 156       * Export personal data for the given approved_contextlist.
 157       * User and context information is contained within the contextlist.
 158       *
 159       * @param approved_contextlist $contextlist a list of contexts approved for export.
 160       */
 161      public static function export_user_data(approved_contextlist $contextlist) {
 162          if (empty($contextlist->count())) {
 163              return;
 164          }
 165  
 166          $user = $contextlist->get_user();
 167  
 168          foreach ($contextlist->get_contexts() as $context) {
 169              if ($context->contextlevel != CONTEXT_MODULE) {
 170                  continue;
 171              }
 172  
 173              // Cannot make use of helper::export_context_files(), need to manually export assignment details.
 174              $assignmentdata = self::get_assignment_by_context($context);
 175  
 176              // Get assignment details object for output.
 177              $assignment = self::get_assignment_output($assignmentdata);
 178              writer::with_context($context)->export_data([], $assignment);
 179  
 180              // Check if the user has marked any assignment's submissions to determine assignment submissions to export.
 181              $teacher = (self::has_marked_assignment_submissions($assignmentdata->id, $user->id) == true) ? true : false;
 182  
 183              // Get the assignment submissions submitted by & marked by the user for an assignment.
 184              $submissionsdata = self::get_assignment_submissions_by_assignment($assignmentdata->id, $user->id, $teacher);
 185  
 186              foreach ($submissionsdata as $submissiondata) {
 187                  // Default subcontext path to export assignment submissions submitted by the user.
 188                  $subcontexts = [
 189                      get_string('privacy:submissionpath', 'mod_assignment')
 190                  ];
 191  
 192                  if ($teacher == true) {
 193                      if ($submissiondata->teacher == $user->id) {
 194                          // Export assignment submissions that have been marked by the user.
 195                          $subcontexts = [
 196                              get_string('privacy:markedsubmissionspath', 'mod_assignment'),
 197                              transform::user($submissiondata->userid)
 198                          ];
 199                      }
 200                  }
 201  
 202                  // Get assignment submission details object for output.
 203                  $submission = self::get_assignment_submission_output($submissiondata);
 204                  $itemid = $submissiondata->id;
 205  
 206                  writer::with_context($context)
 207                      ->export_data($subcontexts, $submission)
 208                      ->export_area_files($subcontexts, 'mod_assignment', 'submission', $itemid);
 209              }
 210          }
 211      }
 212  
 213      /**
 214       * Stores the user preferences related to mod_assign.
 215       *
 216       * @param  int $userid The user ID that we want the preferences for.
 217       */
 218      public static function export_user_preferences(int $userid) {
 219          $context = \context_system::instance();
 220          $assignmentpreferences = [
 221              'assignment_filter' => [
 222                  'string' => get_string('privacy:metadata:assignmentfilter', 'mod_assignment'),
 223                  'bool' => false
 224              ],
 225              'assignment_mailinfo' => [
 226                  'string' => get_string('privacy:metadata:assignmentmailinfo', 'mod_assignment'),
 227                  'bool' => false
 228              ],
 229              'assignment_perpage' => [
 230                  'string' => get_string('privacy:metadata:assignmentperpage', 'mod_assignment'),
 231                  'bool' => false
 232              ],
 233              'assignment_quickgrade' => [
 234                  'string' => get_string('privacy:metadata:assignmentquickgrade', 'mod_assignment'),
 235                  'bool' => false
 236              ],
 237          ];
 238          foreach ($assignmentpreferences as $key => $preference) {
 239              $value = get_user_preferences($key, null, $userid);
 240              if ($preference['bool']) {
 241                  $value = transform::yesno($value);
 242              }
 243              if (isset($value)) {
 244                  writer::with_context($context)
 245                      ->export_user_preference('mod_assignment', $key, $value, $preference['string']);
 246              }
 247          }
 248      }
 249  
 250      /**
 251       * Delete all data for all users in the specified context.
 252       *
 253       * @param \context $context the context to delete in.
 254       */
 255      public static function delete_data_for_all_users_in_context(\context $context) {
 256          global $DB;
 257  
 258          if ($context->contextlevel == CONTEXT_MODULE) {
 259              // Delete all assignment submissions for the assignment associated with the context module.
 260              $assignment = self::get_assignment_by_context($context);
 261              if ($assignment != null) {
 262                  $DB->delete_records('assignment_submissions', ['assignment' => $assignment->id]);
 263  
 264                  // Delete all file uploads associated with the assignment submission for the specified context.
 265                  $fs = get_file_storage();
 266                  $fs->delete_area_files($context->id, 'mod_assignment', 'submission');
 267              }
 268          }
 269      }
 270  
 271      /**
 272       * Delete all user data for the specified user, in the specified contexts.
 273       *
 274       * @param approved_contextlist $contextlist a list of contexts approved for deletion.
 275       */
 276      public static function delete_data_for_user(approved_contextlist $contextlist) {
 277          global $DB;
 278  
 279          if (empty($contextlist->count())) {
 280              return;
 281          }
 282  
 283          $userid = $contextlist->get_user()->id;
 284  
 285          // Only retrieve assignment submissions submitted by the user for deletion.
 286          $assignmentsubmissionids = array_keys(self::get_assignment_submissions_by_contextlist($contextlist, $userid));
 287          $DB->delete_records_list('assignment_submissions', 'id', $assignmentsubmissionids);
 288  
 289          // Delete all file uploads associated with the assignment submission for the user's specified list of contexts.
 290          $fs = get_file_storage();
 291          foreach ($contextlist->get_contextids() as $contextid) {
 292              foreach ($assignmentsubmissionids as $submissionid) {
 293                  $fs->delete_area_files($contextid, 'mod_assignment', 'submission', $submissionid);
 294              }
 295          }
 296      }
 297  
 298      /**
 299       * Delete multiple users within a single context.
 300       *
 301       * @param   approved_userlist       $userlist The approved context and user information to delete information for.
 302       */
 303      public static function delete_data_for_users(approved_userlist $userlist) {
 304          global $DB;
 305  
 306          $context = $userlist->get_context();
 307          // If the context isn't for a module then return early.
 308          if ($context->contextlevel != CONTEXT_MODULE) {
 309              return;
 310          }
 311          // Fetch the assignment.
 312          $assignment = self::get_assignment_by_context($context);
 313          $userids = $userlist->get_userids();
 314  
 315          list($inorequalsql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 316          $params['assignmentid'] = $assignment->id;
 317  
 318          // Get submission ids.
 319          $sql = "
 320              SELECT s.id
 321              FROM {assignment_submissions} s
 322              JOIN {assignment} a ON s.assignment = a.id
 323              WHERE a.id = :assignmentid
 324              AND s.userid $inorequalsql
 325          ";
 326  
 327          $submissionids = $DB->get_records_sql($sql, $params);
 328          list($submissionidsql, $submissionparams) = $DB->get_in_or_equal(array_keys($submissionids), SQL_PARAMS_NAMED);
 329          $fs = get_file_storage();
 330          $fs->delete_area_files_select($context->id, 'mod_assignment', 'submission', $submissionidsql, $submissionparams);
 331          // Delete related tables.
 332          $DB->delete_records_list('assignment_submissions', 'id', array_keys($submissionids));
 333      }
 334  
 335      // Start of helper functions.
 336  
 337      /**
 338       * Helper function to check if a user has marked assignment submissions for a given assignment.
 339       *
 340       * @param int $assignmentid The assignment ID to check if user has marked associated submissions.
 341       * @param int $userid       The user ID to check if user has marked associated submissions.
 342       * @return bool             If user has marked associated submissions returns true, otherwise false.
 343       * @throws \dml_exception
 344       */
 345      protected static function has_marked_assignment_submissions($assignmentid, $userid) {
 346          global $DB;
 347  
 348          $params = [
 349              'assignment' => $assignmentid,
 350              'teacher'    => $userid
 351          ];
 352  
 353          $sql = "SELECT count(s.id) as nomarked
 354                    FROM {assignment_submissions} s
 355                   WHERE s.assignment = :assignment
 356                     AND s.teacher = :teacher";
 357  
 358          $results = $DB->get_record_sql($sql, $params);
 359  
 360          return ($results->nomarked > 0) ? true : false;
 361      }
 362  
 363      /**
 364       * Helper function to return assignment for a context module.
 365       *
 366       * @param object $context   The context module object to return the assignment record by.
 367       * @return mixed            The assignment details or null record associated with the context module.
 368       * @throws \dml_exception
 369       */
 370      protected static function get_assignment_by_context($context) {
 371          global $DB;
 372  
 373          $params = [
 374              'modulename' => 'assignment',
 375              'contextmodule' => CONTEXT_MODULE,
 376              'contextid' => $context->id
 377          ];
 378  
 379          $sql = "SELECT a.id,
 380                         a.name,
 381                         a.intro,
 382                         a.assignmenttype,
 383                         a.grade,
 384                         a.timedue,
 385                         a.timeavailable,
 386                         a.timemodified
 387                    FROM {assignment} a
 388                    JOIN {course_modules} cm ON a.id = cm.instance
 389                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
 390                    JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule
 391                   WHERE ctx.id = :contextid";
 392  
 393          return $DB->get_record_sql($sql, $params);
 394      }
 395  
 396      /**
 397       * Helper function to return assignment submissions submitted by / marked by a user and their contextlist.
 398       *
 399       * @param object $contextlist   Object with the contexts related to a userid to retrieve assignment submissions by.
 400       * @param int $userid           The user ID to find assignment submissions that were submitted by.
 401       * @param bool $teacher         The teacher status to determine if marked assignment submissions should be returned.
 402       * @return array                Array of assignment submission details.
 403       * @throws \coding_exception
 404       * @throws \dml_exception
 405       */
 406      protected static function get_assignment_submissions_by_contextlist($contextlist, $userid, $teacher = false) {
 407          global $DB;
 408  
 409          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 410  
 411          $params = [
 412              'contextmodule' => CONTEXT_MODULE,
 413              'modulename' => 'assignment',
 414              'userid' => $userid
 415          ];
 416  
 417          $sql = "SELECT s.id as id,
 418                         s.assignment as assignment,
 419                         s.numfiles as numfiles,
 420                         s.data1 as data1,
 421                         s.data2 as data2,
 422                         s.grade as grade,
 423                         s.submissioncomment as submissioncomment,
 424                         s.teacher as teacher,
 425                         s.timemarked as timemarked,
 426                         s.timecreated as timecreated,
 427                         s.timemodified as timemodified
 428                    FROM {context} ctx
 429                    JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmodule
 430                    JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
 431                    JOIN {assignment} a ON cm.instance = a.id
 432                    JOIN {assignment_submissions} s ON s.assignment = a.id
 433                   WHERE (s.userid = :userid";
 434  
 435          if ($teacher == true) {
 436              $sql .= " OR s.teacher = :teacher";
 437              $params['teacher'] = $userid;
 438          }
 439  
 440          $sql .= ")";
 441  
 442          $sql .= " AND ctx.id {$contextsql}";
 443          $params += $contextparams;
 444  
 445          return $DB->get_records_sql($sql, $params);
 446      }
 447  
 448      /**
 449       * Helper function to retrieve assignment submissions submitted by / marked by a user for a specific assignment.
 450       *
 451       * @param int $assignmentid     The assignment ID to retrieve assignment submissions by.
 452       * @param int $userid           The user ID to retrieve assignment submissions submitted / marked by.
 453       * @param bool $teacher         The teacher status to determine if marked assignment submissions should be returned.
 454       * @return array                Array of assignment submissions details.
 455       * @throws \dml_exception
 456       */
 457      protected static function get_assignment_submissions_by_assignment($assignmentid, $userid, $teacher = false) {
 458          global $DB;
 459  
 460          $params = [
 461              'assignment' => $assignmentid,
 462              'userid' => $userid
 463          ];
 464  
 465          $sql = "SELECT s.id as id,
 466                         s.assignment as assignment,
 467                         s.numfiles as numfiles,
 468                         s.data1 as data1,
 469                         s.data2 as data2,
 470                         s.grade as grade,
 471                         s.submissioncomment as submissioncomment,
 472                         s.teacher as teacher,
 473                         s.timemarked as timemarked,
 474                         s.timecreated as timecreated,
 475                         s.timemodified as timemodified,
 476                         s.userid as userid
 477                    FROM {assignment_submissions} s
 478                   WHERE s.assignment = :assignment
 479                     AND (s.userid = :userid";
 480  
 481          if ($teacher == true) {
 482              $sql .= " OR s.teacher = :teacher";
 483              $params['teacher'] = $userid;
 484          }
 485  
 486          $sql .= ")";
 487  
 488          return $DB->get_records_sql($sql, $params);
 489      }
 490  
 491      /**
 492       * Helper function generate assignment output object for exporting.
 493       *
 494       * @param object $assignmentdata    Object containing assignment data.
 495       * @return object                   Formatted assignment output object for exporting.
 496       */
 497      protected static function get_assignment_output($assignmentdata) {
 498          $assignment = (object) [
 499              'name' => $assignmentdata->name,
 500              'intro' => $assignmentdata->intro,
 501              'assignmenttype' => $assignmentdata->assignmenttype,
 502              'grade' => $assignmentdata->grade,
 503              'timemodified' => transform::datetime($assignmentdata->timemodified)
 504          ];
 505  
 506          if ($assignmentdata->timeavailable != 0) {
 507              $assignment->timeavailable = transform::datetime($assignmentdata->timeavailable);
 508          }
 509  
 510          if ($assignmentdata->timedue != 0) {
 511              $assignment->timedue = transform::datetime($assignmentdata->timedue);
 512          }
 513  
 514          return $assignment;
 515      }
 516  
 517      /**
 518       * Helper function generate assignment submission output object for exporting.
 519       *
 520       * @param object $submissiondata    Object containing assignment submission data.
 521       * @return object                   Formatted assignment submission output for exporting.
 522       */
 523      protected static function get_assignment_submission_output($submissiondata) {
 524          $submission = (object) [
 525              'assignment' => $submissiondata->assignment,
 526              'numfiles' => $submissiondata->numfiles,
 527              'data1' => $submissiondata->data1,
 528              'data2' => $submissiondata->data2,
 529              'grade' => $submissiondata->grade,
 530              'submissioncomment' => $submissiondata->submissioncomment,
 531              'teacher' => transform::user($submissiondata->teacher)
 532          ];
 533  
 534          if ($submissiondata->timecreated != 0) {
 535              $submission->timecreated = transform::datetime($submissiondata->timecreated);
 536          }
 537  
 538          if ($submissiondata->timemarked != 0) {
 539              $submission->timemarked = transform::datetime($submissiondata->timemarked);
 540          }
 541  
 542          if ($submissiondata->timemodified != 0) {
 543              $submission->timemodified = transform::datetime($submissiondata->timemodified);
 544          }
 545  
 546          return $submission;
 547      }
 548  }