MDL-42266 Lib: Allow additional max upload file sizes
[moodle.git] / mod / quiz / classes / external.php
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/>.
17 /**
18  * Quiz external API
19  *
20  * @package    mod_quiz
21  * @category   external
22  * @copyright  2016 Juan Leyva <juan@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      Moodle 3.1
25  */
27 defined('MOODLE_INTERNAL') || die;
29 require_once($CFG->libdir . '/externallib.php');
30 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
32 /**
33  * Quiz external functions
34  *
35  * @package    mod_quiz
36  * @category   external
37  * @copyright  2016 Juan Leyva <juan@moodle.com>
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  * @since      Moodle 3.1
40  */
41 class mod_quiz_external extends external_api {
43     /**
44      * Describes the parameters for get_quizzes_by_courses.
45      *
46      * @return external_external_function_parameters
47      * @since Moodle 3.1
48      */
49     public static function get_quizzes_by_courses_parameters() {
50         return new external_function_parameters (
51             array(
52                 'courseids' => new external_multiple_structure(
53                     new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
54                 ),
55             )
56         );
57     }
59     /**
60      * Returns a list of quizzes in a provided list of courses,
61      * if no list is provided all quizzes that the user can view will be returned.
62      *
63      * @param array $courseids Array of course ids
64      * @return array of quizzes details
65      * @since Moodle 3.1
66      */
67     public static function get_quizzes_by_courses($courseids = array()) {
68         global $USER;
70         $warnings = array();
71         $returnedquizzes = array();
73         $params = array(
74             'courseids' => $courseids,
75         );
76         $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
78         $mycourses = array();
79         if (empty($params['courseids'])) {
80             $mycourses = enrol_get_my_courses();
81             $params['courseids'] = array_keys($mycourses);
82         }
84         // Ensure there are courseids to loop through.
85         if (!empty($params['courseids'])) {
87             list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
89             // Get the quizzes in this course, this function checks users visibility permissions.
90             // We can avoid then additional validate_context calls.
91             $quizzes = get_all_instances_in_courses("quiz", $courses);
92             foreach ($quizzes as $quiz) {
93                 $context = context_module::instance($quiz->coursemodule);
95                 // Update quiz with override information.
96                 $quiz = quiz_update_effective_access($quiz, $USER->id);
98                 // Entry to return.
99                 $quizdetails = array();
100                 // First, we return information that any user can see in the web interface.
101                 $quizdetails['id'] = $quiz->id;
102                 $quizdetails['coursemodule']      = $quiz->coursemodule;
103                 $quizdetails['course']            = $quiz->course;
104                 $quizdetails['name']              = external_format_string($quiz->name, $context->id);
106                 if (has_capability('mod/quiz:view', $context)) {
107                     // Format intro.
108                     list($quizdetails['intro'], $quizdetails['introformat']) = external_format_text($quiz->intro,
109                                                                     $quiz->introformat, $context->id, 'mod_quiz', 'intro', null);
111                     $quizdetails['introfiles'] = external_util::get_area_files($context->id, 'mod_quiz', 'intro', false, false);
112                     $viewablefields = array('timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode',
113                                             'groupingid');
115                     $timenow = time();
116                     $quizobj = quiz::create($quiz->id, $USER->id);
117                     $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
118                                                                 $context, null, false));
120                     // Fields the user could see if have access to the quiz.
121                     if (!$accessmanager->prevent_access()) {
122                         // Some times this function returns just empty.
123                         $hasfeedback = quiz_has_feedback($quiz);
124                         $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
125                         $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
126                         $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
128                         $additionalfields = array('timelimit', 'attempts', 'attemptonlast', 'grademethod', 'decimalpoints',
129                                                     'questiondecimalpoints', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
130                                                     'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
131                                                     'reviewoverallfeedback', 'questionsperpage', 'navmethod', 'sumgrades', 'grade',
132                                                     'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
133                                                     'completionattemptsexhausted', 'completionpass', 'overduehandling',
134                                                     'graceperiod', 'preferredbehaviour', 'canredoquestions',
135                                                     'allowofflineattempts');
136                         $viewablefields = array_merge($viewablefields, $additionalfields);
137                     }
139                     // Fields only for managers.
140                     if (has_capability('moodle/course:manageactivities', $context)) {
141                         $additionalfields = array('shuffleanswers', 'timecreated', 'timemodified', 'password', 'subnet');
142                         $viewablefields = array_merge($viewablefields, $additionalfields);
143                     }
145                     foreach ($viewablefields as $field) {
146                         $quizdetails[$field] = $quiz->{$field};
147                     }
148                 }
149                 $returnedquizzes[] = $quizdetails;
150             }
151         }
152         $result = array();
153         $result['quizzes'] = $returnedquizzes;
154         $result['warnings'] = $warnings;
155         return $result;
156     }
158     /**
159      * Describes the get_quizzes_by_courses return value.
160      *
161      * @return external_single_structure
162      * @since Moodle 3.1
163      */
164     public static function get_quizzes_by_courses_returns() {
165         return new external_single_structure(
166             array(
167                 'quizzes' => new external_multiple_structure(
168                     new external_single_structure(
169                         array(
170                             'id' => new external_value(PARAM_INT, 'Standard Moodle primary key.'),
171                             'course' => new external_value(PARAM_INT, 'Foreign key reference to the course this quiz is part of.'),
172                             'coursemodule' => new external_value(PARAM_INT, 'Course module id.'),
173                             'name' => new external_value(PARAM_RAW, 'Quiz name.'),
174                             'intro' => new external_value(PARAM_RAW, 'Quiz introduction text.', VALUE_OPTIONAL),
175                             'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
176                             'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
177                             'timeopen' => new external_value(PARAM_INT, 'The time when this quiz opens. (0 = no restriction.)',
178                                                                 VALUE_OPTIONAL),
179                             'timeclose' => new external_value(PARAM_INT, 'The time when this quiz closes. (0 = no restriction.)',
180                                                                 VALUE_OPTIONAL),
181                             'timelimit' => new external_value(PARAM_INT, 'The time limit for quiz attempts, in seconds.',
182                                                                 VALUE_OPTIONAL),
183                             'overduehandling' => new external_value(PARAM_ALPHA, 'The method used to handle overdue attempts.
184                                                                     \'autosubmit\', \'graceperiod\' or \'autoabandon\'.',
185                                                                     VALUE_OPTIONAL),
186                             'graceperiod' => new external_value(PARAM_INT, 'The amount of time (in seconds) after the time limit
187                                                                 runs out during which attempts can still be submitted,
188                                                                 if overduehandling is set to allow it.', VALUE_OPTIONAL),
189                             'preferredbehaviour' => new external_value(PARAM_ALPHANUMEXT, 'The behaviour to ask questions to use.',
190                                                                         VALUE_OPTIONAL),
191                             'canredoquestions' => new external_value(PARAM_INT, 'Allows students to redo any completed question
192                                                                         within a quiz attempt.', VALUE_OPTIONAL),
193                             'attempts' => new external_value(PARAM_INT, 'The maximum number of attempts a student is allowed.',
194                                                                 VALUE_OPTIONAL),
195                             'attemptonlast' => new external_value(PARAM_INT, 'Whether subsequent attempts start from the answer
196                                                                     to the previous attempt (1) or start blank (0).',
197                                                                     VALUE_OPTIONAL),
198                             'grademethod' => new external_value(PARAM_INT, 'One of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
199                                                                     QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.', VALUE_OPTIONAL),
200                             'decimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when displaying
201                                                                     grades.', VALUE_OPTIONAL),
202                             'questiondecimalpoints' => new external_value(PARAM_INT, 'Number of decimal points to use when
203                                                                             displaying question grades.
204                                                                             (-1 means use decimalpoints.)', VALUE_OPTIONAL),
205                             'reviewattempt' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
206                                                                     attempts at various times. This is a bit field, decoded by the
207                                                                     mod_quiz_display_options class. It is formed by ORing together
208                                                                     the constants defined there.', VALUE_OPTIONAL),
209                             'reviewcorrectness' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
210                                                                         attempts at various times.
211                                                                         A bit field, like reviewattempt.', VALUE_OPTIONAL),
212                             'reviewmarks' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz attempts
213                                                                 at various times. A bit field, like reviewattempt.',
214                                                                 VALUE_OPTIONAL),
215                             'reviewspecificfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
216                                                                             quiz attempts at various times. A bit field, like
217                                                                             reviewattempt.', VALUE_OPTIONAL),
218                             'reviewgeneralfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their
219                                                                             quiz attempts at various times. A bit field, like
220                                                                             reviewattempt.', VALUE_OPTIONAL),
221                             'reviewrightanswer' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
222                                                                         attempts at various times. A bit field, like
223                                                                         reviewattempt.', VALUE_OPTIONAL),
224                             'reviewoverallfeedback' => new external_value(PARAM_INT, 'Whether users are allowed to review their quiz
225                                                                             attempts at various times. A bit field, like
226                                                                             reviewattempt.', VALUE_OPTIONAL),
227                             'questionsperpage' => new external_value(PARAM_INT, 'How often to insert a page break when editing
228                                                                         the quiz, or when shuffling the question order.',
229                                                                         VALUE_OPTIONAL),
230                             'navmethod' => new external_value(PARAM_ALPHA, 'Any constraints on how the user is allowed to navigate
231                                                                 around the quiz. Currently recognised values are
232                                                                 \'free\' and \'seq\'.', VALUE_OPTIONAL),
233                             'shuffleanswers' => new external_value(PARAM_INT, 'Whether the parts of the question should be shuffled,
234                                                                     in those question types that support it.', VALUE_OPTIONAL),
235                             'sumgrades' => new external_value(PARAM_FLOAT, 'The total of all the question instance maxmarks.',
236                                                                 VALUE_OPTIONAL),
237                             'grade' => new external_value(PARAM_FLOAT, 'The total that the quiz overall grade is scaled to be
238                                                             out of.', VALUE_OPTIONAL),
239                             'timecreated' => new external_value(PARAM_INT, 'The time when the quiz was added to the course.',
240                                                                 VALUE_OPTIONAL),
241                             'timemodified' => new external_value(PARAM_INT, 'Last modified time.',
242                                                                     VALUE_OPTIONAL),
243                             'password' => new external_value(PARAM_RAW, 'A password that the student must enter before starting or
244                                                                 continuing a quiz attempt.', VALUE_OPTIONAL),
245                             'subnet' => new external_value(PARAM_RAW, 'Used to restrict the IP addresses from which this quiz can
246                                                             be attempted. The format is as requried by the address_in_subnet
247                                                             function.', VALUE_OPTIONAL),
248                             'browsersecurity' => new external_value(PARAM_ALPHANUMEXT, 'Restriciton on the browser the student must
249                                                                     use. E.g. \'securewindow\'.', VALUE_OPTIONAL),
250                             'delay1' => new external_value(PARAM_INT, 'Delay that must be left between the first and second attempt,
251                                                             in seconds.', VALUE_OPTIONAL),
252                             'delay2' => new external_value(PARAM_INT, 'Delay that must be left between the second and subsequent
253                                                             attempt, in seconds.', VALUE_OPTIONAL),
254                             'showuserpicture' => new external_value(PARAM_INT, 'Option to show the user\'s picture during the
255                                                                     attempt and on the review page.', VALUE_OPTIONAL),
256                             'showblocks' => new external_value(PARAM_INT, 'Whether blocks should be shown on the attempt.php and
257                                                                 review.php pages.', VALUE_OPTIONAL),
258                             'completionattemptsexhausted' => new external_value(PARAM_INT, 'Mark quiz complete when the student has
259                                                                                 exhausted the maximum number of attempts',
260                                                                                 VALUE_OPTIONAL),
261                             'completionpass' => new external_value(PARAM_INT, 'Whether to require passing grade', VALUE_OPTIONAL),
262                             'allowofflineattempts' => new external_value(PARAM_INT, 'Whether to allow the quiz to be attempted
263                                                                             offline in the mobile app', VALUE_OPTIONAL),
264                             'autosaveperiod' => new external_value(PARAM_INT, 'Auto-save delay', VALUE_OPTIONAL),
265                             'hasfeedback' => new external_value(PARAM_INT, 'Whether the quiz has any non-blank feedback text',
266                                                                 VALUE_OPTIONAL),
267                             'hasquestions' => new external_value(PARAM_INT, 'Whether the quiz has questions', VALUE_OPTIONAL),
268                             'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
269                             'visible' => new external_value(PARAM_INT, 'Module visibility', VALUE_OPTIONAL),
270                             'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
271                             'groupingid' => new external_value(PARAM_INT, 'Grouping id', VALUE_OPTIONAL),
272                         )
273                     )
274                 ),
275                 'warnings' => new external_warnings(),
276             )
277         );
278     }
281     /**
282      * Utility function for validating a quiz.
283      *
284      * @param int $quizid quiz instance id
285      * @return array array containing the quiz, course, context and course module objects
286      * @since  Moodle 3.1
287      */
288     protected static function validate_quiz($quizid) {
289         global $DB;
291         // Request and permission validation.
292         $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
293         list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
295         $context = context_module::instance($cm->id);
296         self::validate_context($context);
298         return array($quiz, $course, $cm, $context);
299     }
301     /**
302      * Describes the parameters for view_quiz.
303      *
304      * @return external_external_function_parameters
305      * @since Moodle 3.1
306      */
307     public static function view_quiz_parameters() {
308         return new external_function_parameters (
309             array(
310                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
311             )
312         );
313     }
315     /**
316      * Trigger the course module viewed event and update the module completion status.
317      *
318      * @param int $quizid quiz instance id
319      * @return array of warnings and status result
320      * @since Moodle 3.1
321      * @throws moodle_exception
322      */
323     public static function view_quiz($quizid) {
324         global $DB;
326         $params = self::validate_parameters(self::view_quiz_parameters(), array('quizid' => $quizid));
327         $warnings = array();
329         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
331         // Trigger course_module_viewed event and completion.
332         quiz_view($quiz, $course, $cm, $context);
334         $result = array();
335         $result['status'] = true;
336         $result['warnings'] = $warnings;
337         return $result;
338     }
340     /**
341      * Describes the view_quiz return value.
342      *
343      * @return external_single_structure
344      * @since Moodle 3.1
345      */
346     public static function view_quiz_returns() {
347         return new external_single_structure(
348             array(
349                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
350                 'warnings' => new external_warnings(),
351             )
352         );
353     }
355     /**
356      * Describes the parameters for get_user_attempts.
357      *
358      * @return external_external_function_parameters
359      * @since Moodle 3.1
360      */
361     public static function get_user_attempts_parameters() {
362         return new external_function_parameters (
363             array(
364                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
365                 'userid' => new external_value(PARAM_INT, 'user id, empty for current user', VALUE_DEFAULT, 0),
366                 'status' => new external_value(PARAM_ALPHA, 'quiz status: all, finished or unfinished', VALUE_DEFAULT, 'finished'),
367                 'includepreviews' => new external_value(PARAM_BOOL, 'whether to include previews or not', VALUE_DEFAULT, false),
369             )
370         );
371     }
373     /**
374      * Return a list of attempts for the given quiz and user.
375      *
376      * @param int $quizid quiz instance id
377      * @param int $userid user id
378      * @param string $status quiz status: all, finished or unfinished
379      * @param bool $includepreviews whether to include previews or not
380      * @return array of warnings and the list of attempts
381      * @since Moodle 3.1
382      * @throws invalid_parameter_exception
383      */
384     public static function get_user_attempts($quizid, $userid = 0, $status = 'finished', $includepreviews = false) {
385         global $DB, $USER;
387         $warnings = array();
389         $params = array(
390             'quizid' => $quizid,
391             'userid' => $userid,
392             'status' => $status,
393             'includepreviews' => $includepreviews,
394         );
395         $params = self::validate_parameters(self::get_user_attempts_parameters(), $params);
397         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
399         if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
400             throw new invalid_parameter_exception('Invalid status value');
401         }
403         // Default value for userid.
404         if (empty($params['userid'])) {
405             $params['userid'] = $USER->id;
406         }
408         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
409         core_user::require_active_user($user);
411         // Extra checks so only users with permissions can view other users attempts.
412         if ($USER->id != $user->id) {
413             require_capability('mod/quiz:viewreports', $context);
414         }
416         $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
418         $result = array();
419         $result['attempts'] = $attempts;
420         $result['warnings'] = $warnings;
421         return $result;
422     }
424     /**
425      * Describes a single attempt structure.
426      *
427      * @return external_single_structure the attempt structure
428      */
429     private static function attempt_structure() {
430         return new external_single_structure(
431             array(
432                 'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
433                 'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
434                                                 VALUE_OPTIONAL),
435                 'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
436                                                 VALUE_OPTIONAL),
437                 'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
438                                                 VALUE_OPTIONAL),
439                 'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
440                                                     details of the the question_attempts that make up this quiz
441                                                     attempt.', VALUE_OPTIONAL),
442                 'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
443                 'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
444                 'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
445                 'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
446                                                 \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
447                 'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
448                 'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
449                                                     0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
450                 'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
451                 'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.', VALUE_OPTIONAL),
452                 'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
453                                                         state changes.  NULL means never check.', VALUE_OPTIONAL),
454                 'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
455             )
456         );
457     }
459     /**
460      * Describes the get_user_attempts return value.
461      *
462      * @return external_single_structure
463      * @since Moodle 3.1
464      */
465     public static function get_user_attempts_returns() {
466         return new external_single_structure(
467             array(
468                 'attempts' => new external_multiple_structure(self::attempt_structure()),
469                 'warnings' => new external_warnings(),
470             )
471         );
472     }
474     /**
475      * Describes the parameters for get_user_best_grade.
476      *
477      * @return external_external_function_parameters
478      * @since Moodle 3.1
479      */
480     public static function get_user_best_grade_parameters() {
481         return new external_function_parameters (
482             array(
483                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
484                 'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
485             )
486         );
487     }
489     /**
490      * Get the best current grade for the given user on a quiz.
491      *
492      * @param int $quizid quiz instance id
493      * @param int $userid user id
494      * @return array of warnings and the grade information
495      * @since Moodle 3.1
496      */
497     public static function get_user_best_grade($quizid, $userid = 0) {
498         global $DB, $USER;
500         $warnings = array();
502         $params = array(
503             'quizid' => $quizid,
504             'userid' => $userid,
505         );
506         $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
508         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
510         // Default value for userid.
511         if (empty($params['userid'])) {
512             $params['userid'] = $USER->id;
513         }
515         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
516         core_user::require_active_user($user);
518         // Extra checks so only users with permissions can view other users attempts.
519         if ($USER->id != $user->id) {
520             require_capability('mod/quiz:viewreports', $context);
521         }
523         $result = array();
524         $grade = quiz_get_best_grade($quiz, $user->id);
526         if ($grade === null) {
527             $result['hasgrade'] = false;
528         } else {
529             $result['hasgrade'] = true;
530             $result['grade'] = $grade;
531         }
532         $result['warnings'] = $warnings;
533         return $result;
534     }
536     /**
537      * Describes the get_user_best_grade return value.
538      *
539      * @return external_single_structure
540      * @since Moodle 3.1
541      */
542     public static function get_user_best_grade_returns() {
543         return new external_single_structure(
544             array(
545                 'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
546                 'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
547                 'warnings' => new external_warnings(),
548             )
549         );
550     }
552     /**
553      * Describes the parameters for get_combined_review_options.
554      *
555      * @return external_external_function_parameters
556      * @since Moodle 3.1
557      */
558     public static function get_combined_review_options_parameters() {
559         return new external_function_parameters (
560             array(
561                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
562                 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
564             )
565         );
566     }
568     /**
569      * Combines the review options from a number of different quiz attempts.
570      *
571      * @param int $quizid quiz instance id
572      * @param int $userid user id (empty for current user)
573      * @return array of warnings and the review options
574      * @since Moodle 3.1
575      */
576     public static function get_combined_review_options($quizid, $userid = 0) {
577         global $DB, $USER;
579         $warnings = array();
581         $params = array(
582             'quizid' => $quizid,
583             'userid' => $userid,
584         );
585         $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
587         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
589         // Default value for userid.
590         if (empty($params['userid'])) {
591             $params['userid'] = $USER->id;
592         }
594         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
595         core_user::require_active_user($user);
597         // Extra checks so only users with permissions can view other users attempts.
598         if ($USER->id != $user->id) {
599             require_capability('mod/quiz:viewreports', $context);
600         }
602         $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
604         $result = array();
605         $result['someoptions'] = [];
606         $result['alloptions'] = [];
608         list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
610         foreach (array('someoptions', 'alloptions') as $typeofoption) {
611             foreach ($$typeofoption as $key => $value) {
612                 $result[$typeofoption][] = array(
613                     "name" => $key,
614                     "value" => (!empty($value)) ? $value : 0
615                 );
616             }
617         }
619         $result['warnings'] = $warnings;
620         return $result;
621     }
623     /**
624      * Describes the get_combined_review_options return value.
625      *
626      * @return external_single_structure
627      * @since Moodle 3.1
628      */
629     public static function get_combined_review_options_returns() {
630         return new external_single_structure(
631             array(
632                 'someoptions' => new external_multiple_structure(
633                     new external_single_structure(
634                         array(
635                             'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
636                             'value' => new external_value(PARAM_INT, 'option value'),
637                         )
638                     )
639                 ),
640                 'alloptions' => new external_multiple_structure(
641                     new external_single_structure(
642                         array(
643                             'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
644                             'value' => new external_value(PARAM_INT, 'option value'),
645                         )
646                     )
647                 ),
648                 'warnings' => new external_warnings(),
649             )
650         );
651     }
653     /**
654      * Describes the parameters for start_attempt.
655      *
656      * @return external_external_function_parameters
657      * @since Moodle 3.1
658      */
659     public static function start_attempt_parameters() {
660         return new external_function_parameters (
661             array(
662                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
663                 'preflightdata' => new external_multiple_structure(
664                     new external_single_structure(
665                         array(
666                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
667                             'value' => new external_value(PARAM_RAW, 'data value'),
668                         )
669                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
670                 ),
671                 'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
673             )
674         );
675     }
677     /**
678      * Starts a new attempt at a quiz.
679      *
680      * @param int $quizid quiz instance id
681      * @param array $preflightdata preflight required data (like passwords)
682      * @param bool $forcenew Whether to force a new attempt or not.
683      * @return array of warnings and the attempt basic data
684      * @since Moodle 3.1
685      * @throws moodle_quiz_exception
686      */
687     public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) {
688         global $DB, $USER;
690         $warnings = array();
691         $attempt = array();
693         $params = array(
694             'quizid' => $quizid,
695             'preflightdata' => $preflightdata,
696             'forcenew' => $forcenew,
697         );
698         $params = self::validate_parameters(self::start_attempt_parameters(), $params);
699         $forcenew = $params['forcenew'];
701         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
703         $quizobj = quiz::create($cm->instance, $USER->id);
705         // Check questions.
706         if (!$quizobj->has_questions()) {
707             throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
708         }
710         // Create an object to manage all the other (non-roles) access rules.
711         $timenow = time();
712         $accessmanager = $quizobj->get_access_manager($timenow);
714         // Validate permissions for creating a new attempt and start a new preview attempt if required.
715         list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
716             quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
718         // Check access.
719         if (!$quizobj->is_preview_user() && $messages) {
720             // Create warnings with the exact messages.
721             foreach ($messages as $message) {
722                 $warnings[] = array(
723                     'item' => 'quiz',
724                     'itemid' => $quiz->id,
725                     'warningcode' => '1',
726                     'message' => clean_text($message, PARAM_TEXT)
727                 );
728             }
729         } else {
730             if ($accessmanager->is_preflight_check_required($currentattemptid)) {
731                 // Need to do some checks before allowing the user to continue.
733                 $provideddata = array();
734                 foreach ($params['preflightdata'] as $data) {
735                     $provideddata[$data['name']] = $data['value'];
736                 }
738                 $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
740                 if (!empty($errors)) {
741                     throw new moodle_quiz_exception($quizobj, array_shift($errors));
742                 }
744                 // Pre-flight check passed.
745                 $accessmanager->notify_preflight_check_passed($currentattemptid);
746             }
748             if ($currentattemptid) {
749                 if ($lastattempt->state == quiz_attempt::OVERDUE) {
750                     throw new moodle_quiz_exception($quizobj, 'stateoverdue');
751                 } else {
752                     throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress');
753                 }
754             }
755             $offlineattempt = WS_SERVER ? true : false;
756             $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt);
757         }
759         $result = array();
760         $result['attempt'] = $attempt;
761         $result['warnings'] = $warnings;
762         return $result;
763     }
765     /**
766      * Describes the start_attempt return value.
767      *
768      * @return external_single_structure
769      * @since Moodle 3.1
770      */
771     public static function start_attempt_returns() {
772         return new external_single_structure(
773             array(
774                 'attempt' => self::attempt_structure(),
775                 'warnings' => new external_warnings(),
776             )
777         );
778     }
780     /**
781      * Utility function for validating a given attempt
782      *
783      * @param  array $params array of parameters including the attemptid and preflight data
784      * @param  bool $checkaccessrules whether to check the quiz access rules or not
785      * @param  bool $failifoverdue whether to return error if the attempt is overdue
786      * @return  array containing the attempt object and access messages
787      * @throws moodle_quiz_exception
788      * @since  Moodle 3.1
789      */
790     protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
791         global $USER;
793         $attemptobj = quiz_attempt::create($params['attemptid']);
795         $context = context_module::instance($attemptobj->get_cm()->id);
796         self::validate_context($context);
798         // Check that this attempt belongs to this user.
799         if ($attemptobj->get_userid() != $USER->id) {
800             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
801         }
803         // General capabilities check.
804         $ispreviewuser = $attemptobj->is_preview_user();
805         if (!$ispreviewuser) {
806             $attemptobj->require_capability('mod/quiz:attempt');
807         }
809         // Check the access rules.
810         $accessmanager = $attemptobj->get_access_manager(time());
811         $messages = array();
812         if ($checkaccessrules) {
813             // If the attempt is now overdue, or abandoned, deal with that.
814             $attemptobj->handle_if_time_expired(time(), true);
816             $messages = $accessmanager->prevent_access();
817             if (!$ispreviewuser && $messages) {
818                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
819             }
820         }
822         // Attempt closed?.
823         if ($attemptobj->is_finished()) {
824             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
825         } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
826             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
827         }
829         // User submitted data (like the quiz password).
830         if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
831             $provideddata = array();
832             foreach ($params['preflightdata'] as $data) {
833                 $provideddata[$data['name']] = $data['value'];
834             }
836             $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
837             if (!empty($errors)) {
838                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors));
839             }
840             // Pre-flight check passed.
841             $accessmanager->notify_preflight_check_passed($params['attemptid']);
842         }
844         if (isset($params['page'])) {
845             // Check if the page is out of range.
846             if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
847                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number');
848             }
850             // Prevent out of sequence access.
851             if (!$attemptobj->check_page_access($params['page'])) {
852                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
853             }
855             // Check slots.
856             $slots = $attemptobj->get_slots($params['page']);
858             if (empty($slots)) {
859                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
860             }
861         }
863         return array($attemptobj, $messages);
864     }
866     /**
867      * Describes a single question structure.
868      *
869      * @return external_single_structure the question structure
870      * @since  Moodle 3.1
871      * @since Moodle 3.2 blockedbyprevious parameter added.
872      */
873     private static function question_structure() {
874         return new external_single_structure(
875             array(
876                 'slot' => new external_value(PARAM_INT, 'slot number'),
877                 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'),
878                 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'),
879                 'html' => new external_value(PARAM_RAW, 'the question rendered'),
880                 'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL),
881                 'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt',
882                                                         VALUE_OPTIONAL),
883                 'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
884                                                             VALUE_OPTIONAL),
885                 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
886                 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
887                 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in', VALUE_OPTIONAL),
888                 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
889                 'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
890                         VALUE_OPTIONAL),
891                 'mark' => new external_value(PARAM_RAW, 'the mark awarded', VALUE_OPTIONAL),
892                 'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt', VALUE_OPTIONAL),
893             )
894         );
895     }
897     /**
898      * Return questions information for a given attempt.
899      *
900      * @param  quiz_attempt  $attemptobj  the quiz attempt object
901      * @param  bool  $review  whether if we are in review mode or not
902      * @param  mixed  $page  string 'all' or integer page number
903      * @return array array of questions including data
904      */
905     private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
906         global $PAGE;
908         $questions = array();
909         $contextid = $attemptobj->get_quizobj()->get_context()->id;
910         $displayoptions = $attemptobj->get_display_options($review);
911         $renderer = $PAGE->get_renderer('mod_quiz');
913         foreach ($attemptobj->get_slots($page) as $slot) {
915             $question = array(
916                 'slot' => $slot,
917                 'type' => $attemptobj->get_question_type_name($slot),
918                 'page' => $attemptobj->get_question_page($slot),
919                 'flagged' => $attemptobj->is_question_flagged($slot),
920                 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(),
921                 'sequencecheck' => $attemptobj->get_question_attempt($slot)->get_sequence_check_count(),
922                 'lastactiontime' => $attemptobj->get_question_attempt($slot)->get_last_step()->get_timecreated(),
923                 'hasautosavedstep' => $attemptobj->get_question_attempt($slot)->has_autosaved_step()
924             );
926             if ($attemptobj->is_real_question($slot)) {
927                 $question['number'] = $attemptobj->get_question_number($slot);
928                 $question['state'] = (string) $attemptobj->get_question_state($slot);
929                 $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
930                 $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
931             }
932             if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
933                 $question['maxmark'] = $attemptobj->get_question_attempt($slot)->get_max_mark();
934             }
935             if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
936                 $question['mark'] = $attemptobj->get_question_mark($slot);
937             }
939             $questions[] = $question;
940         }
941         return $questions;
942     }
944     /**
945      * Describes the parameters for get_attempt_data.
946      *
947      * @return external_external_function_parameters
948      * @since Moodle 3.1
949      */
950     public static function get_attempt_data_parameters() {
951         return new external_function_parameters (
952             array(
953                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
954                 'page' => new external_value(PARAM_INT, 'page number'),
955                 'preflightdata' => new external_multiple_structure(
956                     new external_single_structure(
957                         array(
958                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
959                             'value' => new external_value(PARAM_RAW, 'data value'),
960                         )
961                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
962                 )
963             )
964         );
965     }
967     /**
968      * Returns information for the given attempt page for a quiz attempt in progress.
969      *
970      * @param int $attemptid attempt id
971      * @param int $page page number
972      * @param array $preflightdata preflight required data (like passwords)
973      * @return array of warnings and the attempt data, next page, message and questions
974      * @since Moodle 3.1
975      * @throws moodle_quiz_exceptions
976      */
977     public static function get_attempt_data($attemptid, $page, $preflightdata = array()) {
979         $warnings = array();
981         $params = array(
982             'attemptid' => $attemptid,
983             'page' => $page,
984             'preflightdata' => $preflightdata,
985         );
986         $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
988         list($attemptobj, $messages) = self::validate_attempt($params);
990         if ($attemptobj->is_last_page($params['page'])) {
991             $nextpage = -1;
992         } else {
993             $nextpage = $params['page'] + 1;
994         }
996         $result = array();
997         $result['attempt'] = $attemptobj->get_attempt();
998         $result['messages'] = $messages;
999         $result['nextpage'] = $nextpage;
1000         $result['warnings'] = $warnings;
1001         $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
1003         return $result;
1004     }
1006     /**
1007      * Describes the get_attempt_data return value.
1008      *
1009      * @return external_single_structure
1010      * @since Moodle 3.1
1011      */
1012     public static function get_attempt_data_returns() {
1013         return new external_single_structure(
1014             array(
1015                 'attempt' => self::attempt_structure(),
1016                 'messages' => new external_multiple_structure(
1017                     new external_value(PARAM_TEXT, 'access message'),
1018                     'access messages, will only be returned for users with mod/quiz:preview capability,
1019                     for other users this method will throw an exception if there are messages'),
1020                 'nextpage' => new external_value(PARAM_INT, 'next page number'),
1021                 'questions' => new external_multiple_structure(self::question_structure()),
1022                 'warnings' => new external_warnings(),
1023             )
1024         );
1025     }
1027     /**
1028      * Describes the parameters for get_attempt_summary.
1029      *
1030      * @return external_external_function_parameters
1031      * @since Moodle 3.1
1032      */
1033     public static function get_attempt_summary_parameters() {
1034         return new external_function_parameters (
1035             array(
1036                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1037                 'preflightdata' => new external_multiple_structure(
1038                     new external_single_structure(
1039                         array(
1040                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1041                             'value' => new external_value(PARAM_RAW, 'data value'),
1042                         )
1043                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1044                 )
1045             )
1046         );
1047     }
1049     /**
1050      * Returns a summary of a quiz attempt before it is submitted.
1051      *
1052      * @param int $attemptid attempt id
1053      * @param int $preflightdata preflight required data (like passwords)
1054      * @return array of warnings and the attempt summary data for each question
1055      * @since Moodle 3.1
1056      */
1057     public static function get_attempt_summary($attemptid, $preflightdata = array()) {
1059         $warnings = array();
1061         $params = array(
1062             'attemptid' => $attemptid,
1063             'preflightdata' => $preflightdata,
1064         );
1065         $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1067         list($attemptobj, $messages) = self::validate_attempt($params, true, false);
1069         $result = array();
1070         $result['warnings'] = $warnings;
1071         $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1073         return $result;
1074     }
1076     /**
1077      * Describes the get_attempt_summary return value.
1078      *
1079      * @return external_single_structure
1080      * @since Moodle 3.1
1081      */
1082     public static function get_attempt_summary_returns() {
1083         return new external_single_structure(
1084             array(
1085                 'questions' => new external_multiple_structure(self::question_structure()),
1086                 'warnings' => new external_warnings(),
1087             )
1088         );
1089     }
1091     /**
1092      * Describes the parameters for save_attempt.
1093      *
1094      * @return external_external_function_parameters
1095      * @since Moodle 3.1
1096      */
1097     public static function save_attempt_parameters() {
1098         return new external_function_parameters (
1099             array(
1100                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1101                 'data' => new external_multiple_structure(
1102                     new external_single_structure(
1103                         array(
1104                             'name' => new external_value(PARAM_RAW, 'data name'),
1105                             'value' => new external_value(PARAM_RAW, 'data value'),
1106                         )
1107                     ), 'the data to be saved'
1108                 ),
1109                 'preflightdata' => new external_multiple_structure(
1110                     new external_single_structure(
1111                         array(
1112                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1113                             'value' => new external_value(PARAM_RAW, 'data value'),
1114                         )
1115                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1116                 )
1117             )
1118         );
1119     }
1121     /**
1122      * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1123      *
1124      * @param int $attemptid attempt id
1125      * @param array $data the data to be saved
1126      * @param  array $preflightdata preflight required data (like passwords)
1127      * @return array of warnings and execution result
1128      * @since Moodle 3.1
1129      */
1130     public static function save_attempt($attemptid, $data, $preflightdata = array()) {
1131         global $DB;
1133         $warnings = array();
1135         $params = array(
1136             'attemptid' => $attemptid,
1137             'data' => $data,
1138             'preflightdata' => $preflightdata,
1139         );
1140         $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1142         // Add a page, required by validate_attempt.
1143         list($attemptobj, $messages) = self::validate_attempt($params);
1145         $transaction = $DB->start_delegated_transaction();
1146         // Create the $_POST object required by the question engine.
1147         $_POST = array();
1148         foreach ($data as $element) {
1149             $_POST[$element['name']] = $element['value'];
1150         }
1151         $timenow = time();
1152         // Update the timemodifiedoffline field.
1153         $attemptobj->set_offline_modified_time($timenow);
1154         $attemptobj->process_auto_save($timenow);
1155         $transaction->allow_commit();
1157         $result = array();
1158         $result['status'] = true;
1159         $result['warnings'] = $warnings;
1160         return $result;
1161     }
1163     /**
1164      * Describes the save_attempt return value.
1165      *
1166      * @return external_single_structure
1167      * @since Moodle 3.1
1168      */
1169     public static function save_attempt_returns() {
1170         return new external_single_structure(
1171             array(
1172                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1173                 'warnings' => new external_warnings(),
1174             )
1175         );
1176     }
1178     /**
1179      * Describes the parameters for process_attempt.
1180      *
1181      * @return external_external_function_parameters
1182      * @since Moodle 3.1
1183      */
1184     public static function process_attempt_parameters() {
1185         return new external_function_parameters (
1186             array(
1187                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1188                 'data' => new external_multiple_structure(
1189                     new external_single_structure(
1190                         array(
1191                             'name' => new external_value(PARAM_RAW, 'data name'),
1192                             'value' => new external_value(PARAM_RAW, 'data value'),
1193                         )
1194                     ),
1195                     'the data to be saved', VALUE_DEFAULT, array()
1196                 ),
1197                 'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
1198                 'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
1199                                                 VALUE_DEFAULT, false),
1200                 'preflightdata' => new external_multiple_structure(
1201                     new external_single_structure(
1202                         array(
1203                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1204                             'value' => new external_value(PARAM_RAW, 'data value'),
1205                         )
1206                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1207                 )
1208             )
1209         );
1210     }
1212     /**
1213      * Process responses during an attempt at a quiz and also deals with attempts finishing.
1214      *
1215      * @param int $attemptid attempt id
1216      * @param array $data the data to be saved
1217      * @param bool $finishattempt whether to finish or not the attempt
1218      * @param bool $timeup whether the WS was called by a timer when the time is up
1219      * @param array $preflightdata preflight required data (like passwords)
1220      * @return array of warnings and the attempt state after the processing
1221      * @since Moodle 3.1
1222      */
1223     public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = array()) {
1225         $warnings = array();
1227         $params = array(
1228             'attemptid' => $attemptid,
1229             'data' => $data,
1230             'finishattempt' => $finishattempt,
1231             'timeup' => $timeup,
1232             'preflightdata' => $preflightdata,
1233         );
1234         $params = self::validate_parameters(self::process_attempt_parameters(), $params);
1236         // Do not check access manager rules.
1237         list($attemptobj, $messages) = self::validate_attempt($params, false);
1239         // Create the $_POST object required by the question engine.
1240         $_POST = array();
1241         foreach ($params['data'] as $element) {
1242             $_POST[$element['name']] = $element['value'];
1243         }
1244         $timenow = time();
1245         $finishattempt = $params['finishattempt'];
1246         $timeup = $params['timeup'];
1248         $result = array();
1249         // Update the timemodifiedoffline field.
1250         $attemptobj->set_offline_modified_time($timenow);
1251         $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1253         $result['warnings'] = $warnings;
1254         return $result;
1255     }
1257     /**
1258      * Describes the process_attempt return value.
1259      *
1260      * @return external_single_structure
1261      * @since Moodle 3.1
1262      */
1263     public static function process_attempt_returns() {
1264         return new external_single_structure(
1265             array(
1266                 'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
1267                                                                     inprogress, finished, overdue, abandoned'),
1268                 'warnings' => new external_warnings(),
1269             )
1270         );
1271     }
1273     /**
1274      * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1275      *
1276      * @param  array $params Array of parameters including the attemptid
1277      * @return  array containing the attempt object and display options
1278      * @since  Moodle 3.1
1279      * @throws  moodle_exception
1280      * @throws  moodle_quiz_exception
1281      */
1282     protected static function validate_attempt_review($params) {
1284         $attemptobj = quiz_attempt::create($params['attemptid']);
1285         $attemptobj->check_review_capability();
1287         $displayoptions = $attemptobj->get_display_options(true);
1288         if ($attemptobj->is_own_attempt()) {
1289             if (!$attemptobj->is_finished()) {
1290                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed');
1291             } else if (!$displayoptions->attempt) {
1292                 throw new moodle_exception($attemptobj->cannot_review_message());
1293             }
1294         } else if (!$attemptobj->is_review_allowed()) {
1295             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
1296         }
1297         return array($attemptobj, $displayoptions);
1298     }
1300     /**
1301      * Describes the parameters for get_attempt_review.
1302      *
1303      * @return external_external_function_parameters
1304      * @since Moodle 3.1
1305      */
1306     public static function get_attempt_review_parameters() {
1307         return new external_function_parameters (
1308             array(
1309                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1310                 'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages',
1311                                                 VALUE_DEFAULT, -1),
1312             )
1313         );
1314     }
1316     /**
1317      * Returns review information for the given finished attempt, can be used by users or teachers.
1318      *
1319      * @param int $attemptid attempt id
1320      * @param int $page page number, empty for all the questions in all the pages
1321      * @return array of warnings and the attempt data, feedback and questions
1322      * @since Moodle 3.1
1323      * @throws  moodle_exception
1324      * @throws  moodle_quiz_exception
1325      */
1326     public static function get_attempt_review($attemptid, $page = -1) {
1327         global $PAGE;
1329         $warnings = array();
1331         $params = array(
1332             'attemptid' => $attemptid,
1333             'page' => $page,
1334         );
1335         $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1337         list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1339         if ($params['page'] !== -1) {
1340             $page = $attemptobj->force_page_number_into_range($params['page']);
1341         } else {
1342             $page = 'all';
1343         }
1345         // Prepare the output.
1346         $result = array();
1347         $result['attempt'] = $attemptobj->get_attempt();
1348         $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true);
1350         $result['additionaldata'] = array();
1351         // Summary data (from behaviours).
1352         $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1353         foreach ($summarydata as $key => $data) {
1354             // This text does not need formatting (no need for external_format_[string|text]).
1355             $result['additionaldata'][] = array(
1356                 'id' => $key,
1357                 'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id,
1358                 'content' => $data['content'],
1359             );
1360         }
1362         // Feedback if there is any, and the user is allowed to see it now.
1363         $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false);
1365         $feedback = $attemptobj->get_overall_feedback($grade);
1366         if ($displayoptions->overallfeedback && $feedback) {
1367             $result['additionaldata'][] = array(
1368                 'id' => 'feedback',
1369                 'title' => get_string('feedback', 'quiz'),
1370                 'content' => $feedback,
1371             );
1372         }
1374         $result['grade'] = $grade;
1375         $result['warnings'] = $warnings;
1376         return $result;
1377     }
1379     /**
1380      * Describes the get_attempt_review return value.
1381      *
1382      * @return external_single_structure
1383      * @since Moodle 3.1
1384      */
1385     public static function get_attempt_review_returns() {
1386         return new external_single_structure(
1387             array(
1388                 'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'),
1389                 'attempt' => self::attempt_structure(),
1390                 'additionaldata' => new external_multiple_structure(
1391                     new external_single_structure(
1392                         array(
1393                             'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'),
1394                             'title' => new external_value(PARAM_TEXT, 'data title'),
1395                             'content' => new external_value(PARAM_RAW, 'data content'),
1396                         )
1397                     )
1398                 ),
1399                 'questions' => new external_multiple_structure(self::question_structure()),
1400                 'warnings' => new external_warnings(),
1401             )
1402         );
1403     }
1405     /**
1406      * Describes the parameters for view_attempt.
1407      *
1408      * @return external_external_function_parameters
1409      * @since Moodle 3.1
1410      */
1411     public static function view_attempt_parameters() {
1412         return new external_function_parameters (
1413             array(
1414                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1415                 'page' => new external_value(PARAM_INT, 'page number'),
1416                 'preflightdata' => new external_multiple_structure(
1417                     new external_single_structure(
1418                         array(
1419                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1420                             'value' => new external_value(PARAM_RAW, 'data value'),
1421                         )
1422                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1423                 )
1424             )
1425         );
1426     }
1428     /**
1429      * Trigger the attempt viewed event.
1430      *
1431      * @param int $attemptid attempt id
1432      * @param int $page page number
1433      * @param array $preflightdata preflight required data (like passwords)
1434      * @return array of warnings and status result
1435      * @since Moodle 3.1
1436      */
1437     public static function view_attempt($attemptid, $page, $preflightdata = array()) {
1439         $warnings = array();
1441         $params = array(
1442             'attemptid' => $attemptid,
1443             'page' => $page,
1444             'preflightdata' => $preflightdata,
1445         );
1446         $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1447         list($attemptobj, $messages) = self::validate_attempt($params);
1449         // Log action.
1450         $attemptobj->fire_attempt_viewed_event();
1452         // Update attempt page, throwing an exception if $page is not valid.
1453         if (!$attemptobj->set_currentpage($params['page'])) {
1454             throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
1455         }
1457         $result = array();
1458         $result['status'] = true;
1459         $result['warnings'] = $warnings;
1460         return $result;
1461     }
1463     /**
1464      * Describes the view_attempt return value.
1465      *
1466      * @return external_single_structure
1467      * @since Moodle 3.1
1468      */
1469     public static function view_attempt_returns() {
1470         return new external_single_structure(
1471             array(
1472                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1473                 'warnings' => new external_warnings(),
1474             )
1475         );
1476     }
1478     /**
1479      * Describes the parameters for view_attempt_summary.
1480      *
1481      * @return external_external_function_parameters
1482      * @since Moodle 3.1
1483      */
1484     public static function view_attempt_summary_parameters() {
1485         return new external_function_parameters (
1486             array(
1487                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1488                 'preflightdata' => new external_multiple_structure(
1489                     new external_single_structure(
1490                         array(
1491                             'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1492                             'value' => new external_value(PARAM_RAW, 'data value'),
1493                         )
1494                     ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1495                 )
1496             )
1497         );
1498     }
1500     /**
1501      * Trigger the attempt summary viewed event.
1502      *
1503      * @param int $attemptid attempt id
1504      * @param array $preflightdata preflight required data (like passwords)
1505      * @return array of warnings and status result
1506      * @since Moodle 3.1
1507      */
1508     public static function view_attempt_summary($attemptid, $preflightdata = array()) {
1510         $warnings = array();
1512         $params = array(
1513             'attemptid' => $attemptid,
1514             'preflightdata' => $preflightdata,
1515         );
1516         $params = self::validate_parameters(self::view_attempt_summary_parameters(), $params);
1517         list($attemptobj, $messages) = self::validate_attempt($params);
1519         // Log action.
1520         $attemptobj->fire_attempt_summary_viewed_event();
1522         $result = array();
1523         $result['status'] = true;
1524         $result['warnings'] = $warnings;
1525         return $result;
1526     }
1528     /**
1529      * Describes the view_attempt_summary return value.
1530      *
1531      * @return external_single_structure
1532      * @since Moodle 3.1
1533      */
1534     public static function view_attempt_summary_returns() {
1535         return new external_single_structure(
1536             array(
1537                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1538                 'warnings' => new external_warnings(),
1539             )
1540         );
1541     }
1543     /**
1544      * Describes the parameters for view_attempt_review.
1545      *
1546      * @return external_external_function_parameters
1547      * @since Moodle 3.1
1548      */
1549     public static function view_attempt_review_parameters() {
1550         return new external_function_parameters (
1551             array(
1552                 'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1553             )
1554         );
1555     }
1557     /**
1558      * Trigger the attempt reviewed event.
1559      *
1560      * @param int $attemptid attempt id
1561      * @return array of warnings and status result
1562      * @since Moodle 3.1
1563      */
1564     public static function view_attempt_review($attemptid) {
1566         $warnings = array();
1568         $params = array(
1569             'attemptid' => $attemptid,
1570         );
1571         $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1572         list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1574         // Log action.
1575         $attemptobj->fire_attempt_reviewed_event();
1577         $result = array();
1578         $result['status'] = true;
1579         $result['warnings'] = $warnings;
1580         return $result;
1581     }
1583     /**
1584      * Describes the view_attempt_review return value.
1585      *
1586      * @return external_single_structure
1587      * @since Moodle 3.1
1588      */
1589     public static function view_attempt_review_returns() {
1590         return new external_single_structure(
1591             array(
1592                 'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1593                 'warnings' => new external_warnings(),
1594             )
1595         );
1596     }
1598     /**
1599      * Describes the parameters for view_quiz.
1600      *
1601      * @return external_external_function_parameters
1602      * @since Moodle 3.1
1603      */
1604     public static function get_quiz_feedback_for_grade_parameters() {
1605         return new external_function_parameters (
1606             array(
1607                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1608                 'grade' => new external_value(PARAM_FLOAT, 'the grade to check'),
1609             )
1610         );
1611     }
1613     /**
1614      * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1615      *
1616      * @param int $quizid quiz instance id
1617      * @param float $grade the grade to check
1618      * @return array of warnings and status result
1619      * @since Moodle 3.1
1620      * @throws moodle_exception
1621      */
1622     public static function get_quiz_feedback_for_grade($quizid, $grade) {
1623         global $DB;
1625         $params = array(
1626             'quizid' => $quizid,
1627             'grade' => $grade,
1628         );
1629         $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1630         $warnings = array();
1632         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1634         $result = array();
1635         $result['feedbacktext'] = '';
1636         $result['feedbacktextformat'] = FORMAT_MOODLE;
1638         $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1639         if (!empty($feedback->feedbacktext)) {
1640             list($text, $format) = external_format_text($feedback->feedbacktext, $feedback->feedbacktextformat, $context->id,
1641                                                         'mod_quiz', 'feedback', $feedback->id);
1642             $result['feedbacktext'] = $text;
1643             $result['feedbacktextformat'] = $format;
1644             $feedbackinlinefiles = external_util::get_area_files($context->id, 'mod_quiz', 'feedback', $feedback->id);
1645             if (!empty($feedbackinlinefiles)) {
1646                 $result['feedbackinlinefiles'] = $feedbackinlinefiles;
1647             }
1648         }
1650         $result['warnings'] = $warnings;
1651         return $result;
1652     }
1654     /**
1655      * Describes the get_quiz_feedback_for_grade return value.
1656      *
1657      * @return external_single_structure
1658      * @since Moodle 3.1
1659      */
1660     public static function get_quiz_feedback_for_grade_returns() {
1661         return new external_single_structure(
1662             array(
1663                 'feedbacktext' => new external_value(PARAM_RAW, 'the comment that corresponds to this grade (empty for none)'),
1664                 'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL),
1665                 'feedbackinlinefiles' => new external_files('feedback inline files', VALUE_OPTIONAL),
1666                 'warnings' => new external_warnings(),
1667             )
1668         );
1669     }
1671     /**
1672      * Describes the parameters for get_quiz_access_information.
1673      *
1674      * @return external_external_function_parameters
1675      * @since Moodle 3.1
1676      */
1677     public static function get_quiz_access_information_parameters() {
1678         return new external_function_parameters (
1679             array(
1680                 'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1681             )
1682         );
1683     }
1685     /**
1686      * Return access information for a given quiz.
1687      *
1688      * @param int $quizid quiz instance id
1689      * @return array of warnings and the access information
1690      * @since Moodle 3.1
1691      * @throws  moodle_quiz_exception
1692      */
1693     public static function get_quiz_access_information($quizid) {
1694         global $DB, $USER;
1696         $warnings = array();
1698         $params = array(
1699             'quizid' => $quizid
1700         );
1701         $params = self::validate_parameters(self::get_quiz_access_information_parameters(), $params);
1703         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1705         $result = array();
1706         // Capabilities first.
1707         $result['canattempt'] = has_capability('mod/quiz:attempt', $context);;
1708         $result['canmanage'] = has_capability('mod/quiz:manage', $context);;
1709         $result['canpreview'] = has_capability('mod/quiz:preview', $context);;
1710         $result['canreviewmyattempts'] = has_capability('mod/quiz:reviewmyattempts', $context);;
1711         $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);;
1713         // Access manager now.
1714         $quizobj = quiz::create($cm->instance, $USER->id);
1715         $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1716         $timenow = time();
1717         $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1719         $result['accessrules'] = $accessmanager->describe_rules();
1720         $result['activerulenames'] = $accessmanager->get_active_rule_names();
1721         $result['preventaccessreasons'] = $accessmanager->prevent_access();
1723         $result['warnings'] = $warnings;
1724         return $result;
1725     }
1727     /**
1728      * Describes the get_quiz_access_information return value.
1729      *
1730      * @return external_single_structure
1731      * @since Moodle 3.1
1732      */
1733     public static function get_quiz_access_information_returns() {
1734         return new external_single_structure(
1735             array(
1736                 'canattempt' => new external_value(PARAM_BOOL, 'Whether the user can do the quiz or not.'),
1737                 'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can edit the quiz settings or not.'),
1738                 'canpreview' => new external_value(PARAM_BOOL, 'Whether the user can preview the quiz or not.'),
1739                 'canreviewmyattempts' => new external_value(PARAM_BOOL, 'Whether the users can review their previous attempts
1740                                                                 or not.'),
1741                 'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the quiz reports or not.'),
1742                 'accessrules' => new external_multiple_structure(
1743                                     new external_value(PARAM_TEXT, 'rule description'), 'list of rules'),
1744                 'activerulenames' => new external_multiple_structure(
1745                                     new external_value(PARAM_PLUGIN, 'rule plugin names'), 'list of active rules'),
1746                 'preventaccessreasons' => new external_multiple_structure(
1747                                             new external_value(PARAM_TEXT, 'access restriction description'), 'list of reasons'),
1748                 'warnings' => new external_warnings(),
1749             )
1750         );
1751     }
1753     /**
1754      * Describes the parameters for get_attempt_access_information.
1755      *
1756      * @return external_external_function_parameters
1757      * @since Moodle 3.1
1758      */
1759     public static function get_attempt_access_information_parameters() {
1760         return new external_function_parameters (
1761             array(
1762                 'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1763                 'attemptid' => new external_value(PARAM_INT, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT, 0),
1764             )
1765         );
1766     }
1768     /**
1769      * Return access information for a given attempt in a quiz.
1770      *
1771      * @param int $quizid quiz instance id
1772      * @param int $attemptid attempt id, 0 for the user last attempt if exists
1773      * @return array of warnings and the access information
1774      * @since Moodle 3.1
1775      * @throws  moodle_quiz_exception
1776      */
1777     public static function get_attempt_access_information($quizid, $attemptid = 0) {
1778         global $DB, $USER;
1780         $warnings = array();
1782         $params = array(
1783             'quizid' => $quizid,
1784             'attemptid' => $attemptid,
1785         );
1786         $params = self::validate_parameters(self::get_attempt_access_information_parameters(), $params);
1788         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1790         $attempttocheck = 0;
1791         if (!empty($params['attemptid'])) {
1792             $attemptobj = quiz_attempt::create($params['attemptid']);
1793             if ($attemptobj->get_userid() != $USER->id) {
1794                 throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
1795             }
1796             $attempttocheck = $attemptobj->get_attempt();
1797         }
1799         // Access manager now.
1800         $quizobj = quiz::create($cm->instance, $USER->id);
1801         $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1802         $timenow = time();
1803         $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1805         $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
1806         $lastfinishedattempt = end($attempts);
1807         if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
1808             $attempts[] = $unfinishedattempt;
1810             // Check if the attempt is now overdue. In that case the state will change.
1811             $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false);
1813             if ($unfinishedattempt->state != quiz_attempt::IN_PROGRESS and $unfinishedattempt->state != quiz_attempt::OVERDUE) {
1814                 $lastfinishedattempt = $unfinishedattempt;
1815             }
1816         }
1817         $numattempts = count($attempts);
1819         if (!$attempttocheck) {
1820             $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt;
1821         }
1823         $result = array();
1824         $result['isfinished'] = $accessmanager->is_finished($numattempts, $lastfinishedattempt);
1825         $result['preventnewattemptreasons'] = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
1827         if ($attempttocheck) {
1828             $endtime = $accessmanager->get_end_time($attempttocheck);
1829             $result['endtime'] = ($endtime === false) ? 0 : $endtime;
1830             $attemptid = $unfinishedattempt ? $unfinishedattempt->id : null;
1831             $result['ispreflightcheckrequired'] = $accessmanager->is_preflight_check_required($attemptid);
1832         }
1834         $result['warnings'] = $warnings;
1835         return $result;
1836     }
1838     /**
1839      * Describes the get_attempt_access_information return value.
1840      *
1841      * @return external_single_structure
1842      * @since Moodle 3.1
1843      */
1844     public static function get_attempt_access_information_returns() {
1845         return new external_single_structure(
1846             array(
1847                 'endtime' => new external_value(PARAM_INT, 'When the attempt must be submitted (determined by rules).',
1848                                                 VALUE_OPTIONAL),
1849                 'isfinished' => new external_value(PARAM_BOOL, 'Whether there is no way the user will ever be allowed to attempt.'),
1850                 'ispreflightcheckrequired' => new external_value(PARAM_BOOL, 'whether a check is required before the user
1851                                                                     starts/continues his attempt.', VALUE_OPTIONAL),
1852                 'preventnewattemptreasons' => new external_multiple_structure(
1853                                                 new external_value(PARAM_TEXT, 'access restriction description'),
1854                                                                     'list of reasons'),
1855                 'warnings' => new external_warnings(),
1856             )
1857         );
1858     }
1860     /**
1861      * Describes the parameters for get_quiz_required_qtypes.
1862      *
1863      * @return external_external_function_parameters
1864      * @since Moodle 3.1
1865      */
1866     public static function get_quiz_required_qtypes_parameters() {
1867         return new external_function_parameters (
1868             array(
1869                 'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1870             )
1871         );
1872     }
1874     /**
1875      * Return the potential question types that would be required for a given quiz.
1876      * Please note that for random question types we return the potential question types in the category choosen.
1877      *
1878      * @param int $quizid quiz instance id
1879      * @return array of warnings and the access information
1880      * @since Moodle 3.1
1881      * @throws  moodle_quiz_exception
1882      */
1883     public static function get_quiz_required_qtypes($quizid) {
1884         global $DB, $USER;
1886         $warnings = array();
1888         $params = array(
1889             'quizid' => $quizid
1890         );
1891         $params = self::validate_parameters(self::get_quiz_required_qtypes_parameters(), $params);
1893         list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1895         $quizobj = quiz::create($cm->instance, $USER->id);
1896         $quizobj->preload_questions();
1897         $quizobj->load_questions();
1899         // Question types used.
1900         $result = array();
1901         $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
1902         $result['warnings'] = $warnings;
1903         return $result;
1904     }
1906     /**
1907      * Describes the get_quiz_required_qtypes return value.
1908      *
1909      * @return external_single_structure
1910      * @since Moodle 3.1
1911      */
1912     public static function get_quiz_required_qtypes_returns() {
1913         return new external_single_structure(
1914             array(
1915                 'questiontypes' => new external_multiple_structure(
1916                                     new external_value(PARAM_PLUGIN, 'question type'), 'list of question types used in the quiz'),
1917                 'warnings' => new external_warnings(),
1918             )
1919         );
1920     }