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 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   * Lesson external API
  19   *
  20   * @package    mod_lesson
  21   * @category   external
  22   * @copyright  2017 Juan Leyva <juan@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @since      Moodle 3.3
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die;
  28  
  29  require_once($CFG->libdir . '/externallib.php');
  30  require_once($CFG->dirroot . '/mod/lesson/locallib.php');
  31  
  32  use mod_lesson\external\lesson_summary_exporter;
  33  
  34  /**
  35   * Lesson external functions
  36   *
  37   * @package    mod_lesson
  38   * @category   external
  39   * @copyright  2017 Juan Leyva <juan@moodle.com>
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   * @since      Moodle 3.3
  42   */
  43  class mod_lesson_external extends external_api {
  44  
  45      /**
  46       * Return a lesson record ready for being exported.
  47       *
  48       * @param  stdClass $lessonrecord lesson record
  49       * @param  string $password       lesson password
  50       * @return stdClass the lesson record ready for exporting.
  51       */
  52      protected static function get_lesson_summary_for_exporter($lessonrecord, $password = '') {
  53          global $USER;
  54  
  55          $lesson = new lesson($lessonrecord);
  56          $lesson->update_effective_access($USER->id);
  57          $lessonavailable = $lesson->get_time_restriction_status() === false;
  58          $lessonavailable = $lessonavailable && $lesson->get_password_restriction_status($password) === false;
  59          $lessonavailable = $lessonavailable && $lesson->get_dependencies_restriction_status() === false;
  60          $canmanage = $lesson->can_manage();
  61  
  62          if (!$canmanage && !$lessonavailable) {
  63              $fields = array('intro', 'introfiles', 'mediafiles', 'practice', 'modattempts', 'usepassword',
  64                  'grade', 'custom', 'ongoing', 'usemaxgrade',
  65                  'maxanswers', 'maxattempts', 'review', 'nextpagedefault', 'feedback', 'minquestions',
  66                  'maxpages', 'timelimit', 'retake', 'mediafile', 'mediaheight', 'mediawidth',
  67                  'mediaclose', 'slideshow', 'width', 'height', 'bgcolor', 'displayleft', 'displayleftif',
  68                  'progressbar');
  69  
  70              foreach ($fields as $field) {
  71                  unset($lessonrecord->{$field});
  72              }
  73          }
  74  
  75          // Fields only for managers.
  76          if (!$canmanage) {
  77              $fields = array('password', 'dependency', 'conditions', 'activitylink', 'available', 'deadline',
  78                              'timemodified', 'completionendreached', 'completiontimespent');
  79  
  80              foreach ($fields as $field) {
  81                  unset($lessonrecord->{$field});
  82              }
  83          }
  84          return $lessonrecord;
  85      }
  86  
  87      /**
  88       * Describes the parameters for get_lessons_by_courses.
  89       *
  90       * @return external_function_parameters
  91       * @since Moodle 3.3
  92       */
  93      public static function get_lessons_by_courses_parameters() {
  94          return new external_function_parameters (
  95              array(
  96                  'courseids' => new external_multiple_structure(
  97                      new external_value(PARAM_INT, 'course id'), 'Array of course ids', VALUE_DEFAULT, array()
  98                  ),
  99              )
 100          );
 101      }
 102  
 103      /**
 104       * Returns a list of lessons in a provided list of courses,
 105       * if no list is provided all lessons that the user can view will be returned.
 106       *
 107       * @param array $courseids Array of course ids
 108       * @return array of lessons details
 109       * @since Moodle 3.3
 110       */
 111      public static function get_lessons_by_courses($courseids = array()) {
 112          global $PAGE;
 113  
 114          $warnings = array();
 115          $returnedlessons = array();
 116  
 117          $params = array(
 118              'courseids' => $courseids,
 119          );
 120          $params = self::validate_parameters(self::get_lessons_by_courses_parameters(), $params);
 121  
 122          $mycourses = array();
 123          if (empty($params['courseids'])) {
 124              $mycourses = enrol_get_my_courses();
 125              $params['courseids'] = array_keys($mycourses);
 126          }
 127  
 128          // Ensure there are courseids to loop through.
 129          if (!empty($params['courseids'])) {
 130  
 131              list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
 132  
 133              // Get the lessons in this course, this function checks users visibility permissions.
 134              // We can avoid then additional validate_context calls.
 135              $lessons = get_all_instances_in_courses("lesson", $courses);
 136              foreach ($lessons as $lessonrecord) {
 137                  $context = context_module::instance($lessonrecord->coursemodule);
 138  
 139                  // Remove fields added by get_all_instances_in_courses.
 140                  unset($lessonrecord->coursemodule, $lessonrecord->section, $lessonrecord->visible, $lessonrecord->groupmode,
 141                      $lessonrecord->groupingid);
 142  
 143                  $lessonrecord = self::get_lesson_summary_for_exporter($lessonrecord);
 144  
 145                  $exporter = new lesson_summary_exporter($lessonrecord, array('context' => $context));
 146                  $lesson = $exporter->export($PAGE->get_renderer('core'));
 147                  $lesson->name = external_format_string($lesson->name, $context);
 148                  $returnedlessons[] = $lesson;
 149              }
 150          }
 151          $result = array();
 152          $result['lessons'] = $returnedlessons;
 153          $result['warnings'] = $warnings;
 154          return $result;
 155      }
 156  
 157      /**
 158       * Describes the get_lessons_by_courses return value.
 159       *
 160       * @return external_single_structure
 161       * @since Moodle 3.3
 162       */
 163      public static function get_lessons_by_courses_returns() {
 164          return new external_single_structure(
 165              array(
 166                  'lessons' => new external_multiple_structure(
 167                      lesson_summary_exporter::get_read_structure()
 168                  ),
 169                  'warnings' => new external_warnings(),
 170              )
 171          );
 172      }
 173  
 174      /**
 175       * Utility function for validating a lesson.
 176       *
 177       * @param int $lessonid lesson instance id
 178       * @return array array containing the lesson, course, context and course module objects
 179       * @since  Moodle 3.3
 180       */
 181      protected static function validate_lesson($lessonid) {
 182          global $DB, $USER;
 183  
 184          // Request and permission validation.
 185          $lessonrecord = $DB->get_record('lesson', array('id' => $lessonid), '*', MUST_EXIST);
 186          list($course, $cm) = get_course_and_cm_from_instance($lessonrecord, 'lesson');
 187  
 188          $lesson = new lesson($lessonrecord, $cm, $course);
 189          $lesson->update_effective_access($USER->id);
 190  
 191          $context = $lesson->context;
 192          self::validate_context($context);
 193  
 194          return array($lesson, $course, $cm, $context, $lessonrecord);
 195      }
 196  
 197      /**
 198       * Validates a new attempt.
 199       *
 200       * @param  lesson  $lesson lesson instance
 201       * @param  array   $params request parameters
 202       * @param  boolean $return whether to return the errors or throw exceptions
 203       * @return array          the errors (if return set to true)
 204       * @since  Moodle 3.3
 205       */
 206      protected static function validate_attempt(lesson $lesson, $params, $return = false) {
 207          global $USER, $CFG;
 208  
 209          $errors = array();
 210  
 211          // Avoid checkings for managers.
 212          if ($lesson->can_manage()) {
 213              return [];
 214          }
 215  
 216          // Dead line.
 217          if ($timerestriction = $lesson->get_time_restriction_status()) {
 218              $error = ["$timerestriction->reason" => userdate($timerestriction->time)];
 219              if (!$return) {
 220                  throw new moodle_exception(key($error), 'lesson', '', current($error));
 221              }
 222              $errors[key($error)] = current($error);
 223          }
 224  
 225          // Password protected lesson code.
 226          if ($passwordrestriction = $lesson->get_password_restriction_status($params['password'])) {
 227              $error = ["passwordprotectedlesson" => external_format_string($lesson->name, $lesson->context->id)];
 228              if (!$return) {
 229                  throw new moodle_exception(key($error), 'lesson', '', current($error));
 230              }
 231              $errors[key($error)] = current($error);
 232          }
 233  
 234          // Check for dependencies.
 235          if ($dependenciesrestriction = $lesson->get_dependencies_restriction_status()) {
 236              $errorhtmllist = implode(get_string('and', 'lesson') . ', ', $dependenciesrestriction->errors);
 237              $error = ["completethefollowingconditions" => $dependenciesrestriction->dependentlesson->name . $errorhtmllist];
 238              if (!$return) {
 239                  throw new moodle_exception(key($error), 'lesson', '', current($error));
 240              }
 241              $errors[key($error)] = current($error);
 242          }
 243  
 244          // To check only when no page is set (starting or continuing a lesson).
 245          if (empty($params['pageid'])) {
 246              // To avoid multiple calls, store the magic property firstpage.
 247              $lessonfirstpage = $lesson->firstpage;
 248              $lessonfirstpageid = $lessonfirstpage ? $lessonfirstpage->id : false;
 249  
 250              // Check if the lesson does not have pages.
 251              if (!$lessonfirstpageid) {
 252                  $error = ["lessonnotready2" => null];
 253                  if (!$return) {
 254                      throw new moodle_exception(key($error), 'lesson');
 255                  }
 256                  $errors[key($error)] = current($error);
 257              }
 258  
 259              // Get the number of retries (also referenced as attempts), and the last page seen.
 260              $attemptscount = $lesson->count_user_retries($USER->id);
 261              $lastpageseen = $lesson->get_last_page_seen($attemptscount);
 262  
 263              // Check if the user left a timed session with no retakes.
 264              if ($lastpageseen !== false && $lastpageseen != LESSON_EOL) {
 265                  if ($lesson->left_during_timed_session($attemptscount) && $lesson->timelimit && !$lesson->retake) {
 266                      $error = ["leftduringtimednoretake" => null];
 267                      if (!$return) {
 268                          throw new moodle_exception(key($error), 'lesson');
 269                      }
 270                      $errors[key($error)] = current($error);
 271                  }
 272              } else if ($attemptscount > 0 && !$lesson->retake) {
 273                  // The user finished the lesson and no retakes are allowed.
 274                  $error = ["noretake" => null];
 275                  if (!$return) {
 276                      throw new moodle_exception(key($error), 'lesson');
 277                  }
 278                  $errors[key($error)] = current($error);
 279              }
 280          } else {
 281              if (!$timers = $lesson->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) {
 282                  $error = ["cannotfindtimer" => null];
 283                  if (!$return) {
 284                      throw new moodle_exception(key($error), 'lesson');
 285                  }
 286                  $errors[key($error)] = current($error);
 287              } else {
 288                  $timer = current($timers);
 289                  if (!$lesson->check_time($timer)) {
 290                      $error = ["eolstudentoutoftime" => null];
 291                      if (!$return) {
 292                          throw new moodle_exception(key($error), 'lesson');
 293                      }
 294                      $errors[key($error)] = current($error);
 295                  }
 296  
 297                  // Check if the user want to review an attempt he just finished.
 298                  if (!empty($params['review'])) {
 299                      // Allow review only for attempts during active session time.
 300                      if ($timer->lessontime + $CFG->sessiontimeout > time()) {
 301                          $ntries = $lesson->count_user_retries($USER->id);
 302                          $ntries--;  // Need to look at the old attempts.
 303                          if ($params['pageid'] == LESSON_EOL) {
 304                              if ($attempts = $lesson->get_attempts($ntries)) {
 305                                  $lastattempt = end($attempts);
 306                                  $USER->modattempts[$lesson->id] = $lastattempt->pageid;
 307                              }
 308                          } else {
 309                              if ($attempts = $lesson->get_attempts($ntries, false, $params['pageid'])) {
 310                                  $lastattempt = end($attempts);
 311                                  $USER->modattempts[$lesson->id] = $lastattempt;
 312                              }
 313                          }
 314                      }
 315  
 316                      if (!isset($USER->modattempts[$lesson->id])) {
 317                          $error = ["studentoutoftimeforreview" => null];
 318                          if (!$return) {
 319                              throw new moodle_exception(key($error), 'lesson');
 320                          }
 321                          $errors[key($error)] = current($error);
 322                      }
 323                  }
 324              }
 325          }
 326  
 327          return $errors;
 328      }
 329  
 330      /**
 331       * Describes the parameters for get_lesson_access_information.
 332       *
 333       * @return external_function_parameters
 334       * @since Moodle 3.3
 335       */
 336      public static function get_lesson_access_information_parameters() {
 337          return new external_function_parameters (
 338              array(
 339                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id')
 340              )
 341          );
 342      }
 343  
 344      /**
 345       * Return access information for a given lesson.
 346       *
 347       * @param int $lessonid lesson instance id
 348       * @return array of warnings and the access information
 349       * @since Moodle 3.3
 350       * @throws  moodle_exception
 351       */
 352      public static function get_lesson_access_information($lessonid) {
 353          global $DB, $USER;
 354  
 355          $warnings = array();
 356  
 357          $params = array(
 358              'lessonid' => $lessonid
 359          );
 360          $params = self::validate_parameters(self::get_lesson_access_information_parameters(), $params);
 361  
 362          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 363  
 364          $result = array();
 365          // Capabilities first.
 366          $result['canmanage'] = $lesson->can_manage();
 367          $result['cangrade'] = has_capability('mod/lesson:grade', $context);
 368          $result['canviewreports'] = has_capability('mod/lesson:viewreports', $context);
 369  
 370          // Status information.
 371          $result['reviewmode'] = $lesson->is_in_review_mode();
 372          $result['attemptscount'] = $lesson->count_user_retries($USER->id);
 373          $lastpageseen = $lesson->get_last_page_seen($result['attemptscount']);
 374          $result['lastpageseen'] = ($lastpageseen !== false) ? $lastpageseen : 0;
 375          $result['leftduringtimedsession'] = $lesson->left_during_timed_session($result['attemptscount']);
 376          // To avoid multiple calls, store the magic property firstpage.
 377          $lessonfirstpage = $lesson->firstpage;
 378          $result['firstpageid'] = $lessonfirstpage ? $lessonfirstpage->id : 0;
 379  
 380          // Access restrictions now, we emulate a new attempt access to get the possible warnings.
 381          $result['preventaccessreasons'] = [];
 382          $validationerrors = self::validate_attempt($lesson, ['password' => ''], true);
 383          foreach ($validationerrors as $reason => $data) {
 384              $result['preventaccessreasons'][] = [
 385                  'reason' => $reason,
 386                  'data' => $data,
 387                  'message' => get_string($reason, 'lesson', $data),
 388              ];
 389          }
 390          $result['warnings'] = $warnings;
 391          return $result;
 392      }
 393  
 394      /**
 395       * Describes the get_lesson_access_information return value.
 396       *
 397       * @return external_single_structure
 398       * @since Moodle 3.3
 399       */
 400      public static function get_lesson_access_information_returns() {
 401          return new external_single_structure(
 402              array(
 403                  'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can manage the lesson or not.'),
 404                  'cangrade' => new external_value(PARAM_BOOL, 'Whether the user can grade the lesson or not.'),
 405                  'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the lesson reports or not.'),
 406                  'reviewmode' => new external_value(PARAM_BOOL, 'Whether the lesson is in review mode for the current user.'),
 407                  'attemptscount' => new external_value(PARAM_INT, 'The number of attempts done by the user.'),
 408                  'lastpageseen' => new external_value(PARAM_INT, 'The last page seen id.'),
 409                  'leftduringtimedsession' => new external_value(PARAM_BOOL, 'Whether the user left during a timed session.'),
 410                  'firstpageid' => new external_value(PARAM_INT, 'The lesson first page id.'),
 411                  'preventaccessreasons' => new external_multiple_structure(
 412                      new external_single_structure(
 413                          array(
 414                              'reason' => new external_value(PARAM_ALPHANUMEXT, 'Reason lang string code'),
 415                              'data' => new external_value(PARAM_RAW, 'Additional data'),
 416                              'message' => new external_value(PARAM_RAW, 'Complete html message'),
 417                          ),
 418                          'The reasons why the user cannot attempt the lesson'
 419                      )
 420                  ),
 421                  'warnings' => new external_warnings(),
 422              )
 423          );
 424      }
 425  
 426      /**
 427       * Describes the parameters for view_lesson.
 428       *
 429       * @return external_function_parameters
 430       * @since Moodle 3.3
 431       */
 432      public static function view_lesson_parameters() {
 433          return new external_function_parameters (
 434              array(
 435                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
 436                  'password' => new external_value(PARAM_RAW, 'lesson password', VALUE_DEFAULT, ''),
 437              )
 438          );
 439      }
 440  
 441      /**
 442       * Trigger the course module viewed event and update the module completion status.
 443       *
 444       * @param int $lessonid lesson instance id
 445       * @param string $password optional password (the lesson may be protected)
 446       * @return array of warnings and status result
 447       * @since Moodle 3.3
 448       * @throws moodle_exception
 449       */
 450      public static function view_lesson($lessonid, $password = '') {
 451          global $DB;
 452  
 453          $params = array('lessonid' => $lessonid, 'password' => $password);
 454          $params = self::validate_parameters(self::view_lesson_parameters(), $params);
 455          $warnings = array();
 456  
 457          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 458          self::validate_attempt($lesson, $params);
 459  
 460          $lesson->set_module_viewed();
 461  
 462          $result = array();
 463          $result['status'] = true;
 464          $result['warnings'] = $warnings;
 465          return $result;
 466      }
 467  
 468      /**
 469       * Describes the view_lesson return value.
 470       *
 471       * @return external_single_structure
 472       * @since Moodle 3.3
 473       */
 474      public static function view_lesson_returns() {
 475          return new external_single_structure(
 476              array(
 477                  'status' => new external_value(PARAM_BOOL, 'status: true if success'),
 478                  'warnings' => new external_warnings(),
 479              )
 480          );
 481      }
 482  
 483      /**
 484       * Check if the current user can retrieve lesson information (grades, attempts) about the given user.
 485       *
 486       * @param int $userid the user to check
 487       * @param stdClass $course course object
 488       * @param stdClass $cm cm object
 489       * @param stdClass $context context object
 490       * @throws moodle_exception
 491       * @since Moodle 3.3
 492       */
 493      protected static function check_can_view_user_data($userid, $course, $cm, $context) {
 494          $user = core_user::get_user($userid, '*', MUST_EXIST);
 495          core_user::require_active_user($user);
 496          // Check permissions and that if users share group (if groups enabled).
 497          require_capability('mod/lesson:viewreports', $context);
 498          if (!groups_user_groups_visible($course, $user->id, $cm)) {
 499              throw new moodle_exception('notingroup');
 500          }
 501      }
 502  
 503      /**
 504       * Describes the parameters for get_questions_attempts.
 505       *
 506       * @return external_function_parameters
 507       * @since Moodle 3.3
 508       */
 509      public static function get_questions_attempts_parameters() {
 510          return new external_function_parameters (
 511              array(
 512                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
 513                  'attempt' => new external_value(PARAM_INT, 'lesson attempt number'),
 514                  'correct' => new external_value(PARAM_BOOL, 'only fetch correct attempts', VALUE_DEFAULT, false),
 515                  'pageid' => new external_value(PARAM_INT, 'only fetch attempts at the given page', VALUE_DEFAULT, null),
 516                  'userid' => new external_value(PARAM_INT, 'only fetch attempts of the given user', VALUE_DEFAULT, null),
 517              )
 518          );
 519      }
 520  
 521      /**
 522       * Return the list of page question attempts in a given lesson.
 523       *
 524       * @param int $lessonid lesson instance id
 525       * @param int $attempt the lesson attempt number
 526       * @param bool $correct only fetch correct attempts
 527       * @param int $pageid only fetch attempts at the given page
 528       * @param int $userid only fetch attempts of the given user
 529       * @return array of warnings and page attempts
 530       * @since Moodle 3.3
 531       * @throws moodle_exception
 532       */
 533      public static function get_questions_attempts($lessonid, $attempt, $correct = false, $pageid = null, $userid = null) {
 534          global $DB, $USER;
 535  
 536          $params = array(
 537              'lessonid' => $lessonid,
 538              'attempt' => $attempt,
 539              'correct' => $correct,
 540              'pageid' => $pageid,
 541              'userid' => $userid,
 542          );
 543          $params = self::validate_parameters(self::get_questions_attempts_parameters(), $params);
 544          $warnings = array();
 545  
 546          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 547  
 548          // Default value for userid.
 549          if (empty($params['userid'])) {
 550              $params['userid'] = $USER->id;
 551          }
 552  
 553          // Extra checks so only users with permissions can view other users attempts.
 554          if ($USER->id != $params['userid']) {
 555              self::check_can_view_user_data($params['userid'], $course, $cm, $context);
 556          }
 557  
 558          $result = array();
 559          $result['attempts'] = $lesson->get_attempts($params['attempt'], $params['correct'], $params['pageid'], $params['userid']);
 560          $result['warnings'] = $warnings;
 561          return $result;
 562      }
 563  
 564      /**
 565       * Describes the get_questions_attempts return value.
 566       *
 567       * @return external_single_structure
 568       * @since Moodle 3.3
 569       */
 570      public static function get_questions_attempts_returns() {
 571          return new external_single_structure(
 572              array(
 573                  'attempts' => new external_multiple_structure(
 574                      new external_single_structure(
 575                          array(
 576                              'id' => new external_value(PARAM_INT, 'The attempt id'),
 577                              'lessonid' => new external_value(PARAM_INT, 'The attempt lessonid'),
 578                              'pageid' => new external_value(PARAM_INT, 'The attempt pageid'),
 579                              'userid' => new external_value(PARAM_INT, 'The user who did the attempt'),
 580                              'answerid' => new external_value(PARAM_INT, 'The attempt answerid'),
 581                              'retry' => new external_value(PARAM_INT, 'The lesson attempt number'),
 582                              'correct' => new external_value(PARAM_INT, 'If it was the correct answer'),
 583                              'useranswer' => new external_value(PARAM_RAW, 'The complete user answer'),
 584                              'timeseen' => new external_value(PARAM_INT, 'The time the question was seen'),
 585                          ),
 586                          'The question page attempts'
 587                      )
 588                  ),
 589                  'warnings' => new external_warnings(),
 590              )
 591          );
 592      }
 593  
 594      /**
 595       * Describes the parameters for get_user_grade.
 596       *
 597       * @return external_function_parameters
 598       * @since Moodle 3.3
 599       */
 600      public static function get_user_grade_parameters() {
 601          return new external_function_parameters (
 602              array(
 603                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
 604                  'userid' => new external_value(PARAM_INT, 'the user id (empty for current user)', VALUE_DEFAULT, null),
 605              )
 606          );
 607      }
 608  
 609      /**
 610       * Return the final grade in the lesson for the given user.
 611       *
 612       * @param int $lessonid lesson instance id
 613       * @param int $userid only fetch grades of this user
 614       * @return array of warnings and page attempts
 615       * @since Moodle 3.3
 616       * @throws moodle_exception
 617       */
 618      public static function get_user_grade($lessonid, $userid = null) {
 619          global $CFG, $USER;
 620          require_once($CFG->libdir . '/gradelib.php');
 621  
 622          $params = array(
 623              'lessonid' => $lessonid,
 624              'userid' => $userid,
 625          );
 626          $params = self::validate_parameters(self::get_user_grade_parameters(), $params);
 627          $warnings = array();
 628  
 629          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 630  
 631          // Default value for userid.
 632          if (empty($params['userid'])) {
 633              $params['userid'] = $USER->id;
 634          }
 635  
 636          // Extra checks so only users with permissions can view other users attempts.
 637          if ($USER->id != $params['userid']) {
 638              self::check_can_view_user_data($params['userid'], $course, $cm, $context);
 639          }
 640  
 641          $grade = null;
 642          $formattedgrade = null;
 643          $grades = lesson_get_user_grades($lesson, $params['userid']);
 644          if (!empty($grades)) {
 645              $grade = $grades[$params['userid']]->rawgrade;
 646              $params = array(
 647                  'itemtype' => 'mod',
 648                  'itemmodule' => 'lesson',
 649                  'iteminstance' => $lesson->id,
 650                  'courseid' => $course->id,
 651                  'itemnumber' => 0
 652              );
 653              $gradeitem = grade_item::fetch($params);
 654              $formattedgrade = grade_format_gradevalue($grade, $gradeitem);
 655          }
 656  
 657          $result = array();
 658          $result['grade'] = $grade;
 659          $result['formattedgrade'] = $formattedgrade;
 660          $result['warnings'] = $warnings;
 661          return $result;
 662      }
 663  
 664      /**
 665       * Describes the get_user_grade return value.
 666       *
 667       * @return external_single_structure
 668       * @since Moodle 3.3
 669       */
 670      public static function get_user_grade_returns() {
 671          return new external_single_structure(
 672              array(
 673                  'grade' => new external_value(PARAM_FLOAT, 'The lesson final raw grade'),
 674                  'formattedgrade' => new external_value(PARAM_RAW, 'The lesson final grade formatted'),
 675                  'warnings' => new external_warnings(),
 676              )
 677          );
 678      }
 679  
 680      /**
 681       * Describes an attempt grade structure.
 682       *
 683       * @param  int $required if the structure is required or optional
 684       * @return external_single_structure the structure
 685       * @since  Moodle 3.3
 686       */
 687      protected static function get_user_attempt_grade_structure($required = VALUE_REQUIRED) {
 688          $data = array(
 689              'nquestions' => new external_value(PARAM_INT, 'Number of questions answered'),
 690              'attempts' => new external_value(PARAM_INT, 'Number of question attempts'),
 691              'total' => new external_value(PARAM_FLOAT, 'Max points possible'),
 692              'earned' => new external_value(PARAM_FLOAT, 'Points earned by student'),
 693              'grade' => new external_value(PARAM_FLOAT, 'Calculated percentage grade'),
 694              'nmanual' => new external_value(PARAM_INT, 'Number of manually graded questions'),
 695              'manualpoints' => new external_value(PARAM_FLOAT, 'Point value for manually graded questions'),
 696          );
 697          return new external_single_structure(
 698              $data, 'Attempt grade', $required
 699          );
 700      }
 701  
 702      /**
 703       * Describes the parameters for get_user_attempt_grade.
 704       *
 705       * @return external_function_parameters
 706       * @since Moodle 3.3
 707       */
 708      public static function get_user_attempt_grade_parameters() {
 709          return new external_function_parameters (
 710              array(
 711                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
 712                  'lessonattempt' => new external_value(PARAM_INT, 'lesson attempt number'),
 713                  'userid' => new external_value(PARAM_INT, 'the user id (empty for current user)', VALUE_DEFAULT, null),
 714              )
 715          );
 716      }
 717  
 718      /**
 719       * Return grade information in the attempt for a given user.
 720       *
 721       * @param int $lessonid lesson instance id
 722       * @param int $lessonattempt lesson attempt number
 723       * @param int $userid only fetch attempts of the given user
 724       * @return array of warnings and page attempts
 725       * @since Moodle 3.3
 726       * @throws moodle_exception
 727       */
 728      public static function get_user_attempt_grade($lessonid, $lessonattempt, $userid = null) {
 729          global $CFG, $USER;
 730          require_once($CFG->libdir . '/gradelib.php');
 731  
 732          $params = array(
 733              'lessonid' => $lessonid,
 734              'lessonattempt' => $lessonattempt,
 735              'userid' => $userid,
 736          );
 737          $params = self::validate_parameters(self::get_user_attempt_grade_parameters(), $params);
 738          $warnings = array();
 739  
 740          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 741  
 742          // Default value for userid.
 743          if (empty($params['userid'])) {
 744              $params['userid'] = $USER->id;
 745          }
 746  
 747          // Extra checks so only users with permissions can view other users attempts.
 748          if ($USER->id != $params['userid']) {
 749              self::check_can_view_user_data($params['userid'], $course, $cm, $context);
 750          }
 751  
 752          $result = array();
 753          $result['grade'] = (array) lesson_grade($lesson, $params['lessonattempt'], $params['userid']);
 754          $result['warnings'] = $warnings;
 755          return $result;
 756      }
 757  
 758      /**
 759       * Describes the get_user_attempt_grade return value.
 760       *
 761       * @return external_single_structure
 762       * @since Moodle 3.3
 763       */
 764      public static function get_user_attempt_grade_returns() {
 765          return new external_single_structure(
 766              array(
 767                  'grade' => self::get_user_attempt_grade_structure(),
 768                  'warnings' => new external_warnings(),
 769              )
 770          );
 771      }
 772  
 773      /**
 774       * Describes the parameters for get_content_pages_viewed.
 775       *
 776       * @return external_function_parameters
 777       * @since Moodle 3.3
 778       */
 779      public static function get_content_pages_viewed_parameters() {
 780          return new external_function_parameters (
 781              array(
 782                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
 783                  'lessonattempt' => new external_value(PARAM_INT, 'lesson attempt number'),
 784                  'userid' => new external_value(PARAM_INT, 'the user id (empty for current user)', VALUE_DEFAULT, null),
 785              )
 786          );
 787      }
 788  
 789      /**
 790       * Return the list of content pages viewed by a user during a lesson attempt.
 791       *
 792       * @param int $lessonid lesson instance id
 793       * @param int $lessonattempt lesson attempt number
 794       * @param int $userid only fetch attempts of the given user
 795       * @return array of warnings and page attempts
 796       * @since Moodle 3.3
 797       * @throws moodle_exception
 798       */
 799      public static function get_content_pages_viewed($lessonid, $lessonattempt, $userid = null) {
 800          global $USER;
 801  
 802          $params = array(
 803              'lessonid' => $lessonid,
 804              'lessonattempt' => $lessonattempt,
 805              'userid' => $userid,
 806          );
 807          $params = self::validate_parameters(self::get_content_pages_viewed_parameters(), $params);
 808          $warnings = array();
 809  
 810          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 811  
 812          // Default value for userid.
 813          if (empty($params['userid'])) {
 814              $params['userid'] = $USER->id;
 815          }
 816  
 817          // Extra checks so only users with permissions can view other users attempts.
 818          if ($USER->id != $params['userid']) {
 819              self::check_can_view_user_data($params['userid'], $course, $cm, $context);
 820          }
 821  
 822          $pages = $lesson->get_content_pages_viewed($params['lessonattempt'], $params['userid']);
 823  
 824          $result = array();
 825          $result['pages'] = $pages;
 826          $result['warnings'] = $warnings;
 827          return $result;
 828      }
 829  
 830      /**
 831       * Describes the get_content_pages_viewed return value.
 832       *
 833       * @return external_single_structure
 834       * @since Moodle 3.3
 835       */
 836      public static function get_content_pages_viewed_returns() {
 837          return new external_single_structure(
 838              array(
 839                  'pages' => new external_multiple_structure(
 840                      new external_single_structure(
 841                          array(
 842                              'id' => new external_value(PARAM_INT, 'The attempt id.'),
 843                              'lessonid' => new external_value(PARAM_INT, 'The lesson id.'),
 844                              'pageid' => new external_value(PARAM_INT, 'The page id.'),
 845                              'userid' => new external_value(PARAM_INT, 'The user who viewed the page.'),
 846                              'retry' => new external_value(PARAM_INT, 'The lesson attempt number.'),
 847                              'flag' => new external_value(PARAM_INT, '1 if the next page was calculated randomly.'),
 848                              'timeseen' => new external_value(PARAM_INT, 'The time the page was seen.'),
 849                              'nextpageid' => new external_value(PARAM_INT, 'The next page chosen id.'),
 850                          ),
 851                          'The content pages viewed.'
 852                      )
 853                  ),
 854                  'warnings' => new external_warnings(),
 855              )
 856          );
 857      }
 858  
 859      /**
 860       * Describes the parameters for get_user_timers.
 861       *
 862       * @return external_function_parameters
 863       * @since Moodle 3.3
 864       */
 865      public static function get_user_timers_parameters() {
 866          return new external_function_parameters (
 867              array(
 868                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
 869                  'userid' => new external_value(PARAM_INT, 'the user id (empty for current user)', VALUE_DEFAULT, null),
 870              )
 871          );
 872      }
 873  
 874      /**
 875       * Return the timers in the current lesson for the given user.
 876       *
 877       * @param int $lessonid lesson instance id
 878       * @param int $userid only fetch timers of the given user
 879       * @return array of warnings and timers
 880       * @since Moodle 3.3
 881       * @throws moodle_exception
 882       */
 883      public static function get_user_timers($lessonid, $userid = null) {
 884          global $USER;
 885  
 886          $params = array(
 887              'lessonid' => $lessonid,
 888              'userid' => $userid,
 889          );
 890          $params = self::validate_parameters(self::get_user_timers_parameters(), $params);
 891          $warnings = array();
 892  
 893          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 894  
 895          // Default value for userid.
 896          if (empty($params['userid'])) {
 897              $params['userid'] = $USER->id;
 898          }
 899  
 900          // Extra checks so only users with permissions can view other users attempts.
 901          if ($USER->id != $params['userid']) {
 902              self::check_can_view_user_data($params['userid'], $course, $cm, $context);
 903          }
 904  
 905          $timers = $lesson->get_user_timers($params['userid']);
 906  
 907          $result = array();
 908          $result['timers'] = $timers;
 909          $result['warnings'] = $warnings;
 910          return $result;
 911      }
 912  
 913      /**
 914       * Describes the get_user_timers return value.
 915       *
 916       * @return external_single_structure
 917       * @since Moodle 3.3
 918       */
 919      public static function get_user_timers_returns() {
 920          return new external_single_structure(
 921              array(
 922                  'timers' => new external_multiple_structure(
 923                      new external_single_structure(
 924                          array(
 925                              'id' => new external_value(PARAM_INT, 'The attempt id'),
 926                              'lessonid' => new external_value(PARAM_INT, 'The lesson id'),
 927                              'userid' => new external_value(PARAM_INT, 'The user id'),
 928                              'starttime' => new external_value(PARAM_INT, 'First access time for a new timer session'),
 929                              'lessontime' => new external_value(PARAM_INT, 'Last access time to the lesson during the timer session'),
 930                              'completed' => new external_value(PARAM_INT, 'If the lesson for this timer was completed'),
 931                              'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.'),
 932                          ),
 933                          'The timers'
 934                      )
 935                  ),
 936                  'warnings' => new external_warnings(),
 937              )
 938          );
 939      }
 940  
 941      /**
 942       * Describes the external structure for a lesson page.
 943       *
 944       * @return external_single_structure
 945       * @since Moodle 3.3
 946       */
 947      protected static function get_page_structure($required = VALUE_REQUIRED) {
 948          return new external_single_structure(
 949              array(
 950                  'id' => new external_value(PARAM_INT, 'The id of this lesson page'),
 951                  'lessonid' => new external_value(PARAM_INT, 'The id of the lesson this page belongs to'),
 952                  'prevpageid' => new external_value(PARAM_INT, 'The id of the page before this one'),
 953                  'nextpageid' => new external_value(PARAM_INT, 'The id of the next page in the page sequence'),
 954                  'qtype' => new external_value(PARAM_INT, 'Identifies the page type of this page'),
 955                  'qoption' => new external_value(PARAM_INT, 'Used to record page type specific options'),
 956                  'layout' => new external_value(PARAM_INT, 'Used to record page specific layout selections'),
 957                  'display' => new external_value(PARAM_INT, 'Used to record page specific display selections'),
 958                  'timecreated' => new external_value(PARAM_INT, 'Timestamp for when the page was created'),
 959                  'timemodified' => new external_value(PARAM_INT, 'Timestamp for when the page was last modified'),
 960                  'title' => new external_value(PARAM_RAW, 'The title of this page', VALUE_OPTIONAL),
 961                  'contents' => new external_value(PARAM_RAW, 'The contents of this page', VALUE_OPTIONAL),
 962                  'contentsformat' => new external_format_value('contents', VALUE_OPTIONAL),
 963                  'displayinmenublock' => new external_value(PARAM_BOOL, 'Toggles display in the left menu block'),
 964                  'type' => new external_value(PARAM_INT, 'The type of the page [question | structure]'),
 965                  'typeid' => new external_value(PARAM_INT, 'The unique identifier for the page type'),
 966                  'typestring' => new external_value(PARAM_RAW, 'The string that describes this page type'),
 967              ),
 968              'Page fields', $required
 969          );
 970      }
 971  
 972      /**
 973       * Returns the fields of a page object
 974       * @param lesson_page $page the lesson page
 975       * @param bool $returncontents whether to return the page title and contents
 976       * @return stdClass          the fields matching the external page structure
 977       * @since Moodle 3.3
 978       */
 979      protected static function get_page_fields(lesson_page $page, $returncontents = false) {
 980          $lesson = $page->lesson;
 981          $context = $lesson->context;
 982  
 983          $pagedata = new stdClass; // Contains the data that will be returned by the WS.
 984  
 985          // Return the visible data.
 986          $visibleproperties = array('id', 'lessonid', 'prevpageid', 'nextpageid', 'qtype', 'qoption', 'layout', 'display',
 987                                      'displayinmenublock', 'type', 'typeid', 'typestring', 'timecreated', 'timemodified');
 988          foreach ($visibleproperties as $prop) {
 989              $pagedata->{$prop} = $page->{$prop};
 990          }
 991  
 992          // Check if we can see title (contents required custom rendering, we won't returning it here @see get_page_data).
 993          $canmanage = $lesson->can_manage();
 994          // If we are managers or the menu block is enabled and is a content page visible always return contents.
 995          if ($returncontents || $canmanage || (lesson_displayleftif($lesson) && $page->displayinmenublock && $page->display)) {
 996              $pagedata->title = external_format_string($page->title, $context->id);
 997  
 998              $options = array('noclean' => true);
 999              list($pagedata->contents, $pagedata->contentsformat) =
1000                  external_format_text($page->contents, $page->contentsformat, $context->id, 'mod_lesson', 'page_contents', $page->id,
1001                      $options);
1002  
1003          }
1004          return $pagedata;
1005      }
1006  
1007      /**
1008       * Describes the parameters for get_pages.
1009       *
1010       * @return external_function_parameters
1011       * @since Moodle 3.3
1012       */
1013      public static function get_pages_parameters() {
1014          return new external_function_parameters (
1015              array(
1016                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
1017                  'password' => new external_value(PARAM_RAW, 'optional password (the lesson may be protected)', VALUE_DEFAULT, ''),
1018              )
1019          );
1020      }
1021  
1022      /**
1023       * Return the list of pages in a lesson (based on the user permissions).
1024       *
1025       * @param int $lessonid lesson instance id
1026       * @param string $password optional password (the lesson may be protected)
1027       * @return array of warnings and status result
1028       * @since Moodle 3.3
1029       * @throws moodle_exception
1030       */
1031      public static function get_pages($lessonid, $password = '') {
1032  
1033          $params = array('lessonid' => $lessonid, 'password' => $password);
1034          $params = self::validate_parameters(self::get_pages_parameters(), $params);
1035          $warnings = array();
1036  
1037          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
1038          self::validate_attempt($lesson, $params);
1039  
1040          $lessonpages = $lesson->load_all_pages();
1041          $pages = array();
1042  
1043          foreach ($lessonpages as $page) {
1044              $pagedata = new stdClass();
1045  
1046              // Get the page object fields.
1047              $pagedata->page = self::get_page_fields($page);
1048  
1049              // Now, calculate the file area files (maybe we need to download a lesson for offline usage).
1050              $pagedata->filescount = 0;
1051              $pagedata->filessizetotal = 0;
1052              $files = $page->get_files(false);   // Get files excluding directories.
1053              foreach ($files as $file) {
1054                  $pagedata->filescount++;
1055                  $pagedata->filessizetotal += $file->get_filesize();
1056              }
1057  
1058              // Now the possible answers and page jumps ids.
1059              $pagedata->answerids = array();
1060              $pagedata->jumps = array();
1061              $answers = $page->get_answers();
1062              foreach ($answers as $answer) {
1063                  $pagedata->answerids[] = $answer->id;
1064                  $pagedata->jumps[] = $answer->jumpto;
1065                  $files = $answer->get_files(false);   // Get files excluding directories.
1066                  foreach ($files as $file) {
1067                      $pagedata->filescount++;
1068                      $pagedata->filessizetotal += $file->get_filesize();
1069                  }
1070              }
1071              $pages[] = $pagedata;
1072          }
1073  
1074          $result = array();
1075          $result['pages'] = $pages;
1076          $result['warnings'] = $warnings;
1077          return $result;
1078      }
1079  
1080      /**
1081       * Describes the get_pages return value.
1082       *
1083       * @return external_single_structure
1084       * @since Moodle 3.3
1085       */
1086      public static function get_pages_returns() {
1087          return new external_single_structure(
1088              array(
1089                  'pages' => new external_multiple_structure(
1090                      new external_single_structure(
1091                          array(
1092                              'page' => self::get_page_structure(),
1093                              'answerids' => new external_multiple_structure(
1094                                  new external_value(PARAM_INT, 'Answer id'), 'List of answers ids (empty for content pages in  Moodle 1.9)'
1095                              ),
1096                              'jumps' => new external_multiple_structure(
1097                                  new external_value(PARAM_INT, 'Page to jump id'), 'List of possible page jumps'
1098                              ),
1099                              'filescount' => new external_value(PARAM_INT, 'The total number of files attached to the page'),
1100                              'filessizetotal' => new external_value(PARAM_INT, 'The total size of the files'),
1101                          ),
1102                          'The lesson pages'
1103                      )
1104                  ),
1105                  'warnings' => new external_warnings(),
1106              )
1107          );
1108      }
1109  
1110      /**
1111       * Describes the parameters for launch_attempt.
1112       *
1113       * @return external_function_parameters
1114       * @since Moodle 3.3
1115       */
1116      public static function launch_attempt_parameters() {
1117          return new external_function_parameters (
1118              array(
1119                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
1120                  'password' => new external_value(PARAM_RAW, 'optional password (the lesson may be protected)', VALUE_DEFAULT, ''),
1121                  'pageid' => new external_value(PARAM_INT, 'page id to continue from (only when continuing an attempt)', VALUE_DEFAULT, 0),
1122                  'review' => new external_value(PARAM_BOOL, 'if we want to review just after finishing', VALUE_DEFAULT, false),
1123              )
1124          );
1125      }
1126  
1127      /**
1128       * Return lesson messages formatted according the external_messages structure
1129       *
1130       * @param  lesson $lesson lesson instance
1131       * @return array          messages formatted
1132       * @since Moodle 3.3
1133       */
1134      protected static function format_lesson_messages($lesson) {
1135          $messages = array();
1136          foreach ($lesson->messages as $message) {
1137              $messages[] = array(
1138                  'message' => $message[0],
1139                  'type' => $message[1],
1140              );
1141          }
1142          return $messages;
1143      }
1144  
1145      /**
1146       * Return a external structure representing messages.
1147       *
1148       * @return external_multiple_structure messages structure
1149       * @since Moodle 3.3
1150       */
1151      protected static function external_messages() {
1152          return new external_multiple_structure(
1153              new external_single_structure(
1154                  array(
1155                      'message' => new external_value(PARAM_RAW, 'Message.'),
1156                      'type' => new external_value(PARAM_ALPHANUMEXT, 'Message type: usually a CSS identifier like:
1157                                  success, info, warning, error, notifyproblem, notifyerror, notifytiny, notifysuccess')
1158                  ), 'The lesson generated messages'
1159              )
1160          );
1161      }
1162  
1163      /**
1164       * Starts a new attempt or continues an existing one.
1165       *
1166       * @param int $lessonid lesson instance id
1167       * @param string $password optional password (the lesson may be protected)
1168       * @param int $pageid page id to continue from (only when continuing an attempt)
1169       * @param bool $review if we want to review just after finishing
1170       * @return array of warnings and status result
1171       * @since Moodle 3.3
1172       * @throws moodle_exception
1173       */
1174      public static function launch_attempt($lessonid, $password = '', $pageid = 0, $review = false) {
1175          global $CFG, $USER;
1176  
1177          $params = array('lessonid' => $lessonid, 'password' => $password, 'pageid' => $pageid, 'review' => $review);
1178          $params = self::validate_parameters(self::launch_attempt_parameters(), $params);
1179          $warnings = array();
1180  
1181          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
1182          self::validate_attempt($lesson, $params);
1183  
1184          $newpageid = 0;
1185          // Starting a new lesson attempt.
1186          if (empty($params['pageid'])) {
1187              // Check if there is a recent timer created during the active session.
1188              $alreadystarted = false;
1189              if ($timers = $lesson->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) {
1190                  $timer = array_shift($timers);
1191                  $endtime = $lesson->timelimit > 0 ? min($CFG->sessiontimeout, $lesson->timelimit) : $CFG->sessiontimeout;
1192                  if (!$timer->completed && $timer->starttime > time() - $endtime) {
1193                      $alreadystarted = true;
1194                  }
1195              }
1196              if (!$alreadystarted && !$lesson->can_manage()) {
1197                  $lesson->start_timer();
1198              }
1199          } else {
1200              if ($params['pageid'] == LESSON_EOL) {
1201                  throw new moodle_exception('endoflesson', 'lesson');
1202              }
1203              $timer = $lesson->update_timer(true, true);
1204              if (!$lesson->check_time($timer)) {
1205                  throw new moodle_exception('eolstudentoutoftime', 'lesson');
1206              }
1207          }
1208          $messages = self::format_lesson_messages($lesson);
1209  
1210          $result = array(
1211              'status' => true,
1212              'messages' => $messages,
1213              'warnings' => $warnings,
1214          );
1215          return $result;
1216      }
1217  
1218      /**
1219       * Describes the launch_attempt return value.
1220       *
1221       * @return external_single_structure
1222       * @since Moodle 3.3
1223       */
1224      public static function launch_attempt_returns() {
1225          return new external_single_structure(
1226              array(
1227                  'messages' => self::external_messages(),
1228                  'warnings' => new external_warnings(),
1229              )
1230          );
1231      }
1232  
1233      /**
1234       * Describes the parameters for get_page_data.
1235       *
1236       * @return external_function_parameters
1237       * @since Moodle 3.3
1238       */
1239      public static function get_page_data_parameters() {
1240          return new external_function_parameters (
1241              array(
1242                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
1243                  'pageid' => new external_value(PARAM_INT, 'the page id'),
1244                  'password' => new external_value(PARAM_RAW, 'optional password (the lesson may be protected)', VALUE_DEFAULT, ''),
1245                  'review' => new external_value(PARAM_BOOL, 'if we want to review just after finishing (1 hour margin)',
1246                      VALUE_DEFAULT, false),
1247                  'returncontents' => new external_value(PARAM_BOOL, 'if we must return the complete page contents once rendered',
1248                      VALUE_DEFAULT, false),
1249              )
1250          );
1251      }
1252  
1253      /**
1254       * Return information of a given page, including its contents.
1255       *
1256       * @param int $lessonid lesson instance id
1257       * @param int $pageid page id
1258       * @param string $password optional password (the lesson may be protected)
1259       * @param bool $review if we want to review just after finishing (1 hour margin)
1260       * @param bool $returncontents if we must return the complete page contents once rendered
1261       * @return array of warnings and status result
1262       * @since Moodle 3.3
1263       * @throws moodle_exception
1264       */
1265      public static function get_page_data($lessonid, $pageid,  $password = '', $review = false, $returncontents = false) {
1266          global $PAGE, $USER;
1267  
1268          $params = array('lessonid' => $lessonid, 'password' => $password, 'pageid' => $pageid, 'review' => $review,
1269              'returncontents' => $returncontents);
1270          $params = self::validate_parameters(self::get_page_data_parameters(), $params);
1271  
1272          $warnings = $contentfiles = $answerfiles = $responsefiles = $answers = array();
1273          $pagecontent = $ongoingscore = '';
1274          $progress = $pagedata = null;
1275  
1276          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
1277          self::validate_attempt($lesson, $params);
1278  
1279          $pageid = $params['pageid'];
1280  
1281          // This is called if a student leaves during a lesson.
1282          if ($pageid == LESSON_UNSEENBRANCHPAGE) {
1283              $pageid = lesson_unseen_question_jump($lesson, $USER->id, $pageid);
1284          }
1285  
1286          if ($pageid != LESSON_EOL) {
1287              $reviewmode = $lesson->is_in_review_mode();
1288              $lessonoutput = $PAGE->get_renderer('mod_lesson');
1289              // Prepare page contents avoiding redirections.
1290              list($pageid, $page, $pagecontent) = $lesson->prepare_page_and_contents($pageid, $lessonoutput, $reviewmode, false);
1291  
1292              if ($pageid > 0) {
1293  
1294                  $pagedata = self::get_page_fields($page, true);
1295  
1296                  // Files.
1297                  $contentfiles = external_util::get_area_files($context->id, 'mod_lesson', 'page_contents', $page->id);
1298  
1299                  // Answers.
1300                  $answers = array();
1301                  $pageanswers = $page->get_answers();
1302                  foreach ($pageanswers as $a) {
1303                      $answer = array(
1304                          'id' => $a->id,
1305                          'answerfiles' => external_util::get_area_files($context->id, 'mod_lesson', 'page_answers', $a->id),
1306                          'responsefiles' => external_util::get_area_files($context->id, 'mod_lesson', 'page_responses', $a->id),
1307                      );
1308                      // For managers, return all the information (including correct answers, jumps).
1309                      // If the teacher enabled offline attempts, this information will be downloaded too.
1310                      if ($lesson->can_manage() || $lesson->allowofflineattempts) {
1311                          $extraproperties = array('jumpto', 'grade', 'score', 'flags', 'timecreated', 'timemodified');
1312                          foreach ($extraproperties as $prop) {
1313                              $answer[$prop] = $a->{$prop};
1314                          }
1315  
1316                          $options = array('noclean' => true);
1317                          list($answer['answer'], $answer['answerformat']) =
1318                              external_format_text($a->answer, $a->answerformat, $context->id, 'mod_lesson', 'page_answers', $a->id,
1319                                  $options);
1320                          list($answer['response'], $answer['responseformat']) =
1321                              external_format_text($a->response, $a->responseformat, $context->id, 'mod_lesson', 'page_responses',
1322                                  $a->id, $options);
1323                      }
1324                      $answers[] = $answer;
1325                  }
1326  
1327                  // Additional lesson information.
1328                  if (!$lesson->can_manage()) {
1329                      if ($lesson->ongoing && !$reviewmode) {
1330                          $ongoingscore = $lesson->get_ongoing_score_message();
1331                      }
1332                      if ($lesson->progressbar) {
1333                          $progress = $lesson->calculate_progress();
1334                      }
1335                  }
1336              }
1337          }
1338  
1339          $messages = self::format_lesson_messages($lesson);
1340  
1341          $result = array(
1342              'newpageid' => $pageid,
1343              'ongoingscore' => $ongoingscore,
1344              'progress' => $progress,
1345              'contentfiles' => $contentfiles,
1346              'answers' => $answers,
1347              'messages' => $messages,
1348              'warnings' => $warnings,
1349              'displaymenu' => !empty(lesson_displayleftif($lesson)),
1350          );
1351  
1352          if (!empty($pagedata)) {
1353              $result['page'] = $pagedata;
1354          }
1355          if ($params['returncontents']) {
1356              $result['pagecontent'] = $pagecontent;  // Return the complete page contents rendered.
1357          }
1358  
1359          return $result;
1360      }
1361  
1362      /**
1363       * Describes the get_page_data return value.
1364       *
1365       * @return external_single_structure
1366       * @since Moodle 3.3
1367       */
1368      public static function get_page_data_returns() {
1369          return new external_single_structure(
1370              array(
1371                  'page' => self::get_page_structure(VALUE_OPTIONAL),
1372                  'newpageid' => new external_value(PARAM_INT, 'New page id (if a jump was made)'),
1373                  'pagecontent' => new external_value(PARAM_RAW, 'Page html content', VALUE_OPTIONAL),
1374                  'ongoingscore' => new external_value(PARAM_TEXT, 'The ongoing score message'),
1375                  'progress' => new external_value(PARAM_INT, 'Progress percentage in the lesson'),
1376                  'contentfiles' => new external_files(),
1377                  'answers' => new external_multiple_structure(
1378                      new external_single_structure(
1379                          array(
1380                              'id' => new external_value(PARAM_INT, 'The ID of this answer in the database'),
1381                              'answerfiles' => new external_files(),
1382                              'responsefiles' => new external_files(),
1383                              'jumpto' => new external_value(PARAM_INT, 'Identifies where the user goes upon completing a page with this answer',
1384                                                              VALUE_OPTIONAL),
1385                              'grade' => new external_value(PARAM_INT, 'The grade this answer is worth', VALUE_OPTIONAL),
1386                              'score' => new external_value(PARAM_INT, 'The score this answer will give', VALUE_OPTIONAL),
1387                              'flags' => new external_value(PARAM_INT, 'Used to store options for the answer', VALUE_OPTIONAL),
1388                              'timecreated' => new external_value(PARAM_INT, 'A timestamp of when the answer was created', VALUE_OPTIONAL),
1389                              'timemodified' => new external_value(PARAM_INT, 'A timestamp of when the answer was modified', VALUE_OPTIONAL),
1390                              'answer' => new external_value(PARAM_RAW, 'Possible answer text', VALUE_OPTIONAL),
1391                              'answerformat' => new external_format_value('answer', VALUE_OPTIONAL),
1392                              'response' => new external_value(PARAM_RAW, 'Response text for the answer', VALUE_OPTIONAL),
1393                              'responseformat' => new external_format_value('response', VALUE_OPTIONAL),
1394                          ), 'The page answers'
1395  
1396                      )
1397                  ),
1398                  'messages' => self::external_messages(),
1399                  'displaymenu' => new external_value(PARAM_BOOL, 'Whether we should display the menu or not in this page.'),
1400                  'warnings' => new external_warnings(),
1401              )
1402          );
1403      }
1404  
1405      /**
1406       * Describes the parameters for process_page.
1407       *
1408       * @return external_function_parameters
1409       * @since Moodle 3.3
1410       */
1411      public static function process_page_parameters() {
1412          return new external_function_parameters (
1413              array(
1414                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
1415                  'pageid' => new external_value(PARAM_INT, 'the page id'),
1416                  'data' => new external_multiple_structure(
1417                      new external_single_structure(
1418                          array(
1419                              'name' => new external_value(PARAM_RAW, 'data name'),
1420                              'value' => new external_value(PARAM_RAW, 'data value'),
1421                          )
1422                      ), 'the data to be saved'
1423                  ),
1424                  'password' => new external_value(PARAM_RAW, 'optional password (the lesson may be protected)', VALUE_DEFAULT, ''),
1425                  'review' => new external_value(PARAM_BOOL, 'if we want to review just after finishing (1 hour margin)',
1426                      VALUE_DEFAULT, false),
1427              )
1428          );
1429      }
1430  
1431      /**
1432       * Processes page responses
1433       *
1434       * @param int $lessonid lesson instance id
1435       * @param int $pageid page id
1436       * @param array $data the data to be saved
1437       * @param string $password optional password (the lesson may be protected)
1438       * @param bool $review if we want to review just after finishing (1 hour margin)
1439       * @return array of warnings and status result
1440       * @since Moodle 3.3
1441       * @throws moodle_exception
1442       */
1443      public static function process_page($lessonid, $pageid,  $data, $password = '', $review = false) {
1444          global $USER;
1445  
1446          $params = array('lessonid' => $lessonid, 'pageid' => $pageid, 'data' => $data, 'password' => $password,
1447              'review' => $review);
1448          $params = self::validate_parameters(self::process_page_parameters(), $params);
1449  
1450          $warnings = array();
1451          $pagecontent = $ongoingscore = '';
1452          $progress = null;
1453  
1454          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
1455  
1456          // Update timer so the validation can check the time restrictions.
1457          $timer = $lesson->update_timer();
1458          self::validate_attempt($lesson, $params);
1459  
1460          // Create the $_POST object required by the lesson question engine.
1461          $_POST = array();
1462          foreach ($data as $element) {
1463              // First check if we are handling editor fields like answer[text].
1464              if (preg_match('/(.+)\[(.+)\]$/', $element['name'], $matches)) {
1465                  $_POST[$matches[1]][$matches[2]] = $element['value'];
1466              } else {
1467                  $_POST[$element['name']] = $element['value'];
1468              }
1469          }
1470  
1471          // Ignore sesskey (deep in some APIs), the request is already validated.
1472          $USER->ignoresesskey = true;
1473  
1474          // Process page.
1475          $page = $lesson->load_page($params['pageid']);
1476          $result = $lesson->process_page_responses($page);
1477  
1478          // Prepare messages.
1479          $reviewmode = $lesson->is_in_review_mode();
1480          $lesson->add_messages_on_page_process($page, $result, $reviewmode);
1481  
1482          // Additional lesson information.
1483          if (!$lesson->can_manage()) {
1484              if ($lesson->ongoing && !$reviewmode) {
1485                  $ongoingscore = $lesson->get_ongoing_score_message();
1486              }
1487              if ($lesson->progressbar) {
1488                  $progress = $lesson->calculate_progress();
1489              }
1490          }
1491  
1492          // Check conditionally everything coming from result (except newpageid because is always set).
1493          $result = array(
1494              'newpageid'         => (int) $result->newpageid,
1495              'inmediatejump'     => $result->inmediatejump,
1496              'nodefaultresponse' => !empty($result->nodefaultresponse),
1497              'feedback'          => (isset($result->feedback)) ? $result->feedback : '',
1498              'attemptsremaining' => (isset($result->attemptsremaining)) ? $result->attemptsremaining : null,
1499              'correctanswer'     => !empty($result->correctanswer),
1500              'noanswer'          => !empty($result->noanswer),
1501              'isessayquestion'   => !empty($result->isessayquestion),
1502              'maxattemptsreached' => !empty($result->maxattemptsreached),
1503              'response'          => (isset($result->response)) ? $result->response : '',
1504              'studentanswer'     => (isset($result->studentanswer)) ? $result->studentanswer : '',
1505              'userresponse'      => (isset($result->userresponse)) ? $result->userresponse : '',
1506              'reviewmode'        => $reviewmode,
1507              'ongoingscore'      => $ongoingscore,
1508              'progress'          => $progress,
1509              'displaymenu'       => !empty(lesson_displayleftif($lesson)),
1510              'messages'          => self::format_lesson_messages($lesson),
1511              'warnings'          => $warnings,
1512          );
1513          return $result;
1514      }
1515  
1516      /**
1517       * Describes the process_page return value.
1518       *
1519       * @return external_single_structure
1520       * @since Moodle 3.3
1521       */
1522      public static function process_page_returns() {
1523          return new external_single_structure(
1524              array(
1525                  'newpageid' => new external_value(PARAM_INT, 'New page id (if a jump was made).'),
1526                  'inmediatejump' => new external_value(PARAM_BOOL, 'Whether the page processing redirect directly to anoter page.'),
1527                  'nodefaultresponse' => new external_value(PARAM_BOOL, 'Whether there is not a default response.'),
1528                  'feedback' => new external_value(PARAM_RAW, 'The response feedback.'),
1529                  'attemptsremaining' => new external_value(PARAM_INT, 'Number of attempts remaining.'),
1530                  'correctanswer' => new external_value(PARAM_BOOL, 'Whether the answer is correct.'),
1531                  'noanswer' => new external_value(PARAM_BOOL, 'Whether there aren\'t answers.'),
1532                  'isessayquestion' => new external_value(PARAM_BOOL, 'Whether is a essay question.'),
1533                  'maxattemptsreached' => new external_value(PARAM_BOOL, 'Whether we reachered the max number of attempts.'),
1534                  'response' => new external_value(PARAM_RAW, 'The response.'),
1535                  'studentanswer' => new external_value(PARAM_RAW, 'The student answer.'),
1536                  'userresponse' => new external_value(PARAM_RAW, 'The user response.'),
1537                  'reviewmode' => new external_value(PARAM_BOOL, 'Whether the user is reviewing.'),
1538                  'ongoingscore' => new external_value(PARAM_TEXT, 'The ongoing message.'),
1539                  'progress' => new external_value(PARAM_INT, 'Progress percentage in the lesson.'),
1540                  'displaymenu' => new external_value(PARAM_BOOL, 'Whether we should display the menu or not in this page.'),
1541                  'messages' => self::external_messages(),
1542                  'warnings' => new external_warnings(),
1543              )
1544          );
1545      }
1546  
1547      /**
1548       * Describes the parameters for finish_attempt.
1549       *
1550       * @return external_function_parameters
1551       * @since Moodle 3.3
1552       */
1553      public static function finish_attempt_parameters() {
1554          return new external_function_parameters (
1555              array(
1556                  'lessonid' => new external_value(PARAM_INT, 'Lesson instance id.'),
1557                  'password' => new external_value(PARAM_RAW, 'Optional password (the lesson may be protected).', VALUE_DEFAULT, ''),
1558                  'outoftime' => new external_value(PARAM_BOOL, 'If the user run out of time.', VALUE_DEFAULT, false),
1559                  'review' => new external_value(PARAM_BOOL, 'If we want to review just after finishing (1 hour margin).',
1560                      VALUE_DEFAULT, false),
1561              )
1562          );
1563      }
1564  
1565      /**
1566       * Finishes the current attempt.
1567       *
1568       * @param int $lessonid lesson instance id
1569       * @param string $password optional password (the lesson may be protected)
1570       * @param bool $outoftime optional if the user run out of time
1571       * @param bool $review if we want to review just after finishing (1 hour margin)
1572       * @return array of warnings and information about the finished attempt
1573       * @since Moodle 3.3
1574       * @throws moodle_exception
1575       */
1576      public static function finish_attempt($lessonid, $password = '', $outoftime = false, $review = false) {
1577  
1578          $params = array('lessonid' => $lessonid, 'password' => $password, 'outoftime' => $outoftime, 'review' => $review);
1579          $params = self::validate_parameters(self::finish_attempt_parameters(), $params);
1580  
1581          $warnings = array();
1582  
1583          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
1584  
1585          // Update timer so the validation can check the time restrictions.
1586          $timer = $lesson->update_timer();
1587  
1588          // Return the validation to avoid exceptions in case the user is out of time.
1589          $params['pageid'] = LESSON_EOL;
1590          $validation = self::validate_attempt($lesson, $params, true);
1591  
1592          if (array_key_exists('eolstudentoutoftime', $validation)) {
1593              // Maybe we run out of time just now.
1594              $params['outoftime'] = true;
1595              unset($validation['eolstudentoutoftime']);
1596          }
1597          // Check if there are more errors.
1598          if (!empty($validation)) {
1599              reset($validation);
1600              throw new moodle_exception(key($validation), 'lesson', '', current($validation));   // Throw first error.
1601          }
1602  
1603          // Set out of time to normal (it is the only existing mode).
1604          $outoftimemode = $params['outoftime'] ? 'normal' : '';
1605          $result = $lesson->process_eol_page($outoftimemode);
1606  
1607          // Return the data.
1608           $validmessages = array(
1609              'notenoughtimespent', 'numberofpagesviewed', 'youshouldview', 'numberofcorrectanswers',
1610              'displayscorewithessays', 'displayscorewithoutessays', 'yourcurrentgradeisoutof', 'eolstudentoutoftimenoanswers',
1611              'welldone', 'displayofgrade', 'modattemptsnoteacher', 'progresscompleted');
1612  
1613          $data = array();
1614          foreach ($result as $el => $value) {
1615              if ($value !== false) {
1616                  $message = '';
1617                  if (in_array($el, $validmessages)) { // Check if the data comes with an informative message.
1618                      $a = (is_bool($value)) ? null : $value;
1619                      $message = get_string($el, 'lesson', $a);
1620                  }
1621                  // Return the data.
1622                  $data[] = array(
1623                      'name' => $el,
1624                      'value' => (is_bool($value)) ? 1 : json_encode($value), // The data can be a php object.
1625                      'message' => $message
1626                  );
1627              }
1628          }
1629  
1630          $result = array(
1631              'data'     => $data,
1632              'messages' => self::format_lesson_messages($lesson),
1633              'warnings' => $warnings,
1634          );
1635          return $result;
1636      }
1637  
1638      /**
1639       * Describes the finish_attempt return value.
1640       *
1641       * @return external_single_structure
1642       * @since Moodle 3.3
1643       */
1644      public static function finish_attempt_returns() {
1645          return new external_single_structure(
1646              array(
1647                  'data' => new external_multiple_structure(
1648                      new external_single_structure(
1649                          array(
1650                              'name' => new external_value(PARAM_ALPHANUMEXT, 'Data name.'),
1651                              'value' => new external_value(PARAM_RAW, 'Data value.'),
1652                              'message' => new external_value(PARAM_RAW, 'Data message (translated string).'),
1653                          )
1654                      ), 'The EOL page information data.'
1655                  ),
1656                  'messages' => self::external_messages(),
1657                  'warnings' => new external_warnings(),
1658              )
1659          );
1660      }
1661  
1662      /**
1663       * Describes the parameters for get_attempts_overview.
1664       *
1665       * @return external_function_parameters
1666       * @since Moodle 3.3
1667       */
1668      public static function get_attempts_overview_parameters() {
1669          return new external_function_parameters (
1670              array(
1671                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
1672                  'groupid' => new external_value(PARAM_INT, 'group id, 0 means that the function will determine the user group',
1673                                                  VALUE_DEFAULT, 0),
1674              )
1675          );
1676      }
1677  
1678      /**
1679       * Get a list of all the attempts made by users in a lesson.
1680       *
1681       * @param int $lessonid lesson instance id
1682       * @param int $groupid group id, 0 means that the function will determine the user group
1683       * @return array of warnings and status result
1684       * @since Moodle 3.3
1685       * @throws moodle_exception
1686       */
1687      public static function get_attempts_overview($lessonid, $groupid = 0) {
1688  
1689          $params = array('lessonid' => $lessonid, 'groupid' => $groupid);
1690          $params = self::validate_parameters(self::get_attempts_overview_parameters(), $params);
1691          $warnings = array();
1692  
1693          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
1694          require_capability('mod/lesson:viewreports', $context);
1695  
1696          if (!empty($params['groupid'])) {
1697              $groupid = $params['groupid'];
1698              // Determine is the group is visible to user.
1699              if (!groups_group_visible($groupid, $course, $cm)) {
1700                  throw new moodle_exception('notingroup');
1701              }
1702          } else {
1703              // Check to see if groups are being used here.
1704              if ($groupmode = groups_get_activity_groupmode($cm)) {
1705                  $groupid = groups_get_activity_group($cm);
1706                  // Determine is the group is visible to user (this is particullary for the group 0 -> all groups).
1707                  if (!groups_group_visible($groupid, $course, $cm)) {
1708                      throw new moodle_exception('notingroup');
1709                  }
1710              } else {
1711                  $groupid = 0;
1712              }
1713          }
1714  
1715          $result = array(
1716              'warnings' => $warnings
1717          );
1718  
1719          list($table, $data) = lesson_get_overview_report_table_and_data($lesson, $groupid);
1720          if ($data !== false) {
1721              $result['data'] = $data;
1722          }
1723  
1724          return $result;
1725      }
1726  
1727      /**
1728       * Describes the get_attempts_overview return value.
1729       *
1730       * @return external_single_structure
1731       * @since Moodle 3.3
1732       */
1733      public static function get_attempts_overview_returns() {
1734          return new external_single_structure(
1735              array(
1736                  'data' => new external_single_structure(
1737                      array(
1738                          'lessonscored' => new external_value(PARAM_BOOL, 'True if the lesson was scored.'),
1739                          'numofattempts' => new external_value(PARAM_INT, 'Number of attempts.'),
1740                          'avescore' => new external_value(PARAM_FLOAT, 'Average score.'),
1741                          'highscore' => new external_value(PARAM_FLOAT, 'High score.'),
1742                          'lowscore' => new external_value(PARAM_FLOAT, 'Low score.'),
1743                          'avetime' => new external_value(PARAM_INT, 'Average time (spent in taking the lesson).'),
1744                          'hightime' => new external_value(PARAM_INT, 'High time.'),
1745                          'lowtime' => new external_value(PARAM_INT, 'Low time.'),
1746                          'students' => new external_multiple_structure(
1747                              new external_single_structure(
1748                                  array(
1749                                      'id' => new external_value(PARAM_INT, 'User id.'),
1750                                      'fullname' => new external_value(PARAM_TEXT, 'User full name.'),
1751                                      'bestgrade' => new external_value(PARAM_FLOAT, 'Best grade.'),
1752                                      'attempts' => new external_multiple_structure(
1753                                          new external_single_structure(
1754                                              array(
1755                                                  'try' => new external_value(PARAM_INT, 'Attempt number.'),
1756                                                  'grade' => new external_value(PARAM_FLOAT, 'Attempt grade.'),
1757                                                  'timestart' => new external_value(PARAM_INT, 'Attempt time started.'),
1758                                                  'timeend' => new external_value(PARAM_INT, 'Attempt last time continued.'),
1759                                                  'end' => new external_value(PARAM_INT, 'Attempt time ended.'),
1760                                              )
1761                                          )
1762                                      )
1763                                  )
1764                              ), 'Students data, including attempts.', VALUE_OPTIONAL
1765                          ),
1766                      ),
1767                      'Attempts overview data (empty for no attemps).', VALUE_OPTIONAL
1768                  ),
1769                  'warnings' => new external_warnings(),
1770              )
1771          );
1772      }
1773  
1774      /**
1775       * Describes the parameters for get_user_attempt.
1776       *
1777       * @return external_function_parameters
1778       * @since Moodle 3.3
1779       */
1780      public static function get_user_attempt_parameters() {
1781          return new external_function_parameters (
1782              array(
1783                  'lessonid' => new external_value(PARAM_INT, 'Lesson instance id.'),
1784                  'userid' => new external_value(PARAM_INT, 'The user id. 0 for current user.'),
1785                  'lessonattempt' => new external_value(PARAM_INT, 'The attempt number.'),
1786              )
1787          );
1788      }
1789  
1790      /**
1791       * Return information about the given user attempt (including answers).
1792       *
1793       * @param int $lessonid lesson instance id
1794       * @param int $userid the user id
1795       * @param int $lessonattempt the attempt number
1796       * @return array of warnings and page attempts
1797       * @since Moodle 3.3
1798       * @throws moodle_exception
1799       */
1800      public static function get_user_attempt($lessonid, $userid, $lessonattempt) {
1801          global $USER;
1802  
1803          $params = array(
1804              'lessonid' => $lessonid,
1805              'userid' => $userid,
1806              'lessonattempt' => $lessonattempt,
1807          );
1808          $params = self::validate_parameters(self::get_user_attempt_parameters(), $params);
1809          $warnings = array();
1810  
1811          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
1812  
1813          // Default value for userid.
1814          if (empty($params['userid'])) {
1815              $params['userid'] = $USER->id;
1816          }
1817  
1818          // Extra checks so only users with permissions can view other users attempts.
1819          if ($USER->id != $params['userid']) {
1820              self::check_can_view_user_data($params['userid'], $course, $cm, $context);
1821          }
1822  
1823          list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $params['lessonattempt']);
1824          // Convert page object to page record.
1825          foreach ($answerpages as $answerp) {
1826              $answerp->page = self::get_page_fields($answerp->page);
1827          }
1828  
1829          $result = array(
1830              'answerpages' => $answerpages,
1831              'userstats' => $userstats,
1832              'warnings' => $warnings,
1833          );
1834          return $result;
1835      }
1836  
1837      /**
1838       * Describes the get_user_attempt return value.
1839       *
1840       * @return external_single_structure
1841       * @since Moodle 3.3
1842       */
1843      public static function get_user_attempt_returns() {
1844          return new external_single_structure(
1845              array(
1846                  'answerpages' => new external_multiple_structure(
1847                      new external_single_structure(
1848                          array(
1849                              'page' => self::get_page_structure(VALUE_OPTIONAL),
1850                              'title' => new external_value(PARAM_RAW, 'Page title.'),
1851                              'contents' => new external_value(PARAM_RAW, 'Page contents.'),
1852                              'qtype' => new external_value(PARAM_TEXT, 'Identifies the page type of this page.'),
1853                              'grayout' => new external_value(PARAM_INT, 'If is required to apply a grayout.'),
1854                              'answerdata' => new external_single_structure(
1855                                  array(
1856                                      'score' => new external_value(PARAM_TEXT, 'The score (text version).'),
1857                                      'response' => new external_value(PARAM_RAW, 'The response text.'),
1858                                      'responseformat' => new external_format_value('response.'),
1859                                      'answers' => new external_multiple_structure(
1860                                          new external_multiple_structure(new external_value(PARAM_RAW, 'Possible answers and info.')),
1861                                          'User answers',
1862                                          VALUE_OPTIONAL
1863                                      ),
1864                                  ), 'Answer data (empty in content pages created in Moodle 1.x).', VALUE_OPTIONAL
1865                              )
1866                          )
1867                      )
1868                  ),
1869                  'userstats' => new external_single_structure(
1870                      array(
1871                          'grade' => new external_value(PARAM_FLOAT, 'Attempt final grade.'),
1872                          'completed' => new external_value(PARAM_INT, 'Time completed.'),
1873                          'timetotake' => new external_value(PARAM_INT, 'Time taken.'),
1874                          'gradeinfo' => self::get_user_attempt_grade_structure(VALUE_OPTIONAL)
1875                      )
1876                  ),
1877                  'warnings' => new external_warnings(),
1878              )
1879          );
1880      }
1881  
1882      /**
1883       * Describes the parameters for get_pages_possible_jumps.
1884       *
1885       * @return external_function_parameters
1886       * @since Moodle 3.3
1887       */
1888      public static function get_pages_possible_jumps_parameters() {
1889          return new external_function_parameters (
1890              array(
1891                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
1892              )
1893          );
1894      }
1895  
1896      /**
1897       * Return all the possible jumps for the pages in a given lesson.
1898       *
1899       * You may expect different results on consecutive executions due to the random nature of the lesson module.
1900       *
1901       * @param int $lessonid lesson instance id
1902       * @return array of warnings and possible jumps
1903       * @since Moodle 3.3
1904       * @throws moodle_exception
1905       */
1906      public static function get_pages_possible_jumps($lessonid) {
1907          global $USER;
1908  
1909          $params = array('lessonid' => $lessonid);
1910          $params = self::validate_parameters(self::get_pages_possible_jumps_parameters(), $params);
1911  
1912          $warnings = $jumps = array();
1913  
1914          list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
1915  
1916          // Only return for managers or if offline attempts are enabled.
1917          if ($lesson->can_manage() || $lesson->allowofflineattempts) {
1918  
1919              $lessonpages = $lesson->load_all_pages();
1920              foreach ($lessonpages as $page) {
1921                  $jump = array();
1922                  $jump['pageid'] = $page->id;
1923  
1924                  $answers = $page->get_answers();
1925                  if (count($answers) > 0) {
1926                      foreach ($answers as $answer) {
1927                          $jump['answerid'] = $answer->id;
1928                          $jump['jumpto'] = $answer->jumpto;
1929                          $jump['calculatedjump'] = $lesson->calculate_new_page_on_jump($page, $answer->jumpto);
1930                          // Special case, only applies to branch/end of branch.
1931                          if ($jump['calculatedjump'] == LESSON_RANDOMBRANCH) {
1932                              $jump['calculatedjump'] = lesson_unseen_branch_jump($lesson, $USER->id);
1933                          }
1934                          $jumps[] = $jump;
1935                      }
1936                  } else {
1937                      // Imported lessons from 1.x.
1938                      $jump['answerid'] = 0;
1939                      $jump['jumpto'] = $page->nextpageid;
1940                      $jump['calculatedjump'] = $lesson->calculate_new_page_on_jump($page, $page->nextpageid);
1941                      $jumps[] = $jump;
1942                  }
1943              }
1944          }
1945  
1946          $result = array(
1947              'jumps' => $jumps,
1948              'warnings' => $warnings,
1949          );
1950          return $result;
1951      }
1952  
1953      /**
1954       * Describes the get_pages_possible_jumps return value.
1955       *
1956       * @return external_single_structure
1957       * @since Moodle 3.3
1958       */
1959      public static function get_pages_possible_jumps_returns() {
1960          return new external_single_structure(
1961              array(
1962                  'jumps' => new external_multiple_structure(
1963                      new external_single_structure(
1964                          array(
1965                              'pageid' => new external_value(PARAM_INT, 'The page id'),
1966                              'answerid' => new external_value(PARAM_INT, 'The answer id'),
1967                              'jumpto' => new external_value(PARAM_INT, 'The jump (page id or type of jump)'),
1968                              'calculatedjump' => new external_value(PARAM_INT, 'The real page id (or EOL) to jump'),
1969                          ), 'Jump for a page answer'
1970                      )
1971                  ),
1972                  'warnings' => new external_warnings(),
1973              )
1974          );
1975      }
1976  
1977      /**
1978       * Describes the parameters for get_lesson.
1979       *
1980       * @return external_function_parameters
1981       * @since Moodle 3.3
1982       */
1983      public static function get_lesson_parameters() {
1984          return new external_function_parameters (
1985              array(
1986                  'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
1987                  'password' => new external_value(PARAM_RAW, 'lesson password', VALUE_DEFAULT, ''),
1988              )
1989          );
1990      }
1991  
1992      /**
1993       * Return information of a given lesson.
1994       *
1995       * @param int $lessonid lesson instance id
1996       * @param string $password optional password (the lesson may be protected)
1997       * @return array of warnings and status result
1998       * @since Moodle 3.3
1999       * @throws moodle_exception
2000       */
2001      public static function get_lesson($lessonid, $password = '') {
2002          global $PAGE;
2003  
2004          $params = array('lessonid' => $lessonid, 'password' => $password);
2005          $params = self::validate_parameters(self::get_lesson_parameters(), $params);
2006          $warnings = array();
2007  
2008          list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
2009  
2010          $lessonrecord = self::get_lesson_summary_for_exporter($lessonrecord, $params['password']);
2011          $exporter = new lesson_summary_exporter($lessonrecord, array('context' => $context));
2012  
2013          $result = array();
2014          $result['lesson'] = $exporter->export($PAGE->get_renderer('core'));
2015          $result['warnings'] = $warnings;
2016          return $result;
2017      }
2018  
2019      /**
2020       * Describes the get_lesson return value.
2021       *
2022       * @return external_single_structure
2023       * @since Moodle 3.3
2024       */
2025      public static function get_lesson_returns() {
2026          return new external_single_structure(
2027              array(
2028                  'lesson' => lesson_summary_exporter::get_read_structure(),
2029                  'warnings' => new external_warnings(),
2030              )
2031          );
2032      }
2033  }