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 * This file contains main class for the course format singleactivity
19 *
20 * @package    format_singleactivity
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();
26require_once($CFG->dirroot. '/course/format/lib.php');
27
28/**
29 * Main class for the singleactivity course format
30 *
31 * @package    format_singleactivity
32 * @copyright  2012 Marina Glancy
33 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35class format_singleactivity extends format_base {
36    /** @var cm_info the current activity. Use get_activity() to retrieve it. */
37    private $activity = false;
38
39    /** @var int The category ID guessed from the form data. */
40    private $categoryid = false;
41
42    /**
43     * The URL to use for the specified course
44     *
45     * @param int|stdClass $section Section object from database or just field course_sections.section
46     *     if null the course view page is returned
47     * @param array $options options for view URL. At the moment core uses:
48     *     'navigation' (bool) if true and section has no separate page, the function returns null
49     *     'sr' (int) used by multipage formats to specify to which section to return
50     * @return null|moodle_url
51     */
52    public function get_view_url($section, $options = array()) {
53        $sectionnum = $section;
54        if (is_object($sectionnum)) {
55            $sectionnum = $section->section;
56        }
57        if ($sectionnum == 1) {
58            return new moodle_url('/course/view.php', array('id' => $this->courseid, 'section' => 1));
59        }
60        if (!empty($options['navigation']) && $section !== null) {
61            return null;
62        }
63        return new moodle_url('/course/view.php', array('id' => $this->courseid));
64    }
65
66    /**
67     * Loads all of the course sections into the navigation
68     *
69     * @param global_navigation $navigation
70     * @param navigation_node $node The course node within the navigation
71     */
72    public function extend_course_navigation($navigation, navigation_node $node) {
73        // Display orphaned activities for the users who can see them.
74        $context = context_course::instance($this->courseid);
75        if (has_capability('moodle/course:viewhiddensections', $context)) {
76            $modinfo = get_fast_modinfo($this->courseid);
77            if (!empty($modinfo->sections[1])) {
78                $section1 = $modinfo->get_section_info(1);
79                // Show orphaned activities.
80                $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'),
81                        $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id);
82                $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
83                $orphanednode->add_class('orphaned');
84                foreach ($modinfo->sections[1] as $cmid) {
85                    if (has_capability('moodle/course:viewhiddenactivities', context_module::instance($cmid))) {
86                        $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
87                    }
88                }
89            }
90        }
91    }
92
93    /**
94     * Adds a course module to the navigation node
95     *
96     * This is basically copied from function global_navigation::load_section_activities()
97     * because it is not accessible from outside.
98     *
99     * @param navigation_node $node
100     * @param cm_info $cm
101     * @return null|navigation_node
102     */
103    protected function navigation_add_activity(navigation_node $node, $cm) {
104        if (!$cm->uservisible) {
105            return null;
106        }
107        $action = $cm->url;
108        if (!$action) {
109            // Do not add to navigation activity without url (i.e. labels).
110            return null;
111        }
112        $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id)));
113        if ($cm->icon) {
114            $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent);
115        } else {
116            $icon = new pix_icon('icon', $cm->modfullname, $cm->modname);
117        }
118        $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
119        if (global_navigation::module_extends_navigation($cm->modname)) {
120            $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
121        } else {
122            $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
123        }
124        return $activitynode;
125    }
126
127    /**
128     * Returns the list of blocks to be automatically added for the newly created course
129     *
130     * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
131     *     each of values is an array of block names (for left and right side columns)
132     */
133    public function get_default_blocks() {
134        // No blocks for this format because course view page is not displayed anyway.
135        return array(
136            BLOCK_POS_LEFT => array(),
137            BLOCK_POS_RIGHT => array()
138        );
139    }
140
141    /**
142     * Definitions of the additional options that this course format uses for course
143     *
144     * Singleactivity course format uses one option 'activitytype'
145     *
146     * @param bool $foreditform
147     * @return array of options
148     */
149    public function course_format_options($foreditform = false) {
150        static $courseformatoptions = false;
151
152        $fetchtypes = $courseformatoptions === false;
153        $fetchtypes = $fetchtypes || ($foreditform && !isset($courseformatoptions['activitytype']['label']));
154
155        if ($fetchtypes) {
156            $availabletypes = $this->get_supported_activities();
157            if ($this->courseid) {
158                // The course exists. Test against the course.
159                $testcontext = context_course::instance($this->courseid);
160            } else if ($this->categoryid) {
161                // The course does not exist yet, but we have a category ID that we can test against.
162                $testcontext = context_coursecat::instance($this->categoryid);
163            } else {
164                // The course does not exist, and we somehow do not have a category. Test capabilities against the system context.
165                $testcontext = context_system::instance();
166            }
167            foreach (array_keys($availabletypes) as $activity) {
168                $capability = "mod/{$activity}:addinstance";
169                if (!has_capability($capability, $testcontext)) {
170                    unset($availabletypes[$activity]);
171                }
172            }
173        }
174
175        if ($courseformatoptions === false) {
176            $config = get_config('format_singleactivity');
177            $courseformatoptions = array(
178                'activitytype' => array(
179                    'default' => $config->activitytype,
180                    'type' => PARAM_TEXT,
181                ),
182            );
183
184            if (!empty($availabletypes) && !isset($availabletypes[$config->activitytype])) {
185                $courseformatoptions['activitytype']['default'] = array_keys($availabletypes)[0];
186            }
187        }
188
189        if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
190            $courseformatoptionsedit = array(
191                'activitytype' => array(
192                    'label' => new lang_string('activitytype', 'format_singleactivity'),
193                    'help' => 'activitytype',
194                    'help_component' => 'format_singleactivity',
195                    'element_type' => 'select',
196                    'element_attributes' => array($availabletypes),
197                ),
198            );
199            $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
200        }
201        return $courseformatoptions;
202    }
203
204    /**
205     * Adds format options elements to the course/section edit form
206     *
207     * This function is called from {@link course_edit_form::definition_after_data()}
208     *
209     * Format singleactivity adds a warning when format of the course is about to be changed.
210     *
211     * @param MoodleQuickForm $mform form the elements are added to
212     * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
213     * @return array array of references to the added form elements
214     */
215    public function create_edit_form_elements(&$mform, $forsection = false) {
216        global $PAGE;
217
218        if (!$this->course && $submitvalues = $mform->getSubmitValues()) {
219            $this->categoryid = $submitvalues['category'];
220        }
221
222        $elements = parent::create_edit_form_elements($mform, $forsection);
223        if (!$forsection && ($course = $PAGE->course) && !empty($course->format) &&
224                $course->format !== 'site' && $course->format !== 'singleactivity') {
225            // This is the existing course in other format, display a warning.
226            $element = $mform->addElement('static', '', '',
227                    html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'),
228                            array('class' => 'error')));
229            array_unshift($elements, $element);
230        }
231        return $elements;
232    }
233
234    /**
235     * Make sure that current active activity is in section 0
236     *
237     * All other activities are moved to section 1 that will be displayed as 'Orphaned'.
238     * It may be needed after the course format was changed or activitytype in
239     * course settings has been changed.
240     *
241     * @return null|cm_info current activity
242     */
243    public function reorder_activities() {
244        course_create_sections_if_missing($this->courseid, array(0, 1));
245        foreach ($this->get_sections() as $sectionnum => $section) {
246            if (($sectionnum && $section->visible) ||
247                    (!$sectionnum && !$section->visible)) {
248                // Make sure that 0 section is visible and all others are hidden.
249                set_section_visible($this->courseid, $sectionnum, $sectionnum == 0);
250            }
251        }
252        $modinfo = get_fast_modinfo($this->courseid);
253
254        // Find the current activity (first activity with the specified type in all course activities).
255        $activitytype = $this->get_activitytype();
256        $activity = null;
257        if (!empty($activitytype)) {
258            foreach ($modinfo->sections as $sectionnum => $cmlist) {
259                foreach ($cmlist as $cmid) {
260                    if ($modinfo->cms[$cmid]->modname === $activitytype) {
261                        $activity = $modinfo->cms[$cmid];
262                        break 2;
263                    }
264                }
265            }
266        }
267
268        // Make sure the current activity is in the 0-section.
269        $changed = false;
270        if ($activity && $activity->sectionnum != 0) {
271            moveto_module($activity, $modinfo->get_section_info(0));
272            $changed = true;
273        }
274        if ($activity && !$activity->visible) {
275            set_coursemodule_visible($activity->id, 1);
276            $changed = true;
277        }
278        if ($changed) {
279            // Cache was reset so get modinfo again.
280            $modinfo = get_fast_modinfo($this->courseid);
281        }
282
283        // Move all other activities into section 1 (the order must be kept).
284        $hasvisibleactivities = false;
285        $firstorphanedcm = null;
286        foreach ($modinfo->sections as $sectionnum => $cmlist) {
287            if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) {
288                $firstorphanedcm = reset($cmlist);
289            }
290            foreach ($cmlist as $cmid) {
291                if ($sectionnum > 1) {
292                    moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1));
293                } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) {
294                    $hasvisibleactivities = true;
295                }
296            }
297        }
298        if (!empty($modinfo->sections[0])) {
299            foreach ($modinfo->sections[0] as $cmid) {
300                if (!$activity || $cmid != $activity->id) {
301                    moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm);
302                }
303            }
304        }
305        if ($hasvisibleactivities) {
306            set_section_visible($this->courseid, 1, false);
307        }
308        return $activity;
309    }
310
311    /**
312     * Returns the name of activity type used for this course
313     *
314     * @return string|null
315     */
316    protected function get_activitytype() {
317        $options = $this->get_format_options();
318        $availabletypes = $this->get_supported_activities();
319        if (!empty($options['activitytype']) &&
320                array_key_exists($options['activitytype'], $availabletypes)) {
321            return $options['activitytype'];
322        } else {
323            return null;
324        }
325    }
326
327    /**
328     * Returns the current activity if exists
329     *
330     * @return null|cm_info
331     */
332    protected function get_activity() {
333        if ($this->activity === false) {
334            $this->activity = $this->reorder_activities();
335        }
336        return $this->activity;
337    }
338
339    /**
340     * Get the activities supported by the format.
341     *
342     * Here we ignore the modules that do not have a page of their own, like the label.
343     *
344     * @return array array($module => $name of the module).
345     */
346    public static function get_supported_activities() {
347        $availabletypes = get_module_types_names();
348        foreach ($availabletypes as $module => $name) {
349            if (plugin_supports('mod', $module, FEATURE_NO_VIEW_LINK, false)) {
350                unset($availabletypes[$module]);
351            }
352        }
353        return $availabletypes;
354    }
355
356    /**
357     * Checks if the current user can add the activity of the specified type to this course.
358     *
359     * @return bool
360     */
361    protected function can_add_activity() {
362        global $CFG;
363        if (!($modname = $this->get_activitytype())) {
364            return false;
365        }
366        if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) {
367            return false;
368        }
369        if (!course_allowed_module($this->get_course(), $modname)) {
370            return false;
371        }
372        $libfile = "$CFG->dirroot/mod/$modname/lib.php";
373        if (!file_exists($libfile)) {
374            return null;
375        }
376        return true;
377    }
378
379    /**
380     * Checks if the activity type has multiple items in the activity chooser.
381     * This may happen as a result of defining callback modulename_get_shortcuts().
382     *
383     * @return bool|null (null if the check is not possible)
384     */
385    public function activity_has_subtypes() {
386        global $USER;
387        if (!($modname = $this->get_activitytype())) {
388            return null;
389        }
390        $contentitemservice = \core_course\local\factory\content_item_service_factory::get_content_item_service();
391        $metadata = $contentitemservice->get_content_items_for_user_in_course($USER, $this->get_course());
392
393        // If there are multiple items originating from this mod_xxx component, then it's deemed to have subtypes.
394        // If there is only 1 item, but it's not a reference to the core content item for the module, then it's also deemed to
395        // have subtypes.
396        $count = 0;
397        foreach ($metadata as $key => $moduledata) {
398            if ('mod_'.$modname === $moduledata->componentname) {
399                $count ++;
400            }
401        }
402        if ($count > 1) {
403            return true;
404        } else {
405            // Get the single item.
406            $itemmetadata = $metadata[array_search('mod_' . $modname, array_column($metadata, 'componentname'))];
407            $urlbase = new \moodle_url('/course/mod.php', ['id' => $this->get_course()->id]);
408            $referenceurl = new \moodle_url($urlbase, ['add' => $modname]);
409            if ($referenceurl->out(false) != $itemmetadata->link) {
410                return true;
411            }
412        }
413        return false;
414    }
415
416    /**
417     * Allows course format to execute code on moodle_page::set_course()
418     *
419     * This function is executed before the output starts.
420     *
421     * If everything is configured correctly, user is redirected from the
422     * default course view page to the activity view page.
423     *
424     * "Section 1" is the administrative page to manage orphaned activities
425     *
426     * If user is on course view page and there is no module added to the course
427     * and the user has 'moodle/course:manageactivities' capability, redirect to create module
428     * form.
429     *
430     * @param moodle_page $page instance of page calling set_course
431     */
432    public function page_set_course(moodle_page $page) {
433        global $PAGE;
434        $page->add_body_class('format-'. $this->get_format());
435        if ($PAGE == $page && $page->has_set_url() &&
436                $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
437            $edit = optional_param('edit', -1, PARAM_BOOL);
438            if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
439                // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
440                return;
441            }
442            $cm = $this->get_activity();
443            $cursection = optional_param('section', null, PARAM_INT);
444            if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
445                    context_course::instance($this->courseid))) {
446                // Display orphaned activities (course view page, section 1).
447                return;
448            }
449            if (!$this->get_activitytype()) {
450                if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
451                    // Teacher is redirected to edit course page.
452                    $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
453                    redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
454                } else {
455                    // Student sees an empty course page.
456                    return;
457                }
458            }
459            if ($cm === null) {
460                if ($this->can_add_activity()) {
461                    // This is a user who has capability to create an activity.
462                    if ($this->activity_has_subtypes()) {
463                        // Activity has multiple items in the activity chooser, it can not be added automatically.
464                        if (optional_param('addactivity', 0, PARAM_INT)) {
465                            return;
466                        } else {
467                            $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
468                            redirect($url);
469                        }
470                    }
471                    // Redirect to the add activity form.
472                    $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
473                        'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
474                    redirect($url);
475                } else {
476                    // Student views an empty course page.
477                    return;
478                }
479            } else if (!$cm->uservisible || !$cm->url) {
480                // Activity is set but not visible to current user or does not have url.
481                // Display course page (either empty or with availability restriction info).
482                return;
483            } else {
484                // Everything is set up and accessible, redirect to the activity page!
485                redirect($cm->url);
486            }
487        }
488    }
489
490    /**
491     * Allows course format to execute code on moodle_page::set_cm()
492     *
493     * If we are inside the main module for this course, remove extra node level
494     * from navigation: substitute course node with activity node, move all children
495     *
496     * @param moodle_page $page instance of page calling set_cm
497     */
498    public function page_set_cm(moodle_page $page) {
499        global $PAGE;
500        parent::page_set_cm($page);
501        if ($PAGE == $page && ($cm = $this->get_activity()) &&
502                $cm->uservisible &&
503                ($cm->id === $page->cm->id) &&
504                ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
505                ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
506            // Substitute course node with activity node, move all children.
507            $node->action = $activitynode->action;
508            $node->type = $activitynode->type;
509            $node->id = $activitynode->id;
510            $node->key = $activitynode->key;
511            $node->isactive = $node->isactive || $activitynode->isactive;
512            $node->icon = null;
513            if ($activitynode->children->count()) {
514                foreach ($activitynode->children as &$child) {
515                    $child->remove();
516                    $node->add_node($child);
517                }
518            } else {
519                $node->search_for_active_node();
520            }
521            $activitynode->remove();
522        }
523    }
524
525    /**
526     * Returns true if the course has a front page.
527     *
528     * @return boolean false
529     */
530    public function has_view_page() {
531        return false;
532    }
533
534    /**
535     * Return the plugin configs for external functions.
536     *
537     * @return array the list of configuration settings
538     * @since Moodle 3.5
539     */
540    public function get_config_for_external() {
541        // Return everything (nothing to hide).
542        return $this->get_format_options();
543    }
544}
545