Search moodle.org's
Developer Documentation

See Release Notes

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

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

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