Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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