Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

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