Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403]

   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_survey
  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_survey\privacy;
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use context;
  30  use context_helper;
  31  use context_module;
  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\helper;
  36  use core_privacy\local\request\transform;
  37  use core_privacy\local\request\userlist;
  38  use core_privacy\local\request\writer;
  39  
  40  require_once($CFG->dirroot . '/mod/survey/lib.php');
  41  
  42  /**
  43   * Data provider class.
  44   *
  45   * @package    mod_survey
  46   * @copyright  2018 Frédéric Massart
  47   * @author     Frédéric Massart <fred@branchup.tech>
  48   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   */
  50  class provider implements
  51      \core_privacy\local\metadata\provider,
  52      \core_privacy\local\request\core_userlist_provider,
  53      \core_privacy\local\request\plugin\provider {
  54  
  55      /**
  56       * Returns metadata.
  57       *
  58       * @param collection $collection The initialised collection to add items to.
  59       * @return collection A listing of user data stored through this system.
  60       */
  61      public static function get_metadata(collection $collection) : collection {
  62          $collection->add_database_table('survey_answers', [
  63              'userid' => 'privacy:metadata:answers:userid',
  64              'question' => 'privacy:metadata:answers:question',
  65              'answer1' => 'privacy:metadata:answers:answer1',
  66              'answer2' => 'privacy:metadata:answers:answer2',
  67              'time' => 'privacy:metadata:answers:time',
  68          ], 'privacy:metadata:answers');
  69  
  70          $collection->add_database_table('survey_analysis', [
  71              'userid' => 'privacy:metadata:analysis:userid',
  72              'notes' => 'privacy:metadata:analysis:notes',
  73          ], 'privacy:metadata:analysis');
  74  
  75          return $collection;
  76      }
  77  
  78      /**
  79       * Get the list of contexts that contain user information for the specified user.
  80       *
  81       * @param int $userid The user to search.
  82       * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
  83       */
  84      public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
  85          $contextlist = new \core_privacy\local\request\contextlist();
  86  
  87          // While we should not have an analysis without answers, it's safer to gather contexts by looking at both tables.
  88          $sql = "
  89              SELECT DISTINCT ctx.id
  90                FROM {survey} s
  91                JOIN {modules} m
  92                  ON m.name = :survey
  93                JOIN {course_modules} cm
  94                  ON cm.instance = s.id
  95                 AND cm.module = m.id
  96                JOIN {context} ctx
  97                  ON ctx.instanceid = cm.id
  98                 AND ctx.contextlevel = :modulelevel
  99           LEFT JOIN {survey_answers} sa
 100                  ON sa.survey = s.id
 101                 AND sa.userid = :userid1
 102           LEFT JOIN {survey_analysis} sy
 103                  ON sy.survey = s.id
 104                 AND sy.userid = :userid2
 105               WHERE s.template <> 0
 106                 AND (sa.id IS NOT NULL
 107                  OR sy.id IS NOT NULL)";
 108  
 109          $contextlist->add_from_sql($sql, [
 110              'survey' => 'survey',
 111              'modulelevel' => CONTEXT_MODULE,
 112              'userid1' => $userid,
 113              'userid2' => $userid,
 114          ]);
 115  
 116          return $contextlist;
 117      }
 118  
 119      /**
 120       * Get the list of users who have data within a context.
 121       *
 122       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 123       */
 124      public static function get_users_in_context(userlist $userlist) {
 125          $context = $userlist->get_context();
 126  
 127          if (!is_a($context, \context_module::class)) {
 128              return;
 129          }
 130  
 131          $params = [
 132              'survey' => 'survey',
 133              'modulelevel' => CONTEXT_MODULE,
 134              'contextid' => $context->id,
 135          ];
 136  
 137          $sql = "
 138              SELECT sa.userid
 139                FROM {survey} s
 140                JOIN {modules} m
 141                  ON m.name = :survey
 142                JOIN {course_modules} cm
 143                  ON cm.instance = s.id
 144                 AND cm.module = m.id
 145                JOIN {context} ctx
 146                  ON ctx.instanceid = cm.id
 147                 AND ctx.contextlevel = :modulelevel
 148                JOIN {survey_answers} sa
 149                  ON sa.survey = s.id
 150               WHERE ctx.id = :contextid
 151                 AND s.template <> 0";
 152  
 153          $userlist->add_from_sql('userid', $sql, $params);
 154  
 155          $sql = "
 156              SELECT sy.userid
 157                FROM {survey} s
 158                JOIN {modules} m
 159                  ON m.name = :survey
 160                JOIN {course_modules} cm
 161                  ON cm.instance = s.id
 162                 AND cm.module = m.id
 163                JOIN {context} ctx
 164                  ON ctx.instanceid = cm.id
 165                 AND ctx.contextlevel = :modulelevel
 166                JOIN {survey_analysis} sy
 167                  ON sy.survey = s.id
 168               WHERE ctx.id = :contextid
 169                 AND s.template <> 0";
 170  
 171          $userlist->add_from_sql('userid', $sql, $params);
 172      }
 173  
 174      /**
 175       * Export all user data for the specified user, in the specified contexts.
 176       *
 177       * @param approved_contextlist $contextlist The approved contexts to export information for.
 178       */
 179      public static function export_user_data(approved_contextlist $contextlist) {
 180          global $DB;
 181  
 182          $user = $contextlist->get_user();
 183          $userid = $user->id;
 184          $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 185              if ($context->contextlevel == CONTEXT_MODULE) {
 186                  $carry[] = $context->instanceid;
 187              }
 188              return $carry;
 189          }, []);
 190  
 191          if (empty($cmids)) {
 192              return;
 193          }
 194  
 195          // Export the answers.
 196          list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
 197          $sql = "
 198              SELECT sa.*,
 199                     sq.id as qid,
 200                     sq.text as qtext,
 201                     sq.shorttext as qshorttext,
 202                     sq.intro as qintro,
 203                     sq.options as qoptions,
 204                     sq.type as qtype,
 205                     cm.id as cmid
 206                FROM {survey_answers} sa
 207                JOIN {survey_questions} sq
 208                  ON sq.id = sa.question
 209                JOIN {survey} s
 210                  ON s.id = sa.survey
 211                JOIN {modules} m
 212                  ON m.name = :survey
 213                JOIN {course_modules} cm
 214                  ON cm.instance = s.id
 215                 AND cm.module = m.id
 216               WHERE cm.id $insql
 217                 AND sa.userid = :userid
 218            ORDER BY s.id, sq.id";
 219          $params = array_merge($inparams, ['survey' => 'survey', 'userid' => $userid]);
 220  
 221          $recordset = $DB->get_recordset_sql($sql, $params);
 222          static::recordset_loop_and_export($recordset, 'cmid', [], function($carry, $record) {
 223              $q = survey_translate_question((object) [
 224                  'text' => $record->qtext,
 225                  'shorttext' => $record->qshorttext,
 226                  'intro' => $record->qintro,
 227                  'options' => $record->qoptions
 228              ]);
 229              $qtype = $record->qtype;
 230              $options = explode(',', $q->options ?? '');
 231  
 232              $carry[] = [
 233                  'question' => array_merge((array) $q, [
 234                      'options' => $qtype > 0 ? $options : '-'
 235                  ]),
 236                  'answer' => [
 237                      'actual' => $qtype > 0 && !empty($record->answer1) ? $options[$record->answer1 - 1] : $record->answer1,
 238                      'preferred' => $qtype > 0 && !empty($record->answer2) ? $options[$record->answer2 - 1] : $record->answer2,
 239                  ],
 240                  'time' => transform::datetime($record->time),
 241              ];
 242              return $carry;
 243  
 244          }, function($cmid, $data) use ($user) {
 245              $context = context_module::instance($cmid);
 246              $contextdata = helper::get_context_data($context, $user);
 247              $contextdata = (object) array_merge((array) $contextdata, ['answers' => $data]);
 248              helper::export_context_files($context, $user);
 249              writer::with_context($context)->export_data([], $contextdata);
 250          });
 251  
 252          // Export the analysis.
 253          $sql = "
 254              SELECT sy.*, cm.id as cmid
 255                FROM {survey_analysis} sy
 256                JOIN {survey} s
 257                  ON s.id = sy.survey
 258                JOIN {modules} m
 259                  ON m.name = :survey
 260                JOIN {course_modules} cm
 261                  ON cm.instance = s.id
 262                 AND cm.module = m.id
 263               WHERE cm.id $insql
 264                 AND sy.userid = :userid
 265            ORDER BY s.id";
 266          $params = array_merge($inparams, ['survey' => 'survey', 'userid' => $userid]);
 267  
 268          $recordset = $DB->get_recordset_sql($sql, $params);
 269          static::recordset_loop_and_export($recordset, 'cmid', null, function($carry, $record) {
 270              $carry = ['notes' => $record->notes];
 271              return $carry;
 272          }, function($cmid, $data) {
 273              $context = context_module::instance($cmid);
 274              writer::with_context($context)->export_related_data([], 'survey_analysis', (object) $data);
 275          });
 276      }
 277  
 278      /**
 279       * Delete all data for all users in the specified context.
 280       *
 281       * @param context $context The specific context to delete data for.
 282       */
 283      public static function delete_data_for_all_users_in_context(context $context) {
 284          global $DB;
 285  
 286          if ($context->contextlevel != CONTEXT_MODULE) {
 287              return;
 288          }
 289  
 290          if ($surveyid = static::get_survey_id_from_context($context)) {
 291              $DB->delete_records('survey_answers', ['survey' => $surveyid]);
 292              $DB->delete_records('survey_analysis', ['survey' => $surveyid]);
 293          }
 294      }
 295  
 296      /**
 297       * Delete all user data for the specified user, in the specified contexts.
 298       *
 299       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 300       */
 301      public static function delete_data_for_user(approved_contextlist $contextlist) {
 302          global $DB;
 303  
 304          $userid = $contextlist->get_user()->id;
 305          $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 306              if ($context->contextlevel == CONTEXT_MODULE) {
 307                  $carry[] = $context->instanceid;
 308              }
 309              return $carry;
 310          }, []);
 311          if (empty($cmids)) {
 312              return;
 313          }
 314  
 315          // Fetch the survey IDs.
 316          list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
 317          $sql = "
 318              SELECT s.id
 319                FROM {survey} s
 320                JOIN {modules} m
 321                  ON m.name = :survey
 322                JOIN {course_modules} cm
 323                  ON cm.instance = s.id
 324                 AND cm.module = m.id
 325               WHERE cm.id $insql";
 326          $params = array_merge($inparams, ['survey' => 'survey']);
 327          $surveyids = $DB->get_fieldset_sql($sql, $params);
 328  
 329          // Delete all the things.
 330          list($insql, $inparams) = $DB->get_in_or_equal($surveyids, SQL_PARAMS_NAMED);
 331          $params = array_merge($inparams, ['userid' => $userid]);
 332          $DB->delete_records_select('survey_answers', "survey $insql AND userid = :userid", $params);
 333          $DB->delete_records_select('survey_analysis', "survey $insql AND userid = :userid", $params);
 334      }
 335  
 336      /**
 337       * Delete multiple users within a single context.
 338       *
 339       * @param   approved_userlist       $userlist The approved context and user information to delete information for.
 340       */
 341      public static function delete_data_for_users(approved_userlist $userlist) {
 342          global $DB;
 343          $context = $userlist->get_context();
 344  
 345          if ($context->contextlevel != CONTEXT_MODULE) {
 346              return;
 347          }
 348  
 349          // Fetch the survey ID.
 350          $sql = "
 351              SELECT s.id
 352                FROM {survey} s
 353                JOIN {modules} m
 354                  ON m.name = :survey
 355                JOIN {course_modules} cm
 356                  ON cm.instance = s.id
 357                 AND cm.module = m.id
 358               WHERE cm.id = :cmid";
 359          $params = [
 360              'survey' => 'survey',
 361              'cmid' => $context->instanceid,
 362              ];
 363          $surveyid = $DB->get_field_sql($sql, $params);
 364          $userids = $userlist->get_userids();
 365  
 366          // Delete all the things.
 367          list($insql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 368          $params['surveyid'] = $surveyid;
 369  
 370          $DB->delete_records_select('survey_answers', "survey = :surveyid AND userid {$insql}", $params);
 371          $DB->delete_records_select('survey_analysis', "survey = :surveyid AND userid {$insql}", $params);
 372      }
 373  
 374      /**
 375       * Get a survey ID from its context.
 376       *
 377       * @param context_module $context The module context.
 378       * @return int
 379       */
 380      protected static function get_survey_id_from_context(context_module $context) {
 381          $cm = get_coursemodule_from_id('survey', $context->instanceid);
 382          return $cm ? (int) $cm->instance : 0;
 383      }
 384      /**
 385       * Loop and export from a recordset.
 386       *
 387       * @param moodle_recordset $recordset The recordset.
 388       * @param string $splitkey The record key to determine when to export.
 389       * @param mixed $initial The initial data to reduce from.
 390       * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
 391       * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
 392       * @return void
 393       */
 394      protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
 395              callable $reducer, callable $export) {
 396  
 397          $data = $initial;
 398          $lastid = null;
 399  
 400          foreach ($recordset as $record) {
 401              if ($lastid && $record->{$splitkey} != $lastid) {
 402                  $export($lastid, $data);
 403                  $data = $initial;
 404              }
 405              $data = $reducer($data, $record);
 406              $lastid = $record->{$splitkey};
 407          }
 408          $recordset->close();
 409  
 410          if (!empty($lastid)) {
 411              $export($lastid, $data);
 412          }
 413      }
 414  }