Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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