Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
namespace core\navigation\views;
18
 
19
use navigation_node;
20
use url_select;
21
use settings_navigation;
22
 
23
/**
24
 * Class secondary_navigation_view.
25
 *
26
 * The secondary navigation view is a stripped down tweaked version of the
27
 * settings_navigation/navigation
28
 *
29
 * @package     core
30
 * @category    navigation
31
 * @copyright   2021 onwards Peter Dias
32
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33
 */
34
class secondary extends view {
35
    /** @var string $headertitle The header for this particular menu*/
36
    public $headertitle;
37
 
38
    /** @var int The maximum limit of navigation nodes displayed in the secondary navigation */
39
    const MAX_DISPLAYED_NAV_NODES = 5;
40
 
41
    /** @var navigation_node The course overflow node. */
42
    protected $courseoverflownode = null;
43
 
44
    /** @var string The key of the node to set as selected in the course overflow menu, if explicitly set by a page. */
45
    protected $overflowselected = null;
46
 
47
    /**
48
     * Defines the default structure for the secondary nav in a course context.
49
     *
50
     * In a course context, we are curating nodes from the settingsnav and navigation objects.
51
     * The following mapping construct specifies which object we are fetching it from, the type of the node, the key
52
     * and in what order we want the node - defined as per the mockups.
53
     *
54
     * @return array
55
     */
56
    protected function get_default_course_mapping(): array {
57
        $nodes = [];
58
        $nodes['settings'] = [
59
            self::TYPE_CONTAINER => [
60
                'coursereports' => 3,
61
                'questionbank' => 4,
62
            ],
63
            self::TYPE_SETTING => [
64
                'editsettings' => 0,
65
                'review' => 1.1,
66
                'manageinstances' => 1.2,
67
                'groups' => 1.3,
68
                'override' => 1.4,
69
                'roles' => 1.5,
70
                'permissions' => 1.6,
71
                'otherusers' => 1.7,
72
                'gradebooksetup' => 2.1,
73
                'outcomes' => 2.2,
74
                'coursecompletion' => 6,
75
                'coursebadges' => 7.1,
76
                'newbadge' => 7.2,
77
                'filtermanagement' => 9,
78
                'unenrolself' => 10,
79
                'coursetags' => 11,
80
                'download' => 12,
81
                'contextlocking' => 13,
82
            ],
83
        ];
84
        $nodes['navigation'] = [
85
            self::TYPE_CONTAINER => [
86
                'participants' => 1,
87
            ],
88
            self::TYPE_SETTING => [
89
                'grades' => 2,
90
                'badgesview' => 7,
91
                'competencies' => 8,
92
                'communication' => 14,
93
            ],
94
            self::TYPE_CUSTOM => [
95
                'contentbank' => 5,
96
                'participants' => 1, // In site home, 'participants' is classified differently.
97
            ],
98
        ];
99
 
100
        return $nodes;
101
    }
102
 
103
    /**
104
     * Defines the default structure for the secondary nav in a module context.
105
     *
106
     * In a module context, we are curating nodes from the settingsnav object.
107
     * The following mapping construct specifies the type of the node, the key
108
     * and in what order we want the node - defined as per the mockups.
109
     *
110
     * @return array
111
     */
112
    protected function get_default_module_mapping(): array {
113
        return [
114
            self::TYPE_SETTING => [
115
                'modedit' => 1,
116
                "mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific.
117
                "mod_{$this->page->activityname}_groupoverrides" => 4,
118
                'roleassign' => 7.2,
119
                'filtermanage' => 6,
120
                'roleoverride' => 7,
121
                'rolecheck' => 7.1,
122
                'logreport' => 8,
123
                'backup' => 9,
124
                'restore' => 10,
125
                'competencybreakdown' => 11,
126
                'sendtomoodlenet' => 16,
127
            ],
128
            self::TYPE_CUSTOM => [
129
                'advgrading' => 2,
130
                'contentbank' => 12,
131
            ],
132
        ];
133
    }
134
 
135
    /**
136
     * Defines the default structure for the secondary nav in a category context.
137
     *
138
     * In a category context, we are curating nodes from the settingsnav object.
139
     * The following mapping construct specifies the type of the node, the key
140
     * and in what order we want the node - defined as per the mockups.
141
     *
142
     * @return array
143
     */
144
    protected function get_default_category_mapping(): array {
145
        return [
146
            self::TYPE_SETTING => [
147
                'edit' => 1,
148
                'permissions' => 2,
149
                'roles' => 2.1,
150
                'rolecheck' => 2.2,
151
            ]
152
        ];
153
    }
154
 
155
    /**
156
     * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
157
     *
158
     * @return array
159
     */
160
    protected function get_default_category_more_menu_nodes(): array {
161
        return ['addsubcat', 'roles', 'permissions', 'contentbank', 'cohort', 'filters', 'restorecourse'];
162
    }
163
    /**
164
     * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
165
     *
166
     * @return array
167
     */
168
    protected function get_default_course_more_menu_nodes(): array {
169
        return [];
170
    }
171
 
172
    /**
173
     * Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default.
174
     *
175
     * @return array
176
     */
177
    protected function get_default_module_more_menu_nodes(): array {
178
        return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore',
179
            'competencybreakdown', "mod_{$this->page->activityname}_useroverrides",
180
            "mod_{$this->page->activityname}_groupoverrides"];
181
    }
182
 
183
    /**
184
     * Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default.
185
     *
186
     * @return array
187
     */
188
    protected function get_default_admin_more_menu_nodes(): array {
189
        return [];
190
    }
191
 
192
    /**
193
     * Initialise the view based navigation based on the current context.
194
     *
195
     * As part of the initial restructure, the secondary nav is only considered for the following pages:
196
     * 1 - Site admin settings
197
     * 2 - Course page - Does not include front_page which has the same context.
198
     * 3 - Module page
199
     */
200
    public function initialise(): void {
201
        global $SITE;
202
 
203
        if (during_initial_install() || $this->initialised) {
204
            return;
205
        }
206
        $this->id = 'secondary_navigation';
207
        $context = $this->context;
208
        $this->headertitle = get_string('menu');
209
        $defaultmoremenunodes = [];
210
        $maxdisplayednodes = self::MAX_DISPLAYED_NAV_NODES;
211
 
212
        switch ($context->contextlevel) {
213
            case CONTEXT_COURSE:
214
                $this->headertitle = get_string('courseheader');
215
                if ($this->page->course->format === 'singleactivity') {
216
                    $this->load_single_activity_course_navigation();
217
                } else {
218
                    $this->load_course_navigation();
219
                    $defaultmoremenunodes = $this->get_default_course_more_menu_nodes();
220
                }
221
                break;
222
            case CONTEXT_MODULE:
223
                $this->headertitle = get_string('activityheader');
224
                if ($this->page->course->format === 'singleactivity') {
225
                    $this->load_single_activity_course_navigation();
226
                } else {
227
                    $this->load_module_navigation($this->page->settingsnav);
228
                    $defaultmoremenunodes = $this->get_default_module_more_menu_nodes();
229
                }
230
                break;
231
            case CONTEXT_COURSECAT:
232
                $this->headertitle = get_string('categoryheader');
233
                $this->load_category_navigation();
234
                $defaultmoremenunodes = $this->get_default_category_more_menu_nodes();
235
                break;
236
            case CONTEXT_SYSTEM:
237
                $this->headertitle = get_string('homeheader');
238
                $this->load_admin_navigation();
239
                // If the site administration navigation was generated after load_admin_navigation().
240
                if ($this->has_children()) {
241
                    // Do not explicitly limit the number of navigation nodes displayed in the site administration
242
                    // navigation menu.
243
                    $maxdisplayednodes = null;
244
                }
245
                $defaultmoremenunodes = $this->get_default_admin_more_menu_nodes();
246
                break;
247
        }
248
 
249
        $this->remove_unwanted_nodes($this);
250
 
251
        // Don't need to show anything if only the view node is available. Remove it.
252
        if ($this->children->count() == 1) {
253
            $this->children->remove('modulepage');
254
        }
255
        // Force certain navigation nodes to be displayed in the "more" menu.
256
        $this->force_nodes_into_more_menu($defaultmoremenunodes, $maxdisplayednodes);
257
        // Search and set the active node.
258
        $this->scan_for_active_node($this);
259
        $this->initialised = true;
260
    }
261
 
262
    /**
263
     * Returns a node with the action being from the first found child node that has an action (Recursive).
264
     *
265
     * @param navigation_node $node The part of the node tree we are checking.
266
     * @param navigation_node $basenode  The very first node to be used for the return.
267
     * @return navigation_node|null
268
     */
269
    protected function get_node_with_first_action(navigation_node $node, navigation_node $basenode): ?navigation_node {
270
        $newnode = null;
271
        if (!$node->has_children()) {
272
            return null;
273
        }
274
 
275
        // Find the first child with an action and update the main node.
276
        foreach ($node->children as $child) {
277
            if ($child->has_action()) {
278
                $newnode = $basenode;
279
                $newnode->action = $child->action;
280
                return $newnode;
281
            }
282
        }
283
        if (is_null($newnode)) {
284
            // Check for children and go again.
285
            foreach ($node->children as $child) {
286
                if ($child->has_children()) {
287
                    $newnode = $this->get_node_with_first_action($child, $basenode);
288
 
289
                    if (!is_null($newnode)) {
290
                        return $newnode;
291
                    }
292
                }
293
            }
294
        }
295
        return null;
296
    }
297
 
298
    /**
299
     * Some nodes are containers only with no action. If this container has an action then nothing is done. If it does not have
300
     * an action then a search is done through the children looking for the first node that has an action. This action is then given
301
     * to the parent node that is initially provided as a parameter.
302
     *
303
     * @param navigation_node $node The navigation node that we want to ensure has an action tied to it.
304
     * @return navigation_node The node intact with an action to use.
305
     */
306
    protected function get_first_action_for_node(navigation_node $node): ?navigation_node {
307
        // If the node does not have children and has no action then no further processing is needed.
308
        $newnode = null;
309
        if ($node->has_children() && !$node->has_action()) {
310
            // We want to find the first child with an action.
311
            // We want to check all children on this level before going further down.
312
            // Note that new node gets changed here.
313
            $newnode = $this->get_node_with_first_action($node, $node);
314
        } else if ($node->has_action()) {
315
            $newnode = $node;
316
        }
317
        return $newnode;
318
    }
319
 
320
    /**
321
     * Recursive call to add all custom navigation nodes to secondary
322
     *
323
     * @param navigation_node $node The node which should be added to secondary
324
     * @param navigation_node $basenode The original parent node
325
     * @param navigation_node|null $root The parent node nodes are to be added/removed to.
326
     * @param bool $forceadd Whether or not to bypass the external action check and force add all nodes
327
     */
328
    protected function add_external_nodes_to_secondary(navigation_node $node, navigation_node $basenode,
329
           ?navigation_node $root = null, bool $forceadd = false) {
330
        $root = $root ?? $this;
331
        // Add the first node.
332
        if ($node->has_action() && !$this->get($node->key)) {
333
            $root->add_node(clone $node);
334
        }
335
 
336
        // If the node has an external action add all children to the secondary navigation.
337
        if (!$node->has_internal_action() || $forceadd) {
338
            if ($node->has_children()) {
339
                foreach ($node->children as $child) {
340
                    if ($child->has_children()) {
341
                        $this->add_external_nodes_to_secondary($child, $basenode, $root, true);
342
                    } else if ($child->has_action() && !$this->get($child->key)) {
343
                        // Check whether the basenode matches a child's url.
344
                        // This would have happened in get_first_action_for_node.
345
                        // In these cases, we prefer the specific child content.
346
                        if ($basenode->has_action() && $basenode->action()->compare($child->action())) {
347
                            $root->children->remove($basenode->key, $basenode->type);
348
                        }
349
                        $root->add_node(clone $child);
350
                    }
351
                }
352
            }
353
        }
354
    }
355
 
356
    /**
357
     * Returns a list of all expected nodes in the course administration.
358
     *
359
     * @return array An array of keys for navigation nodes in the course administration.
360
     */
361
    protected function get_expected_course_admin_nodes(): array {
362
        $expectednodes = [];
363
        foreach ($this->get_default_course_mapping()['settings'] as $value) {
364
            foreach ($value as $nodekey => $notused) {
365
                $expectednodes[] = $nodekey;
366
            }
367
        }
368
        foreach ($this->get_default_course_mapping()['navigation'] as $value) {
369
            foreach ($value as $nodekey => $notused) {
370
                $expectednodes[] = $nodekey;
371
            }
372
        }
373
        $othernodes = ['users', 'gradeadmin', 'coursereports', 'coursebadges'];
374
        $leftovercourseadminnodes = ['backup', 'restore', 'import', 'copy', 'reset'];
375
        $expectednodes = array_merge($expectednodes, $othernodes);
376
        $expectednodes = array_merge($expectednodes, $leftovercourseadminnodes);
377
        return $expectednodes;
378
    }
379
 
380
    /**
381
     * Load the course secondary navigation. Since we are sourcing all the info from existing objects that already do
382
     * the relevant checks, we don't do it again here.
383
     *
384
     * @param navigation_node|null $rootnode The node where the course navigation nodes should be added into as children.
385
     *                                       If not explicitly defined, the nodes will be added to the secondary root
386
     *                                       node by default.
387
     */
388
    protected function load_course_navigation(?navigation_node $rootnode = null): void {
389
        global $SITE;
390
 
391
        $rootnode = $rootnode ?? $this;
392
        $course = $this->page->course;
393
        // Initialise the main navigation and settings nav.
394
        // It is important that this is done before we try anything.
395
        $settingsnav = $this->page->settingsnav;
396
        $navigation = $this->page->navigation;
397
 
398
        if ($course->id == $SITE->id) {
399
            $firstnodeidentifier = get_string('home'); // The first node in the site course nav is called 'Home'.
400
            $frontpage = $settingsnav->get('frontpage'); // The site course nodes are children of a dedicated 'frontpage' node.
401
            $settingsnav = $frontpage ?: $settingsnav;
402
            $courseadminnode = $frontpage ?: null; // Custom nodes for the site course are also children of the 'frontpage' node.
403
        } else {
404
            $firstnodeidentifier = get_string('course'); // Regular courses have a first node called 'Course'.
405
            $courseadminnode = $settingsnav->get('courseadmin'); // Custom nodes for regular courses live under 'courseadmin'.
406
        }
407
 
408
        // Add the known nodes from settings and navigation.
409
        $nodes = $this->get_default_course_mapping();
410
        $nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []);
411
        $nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []);
412
        $this->add_ordered_nodes($nodesordered, $rootnode);
413
 
414
        // Try to get any custom nodes defined by plugins, which may include containers.
415
        if ($courseadminnode) {
416
            $expectedcourseadmin = $this->get_expected_course_admin_nodes();
417
            foreach ($courseadminnode->children as $other) {
418
                if (array_search($other->key, $expectedcourseadmin, true) === false) {
419
                    $othernode = $this->get_first_action_for_node($other);
420
                    $recursivenode = $othernode && !$rootnode->get($othernode->key) ? $othernode : $other;
421
                    // Get the first node and check whether it's been added already.
422
                    // Also check if the first node is an external link. If it is, add all children.
423
                    $this->add_external_nodes_to_secondary($recursivenode, $recursivenode, $rootnode);
424
                }
425
            }
426
        }
427
 
428
        // Add the respective first node, provided there are other nodes included.
429
        if (!empty($nodekeys = $rootnode->children->get_key_list())) {
430
            $rootnode->add_node(
431
                navigation_node::create($firstnodeidentifier, new \moodle_url('/course/view.php', ['id' => $course->id]),
432
                    self::TYPE_COURSE, null, 'coursehome'), reset($nodekeys)
433
            );
434
        }
435
    }
436
 
437
    /**
438
     * Gets the overflow navigation nodes for the course administration category.
439
     *
440
     * @param navigation_node|null $rootnode The node from where the course overflow nodes should be obtained.
441
     *                                       If not explicitly defined, the nodes will be obtained from the secondary root
442
     *                                       node by default.
443
     * @return navigation_node  The course overflow nodes.
444
     */
445
    protected function get_course_overflow_nodes(?navigation_node $rootnode = null): ?navigation_node {
446
        global $SITE;
447
 
448
        $rootnode = $rootnode ?? $this;
449
        // This gets called twice on some pages, and so trying to create this navigation node twice results in no children being
450
        // present the second time this is called.
451
        if (isset($this->courseoverflownode)) {
452
            return $this->courseoverflownode;
453
        }
454
 
455
        // Start with getting the base node for the front page or the course.
456
        $node = null;
457
        if ($this->page->course->id == $SITE->id) {
458
            $node = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
459
        } else {
460
            $node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
461
        }
462
        $coursesettings = $node ? $node->get_children_key_list() : [];
463
        $thissettings = $rootnode->get_children_key_list();
464
        $diff = array_diff($coursesettings, $thissettings);
465
 
466
        // Remove our specific created elements (user - participants, badges - coursebadges, grades - gradebooksetup,
467
        // grades - outcomes).
468
        $shortdiff = array_filter($diff, function($value) {
469
            return !($value == 'users' || $value == 'coursebadges' || $value == 'gradebooksetup' ||
470
                $value == 'outcomes');
471
        });
472
 
473
        // Permissions may be in play here that ultimately will show no overflow.
474
        if (empty($shortdiff)) {
475
            return null;
476
        }
477
 
478
        $firstitem = array_shift($shortdiff);
479
        $navnode = $node->get($firstitem);
480
        foreach ($shortdiff as $key) {
481
            $courseadminnodes = $node->get($key);
482
            if ($courseadminnodes) {
483
                if ($courseadminnodes->parent->key == $node->key) {
484
                    $navnode->add_node($courseadminnodes);
485
                }
486
            }
487
        }
488
        $this->courseoverflownode = $navnode;
489
        return $navnode;
490
 
491
    }
492
 
493
    /**
494
     * Recursively looks for a match to the current page url.
495
     *
496
     * @param navigation_node $node The node to look through.
497
     * @return navigation_node|null The node that matches this page's url.
498
     */
499
    protected function nodes_match_current_url(navigation_node $node): ?navigation_node {
500
        $pagenode = $this->page->url;
501
        if ($node->has_action()) {
502
            // Check this node first.
503
            if ($node->action->compare($pagenode)) {
504
                return $node;
505
            }
506
        }
507
        if ($node->has_children()) {
508
            foreach ($node->children as $child) {
509
                $result = $this->nodes_match_current_url($child);
510
                if ($result) {
511
                    return $result;
512
                }
513
            }
514
        }
515
        return null;
516
    }
517
 
518
    /**
519
     * Recursively search a node and its children for a node matching the key string $key.
520
     *
521
     * @param navigation_node $node the navigation node to check.
522
     * @param string $key the key of the node to match.
523
     * @return navigation_node|null node if found, otherwise null.
524
     */
525
    protected function node_matches_key_string(navigation_node $node, string $key): ?navigation_node {
526
        if ($node->has_action()) {
527
            // Check this node first.
528
            if ($node->key == $key) {
529
                return $node;
530
            }
531
        }
532
        if ($node->has_children()) {
533
            foreach ($node->children as $child) {
534
                $result = $this->node_matches_key_string($child, $key);
535
                if ($result) {
536
                    return $result;
537
                }
538
            }
539
        }
540
        return null;
541
    }
542
 
543
    /**
544
     * Force a specific node in the 'coursereuse' course overflow to be selected, based on the provided node key.
545
     *
546
     * Normally, the selected node is determined by matching the page URL to the node URL. E.g. The page 'backup/restorefile.php'
547
     * will match the "Restore" node which has a registered URL of 'backup/restorefile.php' because the URLs match.
548
     *
549
     * This method allows a page to choose a specific node to match, which is useful in cases where the page knows its URL won't
550
     * match the node it needs to reside under. I.e. this permits several pages to 'share' the same overflow node. When the page
551
     * knows the PAGE->url won't match the node URL, the page can simply say "I want to match the 'XXX' node".
552
     *
553
     * E.g.
554
     * - The $PAGE->url is 'backup/restore.php' (this page is used during restores but isn't the main landing page for a restore)
555
     * - The 'Restore' node in the overflow has a key of 'restore' and will only match 'backup/restorefile.php' by default (the
556
     * main restore landing page).
557
     * - The backup/restore.php page calls:
558
     * $PAGE->secondarynav->set_overflow_selected_node(new moodle_url('restore');
559
     * and when the page is loaded, the 'Restore' node be presented as the selected node.
560
     *
561
     * @param string $nodekey The string key of the overflow node to match.
562
     */
563
    public function set_overflow_selected_node(string $nodekey): void {
564
        $this->overflowselected = $nodekey;
565
    }
566
 
567
    /**
568
     * Returns a url_select object with overflow navigation nodes.
569
     * This looks to see if the current page is within the course administration, or some other page that requires an overflow
570
     * select object.
571
     *
572
     * @return url_select|null The overflow menu data.
573
     */
574
    public function get_overflow_menu_data(): ?url_select {
575
 
576
        if (!$this->page->get_navigation_overflow_state()) {
577
            return null;
578
        }
579
 
580
        $issingleactivitycourse = $this->page->course->format === 'singleactivity';
581
        $rootnode = $issingleactivitycourse ? $this->find('course', self::TYPE_COURSE) : $this;
582
        $activenode = $this->find_active_node();
583
        $incourseadmin = false;
584
 
585
        if (!$activenode || ($issingleactivitycourse && $activenode->key === 'course')) {
586
            // Could be in the course admin section.
587
            $courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
588
            if (!$courseadmin) {
589
                return null;
590
            }
591
 
592
            $activenode = $courseadmin->find_active_node();
593
            if (!$activenode) {
594
                return null;
595
            }
596
            $incourseadmin = true;
597
        }
598
 
599
        if ($activenode->key === 'coursereuse' || $incourseadmin) {
600
            $courseoverflownode = $this->get_course_overflow_nodes($rootnode);
601
            if (is_null($courseoverflownode)) {
602
                return null;
603
            }
604
            if ($incourseadmin) {
605
                // Validate whether the active node is part of the expected course overflow nodes.
606
                if (($activenode->key !== $courseoverflownode->key) &&
607
                    !$courseoverflownode->find($activenode->key, $activenode->type)) {
608
                    return null;
609
                }
610
            }
611
            $menuarray = static::create_menu_element([$courseoverflownode]);
612
            if ($activenode->key != 'coursereuse') {
613
                $inmenu = false;
614
                foreach ($menuarray as $key => $value) {
615
                    if ($this->page->url->out(false) == $key) {
616
                        $inmenu = true;
617
                    }
618
                }
619
                if (!$inmenu) {
620
                    return null;
621
                }
622
            }
623
            // If the page has explicitly set the overflow node it would like selected, find and use that node.
624
            if ($this->overflowselected) {
625
                $selectedoverflownode = $this->node_matches_key_string($courseoverflownode, $this->overflowselected);
626
                $selectedoverflownodeurl = $selectedoverflownode ? $selectedoverflownode->action->out(false) : null;
627
            }
628
 
629
            $menuselect = new url_select($menuarray, $selectedoverflownodeurl ?? $this->page->url, null);
630
            $menuselect->set_label(get_string('browsecourseadminindex', 'course'), ['class' => 'sr-only']);
631
            return $menuselect;
632
        } else {
633
            return $this->get_other_overflow_menu_data($activenode);
634
        }
635
    }
636
 
637
    /**
638
     * Gets overflow menu data for third party plugin settings.
639
     *
640
     * @param navigation_node $activenode The node to gather the children for to put into the overflow menu.
641
     * @return url_select|null The overflow menu in a url_select object.
642
     */
643
    protected function get_other_overflow_menu_data(navigation_node $activenode): ?url_select {
644
        if (!$activenode->has_action()) {
645
            return null;
646
        }
647
 
648
        if (!$activenode->has_children()) {
649
            return null;
650
        }
651
 
652
        // If the setting is extending the course navigation then the page being redirected to should be in the course context.
653
        // It was decided on the issue that put this code here that plugins that extend the course navigation should have the pages
654
        // that are redirected to, be in the course context or module context depending on which callback was used.
655
        // Third part plugins were checked to see if any existing plugins had settings in a system context and none were found.
656
        // The request of third party developers is to keep their settings within the specified context.
657
        if ($this->page->context->contextlevel != CONTEXT_COURSE
658
                && $this->page->context->contextlevel != CONTEXT_MODULE
659
                && $this->page->context->contextlevel != CONTEXT_COURSECAT) {
660
            return null;
661
        }
662
 
663
        // These areas have their own code to retrieve added plugin navigation nodes.
664
        if ($activenode->key == 'coursehome' || $activenode->key == 'questionbank' || $activenode->key == 'coursereports') {
665
            return null;
666
        }
667
 
668
        $menunode = $this->page->settingsnav->find($activenode->key, null);
669
 
670
        if (!$menunode instanceof navigation_node) {
671
            return null;
672
        }
673
        // Loop through all children and try and find a match to the current url.
674
        $matchednode = $this->nodes_match_current_url($menunode);
675
        if (is_null($matchednode)) {
676
            return null;
677
        }
678
        if (!isset($menunode) || !$menunode->has_children()) {
679
            return null;
680
        }
681
        $selectdata = static::create_menu_element([$menunode], false);
682
        $urlselect = new url_select($selectdata, $matchednode->action->out(false), null);
683
        $urlselect->set_label(get_string('browsesettingindex', 'course'), ['class' => 'sr-only']);
684
        return $urlselect;
685
    }
686
 
687
    /**
688
     * Get the module's secondary navigation. This is based on settings_nav and would include plugin nodes added via
689
     * '_extend_settings_navigation'.
690
     * It populates the tree based on the nav mockup
691
     *
692
     * If nodes change, we will have to explicitly call the callback again.
693
     *
694
     * @param settings_navigation $settingsnav The settings navigation object related to the module page
695
     * @param navigation_node|null $rootnode The node where the module navigation nodes should be added into as children.
696
     *                                       If not explicitly defined, the nodes will be added to the secondary root
697
     *                                       node by default.
698
     */
699
    protected function load_module_navigation(settings_navigation $settingsnav, ?navigation_node $rootnode = null): void {
700
        $rootnode = $rootnode ?? $this;
701
        $mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING);
702
        $nodes = $this->get_default_module_mapping();
703
 
704
        if ($mainnode) {
705
            $url = new \moodle_url('/mod/' . $settingsnav->get_page()->activityname . '/view.php',
706
                ['id' => $settingsnav->get_page()->cm->id]);
707
            $setactive = $url->compare($settingsnav->get_page()->url, URL_MATCH_BASE);
708
            $node = $rootnode->add(get_string('modulename', $settingsnav->get_page()->activityname), $url,
709
                null, null, 'modulepage');
710
            if ($setactive) {
711
                $node->make_active();
712
            }
713
            // Add the initial nodes.
714
            $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
715
            $this->add_ordered_nodes($nodesordered, $rootnode);
716
 
717
            // We have finished inserting the initial structure.
718
            // Populate the menu with the rest of the nodes available.
719
            $this->load_remaining_nodes($mainnode, $nodes, $rootnode);
720
        }
721
    }
722
 
723
    /**
724
     * Load the course category navigation.
725
     */
726
    protected function load_category_navigation(): void {
727
        $settingsnav = $this->page->settingsnav;
728
        $mainnode = $settingsnav->find('categorysettings', self::TYPE_CONTAINER);
729
        $nodes = $this->get_default_category_mapping();
730
 
731
        if ($mainnode) {
732
            $url = new \moodle_url('/course/index.php', ['categoryid' => $this->context->instanceid]);
733
            $this->add(get_string('category'), $url, self::TYPE_CONTAINER, null, 'categorymain');
734
 
735
            // Add the initial nodes.
736
            $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
737
            $this->add_ordered_nodes($nodesordered);
738
 
739
            // We have finished inserting the initial structure.
740
            // Populate the menu with the rest of the nodes available.
741
            $this->load_remaining_nodes($mainnode, $nodes);
742
        }
743
    }
744
 
745
    /**
746
     * Load the site admin navigation
747
     */
748
    protected function load_admin_navigation(): void {
749
        global $PAGE, $SITE;
750
 
751
        $settingsnav = $this->page->settingsnav;
752
        $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN);
753
        // We need to know if we are on the main site admin search page. Here the navigation between tabs are done via
754
        // anchors and page reload doesn't happen. On every nested admin settings page, the secondary nav needs to
755
        // exist as links with anchors appended in order to redirect back to the admin search page and the corresponding
756
        // tab. Note this value refers to being present on the page itself, before a search has been performed.
757
        $isadminsearchpage = $PAGE->url->compare(new \moodle_url('/admin/search.php', ['query' => '']), URL_MATCH_PARAMS);
758
        if ($node) {
759
            $siteadminnode = $this->add(get_string('general'), "#link$node->key", null, null, 'siteadminnode');
760
            if ($isadminsearchpage) {
761
                $siteadminnode->action = false;
762
                $siteadminnode->tab = "#link$node->key";
763
            } else {
764
                $siteadminnode->action = new \moodle_url("/admin/search.php", [], "link$node->key");
765
            }
766
            foreach ($node->children as $child) {
767
                if ($child->display && !$child->is_short_branch()) {
768
                    // Mimic the current boost behaviour and pass down anchors for the tabs.
769
                    if ($isadminsearchpage) {
770
                        $child->action = false;
771
                        $child->tab = "#link$child->key";
772
                    } else {
773
                        $child->action = new \moodle_url("/admin/search.php", [], "link$child->key");
774
                    }
775
                    $this->add_node(clone $child);
776
                } else {
777
                    $siteadminnode->add_node(clone $child);
778
                }
779
            }
780
        }
781
    }
782
 
783
    /**
784
     * Adds the indexed nodes to the current view or a given node. The key should indicate it's position in the tree.
785
     * Any sub nodes needs to be numbered appropriately, e.g. 3.1 would make the identified node be listed  under #3 node.
786
     *
787
     * @param array $nodes An array of navigation nodes to be added.
788
     * @param navigation_node|null $rootnode The node where the nodes should be added into as children. If not explicitly
789
     *                                       defined, the nodes will be added to the secondary root node by default.
790
     */
791
    protected function add_ordered_nodes(array $nodes, ?navigation_node $rootnode = null): void {
792
        $rootnode = $rootnode ?? $this;
793
        ksort($nodes);
794
        foreach ($nodes as $key => $node) {
795
            // If the key is a string then we are assuming this is a nested element.
796
            if (is_string($key)) {
797
                $parentnode = $nodes[floor($key)] ?? null;
798
                if ($parentnode) {
799
                    $parentnode->add_node(clone $node);
800
                }
801
            } else {
802
                $rootnode->add_node(clone $node);
803
            }
804
        }
805
    }
806
 
807
    /**
808
     * Find the remaining nodes that need to be loaded into secondary based on the current context or a given node.
809
     *
810
     * @param navigation_node $completenode The original node that we are sourcing information from
811
     * @param array           $nodesmap The map used to populate secondary nav in the given context
812
     * @param navigation_node|null $rootnode The node where the remaining nodes should be added into as children. If not
813
     *                                       explicitly defined, the nodes will be added to the secondary root node by
814
     *                                       default.
815
     */
816
    protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap,
817
            ?navigation_node $rootnode = null): void {
818
        $flattenednodes = [];
819
        $rootnode = $rootnode ?? $this;
820
        foreach ($nodesmap as $nodecontainer) {
821
            $flattenednodes = array_merge(array_keys($nodecontainer), $flattenednodes);
822
        }
823
 
824
        $populatedkeys = $this->get_children_key_list();
825
        $existingkeys = $completenode->get_children_key_list();
826
        $leftover = array_diff($existingkeys, $populatedkeys);
827
        foreach ($leftover as $key) {
828
            if (!in_array($key, $flattenednodes, true) && $leftovernode = $completenode->get($key)) {
829
                // Check for nodes with children and potentially no action to direct to.
830
                if ($leftovernode->has_children()) {
831
                    $leftovernode = $this->get_first_action_for_node($leftovernode);
832
                }
833
 
834
                // We have found the first node with an action.
835
                if ($leftovernode) {
836
                    $this->add_external_nodes_to_secondary($leftovernode, $leftovernode, $rootnode);
837
                }
838
            }
839
        }
840
    }
841
 
842
    /**
843
     * Force certain secondary navigation nodes to be displayed in the "more" menu.
844
     *
845
     * @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that
846
     *                                    should be added into the "more" menu by default
847
     * @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation
848
     */
849
    protected function force_nodes_into_more_menu(array $defaultmoremenunodes = [], ?int $maxdisplayednodes = null) {
850
        // Counter of the navigation nodes that are initially displayed in the secondary nav
851
        // (excludes the nodes from the "more" menu).
852
        $displayednodescount = 0;
853
        foreach ($this->children as $child) {
854
            // Skip if the navigation node has been already forced into the "more" menu.
855
            if ($child->forceintomoremenu) {
856
                continue;
857
            }
858
            // If the navigation node is in the pre-defined list of nodes that should be added by default in the
859
            // "more" menu or the maximum limit of displayed navigation nodes has been reached (if defined).
860
            if (in_array($child->key, $defaultmoremenunodes) ||
861
                    (!is_null($maxdisplayednodes) && $displayednodescount >= $maxdisplayednodes)) {
862
                // Force the node and its children into the "more" menu.
863
                $child->set_force_into_more_menu(true);
864
                continue;
865
            }
866
            $displayednodescount++;
867
        }
868
    }
869
 
870
    /**
871
     * Recursively remove navigation nodes that should not be displayed in the secondary navigation.
872
     *
873
     * @param navigation_node $node The starting navigation node.
874
     */
875
    protected function remove_unwanted_nodes(navigation_node $node) {
876
        foreach ($node->children as $child) {
877
            if (!$child->showinsecondarynavigation) {
878
                $child->remove();
879
                continue;
880
            }
881
            if (!empty($child->children)) {
882
                $this->remove_unwanted_nodes($child);
883
            }
884
        }
885
    }
886
 
887
    /**
888
     * Takes the given navigation nodes and searches for children and formats it all into an array in a format to be used by a
889
     * url_select element.
890
     *
891
     * @param navigation_node[] $navigationnodes Navigation nodes to format into a menu.
892
     * @param bool $forceheadings Whether the returned array should be forced to use headings.
893
     * @return array|null A url select element for navigating through the navigation nodes.
894
     */
895
    public static function create_menu_element(array $navigationnodes, bool $forceheadings = false): ?array {
896
        if (empty($navigationnodes)) {
897
            return null;
898
        }
899
 
900
        // If one item, do we put this into a url_select?
901
        if (count($navigationnodes) < 2) {
902
            // Check if there are children.
903
            $navnode = array_shift($navigationnodes);
904
            $menudata = [];
905
            if (!$navnode->has_children()) {
906
                // Just one item.
907
                if (!$navnode->has_action()) {
908
                    return null;
909
                }
910
                $menudata[$navnode->action->out(false)] = static::format_node_text($navnode);
911
            } else {
912
                if (static::does_menu_need_headings($navnode) || $forceheadings) {
913
                    // Let's do headings.
914
                    $menudata = static::get_headings_nav_array($navnode);
915
                } else {
916
                    // Simple flat nav.
917
                    $menudata = static::get_flat_nav_array($navnode);
918
                }
919
            }
920
            return $menudata;
921
        } else {
922
            // We have more than one navigation node to handle. Put each node in it's own heading.
923
            $menudata = [];
924
            $titledata = [];
925
            foreach ($navigationnodes as $navigationnode) {
926
                if ($navigationnode->has_children()) {
927
                    $menuarray = [];
928
                    // Add a heading and flatten out everything else.
929
                    if ($navigationnode->has_action()) {
930
                        $menuarray[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
931
                            static::format_node_text($navigationnode);
932
                        $menuarray[static::format_node_text($navigationnode)] += static::get_whole_tree_flat($navigationnode);
933
                    } else {
934
                        $menuarray[static::format_node_text($navigationnode)] = static::get_whole_tree_flat($navigationnode);
935
                    }
936
 
937
                    $titledata += $menuarray;
938
                } else {
939
                    // Add with no heading.
940
                    if (!$navigationnode->has_action()) {
941
                        return null;
942
                    }
943
                    $menudata[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
944
                }
945
            }
946
            $menudata += [$titledata];
947
            return $menudata;
948
        }
949
    }
950
 
951
    /**
952
     * Recursively goes through the provided navigation node and returns a flat version.
953
     *
954
     * @param navigation_node $navigationnode The navigationnode.
955
     * @return array The whole tree flat.
956
     */
957
    protected static function get_whole_tree_flat(navigation_node $navigationnode): array {
958
        $nodes = [];
959
        foreach ($navigationnode->children as $child) {
960
            if ($child->has_action()) {
961
                $nodes[$child->action->out()] = $child->text;
962
            }
963
            if ($child->has_children()) {
964
                $childnodes = static::get_whole_tree_flat($child);
965
                $nodes = array_merge($nodes, $childnodes);
966
            }
967
        }
968
        return $nodes;
969
    }
970
 
971
    /**
972
     * Checks to see if the provided navigation node has children and determines if we want headings for a url select element.
973
     *
974
     * @param navigation_node  $navigationnode  The navigation node we are checking.
975
     * @return bool Whether we want headings or not.
976
     */
977
    protected static function does_menu_need_headings(navigation_node $navigationnode): bool {
978
        if (!$navigationnode->has_children()) {
979
            return false;
980
        }
981
        foreach ($navigationnode->children as $child) {
982
            if ($child->has_children()) {
983
                return true;
984
            }
985
        }
986
        return false;
987
    }
988
 
989
    /**
990
     * Takes the navigation node and returns it in a flat fashion. This is not recursive.
991
     *
992
     * @param navigation_node $navigationnode The navigation node that we want to format into an array in a flat structure.
993
     * @return array The flat navigation array.
994
     */
995
    protected static function get_flat_nav_array(navigation_node $navigationnode): array {
996
        $menuarray = [];
997
        if ($navigationnode->has_action()) {
998
            $menuarray[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
999
        }
1000
 
1001
        foreach ($navigationnode->children as $child) {
1002
            if ($child->has_action()) {
1003
                $menuarray[$child->action->out(false)] = static::format_node_text($child);
1004
            }
1005
        }
1006
        return $menuarray;
1007
    }
1008
 
1009
    /**
1010
     * For any navigation node that we have determined needs headings we return a more tree like array structure.
1011
     *
1012
     * @param navigation_node $navigationnode The navigation node to use for the formatted array structure.
1013
     * @return array The headings navigation array structure.
1014
     */
1015
    protected static function get_headings_nav_array(navigation_node $navigationnode): array {
1016
        $menublock = [];
1017
        // We know that this single node has headings, so grab this for the first heading.
1018
        $firstheading = [];
1019
        if ($navigationnode->has_action()) {
1020
            $firstheading[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
1021
                static::format_node_text($navigationnode);
1022
            $firstheading[static::format_node_text($navigationnode)] += static::get_more_child_nodes($navigationnode, $menublock);
1023
        } else {
1024
            $firstheading[static::format_node_text($navigationnode)] = static::get_more_child_nodes($navigationnode, $menublock);
1025
        }
1026
         return [$firstheading + $menublock];
1027
    }
1028
 
1029
    /**
1030
     * Recursively goes and gets all children nodes.
1031
     *
1032
     * @param navigation_node $node The node to get the children of.
1033
     * @param array $menublock Used to put all child nodes in its own container.
1034
     * @return array The additional child nodes.
1035
     */
1036
    protected static function get_more_child_nodes(navigation_node $node, array &$menublock): array {
1037
        $nodes = [];
1038
        foreach ($node->children as $child) {
1039
            if (!$child->has_children()) {
1040
                if (!$child->has_action()) {
1041
                    continue;
1042
                }
1043
                $nodes[$child->action->out(false)] = static::format_node_text($child);
1044
            } else {
1045
                $newarray = [];
1046
                if ($child->has_action()) {
1047
                    $newarray[static::format_node_text($child)][$child->action->out(false)] = static::format_node_text($child);
1048
                    $newarray[static::format_node_text($child)] += static::get_more_child_nodes($child, $menublock);
1049
                } else {
1050
                    $newarray[static::format_node_text($child)] = static::get_more_child_nodes($child, $menublock);
1051
                }
1052
                $menublock += $newarray;
1053
            }
1054
        }
1055
        return $nodes;
1056
    }
1057
 
1058
    /**
1059
     * Returns the navigation node text in a string.
1060
     *
1061
     * @param navigation_node $navigationnode The navigationnode to return the text string of.
1062
     * @return string The navigation node text string.
1063
     */
1064
    protected static function format_node_text(navigation_node $navigationnode): string {
1065
        return (is_a($navigationnode->text, 'lang_string')) ? $navigationnode->text->out() : $navigationnode->text;
1066
    }
1067
 
1068
    /**
1069
     * Load the single activity course secondary navigation.
1070
     */
1071
    protected function load_single_activity_course_navigation(): void {
1072
        $page = $this->page;
1073
        $course = $page->course;
1074
 
1075
        // Create 'Course' navigation node.
1076
        $coursesecondarynode = navigation_node::create(get_string('course'), null, self::TYPE_COURSE, null, 'course');
1077
        $this->load_course_navigation($coursesecondarynode);
1078
        // Remove the unnecessary 'Course' child node generated in load_course_navigation().
1079
        $coursehomenode = $coursesecondarynode->find('coursehome', self::TYPE_COURSE);
1080
        if (!empty($coursehomenode)) {
1081
            $coursehomenode->remove();
1082
        }
1083
 
1084
        // Add the 'Course' node to the secondary navigation only if this node has children nodes.
1085
        if (count($coursesecondarynode->children) > 0) {
1086
            $this->add_node($coursesecondarynode);
1087
            // Once all the items have been added to the 'Course' secondary navigation node, set the 'showchildreninsubmenu'
1088
            // property to true. This is required to force the template to output these items within a dropdown menu.
1089
            $coursesecondarynode->showchildreninsubmenu = true;
1090
        }
1091
 
1092
        // Create 'Activity' navigation node.
1093
        $activitysecondarynode = navigation_node::create(get_string('activity'), null, self::TYPE_ACTIVITY, null, 'activity');
1094
 
1095
        // We should display the module related navigation in the course context as well. Therefore, we need to
1096
        // re-initialize the page object and manually set the course module to the one that it is currently visible in
1097
        // the course in order to obtain the required module settings navigation.
1098
        if ($page->context instanceof \context_course) {
1099
            $this->page->set_secondary_active_tab($coursesecondarynode->key);
1100
            // Get the currently used module in the single activity course.
1101
            $module = current(array_filter(get_course_mods($course->id), function ($module) {
1102
                return $module->visible == 1;
1103
            }));
1104
            // If the default module for the single course format has not been set yet, skip displaying the module
1105
            // related navigation in the secondary navigation.
1106
            if (!$module) {
1107
                return;
1108
            }
1109
            $page = new \moodle_page();
1110
            $page->set_cm($module, $course);
1111
            $page->set_url(new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]));
1112
        }
1113
 
1114
        $this->load_module_navigation($page->settingsnav, $activitysecondarynode);
1115
 
1116
        // Add the 'Activity' node to the secondary navigation only if this node has more that one child node.
1117
        if (count($activitysecondarynode->children) > 1) {
1118
            // Set the 'showchildreninsubmenu' property to true to later output the the module navigation items within
1119
            // a dropdown menu.
1120
            $activitysecondarynode->showchildreninsubmenu = true;
1121
            $this->add_node($activitysecondarynode);
1122
            if ($this->context instanceof \context_module) {
1123
                $this->page->set_secondary_active_tab($activitysecondarynode->key);
1124
            }
1125
        } else { // Otherwise, add the 'View activity' node to the secondary navigation.
1126
            $viewactivityurl = new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]);
1127
            $this->add(get_string('modulename', $page->activityname), $viewactivityurl, null, null, 'modulepage');
1128
            if ($this->context instanceof \context_module) {
1129
                $this->page->set_secondary_active_tab('modulepage');
1130
            }
1131
        }
1132
    }
1133
}