| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 12 | // GNU General Public License for more details.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 14 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 15 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * Privacy Subsystem implementation for core_question.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package    core_question
 | 
        
           |  |  | 21 |  * @category   privacy
 | 
        
           |  |  | 22 |  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
 | 
        
           |  |  | 23 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 24 |  */
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 | namespace core_question\privacy;
 | 
        
           |  |  | 27 |   | 
        
           |  |  | 28 | use core_privacy\local\metadata\collection;
 | 
        
           |  |  | 29 | use core_privacy\local\request\approved_contextlist;
 | 
        
           |  |  | 30 | use core_privacy\local\request\approved_userlist;
 | 
        
           |  |  | 31 | use core_privacy\local\request\contextlist;
 | 
        
           |  |  | 32 | use core_privacy\local\request\transform;
 | 
        
           |  |  | 33 | use core_privacy\local\request\userlist;
 | 
        
           |  |  | 34 | use core_privacy\local\request\writer;
 | 
        
           |  |  | 35 |   | 
        
           |  |  | 36 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 37 |   | 
        
           |  |  | 38 | require_once($CFG->libdir . '/questionlib.php');
 | 
        
           |  |  | 39 | require_once($CFG->dirroot . '/question/format.php');
 | 
        
           |  |  | 40 | require_once($CFG->dirroot . '/question/editlib.php');
 | 
        
           |  |  | 41 | require_once($CFG->dirroot . '/question/engine/datalib.php');
 | 
        
           |  |  | 42 |   | 
        
           |  |  | 43 | /**
 | 
        
           |  |  | 44 |  * Privacy Subsystem implementation for core_question.
 | 
        
           |  |  | 45 |  *
 | 
        
           |  |  | 46 |  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
 | 
        
           |  |  | 47 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 48 |  */
 | 
        
           |  |  | 49 | class provider implements
 | 
        
           |  |  | 50 |     // This component has data.
 | 
        
           |  |  | 51 |     // We need to return all question information where the user is
 | 
        
           |  |  | 52 |     // listed in either the question.createdby or question.modifiedby fields.
 | 
        
           |  |  | 53 |     // We may also need to fetch this informtion from individual plugins in some cases.
 | 
        
           |  |  | 54 |     // e.g. to fetch the full and other question-specific meta-data.
 | 
        
           |  |  | 55 |     \core_privacy\local\metadata\provider,
 | 
        
           |  |  | 56 |   | 
        
           |  |  | 57 |     // This is a subsysytem which provides information to core.
 | 
        
           |  |  | 58 |     \core_privacy\local\request\subsystem\provider,
 | 
        
           |  |  | 59 |   | 
        
           |  |  | 60 |     // This is a subsysytem which provides information to plugins.
 | 
        
           |  |  | 61 |     \core_privacy\local\request\subsystem\plugin_provider,
 | 
        
           |  |  | 62 |   | 
        
           |  |  | 63 |     // This plugin is capable of determining which users have data within it.
 | 
        
           |  |  | 64 |     \core_privacy\local\request\core_userlist_provider,
 | 
        
           |  |  | 65 |   | 
        
           |  |  | 66 |     // This plugin is capable of determining which users have data within it for the plugins it provides data to.
 | 
        
           |  |  | 67 |     \core_privacy\local\request\shared_userlist_provider
 | 
        
           |  |  | 68 | {
 | 
        
           |  |  | 69 |   | 
        
           |  |  | 70 |     /**
 | 
        
           |  |  | 71 |      * Describe the types of data stored by the question subsystem.
 | 
        
           |  |  | 72 |      *
 | 
        
           |  |  | 73 |      * @param   collection  $items  The collection to add metadata to.
 | 
        
           |  |  | 74 |      * @return  collection  The array of metadata
 | 
        
           |  |  | 75 |      */
 | 
        
           |  |  | 76 |     public static function get_metadata(collection $items): collection {
 | 
        
           |  |  | 77 |         // Other tables link against it.
 | 
        
           |  |  | 78 |   | 
        
           |  |  | 79 |         // The 'question_usages' table does not contain any user data.
 | 
        
           |  |  | 80 |         // The table links the but doesn't store itself.
 | 
        
           |  |  | 81 |   | 
        
           |  |  | 82 |         // The 'question_attempts' table contains data about question attempts.
 | 
        
           |  |  | 83 |         // It does not contain any user ids - these are stored by the caller.
 | 
        
           |  |  | 84 |         $items->add_database_table('question_attempts', [
 | 
        
           |  |  | 85 |             'flagged'           => 'privacy:metadata:database:question_attempts:flagged',
 | 
        
           |  |  | 86 |             'responsesummary'   => 'privacy:metadata:database:question_attempts:responsesummary',
 | 
        
           |  |  | 87 |             'timemodified'      => 'privacy:metadata:database:question_attempts:timemodified',
 | 
        
           |  |  | 88 |         ], 'privacy:metadata:database:question_attempts');;
 | 
        
           |  |  | 89 |   | 
        
           |  |  | 90 |         // The 'question_attempt_steps' table contains data about changes to the state of a question attempt.
 | 
        
           |  |  | 91 |         $items->add_database_table('question_attempt_steps', [
 | 
        
           |  |  | 92 |             'state'             => 'privacy:metadata:database:question_attempt_steps:state',
 | 
        
           |  |  | 93 |             'timecreated'       => 'privacy:metadata:database:question_attempt_steps:timecreated',
 | 
        
           |  |  | 94 |             'fraction'          => 'privacy:metadata:database:question_attempt_steps:fraction',
 | 
        
           |  |  | 95 |             'userid'            => 'privacy:metadata:database:question_attempt_steps:userid',
 | 
        
           |  |  | 96 |         ], 'privacy:metadata:database:question_attempt_steps');
 | 
        
           |  |  | 97 |   | 
        
           |  |  | 98 |         // The 'question_attempt_step_data' table contains specific all metadata for each state.
 | 
        
           |  |  | 99 |         $items->add_database_table('question_attempt_step_data', [
 | 
        
           |  |  | 100 |             'name'              => 'privacy:metadata:database:question_attempt_step_data:name',
 | 
        
           |  |  | 101 |             'value'             => 'privacy:metadata:database:question_attempt_step_data:value',
 | 
        
           |  |  | 102 |         ], 'privacy:metadata:database:question_attempt_step_data');
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |         // These are all part of the set of the question definition
 | 
        
           |  |  | 105 |         // The 'question' table is used to store instances of each question.
 | 
        
           |  |  | 106 |         // It contains a createdby and modifiedby which related to specific users.
 | 
        
           |  |  | 107 |         $items->add_database_table('question', [
 | 
        
           |  |  | 108 |             'name'              => 'privacy:metadata:database:question:name',
 | 
        
           |  |  | 109 |             'questiontext'      => 'privacy:metadata:database:question:questiontext',
 | 
        
           |  |  | 110 |             'generalfeedback'   => 'privacy:metadata:database:question:generalfeedback',
 | 
        
           |  |  | 111 |             'timecreated'       => 'privacy:metadata:database:question:timecreated',
 | 
        
           |  |  | 112 |             'timemodified'      => 'privacy:metadata:database:question:timemodified',
 | 
        
           |  |  | 113 |             'createdby'         => 'privacy:metadata:database:question:createdby',
 | 
        
           |  |  | 114 |             'modifiedby'        => 'privacy:metadata:database:question:modifiedby',
 | 
        
           |  |  | 115 |         ], 'privacy:metadata:database:question');
 | 
        
           |  |  | 116 |   | 
        
           |  |  | 117 |         // The 'question_answers' table is used to store the set of answers, with appropriate feedback for each question.
 | 
        
           |  |  | 118 |         // It does not contain user data.
 | 
        
           |  |  | 119 |   | 
        
           |  |  | 120 |         // The 'question_hints' table is used to store hints about the correct answer for a question.
 | 
        
           |  |  | 121 |         // It does not contain user data.
 | 
        
           |  |  | 122 |   | 
        
           |  |  | 123 |         // The 'question_categories' table contains structural information about how questions are presented in the UI.
 | 
        
           |  |  | 124 |         // It does not contain user data.
 | 
        
           |  |  | 125 |   | 
        
           |  |  | 126 |         // The 'question_statistics' table contains aggregated statistics about responses.
 | 
        
           |  |  | 127 |         // It does not contain any identifiable user data.
 | 
        
           |  |  | 128 |   | 
        
           |  |  | 129 |         $items->add_database_table('question_bank_entries', [
 | 
        
           |  |  | 130 |             'ownerid' => 'privacy:metadata:database:question_bank_entries:ownerid',
 | 
        
           |  |  | 131 |         ], 'privacy:metadata:database:question_bank_entries');
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 |         // The question subsystem makes use of the qtype, qformat, and qbehaviour plugin types.
 | 
        
           |  |  | 134 |         $items->add_plugintype_link('qtype', [], 'privacy:metadata:link:qtype');
 | 
        
           |  |  | 135 |         $items->add_plugintype_link('qformat', [], 'privacy:metadata:link:qformat');
 | 
        
           |  |  | 136 |         $items->add_plugintype_link('qbehaviour', [], 'privacy:metadata:link:qbehaviour');
 | 
        
           |  |  | 137 |   | 
        
           |  |  | 138 |         return $items;
 | 
        
           |  |  | 139 |     }
 | 
        
           |  |  | 140 |   | 
        
           |  |  | 141 |     /**
 | 
        
           |  |  | 142 |      * Export the data for all question attempts on this question usage.
 | 
        
           |  |  | 143 |      *
 | 
        
           |  |  | 144 |      * Where a user is the owner of the usage, then the full detail of that usage will be included.
 | 
        
           |  |  | 145 |      * Where a user has been involved in the usage, but it is not their own usage, then only their specific
 | 
        
           |  |  | 146 |      * involvement will be exported.
 | 
        
           |  |  | 147 |      *
 | 
        
           |  |  | 148 |      * @param   int             $userid     The userid to export.
 | 
        
           |  |  | 149 |      * @param   \context        $context    The context that the question was used within.
 | 
        
           |  |  | 150 |      * @param   array           $usagecontext  The subcontext of this usage.
 | 
        
           |  |  | 151 |      * @param   int             $usage      The question usage ID.
 | 
        
           |  |  | 152 |      * @param   \question_display_options   $options    The display options used for formatting.
 | 
        
           |  |  | 153 |      * @param   bool            $isowner    Whether the user being exported is the user who used the question.
 | 
        
           |  |  | 154 |      */
 | 
        
           |  |  | 155 |     public static function export_question_usage(
 | 
        
           |  |  | 156 |             int $userid,
 | 
        
           |  |  | 157 |             \context $context,
 | 
        
           |  |  | 158 |             array $usagecontext,
 | 
        
           |  |  | 159 |             int $usage,
 | 
        
           |  |  | 160 |             \question_display_options $options,
 | 
        
           |  |  | 161 |             bool $isowner
 | 
        
           |  |  | 162 |         ) {
 | 
        
           |  |  | 163 |         // Determine the questions in this usage.
 | 
        
           |  |  | 164 |         $quba = \question_engine::load_questions_usage_by_activity($usage);
 | 
        
           |  |  | 165 |   | 
        
           |  |  | 166 |         $basepath = $usagecontext;
 | 
        
           |  |  | 167 |         $questionscontext = array_merge($usagecontext, [
 | 
        
           |  |  | 168 |             get_string('questions', 'core_question'),
 | 
        
           |  |  | 169 |         ]);
 | 
        
           |  |  | 170 |   | 
        
           |  |  | 171 |         foreach ($quba->get_attempt_iterator() as $qa) {
 | 
        
           | 1441 | ariadna | 172 |             $question = $qa->get_question();
 | 
        
           | 1 | efrain | 173 |             $slotno = $qa->get_slot();
 | 
        
           |  |  | 174 |             $questionnocontext = array_merge($questionscontext, [$slotno]);
 | 
        
           |  |  | 175 |   | 
        
           |  |  | 176 |             if ($isowner) {
 | 
        
           |  |  | 177 |                 // This user is the overal owner of the question attempt and all data wil therefore be exported.
 | 
        
           |  |  | 178 |                 //
 | 
        
           |  |  | 179 |                 // Respect _some_ of the question_display_options to ensure that they don't have access to
 | 
        
           |  |  | 180 |                 // generalfeedback and mark if the display options prevent this.
 | 
        
           |  |  | 181 |                 // This is defensible because they can submit questions without completing a quiz and perform an SAR to
 | 
        
           |  |  | 182 |                 // get prior access to the feedback and mark to improve upon it.
 | 
        
           |  |  | 183 |                 // Export the response.
 | 
        
           |  |  | 184 |                 $data = (object) [
 | 
        
           |  |  | 185 |                     'name' => $question->name,
 | 
        
           |  |  | 186 |                     'question' => $qa->get_question_summary(),
 | 
        
           |  |  | 187 |                     'answer' => $qa->get_response_summary(),
 | 
        
           |  |  | 188 |                     'timemodified' => transform::datetime($qa->timemodified),
 | 
        
           |  |  | 189 |                 ];
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |                 if ($options->marks >= \question_display_options::MARK_AND_MAX) {
 | 
        
           |  |  | 192 |                     $data->mark = $qa->format_mark($options->markdp);
 | 
        
           |  |  | 193 |                 }
 | 
        
           |  |  | 194 |   | 
        
           |  |  | 195 |                 if ($options->flags != \question_display_options::HIDDEN) {
 | 
        
           |  |  | 196 |                     $data->flagged = transform::yesno($qa->is_flagged());
 | 
        
           |  |  | 197 |                 }
 | 
        
           |  |  | 198 |   | 
        
           |  |  | 199 |                 if ($options->generalfeedback != \question_display_options::HIDDEN) {
 | 
        
           |  |  | 200 |                     $data->generalfeedback = $question->format_generalfeedback($qa);
 | 
        
           |  |  | 201 |                 }
 | 
        
           |  |  | 202 |   | 
        
           |  |  | 203 |                 if ($options->manualcomment != \question_display_options::HIDDEN) {
 | 
        
           |  |  | 204 |                     if ($qa->has_manual_comment()) {
 | 
        
           |  |  | 205 |                         // Note - the export of the step data will ensure that the files are exported.
 | 
        
           |  |  | 206 |                         // No need to do it again here.
 | 
        
           |  |  | 207 |                         list($comment, $commentformat, $step) = $qa->get_manual_comment();
 | 
        
           |  |  | 208 |   | 
        
           |  |  | 209 |                         $comment = writer::with_context($context)
 | 
        
           |  |  | 210 |                             ->rewrite_pluginfile_urls(
 | 
        
           |  |  | 211 |                                 $questionnocontext,
 | 
        
           |  |  | 212 |                                 'question',
 | 
        
           |  |  | 213 |                                 'response_bf_comment',
 | 
        
           |  |  | 214 |                                 $step->get_id(),
 | 
        
           |  |  | 215 |                                 $comment
 | 
        
           |  |  | 216 |                             );
 | 
        
           |  |  | 217 |                         $data->comment = $qa->get_behaviour(false)->format_comment($comment, $commentformat);
 | 
        
           |  |  | 218 |                     }
 | 
        
           |  |  | 219 |                 }
 | 
        
           |  |  | 220 |   | 
        
           |  |  | 221 |                 writer::with_context($context)
 | 
        
           |  |  | 222 |                     ->export_data($questionnocontext, $data);
 | 
        
           |  |  | 223 |   | 
        
           |  |  | 224 |                 // Export the step data.
 | 
        
           |  |  | 225 |                 static::export_question_attempt_steps($userid, $context, $questionnocontext, $qa, $options, $isowner);
 | 
        
           |  |  | 226 |             }
 | 
        
           |  |  | 227 |         }
 | 
        
           |  |  | 228 |     }
 | 
        
           |  |  | 229 |   | 
        
           |  |  | 230 |     /**
 | 
        
           |  |  | 231 |      * Export the data for each step transition for each question in each question attempt.
 | 
        
           |  |  | 232 |      *
 | 
        
           |  |  | 233 |      * Where a user is the owner of the usage, then all steps in the question usage will be exported.
 | 
        
           |  |  | 234 |      * Where a user is not the owner, but has been involved in the usage, then only their specific
 | 
        
           |  |  | 235 |      * involvement will be exported.
 | 
        
           |  |  | 236 |      *
 | 
        
           |  |  | 237 |      * @param   int                 $userid     The user to export for
 | 
        
           |  |  | 238 |      * @param   \context            $context    The context that the question was used within.
 | 
        
           |  |  | 239 |      * @param   array               $questionnocontext  The subcontext of this question number.
 | 
        
           |  |  | 240 |      * @param   \question_attempt   $qa         The attempt being checked
 | 
        
           |  |  | 241 |      * @param   \question_display_options   $options    The display options used for formatting.
 | 
        
           |  |  | 242 |      * @param   bool                $isowner    Whether the user being exported is the user who used the question.
 | 
        
           |  |  | 243 |      */
 | 
        
           |  |  | 244 |     public static function export_question_attempt_steps(
 | 
        
           |  |  | 245 |             int $userid,
 | 
        
           |  |  | 246 |             \context $context,
 | 
        
           |  |  | 247 |             array $questionnocontext,
 | 
        
           |  |  | 248 |             \question_attempt $qa,
 | 
        
           |  |  | 249 |             \question_display_options $options,
 | 
        
           |  |  | 250 |             $isowner
 | 
        
           |  |  | 251 |         ) {
 | 
        
           |  |  | 252 |         $attemptdata = (object) [
 | 
        
           |  |  | 253 |                 'steps' => [],
 | 
        
           |  |  | 254 |             ];
 | 
        
           |  |  | 255 |         $stepno = 0;
 | 
        
           |  |  | 256 |         foreach ($qa->get_step_iterator() as $i => $step) {
 | 
        
           |  |  | 257 |             $stepno++;
 | 
        
           |  |  | 258 |   | 
        
           |  |  | 259 |             if ($isowner || ($step->get_user_id() != $userid)) {
 | 
        
           |  |  | 260 |                 // The user is the owner, or the author of the step.
 | 
        
           |  |  | 261 |   | 
        
           |  |  | 262 |                 $restrictedqa = new \question_attempt_with_restricted_history($qa, $i, null);
 | 
        
           |  |  | 263 |                 $stepdata = (object) [
 | 
        
           |  |  | 264 |                     // Note: Do not include the user here.
 | 
        
           |  |  | 265 |                     'time' => transform::datetime($step->get_timecreated()),
 | 
        
           |  |  | 266 |                     'action' => $qa->summarise_action($step),
 | 
        
           |  |  | 267 |                 ];
 | 
        
           |  |  | 268 |   | 
        
           |  |  | 269 |                 if ($options->marks >= \question_display_options::MARK_AND_MAX) {
 | 
        
           |  |  | 270 |                     $stepdata->mark = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp);
 | 
        
           |  |  | 271 |                 }
 | 
        
           |  |  | 272 |   | 
        
           |  |  | 273 |                 if ($options->correctness != \question_display_options::HIDDEN) {
 | 
        
           |  |  | 274 |                     $stepdata->state = $restrictedqa->get_state_string($options->correctness);
 | 
        
           |  |  | 275 |                 }
 | 
        
           |  |  | 276 |   | 
        
           |  |  | 277 |                 if ($step->has_behaviour_var('comment')) {
 | 
        
           |  |  | 278 |                     $comment = $step->get_behaviour_var('comment');
 | 
        
           |  |  | 279 |                     $commentformat = $step->get_behaviour_var('commentformat');
 | 
        
           |  |  | 280 |   | 
        
           |  |  | 281 |                     if (empty(trim($comment))) {
 | 
        
           |  |  | 282 |                         // Skip empty comments.
 | 
        
           |  |  | 283 |                         continue;
 | 
        
           |  |  | 284 |                     }
 | 
        
           |  |  | 285 |   | 
        
           |  |  | 286 |                     // Format the comment.
 | 
        
           |  |  | 287 |                     $comment = writer::with_context($context)
 | 
        
           |  |  | 288 |                         ->rewrite_pluginfile_urls(
 | 
        
           |  |  | 289 |                             $questionnocontext,
 | 
        
           |  |  | 290 |                             'question',
 | 
        
           |  |  | 291 |                             'response_bf_comment',
 | 
        
           |  |  | 292 |                             $step->get_id(),
 | 
        
           |  |  | 293 |                             $comment
 | 
        
           |  |  | 294 |                         );
 | 
        
           |  |  | 295 |   | 
        
           |  |  | 296 |                     // Export any files associated with the comment files area.
 | 
        
           |  |  | 297 |                     writer::with_context($context)
 | 
        
           |  |  | 298 |                         ->export_area_files(
 | 
        
           |  |  | 299 |                             $questionnocontext,
 | 
        
           |  |  | 300 |                             'question',
 | 
        
           |  |  | 301 |                             "response_bf_comment",
 | 
        
           |  |  | 302 |                             $step->get_id()
 | 
        
           |  |  | 303 |                         );
 | 
        
           |  |  | 304 |   | 
        
           | 1441 | ariadna | 305 |                     $stepdata->comment = $qa->get_behaviour()->format_comment($comment, $commentformat);
 | 
        
           | 1 | efrain | 306 |                 }
 | 
        
           |  |  | 307 |   | 
        
           |  |  | 308 |                 // Export any response files associated with this step.
 | 
        
           |  |  | 309 |                 foreach (\question_engine::get_all_response_file_areas() as $filearea) {
 | 
        
           |  |  | 310 |                     writer::with_context($context)
 | 
        
           |  |  | 311 |                         ->export_area_files(
 | 
        
           |  |  | 312 |                                 $questionnocontext,
 | 
        
           |  |  | 313 |                                 'question',
 | 
        
           |  |  | 314 |                                 $filearea,
 | 
        
           |  |  | 315 |                                 $step->get_id()
 | 
        
           |  |  | 316 |                             );
 | 
        
           |  |  | 317 |                 }
 | 
        
           |  |  | 318 |   | 
        
           |  |  | 319 |                 $attemptdata->steps[$stepno] = $stepdata;
 | 
        
           |  |  | 320 |             }
 | 
        
           |  |  | 321 |         }
 | 
        
           |  |  | 322 |   | 
        
           |  |  | 323 |         if (!empty($attemptdata->steps)) {
 | 
        
           |  |  | 324 |             writer::with_context($context)
 | 
        
           |  |  | 325 |                 ->export_related_data($questionnocontext, 'steps', $attemptdata);
 | 
        
           |  |  | 326 |         }
 | 
        
           |  |  | 327 |     }
 | 
        
           |  |  | 328 |   | 
        
           |  |  | 329 |     /**
 | 
        
           |  |  | 330 |      * Get the list of contexts where the specified user has either created, or edited a question.
 | 
        
           |  |  | 331 |      *
 | 
        
           |  |  | 332 |      * To export usage of a question, please call {@link provider::export_question_usage()} from the module which
 | 
        
           |  |  | 333 |      * instantiated the usage of the question.
 | 
        
           |  |  | 334 |      *
 | 
        
           |  |  | 335 |      * @param   int             $userid The user to search.
 | 
        
           |  |  | 336 |      * @return  contextlist     $contextlist The contextlist containing the list of contexts used in this plugin.
 | 
        
           |  |  | 337 |      */
 | 
        
           |  |  | 338 |     public static function get_contexts_for_userid(int $userid): contextlist {
 | 
        
           |  |  | 339 |         $contextlist = new contextlist();
 | 
        
           |  |  | 340 |   | 
        
           |  |  | 341 |         // A user may have created or updated a question.
 | 
        
           |  |  | 342 |         // Questions are linked against a question category, which has a contextid field.
 | 
        
           |  |  | 343 |         $sql = "SELECT qc.contextid
 | 
        
           |  |  | 344 |                   FROM {question} q
 | 
        
           |  |  | 345 |                   JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 346 |                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 347 |                   JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 348 |                  WHERE q.createdby = :useridcreated
 | 
        
           |  |  | 349 |                        OR q.modifiedby = :useridmodified";
 | 
        
           |  |  | 350 |         $params = [
 | 
        
           |  |  | 351 |             'useridcreated' => $userid,
 | 
        
           |  |  | 352 |             'useridmodified' => $userid,
 | 
        
           |  |  | 353 |         ];
 | 
        
           |  |  | 354 |         $contextlist->add_from_sql($sql, $params);
 | 
        
           |  |  | 355 |   | 
        
           |  |  | 356 |         return $contextlist;
 | 
        
           |  |  | 357 |     }
 | 
        
           |  |  | 358 |   | 
        
           |  |  | 359 |     /**
 | 
        
           |  |  | 360 |      * Get the list of users who have data within a context.
 | 
        
           |  |  | 361 |      *
 | 
        
           |  |  | 362 |      * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 | 
        
           |  |  | 363 |      */
 | 
        
           |  |  | 364 |     public static function get_users_in_context(userlist $userlist) {
 | 
        
           |  |  | 365 |         $context = $userlist->get_context();
 | 
        
           |  |  | 366 |   | 
        
           |  |  | 367 |         // A user may have created or updated a question.
 | 
        
           |  |  | 368 |         // Questions are linked against a question category, which has a contextid field.
 | 
        
           |  |  | 369 |         $sql = "SELECT q.createdby, q.modifiedby
 | 
        
           |  |  | 370 |                   FROM {question} q
 | 
        
           |  |  | 371 |                   JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 372 |                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 373 |                   JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 374 |                  WHERE qc.contextid = :contextid";
 | 
        
           |  |  | 375 |   | 
        
           |  |  | 376 |         $params = [
 | 
        
           |  |  | 377 |             'contextid' => $context->id
 | 
        
           |  |  | 378 |         ];
 | 
        
           |  |  | 379 |   | 
        
           |  |  | 380 |         $userlist->add_from_sql('createdby', $sql, $params);
 | 
        
           |  |  | 381 |         $userlist->add_from_sql('modifiedby', $sql, $params);
 | 
        
           |  |  | 382 |     }
 | 
        
           |  |  | 383 |   | 
        
           |  |  | 384 |     /**
 | 
        
           |  |  | 385 |      * Determine related question usages for a user.
 | 
        
           |  |  | 386 |      *
 | 
        
           |  |  | 387 |      * @param   string          $prefix     A unique prefix to add to the table alias
 | 
        
           |  |  | 388 |      * @param   string          $component  The name of the component to fetch usages for.
 | 
        
           |  |  | 389 |      * @param   string          $joinfield  The SQL field name to use in the JOIN ON - e.g. q.usageid
 | 
        
           |  |  | 390 |      * @param   int             $userid     The user to search.
 | 
        
           |  |  | 391 |      * @return  \qubaid_join
 | 
        
           |  |  | 392 |      */
 | 
        
           |  |  | 393 |     public static function get_related_question_usages_for_user(string $prefix, string $component, string $joinfield, int $userid): \qubaid_join {
 | 
        
           |  |  | 394 |         return new \qubaid_join("
 | 
        
           |  |  | 395 |                 JOIN {question_usages} {$prefix}_qu ON {$prefix}_qu.id = {$joinfield}
 | 
        
           |  |  | 396 |                  AND {$prefix}_qu.component = :{$prefix}_usagecomponent
 | 
        
           |  |  | 397 |                 JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qa.questionusageid = {$prefix}_qu.id
 | 
        
           |  |  | 398 |                 JOIN {question_attempt_steps} {$prefix}_qas ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id",
 | 
        
           |  |  | 399 |             "{$prefix}_qu.id",
 | 
        
           |  |  | 400 |             "{$prefix}_qas.userid = :{$prefix}_stepuserid",
 | 
        
           |  |  | 401 |             [
 | 
        
           |  |  | 402 |                 "{$prefix}_stepuserid" => $userid,
 | 
        
           |  |  | 403 |                 "{$prefix}_usagecomponent" => $component,
 | 
        
           |  |  | 404 |             ]);
 | 
        
           |  |  | 405 |     }
 | 
        
           |  |  | 406 |   | 
        
           |  |  | 407 |     /**
 | 
        
           |  |  | 408 |      * Add the list of users who have rated in the specified constraints.
 | 
        
           |  |  | 409 |      *
 | 
        
           |  |  | 410 |      * @param   userlist    $userlist   The userlist to add the users to.
 | 
        
           |  |  | 411 |      * @param   string      $prefix     A unique prefix to add to the table alias to avoid interference with your own sql.
 | 
        
           |  |  | 412 |      * @param   string      $insql      The SQL to use in a sub-select for the question_usages.id query.
 | 
        
           |  |  | 413 |      * @param   array       $params     The params required for the insql.
 | 
        
           |  |  | 414 |      * @param   int|null    $contextid  An optional context id, in case the $sql query is not already filtered by that.
 | 
        
           |  |  | 415 |      */
 | 
        
           |  |  | 416 |     public static function get_users_in_context_from_sql(userlist $userlist, string $prefix, string $insql, $params,
 | 
        
           | 1441 | ariadna | 417 |             ?int $contextid = null) {
 | 
        
           | 1 | efrain | 418 |   | 
        
           |  |  | 419 |         $sql = "SELECT {$prefix}_qas.userid
 | 
        
           |  |  | 420 |                   FROM {question_attempt_steps} {$prefix}_qas
 | 
        
           |  |  | 421 |                   JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id
 | 
        
           |  |  | 422 |                   JOIN {question_usages} {$prefix}_qu ON {$prefix}_qa.questionusageid = {$prefix}_qu.id
 | 
        
           |  |  | 423 |                  WHERE {$prefix}_qu.id IN ({$insql})";
 | 
        
           |  |  | 424 |   | 
        
           |  |  | 425 |         if ($contextid) {
 | 
        
           |  |  | 426 |             $sql .= " AND {$prefix}_qu.contextid = :{$prefix}_contextid";
 | 
        
           |  |  | 427 |             $params["{$prefix}_contextid"] = $contextid;
 | 
        
           |  |  | 428 |         }
 | 
        
           |  |  | 429 |   | 
        
           |  |  | 430 |         $userlist->add_from_sql('userid', $sql, $params);
 | 
        
           |  |  | 431 |     }
 | 
        
           |  |  | 432 |   | 
        
           |  |  | 433 |     /**
 | 
        
           |  |  | 434 |      * Export all user data for the specified user, in the specified contexts.
 | 
        
           |  |  | 435 |      *
 | 
        
           |  |  | 436 |      * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
 | 
        
           |  |  | 437 |      */
 | 
        
           |  |  | 438 |     public static function export_user_data(approved_contextlist $contextlist) {
 | 
        
           |  |  | 439 |         global $CFG, $DB, $SITE;
 | 
        
           |  |  | 440 |         if (empty($contextlist)) {
 | 
        
           |  |  | 441 |             return;
 | 
        
           |  |  | 442 |         }
 | 
        
           |  |  | 443 |   | 
        
           |  |  | 444 |         // Use the Moodle XML Data format.
 | 
        
           |  |  | 445 |         // It is the only lossless format that we support.
 | 
        
           |  |  | 446 |         $format = "xml";
 | 
        
           |  |  | 447 |         require_once($CFG->dirroot . "/question/format/{$format}/format.php");
 | 
        
           |  |  | 448 |   | 
        
           |  |  | 449 |         // THe export system needs questions in a particular format.
 | 
        
           |  |  | 450 |         // The easiest way to fetch these is with get_questions_category() which takes the details of a question
 | 
        
           |  |  | 451 |         // category.
 | 
        
           |  |  | 452 |         // We fetch the root question category for each context and the get_questions_category function recurses to
 | 
        
           |  |  | 453 |         // After fetching them, we filter out any not created or modified by the requestor.
 | 
        
           |  |  | 454 |         $user = $contextlist->get_user();
 | 
        
           |  |  | 455 |         $userid = $user->id;
 | 
        
           |  |  | 456 |   | 
        
           |  |  | 457 |         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 | 
        
           |  |  | 458 |         $categories = $DB->get_records_select('question_categories', "contextid {$contextsql} AND parent = 0", $contextparams);
 | 
        
           |  |  | 459 |   | 
        
           |  |  | 460 |         $classname = "qformat_{$format}";
 | 
        
           |  |  | 461 |         foreach ($categories as $category) {
 | 
        
           |  |  | 462 |             $context = \context::instance_by_id($category->contextid);
 | 
        
           |  |  | 463 |   | 
        
           |  |  | 464 |             $questions = get_questions_category($category, true);
 | 
        
           |  |  | 465 |             $questions = array_filter($questions, function($question) use ($userid) {
 | 
        
           |  |  | 466 |                 return ($question->createdby == $userid) || ($question->modifiedby == $userid);
 | 
        
           |  |  | 467 |             }, ARRAY_FILTER_USE_BOTH);
 | 
        
           |  |  | 468 |   | 
        
           |  |  | 469 |             if (empty($questions)) {
 | 
        
           |  |  | 470 |                 continue;
 | 
        
           |  |  | 471 |             }
 | 
        
           |  |  | 472 |   | 
        
           |  |  | 473 |             $qformat = new $classname();
 | 
        
           |  |  | 474 |             $qformat->setQuestions($questions);
 | 
        
           |  |  | 475 |   | 
        
           |  |  | 476 |             $qformat->setContexts([$context]);
 | 
        
           |  |  | 477 |             $qformat->setContexttofile(true);
 | 
        
           |  |  | 478 |   | 
        
           |  |  | 479 |             // We do not know which course this belongs to, and it's not actually used except in error, so use Site.
 | 
        
           |  |  | 480 |             $qformat->setCourse($SITE);
 | 
        
           |  |  | 481 |             $content = '';
 | 
        
           |  |  | 482 |             if ($qformat->exportpreprocess()) {
 | 
        
           |  |  | 483 |                 $content = $qformat->exportprocess(false);
 | 
        
           |  |  | 484 |             }
 | 
        
           |  |  | 485 |   | 
        
           |  |  | 486 |             $subcontext = [
 | 
        
           |  |  | 487 |                 get_string('questionbank', 'core_question'),
 | 
        
           |  |  | 488 |             ];
 | 
        
           |  |  | 489 |             writer::with_context($context)->export_custom_file($subcontext, 'questions.xml', $content);
 | 
        
           |  |  | 490 |         }
 | 
        
           |  |  | 491 |     }
 | 
        
           |  |  | 492 |   | 
        
           |  |  | 493 |     /**
 | 
        
           |  |  | 494 |      * Delete all data for all users in the specified context.
 | 
        
           |  |  | 495 |      *
 | 
        
           |  |  | 496 |      * @param \context $context The specific context to delete data for.
 | 
        
           |  |  | 497 |      * @throws \dml_exception
 | 
        
           |  |  | 498 |      */
 | 
        
           |  |  | 499 |     public static function delete_data_for_all_users_in_context(\context $context) {
 | 
        
           |  |  | 500 |         global $DB;
 | 
        
           |  |  | 501 |   | 
        
           |  |  | 502 |         // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
 | 
        
           |  |  | 503 |         // user. They are still exported in the list of a users data, but they are not removed.
 | 
        
           |  |  | 504 |         // The userid is instead anonymised.
 | 
        
           |  |  | 505 |   | 
        
           |  |  | 506 |         $sql = 'SELECT q.*
 | 
        
           |  |  | 507 |                   FROM {question} q
 | 
        
           |  |  | 508 |                   JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 509 |                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 510 |                   JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 511 |                  WHERE qc.contextid = ?';
 | 
        
           |  |  | 512 |   | 
        
           |  |  | 513 |         $questions = $DB->get_records_sql($sql, [$context->id]);
 | 
        
           |  |  | 514 |         foreach ($questions as $question) {
 | 
        
           |  |  | 515 |             $question->createdby = 0;
 | 
        
           |  |  | 516 |             $question->modifiedby = 0;
 | 
        
           |  |  | 517 |             $DB->update_record('question', $question);
 | 
        
           |  |  | 518 |         }
 | 
        
           |  |  | 519 |     }
 | 
        
           |  |  | 520 |   | 
        
           |  |  | 521 |     /**
 | 
        
           |  |  | 522 |      * Delete all user data for the specified user, in the specified contexts.
 | 
        
           |  |  | 523 |      *
 | 
        
           |  |  | 524 |      * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
 | 
        
           |  |  | 525 |      */
 | 
        
           |  |  | 526 |     public static function delete_data_for_user(approved_contextlist $contextlist) {
 | 
        
           |  |  | 527 |         global $DB;
 | 
        
           |  |  | 528 |   | 
        
           |  |  | 529 |         // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
 | 
        
           |  |  | 530 |         // user. They are still exported in the list of a users data, but they are not removed.
 | 
        
           |  |  | 531 |         // The userid is instead anonymised.
 | 
        
           |  |  | 532 |   | 
        
           |  |  | 533 |         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 | 
        
           |  |  | 534 |         $contextparams['createdby'] = $contextlist->get_user()->id;
 | 
        
           |  |  | 535 |         $questiondata = $DB->get_records_sql(
 | 
        
           |  |  | 536 |             "SELECT q.*
 | 
        
           |  |  | 537 |                FROM {question} q
 | 
        
           |  |  | 538 |                JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 539 |                JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 540 |                JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 541 |               WHERE qc.contextid {$contextsql}
 | 
        
           |  |  | 542 |                     AND q.createdby = :createdby", $contextparams);
 | 
        
           |  |  | 543 |   | 
        
           |  |  | 544 |         foreach ($questiondata as $question) {
 | 
        
           |  |  | 545 |             $question->createdby = 0;
 | 
        
           |  |  | 546 |             $DB->update_record('question', $question);
 | 
        
           |  |  | 547 |         }
 | 
        
           |  |  | 548 |   | 
        
           |  |  | 549 |         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
 | 
        
           |  |  | 550 |         $contextparams['modifiedby'] = $contextlist->get_user()->id;
 | 
        
           |  |  | 551 |         $questiondata = $DB->get_records_sql(
 | 
        
           |  |  | 552 |             "SELECT q.*
 | 
        
           |  |  | 553 |                FROM {question} q
 | 
        
           |  |  | 554 |                JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 555 |                JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 556 |                JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 557 |               WHERE qc.contextid {$contextsql}
 | 
        
           |  |  | 558 |                     AND q.modifiedby = :modifiedby", $contextparams);
 | 
        
           |  |  | 559 |   | 
        
           |  |  | 560 |         foreach ($questiondata as $question) {
 | 
        
           |  |  | 561 |             $question->modifiedby = 0;
 | 
        
           |  |  | 562 |             $DB->update_record('question', $question);
 | 
        
           |  |  | 563 |         }
 | 
        
           |  |  | 564 |   | 
        
           |  |  | 565 |     }
 | 
        
           |  |  | 566 |   | 
        
           |  |  | 567 |     /**
 | 
        
           |  |  | 568 |      * Delete multiple users within a single context.
 | 
        
           |  |  | 569 |      *
 | 
        
           |  |  | 570 |      * @param   approved_userlist   $userlist   The approved context and user information to delete information for.
 | 
        
           |  |  | 571 |      */
 | 
        
           |  |  | 572 |     public static function delete_data_for_users(approved_userlist $userlist) {
 | 
        
           |  |  | 573 |         global $DB;
 | 
        
           |  |  | 574 |   | 
        
           |  |  | 575 |         // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
 | 
        
           |  |  | 576 |         // user. They are still exported in the list of a users data, but they are not removed.
 | 
        
           |  |  | 577 |         // The userid is instead anonymised.
 | 
        
           |  |  | 578 |   | 
        
           |  |  | 579 |         $context = $userlist->get_context();
 | 
        
           |  |  | 580 |         $userids = $userlist->get_userids();
 | 
        
           |  |  | 581 |   | 
        
           |  |  | 582 |         list($createdbysql, $createdbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 | 
        
           |  |  | 583 |         list($modifiedbysql, $modifiedbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 | 
        
           |  |  | 584 |   | 
        
           |  |  | 585 |         $params = ['contextid' => $context->id];
 | 
        
           |  |  | 586 |   | 
        
           |  |  | 587 |         $questiondata = $DB->get_records_sql(
 | 
        
           |  |  | 588 |             "SELECT q.*
 | 
        
           |  |  | 589 |                FROM {question} q
 | 
        
           |  |  | 590 |                JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 591 |                JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 592 |                JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 593 |               WHERE qc.contextid = :contextid
 | 
        
           |  |  | 594 |                     AND q.createdby {$createdbysql}", $params + $createdbyparams);
 | 
        
           |  |  | 595 |   | 
        
           |  |  | 596 |         foreach ($questiondata as $question) {
 | 
        
           |  |  | 597 |             $question->createdby = 0;
 | 
        
           |  |  | 598 |             $DB->update_record('question', $question);
 | 
        
           |  |  | 599 |         }
 | 
        
           |  |  | 600 |   | 
        
           |  |  | 601 |         $questiondata = $DB->get_records_sql(
 | 
        
           |  |  | 602 |             "SELECT q.*
 | 
        
           |  |  | 603 |                FROM {question} q
 | 
        
           |  |  | 604 |                JOIN {question_versions} qv ON qv.questionid = q.id
 | 
        
           |  |  | 605 |                JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 | 
        
           |  |  | 606 |                JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
 | 
        
           |  |  | 607 |               WHERE qc.contextid = :contextid
 | 
        
           |  |  | 608 |                     AND q.modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
 | 
        
           |  |  | 609 |   | 
        
           |  |  | 610 |         foreach ($questiondata as $question) {
 | 
        
           |  |  | 611 |             $question->modifiedby = 0;
 | 
        
           |  |  | 612 |             $DB->update_record('question', $question);
 | 
        
           |  |  | 613 |         }
 | 
        
           |  |  | 614 |     }
 | 
        
           |  |  | 615 | }
 |