Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
   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   * Data provider.
  19   *
  20   * @package    mod_feedback
  21   * @copyright  2018 Frédéric Massart
  22   * @author     Frédéric Massart <fred@branchup.tech>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace mod_feedback\privacy;
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use context;
  30  use context_helper;
  31  use stdClass;
  32  use core_privacy\local\metadata\collection;
  33  use core_privacy\local\request\approved_contextlist;
  34  use core_privacy\local\request\approved_userlist;
  35  use core_privacy\local\request\contextlist;
  36  use core_privacy\local\request\helper;
  37  use core_privacy\local\request\transform;
  38  use core_privacy\local\request\userlist;
  39  use core_privacy\local\request\writer;
  40  
  41  require_once($CFG->dirroot . '/mod/feedback/lib.php');
  42  
  43  /**
  44   * Data provider class.
  45   *
  46   * @package    mod_feedback
  47   * @copyright  2018 Frédéric Massart
  48   * @author     Frédéric Massart <fred@branchup.tech>
  49   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  50   */
  51  class provider implements
  52      \core_privacy\local\metadata\provider,
  53      \core_privacy\local\request\core_userlist_provider,
  54      \core_privacy\local\request\plugin\provider {
  55  
  56      /**
  57       * Returns metadata.
  58       *
  59       * @param collection $collection The initialised collection to add items to.
  60       * @return collection A listing of user data stored through this system.
  61       */
  62      public static function get_metadata(collection $collection) : collection {
  63          $completedfields = [
  64              'userid' => 'privacy:metadata:completed:userid',
  65              'timemodified' => 'privacy:metadata:completed:timemodified',
  66              'anonymous_response' => 'privacy:metadata:completed:anonymousresponse',
  67          ];
  68  
  69          $collection->add_database_table('feedback_completed', $completedfields, 'privacy:metadata:completed');
  70          $collection->add_database_table('feedback_completedtmp', $completedfields, 'privacy:metadata:completedtmp');
  71  
  72          $valuefields = [
  73              'value' => 'privacy:metadata:value:value'
  74          ];
  75  
  76          $collection->add_database_table('feedback_value', $valuefields, 'privacy:metadata:value');
  77          $collection->add_database_table('feedback_valuetmp', $valuefields, 'privacy:metadata:valuetmp');
  78  
  79          return $collection;
  80      }
  81  
  82      /**
  83       * Get the list of contexts that contain user information for the specified user.
  84       *
  85       * @param int $userid The user to search.
  86       * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
  87       */
  88      public static function get_contexts_for_userid(int $userid) : contextlist {
  89          $sql = "
  90              SELECT DISTINCT ctx.id
  91                FROM {%s} fc
  92                JOIN {modules} m
  93                  ON m.name = :feedback
  94                JOIN {course_modules} cm
  95                  ON cm.instance = fc.feedback
  96                 AND cm.module = m.id
  97                JOIN {context} ctx
  98                  ON ctx.instanceid = cm.id
  99                 AND ctx.contextlevel = :modlevel
 100               WHERE fc.userid = :userid";
 101          $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'userid' => $userid];
 102          $contextlist = new contextlist();
 103          $contextlist->add_from_sql(sprintf($sql, 'feedback_completed'), $params);
 104          $contextlist->add_from_sql(sprintf($sql, 'feedback_completedtmp'), $params);
 105          return $contextlist;
 106      }
 107  
 108      /**
 109       * Get the list of users who have data within a context.
 110       *
 111       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 112       *
 113       */
 114      public static function get_users_in_context(userlist $userlist) {
 115          $context = $userlist->get_context();
 116  
 117          if (!is_a($context, \context_module::class)) {
 118              return;
 119          }
 120  
 121          // Find users with feedback entries.
 122          $sql = "
 123              SELECT fc.userid
 124                FROM {%s} fc
 125                JOIN {modules} m
 126                  ON m.name = :feedback
 127                JOIN {course_modules} cm
 128                  ON cm.instance = fc.feedback
 129                 AND cm.module = m.id
 130                JOIN {context} ctx
 131                  ON ctx.instanceid = cm.id
 132                 AND ctx.contextlevel = :modlevel
 133               WHERE ctx.id = :contextid";
 134          $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
 135  
 136          $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completed'), $params);
 137          $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completedtmp'), $params);
 138      }
 139  
 140      /**
 141       * Export all user data for the specified user, in the specified contexts.
 142       *
 143       * @param approved_contextlist $contextlist The approved contexts to export information for.
 144       */
 145      public static function export_user_data(approved_contextlist $contextlist) {
 146          global $DB;
 147  
 148          $user = $contextlist->get_user();
 149          $userid = $user->id;
 150          $contextids = array_map(function($context) {
 151              return $context->id;
 152          }, array_filter($contextlist->get_contexts(), function($context) {
 153              return $context->contextlevel == CONTEXT_MODULE;
 154          }));
 155  
 156          if (empty($contextids)) {
 157              return;
 158          }
 159  
 160          $flushdata = function($context, $data) use ($user) {
 161              $contextdata = helper::get_context_data($context, $user);
 162              helper::export_context_files($context, $user);
 163              $mergeddata = array_merge((array) $contextdata, (array) $data);
 164  
 165              // Drop the temporary keys.
 166              if (array_key_exists('submissions', $mergeddata)) {
 167                  $mergeddata['submissions'] = array_values($mergeddata['submissions']);
 168              }
 169  
 170              writer::with_context($context)->export_data([], (object) $mergeddata);
 171          };
 172  
 173          $lastctxid = null;
 174          $data = (object) [];
 175          list($sql, $params) = static::prepare_export_query($contextids, $userid);
 176          $recordset = $DB->get_recordset_sql($sql, $params);
 177          foreach ($recordset as $record) {
 178              if ($lastctxid && $lastctxid != $record->contextid) {
 179                  $flushdata(context::instance_by_id($lastctxid), $data);
 180                  $data = (object) [];
 181              }
 182  
 183              context_helper::preload_from_record($record);
 184              $id = ($record->istmp ? 'tmp' : 'notmp') . $record->submissionid;
 185  
 186              if (!isset($data->submissions)) {
 187                  $data->submissions = [];
 188              }
 189  
 190              if (!isset($data->submissions[$id])) {
 191                  $data->submissions[$id] = [
 192                      'inprogress' => transform::yesno($record->istmp),
 193                      'anonymousresponse' => transform::yesno($record->anonymousresponse == FEEDBACK_ANONYMOUS_YES),
 194                      'timemodified' => transform::datetime($record->timemodified),
 195                      'answers' => []
 196                  ];
 197              }
 198              $item = static::extract_item_record_from_record($record);
 199              $value = static::extract_value_record_from_record($record);
 200              $itemobj = feedback_get_item_class($record->itemtyp);
 201              $data->submissions[$id]['answers'][] = [
 202                  'question' => format_text($record->itemname, FORMAT_HTML, [
 203                      'context' => context::instance_by_id($record->contextid),
 204                      'para' => false,
 205                      'noclean' => true,
 206                  ]),
 207                  'answer' => $itemobj->get_printval($item, $value)
 208              ];
 209  
 210              $lastctxid = $record->contextid;
 211          }
 212  
 213          if (!empty($lastctxid)) {
 214              $flushdata(context::instance_by_id($lastctxid), $data);
 215          }
 216  
 217          $recordset->close();
 218      }
 219  
 220      /**
 221       * Delete all data for all users in the specified context.
 222       *
 223       * @param context $context The specific context to delete data for.
 224       */
 225      public static function delete_data_for_all_users_in_context(\context $context) {
 226          global $DB;
 227  
 228          // This should not happen, but just in case.
 229          if ($context->contextlevel != CONTEXT_MODULE) {
 230              return;
 231          }
 232  
 233          // Prepare SQL to gather all completed IDs.
 234  
 235          $completedsql = "
 236              SELECT fc.id
 237                FROM {%s} fc
 238                JOIN {modules} m
 239                  ON m.name = :feedback
 240                JOIN {course_modules} cm
 241                  ON cm.instance = fc.feedback
 242                 AND cm.module = m.id
 243               WHERE cm.id = :cmid";
 244          $completedparams = ['cmid' => $context->instanceid, 'feedback' => 'feedback'];
 245  
 246          // Delete temp answers and submissions.
 247          $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
 248          if (!empty($completedtmpids)) {
 249              list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
 250              $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
 251              $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
 252          }
 253  
 254          // Delete answers and submissions.
 255          $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
 256          if (!empty($completedids)) {
 257              list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
 258              $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
 259              $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
 260          }
 261      }
 262  
 263      /**
 264       * Delete all user data for the specified user, in the specified contexts.
 265       *
 266       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 267       */
 268      public static function delete_data_for_user(approved_contextlist $contextlist) {
 269          global $DB;
 270          $userid = $contextlist->get_user()->id;
 271  
 272          // Ensure that we only act on module contexts.
 273          $contextids = array_map(function($context) {
 274              return $context->instanceid;
 275          }, array_filter($contextlist->get_contexts(), function($context) {
 276              return $context->contextlevel == CONTEXT_MODULE;
 277          }));
 278  
 279          // Prepare SQL to gather all completed IDs.
 280          list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
 281          $completedsql = "
 282              SELECT fc.id
 283                FROM {%s} fc
 284                JOIN {modules} m
 285                  ON m.name = :feedback
 286                JOIN {course_modules} cm
 287                  ON cm.instance = fc.feedback
 288                 AND cm.module = m.id
 289               WHERE fc.userid = :userid
 290                 AND cm.id $insql";
 291          $completedparams = array_merge($inparams, ['userid' => $userid, 'feedback' => 'feedback']);
 292  
 293          // Delete all submissions in progress.
 294          $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
 295          if (!empty($completedtmpids)) {
 296              list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
 297              $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
 298              $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
 299          }
 300  
 301          // Delete all final submissions.
 302          $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
 303          if (!empty($completedids)) {
 304              list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
 305              $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
 306              $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
 307          }
 308      }
 309  
 310      /**
 311       * Delete multiple users within a single context.
 312       *
 313       * @param   approved_userlist    $userlist The approved context and user information to delete information for.
 314       */
 315      public static function delete_data_for_users(approved_userlist $userlist) {
 316          global $DB;
 317  
 318          $context = $userlist->get_context();
 319          $userids = $userlist->get_userids();
 320  
 321          // Prepare SQL to gather all completed IDs.
 322          list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 323          $completedsql = "
 324              SELECT fc.id
 325                FROM {%s} fc
 326                JOIN {modules} m
 327                  ON m.name = :feedback
 328                JOIN {course_modules} cm
 329                  ON cm.instance = fc.feedback
 330                 AND cm.module = m.id
 331               WHERE cm.id = :instanceid
 332                 AND fc.userid $insql";
 333          $completedparams = array_merge($inparams, ['instanceid' => $context->instanceid, 'feedback' => 'feedback']);
 334  
 335          // Delete all submissions in progress.
 336          $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
 337          if (!empty($completedtmpids)) {
 338              list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
 339              $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
 340              $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
 341          }
 342  
 343          // Delete all final submissions.
 344          $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
 345          if (!empty($completedids)) {
 346              list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
 347              $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
 348              $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
 349          }
 350      }
 351  
 352      /**
 353       * Extract an item record from a database record.
 354       *
 355       * @param stdClass $record The record.
 356       * @return The item record.
 357       */
 358      protected static function extract_item_record_from_record(stdClass $record) {
 359          $newrec = new stdClass();
 360          foreach ($record as $key => $value) {
 361              if (strpos($key, 'item') !== 0) {
 362                  continue;
 363              }
 364              $key = substr($key, 4);
 365              $newrec->{$key} = $value;
 366          }
 367          return $newrec;
 368      }
 369  
 370      /**
 371       * Extract a value record from a database record.
 372       *
 373       * @param stdClass $record The record.
 374       * @return The value record.
 375       */
 376      protected static function extract_value_record_from_record(stdClass $record) {
 377          $newrec = new stdClass();
 378          foreach ($record as $key => $value) {
 379              if (strpos($key, 'value') !== 0) {
 380                  continue;
 381              }
 382              $key = substr($key, 5);
 383              $newrec->{$key} = $value;
 384          }
 385          return $newrec;
 386      }
 387  
 388      /**
 389       * Prepare the query to export all data.
 390       *
 391       * Doing it this way allows for merging all records from both the temporary and final tables
 392       * as most of their columns are shared. It is a lot easier to deal with the records when
 393       * exporting as we do not need to try to manually group the two types of submissions in the
 394       * same reported dataset.
 395       *
 396       * The ordering may affect performance on large datasets.
 397       *
 398       * @param array $contextids The context IDs.
 399       * @param int $userid The user ID.
 400       * @return array With SQL and params.
 401       */
 402      protected static function prepare_export_query(array $contextids, $userid) {
 403          global $DB;
 404  
 405          $makefetchsql = function($istmp) use ($DB, $contextids, $userid) {
 406              $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
 407              list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
 408  
 409              $i = $istmp ? 0 : 1;
 410              $istmpsqlval = $istmp ? 1 : 0;
 411              $prefix = $istmp ? 'idtmp' : 'id';
 412              $uniqid = $DB->sql_concat("'$prefix'", 'fc.id');
 413  
 414              $sql = "
 415                  SELECT $uniqid AS uniqid,
 416                         f.id AS feedbackid,
 417                         ctx.id AS contextid,
 418  
 419                         $istmpsqlval AS istmp,
 420                         fc.id AS submissionid,
 421                         fc.anonymous_response AS anonymousresponse,
 422                         fc.timemodified AS timemodified,
 423  
 424                         fv.id AS valueid,
 425                         fv.course_id AS valuecourse_id,
 426                         fv.item AS valueitem,
 427                         fv.completed AS valuecompleted,
 428                         fv.tmp_completed AS valuetmp_completed,
 429  
 430                         $ctxfields
 431                    FROM {context} ctx
 432                    JOIN {course_modules} cm
 433                      ON cm.id = ctx.instanceid
 434                    JOIN {feedback} f
 435                      ON f.id = cm.instance
 436                    JOIN {%s} fc
 437                      ON fc.feedback = f.id
 438                    JOIN {%s} fv
 439                      ON fv.completed = fc.id
 440                   WHERE ctx.id $insql
 441                     AND fc.userid = :userid{$i}";
 442  
 443              $params = array_merge($inparams, [
 444                  'userid' . $i => $userid,
 445              ]);
 446  
 447              $completedtbl = $istmp ? 'feedback_completedtmp' : 'feedback_completed';
 448              $valuetbl = $istmp ? 'feedback_valuetmp' : 'feedback_value';
 449              return [sprintf($sql, $completedtbl, $valuetbl), $params];
 450          };
 451  
 452          list($nontmpsql, $nontmpparams) = $makefetchsql(false);
 453          list($tmpsql, $tmpparams) = $makefetchsql(true);
 454  
 455          // Oracle does not support UNION on text fields, therefore we must get the itemdescription
 456          // and valuevalue after doing the union by joining on the result.
 457          $sql = "
 458              SELECT q.*,
 459  
 460                     COALESCE(fv.value, fvt.value) AS valuevalue,
 461  
 462                     fi.id AS itemid,
 463                     fi.feedback AS itemfeedback,
 464                     fi.template AS itemtemplate,
 465                     fi.name AS itemname,
 466                     fi.label AS itemlabel,
 467                     fi.presentation AS itempresentation,
 468                     fi.typ AS itemtyp,
 469                     fi.hasvalue AS itemhasvalue,
 470                     fi.position AS itemposition,
 471                     fi.required AS itemrequired,
 472                     fi.dependitem AS itemdependitem,
 473                     fi.dependvalue AS itemdependvalue,
 474                     fi.options AS itemoptions
 475  
 476                FROM ($nontmpsql UNION $tmpsql) q
 477           LEFT JOIN {feedback_value} fv
 478                  ON fv.id = q.valueid AND q.istmp = 0
 479           LEFT JOIN {feedback_valuetmp} fvt
 480                  ON fvt.id = q.valueid AND q.istmp = 1
 481                JOIN {feedback_item} fi
 482                  ON (fi.id = fv.item OR fi.id = fvt.item)
 483            ORDER BY q.contextid, q.istmp, q.submissionid, q.valueid";
 484          $params = array_merge($nontmpparams, $tmpparams);
 485  
 486          return [$sql, $params];
 487      }
 488  }