Search moodle.org's
Developer Documentation

See Release Notes

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

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