Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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