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 * 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 */
26
27defined('MOODLE_INTERNAL') || die;
28
29require_once($CFG->libdir . '/externallib.php');
30require_once($CFG->dirroot . '/mod/quiz/locallib.php');
31
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 */
41class mod_quiz_external extends external_api {
42
43    /**
44     * Describes the parameters for get_quizzes_by_courses.
45     *
46     * @return 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    }
58
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;
69
70        $warnings = array();
71        $returnedquizzes = array();
72
73        $params = array(
74            'courseids' => $courseids,
75        );
76        $params = self::validate_parameters(self::get_quizzes_by_courses_parameters(), $params);
77
78        $mycourses = array();
79        if (empty($params['courseids'])) {
80            $mycourses = enrol_get_my_courses();
81            $params['courseids'] = array_keys($mycourses);
82        }
83
84        // Ensure there are courseids to loop through.
85        if (!empty($params['courseids'])) {
86
87            list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
88
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);
94
95                // Update quiz with override information.
96                $quiz = quiz_update_effective_access($quiz, $USER->id);
97
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);
105
106                if (has_capability('mod/quiz:view', $context)) {
107                    // Format intro.
108                    $options = array('noclean' => true);
109                    list($quizdetails['intro'], $quizdetails['introformat']) =
110                        external_format_text($quiz->intro, $quiz->introformat, $context->id, 'mod_quiz', 'intro', null, $options);
111
112                    $quizdetails['introfiles'] = external_util::get_area_files($context->id, 'mod_quiz', 'intro', false, false);
113                    $viewablefields = array('timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode',
114                                            'groupingid', 'attempts', 'timelimit', 'grademethod', 'decimalpoints',
115                                            'questiondecimalpoints', 'sumgrades', 'grade', 'preferredbehaviour');
116                    // Some times this function returns just empty.
117                    $hasfeedback = quiz_has_feedback($quiz);
118                    $quizdetails['hasfeedback'] = (!empty($hasfeedback)) ? 1 : 0;
119
120                    $timenow = time();
121                    $quizobj = quiz::create($quiz->id, $USER->id);
122                    $accessmanager = new quiz_access_manager($quizobj, $timenow, has_capability('mod/quiz:ignoretimelimits',
123                                                                $context, null, false));
124
125                    // Fields the user could see if have access to the quiz.
126                    if (!$accessmanager->prevent_access()) {
127                        $quizdetails['hasquestions'] = (int) $quizobj->has_questions();
128                        $quizdetails['autosaveperiod'] = get_config('quiz', 'autosaveperiod');
129
130                        $additionalfields = array('attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmarks',
131                                                    'reviewspecificfeedback', 'reviewgeneralfeedback', 'reviewrightanswer',
132                                                    'reviewoverallfeedback', 'questionsperpage', 'navmethod',
133                                                    'browsersecurity', 'delay1', 'delay2', 'showuserpicture', 'showblocks',
134                                                    'completionattemptsexhausted', 'completionpass', 'overduehandling',
135                                                    'graceperiod', 'canredoquestions', 'allowofflineattempts');
136                        $viewablefields = array_merge($viewablefields, $additionalfields);
137                    }
138
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                    }
144
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    }
157
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    }
279
280
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;
290
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');
294
295        $context = context_module::instance($cm->id);
296        self::validate_context($context);
297
298        return array($quiz, $course, $cm, $context);
299    }
300
301    /**
302     * Describes the parameters for view_quiz.
303     *
304     * @return 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    }
314
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;
325
326        $params = self::validate_parameters(self::view_quiz_parameters(), array('quizid' => $quizid));
327        $warnings = array();
328
329        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
330
331        // Trigger course_module_viewed event and completion.
332        quiz_view($quiz, $course, $cm, $context);
333
334        $result = array();
335        $result['status'] = true;
336        $result['warnings'] = $warnings;
337        return $result;
338    }
339
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    }
354
355    /**
356     * Describes the parameters for get_user_attempts.
357     *
358     * @return 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),
368
369            )
370        );
371    }
372
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;
386
387        $warnings = array();
388
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);
396
397        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
398
399        if (!in_array($params['status'], array('all', 'finished', 'unfinished'))) {
400            throw new invalid_parameter_exception('Invalid status value');
401        }
402
403        // Default value for userid.
404        if (empty($params['userid'])) {
405            $params['userid'] = $USER->id;
406        }
407
408        $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
409        core_user::require_active_user($user);
410
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        }
415
416        // Update quiz with override information.
417        $quiz = quiz_update_effective_access($quiz, $params['userid']);
418        $attempts = quiz_get_user_attempts($quiz->id, $user->id, $params['status'], $params['includepreviews']);
419        $attemptresponse = [];
420        foreach ($attempts as $attempt) {
421            $reviewoptions = quiz_get_review_options($quiz, $attempt, $context);
422            if (!has_capability('mod/quiz:viewreports', $context) &&
423                    ($reviewoptions->marks < question_display_options::MARK_AND_MAX || $attempt->state != quiz_attempt::FINISHED)) {
424                // Blank the mark if the teacher does not allow it.
425                $attempt->sumgrades = null;
426            }
427            $attemptresponse[] = $attempt;
428        }
429        $result = array();
430        $result['attempts'] = $attemptresponse;
431        $result['warnings'] = $warnings;
432        return $result;
433    }
434
435    /**
436     * Describes a single attempt structure.
437     *
438     * @return external_single_structure the attempt structure
439     */
440    private static function attempt_structure() {
441        return new external_single_structure(
442            array(
443                'id' => new external_value(PARAM_INT, 'Attempt id.', VALUE_OPTIONAL),
444                'quiz' => new external_value(PARAM_INT, 'Foreign key reference to the quiz that was attempted.',
445                                                VALUE_OPTIONAL),
446                'userid' => new external_value(PARAM_INT, 'Foreign key reference to the user whose attempt this is.',
447                                                VALUE_OPTIONAL),
448                'attempt' => new external_value(PARAM_INT, 'Sequentially numbers this students attempts at this quiz.',
449                                                VALUE_OPTIONAL),
450                'uniqueid' => new external_value(PARAM_INT, 'Foreign key reference to the question_usage that holds the
451                                                    details of the the question_attempts that make up this quiz
452                                                    attempt.', VALUE_OPTIONAL),
453                'layout' => new external_value(PARAM_RAW, 'Attempt layout.', VALUE_OPTIONAL),
454                'currentpage' => new external_value(PARAM_INT, 'Attempt current page.', VALUE_OPTIONAL),
455                'preview' => new external_value(PARAM_INT, 'Whether is a preview attempt or not.', VALUE_OPTIONAL),
456                'state' => new external_value(PARAM_ALPHA, 'The current state of the attempts. \'inprogress\',
457                                                \'overdue\', \'finished\' or \'abandoned\'.', VALUE_OPTIONAL),
458                'timestart' => new external_value(PARAM_INT, 'Time when the attempt was started.', VALUE_OPTIONAL),
459                'timefinish' => new external_value(PARAM_INT, 'Time when the attempt was submitted.
460                                                    0 if the attempt has not been submitted yet.', VALUE_OPTIONAL),
461                'timemodified' => new external_value(PARAM_INT, 'Last modified time.', VALUE_OPTIONAL),
462                'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.', VALUE_OPTIONAL),
463                'timecheckstate' => new external_value(PARAM_INT, 'Next time quiz cron should check attempt for
464                                                        state changes.  NULL means never check.', VALUE_OPTIONAL),
465                'sumgrades' => new external_value(PARAM_FLOAT, 'Total marks for this attempt.', VALUE_OPTIONAL),
466            )
467        );
468    }
469
470    /**
471     * Describes the get_user_attempts return value.
472     *
473     * @return external_single_structure
474     * @since Moodle 3.1
475     */
476    public static function get_user_attempts_returns() {
477        return new external_single_structure(
478            array(
479                'attempts' => new external_multiple_structure(self::attempt_structure()),
480                'warnings' => new external_warnings(),
481            )
482        );
483    }
484
485    /**
486     * Describes the parameters for get_user_best_grade.
487     *
488     * @return external_function_parameters
489     * @since Moodle 3.1
490     */
491    public static function get_user_best_grade_parameters() {
492        return new external_function_parameters (
493            array(
494                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
495                'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0),
496            )
497        );
498    }
499
500    /**
501     * Get the best current grade for the given user on a quiz.
502     *
503     * @param int $quizid quiz instance id
504     * @param int $userid user id
505     * @return array of warnings and the grade information
506     * @since Moodle 3.1
507     */
508    public static function get_user_best_grade($quizid, $userid = 0) {
509        global $DB, $USER;
510
511        $warnings = array();
512
513        $params = array(
514            'quizid' => $quizid,
515            'userid' => $userid,
516        );
517        $params = self::validate_parameters(self::get_user_best_grade_parameters(), $params);
518
519        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
520
521        // Default value for userid.
522        if (empty($params['userid'])) {
523            $params['userid'] = $USER->id;
524        }
525
526        $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
527        core_user::require_active_user($user);
528
529        // Extra checks so only users with permissions can view other users attempts.
530        if ($USER->id != $user->id) {
531            require_capability('mod/quiz:viewreports', $context);
532        }
533
534        $result = array();
535
536        // This code was mostly copied from mod/quiz/view.php. We need to make the web service logic consistent.
537        // Get this user's attempts.
538        $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all');
539        $canviewgrade = false;
540        if ($attempts) {
541            if ($USER->id != $user->id) {
542                // No need to check the permission here. We did it at by require_capability('mod/quiz:viewreports', $context).
543                $canviewgrade = true;
544            } else {
545                // Work out which columns we need, taking account what data is available in each attempt.
546                [$notused, $alloptions] = quiz_get_combined_reviewoptions($quiz, $attempts);
547                $canviewgrade = $alloptions->marks >= question_display_options::MARK_AND_MAX;
548            }
549        }
550
551        $grade = $canviewgrade ? quiz_get_best_grade($quiz, $user->id) : null;
552
553        if ($grade === null) {
554            $result['hasgrade'] = false;
555        } else {
556            $result['hasgrade'] = true;
557            $result['grade'] = $grade;
558        }
559        $result['warnings'] = $warnings;
560        return $result;
561    }
562
563    /**
564     * Describes the get_user_best_grade return value.
565     *
566     * @return external_single_structure
567     * @since Moodle 3.1
568     */
569    public static function get_user_best_grade_returns() {
570        return new external_single_structure(
571            array(
572                'hasgrade' => new external_value(PARAM_BOOL, 'Whether the user has a grade on the given quiz.'),
573                'grade' => new external_value(PARAM_FLOAT, 'The grade (only if the user has a grade).', VALUE_OPTIONAL),
574                'warnings' => new external_warnings(),
575            )
576        );
577    }
578
579    /**
580     * Describes the parameters for get_combined_review_options.
581     *
582     * @return external_function_parameters
583     * @since Moodle 3.1
584     */
585    public static function get_combined_review_options_parameters() {
586        return new external_function_parameters (
587            array(
588                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
589                'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
590
591            )
592        );
593    }
594
595    /**
596     * Combines the review options from a number of different quiz attempts.
597     *
598     * @param int $quizid quiz instance id
599     * @param int $userid user id (empty for current user)
600     * @return array of warnings and the review options
601     * @since Moodle 3.1
602     */
603    public static function get_combined_review_options($quizid, $userid = 0) {
604        global $DB, $USER;
605
606        $warnings = array();
607
608        $params = array(
609            'quizid' => $quizid,
610            'userid' => $userid,
611        );
612        $params = self::validate_parameters(self::get_combined_review_options_parameters(), $params);
613
614        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
615
616        // Default value for userid.
617        if (empty($params['userid'])) {
618            $params['userid'] = $USER->id;
619        }
620
621        $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
622        core_user::require_active_user($user);
623
624        // Extra checks so only users with permissions can view other users attempts.
625        if ($USER->id != $user->id) {
626            require_capability('mod/quiz:viewreports', $context);
627        }
628
629        $attempts = quiz_get_user_attempts($quiz->id, $user->id, 'all', true);
630
631        $result = array();
632        $result['someoptions'] = [];
633        $result['alloptions'] = [];
634
635        list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts);
636
637        foreach (array('someoptions', 'alloptions') as $typeofoption) {
638            foreach ($$typeofoption as $key => $value) {
639                $result[$typeofoption][] = array(
640                    "name" => $key,
641                    "value" => (!empty($value)) ? $value : 0
642                );
643            }
644        }
645
646        $result['warnings'] = $warnings;
647        return $result;
648    }
649
650    /**
651     * Describes the get_combined_review_options return value.
652     *
653     * @return external_single_structure
654     * @since Moodle 3.1
655     */
656    public static function get_combined_review_options_returns() {
657        return new external_single_structure(
658            array(
659                'someoptions' => new external_multiple_structure(
660                    new external_single_structure(
661                        array(
662                            'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
663                            'value' => new external_value(PARAM_INT, 'option value'),
664                        )
665                    )
666                ),
667                'alloptions' => new external_multiple_structure(
668                    new external_single_structure(
669                        array(
670                            'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'),
671                            'value' => new external_value(PARAM_INT, 'option value'),
672                        )
673                    )
674                ),
675                'warnings' => new external_warnings(),
676            )
677        );
678    }
679
680    /**
681     * Describes the parameters for start_attempt.
682     *
683     * @return external_function_parameters
684     * @since Moodle 3.1
685     */
686    public static function start_attempt_parameters() {
687        return new external_function_parameters (
688            array(
689                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
690                'preflightdata' => new external_multiple_structure(
691                    new external_single_structure(
692                        array(
693                            'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
694                            'value' => new external_value(PARAM_RAW, 'data value'),
695                        )
696                    ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
697                ),
698                'forcenew' => new external_value(PARAM_BOOL, 'Whether to force a new attempt or not.', VALUE_DEFAULT, false),
699
700            )
701        );
702    }
703
704    /**
705     * Starts a new attempt at a quiz.
706     *
707     * @param int $quizid quiz instance id
708     * @param array $preflightdata preflight required data (like passwords)
709     * @param bool $forcenew Whether to force a new attempt or not.
710     * @return array of warnings and the attempt basic data
711     * @since Moodle 3.1
712     * @throws moodle_quiz_exception
713     */
714    public static function start_attempt($quizid, $preflightdata = array(), $forcenew = false) {
715        global $DB, $USER;
716
717        $warnings = array();
718        $attempt = array();
719
720        $params = array(
721            'quizid' => $quizid,
722            'preflightdata' => $preflightdata,
723            'forcenew' => $forcenew,
724        );
725        $params = self::validate_parameters(self::start_attempt_parameters(), $params);
726        $forcenew = $params['forcenew'];
727
728        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
729
730        $quizobj = quiz::create($cm->instance, $USER->id);
731
732        // Check questions.
733        if (!$quizobj->has_questions()) {
734            throw new moodle_quiz_exception($quizobj, 'noquestionsfound');
735        }
736
737        // Create an object to manage all the other (non-roles) access rules.
738        $timenow = time();
739        $accessmanager = $quizobj->get_access_manager($timenow);
740
741        // Validate permissions for creating a new attempt and start a new preview attempt if required.
742        list($currentattemptid, $attemptnumber, $lastattempt, $messages, $page) =
743            quiz_validate_new_attempt($quizobj, $accessmanager, $forcenew, -1, false);
744
745        // Check access.
746        if (!$quizobj->is_preview_user() && $messages) {
747            // Create warnings with the exact messages.
748            foreach ($messages as $message) {
749                $warnings[] = array(
750                    'item' => 'quiz',
751                    'itemid' => $quiz->id,
752                    'warningcode' => '1',
753                    'message' => clean_text($message, PARAM_TEXT)
754                );
755            }
756        } else {
757            if ($accessmanager->is_preflight_check_required($currentattemptid)) {
758                // Need to do some checks before allowing the user to continue.
759
760                $provideddata = array();
761                foreach ($params['preflightdata'] as $data) {
762                    $provideddata[$data['name']] = $data['value'];
763                }
764
765                $errors = $accessmanager->validate_preflight_check($provideddata, [], $currentattemptid);
766
767                if (!empty($errors)) {
768                    throw new moodle_quiz_exception($quizobj, array_shift($errors));
769                }
770
771                // Pre-flight check passed.
772                $accessmanager->notify_preflight_check_passed($currentattemptid);
773            }
774
775            if ($currentattemptid) {
776                if ($lastattempt->state == quiz_attempt::OVERDUE) {
777                    throw new moodle_quiz_exception($quizobj, 'stateoverdue');
778                } else {
779                    throw new moodle_quiz_exception($quizobj, 'attemptstillinprogress');
780                }
781            }
782            $offlineattempt = WS_SERVER ? true : false;
783            $attempt = quiz_prepare_and_start_new_attempt($quizobj, $attemptnumber, $lastattempt, $offlineattempt);
784        }
785
786        $result = array();
787        $result['attempt'] = $attempt;
788        $result['warnings'] = $warnings;
789        return $result;
790    }
791
792    /**
793     * Describes the start_attempt return value.
794     *
795     * @return external_single_structure
796     * @since Moodle 3.1
797     */
798    public static function start_attempt_returns() {
799        return new external_single_structure(
800            array(
801                'attempt' => self::attempt_structure(),
802                'warnings' => new external_warnings(),
803            )
804        );
805    }
806
807    /**
808     * Utility function for validating a given attempt
809     *
810     * @param  array $params array of parameters including the attemptid and preflight data
811     * @param  bool $checkaccessrules whether to check the quiz access rules or not
812     * @param  bool $failifoverdue whether to return error if the attempt is overdue
813     * @return  array containing the attempt object and access messages
814     * @throws moodle_quiz_exception
815     * @since  Moodle 3.1
816     */
817    protected static function validate_attempt($params, $checkaccessrules = true, $failifoverdue = true) {
818        global $USER;
819
820        $attemptobj = quiz_attempt::create($params['attemptid']);
821
822        $context = context_module::instance($attemptobj->get_cm()->id);
823        self::validate_context($context);
824
825        // Check that this attempt belongs to this user.
826        if ($attemptobj->get_userid() != $USER->id) {
827            throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
828        }
829
830        // General capabilities check.
831        $ispreviewuser = $attemptobj->is_preview_user();
832        if (!$ispreviewuser) {
833            $attemptobj->require_capability('mod/quiz:attempt');
834        }
835
836        // Check the access rules.
837        $accessmanager = $attemptobj->get_access_manager(time());
838        $messages = array();
839        if ($checkaccessrules) {
840            // If the attempt is now overdue, or abandoned, deal with that.
841            $attemptobj->handle_if_time_expired(time(), true);
842
843            $messages = $accessmanager->prevent_access();
844            if (!$ispreviewuser && $messages) {
845                throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror');
846            }
847        }
848
849        // Attempt closed?.
850        if ($attemptobj->is_finished()) {
851            throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed');
852        } else if ($failifoverdue && $attemptobj->get_state() == quiz_attempt::OVERDUE) {
853            throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue');
854        }
855
856        // User submitted data (like the quiz password).
857        if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
858            $provideddata = array();
859            foreach ($params['preflightdata'] as $data) {
860                $provideddata[$data['name']] = $data['value'];
861            }
862
863            $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']);
864            if (!empty($errors)) {
865                throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors));
866            }
867            // Pre-flight check passed.
868            $accessmanager->notify_preflight_check_passed($params['attemptid']);
869        }
870
871        if (isset($params['page'])) {
872            // Check if the page is out of range.
873            if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) {
874                throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number');
875            }
876
877            // Prevent out of sequence access.
878            if (!$attemptobj->check_page_access($params['page'])) {
879                throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
880            }
881
882            // Check slots.
883            $slots = $attemptobj->get_slots($params['page']);
884
885            if (empty($slots)) {
886                throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound');
887            }
888        }
889
890        return array($attemptobj, $messages);
891    }
892
893    /**
894     * Describes a single question structure.
895     *
896     * @return external_single_structure the question data. Some fields may not be returned depending on the quiz display settings.
897     * @since  Moodle 3.1
898     * @since Moodle 3.2 blockedbyprevious parameter added.
899     */
900    private static function question_structure() {
901        return new external_single_structure(
902            array(
903                'slot' => new external_value(PARAM_INT, 'slot number'),
904                'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'),
905                'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'),
906                'html' => new external_value(PARAM_RAW, 'the question rendered'),
907                'responsefileareas' => new external_multiple_structure(
908                    new external_single_structure(
909                        array(
910                            'area' => new external_value(PARAM_NOTAGS, 'File area name'),
911                            'files' => new external_files('Response files for the question', VALUE_OPTIONAL),
912                        )
913                    ), 'Response file areas including files', VALUE_OPTIONAL
914                ),
915                'sequencecheck' => new external_value(PARAM_INT, 'the number of real steps in this attempt', VALUE_OPTIONAL),
916                'lastactiontime' => new external_value(PARAM_INT, 'the timestamp of the most recent step in this question attempt',
917                                                        VALUE_OPTIONAL),
918                'hasautosavedstep' => new external_value(PARAM_BOOL, 'whether this question attempt has autosaved data',
919                                                            VALUE_OPTIONAL),
920                'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'),
921                'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL),
922                'state' => new external_value(PARAM_ALPHA, 'the state where the question is in.
923                    It will not be returned if the user cannot see it due to the quiz display correctness settings.',
924                    VALUE_OPTIONAL),
925                'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL),
926                'blockedbyprevious' => new external_value(PARAM_BOOL, 'whether the question is blocked by the previous question',
927                    VALUE_OPTIONAL),
928                'mark' => new external_value(PARAM_RAW, 'the mark awarded.
929                    It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
930                'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt.
931                    It will be returned only if the user is allowed to see it.', VALUE_OPTIONAL),
932                'settings' => new external_value(PARAM_RAW, 'Question settings (JSON encoded).', VALUE_OPTIONAL),
933            ),
934            'The question data. Some fields may not be returned depending on the quiz display settings.'
935        );
936    }
937
938    /**
939     * Return questions information for a given attempt.
940     *
941     * @param  quiz_attempt  $attemptobj  the quiz attempt object
942     * @param  bool  $review  whether if we are in review mode or not
943     * @param  mixed  $page  string 'all' or integer page number
944     * @return array array of questions including data
945     */
946    private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') {
947        global $PAGE;
948
949        $questions = array();
950        $contextid = $attemptobj->get_quizobj()->get_context()->id;
951        $displayoptions = $attemptobj->get_display_options($review);
952        $renderer = $PAGE->get_renderer('mod_quiz');
953        $contextid = $attemptobj->get_quizobj()->get_context()->id;
954
955        foreach ($attemptobj->get_slots($page) as $slot) {
956            $qtype = $attemptobj->get_question_type_name($slot);
957            $qattempt = $attemptobj->get_question_attempt($slot);
958            $questiondef = $qattempt->get_question(true);
959
960            // Get response files (for questions like essay that allows attachments).
961            $responsefileareas = [];
962            foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) {
963                if ($files = $attemptobj->get_question_attempt($slot)->get_last_qt_files($area, $contextid)) {
964                    $responsefileareas[$area]['area'] = $area;
965                    $responsefileareas[$area]['files'] = [];
966
967                    foreach ($files as $file) {
968                        $responsefileareas[$area]['files'][] = array(
969                            'filename' => $file->get_filename(),
970                            'fileurl' => $qattempt->get_response_file_url($file),
971                            'filesize' => $file->get_filesize(),
972                            'filepath' => $file->get_filepath(),
973                            'mimetype' => $file->get_mimetype(),
974                            'timemodified' => $file->get_timemodified(),
975                        );
976                    }
977                }
978            }
979
980            // Check display settings for question.
981            $settings = $questiondef->get_question_definition_for_external_rendering($qattempt, $displayoptions);
982
983            $question = array(
984                'slot' => $slot,
985                'type' => $qtype,
986                'page' => $attemptobj->get_question_page($slot),
987                'flagged' => $attemptobj->is_question_flagged($slot),
988                'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code(),
989                'responsefileareas' => $responsefileareas,
990                'sequencecheck' => $qattempt->get_sequence_check_count(),
991                'lastactiontime' => $qattempt->get_last_step()->get_timecreated(),
992                'hasautosavedstep' => $qattempt->has_autosaved_step(),
993                'settings' => !empty($settings) ? json_encode($settings) : null,
994            );
995
996            if ($attemptobj->is_real_question($slot)) {
997                $question['number'] = $attemptobj->get_question_number($slot);
998                $showcorrectness = $displayoptions->correctness && $qattempt->has_marks();
999                if ($showcorrectness) {
1000                    $question['state'] = (string) $attemptobj->get_question_state($slot);
1001                }
1002                $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness);
1003                $question['blockedbyprevious'] = $attemptobj->is_blocked_by_previous_question($slot);
1004            }
1005            if ($displayoptions->marks >= question_display_options::MAX_ONLY) {
1006                $question['maxmark'] = $qattempt->get_max_mark();
1007            }
1008            if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
1009                $question['mark'] = $attemptobj->get_question_mark($slot);
1010            }
1011
1012            $questions[] = $question;
1013        }
1014        return $questions;
1015    }
1016
1017    /**
1018     * Describes the parameters for get_attempt_data.
1019     *
1020     * @return external_function_parameters
1021     * @since Moodle 3.1
1022     */
1023    public static function get_attempt_data_parameters() {
1024        return new external_function_parameters (
1025            array(
1026                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1027                'page' => new external_value(PARAM_INT, 'page number'),
1028                'preflightdata' => new external_multiple_structure(
1029                    new external_single_structure(
1030                        array(
1031                            'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1032                            'value' => new external_value(PARAM_RAW, 'data value'),
1033                        )
1034                    ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1035                )
1036            )
1037        );
1038    }
1039
1040    /**
1041     * Returns information for the given attempt page for a quiz attempt in progress.
1042     *
1043     * @param int $attemptid attempt id
1044     * @param int $page page number
1045     * @param array $preflightdata preflight required data (like passwords)
1046     * @return array of warnings and the attempt data, next page, message and questions
1047     * @since Moodle 3.1
1048     * @throws moodle_quiz_exceptions
1049     */
1050    public static function get_attempt_data($attemptid, $page, $preflightdata = array()) {
1051
1052        $warnings = array();
1053
1054        $params = array(
1055            'attemptid' => $attemptid,
1056            'page' => $page,
1057            'preflightdata' => $preflightdata,
1058        );
1059        $params = self::validate_parameters(self::get_attempt_data_parameters(), $params);
1060
1061        list($attemptobj, $messages) = self::validate_attempt($params);
1062
1063        if ($attemptobj->is_last_page($params['page'])) {
1064            $nextpage = -1;
1065        } else {
1066            $nextpage = $params['page'] + 1;
1067        }
1068
1069        $result = array();
1070        $result['attempt'] = $attemptobj->get_attempt();
1071        $result['messages'] = $messages;
1072        $result['nextpage'] = $nextpage;
1073        $result['warnings'] = $warnings;
1074        $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']);
1075
1076        return $result;
1077    }
1078
1079    /**
1080     * Describes the get_attempt_data return value.
1081     *
1082     * @return external_single_structure
1083     * @since Moodle 3.1
1084     */
1085    public static function get_attempt_data_returns() {
1086        return new external_single_structure(
1087            array(
1088                'attempt' => self::attempt_structure(),
1089                'messages' => new external_multiple_structure(
1090                    new external_value(PARAM_TEXT, 'access message'),
1091                    'access messages, will only be returned for users with mod/quiz:preview capability,
1092                    for other users this method will throw an exception if there are messages'),
1093                'nextpage' => new external_value(PARAM_INT, 'next page number'),
1094                'questions' => new external_multiple_structure(self::question_structure()),
1095                'warnings' => new external_warnings(),
1096            )
1097        );
1098    }
1099
1100    /**
1101     * Describes the parameters for get_attempt_summary.
1102     *
1103     * @return external_function_parameters
1104     * @since Moodle 3.1
1105     */
1106    public static function get_attempt_summary_parameters() {
1107        return new external_function_parameters (
1108            array(
1109                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1110                'preflightdata' => new external_multiple_structure(
1111                    new external_single_structure(
1112                        array(
1113                            'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1114                            'value' => new external_value(PARAM_RAW, 'data value'),
1115                        )
1116                    ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1117                )
1118            )
1119        );
1120    }
1121
1122    /**
1123     * Returns a summary of a quiz attempt before it is submitted.
1124     *
1125     * @param int $attemptid attempt id
1126     * @param int $preflightdata preflight required data (like passwords)
1127     * @return array of warnings and the attempt summary data for each question
1128     * @since Moodle 3.1
1129     */
1130    public static function get_attempt_summary($attemptid, $preflightdata = array()) {
1131
1132        $warnings = array();
1133
1134        $params = array(
1135            'attemptid' => $attemptid,
1136            'preflightdata' => $preflightdata,
1137        );
1138        $params = self::validate_parameters(self::get_attempt_summary_parameters(), $params);
1139
1140        list($attemptobj, $messages) = self::validate_attempt($params, true, false);
1141
1142        $result = array();
1143        $result['warnings'] = $warnings;
1144        $result['questions'] = self::get_attempt_questions_data($attemptobj, false, 'all');
1145
1146        return $result;
1147    }
1148
1149    /**
1150     * Describes the get_attempt_summary return value.
1151     *
1152     * @return external_single_structure
1153     * @since Moodle 3.1
1154     */
1155    public static function get_attempt_summary_returns() {
1156        return new external_single_structure(
1157            array(
1158                'questions' => new external_multiple_structure(self::question_structure()),
1159                'warnings' => new external_warnings(),
1160            )
1161        );
1162    }
1163
1164    /**
1165     * Describes the parameters for save_attempt.
1166     *
1167     * @return external_function_parameters
1168     * @since Moodle 3.1
1169     */
1170    public static function save_attempt_parameters() {
1171        return new external_function_parameters (
1172            array(
1173                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1174                'data' => new external_multiple_structure(
1175                    new external_single_structure(
1176                        array(
1177                            'name' => new external_value(PARAM_RAW, 'data name'),
1178                            'value' => new external_value(PARAM_RAW, 'data value'),
1179                        )
1180                    ), 'the data to be saved'
1181                ),
1182                'preflightdata' => new external_multiple_structure(
1183                    new external_single_structure(
1184                        array(
1185                            'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1186                            'value' => new external_value(PARAM_RAW, 'data value'),
1187                        )
1188                    ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1189                )
1190            )
1191        );
1192    }
1193
1194    /**
1195     * Processes save requests during the quiz. This function is intended for the quiz auto-save feature.
1196     *
1197     * @param int $attemptid attempt id
1198     * @param array $data the data to be saved
1199     * @param  array $preflightdata preflight required data (like passwords)
1200     * @return array of warnings and execution result
1201     * @since Moodle 3.1
1202     */
1203    public static function save_attempt($attemptid, $data, $preflightdata = array()) {
1204        global $DB, $USER;
1205
1206        $warnings = array();
1207
1208        $params = array(
1209            'attemptid' => $attemptid,
1210            'data' => $data,
1211            'preflightdata' => $preflightdata,
1212        );
1213        $params = self::validate_parameters(self::save_attempt_parameters(), $params);
1214
1215        // Add a page, required by validate_attempt.
1216        list($attemptobj, $messages) = self::validate_attempt($params);
1217
1218        // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1219        if (WS_SERVER || PHPUNIT_TEST) {
1220            $USER->ignoresesskey = true;
1221        }
1222        $transaction = $DB->start_delegated_transaction();
1223        // Create the $_POST object required by the question engine.
1224        $_POST = array();
1225        foreach ($data as $element) {
1226            $_POST[$element['name']] = $element['value'];
1227            // Some deep core functions like file_get_submitted_draft_itemid() also requires $_REQUEST to be filled.
1228            $_REQUEST[$element['name']] = $element['value'];
1229        }
1230        $timenow = time();
1231        // Update the timemodifiedoffline field.
1232        $attemptobj->set_offline_modified_time($timenow);
1233        $attemptobj->process_auto_save($timenow);
1234        $transaction->allow_commit();
1235
1236        $result = array();
1237        $result['status'] = true;
1238        $result['warnings'] = $warnings;
1239        return $result;
1240    }
1241
1242    /**
1243     * Describes the save_attempt return value.
1244     *
1245     * @return external_single_structure
1246     * @since Moodle 3.1
1247     */
1248    public static function save_attempt_returns() {
1249        return new external_single_structure(
1250            array(
1251                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1252                'warnings' => new external_warnings(),
1253            )
1254        );
1255    }
1256
1257    /**
1258     * Describes the parameters for process_attempt.
1259     *
1260     * @return external_function_parameters
1261     * @since Moodle 3.1
1262     */
1263    public static function process_attempt_parameters() {
1264        return new external_function_parameters (
1265            array(
1266                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1267                'data' => new external_multiple_structure(
1268                    new external_single_structure(
1269                        array(
1270                            'name' => new external_value(PARAM_RAW, 'data name'),
1271                            'value' => new external_value(PARAM_RAW, 'data value'),
1272                        )
1273                    ),
1274                    'the data to be saved', VALUE_DEFAULT, array()
1275                ),
1276                'finishattempt' => new external_value(PARAM_BOOL, 'whether to finish or not the attempt', VALUE_DEFAULT, false),
1277                'timeup' => new external_value(PARAM_BOOL, 'whether the WS was called by a timer when the time is up',
1278                                                VALUE_DEFAULT, false),
1279                'preflightdata' => new external_multiple_structure(
1280                    new external_single_structure(
1281                        array(
1282                            'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1283                            'value' => new external_value(PARAM_RAW, 'data value'),
1284                        )
1285                    ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1286                )
1287            )
1288        );
1289    }
1290
1291    /**
1292     * Process responses during an attempt at a quiz and also deals with attempts finishing.
1293     *
1294     * @param int $attemptid attempt id
1295     * @param array $data the data to be saved
1296     * @param bool $finishattempt whether to finish or not the attempt
1297     * @param bool $timeup whether the WS was called by a timer when the time is up
1298     * @param array $preflightdata preflight required data (like passwords)
1299     * @return array of warnings and the attempt state after the processing
1300     * @since Moodle 3.1
1301     */
1302    public static function process_attempt($attemptid, $data, $finishattempt = false, $timeup = false, $preflightdata = array()) {
1303        global $USER;
1304
1305        $warnings = array();
1306
1307        $params = array(
1308            'attemptid' => $attemptid,
1309            'data' => $data,
1310            'finishattempt' => $finishattempt,
1311            'timeup' => $timeup,
1312            'preflightdata' => $preflightdata,
1313        );
1314        $params = self::validate_parameters(self::process_attempt_parameters(), $params);
1315
1316        // Do not check access manager rules and evaluate fail if overdue.
1317        $attemptobj = quiz_attempt::create($params['attemptid']);
1318        $failifoverdue = !($attemptobj->get_quizobj()->get_quiz()->overduehandling == 'graceperiod');
1319
1320        list($attemptobj, $messages) = self::validate_attempt($params, false, $failifoverdue);
1321
1322        // Prevent functions like file_get_submitted_draft_itemid() or form library requiring a sesskey for WS requests.
1323        if (WS_SERVER || PHPUNIT_TEST) {
1324            $USER->ignoresesskey = true;
1325        }
1326        // Create the $_POST object required by the question engine.
1327        $_POST = array();
1328        foreach ($params['data'] as $element) {
1329            $_POST[$element['name']] = $element['value'];
1330            $_REQUEST[$element['name']] = $element['value'];
1331        }
1332        $timenow = time();
1333        $finishattempt = $params['finishattempt'];
1334        $timeup = $params['timeup'];
1335
1336        $result = array();
1337        // Update the timemodifiedoffline field.
1338        $attemptobj->set_offline_modified_time($timenow);
1339        $result['state'] = $attemptobj->process_attempt($timenow, $finishattempt, $timeup, 0);
1340
1341        $result['warnings'] = $warnings;
1342        return $result;
1343    }
1344
1345    /**
1346     * Describes the process_attempt return value.
1347     *
1348     * @return external_single_structure
1349     * @since Moodle 3.1
1350     */
1351    public static function process_attempt_returns() {
1352        return new external_single_structure(
1353            array(
1354                'state' => new external_value(PARAM_ALPHANUMEXT, 'state: the new attempt state:
1355                                                                    inprogress, finished, overdue, abandoned'),
1356                'warnings' => new external_warnings(),
1357            )
1358        );
1359    }
1360
1361    /**
1362     * Validate an attempt finished for review. The attempt would be reviewed by a user or a teacher.
1363     *
1364     * @param  array $params Array of parameters including the attemptid
1365     * @return  array containing the attempt object and display options
1366     * @since  Moodle 3.1
1367     * @throws  moodle_exception
1368     * @throws  moodle_quiz_exception
1369     */
1370    protected static function validate_attempt_review($params) {
1371
1372        $attemptobj = quiz_attempt::create($params['attemptid']);
1373        $attemptobj->check_review_capability();
1374
1375        $displayoptions = $attemptobj->get_display_options(true);
1376        if ($attemptobj->is_own_attempt()) {
1377            if (!$attemptobj->is_finished()) {
1378                throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptclosed');
1379            } else if (!$displayoptions->attempt) {
1380                throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreview', null, '',
1381                    $attemptobj->cannot_review_message());
1382            }
1383        } else if (!$attemptobj->is_review_allowed()) {
1384            throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
1385        }
1386        return array($attemptobj, $displayoptions);
1387    }
1388
1389    /**
1390     * Describes the parameters for get_attempt_review.
1391     *
1392     * @return external_function_parameters
1393     * @since Moodle 3.1
1394     */
1395    public static function get_attempt_review_parameters() {
1396        return new external_function_parameters (
1397            array(
1398                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1399                'page' => new external_value(PARAM_INT, 'page number, empty for all the questions in all the pages',
1400                                                VALUE_DEFAULT, -1),
1401            )
1402        );
1403    }
1404
1405    /**
1406     * Returns review information for the given finished attempt, can be used by users or teachers.
1407     *
1408     * @param int $attemptid attempt id
1409     * @param int $page page number, empty for all the questions in all the pages
1410     * @return array of warnings and the attempt data, feedback and questions
1411     * @since Moodle 3.1
1412     * @throws  moodle_exception
1413     * @throws  moodle_quiz_exception
1414     */
1415    public static function get_attempt_review($attemptid, $page = -1) {
1416        global $PAGE;
1417
1418        $warnings = array();
1419
1420        $params = array(
1421            'attemptid' => $attemptid,
1422            'page' => $page,
1423        );
1424        $params = self::validate_parameters(self::get_attempt_review_parameters(), $params);
1425
1426        list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1427
1428        if ($params['page'] !== -1) {
1429            $page = $attemptobj->force_page_number_into_range($params['page']);
1430        } else {
1431            $page = 'all';
1432        }
1433
1434        // Prepare the output.
1435        $result = array();
1436        $result['attempt'] = $attemptobj->get_attempt();
1437        $result['questions'] = self::get_attempt_questions_data($attemptobj, true, $page, true);
1438
1439        $result['additionaldata'] = array();
1440        // Summary data (from behaviours).
1441        $summarydata = $attemptobj->get_additional_summary_data($displayoptions);
1442        foreach ($summarydata as $key => $data) {
1443            // This text does not need formatting (no need for external_format_[string|text]).
1444            $result['additionaldata'][] = array(
1445                'id' => $key,
1446                'title' => $data['title'], $attemptobj->get_quizobj()->get_context()->id,
1447                'content' => $data['content'],
1448            );
1449        }
1450
1451        // Feedback if there is any, and the user is allowed to see it now.
1452        $grade = quiz_rescale_grade($attemptobj->get_attempt()->sumgrades, $attemptobj->get_quiz(), false);
1453
1454        $feedback = $attemptobj->get_overall_feedback($grade);
1455        if ($displayoptions->overallfeedback && $feedback) {
1456            $result['additionaldata'][] = array(
1457                'id' => 'feedback',
1458                'title' => get_string('feedback', 'quiz'),
1459                'content' => $feedback,
1460            );
1461        }
1462
1463        $result['grade'] = $grade;
1464        $result['warnings'] = $warnings;
1465        return $result;
1466    }
1467
1468    /**
1469     * Describes the get_attempt_review return value.
1470     *
1471     * @return external_single_structure
1472     * @since Moodle 3.1
1473     */
1474    public static function get_attempt_review_returns() {
1475        return new external_single_structure(
1476            array(
1477                'grade' => new external_value(PARAM_RAW, 'grade for the quiz (or empty or "notyetgraded")'),
1478                'attempt' => self::attempt_structure(),
1479                'additionaldata' => new external_multiple_structure(
1480                    new external_single_structure(
1481                        array(
1482                            'id' => new external_value(PARAM_ALPHANUMEXT, 'id of the data'),
1483                            'title' => new external_value(PARAM_TEXT, 'data title'),
1484                            'content' => new external_value(PARAM_RAW, 'data content'),
1485                        )
1486                    )
1487                ),
1488                'questions' => new external_multiple_structure(self::question_structure()),
1489                'warnings' => new external_warnings(),
1490            )
1491        );
1492    }
1493
1494    /**
1495     * Describes the parameters for view_attempt.
1496     *
1497     * @return external_function_parameters
1498     * @since Moodle 3.1
1499     */
1500    public static function view_attempt_parameters() {
1501        return new external_function_parameters (
1502            array(
1503                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1504                'page' => new external_value(PARAM_INT, 'page number'),
1505                'preflightdata' => new external_multiple_structure(
1506                    new external_single_structure(
1507                        array(
1508                            'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1509                            'value' => new external_value(PARAM_RAW, 'data value'),
1510                        )
1511                    ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1512                )
1513            )
1514        );
1515    }
1516
1517    /**
1518     * Trigger the attempt viewed event.
1519     *
1520     * @param int $attemptid attempt id
1521     * @param int $page page number
1522     * @param array $preflightdata preflight required data (like passwords)
1523     * @return array of warnings and status result
1524     * @since Moodle 3.1
1525     */
1526    public static function view_attempt($attemptid, $page, $preflightdata = array()) {
1527
1528        $warnings = array();
1529
1530        $params = array(
1531            'attemptid' => $attemptid,
1532            'page' => $page,
1533            'preflightdata' => $preflightdata,
1534        );
1535        $params = self::validate_parameters(self::view_attempt_parameters(), $params);
1536        list($attemptobj, $messages) = self::validate_attempt($params);
1537
1538        // Log action.
1539        $attemptobj->fire_attempt_viewed_event();
1540
1541        // Update attempt page, throwing an exception if $page is not valid.
1542        if (!$attemptobj->set_currentpage($params['page'])) {
1543            throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access');
1544        }
1545
1546        $result = array();
1547        $result['status'] = true;
1548        $result['warnings'] = $warnings;
1549        return $result;
1550    }
1551
1552    /**
1553     * Describes the view_attempt return value.
1554     *
1555     * @return external_single_structure
1556     * @since Moodle 3.1
1557     */
1558    public static function view_attempt_returns() {
1559        return new external_single_structure(
1560            array(
1561                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1562                'warnings' => new external_warnings(),
1563            )
1564        );
1565    }
1566
1567    /**
1568     * Describes the parameters for view_attempt_summary.
1569     *
1570     * @return external_function_parameters
1571     * @since Moodle 3.1
1572     */
1573    public static function view_attempt_summary_parameters() {
1574        return new external_function_parameters (
1575            array(
1576                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1577                'preflightdata' => new external_multiple_structure(
1578                    new external_single_structure(
1579                        array(
1580                            'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'),
1581                            'value' => new external_value(PARAM_RAW, 'data value'),
1582                        )
1583                    ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array()
1584                )
1585            )
1586        );
1587    }
1588
1589    /**
1590     * Trigger the attempt summary viewed event.
1591     *
1592     * @param int $attemptid attempt id
1593     * @param array $preflightdata preflight required data (like passwords)
1594     * @return array of warnings and status result
1595     * @since Moodle 3.1
1596     */
1597    public static function view_attempt_summary($attemptid, $preflightdata = array()) {
1598
1599        $warnings = array();
1600
1601        $params = array(
1602            'attemptid' => $attemptid,
1603            'preflightdata' => $preflightdata,
1604        );
1605        $params = self::validate_parameters(self::view_attempt_summary_parameters(), $params);
1606        list($attemptobj, $messages) = self::validate_attempt($params);
1607
1608        // Log action.
1609        $attemptobj->fire_attempt_summary_viewed_event();
1610
1611        $result = array();
1612        $result['status'] = true;
1613        $result['warnings'] = $warnings;
1614        return $result;
1615    }
1616
1617    /**
1618     * Describes the view_attempt_summary return value.
1619     *
1620     * @return external_single_structure
1621     * @since Moodle 3.1
1622     */
1623    public static function view_attempt_summary_returns() {
1624        return new external_single_structure(
1625            array(
1626                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1627                'warnings' => new external_warnings(),
1628            )
1629        );
1630    }
1631
1632    /**
1633     * Describes the parameters for view_attempt_review.
1634     *
1635     * @return external_function_parameters
1636     * @since Moodle 3.1
1637     */
1638    public static function view_attempt_review_parameters() {
1639        return new external_function_parameters (
1640            array(
1641                'attemptid' => new external_value(PARAM_INT, 'attempt id'),
1642            )
1643        );
1644    }
1645
1646    /**
1647     * Trigger the attempt reviewed event.
1648     *
1649     * @param int $attemptid attempt id
1650     * @return array of warnings and status result
1651     * @since Moodle 3.1
1652     */
1653    public static function view_attempt_review($attemptid) {
1654
1655        $warnings = array();
1656
1657        $params = array(
1658            'attemptid' => $attemptid,
1659        );
1660        $params = self::validate_parameters(self::view_attempt_review_parameters(), $params);
1661        list($attemptobj, $displayoptions) = self::validate_attempt_review($params);
1662
1663        // Log action.
1664        $attemptobj->fire_attempt_reviewed_event();
1665
1666        $result = array();
1667        $result['status'] = true;
1668        $result['warnings'] = $warnings;
1669        return $result;
1670    }
1671
1672    /**
1673     * Describes the view_attempt_review return value.
1674     *
1675     * @return external_single_structure
1676     * @since Moodle 3.1
1677     */
1678    public static function view_attempt_review_returns() {
1679        return new external_single_structure(
1680            array(
1681                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
1682                'warnings' => new external_warnings(),
1683            )
1684        );
1685    }
1686
1687    /**
1688     * Describes the parameters for view_quiz.
1689     *
1690     * @return external_function_parameters
1691     * @since Moodle 3.1
1692     */
1693    public static function get_quiz_feedback_for_grade_parameters() {
1694        return new external_function_parameters (
1695            array(
1696                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1697                'grade' => new external_value(PARAM_FLOAT, 'the grade to check'),
1698            )
1699        );
1700    }
1701
1702    /**
1703     * Get the feedback text that should be show to a student who got the given grade in the given quiz.
1704     *
1705     * @param int $quizid quiz instance id
1706     * @param float $grade the grade to check
1707     * @return array of warnings and status result
1708     * @since Moodle 3.1
1709     * @throws moodle_exception
1710     */
1711    public static function get_quiz_feedback_for_grade($quizid, $grade) {
1712        global $DB;
1713
1714        $params = array(
1715            'quizid' => $quizid,
1716            'grade' => $grade,
1717        );
1718        $params = self::validate_parameters(self::get_quiz_feedback_for_grade_parameters(), $params);
1719        $warnings = array();
1720
1721        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1722
1723        $result = array();
1724        $result['feedbacktext'] = '';
1725        $result['feedbacktextformat'] = FORMAT_MOODLE;
1726
1727        $feedback = quiz_feedback_record_for_grade($params['grade'], $quiz);
1728        if (!empty($feedback->feedbacktext)) {
1729            list($text, $format) = external_format_text($feedback->feedbacktext, $feedback->feedbacktextformat, $context->id,
1730                                                        'mod_quiz', 'feedback', $feedback->id);
1731            $result['feedbacktext'] = $text;
1732            $result['feedbacktextformat'] = $format;
1733            $feedbackinlinefiles = external_util::get_area_files($context->id, 'mod_quiz', 'feedback', $feedback->id);
1734            if (!empty($feedbackinlinefiles)) {
1735                $result['feedbackinlinefiles'] = $feedbackinlinefiles;
1736            }
1737        }
1738
1739        $result['warnings'] = $warnings;
1740        return $result;
1741    }
1742
1743    /**
1744     * Describes the get_quiz_feedback_for_grade return value.
1745     *
1746     * @return external_single_structure
1747     * @since Moodle 3.1
1748     */
1749    public static function get_quiz_feedback_for_grade_returns() {
1750        return new external_single_structure(
1751            array(
1752                'feedbacktext' => new external_value(PARAM_RAW, 'the comment that corresponds to this grade (empty for none)'),
1753                'feedbacktextformat' => new external_format_value('feedbacktext', VALUE_OPTIONAL),
1754                'feedbackinlinefiles' => new external_files('feedback inline files', VALUE_OPTIONAL),
1755                'warnings' => new external_warnings(),
1756            )
1757        );
1758    }
1759
1760    /**
1761     * Describes the parameters for get_quiz_access_information.
1762     *
1763     * @return external_function_parameters
1764     * @since Moodle 3.1
1765     */
1766    public static function get_quiz_access_information_parameters() {
1767        return new external_function_parameters (
1768            array(
1769                'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1770            )
1771        );
1772    }
1773
1774    /**
1775     * Return access information for a given quiz.
1776     *
1777     * @param int $quizid quiz instance id
1778     * @return array of warnings and the access information
1779     * @since Moodle 3.1
1780     * @throws  moodle_quiz_exception
1781     */
1782    public static function get_quiz_access_information($quizid) {
1783        global $DB, $USER;
1784
1785        $warnings = array();
1786
1787        $params = array(
1788            'quizid' => $quizid
1789        );
1790        $params = self::validate_parameters(self::get_quiz_access_information_parameters(), $params);
1791
1792        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1793
1794        $result = array();
1795        // Capabilities first.
1796        $result['canattempt'] = has_capability('mod/quiz:attempt', $context);;
1797        $result['canmanage'] = has_capability('mod/quiz:manage', $context);;
1798        $result['canpreview'] = has_capability('mod/quiz:preview', $context);;
1799        $result['canreviewmyattempts'] = has_capability('mod/quiz:reviewmyattempts', $context);;
1800        $result['canviewreports'] = has_capability('mod/quiz:viewreports', $context);;
1801
1802        // Access manager now.
1803        $quizobj = quiz::create($cm->instance, $USER->id);
1804        $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1805        $timenow = time();
1806        $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1807
1808        $result['accessrules'] = $accessmanager->describe_rules();
1809        $result['activerulenames'] = $accessmanager->get_active_rule_names();
1810        $result['preventaccessreasons'] = $accessmanager->prevent_access();
1811
1812        $result['warnings'] = $warnings;
1813        return $result;
1814    }
1815
1816    /**
1817     * Describes the get_quiz_access_information return value.
1818     *
1819     * @return external_single_structure
1820     * @since Moodle 3.1
1821     */
1822    public static function get_quiz_access_information_returns() {
1823        return new external_single_structure(
1824            array(
1825                'canattempt' => new external_value(PARAM_BOOL, 'Whether the user can do the quiz or not.'),
1826                'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can edit the quiz settings or not.'),
1827                'canpreview' => new external_value(PARAM_BOOL, 'Whether the user can preview the quiz or not.'),
1828                'canreviewmyattempts' => new external_value(PARAM_BOOL, 'Whether the users can review their previous attempts
1829                                                                or not.'),
1830                'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the quiz reports or not.'),
1831                'accessrules' => new external_multiple_structure(
1832                                    new external_value(PARAM_TEXT, 'rule description'), 'list of rules'),
1833                'activerulenames' => new external_multiple_structure(
1834                                    new external_value(PARAM_PLUGIN, 'rule plugin names'), 'list of active rules'),
1835                'preventaccessreasons' => new external_multiple_structure(
1836                                            new external_value(PARAM_TEXT, 'access restriction description'), 'list of reasons'),
1837                'warnings' => new external_warnings(),
1838            )
1839        );
1840    }
1841
1842    /**
1843     * Describes the parameters for get_attempt_access_information.
1844     *
1845     * @return external_function_parameters
1846     * @since Moodle 3.1
1847     */
1848    public static function get_attempt_access_information_parameters() {
1849        return new external_function_parameters (
1850            array(
1851                'quizid' => new external_value(PARAM_INT, 'quiz instance id'),
1852                'attemptid' => new external_value(PARAM_INT, 'attempt id, 0 for the user last attempt if exists', VALUE_DEFAULT, 0),
1853            )
1854        );
1855    }
1856
1857    /**
1858     * Return access information for a given attempt in a quiz.
1859     *
1860     * @param int $quizid quiz instance id
1861     * @param int $attemptid attempt id, 0 for the user last attempt if exists
1862     * @return array of warnings and the access information
1863     * @since Moodle 3.1
1864     * @throws  moodle_quiz_exception
1865     */
1866    public static function get_attempt_access_information($quizid, $attemptid = 0) {
1867        global $DB, $USER;
1868
1869        $warnings = array();
1870
1871        $params = array(
1872            'quizid' => $quizid,
1873            'attemptid' => $attemptid,
1874        );
1875        $params = self::validate_parameters(self::get_attempt_access_information_parameters(), $params);
1876
1877        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1878
1879        $attempttocheck = 0;
1880        if (!empty($params['attemptid'])) {
1881            $attemptobj = quiz_attempt::create($params['attemptid']);
1882            if ($attemptobj->get_userid() != $USER->id) {
1883                throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
1884            }
1885            $attempttocheck = $attemptobj->get_attempt();
1886        }
1887
1888        // Access manager now.
1889        $quizobj = quiz::create($cm->instance, $USER->id);
1890        $ignoretimelimits = has_capability('mod/quiz:ignoretimelimits', $context, null, false);
1891        $timenow = time();
1892        $accessmanager = new quiz_access_manager($quizobj, $timenow, $ignoretimelimits);
1893
1894        $attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
1895        $lastfinishedattempt = end($attempts);
1896        if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
1897            $attempts[] = $unfinishedattempt;
1898
1899            // Check if the attempt is now overdue. In that case the state will change.
1900            $quizobj->create_attempt_object($unfinishedattempt)->handle_if_time_expired(time(), false);
1901
1902            if ($unfinishedattempt->state != quiz_attempt::IN_PROGRESS and $unfinishedattempt->state != quiz_attempt::OVERDUE) {
1903                $lastfinishedattempt = $unfinishedattempt;
1904            }
1905        }
1906        $numattempts = count($attempts);
1907
1908        if (!$attempttocheck) {
1909            $attempttocheck = $unfinishedattempt ? $unfinishedattempt : $lastfinishedattempt;
1910        }
1911
1912        $result = array();
1913        $result['isfinished'] = $accessmanager->is_finished($numattempts, $lastfinishedattempt);
1914        $result['preventnewattemptreasons'] = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
1915
1916        if ($attempttocheck) {
1917            $endtime = $accessmanager->get_end_time($attempttocheck);
1918            $result['endtime'] = ($endtime === false) ? 0 : $endtime;
1919            $attemptid = $unfinishedattempt ? $unfinishedattempt->id : null;
1920            $result['ispreflightcheckrequired'] = $accessmanager->is_preflight_check_required($attemptid);
1921        }
1922
1923        $result['warnings'] = $warnings;
1924        return $result;
1925    }
1926
1927    /**
1928     * Describes the get_attempt_access_information return value.
1929     *
1930     * @return external_single_structure
1931     * @since Moodle 3.1
1932     */
1933    public static function get_attempt_access_information_returns() {
1934        return new external_single_structure(
1935            array(
1936                'endtime' => new external_value(PARAM_INT, 'When the attempt must be submitted (determined by rules).',
1937                                                VALUE_OPTIONAL),
1938                'isfinished' => new external_value(PARAM_BOOL, 'Whether there is no way the user will ever be allowed to attempt.'),
1939                'ispreflightcheckrequired' => new external_value(PARAM_BOOL, 'whether a check is required before the user
1940                                                                    starts/continues his attempt.', VALUE_OPTIONAL),
1941                'preventnewattemptreasons' => new external_multiple_structure(
1942                                                new external_value(PARAM_TEXT, 'access restriction description'),
1943                                                                    'list of reasons'),
1944                'warnings' => new external_warnings(),
1945            )
1946        );
1947    }
1948
1949    /**
1950     * Describes the parameters for get_quiz_required_qtypes.
1951     *
1952     * @return external_function_parameters
1953     * @since Moodle 3.1
1954     */
1955    public static function get_quiz_required_qtypes_parameters() {
1956        return new external_function_parameters (
1957            array(
1958                'quizid' => new external_value(PARAM_INT, 'quiz instance id')
1959            )
1960        );
1961    }
1962
1963    /**
1964     * Return the potential question types that would be required for a given quiz.
1965     * Please note that for random question types we return the potential question types in the category choosen.
1966     *
1967     * @param int $quizid quiz instance id
1968     * @return array of warnings and the access information
1969     * @since Moodle 3.1
1970     * @throws  moodle_quiz_exception
1971     */
1972    public static function get_quiz_required_qtypes($quizid) {
1973        global $DB, $USER;
1974
1975        $warnings = array();
1976
1977        $params = array(
1978            'quizid' => $quizid
1979        );
1980        $params = self::validate_parameters(self::get_quiz_required_qtypes_parameters(), $params);
1981
1982        list($quiz, $course, $cm, $context) = self::validate_quiz($params['quizid']);
1983
1984        $quizobj = quiz::create($cm->instance, $USER->id);
1985        $quizobj->preload_questions();
1986        $quizobj->load_questions();
1987
1988        // Question types used.
1989        $result = array();
1990        $result['questiontypes'] = $quizobj->get_all_question_types_used(true);
1991        $result['warnings'] = $warnings;
1992        return $result;
1993    }
1994
1995    /**
1996     * Describes the get_quiz_required_qtypes return value.
1997     *
1998     * @return external_single_structure
1999     * @since Moodle 3.1
2000     */
2001    public static function get_quiz_required_qtypes_returns() {
2002        return new external_single_structure(
2003            array(
2004                'questiontypes' => new external_multiple_structure(
2005                                    new external_value(PARAM_PLUGIN, 'question type'), 'list of question types used in the quiz'),
2006                'warnings' => new external_warnings(),
2007            )
2008        );
2009    }
2010
2011}
2012