Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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