Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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   * Contains the base definition class for any course format plugin.
  19   *
  20   * @package   core_courseformat
  21   * @copyright 2020 Ferran Recio <ferran@moodle.com>
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_courseformat;
  26  
  27  use navigation_node;
  28  use moodle_page;
  29  use cm_info;
  30  use core_component;
  31  use course_modinfo;
  32  use html_writer;
  33  use section_info;
  34  use context_course;
  35  use editsection_form;
  36  use moodle_exception;
  37  use coding_exception;
  38  use moodle_url;
  39  use lang_string;
  40  use completion_info;
  41  use core_external\external_api;
  42  use stdClass;
  43  use cache;
  44  use core_courseformat\output\legacy_renderer;
  45  
  46  /**
  47   * Base class for course formats
  48   *
  49   * Each course format must declare class
  50   * class format_FORMATNAME extends core_courseformat\base {}
  51   * in file lib.php
  52   *
  53   * For each course just one instance of this class is created and it will always be returned by
  54   * course_get_format($courseorid). Format may store it's specific course-dependent options in
  55   * variables of this class.
  56   *
  57   * In rare cases instance of child class may be created just for format without course id
  58   * i.e. to check if format supports AJAX.
  59   *
  60   * Also course formats may extend class section_info and overwrite
  61   * course_format::build_section_cache() to return more information about sections.
  62   *
  63   * If you are upgrading from Moodle 2.3 start with copying the class format_legacy and renaming
  64   * it to format_FORMATNAME, then move the code from your callback functions into
  65   * appropriate functions of the class.
  66   *
  67   * @package    core_courseformat
  68   * @copyright  2012 Marina Glancy
  69   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  70   */
  71  abstract class base {
  72      /** @var int Id of the course in this instance (maybe 0) */
  73      protected $courseid;
  74      /** @var string format used for this course. Please note that it can be different from
  75       * course.format field if course referes to non-existing of disabled format */
  76      protected $format;
  77      /** @var stdClass course data for course object, please use course_format::get_course() */
  78      protected $course = false;
  79      /** @var array caches format options, please use course_format::get_format_options() */
  80      protected $formatoptions = array();
  81      /** @var int the section number in single section format, zero for multiple section formats. */
  82      protected $singlesection = 0;
  83      /** @var course_modinfo the current course modinfo, please use course_format::get_modinfo() */
  84      private $modinfo = null;
  85      /** @var array cached instances */
  86      private static $instances = array();
  87      /** @var array plugin name => class name. */
  88      private static $classesforformat = array('site' => 'site');
  89      /** @var sectionmanager the format section manager. */
  90      protected $sectionmanager = null;
  91  
  92      /**
  93       * Creates a new instance of class
  94       *
  95       * Please use course_get_format($courseorid) to get an instance of the format class
  96       *
  97       * @param string $format
  98       * @param int $courseid
  99       * @return course_format
 100       */
 101      protected function __construct($format, $courseid) {
 102          $this->format = $format;
 103          $this->courseid = $courseid;
 104      }
 105  
 106      /**
 107       * Validates that course format exists and enabled and returns either itself or default format
 108       *
 109       * @param string $format
 110       * @return string
 111       */
 112      protected static final function get_format_or_default($format) {
 113          global $CFG;
 114          require_once($CFG->dirroot . '/course/lib.php');
 115  
 116          if (array_key_exists($format, self::$classesforformat)) {
 117              return self::$classesforformat[$format];
 118          }
 119  
 120          $plugins = get_sorted_course_formats();
 121          foreach ($plugins as $plugin) {
 122              self::$classesforformat[$plugin] = $plugin;
 123          }
 124  
 125          if (array_key_exists($format, self::$classesforformat)) {
 126              return self::$classesforformat[$format];
 127          }
 128  
 129          if (PHPUNIT_TEST && class_exists('format_' . $format)) {
 130              // Allow unittests to use non-existing course formats.
 131              return $format;
 132          }
 133  
 134          // Else return default format.
 135          $defaultformat = get_config('moodlecourse', 'format');
 136          if (!in_array($defaultformat, $plugins)) {
 137              // When default format is not set correctly, use the first available format.
 138              $defaultformat = reset($plugins);
 139          }
 140          debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER);
 141  
 142          self::$classesforformat[$format] = $defaultformat;
 143          return $defaultformat;
 144      }
 145  
 146      /**
 147       * Get class name for the format.
 148       *
 149       * If course format xxx does not declare class format_xxx, format_legacy will be returned.
 150       * This function also includes lib.php file from corresponding format plugin
 151       *
 152       * @param string $format
 153       * @return string
 154       */
 155      protected static final function get_class_name($format) {
 156          global $CFG;
 157          static $classnames = array('site' => 'format_site');
 158          if (!isset($classnames[$format])) {
 159              $plugins = core_component::get_plugin_list('format');
 160              $usedformat = self::get_format_or_default($format);
 161              if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) {
 162                  require_once($plugins[$usedformat].'/lib.php');
 163              }
 164              $classnames[$format] = 'format_'. $usedformat;
 165              if (!class_exists($classnames[$format])) {
 166                  require_once($CFG->dirroot.'/course/format/formatlegacy.php');
 167                  $classnames[$format] = 'format_legacy';
 168              }
 169          }
 170          return $classnames[$format];
 171      }
 172  
 173      /**
 174       * Returns an instance of the class
 175       *
 176       * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances
 177       *
 178       * @param int|stdClass $courseorid either course id or
 179       *     an object that has the property 'format' and may contain property 'id'
 180       * @return course_format
 181       */
 182      public static final function instance($courseorid) {
 183          global $DB;
 184          if (!is_object($courseorid)) {
 185              $courseid = (int)$courseorid;
 186              if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) {
 187                  $formats = array_keys(self::$instances[$courseid]);
 188                  $format = reset($formats);
 189              } else {
 190                  $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST);
 191              }
 192          } else {
 193              $format = $courseorid->format;
 194              if (isset($courseorid->id)) {
 195                  $courseid = clean_param($courseorid->id, PARAM_INT);
 196              } else {
 197                  $courseid = 0;
 198              }
 199          }
 200          // Validate that format exists and enabled, use default otherwise.
 201          $format = self::get_format_or_default($format);
 202          if (!isset(self::$instances[$courseid][$format])) {
 203              $classname = self::get_class_name($format);
 204              self::$instances[$courseid][$format] = new $classname($format, $courseid);
 205          }
 206          return self::$instances[$courseid][$format];
 207      }
 208  
 209      /**
 210       * Resets cache for the course (or all caches)
 211       *
 212       * To be called from rebuild_course_cache()
 213       *
 214       * @param int $courseid
 215       */
 216      public static final function reset_course_cache($courseid = 0) {
 217          if ($courseid) {
 218              if (isset(self::$instances[$courseid])) {
 219                  foreach (self::$instances[$courseid] as $format => $object) {
 220                      // In case somebody keeps the reference to course format object.
 221                      self::$instances[$courseid][$format]->course = false;
 222                      self::$instances[$courseid][$format]->formatoptions = array();
 223                      self::$instances[$courseid][$format]->modinfo = null;
 224                  }
 225                  unset(self::$instances[$courseid]);
 226              }
 227          } else {
 228              self::$instances = array();
 229          }
 230      }
 231      /**
 232       * Reset the current user for all courses.
 233       *
 234       * The course format cache resets every time the course cache resets but
 235       * also when the user changes their language, all course editors
 236       *
 237       * @return void
 238       */
 239      public static function session_cache_reset_all(): void {
 240          $statecache = cache::make('core', 'courseeditorstate');
 241          $statecache->purge();
 242      }
 243  
 244      /**
 245       * Reset the current user course format cache.
 246       *
 247       * The course format cache resets every time the course cache resets but
 248       * also when the user changes their course format preference, complete
 249       * an activity...
 250       *
 251       * @param stdClass $course the course object
 252       * @return string the new statekey
 253       */
 254      public static function session_cache_reset(stdClass $course): string {
 255          $statecache = cache::make('core', 'courseeditorstate');
 256          $newkey = $course->cacherev . '_' . time();
 257          $statecache->set($course->id, $newkey);
 258          return $newkey;
 259      }
 260  
 261      /**
 262       * Return the current user course format cache key.
 263       *
 264       * The course format session cache can be used to cache the
 265       * user course representation. The statekey will be reset when the
 266       * the course state changes. For example when the course is edited,
 267       * the user completes an activity or simply some course preference
 268       * like collapsing a section happens.
 269       *
 270       * @param stdClass $course the course object
 271       * @return string the current statekey
 272       */
 273      public static function session_cache(stdClass $course): string {
 274          $statecache = cache::make('core', 'courseeditorstate');
 275          $statekey = $statecache->get($course->id);
 276          // Validate the statekey code.
 277          if (preg_match('/^[0-9]+_[0-9]+$/', $statekey)) {
 278              list($cacherev) = explode('_', $statekey);
 279              if ($cacherev == $course->cacherev) {
 280                  return $statekey;
 281              }
 282          }
 283          return self::session_cache_reset($course);
 284      }
 285  
 286      /**
 287       * Returns the format name used by this course
 288       *
 289       * @return string
 290       */
 291      public final function get_format() {
 292          return $this->format;
 293      }
 294  
 295      /**
 296       * Returns id of the course (0 if course is not specified)
 297       *
 298       * @return int
 299       */
 300      public final function get_courseid() {
 301          return $this->courseid;
 302      }
 303  
 304      /**
 305       * Returns the course context.
 306       *
 307       * @return context_course the course context
 308       */
 309      final public function get_context(): context_course {
 310          return context_course::instance($this->courseid);
 311      }
 312  
 313      /**
 314       * Returns a record from course database table plus additional fields
 315       * that course format defines
 316       *
 317       * @return stdClass
 318       */
 319      public function get_course() {
 320          global $DB;
 321          if (!$this->courseid) {
 322              return null;
 323          }
 324          if ($this->course === false) {
 325              $this->course = get_course($this->courseid);
 326              $options = $this->get_format_options();
 327              $dbcoursecolumns = null;
 328              foreach ($options as $optionname => $optionvalue) {
 329                  if (isset($this->course->$optionname)) {
 330                      // Course format options must not have the same names as existing columns in db table "course".
 331                      if (!isset($dbcoursecolumns)) {
 332                          $dbcoursecolumns = $DB->get_columns('course');
 333                      }
 334                      if (isset($dbcoursecolumns[$optionname])) {
 335                          debugging('The option name '.$optionname.' in course format '.$this->format.
 336                              ' is invalid because the field with the same name exists in {course} table',
 337                              DEBUG_DEVELOPER);
 338                          continue;
 339                      }
 340                  }
 341                  $this->course->$optionname = $optionvalue;
 342              }
 343          }
 344          return $this->course;
 345      }
 346  
 347      /**
 348       * Get the course display value for the current course.
 349       *
 350       * Formats extending topics or weeks will use coursedisplay as this setting name
 351       * so they don't need to override the method. However, if the format uses a different
 352       * display logic it must override this method to ensure the core renderers know
 353       * if a COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE is being used.
 354       *
 355       * @return int The current value (COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE)
 356       */
 357      public function get_course_display(): int {
 358          return $this->get_course()->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE;
 359      }
 360  
 361      /**
 362       * Return the current course modinfo.
 363       *
 364       * This method is used mainly by the output components to avoid unnecesary get_fast_modinfo calls.
 365       *
 366       * @return course_modinfo
 367       */
 368      public function get_modinfo(): course_modinfo {
 369          global $USER;
 370          if ($this->modinfo === null) {
 371              $this->modinfo = course_modinfo::instance($this->courseid, $USER->id);
 372          }
 373          return $this->modinfo;
 374      }
 375  
 376      /**
 377       * Method used in the rendered and during backup instead of legacy 'numsections'
 378       *
 379       * Default renderer will treat sections with sectionnumber greater that the value returned by this
 380       * method as "orphaned" and not display them on the course page unless in editing mode.
 381       * Backup will store this value as 'numsections'.
 382       *
 383       * This method ensures that 3rd party course format plugins that still use 'numsections' continue to
 384       * work but at the same time we no longer expect formats to have 'numsections' property.
 385       *
 386       * @return int The last section number, or -1 if sections are entirely missing
 387       */
 388      public function get_last_section_number() {
 389          $course = $this->get_course();
 390          if (isset($course->numsections)) {
 391              return $course->numsections;
 392          }
 393          $modinfo = get_fast_modinfo($course);
 394          $sections = $modinfo->get_section_info_all();
 395  
 396          // Sections seem to be missing entirely. Avoid subsequent errors and return early.
 397          if (count($sections) === 0) {
 398              return -1;
 399          }
 400  
 401          return (int)max(array_keys($sections));
 402      }
 403  
 404      /**
 405       * Method used to get the maximum number of sections for this course format.
 406       * @return int
 407       */
 408      public function get_max_sections() {
 409          $maxsections = get_config('moodlecourse', 'maxsections');
 410          if (!isset($maxsections) || !is_numeric($maxsections)) {
 411              $maxsections = 52;
 412          }
 413          return $maxsections;
 414      }
 415  
 416      /**
 417       * Returns true if the course has a front page.
 418       *
 419       * This function is called to determine if the course has a view page, whether or not
 420       * it contains a listing of activities. It can be useful to set this to false when the course
 421       * format has only one activity and ignores the course page. Or if there are multiple
 422       * activities but no page to see the centralised information.
 423       *
 424       * Initially this was created to know if forms should add a button to return to the course page.
 425       * So if 'Return to course' does not make sense in your format your should probably return false.
 426       *
 427       * @return boolean
 428       * @since Moodle 2.6
 429       */
 430      public function has_view_page() {
 431          return true;
 432      }
 433  
 434      /**
 435       * Generate the title for this section page.
 436       *
 437       * @return string the page title
 438       */
 439      public function page_title(): string {
 440          global $PAGE;
 441          return $PAGE->title;
 442      }
 443  
 444      /**
 445       * Returns true if this course format uses sections
 446       *
 447       * This function may be called without specifying the course id
 448       * i.e. in course_format_uses_sections()
 449       *
 450       * Developers, note that if course format does use sections there should be defined a language
 451       * string with the name 'sectionname' defining what the section relates to in the format, i.e.
 452       * $string['sectionname'] = 'Topic';
 453       * or
 454       * $string['sectionname'] = 'Week';
 455       *
 456       * @return bool
 457       */
 458      public function uses_sections() {
 459          return false;
 460      }
 461  
 462      /**
 463       * Returns true if this course format uses course index
 464       *
 465       * This function may be called without specifying the course id
 466       * i.e. in course_index_drawer()
 467       *
 468       * @return bool
 469       */
 470      public function uses_course_index() {
 471          return false;
 472      }
 473  
 474      /**
 475       * Returns true if this course format uses activity indentation.
 476       *
 477       * @return bool if the course format uses indentation.
 478       */
 479      public function uses_indentation(): bool {
 480          return true;
 481      }
 482  
 483      /**
 484       * Returns a list of sections used in the course
 485       *
 486       * This is a shortcut to get_fast_modinfo()->get_section_info_all()
 487       * @see get_fast_modinfo()
 488       * @see course_modinfo::get_section_info_all()
 489       *
 490       * @return array of section_info objects
 491       */
 492      public final function get_sections() {
 493          if ($course = $this->get_course()) {
 494              $modinfo = get_fast_modinfo($course);
 495              return $modinfo->get_section_info_all();
 496          }
 497          return array();
 498      }
 499  
 500      /**
 501       * Returns information about section used in course
 502       *
 503       * @param int|stdClass $section either section number (field course_section.section) or row from course_section table
 504       * @param int $strictness
 505       * @return section_info
 506       */
 507      public final function get_section($section, $strictness = IGNORE_MISSING) {
 508          if (is_object($section)) {
 509              $sectionnum = $section->section;
 510          } else {
 511              $sectionnum = $section;
 512          }
 513          $sections = $this->get_sections();
 514          if (array_key_exists($sectionnum, $sections)) {
 515              return $sections[$sectionnum];
 516          }
 517          if ($strictness == MUST_EXIST) {
 518              throw new moodle_exception('sectionnotexist');
 519          }
 520          return null;
 521      }
 522  
 523      /**
 524       * Returns the display name of the given section that the course prefers.
 525       *
 526       * @param int|stdClass $section Section object from database or just field course_sections.section
 527       * @return string Display name that the course format prefers, e.g. "Topic 2"
 528       */
 529      public function get_section_name($section) {
 530          if (is_object($section)) {
 531              $sectionnum = $section->section;
 532          } else {
 533              $sectionnum = $section;
 534          }
 535  
 536          if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) {
 537              return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum;
 538          }
 539  
 540          // Return an empty string if there's no available section name string for the given format.
 541          return '';
 542      }
 543  
 544      /**
 545       * Returns the default section using course_format's implementation of get_section_name.
 546       *
 547       * @param int|stdClass $section Section object from database or just field course_sections section
 548       * @return string The default value for the section name based on the given course format.
 549       */
 550      public function get_default_section_name($section) {
 551          return self::get_section_name($section);
 552      }
 553  
 554      /**
 555       * Returns the name for the highlighted section.
 556       *
 557       * @return string The name for the highlighted section based on the given course format.
 558       */
 559      public function get_section_highlighted_name(): string {
 560          return get_string('highlighted');
 561      }
 562  
 563      /**
 564       * Set if the current format instance will show multiple sections or an individual one.
 565       *
 566       * Some formats has the hability to swith from one section to multiple sections per page,
 567       * this method replaces the old print_multiple_section_page and print_single_section_page.
 568       *
 569       * @param int $singlesection zero for all sections or a section number
 570       */
 571      public function set_section_number(int $singlesection): void {
 572          $this->singlesection = $singlesection;
 573      }
 574  
 575      /**
 576       * Set if the current format instance will show multiple sections or an individual one.
 577       *
 578       * Some formats has the hability to swith from one section to multiple sections per page,
 579       * output components will use this method to know if the current display is a single or
 580       * multiple sections.
 581       *
 582       * @return int zero for all sections or the sectin number
 583       */
 584      public function get_section_number(): int {
 585          return $this->singlesection;
 586      }
 587  
 588      /**
 589       * Return the format section preferences.
 590       *
 591       * @return array of preferences indexed by sectionid
 592       */
 593      public function get_sections_preferences(): array {
 594          global $USER;
 595  
 596          $result = [];
 597  
 598          $course = $this->get_course();
 599          $coursesectionscache = cache::make('core', 'coursesectionspreferences');
 600  
 601          $coursesections = $coursesectionscache->get($course->id);
 602          if ($coursesections) {
 603              return $coursesections;
 604          }
 605  
 606          $sectionpreferences = $this->get_sections_preferences_by_preference();
 607  
 608          foreach ($sectionpreferences as $preference => $sectionids) {
 609              if (!empty($sectionids) && is_array($sectionids)) {
 610                  foreach ($sectionids as $sectionid) {
 611                      if (!isset($result[$sectionid])) {
 612                          $result[$sectionid] = new stdClass();
 613                      }
 614                      $result[$sectionid]->$preference = true;
 615                  }
 616              }
 617          }
 618  
 619          $coursesectionscache->set($course->id, $result);
 620          return $result;
 621      }
 622  
 623      /**
 624       * Return the format section preferences.
 625       *
 626       * @return array of preferences indexed by preference name
 627       */
 628      public function get_sections_preferences_by_preference(): array {
 629          global $USER;
 630          $course = $this->get_course();
 631          try {
 632              $sectionpreferences = (array) json_decode(
 633                  get_user_preferences("coursesectionspreferences_{$course->id}", '', $USER->id)
 634              );
 635              if (empty($sectionpreferences)) {
 636                  $sectionpreferences = [];
 637              }
 638          } catch (\Throwable $e) {
 639              $sectionpreferences = [];
 640          }
 641          return $sectionpreferences;
 642      }
 643  
 644      /**
 645       * Return the format section preferences.
 646       *
 647       * @param string $preferencename preference name
 648       * @param int[] $sectionids affected section ids
 649       *
 650       */
 651      public function set_sections_preference(string $preferencename, array $sectionids) {
 652          global $USER;
 653          $course = $this->get_course();
 654          $sectionpreferences = $this->get_sections_preferences_by_preference();
 655          $sectionpreferences[$preferencename] = $sectionids;
 656          set_user_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences), $USER->id);
 657          // Invalidate section preferences cache.
 658          $coursesectionscache = cache::make('core', 'coursesectionspreferences');
 659          $coursesectionscache->delete($course->id);
 660      }
 661  
 662      /**
 663       * Returns the information about the ajax support in the given source format
 664       *
 665       * The returned object's property (boolean)capable indicates that
 666       * the course format supports Moodle course ajax features.
 667       *
 668       * @return stdClass
 669       */
 670      public function supports_ajax() {
 671          // No support by default.
 672          $ajaxsupport = new stdClass();
 673          $ajaxsupport->capable = false;
 674          return $ajaxsupport;
 675      }
 676  
 677      /**
 678       * Returns true if this course format is compatible with content components.
 679       *
 680       * Using components means the content elements can watch the frontend course state and
 681       * react to the changes. Formats with component compatibility can have more interactions
 682       * without refreshing the page, like having drag and drop from the course index to reorder
 683       * sections and activities.
 684       *
 685       * @return bool if the format is compatible with components.
 686       */
 687      public function supports_components() {
 688          return false;
 689      }
 690  
 691  
 692      /**
 693       * Custom action after section has been moved in AJAX mode
 694       *
 695       * Used in course/rest.php
 696       *
 697       * @return array This will be passed in ajax respose
 698       */
 699      public function ajax_section_move() {
 700          return null;
 701      }
 702  
 703      /**
 704       * The URL to use for the specified course (with section)
 705       *
 706       * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many
 707       * places in core and contributed modules. If course format wants to change the location
 708       * of the view script, it is not enough to change just this function. Do not forget
 709       * to add proper redirection.
 710       *
 711       * @param int|stdClass $section Section object from database or just field course_sections.section
 712       *     if null the course view page is returned
 713       * @param array $options options for view URL. At the moment core uses:
 714       *     'navigation' (bool) if true and section has no separate page, the function returns null
 715       *     'sr' (int) used by multipage formats to specify to which section to return
 716       *     'expanded' (bool) if true the section will be shown expanded, true by default
 717       * @return null|moodle_url
 718       */
 719      public function get_view_url($section, $options = array()) {
 720          global $CFG;
 721          $course = $this->get_course();
 722          $url = new moodle_url('/course/view.php', array('id' => $course->id));
 723  
 724          if (array_key_exists('sr', $options)) {
 725              $sectionno = $options['sr'];
 726          } else if (is_object($section)) {
 727              $sectionno = $section->section;
 728          } else {
 729              $sectionno = $section;
 730          }
 731          if (empty($CFG->linkcoursesections) && !empty($options['navigation']) && $sectionno !== null) {
 732              // By default assume that sections are never displayed on separate pages.
 733              return null;
 734          }
 735          if ($this->uses_sections() && $sectionno !== null) {
 736              // The url includes the parameter to expand the section by default.
 737              if (!array_key_exists('expanded', $options)) {
 738                  $options['expanded'] = true;
 739              }
 740              if ($options['expanded']) {
 741                  // This parameter is being set by default.
 742                  $url->param('expandsection', $sectionno);
 743              }
 744              $url->set_anchor('section-'.$sectionno);
 745          }
 746          return $url;
 747      }
 748  
 749      /**
 750       * Return the old non-ajax activity action url.
 751       *
 752       * BrowserKit behats tests cannot trigger javascript events,
 753       * so we must translate to an old non-ajax url while non-ajax
 754       * course editing is still supported.
 755       *
 756       * @param string $action action name the reactive action
 757       * @param cm_info $cm course module
 758       * @return moodle_url
 759       */
 760      public function get_non_ajax_cm_action_url(string $action, cm_info $cm): moodle_url {
 761          $nonajaxactions = [
 762              'cmDelete' => 'delete',
 763              'cmDuplicate' => 'duplicate',
 764              'cmHide' => 'hide',
 765              'cmShow' => 'show',
 766              'cmStealth' => 'stealth',
 767          ];
 768          if (!isset($nonajaxactions[$action])) {
 769              throw new coding_exception('Unknown activity action: ' . $action);
 770          }
 771          $nonajaxaction = $nonajaxactions[$action];
 772          $nonajaxurl = new moodle_url(
 773              '/course/mod.php',
 774              ['sesskey' => sesskey(), $nonajaxaction => $cm->id]
 775          );
 776          $nonajaxurl->param('sr', $this->get_section_number());
 777          return $nonajaxurl;
 778      }
 779  
 780      /**
 781       * Loads all of the course sections into the navigation
 782       *
 783       * This method is called from global_navigation::load_course_sections()
 784       *
 785       * By default the method global_navigation::load_generic_course_sections() is called
 786       *
 787       * When overwriting please note that navigationlib relies on using the correct values for
 788       * arguments $type and $key in navigation_node::add()
 789       *
 790       * Example of code creating a section node:
 791       * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id);
 792       * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH;
 793       *
 794       * Example of code creating an activity node:
 795       * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon);
 796       * if (global_navigation::module_extends_navigation($activity->modname)) {
 797       *     $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
 798       * } else {
 799       *     $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
 800       * }
 801       *
 802       * Also note that if $navigation->includesectionnum is not null, the section with this relative
 803       * number needs is expected to be loaded
 804       *
 805       * @param global_navigation $navigation
 806       * @param navigation_node $node The course node within the navigation
 807       */
 808      public function extend_course_navigation($navigation, navigation_node $node) {
 809          if ($course = $this->get_course()) {
 810              $navigation->load_generic_course_sections($course, $node);
 811          }
 812          return array();
 813      }
 814  
 815      /**
 816       * Returns the list of blocks to be automatically added for the newly created course
 817       *
 818       * @see blocks_add_default_course_blocks()
 819       *
 820       * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
 821       *     each of values is an array of block names (for left and right side columns)
 822       */
 823      public function get_default_blocks() {
 824          global $CFG;
 825          if (isset($CFG->defaultblocks)) {
 826              return blocks_parse_default_blocks_list($CFG->defaultblocks);
 827          }
 828          $blocknames = array(
 829              BLOCK_POS_LEFT => array(),
 830              BLOCK_POS_RIGHT => array()
 831          );
 832          return $blocknames;
 833      }
 834  
 835      /**
 836       * Return custom strings for the course editor.
 837       *
 838       * This method is mainly used to translate the "section" related strings into
 839       * the specific format plugins name such as "Topics" or "Weeks".
 840       *
 841       * @return stdClass[] an array of objects with string "component" and "key"
 842       */
 843      public function get_editor_custom_strings(): array {
 844          $result = [];
 845          $stringmanager = get_string_manager();
 846          $component = 'format_' . $this->format;
 847          $formatoverridbles = [
 848              'sectionavailability_title',
 849              'sectiondelete_title',
 850              'sectiondelete_info',
 851              'sectionsdelete_title',
 852              'sectionsdelete_info',
 853              'sectionmove_title',
 854              'sectionmove_info',
 855              'sectionsavailability_title',
 856              'sectionsmove_title',
 857              'sectionsmove_info',
 858              'selectsection'
 859          ];
 860          foreach ($formatoverridbles as $key) {
 861              if ($stringmanager->string_exists($key, $component)) {
 862                  $result[] = (object)['component' => $component, 'key' => $key];
 863              }
 864          }
 865          return $result;
 866      }
 867  
 868      /**
 869       * Get the proper format plugin string if it exists.
 870       *
 871       * If the format_PLUGINNAME does not provide a valid string,
 872       * core_courseformat will be user as the component.
 873       *
 874       * @param string $key the string key
 875       * @param string|object|array $data extra data that can be used within translation strings
 876       * @param string|null $lang moodle translation language, null means use current
 877       * @return string the get_string result
 878       */
 879      public function get_format_string(string $key, $data = null, $lang = null): string {
 880          $component = 'format_' . $this->get_format();
 881          if (!get_string_manager()->string_exists($key, $component)) {
 882              $component = 'core_courseformat';
 883          }
 884          return get_string($key, $component, $data, $lang);
 885      }
 886  
 887      /**
 888       * Returns the localised name of this course format plugin
 889       *
 890       * @return lang_string
 891       */
 892      public final function get_format_name() {
 893          return new lang_string('pluginname', 'format_'.$this->get_format());
 894      }
 895  
 896      /**
 897       * Definitions of the additional options that this course format uses for course
 898       *
 899       * This function may be called often, it should be as fast as possible.
 900       * Avoid using get_string() method, use "new lang_string()" instead
 901       * It is not recommended to use dynamic or course-dependant expressions here
 902       * This function may be also called when course does not exist yet.
 903       *
 904       * Option names must be different from fields in the {course} talbe or any form elements on
 905       * course edit form, it may even make sence to use special prefix for them.
 906       *
 907       * Each option must have the option name as a key and the array of properties as a value:
 908       * 'default' - default value for this option (assumed null if not specified)
 909       * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.)
 910       *
 911       * Additional properties used by default implementation of
 912       * course_format::create_edit_form_elements() (calls this method with $foreditform = true)
 913       * 'label' - localised human-readable label for the edit form
 914       * 'element_type' - type of the form element, default 'text'
 915       * 'element_attributes' - additional attributes for the form element, these are 4th and further
 916       *    arguments in the moodleform::addElement() method
 917       * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with
 918       *    the name 'myoption_help' must exist in the language file
 919       * 'help_component' - language component to look for help string, by default this the component
 920       *    for this course format
 921       *
 922       * This is an interface for creating simple form elements. If format plugin wants to use other
 923       * methods such as disableIf, it can be done by overriding create_edit_form_elements().
 924       *
 925       * Course format options can be accessed as:
 926       * $this->get_course()->OPTIONNAME (inside the format class)
 927       * course_get_format($course)->get_course()->OPTIONNAME (outside of format class)
 928       *
 929       * All course options are returned by calling:
 930       * $this->get_format_options();
 931       *
 932       * @param bool $foreditform
 933       * @return array of options
 934       */
 935      public function course_format_options($foreditform = false) {
 936          return array();
 937      }
 938  
 939      /**
 940       * Definitions of the additional options that this course format uses for section
 941       *
 942       * See course_format::course_format_options() for return array definition.
 943       *
 944       * Additionally section format options may have property 'cache' set to true
 945       * if this option needs to be cached in get_fast_modinfo(). The 'cache' property
 946       * is recommended to be set only for fields used in course_format::get_section_name(),
 947       * course_format::extend_course_navigation() and course_format::get_view_url()
 948       *
 949       * For better performance cached options are recommended to have 'cachedefault' property
 950       * Unlike 'default', 'cachedefault' should be static and not access get_config().
 951       *
 952       * Regardless of value of 'cache' all options are accessed in the code as
 953       * $sectioninfo->OPTIONNAME
 954       * where $sectioninfo is instance of section_info, returned by
 955       * get_fast_modinfo($course)->get_section_info($sectionnum)
 956       * or get_fast_modinfo($course)->get_section_info_all()
 957       *
 958       * All format options for particular section are returned by calling:
 959       * $this->get_format_options($section);
 960       *
 961       * @param bool $foreditform
 962       * @return array
 963       */
 964      public function section_format_options($foreditform = false) {
 965          return array();
 966      }
 967  
 968      /**
 969       * Returns the format options stored for this course or course section
 970       *
 971       * When overriding please note that this function is called from rebuild_course_cache()
 972       * and section_info object, therefore using of get_fast_modinfo() and/or any function that
 973       * accesses it may lead to recursion.
 974       *
 975       * @param null|int|stdClass|section_info $section if null the course format options will be returned
 976       *     otherwise options for specified section will be returned. This can be either
 977       *     section object or relative section number (field course_sections.section)
 978       * @return array
 979       */
 980      public function get_format_options($section = null) {
 981          global $DB;
 982          if ($section === null) {
 983              $options = $this->course_format_options();
 984          } else {
 985              $options = $this->section_format_options();
 986          }
 987          if (empty($options)) {
 988              // There are no option for course/sections anyway, no need to go further.
 989              return array();
 990          }
 991          if ($section === null) {
 992              // Course format options will be returned.
 993              $sectionid = 0;
 994          } else if ($this->courseid && isset($section->id)) {
 995              // Course section format options will be returned.
 996              $sectionid = $section->id;
 997          } else if ($this->courseid && is_int($section) &&
 998                  ($sectionobj = $DB->get_record('course_sections',
 999                          array('section' => $section, 'course' => $this->courseid), 'id'))) {
1000              // Course section format options will be returned.
1001              $sectionid = $sectionobj->id;
1002          } else {
1003              // Non-existing (yet) section was passed as an argument
1004              // default format options for course section will be returned.
1005              $sectionid = -1;
1006          }
1007          if (!array_key_exists($sectionid, $this->formatoptions)) {
1008              $this->formatoptions[$sectionid] = array();
1009              // First fill with default values.
1010              foreach ($options as $optionname => $optionparams) {
1011                  $this->formatoptions[$sectionid][$optionname] = null;
1012                  if (array_key_exists('default', $optionparams)) {
1013                      $this->formatoptions[$sectionid][$optionname] = $optionparams['default'];
1014                  }
1015              }
1016              if ($this->courseid && $sectionid !== -1) {
1017                  // Overwrite the default options values with those stored in course_format_options table
1018                  // nothing can be stored if we are interested in generic course ($this->courseid == 0)
1019                  // or generic section ($sectionid === 0).
1020                  $records = $DB->get_records('course_format_options',
1021                          array('courseid' => $this->courseid,
1022                                'format' => $this->format,
1023                                'sectionid' => $sectionid
1024                              ), '', 'id,name,value');
1025                  $indexedrecords = [];
1026                  foreach ($records as $record) {
1027                      $indexedrecords[$record->name] = $record->value;
1028                  }
1029                  foreach ($options as $optionname => $option) {
1030                      contract_value($this->formatoptions[$sectionid], $indexedrecords, $option, $optionname);
1031                  }
1032              }
1033          }
1034          return $this->formatoptions[$sectionid];
1035      }
1036  
1037      /**
1038       * Adds format options elements to the course/section edit form
1039       *
1040       * This function is called from course_edit_form::definition_after_data()
1041       *
1042       * @param MoodleQuickForm $mform form the elements are added to
1043       * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
1044       * @return array array of references to the added form elements
1045       */
1046      public function create_edit_form_elements(&$mform, $forsection = false) {
1047          $elements = array();
1048          if ($forsection) {
1049              $options = $this->section_format_options(true);
1050          } else {
1051              $options = $this->course_format_options(true);
1052          }
1053          foreach ($options as $optionname => $option) {
1054              if (!isset($option['element_type'])) {
1055                  $option['element_type'] = 'text';
1056              }
1057              $args = array($option['element_type'], $optionname, $option['label']);
1058              if (!empty($option['element_attributes'])) {
1059                  $args = array_merge($args, $option['element_attributes']);
1060              }
1061              $elements[] = call_user_func_array(array($mform, 'addElement'), $args);
1062              if (isset($option['help'])) {
1063                  $helpcomponent = 'format_'. $this->get_format();
1064                  if (isset($option['help_component'])) {
1065                      $helpcomponent = $option['help_component'];
1066                  }
1067                  $mform->addHelpButton($optionname, $option['help'], $helpcomponent);
1068              }
1069              if (isset($option['type'])) {
1070                  $mform->setType($optionname, $option['type']);
1071              }
1072              if (isset($option['default']) && !array_key_exists($optionname, $mform->_defaultValues)) {
1073                  // Set defaults for the elements in the form.
1074                  // Since we call this method after set_data() make sure that we don't override what was already set.
1075                  $mform->setDefault($optionname, $option['default']);
1076              }
1077          }
1078  
1079          if (!$forsection && empty($this->courseid)) {
1080              // Check if course end date form field should be enabled by default.
1081              // If a default date is provided to the form element, it is magically enabled by default in the
1082              // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default.
1083              if (get_config('moodlecourse', 'courseenddateenabled')) {
1084                  // At this stage (this is called from definition_after_data) course data is already set as default.
1085                  // We can not overwrite what is in the database.
1086                  $mform->setDefault('enddate', $this->get_default_course_enddate($mform));
1087              }
1088          }
1089  
1090          return $elements;
1091      }
1092  
1093      /**
1094       * Override if you need to perform some extra validation of the format options
1095       *
1096       * @param array $data array of ("fieldname"=>value) of submitted data
1097       * @param array $files array of uploaded files "element_name"=>tmp_file_path
1098       * @param array $errors errors already discovered in edit form validation
1099       * @return array of "element_name"=>"error_description" if there are errors,
1100       *         or an empty array if everything is OK.
1101       *         Do not repeat errors from $errors param here
1102       */
1103      public function edit_form_validation($data, $files, $errors) {
1104          return array();
1105      }
1106  
1107      /**
1108       * Prepares values of course or section format options before storing them in DB
1109       *
1110       * If an option has invalid value it is not returned
1111       *
1112       * @param array $rawdata associative array of the proposed course/section format options
1113       * @param int|null $sectionid null if it is course format option
1114       * @return array array of options that have valid values
1115       */
1116      protected function validate_format_options(array $rawdata, int $sectionid = null) : array {
1117          if (!$sectionid) {
1118              $allformatoptions = $this->course_format_options(true);
1119          } else {
1120              $allformatoptions = $this->section_format_options(true);
1121          }
1122          $data = array_intersect_key($rawdata, $allformatoptions);
1123          foreach ($data as $key => $value) {
1124              $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
1125              expand_value($data, $data, $option, $key);
1126              if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
1127                  // Value invalid for select element, skip.
1128                  unset($data[$key]);
1129              }
1130          }
1131          return $data;
1132      }
1133  
1134      /**
1135       * Validates format options for the course
1136       *
1137       * @param array $data data to insert/update
1138       * @return array array of options that have valid values
1139       */
1140      public function validate_course_format_options(array $data) : array {
1141          return $this->validate_format_options($data);
1142      }
1143  
1144      /**
1145       * Updates format options for a course or section
1146       *
1147       * If $data does not contain property with the option name, the option will not be updated
1148       *
1149       * @param stdClass|array $data return value from moodleform::get_data() or array with data
1150       * @param null|int $sectionid null if these are options for course or section id (course_sections.id)
1151       *     if these are options for section
1152       * @return bool whether there were any changes to the options values
1153       */
1154      protected function update_format_options($data, $sectionid = null) {
1155          global $DB;
1156          $data = $this->validate_format_options((array)$data, $sectionid);
1157          if (!$sectionid) {
1158              $allformatoptions = $this->course_format_options();
1159              $sectionid = 0;
1160          } else {
1161              $allformatoptions = $this->section_format_options();
1162          }
1163          if (empty($allformatoptions)) {
1164              // Nothing to update anyway.
1165              return false;
1166          }
1167          $defaultoptions = array();
1168          $cached = array();
1169          foreach ($allformatoptions as $key => $option) {
1170              $defaultoptions[$key] = null;
1171              if (array_key_exists('default', $option)) {
1172                  $defaultoptions[$key] = $option['default'];
1173              }
1174              expand_value($defaultoptions, $defaultoptions, $option, $key);
1175              $cached[$key] = ($sectionid === 0 || !empty($option['cache']));
1176          }
1177          $records = $DB->get_records('course_format_options',
1178                  array('courseid' => $this->courseid,
1179                        'format' => $this->format,
1180                        'sectionid' => $sectionid
1181                      ), '', 'name,id,value');
1182          $changed = $needrebuild = false;
1183          foreach ($defaultoptions as $key => $value) {
1184              if (isset($records[$key])) {
1185                  if (array_key_exists($key, $data) && $records[$key]->value != $data[$key]) {
1186                      $DB->set_field('course_format_options', 'value',
1187                              $data[$key], array('id' => $records[$key]->id));
1188                      $changed = true;
1189                      $needrebuild = $needrebuild || $cached[$key];
1190                  }
1191              } else {
1192                  if (array_key_exists($key, $data) && $data[$key] !== $value) {
1193                      $newvalue = $data[$key];
1194                      $changed = true;
1195                      $needrebuild = $needrebuild || $cached[$key];
1196                  } else {
1197                      $newvalue = $value;
1198                      // We still insert entry in DB but there are no changes from user point of
1199                      // view and no need to call rebuild_course_cache().
1200                  }
1201                  $DB->insert_record('course_format_options', array(
1202                      'courseid' => $this->courseid,
1203                      'format' => $this->format,
1204                      'sectionid' => $sectionid,
1205                      'name' => $key,
1206                      'value' => $newvalue
1207                  ));
1208              }
1209          }
1210          if ($needrebuild) {
1211              if ($sectionid) {
1212                  // Invalidate the section cache by given section id.
1213                  course_modinfo::purge_course_section_cache_by_id($this->courseid, $sectionid);
1214                  // Partial rebuild sections that have been invalidated.
1215                  rebuild_course_cache($this->courseid, true, true);
1216              } else {
1217                  // Full rebuild if sectionid is null.
1218                  rebuild_course_cache($this->courseid);
1219              }
1220          }
1221          if ($changed) {
1222              // Reset internal caches.
1223              if (!$sectionid) {
1224                  $this->course = false;
1225              }
1226              unset($this->formatoptions[$sectionid]);
1227          }
1228          return $changed;
1229      }
1230  
1231      /**
1232       * Updates format options for a course
1233       *
1234       * If $data does not contain property with the option name, the option will not be updated
1235       *
1236       * @param stdClass|array $data return value from moodleform::get_data() or array with data
1237       * @param stdClass $oldcourse if this function is called from update_course()
1238       *     this object contains information about the course before update
1239       * @return bool whether there were any changes to the options values
1240       */
1241      public function update_course_format_options($data, $oldcourse = null) {
1242          return $this->update_format_options($data);
1243      }
1244  
1245      /**
1246       * Updates format options for a section
1247       *
1248       * Section id is expected in $data->id (or $data['id'])
1249       * If $data does not contain property with the option name, the option will not be updated
1250       *
1251       * @param stdClass|array $data return value from moodleform::get_data() or array with data
1252       * @return bool whether there were any changes to the options values
1253       */
1254      public function update_section_format_options($data) {
1255          $data = (array)$data;
1256          return $this->update_format_options($data, $data['id']);
1257      }
1258  
1259      /**
1260       * Return an instance of moodleform to edit a specified section
1261       *
1262       * Default implementation returns instance of editsection_form that automatically adds
1263       * additional fields defined in course_format::section_format_options()
1264       *
1265       * Format plugins may extend editsection_form if they want to have custom edit section form.
1266       *
1267       * @param mixed $action the action attribute for the form. If empty defaults to auto detect the
1268       *              current url. If a moodle_url object then outputs params as hidden variables.
1269       * @param array $customdata the array with custom data to be passed to the form
1270       *     /course/editsection.php passes section_info object in 'cs' field
1271       *     for filling availability fields
1272       * @return moodleform
1273       */
1274      public function editsection_form($action, $customdata = array()) {
1275          global $CFG;
1276          require_once($CFG->dirroot. '/course/editsection_form.php');
1277          if (!array_key_exists('course', $customdata)) {
1278              $customdata['course'] = $this->get_course();
1279          }
1280          return new editsection_form($action, $customdata);
1281      }
1282  
1283      /**
1284       * Allows course format to execute code on moodle_page::set_course()
1285       *
1286       * @param moodle_page $page instance of page calling set_course
1287       */
1288      public function page_set_course(moodle_page $page) {
1289      }
1290  
1291      /**
1292       * Allows course format to execute code on moodle_page::set_cm()
1293       *
1294       * Current module can be accessed as $page->cm (returns instance of cm_info)
1295       *
1296       * @param moodle_page $page instance of page calling set_cm
1297       */
1298      public function page_set_cm(moodle_page $page) {
1299      }
1300  
1301      /**
1302       * Course-specific information to be output on any course page (usually above navigation bar)
1303       *
1304       * Example of usage:
1305       * define
1306       * class format_FORMATNAME_XXX implements renderable {}
1307       *
1308       * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function:
1309       * class format_FORMATNAME_renderer extends plugin_renderer_base {
1310       *     protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) {
1311       *         return html_writer::tag('div', 'This is my header/footer');
1312       *     }
1313       * }
1314       *
1315       * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from
1316       * plugin renderer will be called
1317       *
1318       * @return null|renderable null for no output or object with data for plugin renderer
1319       */
1320      public function course_header() {
1321          return null;
1322      }
1323  
1324      /**
1325       * Course-specific information to be output on any course page (usually in the beginning of
1326       * standard footer)
1327       *
1328       * See course_format::course_header() for usage
1329       *
1330       * @return null|renderable null for no output or object with data for plugin renderer
1331       */
1332      public function course_footer() {
1333          return null;
1334      }
1335  
1336      /**
1337       * Course-specific information to be output immediately above content on any course page
1338       *
1339       * See course_format::course_header() for usage
1340       *
1341       * @return null|renderable null for no output or object with data for plugin renderer
1342       */
1343      public function course_content_header() {
1344          return null;
1345      }
1346  
1347      /**
1348       * Course-specific information to be output immediately below content on any course page
1349       *
1350       * See course_format::course_header() for usage
1351       *
1352       * @return null|renderable null for no output or object with data for plugin renderer
1353       */
1354      public function course_content_footer() {
1355          return null;
1356      }
1357  
1358      /**
1359       * Returns instance of page renderer used by this plugin
1360       *
1361       * @param moodle_page $page
1362       * @return renderer_base
1363       */
1364      public function get_renderer(moodle_page $page) {
1365          try {
1366              $renderer = $page->get_renderer('format_'. $this->get_format());
1367          } catch (moodle_exception $e) {
1368              $formatname = $this->get_format();
1369              $expectedrenderername = 'format_'. $this->get_format() . '\output\renderer';
1370              debugging(
1371                  "The '{$formatname}' course format does not define the {$expectedrenderername} renderer class. This is required since Moodle 4.0.",
1372                   DEBUG_DEVELOPER
1373              );
1374              $renderer = new legacy_renderer($page, null);
1375          }
1376  
1377          return $renderer;
1378      }
1379  
1380      /**
1381       * Returns instance of output component used by this plugin
1382       *
1383       * @throws coding_exception if the format class does not extends the original core one.
1384       * @param string $outputname the element to render (section, activity...)
1385       * @return string the output component classname
1386       */
1387      public function get_output_classname(string $outputname): string {
1388          // The core output class.
1389          $baseclass = "core_courseformat\\output\\local\\$outputname";
1390  
1391          // Look in this format and any parent formats before we get to the base one.
1392          $classes = array_merge([get_class($this)], class_parents($this));
1393          foreach ($classes as $component) {
1394              if ($component === self::class) {
1395                  break;
1396              }
1397  
1398              // Because course formats are in the root namespace, there is no need to process the
1399              // class name - it is already a Frankenstyle component name beginning 'format_'.
1400  
1401              // Check if there is a specific class in this format.
1402              $outputclass = "$component\\output\\courseformat\\$outputname";
1403              if (class_exists($outputclass)) {
1404                  // Check that the outputclass is a subclass of the base class.
1405                  if (!is_subclass_of($outputclass, $baseclass)) {
1406                      throw new coding_exception("The \"$outputclass\" must extend \"$baseclass\"");
1407                  }
1408                  return $outputclass;
1409              }
1410          }
1411  
1412          return $baseclass;
1413      }
1414  
1415      /**
1416       * Returns true if the specified section is current
1417       *
1418       * By default we analyze $course->marker
1419       *
1420       * @param int|stdClass|section_info $section
1421       * @return bool
1422       */
1423      public function is_section_current($section) {
1424          if (is_object($section)) {
1425              $sectionnum = $section->section;
1426          } else {
1427              $sectionnum = $section;
1428          }
1429          return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum);
1430      }
1431  
1432      /**
1433       * Returns if an specific section is visible to the current user.
1434       *
1435       * Formats can overrride this method to implement any special section logic.
1436       *
1437       * @param section_info $section the section modinfo
1438       * @return bool;
1439       */
1440      public function is_section_visible(section_info $section): bool {
1441          // Previous to Moodle 4.0 thas logic was hardcoded. To prevent errors in the contrib plugins
1442          // the default logic is the same required for topics and weeks format and still uses
1443          // a "hiddensections" format setting.
1444          $course = $this->get_course();
1445          $hidesections = $course->hiddensections ?? true;
1446          // Show the section if the user is permitted to access it, OR if it's not available
1447          // but there is some available info text which explains the reason & should display,
1448          // OR it is hidden but the course has a setting to display hidden sections as unavailable.
1449          return $section->uservisible ||
1450              ($section->visible && !$section->available && !empty($section->availableinfo)) ||
1451              (!$section->visible && !$hidesections);
1452      }
1453  
1454      /**
1455       * return true if the course editor must be displayed.
1456       *
1457       * @param array|null $capabilities array of capabilities a user needs to have to see edit controls in general.
1458       *  If null or not specified, the user needs to have 'moodle/course:manageactivities'.
1459       * @return bool true if edit controls must be displayed
1460       */
1461      public function show_editor(?array $capabilities = ['moodle/course:manageactivities']): bool {
1462          global $PAGE;
1463          $course = $this->get_course();
1464          $coursecontext = context_course::instance($course->id);
1465          if ($capabilities === null) {
1466              $capabilities = ['moodle/course:manageactivities'];
1467          }
1468          return $PAGE->user_is_editing() && has_all_capabilities($capabilities, $coursecontext);
1469      }
1470  
1471      /**
1472       * Check if the group mode can be displayed.
1473       * @param cm_info $cm the activity module
1474       * @return bool
1475       */
1476      public function show_groupmode(cm_info $cm): bool {
1477          if (!plugin_supports('mod', $cm->modname, FEATURE_GROUPS, false)) {
1478              return false;
1479          }
1480          if (!has_capability('moodle/course:manageactivities', $cm->context)) {
1481              return false;
1482          }
1483          return true;
1484      }
1485  
1486      /**
1487       * Allows to specify for modinfo that section is not available even when it is visible and conditionally available.
1488       *
1489       * Note: affected user can be retrieved as: $section->modinfo->userid
1490       *
1491       * Course format plugins can override the method to change the properties $available and $availableinfo that were
1492       * calculated by conditional availability.
1493       * To make section unavailable set:
1494       *     $available = false;
1495       * To make unavailable section completely hidden set:
1496       *     $availableinfo = '';
1497       * To make unavailable section visible with availability message set:
1498       *     $availableinfo = get_string('sectionhidden', 'format_xxx');
1499       *
1500       * @param section_info $section
1501       * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability.
1502       *     Can be changed by the method but 'false' can not be overridden by 'true'.
1503       * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability.
1504       *     Can be changed by the method
1505       */
1506      public function section_get_available_hook(section_info $section, &$available, &$availableinfo) {
1507      }
1508  
1509      /**
1510       * Whether this format allows to delete sections
1511       *
1512       * If format supports deleting sections it is also recommended to define language string
1513       * 'deletesection' inside the format.
1514       *
1515       * Do not call this function directly, instead use course_can_delete_section()
1516       *
1517       * @param int|stdClass|section_info $section
1518       * @return bool
1519       */
1520      public function can_delete_section($section) {
1521          return false;
1522      }
1523  
1524      /**
1525       * Deletes a section
1526       *
1527       * Do not call this function directly, instead call course_delete_section()
1528       *
1529       * @param int|stdClass|section_info $section
1530       * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
1531       * @return bool whether section was deleted
1532       */
1533      public function delete_section($section, $forcedeleteifnotempty = false) {
1534          global $DB;
1535          if (!$this->uses_sections()) {
1536              // Not possible to delete section if sections are not used.
1537              return false;
1538          }
1539          if (!is_object($section)) {
1540              $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
1541                  'id,section,sequence,summary');
1542          }
1543          if (!$section || !$section->section) {
1544              // Not possible to delete 0-section.
1545              return false;
1546          }
1547  
1548          if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
1549              return false;
1550          }
1551  
1552          $course = $this->get_course();
1553  
1554          // Remove the marker if it points to this section.
1555          if ($section->section == $course->marker) {
1556              course_set_marker($course->id, 0);
1557          }
1558  
1559          $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections}
1560                              WHERE course = ?', array($course->id));
1561  
1562          // Find out if we need to descrease the 'numsections' property later.
1563          $courseformathasnumsections = array_key_exists('numsections',
1564              $this->get_format_options());
1565          $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections);
1566  
1567          // Move the section to the end.
1568          move_section_to($course, $section->section, $lastsection, true);
1569  
1570          // Delete all modules from the section.
1571          foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) {
1572              course_delete_module($cmid);
1573          }
1574  
1575          // Delete section and it's format options.
1576          $DB->delete_records('course_format_options', array('sectionid' => $section->id));
1577          $DB->delete_records('course_sections', array('id' => $section->id));
1578          // Invalidate the section cache by given section id.
1579          course_modinfo::purge_course_section_cache_by_id($course->id, $section->id);
1580          // Partial rebuild section cache that has been purged.
1581          rebuild_course_cache($course->id, true, true);
1582  
1583          // Delete section summary files.
1584          $context = \context_course::instance($course->id);
1585          $fs = get_file_storage();
1586          $fs->delete_area_files($context->id, 'course', 'section', $section->id);
1587  
1588          // Descrease 'numsections' if needed.
1589          if ($decreasenumsections) {
1590              $this->update_course_format_options(array('numsections' => $course->numsections - 1));
1591          }
1592  
1593          return true;
1594      }
1595  
1596      /**
1597       * Wrapper for course_delete_module method.
1598       *
1599       * Format plugins can override this method to provide their own implementation of course_delete_module.
1600       *
1601       * @param cm_info $cm the course module information
1602       * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
1603       * @throws moodle_exception
1604       */
1605      public function delete_module(cm_info $cm, bool $async = false) {
1606          course_delete_module($cm->id, $async);
1607      }
1608  
1609      /**
1610       * Moves a section just after the target section.
1611       *
1612       * @param section_info $section the section to move
1613       * @param section_info $destination the section that should be below the moved section
1614       * @return boolean if the section can be moved or not
1615       */
1616      public function move_section_after(section_info $section, section_info $destination): bool {
1617          if ($section->section == $destination->section || $section->section == $destination->section + 1) {
1618              return true;
1619          }
1620          // The move_section_to moves relative to the section to move. However, this
1621          // method will move the target section always after the destination.
1622          if ($section->section > $destination->section) {
1623              $newsectionnumber = $destination->section + 1;
1624          } else {
1625              $newsectionnumber = $destination->section;
1626          }
1627          return move_section_to(
1628              $this->get_course(),
1629              $section->section,
1630              $newsectionnumber
1631          );
1632      }
1633  
1634      /**
1635       * Prepares the templateable object to display section name
1636       *
1637       * @param \section_info|\stdClass $section
1638       * @param bool $linkifneeded
1639       * @param bool $editable
1640       * @param null|lang_string|string $edithint
1641       * @param null|lang_string|string $editlabel
1642       * @return \core\output\inplace_editable
1643       */
1644      public function inplace_editable_render_section_name($section, $linkifneeded = true,
1645                                                           $editable = null, $edithint = null, $editlabel = null) {
1646          global $USER, $CFG;
1647          require_once($CFG->dirroot.'/course/lib.php');
1648  
1649          if ($editable === null) {
1650              $editable = !empty($USER->editing) && has_capability('moodle/course:update',
1651                      context_course::instance($section->course));
1652          }
1653  
1654          $displayvalue = $title = get_section_name($section->course, $section);
1655          if ($linkifneeded) {
1656              // Display link under the section name if the course format setting is to display one section per page.
1657              $url = course_get_url($section->course, $section->section, array('navigation' => true));
1658              if ($url) {
1659                  $displayvalue = html_writer::link($url, $title);
1660              }
1661              $itemtype = 'sectionname';
1662          } else {
1663              // If $linkifneeded==false, we never display the link (this is used when rendering the section header).
1664              // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered -
1665              // there is no other way callback can know where we display the section name.
1666              $itemtype = 'sectionnamenl';
1667          }
1668          if (empty($edithint)) {
1669              $edithint = new lang_string('editsectionname');
1670          }
1671          if (empty($editlabel)) {
1672              $editlabel = new lang_string('newsectionname', '', $title);
1673          }
1674  
1675          return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable,
1676              $displayvalue, $section->name, $edithint, $editlabel);
1677      }
1678  
1679      /**
1680       * Updates the value in the database and modifies this object respectively.
1681       *
1682       * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient
1683       * or value is not legit.
1684       *
1685       * @param stdClass $section
1686       * @param string $itemtype
1687       * @param mixed $newvalue
1688       * @return \core\output\inplace_editable
1689       */
1690      public function inplace_editable_update_section_name($section, $itemtype, $newvalue) {
1691          if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
1692              $context = context_course::instance($section->course);
1693              external_api::validate_context($context);
1694              require_capability('moodle/course:update', $context);
1695  
1696              $newtitle = clean_param($newvalue, PARAM_TEXT);
1697              if (strval($section->name) !== strval($newtitle)) {
1698                  course_update_section($section->course, $section, array('name' => $newtitle));
1699              }
1700              return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true);
1701          }
1702      }
1703  
1704  
1705      /**
1706       * Returns the default end date value based on the start date.
1707       *
1708       * This is the default implementation for course formats, it is based on
1709       * moodlecourse/courseduration setting. Course formats like format_weeks for
1710       * example can overwrite this method and return a value based on their internal options.
1711       *
1712       * @param moodleform $mform
1713       * @param array $fieldnames The form - field names mapping.
1714       * @return int
1715       */
1716      public function get_default_course_enddate($mform, $fieldnames = array()) {
1717  
1718          if (empty($fieldnames)) {
1719              $fieldnames = array('startdate' => 'startdate');
1720          }
1721  
1722          $startdate = $this->get_form_start_date($mform, $fieldnames);
1723          $courseduration = intval(get_config('moodlecourse', 'courseduration'));
1724          if (!$courseduration) {
1725              // Default, it should be already set during upgrade though.
1726              $courseduration = YEARSECS;
1727          }
1728  
1729          return $startdate + $courseduration;
1730      }
1731  
1732      /**
1733       * Indicates whether the course format supports the creation of the Announcements forum.
1734       *
1735       * For course format plugin developers, please override this to return true if you want the Announcements forum
1736       * to be created upon course creation.
1737       *
1738       * @return bool
1739       */
1740      public function supports_news() {
1741          // For backwards compatibility, check if default blocks include the news_items block.
1742          $defaultblocks = $this->get_default_blocks();
1743          foreach ($defaultblocks as $blocks) {
1744              if (in_array('news_items', $blocks)) {
1745                  return true;
1746              }
1747          }
1748          // Return false by default.
1749          return false;
1750      }
1751  
1752      /**
1753       * Get the start date value from the course settings page form.
1754       *
1755       * @param moodleform $mform
1756       * @param array $fieldnames The form - field names mapping.
1757       * @return int
1758       */
1759      protected function get_form_start_date($mform, $fieldnames) {
1760          $startdate = $mform->getElementValue($fieldnames['startdate']);
1761          return $mform->getElement($fieldnames['startdate'])->exportValue($startdate);
1762      }
1763  
1764      /**
1765       * Returns whether this course format allows the activity to
1766       * have "triple visibility state" - visible always, hidden on course page but available, hidden.
1767       *
1768       * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
1769       * @param stdClass|section_info $section section where this module is located or will be added to
1770       * @return bool
1771       */
1772      public function allow_stealth_module_visibility($cm, $section) {
1773          return false;
1774      }
1775  
1776      /**
1777       * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide)
1778       *
1779       * Access to the course is already validated in the WS but the callback has to make sure
1780       * that particular action is allowed by checking capabilities
1781       *
1782       * Course formats should register
1783       *
1784       * @param stdClass|section_info $section
1785       * @param string $action
1786       * @param int $sr the section return
1787       * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable)
1788       */
1789      public function section_action($section, $action, $sr) {
1790          global $PAGE;
1791          if (!$this->uses_sections() || !$section->section) {
1792              // No section actions are allowed if course format does not support sections.
1793              // No actions are allowed on the 0-section by default (overwrite in course format if needed).
1794              throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
1795          }
1796  
1797          $course = $this->get_course();
1798          $coursecontext = context_course::instance($course->id);
1799          $modinfo = $this->get_modinfo();
1800          $renderer = $this->get_renderer($PAGE);
1801  
1802          if (!($section instanceof section_info)) {
1803              $section = $modinfo->get_section_info($section->section);
1804          }
1805  
1806          if ($sr) {
1807              $this->set_section_number($sr);
1808          }
1809  
1810          switch($action) {
1811              case 'hide':
1812              case 'show':
1813                  require_capability('moodle/course:sectionvisibility', $coursecontext);
1814                  $visible = ($action === 'hide') ? 0 : 1;
1815                  course_update_section($course, $section, array('visible' => $visible));
1816                  break;
1817              case 'refresh':
1818                  return [
1819                      'content' => $renderer->course_section_updated($this, $section),
1820                  ];
1821              default:
1822                  throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
1823          }
1824  
1825          return ['modules' => $this->get_section_modules_updated($section)];
1826      }
1827  
1828      /**
1829       * Return an array with all section modules content.
1830       *
1831       * This method is used in section_action method to generate the updated modules content
1832       * after a modinfo change.
1833       *
1834       * @param section_info $section the section
1835       * @return string[] the full modules content.
1836       */
1837      protected function get_section_modules_updated(section_info $section): array {
1838          global $PAGE;
1839  
1840          $modules = [];
1841  
1842          if (!$this->uses_sections() || !$section->section) {
1843              return $modules;
1844          }
1845  
1846          // Load the cmlist output from the updated modinfo.
1847          $renderer = $this->get_renderer($PAGE);
1848          $modinfo = $this->get_modinfo();
1849          $coursesections = $modinfo->sections;
1850          if (array_key_exists($section->section, $coursesections)) {
1851              foreach ($coursesections[$section->section] as $cmid) {
1852                  $cm = $modinfo->get_cm($cmid);
1853                  $modules[] = $renderer->course_section_updated_cm_item($this, $section, $cm);
1854              }
1855          }
1856          return $modules;
1857      }
1858  
1859      /**
1860       * Return the plugin config settings for external functions,
1861       * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled.
1862       *
1863       * @return array the list of configs
1864       * @since Moodle 3.5
1865       */
1866      public function get_config_for_external() {
1867          return array();
1868      }
1869  
1870      /**
1871       * Course deletion hook.
1872       *
1873       * Format plugins can override this method to clean any format specific data and dependencies.
1874       *
1875       */
1876      public function delete_format_data() {
1877          global $DB;
1878          $course = $this->get_course();
1879          // By default, formats store some most display specifics in a user preference.
1880          $DB->delete_records('user_preferences', ['name' => 'coursesectionspreferences_' . $course->id]);
1881      }
1882  
1883      /**
1884       * Duplicate a section
1885       *
1886       * @param section_info $originalsection The section to be duplicated
1887       * @return section_info The new duplicated section
1888       * @since Moodle 4.2
1889       */
1890      public function duplicate_section(section_info $originalsection): section_info {
1891          if (!$this->uses_sections()) {
1892              throw new moodle_exception('sectionsnotsupported', 'core_courseformat');
1893          }
1894  
1895          $course = $this->get_course();
1896          $oldsectioninfo = get_fast_modinfo($course)->get_section_info($originalsection->section);
1897          $newsection = course_create_section($course, $oldsectioninfo->section + 1); // Place new section after existing one.
1898  
1899          if (!empty($originalsection->name)) {
1900              $newsection->name = get_string('duplicatedsection', 'moodle', $originalsection->name);
1901          } else {
1902              $newsection->name = $originalsection->name;
1903          }
1904          $newsection->summary = $originalsection->summary;
1905          $newsection->summaryformat = $originalsection->summaryformat;
1906          $newsection->visible = $originalsection->visible;
1907          $newsection->availability = $originalsection->availability;
1908          course_update_section($course, $newsection, $newsection);
1909  
1910          $modinfo = $this->get_modinfo();
1911          foreach ($modinfo->sections[$originalsection->section] as $modnumber) {
1912              $originalcm = $modinfo->cms[$modnumber];
1913              duplicate_module($course, $originalcm, $newsection->id, false);
1914          }
1915  
1916          return get_fast_modinfo($course)->get_section_info_by_id($newsection->id);
1917      }
1918  }