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