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
/**
18
 * This file contains classes used to manage the navigation structures within Moodle.
19
 *
20
 * @since      Moodle 2.0
21
 * @package    core
22
 * @copyright  2009 Sam Hemelryk
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
use core\moodlenet\utilities;
27
use core_contentbank\contentbank;
28
 
29
defined('MOODLE_INTERNAL') || die();
30
 
31
/**
32
 * The name that will be used to separate the navigation cache within SESSION
33
 */
34
define('NAVIGATION_CACHE_NAME', 'navigation');
35
define('NAVIGATION_SITE_ADMIN_CACHE_NAME', 'navigationsiteadmin');
36
 
37
/**
38
 * This class is used to represent a node in a navigation tree
39
 *
40
 * This class is used to represent a node in a navigation tree within Moodle,
41
 * the tree could be one of global navigation, settings navigation, or the navbar.
42
 * Each node can be one of two types either a Leaf (default) or a branch.
43
 * When a node is first created it is created as a leaf, when/if children are added
44
 * the node then becomes a branch.
45
 *
46
 * @package   core
47
 * @category  navigation
48
 * @copyright 2009 Sam Hemelryk
49
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
50
 */
51
class navigation_node implements renderable {
52
    /** @var int Used to identify this node a leaf (default) 0 */
53
    const NODETYPE_LEAF =   0;
54
    /** @var int Used to identify this node a branch, happens with children  1 */
55
    const NODETYPE_BRANCH = 1;
56
    /** @var null Unknown node type null */
57
    const TYPE_UNKNOWN =    null;
58
    /** @var int System node type 0 */
59
    const TYPE_ROOTNODE =   0;
60
    /** @var int System node type 1 */
61
    const TYPE_SYSTEM =     1;
62
    /** @var int Category node type 10 */
63
    const TYPE_CATEGORY =   10;
64
    /** var int Category displayed in MyHome navigation node */
65
    const TYPE_MY_CATEGORY = 11;
66
    /** @var int Course node type 20 */
67
    const TYPE_COURSE =     20;
68
    /** @var int Course Structure node type 30 */
69
    const TYPE_SECTION =    30;
70
    /** @var int Activity node type, e.g. Forum, Quiz 40 */
71
    const TYPE_ACTIVITY =   40;
72
    /** @var int Resource node type, e.g. Link to a file, or label 50 */
73
    const TYPE_RESOURCE =   50;
74
    /** @var int A custom node type, default when adding without specifing type 60 */
75
    const TYPE_CUSTOM =     60;
76
    /** @var int Setting node type, used only within settings nav 70 */
77
    const TYPE_SETTING =    70;
78
    /** @var int site admin branch node type, used only within settings nav 71 */
79
    const TYPE_SITE_ADMIN = 71;
80
    /** @var int Setting node type, used only within settings nav 80 */
81
    const TYPE_USER =       80;
82
    /** @var int Setting node type, used for containers of no importance 90 */
83
    const TYPE_CONTAINER =  90;
84
    /** var int Course the current user is not enrolled in */
85
    const COURSE_OTHER = 0;
86
    /** var int Course the current user is enrolled in but not viewing */
87
    const COURSE_MY = 1;
88
    /** var int Course the current user is currently viewing */
89
    const COURSE_CURRENT = 2;
90
    /** var string The course index page navigation node */
91
    const COURSE_INDEX_PAGE = 'courseindexpage';
92
 
93
    /** @var int Parameter to aid the coder in tracking [optional] */
94
    public $id = null;
95
    /** @var string|int The identifier for the node, used to retrieve the node */
96
    public $key = null;
97
    /** @var string|lang_string The text to use for the node */
98
    public $text = null;
99
    /** @var string Short text to use if requested [optional] */
100
    public $shorttext = null;
101
    /** @var string The title attribute for an action if one is defined */
102
    public $title = null;
103
    /** @var string A string that can be used to build a help button */
104
    public $helpbutton = null;
105
    /** @var moodle_url|action_link|null An action for the node (link) */
106
    public $action = null;
107
    /** @var pix_icon The path to an icon to use for this node */
108
    public $icon = null;
109
    /** @var int See TYPE_* constants defined for this class */
110
    public $type = self::TYPE_UNKNOWN;
111
    /** @var int See NODETYPE_* constants defined for this class */
112
    public $nodetype = self::NODETYPE_LEAF;
113
    /** @var bool If set to true the node will be collapsed by default */
114
    public $collapse = false;
115
    /** @var bool If set to true the node will be expanded by default */
116
    public $forceopen = false;
117
    /** @var array An array of CSS classes for the node */
118
    public $classes = array();
119
    /** @var array An array of HTML attributes for the node */
120
    public $attributes = [];
121
    /** @var navigation_node_collection An array of child nodes */
122
    public $children = array();
123
    /** @var bool If set to true the node will be recognised as active */
124
    public $isactive = false;
125
    /** @var bool If set to true the node will be dimmed */
126
    public $hidden = false;
127
    /** @var bool If set to false the node will not be displayed */
128
    public $display = true;
129
    /** @var bool If set to true then an HR will be printed before the node */
130
    public $preceedwithhr = false;
131
    /** @var bool If set to true the the navigation bar should ignore this node */
132
    public $mainnavonly = false;
133
    /** @var bool If set to true a title will be added to the action no matter what */
134
    public $forcetitle = false;
135
    /** @var navigation_node A reference to the node parent, you should never set this directly you should always call set_parent */
136
    public $parent = null;
137
    /** @var bool Override to not display the icon even if one is provided **/
138
    public $hideicon = false;
139
    /** @var bool Set to true if we KNOW that this node can be expanded.  */
140
    public $isexpandable = false;
141
    /** @var array */
142
    protected $namedtypes = array(0 => 'system', 10 => 'category', 20 => 'course', 30 => 'structure', 40 => 'activity',
143
                                  50 => 'resource', 60 => 'custom', 70 => 'setting', 71 => 'siteadmin', 80 => 'user',
144
                                  90 => 'container');
145
    /** @var moodle_url */
146
    protected static $fullmeurl = null;
147
    /** @var bool toogles auto matching of active node */
148
    public static $autofindactive = true;
149
    /** @var bool should we load full admin tree or rely on AJAX for performance reasons */
150
    protected static $loadadmintree = false;
151
    /** @var mixed If set to an int, that section will be included even if it has no activities */
152
    public $includesectionnum = false;
153
    /** @var bool does the node need to be loaded via ajax */
154
    public $requiresajaxloading = false;
155
    /** @var bool If set to true this node will be added to the "flat" navigation */
156
    public $showinflatnavigation = false;
157
    /** @var bool If set to true this node will be forced into a "more" menu whenever possible */
158
    public $forceintomoremenu = false;
159
    /** @var bool If set to true this node will be displayed in the "secondary" navigation when applicable */
160
    public $showinsecondarynavigation = true;
161
    /** @var bool If set to true the children of this node will be displayed within a submenu when applicable */
162
    public $showchildreninsubmenu = false;
163
    /** @var string tab element ID. */
164
    public $tab;
165
    /** @var string unique identifier. */
166
    public $moremenuid;
167
    /** @var bool node that have children. */
168
    public $haschildren;
169
 
170
    /**
171
     * Constructs a new navigation_node
172
     *
173
     * @param array|string $properties Either an array of properties or a string to use
174
     *                     as the text for the node
175
     */
176
    public function __construct($properties) {
177
        if (is_array($properties)) {
178
            // Check the array for each property that we allow to set at construction.
179
            // text         - The main content for the node
180
            // shorttext    - A short text if required for the node
181
            // icon         - The icon to display for the node
182
            // type         - The type of the node
183
            // key          - The key to use to identify the node
184
            // parent       - A reference to the nodes parent
185
            // action       - The action to attribute to this node, usually a URL to link to
186
            if (array_key_exists('text', $properties)) {
187
                $this->text = $properties['text'];
188
            }
189
            if (array_key_exists('shorttext', $properties)) {
190
                $this->shorttext = $properties['shorttext'];
191
            }
192
            if (!array_key_exists('icon', $properties)) {
193
                $properties['icon'] = new pix_icon('i/navigationitem', '');
194
            }
195
            $this->icon = $properties['icon'];
196
            if ($this->icon instanceof pix_icon) {
197
                if (empty($this->icon->attributes['class'])) {
198
                    $this->icon->attributes['class'] = 'navicon';
199
                } else {
200
                    $this->icon->attributes['class'] .= ' navicon';
201
                }
202
            }
203
            if (array_key_exists('type', $properties)) {
204
                $this->type = $properties['type'];
205
            } else {
206
                $this->type = self::TYPE_CUSTOM;
207
            }
208
            if (array_key_exists('key', $properties)) {
209
                $this->key = $properties['key'];
210
            }
211
            // This needs to happen last because of the check_if_active call that occurs
212
            if (array_key_exists('action', $properties)) {
213
                $this->action = $properties['action'];
214
                if (is_string($this->action)) {
215
                    $this->action = new moodle_url($this->action);
216
                }
217
                if (self::$autofindactive) {
218
                    $this->check_if_active();
219
                }
220
            }
221
            if (array_key_exists('parent', $properties)) {
222
                $this->set_parent($properties['parent']);
223
            }
224
        } else if (is_string($properties)) {
225
            $this->text = $properties;
226
        }
227
        if ($this->text === null) {
228
            throw new coding_exception('You must set the text for the node when you create it.');
229
        }
230
        // Instantiate a new navigation node collection for this nodes children
231
        $this->children = new navigation_node_collection();
232
    }
233
 
234
    /**
235
     * Checks if this node is the active node.
236
     *
237
     * This is determined by comparing the action for the node against the
238
     * defined URL for the page. A match will see this node marked as active.
239
     *
240
     * @param int $strength One of URL_MATCH_EXACT, URL_MATCH_PARAMS, or URL_MATCH_BASE
241
     * @return bool
242
     */
243
    public function check_if_active($strength=URL_MATCH_EXACT) {
244
        global $FULLME, $PAGE;
245
        // Set fullmeurl if it hasn't already been set
246
        if (self::$fullmeurl == null) {
247
            if ($PAGE->has_set_url()) {
248
                self::override_active_url(new moodle_url($PAGE->url));
249
            } else {
250
                self::override_active_url(new moodle_url($FULLME));
251
            }
252
        }
253
 
254
        // Compare the action of this node against the fullmeurl
255
        if ($this->action instanceof moodle_url && $this->action->compare(self::$fullmeurl, $strength)) {
256
            $this->make_active();
257
            return true;
258
        }
259
        return false;
260
    }
261
 
262
    /**
263
     * True if this nav node has siblings in the tree.
264
     *
265
     * @return bool
266
     */
267
    public function has_siblings() {
268
        if (empty($this->parent) || empty($this->parent->children)) {
269
            return false;
270
        }
271
        if ($this->parent->children instanceof navigation_node_collection) {
272
            $count = $this->parent->children->count();
273
        } else {
274
            $count = count($this->parent->children);
275
        }
276
        return ($count > 1);
277
    }
278
 
279
    /**
280
     * Get a list of sibling navigation nodes at the same level as this one.
281
     *
282
     * @return bool|array of navigation_node
283
     */
284
    public function get_siblings() {
285
        // Returns a list of the siblings of the current node for display in a flat navigation element. Either
286
        // the in-page links or the breadcrumb links.
287
        $siblings = false;
288
 
289
        if ($this->has_siblings()) {
290
            $siblings = [];
291
            foreach ($this->parent->children as $child) {
292
                if ($child->display) {
293
                    $siblings[] = $child;
294
                }
295
            }
296
        }
297
        return $siblings;
298
    }
299
 
300
    /**
301
     * This sets the URL that the URL of new nodes get compared to when locating
302
     * the active node.
303
     *
304
     * The active node is the node that matches the URL set here. By default this
305
     * is either $PAGE->url or if that hasn't been set $FULLME.
306
     *
307
     * @param moodle_url $url The url to use for the fullmeurl.
308
     * @param bool $loadadmintree use true if the URL point to administration tree
309
     */
310
    public static function override_active_url(moodle_url $url, $loadadmintree = false) {
311
        // Clone the URL, in case the calling script changes their URL later.
312
        self::$fullmeurl = new moodle_url($url);
313
        // True means we do not want AJAX loaded admin tree, required for all admin pages.
314
        if ($loadadmintree) {
315
            // Do not change back to false if already set.
316
            self::$loadadmintree = true;
317
        }
318
    }
319
 
320
    /**
321
     * Use when page is linked from the admin tree,
322
     * if not used navigation could not find the page using current URL
323
     * because the tree is not fully loaded.
324
     */
325
    public static function require_admin_tree() {
326
        self::$loadadmintree = true;
327
    }
328
 
329
    /**
330
     * Creates a navigation node, ready to add it as a child using add_node
331
     * function. (The created node needs to be added before you can use it.)
332
     * @param string $text
333
     * @param moodle_url|action_link $action
334
     * @param int $type
335
     * @param string $shorttext
336
     * @param string|int $key
337
     * @param pix_icon $icon
338
     * @return navigation_node
339
     */
340
    public static function create($text, $action=null, $type=self::TYPE_CUSTOM,
341
            $shorttext=null, $key=null, pix_icon $icon=null) {
342
        if ($action && !($action instanceof moodle_url || $action instanceof action_link)) {
343
            debugging(
344
                "It is required that the action provided be either an action_url|moodle_url." .
345
                " Please update your definition.", E_NOTICE);
346
        }
347
        // Properties array used when creating the new navigation node
348
        $itemarray = array(
349
            'text' => $text,
350
            'type' => $type
351
        );
352
        // Set the action if one was provided
353
        if ($action!==null) {
354
            $itemarray['action'] = $action;
355
        }
356
        // Set the shorttext if one was provided
357
        if ($shorttext!==null) {
358
            $itemarray['shorttext'] = $shorttext;
359
        }
360
        // Set the icon if one was provided
361
        if ($icon!==null) {
362
            $itemarray['icon'] = $icon;
363
        }
364
        // Set the key
365
        $itemarray['key'] = $key;
366
        // Construct and return
367
        return new navigation_node($itemarray);
368
    }
369
 
370
    /**
371
     * Adds a navigation node as a child of this node.
372
     *
373
     * @param string $text
374
     * @param moodle_url|action_link|string $action
375
     * @param ?int $type
376
     * @param string $shorttext
377
     * @param string|int $key
378
     * @param pix_icon $icon
379
     * @return navigation_node
380
     */
381
    public function add($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=null, $key=null, pix_icon $icon=null) {
382
        if ($action && is_string($action)) {
383
            $action = new moodle_url($action);
384
        }
385
        // Create child node
386
        $childnode = self::create($text, $action, $type, $shorttext, $key, $icon);
387
 
388
        // Add the child to end and return
389
        return $this->add_node($childnode);
390
    }
391
 
392
    /**
393
     * Adds a navigation node as a child of this one, given a $node object
394
     * created using the create function.
395
     * @param navigation_node $childnode Node to add
396
     * @param string $beforekey
397
     * @return navigation_node The added node
398
     */
399
    public function add_node(navigation_node $childnode, $beforekey=null) {
400
        // First convert the nodetype for this node to a branch as it will now have children
401
        if ($this->nodetype !== self::NODETYPE_BRANCH) {
402
            $this->nodetype = self::NODETYPE_BRANCH;
403
        }
404
        // Set the parent to this node
405
        $childnode->set_parent($this);
406
 
407
        // Default the key to the number of children if not provided
408
        if ($childnode->key === null) {
409
            $childnode->key = $this->children->count();
410
        }
411
 
412
        // Add the child using the navigation_node_collections add method
413
        $node = $this->children->add($childnode, $beforekey);
414
 
415
        // If added node is a category node or the user is logged in and it's a course
416
        // then mark added node as a branch (makes it expandable by AJAX)
417
        $type = $childnode->type;
418
        if (($type == self::TYPE_CATEGORY) || (isloggedin() && ($type == self::TYPE_COURSE)) || ($type == self::TYPE_MY_CATEGORY) ||
419
                ($type === self::TYPE_SITE_ADMIN)) {
420
            $node->nodetype = self::NODETYPE_BRANCH;
421
        }
422
        // If this node is hidden mark it's children as hidden also
423
        if ($this->hidden) {
424
            $node->hidden = true;
425
        }
426
        // Return added node (reference returned by $this->children->add()
427
        return $node;
428
    }
429
 
430
    /**
431
     * Return a list of all the keys of all the child nodes.
432
     * @return array the keys.
433
     */
434
    public function get_children_key_list() {
435
        return $this->children->get_key_list();
436
    }
437
 
438
    /**
439
     * Searches for a node of the given type with the given key.
440
     *
441
     * This searches this node plus all of its children, and their children....
442
     * If you know the node you are looking for is a child of this node then please
443
     * use the get method instead.
444
     *
445
     * @param int|string $key The key of the node we are looking for
446
     * @param ?int $type One of navigation_node::TYPE_*
447
     * @return navigation_node|false
448
     */
449
    public function find($key, $type) {
450
        return $this->children->find($key, $type);
451
    }
452
 
453
    /**
454
     * Walk the tree building up a list of all the flat navigation nodes.
455
     *
456
     * @deprecated since Moodle 4.0
457
     * @param flat_navigation $nodes List of the found flat navigation nodes.
458
     * @param boolean $showdivider Show a divider before the first node.
459
     * @param string $label A label for the collection of navigation links.
460
     */
461
    public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false, $label = '') {
462
        debugging("Function has been deprecated with the deprecation of the flat_navigation class.");
463
        if ($this->showinflatnavigation) {
464
            $indent = 0;
465
            if ($this->type == self::TYPE_COURSE || $this->key === self::COURSE_INDEX_PAGE) {
466
                $indent = 1;
467
            }
468
            $flat = new flat_navigation_node($this, $indent);
469
            $flat->set_showdivider($showdivider, $label);
470
            $nodes->add($flat);
471
        }
472
        foreach ($this->children as $child) {
473
            $child->build_flat_navigation_list($nodes, false);
474
        }
475
    }
476
 
477
    /**
478
     * Get the child of this node that has the given key + (optional) type.
479
     *
480
     * If you are looking for a node and want to search all children + their children
481
     * then please use the find method instead.
482
     *
483
     * @param int|string $key The key of the node we are looking for
484
     * @param int $type One of navigation_node::TYPE_*
485
     * @return navigation_node|false
486
     */
487
    public function get($key, $type=null) {
488
        return $this->children->get($key, $type);
489
    }
490
 
491
    /**
492
     * Removes this node.
493
     *
494
     * @return bool
495
     */
496
    public function remove() {
497
        return $this->parent->children->remove($this->key, $this->type);
498
    }
499
 
500
    /**
501
     * Checks if this node has or could have any children
502
     *
503
     * @return bool Returns true if it has children or could have (by AJAX expansion)
504
     */
505
    public function has_children() {
506
        return ($this->nodetype === navigation_node::NODETYPE_BRANCH || $this->children->count()>0 || $this->isexpandable);
507
    }
508
 
509
    /**
510
     * Marks this node as active and forces it open.
511
     *
512
     * Important: If you are here because you need to mark a node active to get
513
     * the navigation to do what you want have you looked at {@link navigation_node::override_active_url()}?
514
     * You can use it to specify a different URL to match the active navigation node on
515
     * rather than having to locate and manually mark a node active.
516
     */
517
    public function make_active() {
518
        $this->isactive = true;
519
        $this->add_class('active_tree_node');
520
        $this->force_open();
521
        if ($this->parent !== null) {
522
            $this->parent->make_inactive();
523
        }
524
    }
525
 
526
    /**
527
     * Marks a node as inactive and recusised back to the base of the tree
528
     * doing the same to all parents.
529
     */
530
    public function make_inactive() {
531
        $this->isactive = false;
532
        $this->remove_class('active_tree_node');
533
        if ($this->parent !== null) {
534
            $this->parent->make_inactive();
535
        }
536
    }
537
 
538
    /**
539
     * Forces this node to be open and at the same time forces open all
540
     * parents until the root node.
541
     *
542
     * Recursive.
543
     */
544
    public function force_open() {
545
        $this->forceopen = true;
546
        if ($this->parent !== null) {
547
            $this->parent->force_open();
548
        }
549
    }
550
 
551
    /**
552
     * Adds a CSS class to this node.
553
     *
554
     * @param string $class
555
     * @return bool
556
     */
557
    public function add_class($class) {
558
        if (!in_array($class, $this->classes)) {
559
            $this->classes[] = $class;
560
        }
561
        return true;
562
    }
563
 
564
    /**
565
     * Adds an HTML attribute to this node.
566
     *
567
     * @param string $name
568
     * @param string $value
569
     */
570
    public function add_attribute(string $name, string $value): void {
571
        $this->attributes[] = ['name' => $name, 'value' => $value];
572
    }
573
 
574
    /**
575
     * Removes a CSS class from this node.
576
     *
577
     * @param string $class
578
     * @return bool True if the class was successfully removed.
579
     */
580
    public function remove_class($class) {
581
        if (in_array($class, $this->classes)) {
582
            $key = array_search($class,$this->classes);
583
            if ($key!==false) {
584
                // Remove the class' array element.
585
                unset($this->classes[$key]);
586
                // Reindex the array to avoid failures when the classes array is iterated later in mustache templates.
587
                $this->classes = array_values($this->classes);
588
 
589
                return true;
590
            }
591
        }
592
        return false;
593
    }
594
 
595
    /**
596
     * Sets the title for this node and forces Moodle to utilise it.
597
     *
598
     * Note that this method is named identically to the public "title" property of the class, which unfortunately confuses
599
     * our Mustache renderer, because it will see the method and try and call it without any arguments (hence must be nullable)
600
     * before trying to access the public property
601
     *
602
     * @param string|null $title
603
     * @return string
604
     */
605
    public function title(?string $title = null): string {
606
        if ($title !== null) {
607
            $this->title = $title;
608
            $this->forcetitle = true;
609
        }
610
        return (string) $this->title;
611
    }
612
 
613
    /**
614
     * Resets the page specific information on this node if it is being unserialised.
615
     */
616
    public function __wakeup(){
617
        $this->forceopen = false;
618
        $this->isactive = false;
619
        $this->remove_class('active_tree_node');
620
    }
621
 
622
    /**
623
     * Checks if this node or any of its children contain the active node.
624
     *
625
     * Recursive.
626
     *
627
     * @return bool
628
     */
629
    public function contains_active_node() {
630
        if ($this->isactive) {
631
            return true;
632
        } else {
633
            foreach ($this->children as $child) {
634
                if ($child->isactive || $child->contains_active_node()) {
635
                    return true;
636
                }
637
            }
638
        }
639
        return false;
640
    }
641
 
642
    /**
643
     * To better balance the admin tree, we want to group all the short top branches together.
644
     *
645
     * This means < 8 nodes and no subtrees.
646
     *
647
     * @return bool
648
     */
649
    public function is_short_branch() {
650
        $limit = 8;
651
        if ($this->children->count() >= $limit) {
652
            return false;
653
        }
654
        foreach ($this->children as $child) {
655
            if ($child->has_children()) {
656
                return false;
657
            }
658
        }
659
        return true;
660
    }
661
 
662
    /**
663
     * Finds the active node.
664
     *
665
     * Searches this nodes children plus all of the children for the active node
666
     * and returns it if found.
667
     *
668
     * Recursive.
669
     *
670
     * @return navigation_node|false
671
     */
672
    public function find_active_node() {
673
        if ($this->isactive) {
674
            return $this;
675
        } else {
676
            foreach ($this->children as &$child) {
677
                $outcome = $child->find_active_node();
678
                if ($outcome !== false) {
679
                    return $outcome;
680
                }
681
            }
682
        }
683
        return false;
684
    }
685
 
686
    /**
687
     * Searches all children for the best matching active node
688
     * @param int $strength The url match to be made.
689
     * @return navigation_node|false
690
     */
691
    public function search_for_active_node($strength = URL_MATCH_BASE) {
692
        if ($this->check_if_active($strength)) {
693
            return $this;
694
        } else {
695
            foreach ($this->children as &$child) {
696
                $outcome = $child->search_for_active_node($strength);
697
                if ($outcome !== false) {
698
                    return $outcome;
699
                }
700
            }
701
        }
702
        return false;
703
    }
704
 
705
    /**
706
     * Gets the content for this node.
707
     *
708
     * @param bool $shorttext If true shorttext is used rather than the normal text
709
     * @return string
710
     */
711
    public function get_content($shorttext=false) {
712
        $navcontext = \context_helper::get_navigation_filter_context(null);
713
        $options = !empty($navcontext) ? ['context' => $navcontext] : null;
714
 
715
        if ($shorttext && $this->shorttext!==null) {
716
            return format_string($this->shorttext, null, $options);
717
        } else {
718
            return format_string($this->text, null, $options);
719
        }
720
    }
721
 
722
    /**
723
     * Gets the title to use for this node.
724
     *
725
     * @return string
726
     */
727
    public function get_title() {
728
        if ($this->forcetitle || $this->action != null){
729
            return $this->title;
730
        } else {
731
            return '';
732
        }
733
    }
734
 
735
    /**
736
     * Used to easily determine if this link in the breadcrumbs has a valid action/url.
737
     *
738
     * @return boolean
739
     */
740
    public function has_action() {
741
        return !empty($this->action);
742
    }
743
 
744
    /**
745
     * Used to easily determine if the action is an internal link.
746
     *
747
     * @return bool
748
     */
749
    public function has_internal_action(): bool {
750
        global $CFG;
751
        if ($this->has_action()) {
752
            $url = $this->action();
753
            if ($this->action() instanceof \action_link) {
754
                $url = $this->action()->url;
755
            }
756
 
757
            if (($url->out() === $CFG->wwwroot) || (strpos($url->out(), $CFG->wwwroot.'/') === 0)) {
758
                return true;
759
            }
760
        }
761
        return false;
762
    }
763
 
764
    /**
765
     * Used to easily determine if this link in the breadcrumbs is hidden.
766
     *
767
     * @return boolean
768
     */
769
    public function is_hidden() {
770
        return $this->hidden;
771
    }
772
 
773
    /**
774
     * Gets the CSS class to add to this node to describe its type
775
     *
776
     * @return string
777
     */
778
    public function get_css_type() {
779
        if (array_key_exists($this->type, $this->namedtypes)) {
780
            return 'type_'.$this->namedtypes[$this->type];
781
        }
782
        return 'type_unknown';
783
    }
784
 
785
    /**
786
     * Finds all nodes that are expandable by AJAX
787
     *
788
     * @param array $expandable An array by reference to populate with expandable nodes.
789
     */
790
    public function find_expandable(array &$expandable) {
791
        foreach ($this->children as &$child) {
792
            if ($child->display && $child->has_children() && $child->children->count() == 0) {
793
                $child->id = 'expandable_branch_'.$child->type.'_'.clean_param($child->key, PARAM_ALPHANUMEXT);
794
                $this->add_class('canexpand');
795
                $child->requiresajaxloading = true;
796
                $expandable[] = array('id' => $child->id, 'key' => $child->key, 'type' => $child->type);
797
            }
798
            $child->find_expandable($expandable);
799
        }
800
    }
801
 
802
    /**
803
     * Finds all nodes of a given type (recursive)
804
     *
805
     * @param int $type One of navigation_node::TYPE_*
806
     * @return array
807
     */
808
    public function find_all_of_type($type) {
809
        $nodes = $this->children->type($type);
810
        foreach ($this->children as &$node) {
811
            $childnodes = $node->find_all_of_type($type);
812
            $nodes = array_merge($nodes, $childnodes);
813
        }
814
        return $nodes;
815
    }
816
 
817
    /**
818
     * Removes this node if it is empty
819
     */
820
    public function trim_if_empty() {
821
        if ($this->children->count() == 0) {
822
            $this->remove();
823
        }
824
    }
825
 
826
    /**
827
     * Creates a tab representation of this nodes children that can be used
828
     * with print_tabs to produce the tabs on a page.
829
     *
830
     * call_user_func_array('print_tabs', $node->get_tabs_array());
831
     *
832
     * @param array $inactive
833
     * @param bool $return
834
     * @return array Array (tabs, selected, inactive, activated, return)
835
     */
836
    public function get_tabs_array(array $inactive=array(), $return=false) {
837
        $tabs = array();
838
        $rows = array();
839
        $selected = null;
840
        $activated = array();
841
        foreach ($this->children as $node) {
842
            $tabs[] = new tabobject($node->key, $node->action, $node->get_content(), $node->get_title());
843
            if ($node->contains_active_node()) {
844
                if ($node->children->count() > 0) {
845
                    $activated[] = $node->key;
846
                    foreach ($node->children as $child) {
847
                        if ($child->contains_active_node()) {
848
                            $selected = $child->key;
849
                        }
850
                        $rows[] = new tabobject($child->key, $child->action, $child->get_content(), $child->get_title());
851
                    }
852
                } else {
853
                    $selected = $node->key;
854
                }
855
            }
856
        }
857
        return array(array($tabs, $rows), $selected, $inactive, $activated, $return);
858
    }
859
 
860
    /**
861
     * Sets the parent for this node and if this node is active ensures that the tree is properly
862
     * adjusted as well.
863
     *
864
     * @param navigation_node $parent
865
     */
866
    public function set_parent(navigation_node $parent) {
867
        // Set the parent (thats the easy part)
868
        $this->parent = $parent;
869
        // Check if this node is active (this is checked during construction)
870
        if ($this->isactive) {
871
            // Force all of the parent nodes open so you can see this node
872
            $this->parent->force_open();
873
            // Make all parents inactive so that its clear where we are.
874
            $this->parent->make_inactive();
875
        }
876
    }
877
 
878
    /**
879
     * Hides the node and any children it has.
880
     *
881
     * @since Moodle 2.5
882
     * @param array $typestohide Optional. An array of node types that should be hidden.
883
     *      If null all nodes will be hidden.
884
     *      If an array is given then nodes will only be hidden if their type mtatches an element in the array.
885
     *          e.g. array(navigation_node::TYPE_COURSE) would hide only course nodes.
886
     */
887
    public function hide(array $typestohide = null) {
888
        if ($typestohide === null || in_array($this->type, $typestohide)) {
889
            $this->display = false;
890
            if ($this->has_children()) {
891
                foreach ($this->children as $child) {
892
                    $child->hide($typestohide);
893
                }
894
            }
895
        }
896
    }
897
 
898
    /**
899
     * Get the action url for this navigation node.
900
     * Called from templates.
901
     *
902
     * @since Moodle 3.2
903
     */
904
    public function action() {
905
        if ($this->action instanceof moodle_url) {
906
            return $this->action;
907
        } else if ($this->action instanceof action_link) {
908
            return $this->action->url;
909
        }
910
        return $this->action;
911
    }
912
 
913
    /**
914
     * Return an array consisting of the additional attributes for the action url.
915
     *
916
     * @return array Formatted array to parse in a template
917
     */
918
    public function actionattributes() {
919
        if ($this->action instanceof action_link) {
920
            return array_map(function($key, $value) {
921
                return [
922
                    'name' => $key,
923
                    'value' => $value
924
                ];
925
            }, array_keys($this->action->attributes), $this->action->attributes);
926
        }
927
 
928
        return [];
929
    }
930
 
931
    /**
932
     * Check whether the node's action is of type action_link.
933
     *
934
     * @return bool
935
     */
936
    public function is_action_link() {
937
        return $this->action instanceof action_link;
938
    }
939
 
940
    /**
941
     * Return an array consisting of the actions for the action link.
942
     *
943
     * @return array Formatted array to parse in a template
944
     */
945
    public function action_link_actions() {
946
        global $PAGE;
947
 
948
        if (!$this->is_action_link()) {
949
            return [];
950
        }
951
 
952
        $actionid = $this->action->attributes['id'];
953
        $actionsdata = array_map(function($action) use ($PAGE, $actionid) {
954
            $data = $action->export_for_template($PAGE->get_renderer('core'));
955
            $data->id = $actionid;
956
            return $data;
957
        }, !empty($this->action->actions) ? $this->action->actions : []);
958
 
959
        return ['actions' => $actionsdata];
960
    }
961
 
962
    /**
963
     * Sets whether the node and its children should be added into a "more" menu whenever possible.
964
     *
965
     * @param bool $forceintomoremenu
966
     */
967
    public function set_force_into_more_menu(bool $forceintomoremenu = false) {
968
        $this->forceintomoremenu = $forceintomoremenu;
969
        foreach ($this->children as $child) {
970
            $child->set_force_into_more_menu($forceintomoremenu);
971
        }
972
    }
973
 
974
    /**
975
     * Sets whether the node and its children should be displayed in the "secondary" navigation when applicable.
976
     *
977
     * @param bool $show
978
     */
979
    public function set_show_in_secondary_navigation(bool $show = true) {
980
        $this->showinsecondarynavigation = $show;
981
        foreach ($this->children as $child) {
982
            $child->set_show_in_secondary_navigation($show);
983
        }
984
    }
985
 
986
    /**
987
     * Add the menu item to handle locking and unlocking of a conext.
988
     *
989
     * @param \navigation_node $node Node to add
990
     * @param \context $context The context to be locked
991
     */
992
    protected function add_context_locking_node(\navigation_node $node, \context $context) {
993
        global $CFG;
994
        // Manage context locking.
995
        if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $context)) {
996
            $parentcontext = $context->get_parent_context();
997
            if (empty($parentcontext) || !$parentcontext->locked) {
998
                if ($context->locked) {
999
                    $lockicon = 'i/unlock';
1000
                    $lockstring = get_string('managecontextunlock', 'admin');
1001
                } else {
1002
                    $lockicon = 'i/lock';
1003
                    $lockstring = get_string('managecontextlock', 'admin');
1004
                }
1005
                $node->add(
1006
                    $lockstring,
1007
                    new moodle_url(
1008
                        '/admin/lock.php',
1009
                        [
1010
                            'id' => $context->id,
1011
                        ]
1012
                    ),
1013
                    self::TYPE_SETTING,
1014
                    null,
1015
                    'contextlocking',
1016
                     new pix_icon($lockicon, '')
1017
                );
1018
            }
1019
        }
1020
 
1021
    }
1022
}
1023
 
1024
/**
1025
 * Navigation node collection
1026
 *
1027
 * This class is responsible for managing a collection of navigation nodes.
1028
 * It is required because a node's unique identifier is a combination of both its
1029
 * key and its type.
1030
 *
1031
 * Originally an array was used with a string key that was a combination of the two
1032
 * however it was decided that a better solution would be to use a class that
1033
 * implements the standard IteratorAggregate interface.
1034
 *
1035
 * @package   core
1036
 * @category  navigation
1037
 * @copyright 2010 Sam Hemelryk
1038
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1039
 */
1040
class navigation_node_collection implements IteratorAggregate, Countable {
1041
    /**
1042
     * A multidimensional array to where the first key is the type and the second
1043
     * key is the nodes key.
1044
     * @var array
1045
     */
1046
    protected $collection = array();
1047
    /**
1048
     * An array that contains references to nodes in the same order they were added.
1049
     * This is maintained as a progressive array.
1050
     * @var array
1051
     */
1052
    protected $orderedcollection = array();
1053
    /**
1054
     * A reference to the last node that was added to the collection
1055
     * @var navigation_node
1056
     */
1057
    protected $last = null;
1058
    /**
1059
     * The total number of items added to this array.
1060
     * @var int
1061
     */
1062
    protected $count = 0;
1063
 
1064
    /**
1065
     * Label for collection of nodes.
1066
     * @var string
1067
     */
1068
    protected $collectionlabel = '';
1069
 
1070
    /**
1071
     * Adds a navigation node to the collection
1072
     *
1073
     * @param navigation_node $node Node to add
1074
     * @param string $beforekey If specified, adds before a node with this key,
1075
     *   otherwise adds at end
1076
     * @return navigation_node Added node
1077
     */
1078
    public function add(navigation_node $node, $beforekey=null) {
1079
        global $CFG;
1080
        $key = $node->key;
1081
        $type = $node->type;
1082
 
1083
        // First check we have a 2nd dimension for this type
1084
        if (!array_key_exists($type, $this->orderedcollection)) {
1085
            $this->orderedcollection[$type] = array();
1086
        }
1087
        // Check for a collision and report if debugging is turned on
1088
        if ($CFG->debug && array_key_exists($key, $this->orderedcollection[$type])) {
1089
            debugging('Navigation node intersect: Adding a node that already exists '.$key, DEBUG_DEVELOPER);
1090
        }
1091
 
1092
        // Find the key to add before
1093
        $newindex = $this->count;
1094
        $last = true;
1095
        if ($beforekey !== null) {
1096
            foreach ($this->collection as $index => $othernode) {
1097
                if ($othernode->key === $beforekey) {
1098
                    $newindex = $index;
1099
                    $last = false;
1100
                    break;
1101
                }
1102
            }
1103
            if ($newindex === $this->count) {
1104
                debugging('Navigation node add_before: Reference node not found ' . $beforekey .
1105
                        ', options: ' . implode(' ', $this->get_key_list()), DEBUG_DEVELOPER);
1106
            }
1107
        }
1108
 
1109
        // Add the node to the appropriate place in the by-type structure (which
1110
        // is not ordered, despite the variable name)
1111
        $this->orderedcollection[$type][$key] = $node;
1112
        if (!$last) {
1113
            // Update existing references in the ordered collection (which is the
1114
            // one that isn't called 'ordered') to shuffle them along if required
1115
            for ($oldindex = $this->count; $oldindex > $newindex; $oldindex--) {
1116
                $this->collection[$oldindex] = $this->collection[$oldindex - 1];
1117
            }
1118
        }
1119
        // Add a reference to the node to the progressive collection.
1120
        $this->collection[$newindex] = $this->orderedcollection[$type][$key];
1121
        // Update the last property to a reference to this new node.
1122
        $this->last = $this->orderedcollection[$type][$key];
1123
 
1124
        // Reorder the array by index if needed
1125
        if (!$last) {
1126
            ksort($this->collection);
1127
        }
1128
        $this->count++;
1129
        // Return the reference to the now added node
1130
        return $node;
1131
    }
1132
 
1133
    /**
1134
     * Return a list of all the keys of all the nodes.
1135
     * @return array the keys.
1136
     */
1137
    public function get_key_list() {
1138
        $keys = array();
1139
        foreach ($this->collection as $node) {
1140
            $keys[] = $node->key;
1141
        }
1142
        return $keys;
1143
    }
1144
 
1145
    /**
1146
     * Set a label for this collection.
1147
     *
1148
     * @param string $label
1149
     */
1150
    public function set_collectionlabel($label) {
1151
        $this->collectionlabel = $label;
1152
    }
1153
 
1154
    /**
1155
     * Return a label for this collection.
1156
     *
1157
     * @return string
1158
     */
1159
    public function get_collectionlabel() {
1160
        return $this->collectionlabel;
1161
    }
1162
 
1163
    /**
1164
     * Fetches a node from this collection.
1165
     *
1166
     * @param string|int $key The key of the node we want to find.
1167
     * @param int $type One of navigation_node::TYPE_*.
1168
     * @return navigation_node|null|false
1169
     */
1170
    public function get($key, $type=null) {
1171
        if ($type !== null) {
1172
            // If the type is known then we can simply check and fetch
1173
            if (!empty($this->orderedcollection[$type][$key])) {
1174
                return $this->orderedcollection[$type][$key];
1175
            }
1176
        } else {
1177
            // Because we don't know the type we look in the progressive array
1178
            foreach ($this->collection as $node) {
1179
                if ($node->key === $key) {
1180
                    return $node;
1181
                }
1182
            }
1183
        }
1184
        return false;
1185
    }
1186
 
1187
    /**
1188
     * Searches for a node with matching key and type.
1189
     *
1190
     * This function searches both the nodes in this collection and all of
1191
     * the nodes in each collection belonging to the nodes in this collection.
1192
     *
1193
     * Recursive.
1194
     *
1195
     * @param string|int $key  The key of the node we want to find.
1196
     * @param int $type  One of navigation_node::TYPE_*.
1197
     * @return navigation_node|false
1198
     */
1199
    public function find($key, $type=null) {
1200
        if ($type !== null && array_key_exists($type, $this->orderedcollection) && array_key_exists($key, $this->orderedcollection[$type])) {
1201
            return $this->orderedcollection[$type][$key];
1202
        } else {
1203
            $nodes = $this->getIterator();
1204
            // Search immediate children first
1205
            foreach ($nodes as &$node) {
1206
                if ($node->key === $key && ($type === null || $type === $node->type)) {
1207
                    return $node;
1208
                }
1209
            }
1210
            // Now search each childs children
1211
            foreach ($nodes as &$node) {
1212
                $result = $node->children->find($key, $type);
1213
                if ($result !== false) {
1214
                    return $result;
1215
                }
1216
            }
1217
        }
1218
        return false;
1219
    }
1220
 
1221
    /**
1222
     * Fetches the last node that was added to this collection
1223
     *
1224
     * @return navigation_node
1225
     */
1226
    public function last() {
1227
        return $this->last;
1228
    }
1229
 
1230
    /**
1231
     * Fetches all nodes of a given type from this collection
1232
     *
1233
     * @param string|int $type  node type being searched for.
1234
     * @return array ordered collection
1235
     */
1236
    public function type($type) {
1237
        if (!array_key_exists($type, $this->orderedcollection)) {
1238
            $this->orderedcollection[$type] = array();
1239
        }
1240
        return $this->orderedcollection[$type];
1241
    }
1242
    /**
1243
     * Removes the node with the given key and type from the collection
1244
     *
1245
     * @param string|int $key The key of the node we want to find.
1246
     * @param int $type
1247
     * @return bool
1248
     */
1249
    public function remove($key, $type=null) {
1250
        $child = $this->get($key, $type);
1251
        if ($child !== false) {
1252
            foreach ($this->collection as $colkey => $node) {
1253
                if ($node->key === $key && (is_null($type) || $node->type == $type)) {
1254
                    unset($this->collection[$colkey]);
1255
                    $this->collection = array_values($this->collection);
1256
                    break;
1257
                }
1258
            }
1259
            unset($this->orderedcollection[$child->type][$child->key]);
1260
            $this->count--;
1261
            return true;
1262
        }
1263
        return false;
1264
    }
1265
 
1266
    /**
1267
     * Gets the number of nodes in this collection
1268
     *
1269
     * This option uses an internal count rather than counting the actual options to avoid
1270
     * a performance hit through the count function.
1271
     *
1272
     * @return int
1273
     */
1274
    public function count(): int {
1275
        return $this->count;
1276
    }
1277
    /**
1278
     * Gets an array iterator for the collection.
1279
     *
1280
     * This is required by the IteratorAggregator interface and is used by routines
1281
     * such as the foreach loop.
1282
     *
1283
     * @return ArrayIterator
1284
     */
1285
    public function getIterator(): Traversable {
1286
        return new ArrayIterator($this->collection);
1287
    }
1288
}
1289
 
1290
/**
1291
 * The global navigation class used for... the global navigation
1292
 *
1293
 * This class is used by PAGE to store the global navigation for the site
1294
 * and is then used by the settings nav and navbar to save on processing and DB calls
1295
 *
1296
 * See
1297
 * {@link lib/pagelib.php} {@link moodle_page::initialise_theme_and_output()}
1298
 * {@link lib/ajax/getnavbranch.php} Called by ajax
1299
 *
1300
 * @package   core
1301
 * @category  navigation
1302
 * @copyright 2009 Sam Hemelryk
1303
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1304
 */
1305
class global_navigation extends navigation_node {
1306
    /** @var moodle_page The Moodle page this navigation object belongs to. */
1307
    protected $page;
1308
    /** @var bool switch to let us know if the navigation object is initialised*/
1309
    protected $initialised = false;
1310
    /** @var array An array of course information */
1311
    protected $mycourses = array();
1312
    /** @var navigation_node[] An array for containing  root navigation nodes */
1313
    protected $rootnodes = array();
1314
    /** @var bool A switch for whether to show empty sections in the navigation */
1315
    protected $showemptysections = true;
1316
    /** @var bool A switch for whether courses should be shown within categories on the navigation. */
1317
    protected $showcategories = null;
1318
    /** @var null@var bool A switch for whether or not to show categories in the my courses branch. */
1319
    protected $showmycategories = null;
1320
    /** @var array An array of stdClasses for users that the navigation is extended for */
1321
    protected $extendforuser = array();
1322
    /** @var navigation_cache */
1323
    protected $cache;
1324
    /** @var array An array of course ids that are present in the navigation */
1325
    protected $addedcourses = array();
1326
    /** @var bool */
1327
    protected $allcategoriesloaded = false;
1328
    /** @var array An array of category ids that are included in the navigation */
1329
    protected $addedcategories = array();
1330
    /** @var int expansion limit */
1331
    protected $expansionlimit = 0;
1332
    /** @var int userid to allow parent to see child's profile page navigation */
1333
    protected $useridtouseforparentchecks = 0;
1334
    /** @var cache_session A cache that stores information on expanded courses */
1335
    protected $cacheexpandcourse = null;
1336
 
1337
    /** Used when loading categories to load all top level categories [parent = 0] **/
1338
    const LOAD_ROOT_CATEGORIES = 0;
1339
    /** Used when loading categories to load all categories **/
1340
    const LOAD_ALL_CATEGORIES = -1;
1341
 
1342
    /**
1343
     * Constructs a new global navigation
1344
     *
1345
     * @param moodle_page $page The page this navigation object belongs to
1346
     */
1347
    public function __construct(moodle_page $page) {
1348
        global $CFG, $SITE, $USER;
1349
 
1350
        if (during_initial_install()) {
1351
            return;
1352
        }
1353
 
1354
        $homepage = get_home_page();
1355
        if ($homepage == HOMEPAGE_SITE) {
1356
            // We are using the site home for the root element.
1357
            $properties = array(
1358
                'key' => 'home',
1359
                'type' => navigation_node::TYPE_SYSTEM,
1360
                'text' => get_string('home'),
1361
                'action' => new moodle_url('/'),
1362
                'icon' => new pix_icon('i/home', '')
1363
            );
1364
        } else if ($homepage == HOMEPAGE_MYCOURSES) {
1365
            // We are using the user's course summary page for the root element.
1366
            $properties = array(
1367
                'key' => 'mycourses',
1368
                'type' => navigation_node::TYPE_SYSTEM,
1369
                'text' => get_string('mycourses'),
1370
                'action' => new moodle_url('/my/courses.php'),
1371
                'icon' => new pix_icon('i/course', '')
1372
            );
1373
        } else {
1374
            // We are using the users my moodle for the root element.
1375
            $properties = array(
1376
                'key' => 'myhome',
1377
                'type' => navigation_node::TYPE_SYSTEM,
1378
                'text' => get_string('myhome'),
1379
                'action' => new moodle_url('/my/'),
1380
                'icon' => new pix_icon('i/dashboard', '')
1381
            );
1382
        }
1383
 
1384
        // Use the parents constructor.... good good reuse
1385
        parent::__construct($properties);
1386
        $this->showinflatnavigation = true;
1387
 
1388
        // Initalise and set defaults
1389
        $this->page = $page;
1390
        $this->forceopen = true;
1391
        $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME);
1392
    }
1393
 
1394
    /**
1395
     * Mutator to set userid to allow parent to see child's profile
1396
     * page navigation. See MDL-25805 for initial issue. Linked to it
1397
     * is an issue explaining why this is a REALLY UGLY HACK thats not
1398
     * for you to use!
1399
     *
1400
     * @param int $userid userid of profile page that parent wants to navigate around.
1401
     */
1402
    public function set_userid_for_parent_checks($userid) {
1403
        $this->useridtouseforparentchecks = $userid;
1404
    }
1405
 
1406
 
1407
    /**
1408
     * Initialises the navigation object.
1409
     *
1410
     * This causes the navigation object to look at the current state of the page
1411
     * that it is associated with and then load the appropriate content.
1412
     *
1413
     * This should only occur the first time that the navigation structure is utilised
1414
     * which will normally be either when the navbar is called to be displayed or
1415
     * when a block makes use of it.
1416
     *
1417
     * @return bool
1418
     */
1419
    public function initialise() {
1420
        global $CFG, $SITE, $USER;
1421
        // Check if it has already been initialised
1422
        if ($this->initialised || during_initial_install()) {
1423
            return true;
1424
        }
1425
        $this->initialised = true;
1426
 
1427
        // Set up the five base root nodes. These are nodes where we will put our
1428
        // content and are as follows:
1429
        // site: Navigation for the front page.
1430
        // myprofile: User profile information goes here.
1431
        // currentcourse: The course being currently viewed.
1432
        // mycourses: The users courses get added here.
1433
        // courses: Additional courses are added here.
1434
        // users: Other users information loaded here.
1435
        $this->rootnodes = array();
1436
        $defaulthomepage = get_home_page();
1437
        if ($defaulthomepage == HOMEPAGE_SITE) {
1438
            // The home element should be my moodle because the root element is the site
1439
            if (isloggedin() && !isguestuser()) {  // Makes no sense if you aren't logged in
1440
                if (!empty($CFG->enabledashboard)) {
1441
                    // Only add dashboard to home if it's enabled.
1442
                    $this->rootnodes['home'] = $this->add(get_string('myhome'), new moodle_url('/my/'),
1443
                        self::TYPE_SETTING, null, 'myhome', new pix_icon('i/dashboard', ''));
1444
                    $this->rootnodes['home']->showinflatnavigation = true;
1445
                }
1446
            }
1447
        } else {
1448
            // The home element should be the site because the root node is my moodle
1449
            $this->rootnodes['home'] = $this->add(get_string('sitehome'), new moodle_url('/'),
1450
                self::TYPE_SETTING, null, 'home', new pix_icon('i/home', ''));
1451
            $this->rootnodes['home']->showinflatnavigation = true;
1452
            if (!empty($CFG->defaulthomepage) &&
1453
                    ($CFG->defaulthomepage == HOMEPAGE_MY || $CFG->defaulthomepage == HOMEPAGE_MYCOURSES)) {
1454
                // We need to stop automatic redirection
1455
                $this->rootnodes['home']->action->param('redirect', '0');
1456
            }
1457
        }
1458
        $this->rootnodes['site'] = $this->add_course($SITE);
1459
        $this->rootnodes['myprofile'] = $this->add(get_string('profile'), null, self::TYPE_USER, null, 'myprofile');
1460
        $this->rootnodes['currentcourse'] = $this->add(get_string('currentcourse'), null, self::TYPE_ROOTNODE, null, 'currentcourse');
1461
        $this->rootnodes['mycourses'] = $this->add(
1462
            get_string('mycourses'),
1463
            new moodle_url('/my/courses.php'),
1464
            self::TYPE_ROOTNODE,
1465
            null,
1466
            'mycourses',
1467
            new pix_icon('i/course', '')
1468
        );
1469
        // We do not need to show this node in the breadcrumbs if the default homepage is mycourses.
1470
        // It will be automatically handled by the breadcrumb generator.
1471
        if ($defaulthomepage == HOMEPAGE_MYCOURSES) {
1472
            $this->rootnodes['mycourses']->mainnavonly = true;
1473
        }
1474
 
1475
        $this->rootnodes['courses'] = $this->add(get_string('courses'), new moodle_url('/course/index.php'), self::TYPE_ROOTNODE, null, 'courses');
1476
        if (!core_course_category::user_top()) {
1477
            $this->rootnodes['courses']->hide();
1478
        }
1479
        $this->rootnodes['users'] = $this->add(get_string('users'), null, self::TYPE_ROOTNODE, null, 'users');
1480
 
1481
        // We always load the frontpage course to ensure it is available without
1482
        // JavaScript enabled.
1483
        $this->add_front_page_course_essentials($this->rootnodes['site'], $SITE);
1484
        $this->load_course_sections($SITE, $this->rootnodes['site']);
1485
 
1486
        $course = $this->page->course;
1487
        $this->load_courses_enrolled();
1488
 
1489
        // $issite gets set to true if the current pages course is the sites frontpage course
1490
        $issite = ($this->page->course->id == $SITE->id);
1491
 
1492
        // Determine if the user is enrolled in any course.
1493
        $enrolledinanycourse = enrol_user_sees_own_courses();
1494
 
1495
        $this->rootnodes['currentcourse']->mainnavonly = true;
1496
        if ($enrolledinanycourse) {
1497
            $this->rootnodes['mycourses']->isexpandable = true;
1498
            $this->rootnodes['mycourses']->showinflatnavigation = true;
1499
            if ($CFG->navshowallcourses) {
1500
                // When we show all courses we need to show both the my courses and the regular courses branch.
1501
                $this->rootnodes['courses']->isexpandable = true;
1502
            }
1503
        } else {
1504
            $this->rootnodes['courses']->isexpandable = true;
1505
        }
1506
        $this->rootnodes['mycourses']->forceopen = true;
1507
 
1508
        $canviewcourseprofile = true;
1509
 
1510
        // Next load context specific content into the navigation
1511
        switch ($this->page->context->contextlevel) {
1512
            case CONTEXT_SYSTEM :
1513
                // Nothing left to do here I feel.
1514
                break;
1515
            case CONTEXT_COURSECAT :
1516
                // This is essential, we must load categories.
1517
                $this->load_all_categories($this->page->context->instanceid, true);
1518
                break;
1519
            case CONTEXT_BLOCK :
1520
            case CONTEXT_COURSE :
1521
                if ($issite) {
1522
                    // Nothing left to do here.
1523
                    break;
1524
                }
1525
 
1526
                // Load the course associated with the current page into the navigation.
1527
                $coursenode = $this->add_course($course, false, self::COURSE_CURRENT);
1528
                // If the course wasn't added then don't try going any further.
1529
                if (!$coursenode) {
1530
                    $canviewcourseprofile = false;
1531
                    break;
1532
                }
1533
 
1534
                // If the user is not enrolled then we only want to show the
1535
                // course node and not populate it.
1536
 
1537
                // Not enrolled, can't view, and hasn't switched roles
1538
                if (!can_access_course($course, null, '', true)) {
1539
                    if ($coursenode->isexpandable === true) {
1540
                        // Obviously the situation has changed, update the cache and adjust the node.
1541
                        // This occurs if the user access to a course has been revoked (one way or another) after
1542
                        // initially logging in for this session.
1543
                        $this->get_expand_course_cache()->set($course->id, 1);
1544
                        $coursenode->isexpandable = true;
1545
                        $coursenode->nodetype = self::NODETYPE_BRANCH;
1546
                    }
1547
                    // Very ugly hack - do not force "parents" to enrol into course their child is enrolled in,
1548
                    // this hack has been propagated from user/view.php to display the navigation node. (MDL-25805)
1549
                    if (!$this->current_user_is_parent_role()) {
1550
                        $coursenode->make_active();
1551
                        $canviewcourseprofile = false;
1552
                        break;
1553
                    }
1554
                } else if ($coursenode->isexpandable === false) {
1555
                    // Obviously the situation has changed, update the cache and adjust the node.
1556
                    // This occurs if the user has been granted access to a course (one way or another) after initially
1557
                    // logging in for this session.
1558
                    $this->get_expand_course_cache()->set($course->id, 1);
1559
                    $coursenode->isexpandable = true;
1560
                    $coursenode->nodetype = self::NODETYPE_BRANCH;
1561
                }
1562
 
1563
                // Add the essentials such as reports etc...
1564
                $this->add_course_essentials($coursenode, $course);
1565
                // Extend course navigation with it's sections/activities
1566
                $this->load_course_sections($course, $coursenode);
1567
                if (!$coursenode->contains_active_node() && !$coursenode->search_for_active_node()) {
1568
                    $coursenode->make_active();
1569
                }
1570
 
1571
                break;
1572
            case CONTEXT_MODULE :
1573
                if ($issite) {
1574
                    // If this is the site course then most information will have
1575
                    // already been loaded.
1576
                    // However we need to check if there is more content that can
1577
                    // yet be loaded for the specific module instance.
1578
                    $activitynode = $this->rootnodes['site']->find($this->page->cm->id, navigation_node::TYPE_ACTIVITY);
1579
                    if ($activitynode) {
1580
                        $this->load_activity($this->page->cm, $this->page->course, $activitynode);
1581
                    }
1582
                    break;
1583
                }
1584
 
1585
                $course = $this->page->course;
1586
                $cm = $this->page->cm;
1587
 
1588
                // Load the course associated with the page into the navigation
1589
                $coursenode = $this->add_course($course, false, self::COURSE_CURRENT);
1590
 
1591
                // If the course wasn't added then don't try going any further.
1592
                if (!$coursenode) {
1593
                    $canviewcourseprofile = false;
1594
                    break;
1595
                }
1596
 
1597
                // If the user is not enrolled then we only want to show the
1598
                // course node and not populate it.
1599
                if (!can_access_course($course, null, '', true)) {
1600
                    $coursenode->make_active();
1601
                    $canviewcourseprofile = false;
1602
                    break;
1603
                }
1604
 
1605
                $this->add_course_essentials($coursenode, $course);
1606
 
1607
                // Load the course sections into the page
1608
                $this->load_course_sections($course, $coursenode, null, $cm);
1609
                $activity = $coursenode->find($cm->id, navigation_node::TYPE_ACTIVITY);
1610
                if (!empty($activity)) {
1611
                    // Finally load the cm specific navigaton information
1612
                    $this->load_activity($cm, $course, $activity);
1613
                    // Check if we have an active ndoe
1614
                    if (!$activity->contains_active_node() && !$activity->search_for_active_node()) {
1615
                        // And make the activity node active.
1616
                        $activity->make_active();
1617
                    }
1618
                }
1619
                break;
1620
            case CONTEXT_USER :
1621
                if ($issite) {
1622
                    // The users profile information etc is already loaded
1623
                    // for the front page.
1624
                    break;
1625
                }
1626
                $course = $this->page->course;
1627
                // Load the course associated with the user into the navigation
1628
                $coursenode = $this->add_course($course, false, self::COURSE_CURRENT);
1629
 
1630
                // If the course wasn't added then don't try going any further.
1631
                if (!$coursenode) {
1632
                    $canviewcourseprofile = false;
1633
                    break;
1634
                }
1635
 
1636
                // If the user is not enrolled then we only want to show the
1637
                // course node and not populate it.
1638
                if (!can_access_course($course, null, '', true)) {
1639
                    $coursenode->make_active();
1640
                    $canviewcourseprofile = false;
1641
                    break;
1642
                }
1643
                $this->add_course_essentials($coursenode, $course);
1644
                $this->load_course_sections($course, $coursenode);
1645
                break;
1646
        }
1647
 
1648
        // Load for the current user
1649
        $this->load_for_user();
1650
        if ($this->page->context->contextlevel >= CONTEXT_COURSE && $this->page->context->instanceid != $SITE->id && $canviewcourseprofile) {
1651
            $this->load_for_user(null, true);
1652
        }
1653
        // Load each extending user into the navigation.
1654
        foreach ($this->extendforuser as $user) {
1655
            if ($user->id != $USER->id) {
1656
                $this->load_for_user($user);
1657
            }
1658
        }
1659
 
1660
        // Give the local plugins a chance to include some navigation if they want.
1661
        $this->load_local_plugin_navigation();
1662
 
1663
        // Remove any empty root nodes
1664
        foreach ($this->rootnodes as $node) {
1665
            // Dont remove the home node
1666
            /** @var navigation_node $node */
1667
            if (!in_array($node->key, ['home', 'mycourses', 'myhome']) && !$node->has_children() && !$node->isactive) {
1668
                $node->remove();
1669
            }
1670
        }
1671
 
1672
        if (!$this->contains_active_node()) {
1673
            $this->search_for_active_node();
1674
        }
1675
 
1676
        // If the user is not logged in modify the navigation structure.
1677
        if (!isloggedin()) {
1678
            $activities = clone($this->rootnodes['site']->children);
1679
            $this->rootnodes['site']->remove();
1680
            $children = clone($this->children);
1681
            $this->children = new navigation_node_collection();
1682
            foreach ($activities as $child) {
1683
                $this->children->add($child);
1684
            }
1685
            foreach ($children as $child) {
1686
                $this->children->add($child);
1687
            }
1688
        }
1689
        return true;
1690
    }
1691
 
1692
    /**
1693
     * This function gives local plugins an opportunity to modify navigation.
1694
     */
1695
    protected function load_local_plugin_navigation() {
1696
        foreach (get_plugin_list_with_function('local', 'extend_navigation') as $function) {
1697
            $function($this);
1698
        }
1699
    }
1700
 
1701
    /**
1702
     * Returns true if the current user is a parent of the user being currently viewed.
1703
     *
1704
     * If the current user is not viewing another user, or if the current user does not hold any parent roles over the
1705
     * other user being viewed this function returns false.
1706
     * In order to set the user for whom we are checking against you must call {@link set_userid_for_parent_checks()}
1707
     *
1708
     * @since Moodle 2.4
1709
     * @return bool
1710
     */
1711
    protected function current_user_is_parent_role() {
1712
        global $USER, $DB;
1713
        if ($this->useridtouseforparentchecks && $this->useridtouseforparentchecks != $USER->id) {
1714
            $usercontext = context_user::instance($this->useridtouseforparentchecks, MUST_EXIST);
1715
            if (!has_capability('moodle/user:viewdetails', $usercontext)) {
1716
                return false;
1717
            }
1718
            if ($DB->record_exists('role_assignments', array('userid' => $USER->id, 'contextid' => $usercontext->id))) {
1719
                return true;
1720
            }
1721
        }
1722
        return false;
1723
    }
1724
 
1725
    /**
1726
     * Returns true if courses should be shown within categories on the navigation.
1727
     *
1728
     * @param bool $ismycourse Set to true if you are calculating this for a course.
1729
     * @return bool
1730
     */
1731
    protected function show_categories($ismycourse = false) {
1732
        global $CFG, $DB;
1733
        if ($ismycourse) {
1734
            return $this->show_my_categories();
1735
        }
1736
        if ($this->showcategories === null) {
1737
            $show = false;
1738
            if ($this->page->context->contextlevel == CONTEXT_COURSECAT) {
1739
                $show = true;
1740
            } else if (!empty($CFG->navshowcategories) && $DB->count_records('course_categories') > 1) {
1741
                $show = true;
1742
            }
1743
            $this->showcategories = $show;
1744
        }
1745
        return $this->showcategories;
1746
    }
1747
 
1748
    /**
1749
     * Returns true if we should show categories in the My Courses branch.
1750
     * @return bool
1751
     */
1752
    protected function show_my_categories() {
1753
        global $CFG;
1754
        if ($this->showmycategories === null) {
1755
            $this->showmycategories = !empty($CFG->navshowmycoursecategories) && !core_course_category::is_simple_site();
1756
        }
1757
        return $this->showmycategories;
1758
    }
1759
 
1760
    /**
1761
     * Loads the courses in Moodle into the navigation.
1762
     *
1763
     * @global moodle_database $DB
1764
     * @param string|array $categoryids An array containing categories to load courses
1765
     *                     for, OR null to load courses for all categories.
1766
     * @return array An array of navigation_nodes one for each course
1767
     */
1768
    protected function load_all_courses($categoryids = null) {
1769
        global $CFG, $DB, $SITE;
1770
 
1771
        // Work out the limit of courses.
1772
        $limit = 20;
1773
        if (!empty($CFG->navcourselimit)) {
1774
            $limit = $CFG->navcourselimit;
1775
        }
1776
 
1777
        $toload = (empty($CFG->navshowallcourses))?self::LOAD_ROOT_CATEGORIES:self::LOAD_ALL_CATEGORIES;
1778
 
1779
        // If we are going to show all courses AND we are showing categories then
1780
        // to save us repeated DB calls load all of the categories now
1781
        if ($this->show_categories()) {
1782
            $this->load_all_categories($toload);
1783
        }
1784
 
1785
        // Will be the return of our efforts
1786
        $coursenodes = array();
1787
 
1788
        // Check if we need to show categories.
1789
        if ($this->show_categories()) {
1790
            // Hmmm we need to show categories... this is going to be painful.
1791
            // We now need to fetch up to $limit courses for each category to
1792
            // be displayed.
1793
            if ($categoryids !== null) {
1794
                if (!is_array($categoryids)) {
1795
                    $categoryids = array($categoryids);
1796
                }
1797
                list($categorywhere, $categoryparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'cc');
1798
                $categorywhere = 'WHERE cc.id '.$categorywhere;
1799
            } else if ($toload == self::LOAD_ROOT_CATEGORIES) {
1800
                $categorywhere = 'WHERE cc.depth = 1 OR cc.depth = 2';
1801
                $categoryparams = array();
1802
            } else {
1803
                $categorywhere = '';
1804
                $categoryparams = array();
1805
            }
1806
 
1807
            // First up we are going to get the categories that we are going to
1808
            // need so that we can determine how best to load the courses from them.
1809
            $sql = "SELECT cc.id, COUNT(c.id) AS coursecount
1810
                        FROM {course_categories} cc
1811
                    LEFT JOIN {course} c ON c.category = cc.id
1812
                            {$categorywhere}
1813
                    GROUP BY cc.id";
1814
            $categories = $DB->get_recordset_sql($sql, $categoryparams);
1815
            $fullfetch = array();
1816
            $partfetch = array();
1817
            foreach ($categories as $category) {
1818
                if (!$this->can_add_more_courses_to_category($category->id)) {
1819
                    continue;
1820
                }
1821
                if ($category->coursecount > $limit * 5) {
1822
                    $partfetch[] = $category->id;
1823
                } else if ($category->coursecount > 0) {
1824
                    $fullfetch[] = $category->id;
1825
                }
1826
            }
1827
            $categories->close();
1828
 
1829
            if (count($fullfetch)) {
1830
                // First up fetch all of the courses in categories where we know that we are going to
1831
                // need the majority of courses.
1832
                list($categoryids, $categoryparams) = $DB->get_in_or_equal($fullfetch, SQL_PARAMS_NAMED, 'lcategory');
1833
                $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1834
                $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1835
                $categoryparams['contextlevel'] = CONTEXT_COURSE;
1836
                $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
1837
                            FROM {course} c
1838
                                $ccjoin
1839
                            WHERE c.category {$categoryids}
1840
                        ORDER BY c.sortorder ASC";
1841
                $coursesrs = $DB->get_recordset_sql($sql, $categoryparams);
1842
                foreach ($coursesrs as $course) {
1843
                    if ($course->id == $SITE->id) {
1844
                        // This should not be necessary, frontpage is not in any category.
1845
                        continue;
1846
                    }
1847
                    if (array_key_exists($course->id, $this->addedcourses)) {
1848
                        // It is probably better to not include the already loaded courses
1849
                        // directly in SQL because inequalities may confuse query optimisers
1850
                        // and may interfere with query caching.
1851
                        continue;
1852
                    }
1853
                    if (!$this->can_add_more_courses_to_category($course->category)) {
1854
                        continue;
1855
                    }
1856
                    context_helper::preload_from_record($course);
1857
                    if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
1858
                        continue;
1859
                    }
1860
                    $coursenodes[$course->id] = $this->add_course($course);
1861
                }
1862
                $coursesrs->close();
1863
            }
1864
 
1865
            if (count($partfetch)) {
1866
                // Next we will work our way through the categories where we will likely only need a small
1867
                // proportion of the courses.
1868
                foreach ($partfetch as $categoryid) {
1869
                    $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1870
                    $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1871
                    $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
1872
                                FROM {course} c
1873
                                    $ccjoin
1874
                                WHERE c.category = :categoryid
1875
                            ORDER BY c.sortorder ASC";
1876
                    $courseparams = array('categoryid' => $categoryid, 'contextlevel' => CONTEXT_COURSE);
1877
                    $coursesrs = $DB->get_recordset_sql($sql, $courseparams, 0, $limit * 5);
1878
                    foreach ($coursesrs as $course) {
1879
                        if ($course->id == $SITE->id) {
1880
                            // This should not be necessary, frontpage is not in any category.
1881
                            continue;
1882
                        }
1883
                        if (array_key_exists($course->id, $this->addedcourses)) {
1884
                            // It is probably better to not include the already loaded courses
1885
                            // directly in SQL because inequalities may confuse query optimisers
1886
                            // and may interfere with query caching.
1887
                            // This also helps to respect expected $limit on repeated executions.
1888
                            continue;
1889
                        }
1890
                        if (!$this->can_add_more_courses_to_category($course->category)) {
1891
                            break;
1892
                        }
1893
                        context_helper::preload_from_record($course);
1894
                        if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
1895
                            continue;
1896
                        }
1897
                        $coursenodes[$course->id] = $this->add_course($course);
1898
                    }
1899
                    $coursesrs->close();
1900
                }
1901
            }
1902
        } else {
1903
            // Prepare the SQL to load the courses and their contexts
1904
            list($courseids, $courseparams) = $DB->get_in_or_equal(array_keys($this->addedcourses), SQL_PARAMS_NAMED, 'lc', false);
1905
            $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1906
            $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1907
            $courseparams['contextlevel'] = CONTEXT_COURSE;
1908
            $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
1909
                        FROM {course} c
1910
                            $ccjoin
1911
                        WHERE c.id {$courseids}
1912
                    ORDER BY c.sortorder ASC";
1913
            $coursesrs = $DB->get_recordset_sql($sql, $courseparams);
1914
            foreach ($coursesrs as $course) {
1915
                if ($course->id == $SITE->id) {
1916
                    // frotpage is not wanted here
1917
                    continue;
1918
                }
1919
                if ($this->page->course && ($this->page->course->id == $course->id)) {
1920
                    // Don't include the currentcourse in this nodelist - it's displayed in the Current course node
1921
                    continue;
1922
                }
1923
                context_helper::preload_from_record($course);
1924
                if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
1925
                    continue;
1926
                }
1927
                $coursenodes[$course->id] = $this->add_course($course);
1928
                if (count($coursenodes) >= $limit) {
1929
                    break;
1930
                }
1931
            }
1932
            $coursesrs->close();
1933
        }
1934
 
1935
        return $coursenodes;
1936
    }
1937
 
1938
    /**
1939
     * Returns true if more courses can be added to the provided category.
1940
     *
1941
     * @param int|navigation_node|stdClass $category
1942
     * @return bool
1943
     */
1944
    protected function can_add_more_courses_to_category($category) {
1945
        global $CFG;
1946
        $limit = 20;
1947
        if (!empty($CFG->navcourselimit)) {
1948
            $limit = (int)$CFG->navcourselimit;
1949
        }
1950
        if (is_numeric($category)) {
1951
            if (!array_key_exists($category, $this->addedcategories)) {
1952
                return true;
1953
            }
1954
            $coursecount = count($this->addedcategories[$category]->children->type(self::TYPE_COURSE));
1955
        } else if ($category instanceof navigation_node) {
1956
            if (($category->type != self::TYPE_CATEGORY) || ($category->type != self::TYPE_MY_CATEGORY)) {
1957
                return false;
1958
            }
1959
            $coursecount = count($category->children->type(self::TYPE_COURSE));
1960
        } else if (is_object($category) && property_exists($category,'id')) {
1961
            $coursecount = count($this->addedcategories[$category->id]->children->type(self::TYPE_COURSE));
1962
        }
1963
        return ($coursecount <= $limit);
1964
    }
1965
 
1966
    /**
1967
     * Loads all categories (top level or if an id is specified for that category)
1968
     *
1969
     * @param int $categoryid The category id to load or null/0 to load all base level categories
1970
     * @param bool $showbasecategories If set to true all base level categories will be loaded as well
1971
     *        as the requested category and any parent categories.
1972
     * @return true|void
1973
     */
1974
    protected function load_all_categories($categoryid = self::LOAD_ROOT_CATEGORIES, $showbasecategories = false) {
1975
        global $CFG, $DB;
1976
 
1977
        // Check if this category has already been loaded
1978
        if ($this->allcategoriesloaded || ($categoryid < 1 && $this->is_category_fully_loaded($categoryid))) {
1979
            return true;
1980
        }
1981
 
1982
        $catcontextsql = context_helper::get_preload_record_columns_sql('ctx');
1983
        $sqlselect = "SELECT cc.*, $catcontextsql
1984
                      FROM {course_categories} cc
1985
                      JOIN {context} ctx ON cc.id = ctx.instanceid";
1986
        $sqlwhere = "WHERE ctx.contextlevel = ".CONTEXT_COURSECAT;
1987
        $sqlorder = "ORDER BY cc.depth ASC, cc.sortorder ASC, cc.id ASC";
1988
        $params = array();
1989
 
1990
        $categoriestoload = array();
1991
        if ($categoryid == self::LOAD_ALL_CATEGORIES) {
1992
            // We are going to load all categories regardless... prepare to fire
1993
            // on the database server!
1994
        } else if ($categoryid == self::LOAD_ROOT_CATEGORIES) { // can be 0
1995
            // We are going to load all of the first level categories (categories without parents)
1996
            $sqlwhere .= " AND cc.parent = 0";
1997
        } else if (array_key_exists($categoryid, $this->addedcategories)) {
1998
            // The category itself has been loaded already so we just need to ensure its subcategories
1999
            // have been loaded
2000
            $addedcategories = $this->addedcategories;
2001
            unset($addedcategories[$categoryid]);
2002
            if (count($addedcategories) > 0) {
2003
                list($sql, $params) = $DB->get_in_or_equal(array_keys($addedcategories), SQL_PARAMS_NAMED, 'parent', false);
2004
                if ($showbasecategories) {
2005
                    // We need to include categories with parent = 0 as well
2006
                    $sqlwhere .= " AND (cc.parent = :categoryid OR cc.parent = 0) AND cc.parent {$sql}";
2007
                } else {
2008
                    // All we need is categories that match the parent
2009
                    $sqlwhere .= " AND cc.parent = :categoryid AND cc.parent {$sql}";
2010
                }
2011
            }
2012
            $params['categoryid'] = $categoryid;
2013
        } else {
2014
            // This category hasn't been loaded yet so we need to fetch it, work out its category path
2015
            // and load this category plus all its parents and subcategories
2016
            $category = $DB->get_record('course_categories', array('id' => $categoryid), 'path', MUST_EXIST);
2017
            $categoriestoload = explode('/', trim($category->path, '/'));
2018
            list($select, $params) = $DB->get_in_or_equal($categoriestoload);
2019
            // We are going to use select twice so double the params
2020
            $params = array_merge($params, $params);
2021
            $basecategorysql = ($showbasecategories)?' OR cc.depth = 1':'';
2022
            $sqlwhere .= " AND (cc.id {$select} OR cc.parent {$select}{$basecategorysql})";
2023
        }
2024
 
2025
        $categoriesrs = $DB->get_recordset_sql("$sqlselect $sqlwhere $sqlorder", $params);
2026
        $categories = array();
2027
        foreach ($categoriesrs as $category) {
2028
            // Preload the context.. we'll need it when adding the category in order
2029
            // to format the category name.
2030
            context_helper::preload_from_record($category);
2031
            if (array_key_exists($category->id, $this->addedcategories)) {
2032
                // Do nothing, its already been added.
2033
            } else if ($category->parent == '0') {
2034
                // This is a root category lets add it immediately
2035
                $this->add_category($category, $this->rootnodes['courses']);
2036
            } else if (array_key_exists($category->parent, $this->addedcategories)) {
2037
                // This categories parent has already been added we can add this immediately
2038
                $this->add_category($category, $this->addedcategories[$category->parent]);
2039
            } else {
2040
                $categories[] = $category;
2041
            }
2042
        }
2043
        $categoriesrs->close();
2044
 
2045
        // Now we have an array of categories we need to add them to the navigation.
2046
        while (!empty($categories)) {
2047
            $category = reset($categories);
2048
            if (array_key_exists($category->id, $this->addedcategories)) {
2049
                // Do nothing
2050
            } else if ($category->parent == '0') {
2051
                $this->add_category($category, $this->rootnodes['courses']);
2052
            } else if (array_key_exists($category->parent, $this->addedcategories)) {
2053
                $this->add_category($category, $this->addedcategories[$category->parent]);
2054
            } else {
2055
                // This category isn't in the navigation and niether is it's parent (yet).
2056
                // We need to go through the category path and add all of its components in order.
2057
                $path = explode('/', trim($category->path, '/'));
2058
                foreach ($path as $catid) {
2059
                    if (!array_key_exists($catid, $this->addedcategories)) {
2060
                        // This category isn't in the navigation yet so add it.
2061
                        $subcategory = $categories[$catid];
2062
                        if ($subcategory->parent == '0') {
2063
                            // Yay we have a root category - this likely means we will now be able
2064
                            // to add categories without problems.
2065
                            $this->add_category($subcategory, $this->rootnodes['courses']);
2066
                        } else if (array_key_exists($subcategory->parent, $this->addedcategories)) {
2067
                            // The parent is in the category (as we'd expect) so add it now.
2068
                            $this->add_category($subcategory, $this->addedcategories[$subcategory->parent]);
2069
                            // Remove the category from the categories array.
2070
                            unset($categories[$catid]);
2071
                        } else {
2072
                            // We should never ever arrive here - if we have then there is a bigger
2073
                            // problem at hand.
2074
                            throw new coding_exception('Category path order is incorrect and/or there are missing categories');
2075
                        }
2076
                    }
2077
                }
2078
            }
2079
            // Remove the category from the categories array now that we know it has been added.
2080
            unset($categories[$category->id]);
2081
        }
2082
        if ($categoryid === self::LOAD_ALL_CATEGORIES) {
2083
            $this->allcategoriesloaded = true;
2084
        }
2085
        // Check if there are any categories to load.
2086
        if (count($categoriestoload) > 0) {
2087
            $readytoloadcourses = array();
2088
            foreach ($categoriestoload as $category) {
2089
                if ($this->can_add_more_courses_to_category($category)) {
2090
                    $readytoloadcourses[] = $category;
2091
                }
2092
            }
2093
            if (count($readytoloadcourses)) {
2094
                $this->load_all_courses($readytoloadcourses);
2095
            }
2096
        }
2097
 
2098
        // Look for all categories which have been loaded
2099
        if (!empty($this->addedcategories)) {
2100
            $categoryids = array();
2101
            foreach ($this->addedcategories as $category) {
2102
                if ($this->can_add_more_courses_to_category($category)) {
2103
                    $categoryids[] = $category->key;
2104
                }
2105
            }
2106
            if ($categoryids) {
2107
                list($categoriessql, $params) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED);
2108
                $params['limit'] = (!empty($CFG->navcourselimit))?$CFG->navcourselimit:20;
2109
                $sql = "SELECT cc.id, COUNT(c.id) AS coursecount
2110
                          FROM {course_categories} cc
2111
                          JOIN {course} c ON c.category = cc.id
2112
                         WHERE cc.id {$categoriessql}
2113
                      GROUP BY cc.id
2114
                        HAVING COUNT(c.id) > :limit";
2115
                $excessivecategories = $DB->get_records_sql($sql, $params);
2116
                foreach ($categories as &$category) {
2117
                    if (array_key_exists($category->key, $excessivecategories) && !$this->can_add_more_courses_to_category($category)) {
2118
                        $url = new moodle_url('/course/index.php', array('categoryid' => $category->key));
2119
                        $category->add(get_string('viewallcourses'), $url, self::TYPE_SETTING);
2120
                    }
2121
                }
2122
            }
2123
        }
2124
    }
2125
 
2126
    /**
2127
     * Adds a structured category to the navigation in the correct order/place
2128
     *
2129
     * @param stdClass $category category to be added in navigation.
2130
     * @param navigation_node $parent parent navigation node
2131
     * @param int $nodetype type of node, if category is under MyHome then it's TYPE_MY_CATEGORY
2132
     * @return void.
2133
     */
2134
    protected function add_category(stdClass $category, navigation_node $parent, $nodetype = self::TYPE_CATEGORY) {
2135
        global $CFG;
2136
        if (array_key_exists($category->id, $this->addedcategories)) {
2137
            return;
2138
        }
2139
        $canview = core_course_category::can_view_category($category);
2140
        $url = $canview ? new moodle_url('/course/index.php', array('categoryid' => $category->id)) : null;
2141
        $context = \context_helper::get_navigation_filter_context(context_coursecat::instance($category->id));
2142
        $categoryname = $canview ? format_string($category->name, true, ['context' => $context]) :
2143
            get_string('categoryhidden');
2144
        $categorynode = $parent->add($categoryname, $url, $nodetype, $categoryname, $category->id);
2145
        if (!$canview) {
2146
            // User does not have required capabilities to view category.
2147
            $categorynode->display = false;
2148
        } else if (!$category->visible) {
2149
            // Category is hidden but user has capability to view hidden categories.
2150
            $categorynode->hidden = true;
2151
        }
2152
        $this->addedcategories[$category->id] = $categorynode;
2153
    }
2154
 
2155
    /**
2156
     * Loads the given course into the navigation
2157
     *
2158
     * @param stdClass $course
2159
     * @return navigation_node
2160
     */
2161
    protected function load_course(stdClass $course) {
2162
        global $SITE;
2163
        if ($course->id == $SITE->id) {
2164
            // This is always loaded during initialisation
2165
            return $this->rootnodes['site'];
2166
        } else if (array_key_exists($course->id, $this->addedcourses)) {
2167
            // The course has already been loaded so return a reference
2168
            return $this->addedcourses[$course->id];
2169
        } else {
2170
            // Add the course
2171
            return $this->add_course($course);
2172
        }
2173
    }
2174
 
2175
    /**
2176
     * Loads all of the courses section into the navigation.
2177
     *
2178
     * This function calls method from current course format, see
2179
     * core_courseformat\base::extend_course_navigation()
2180
     * If course module ($cm) is specified but course format failed to create the node,
2181
     * the activity node is created anyway.
2182
     *
2183
     * By default course formats call the method global_navigation::load_generic_course_sections()
2184
     *
2185
     * @param stdClass $course Database record for the course
2186
     * @param navigation_node $coursenode The course node within the navigation
2187
     * @param null|int $sectionnum If specified load the contents of section with this relative number
2188
     * @param null|cm_info $cm If specified make sure that activity node is created (either
2189
     *    in containg section or by calling load_stealth_activity() )
2190
     */
2191
    protected function load_course_sections(stdClass $course, navigation_node $coursenode, $sectionnum = null, $cm = null) {
2192
        global $CFG, $SITE;
2193
        require_once($CFG->dirroot.'/course/lib.php');
2194
        if (isset($cm->sectionnum)) {
2195
            $sectionnum = $cm->sectionnum;
2196
        }
2197
        if ($sectionnum !== null) {
2198
            $this->includesectionnum = $sectionnum;
2199
        }
2200
        course_get_format($course)->extend_course_navigation($this, $coursenode, $sectionnum, $cm);
2201
        if (isset($cm->id)) {
2202
            $activity = $coursenode->find($cm->id, self::TYPE_ACTIVITY);
2203
            if (empty($activity)) {
2204
                $activity = $this->load_stealth_activity($coursenode, get_fast_modinfo($course));
2205
            }
2206
        }
2207
   }
2208
 
2209
    /**
2210
     * Generates an array of sections and an array of activities for the given course.
2211
     *
2212
     * This method uses the cache to improve performance and avoid the get_fast_modinfo call
2213
     *
2214
     * @param stdClass $course
2215
     * @return array Array($sections, $activities)
2216
     */
2217
    protected function generate_sections_and_activities(stdClass $course) {
2218
        global $CFG;
2219
        require_once($CFG->dirroot.'/course/lib.php');
2220
 
2221
        $modinfo = get_fast_modinfo($course);
2222
        $sections = $modinfo->get_section_info_all();
2223
 
2224
        // For course formats using 'numsections' trim the sections list
2225
        $courseformatoptions = course_get_format($course)->get_format_options();
2226
        if (isset($courseformatoptions['numsections'])) {
2227
            $sections = array_slice($sections, 0, $courseformatoptions['numsections']+1, true);
2228
        }
2229
 
2230
        $activities = array();
2231
 
2232
        foreach ($sections as $key => $section) {
2233
            // Clone and unset summary to prevent $SESSION bloat (MDL-31802).
2234
            $sections[$key] = clone($section);
2235
            unset($sections[$key]->summary);
2236
            $sections[$key]->hasactivites = false;
2237
            if (!array_key_exists($section->section, $modinfo->sections)) {
2238
                continue;
2239
            }
2240
            foreach ($modinfo->sections[$section->section] as $cmid) {
2241
                $cm = $modinfo->cms[$cmid];
2242
                $activity = new stdClass;
2243
                $activity->id = $cm->id;
2244
                $activity->course = $course->id;
2245
                $activity->section = $section->section;
2246
                $activity->name = $cm->name;
2247
                $activity->icon = $cm->icon;
2248
                $activity->iconcomponent = $cm->iconcomponent;
2249
                $activity->hidden = (!$cm->visible);
2250
                $activity->modname = $cm->modname;
2251
                $activity->nodetype = navigation_node::NODETYPE_LEAF;
2252
                $activity->onclick = $cm->onclick;
2253
                $url = $cm->url;
2254
                if (!$url) {
2255
                    $activity->url = null;
2256
                    $activity->display = false;
2257
                } else {
2258
                    $activity->url = $url->out();
2259
                    $activity->display = $cm->is_visible_on_course_page() ? true : false;
2260
                    if (self::module_extends_navigation($cm->modname)) {
2261
                        $activity->nodetype = navigation_node::NODETYPE_BRANCH;
2262
                    }
2263
                }
2264
                $activities[$cmid] = $activity;
2265
                if ($activity->display) {
2266
                    $sections[$key]->hasactivites = true;
2267
                }
2268
            }
2269
        }
2270
 
2271
        return array($sections, $activities);
2272
    }
2273
 
2274
    /**
2275
     * Generically loads the course sections into the course's navigation.
2276
     *
2277
     * @param stdClass $course
2278
     * @param navigation_node $coursenode
2279
     * @return array An array of course section nodes
2280
     */
2281
    public function load_generic_course_sections(stdClass $course, navigation_node $coursenode) {
2282
        global $CFG, $DB, $USER, $SITE;
2283
        require_once($CFG->dirroot.'/course/lib.php');
2284
 
2285
        list($sections, $activities) = $this->generate_sections_and_activities($course);
2286
 
2287
        $navigationsections = array();
2288
        foreach ($sections as $sectionid => $section) {
2289
            $section = clone($section);
2290
            if ($course->id == $SITE->id) {
2291
                $this->load_section_activities($coursenode, $section->section, $activities);
2292
            } else {
2293
                if (!$section->uservisible || (!$this->showemptysections &&
2294
                        !$section->hasactivites && $this->includesectionnum !== $section->section)) {
2295
                    continue;
2296
                }
2297
 
2298
                $sectionname = get_section_name($course, $section);
2299
                $url = course_get_url($course, $section->section, array('navigation' => true));
2300
 
2301
                $sectionnode = $coursenode->add($sectionname, $url, navigation_node::TYPE_SECTION,
2302
                    null, $section->id, new pix_icon('i/section', ''));
2303
                $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH;
2304
                $sectionnode->hidden = (!$section->visible || !$section->available);
2305
                $sectionnode->add_attribute('data-section-name-for', $section->id);
2306
                if ($this->includesectionnum !== false && $this->includesectionnum == $section->section) {
2307
                    $this->load_section_activities($sectionnode, $section->section, $activities);
2308
                }
2309
                $navigationsections[$sectionid] = $section;
2310
            }
2311
        }
2312
        return $navigationsections;
2313
    }
2314
 
2315
    /**
2316
     * Loads all of the activities for a section into the navigation structure.
2317
     *
2318
     * @param navigation_node $sectionnode
2319
     * @param int $sectionnumber
2320
     * @param array $activities An array of activites as returned by {@link global_navigation::generate_sections_and_activities()}
2321
     * @param stdClass $course The course object the section and activities relate to.
2322
     * @return array Array of activity nodes
2323
     */
2324
    protected function load_section_activities(navigation_node $sectionnode, $sectionnumber, array $activities, $course = null) {
2325
        global $CFG, $SITE;
2326
        // A static counter for JS function naming
2327
        static $legacyonclickcounter = 0;
2328
 
2329
        $activitynodes = array();
2330
        if (empty($activities)) {
2331
            return $activitynodes;
2332
        }
2333
 
2334
        if (!is_object($course)) {
2335
            $activity = reset($activities);
2336
            $courseid = $activity->course;
2337
        } else {
2338
            $courseid = $course->id;
2339
        }
2340
        $showactivities = ($courseid != $SITE->id || !empty($CFG->navshowfrontpagemods));
2341
 
2342
        foreach ($activities as $activity) {
2343
            if ($activity->section != $sectionnumber) {
2344
                continue;
2345
            }
2346
            if ($activity->icon) {
2347
                $icon = new pix_icon($activity->icon, get_string('modulename', $activity->modname), $activity->iconcomponent);
2348
            } else {
2349
                $icon = new pix_icon('monologo', get_string('modulename', $activity->modname), $activity->modname);
2350
            }
2351
 
2352
            // Prepare the default name and url for the node
2353
            $displaycontext = \context_helper::get_navigation_filter_context(context_module::instance($activity->id));
2354
            $activityname = format_string($activity->name, true, ['context' => $displaycontext]);
2355
            $action = new moodle_url($activity->url);
2356
 
2357
            // Check if the onclick property is set (puke!)
2358
            if (!empty($activity->onclick)) {
2359
                // Increment the counter so that we have a unique number.
2360
                $legacyonclickcounter++;
2361
                // Generate the function name we will use
2362
                $functionname = 'legacy_activity_onclick_handler_'.$legacyonclickcounter;
2363
                $propogrationhandler = '';
2364
                // Check if we need to cancel propogation. Remember inline onclick
2365
                // events would return false if they wanted to prevent propogation and the
2366
                // default action.
2367
                if (strpos($activity->onclick, 'return false')) {
2368
                    $propogrationhandler = 'e.halt();';
2369
                }
2370
                // Decode the onclick - it has already been encoded for display (puke)
2371
                $onclick = htmlspecialchars_decode($activity->onclick, ENT_QUOTES);
2372
                // Build the JS function the click event will call
2373
                $jscode = "function {$functionname}(e) { $propogrationhandler $onclick }";
2374
                $this->page->requires->js_amd_inline($jscode);
2375
                // Override the default url with the new action link
2376
                $action = new action_link($action, $activityname, new component_action('click', $functionname));
2377
            }
2378
 
2379
            $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon);
2380
            $activitynode->title(get_string('modulename', $activity->modname));
2381
            $activitynode->hidden = $activity->hidden;
2382
            $activitynode->display = $showactivities && $activity->display;
2383
            $activitynode->nodetype = $activity->nodetype;
2384
            $activitynodes[$activity->id] = $activitynode;
2385
        }
2386
 
2387
        return $activitynodes;
2388
    }
2389
    /**
2390
     * Loads a stealth module from unavailable section
2391
     * @param navigation_node $coursenode
2392
     * @param stdClass|course_modinfo $modinfo
2393
     * @return navigation_node or null if not accessible
2394
     */
2395
    protected function load_stealth_activity(navigation_node $coursenode, $modinfo) {
2396
        if (empty($modinfo->cms[$this->page->cm->id])) {
2397
            return null;
2398
        }
2399
        $cm = $modinfo->cms[$this->page->cm->id];
2400
        if ($cm->icon) {
2401
            $icon = new pix_icon($cm->icon, get_string('modulename', $cm->modname), $cm->iconcomponent);
2402
        } else {
2403
            $icon = new pix_icon('monologo', get_string('modulename', $cm->modname), $cm->modname);
2404
        }
2405
        $url = $cm->url;
2406
        $activitynode = $coursenode->add(format_string($cm->name), $url, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
2407
        $activitynode->title(get_string('modulename', $cm->modname));
2408
        $activitynode->hidden = (!$cm->visible);
2409
        if (!$cm->is_visible_on_course_page()) {
2410
            // Do not show any error here, let the page handle exception that activity is not visible for the current user.
2411
            // Also there may be no exception at all in case when teacher is logged in as student.
2412
            $activitynode->display = false;
2413
        } else if (!$url) {
2414
            // Don't show activities that don't have links!
2415
            $activitynode->display = false;
2416
        } else if (self::module_extends_navigation($cm->modname)) {
2417
            $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
2418
        }
2419
        return $activitynode;
2420
    }
2421
    /**
2422
     * Loads the navigation structure for the given activity into the activities node.
2423
     *
2424
     * This method utilises a callback within the modules lib.php file to load the
2425
     * content specific to activity given.
2426
     *
2427
     * The callback is a method: {modulename}_extend_navigation()
2428
     * Examples:
2429
     *  * {@link forum_extend_navigation()}
2430
     *  * {@link workshop_extend_navigation()}
2431
     *
2432
     * @param cm_info|stdClass $cm
2433
     * @param stdClass $course
2434
     * @param navigation_node $activity
2435
     * @return bool
2436
     */
2437
    protected function load_activity($cm, stdClass $course, navigation_node $activity) {
2438
        global $CFG, $DB;
2439
 
2440
        // make sure we have a $cm from get_fast_modinfo as this contains activity access details
2441
        if (!($cm instanceof cm_info)) {
2442
            $modinfo = get_fast_modinfo($course);
2443
            $cm = $modinfo->get_cm($cm->id);
2444
        }
2445
        $activity->nodetype = navigation_node::NODETYPE_LEAF;
2446
        $activity->make_active();
2447
        $file = $CFG->dirroot.'/mod/'.$cm->modname.'/lib.php';
2448
        $function = $cm->modname.'_extend_navigation';
2449
 
2450
        if (file_exists($file)) {
2451
            require_once($file);
2452
            if (function_exists($function)) {
2453
                $activtyrecord = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
2454
                $function($activity, $course, $activtyrecord, $cm);
2455
            }
2456
        }
2457
 
2458
        // Allow the active advanced grading method plugin to append module navigation
2459
        $featuresfunc = $cm->modname.'_supports';
2460
        if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING)) {
2461
            require_once($CFG->dirroot.'/grade/grading/lib.php');
2462
            $gradingman = get_grading_manager($cm->context,  'mod_'.$cm->modname);
2463
            $gradingman->extend_navigation($this, $activity);
2464
        }
2465
 
2466
        return $activity->has_children();
2467
    }
2468
    /**
2469
     * Loads user specific information into the navigation in the appropriate place.
2470
     *
2471
     * If no user is provided the current user is assumed.
2472
     *
2473
     * @param stdClass $user
2474
     * @param bool $forceforcontext probably force something to be loaded somewhere (ask SamH if not sure what this means)
2475
     * @return bool
2476
     */
2477
    protected function load_for_user($user=null, $forceforcontext=false) {
2478
        global $DB, $CFG, $USER, $SITE;
2479
 
2480
        require_once($CFG->dirroot . '/course/lib.php');
2481
 
2482
        if ($user === null) {
2483
            // We can't require login here but if the user isn't logged in we don't
2484
            // want to show anything
2485
            if (!isloggedin() || isguestuser()) {
2486
                return false;
2487
            }
2488
            $user = $USER;
2489
        } else if (!is_object($user)) {
2490
            // If the user is not an object then get them from the database
2491
            $select = context_helper::get_preload_record_columns_sql('ctx');
2492
            $sql = "SELECT u.*, $select
2493
                      FROM {user} u
2494
                      JOIN {context} ctx ON u.id = ctx.instanceid
2495
                     WHERE u.id = :userid AND
2496
                           ctx.contextlevel = :contextlevel";
2497
            $user = $DB->get_record_sql($sql, array('userid' => (int)$user, 'contextlevel' => CONTEXT_USER), MUST_EXIST);
2498
            context_helper::preload_from_record($user);
2499
        }
2500
 
2501
        $iscurrentuser = ($user->id == $USER->id);
2502
 
2503
        $usercontext = context_user::instance($user->id);
2504
 
2505
        // Get the course set against the page, by default this will be the site
2506
        $course = $this->page->course;
2507
        $baseargs = array('id'=>$user->id);
2508
        if ($course->id != $SITE->id && (!$iscurrentuser || $forceforcontext)) {
2509
            $coursenode = $this->add_course($course, false, self::COURSE_CURRENT);
2510
            $baseargs['course'] = $course->id;
2511
            $coursecontext = context_course::instance($course->id);
2512
            $issitecourse = false;
2513
        } else {
2514
            // Load all categories and get the context for the system
2515
            $coursecontext = context_system::instance();
2516
            $issitecourse = true;
2517
        }
2518
 
2519
        // Create a node to add user information under.
2520
        $usersnode = null;
2521
        if (!$issitecourse) {
2522
            // Not the current user so add it to the participants node for the current course.
2523
            $usersnode = $coursenode->get('participants', navigation_node::TYPE_CONTAINER);
2524
            $userviewurl = new moodle_url('/user/view.php', $baseargs);
2525
        } else if ($USER->id != $user->id) {
2526
            // This is the site so add a users node to the root branch.
2527
            $usersnode = $this->rootnodes['users'];
2528
            if (course_can_view_participants($coursecontext)) {
2529
                $usersnode->action = new moodle_url('/user/index.php', array('id' => $course->id));
2530
            }
2531
            $userviewurl = new moodle_url('/user/profile.php', $baseargs);
2532
        }
2533
        if (!$usersnode) {
2534
            // We should NEVER get here, if the course hasn't been populated
2535
            // with a participants node then the navigaiton either wasn't generated
2536
            // for it (you are missing a require_login or set_context call) or
2537
            // you don't have access.... in the interests of no leaking informatin
2538
            // we simply quit...
2539
            return false;
2540
        }
2541
        // Add a branch for the current user.
2542
        // Only reveal user details if $user is the current user, or a user to which the current user has access.
2543
        $viewprofile = true;
2544
        if (!$iscurrentuser) {
2545
            require_once($CFG->dirroot . '/user/lib.php');
2546
            if ($this->page->context->contextlevel == CONTEXT_USER && !has_capability('moodle/user:viewdetails', $usercontext) ) {
2547
                $viewprofile = false;
2548
            } else if ($this->page->context->contextlevel != CONTEXT_USER && !user_can_view_profile($user, $course, $usercontext)) {
2549
                $viewprofile = false;
2550
            }
2551
            if (!$viewprofile) {
2552
                $viewprofile = user_can_view_profile($user, null, $usercontext);
2553
            }
2554
        }
2555
 
2556
        // Now, conditionally add the user node.
2557
        if ($viewprofile) {
2558
            $canseefullname = has_capability('moodle/site:viewfullnames', $coursecontext);
2559
            $usernode = $usersnode->add(fullname($user, $canseefullname), $userviewurl, self::TYPE_USER, null, 'user' . $user->id);
2560
        } else {
2561
            $usernode = $usersnode->add(get_string('user'));
2562
        }
2563
 
2564
        if ($this->page->context->contextlevel == CONTEXT_USER && $user->id == $this->page->context->instanceid) {
2565
            $usernode->make_active();
2566
        }
2567
 
2568
        // Add user information to the participants or user node.
2569
        if ($issitecourse) {
2570
 
2571
            // If the user is the current user or has permission to view the details of the requested
2572
            // user than add a view profile link.
2573
            if ($iscurrentuser || has_capability('moodle/user:viewdetails', $coursecontext) ||
2574
                    has_capability('moodle/user:viewdetails', $usercontext)) {
2575
                if ($issitecourse || ($iscurrentuser && !$forceforcontext)) {
2576
                    $usernode->add(get_string('viewprofile'), new moodle_url('/user/profile.php', $baseargs));
2577
                } else {
2578
                    $usernode->add(get_string('viewprofile'), new moodle_url('/user/view.php', $baseargs));
2579
                }
2580
            }
2581
 
2582
            if (!empty($CFG->navadduserpostslinks)) {
2583
                // Add nodes for forum posts and discussions if the user can view either or both
2584
                // There are no capability checks here as the content of the page is based
2585
                // purely on the forums the current user has access too.
2586
                $forumtab = $usernode->add(get_string('forumposts', 'forum'));
2587
                $forumtab->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', $baseargs));
2588
                $forumtab->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php',
2589
                        array_merge($baseargs, array('mode' => 'discussions'))));
2590
            }
2591
 
2592
            // Add blog nodes.
2593
            if (!empty($CFG->enableblogs)) {
2594
                if (!$this->cache->cached('userblogoptions'.$user->id)) {
2595
                    require_once($CFG->dirroot.'/blog/lib.php');
2596
                    // Get all options for the user.
2597
                    $options = blog_get_options_for_user($user);
2598
                    $this->cache->set('userblogoptions'.$user->id, $options);
2599
                } else {
2600
                    $options = $this->cache->{'userblogoptions'.$user->id};
2601
                }
2602
 
2603
                if (count($options) > 0) {
2604
                    $blogs = $usernode->add(get_string('blogs', 'blog'), null, navigation_node::TYPE_CONTAINER);
2605
                    foreach ($options as $type => $option) {
2606
                        if ($type == "rss") {
2607
                            $blogs->add($option['string'], $option['link'], settings_navigation::TYPE_SETTING, null, null,
2608
                                    new pix_icon('i/rss', ''));
2609
                        } else {
2610
                            $blogs->add($option['string'], $option['link']);
2611
                        }
2612
                    }
2613
                }
2614
            }
2615
 
2616
            // Add the messages link.
2617
            // It is context based so can appear in the user's profile and in course participants information.
2618
            if (!empty($CFG->messaging)) {
2619
                $messageargs = array('user1' => $USER->id);
2620
                if ($USER->id != $user->id) {
2621
                    $messageargs['user2'] = $user->id;
2622
                }
2623
                $url = new moodle_url('/message/index.php', $messageargs);
2624
                $usernode->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages');
2625
            }
2626
 
2627
            // Add the "My private files" link.
2628
            // This link doesn't have a unique display for course context so only display it under the user's profile.
2629
            if ($issitecourse && $iscurrentuser && has_capability('moodle/user:manageownfiles', $usercontext)) {
2630
                $url = new moodle_url('/user/files.php');
2631
                $usernode->add(get_string('privatefiles'), $url, self::TYPE_SETTING, null, 'privatefiles');
2632
            }
2633
 
2634
            // Add a node to view the users notes if permitted.
2635
            if (!empty($CFG->enablenotes) &&
2636
                    has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $coursecontext)) {
2637
                $url = new moodle_url('/notes/index.php', array('user' => $user->id));
2638
                if ($coursecontext->instanceid != SITEID) {
2639
                    $url->param('course', $coursecontext->instanceid);
2640
                }
2641
                $usernode->add(get_string('notes', 'notes'), $url);
2642
            }
2643
 
2644
            // Show the grades node.
2645
            if (($issitecourse && $iscurrentuser) || has_capability('moodle/user:viewdetails', $usercontext)) {
2646
                require_once($CFG->dirroot . '/user/lib.php');
2647
                // Set the grades node to link to the "Grades" page.
2648
                if ($course->id == SITEID) {
2649
                    $url = user_mygrades_url($user->id, $course->id);
2650
                } else { // Otherwise we are in a course and should redirect to the user grade report (Activity report version).
2651
                    $url = new moodle_url('/course/user.php', array('mode' => 'grade', 'id' => $course->id, 'user' => $user->id));
2652
                }
2653
                if ($USER->id != $user->id) {
2654
                    $usernode->add(get_string('grades', 'grades'), $url, self::TYPE_SETTING, null, 'usergrades');
2655
                } else {
2656
                    $usernode->add(get_string('grades', 'grades'), $url);
2657
                }
2658
            }
2659
 
2660
            // If the user is the current user add the repositories for the current user.
2661
            $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
2662
            if (!$iscurrentuser &&
2663
                    $course->id == $SITE->id &&
2664
                    has_capability('moodle/user:viewdetails', $usercontext) &&
2665
                    (!in_array('mycourses', $hiddenfields) || has_capability('moodle/user:viewhiddendetails', $coursecontext))) {
2666
 
2667
                // Add view grade report is permitted.
2668
                $reports = core_component::get_plugin_list('gradereport');
2669
                arsort($reports); // User is last, we want to test it first.
2670
 
2671
                $userscourses = enrol_get_users_courses($user->id, false, '*');
2672
                $userscoursesnode = $usernode->add(get_string('courses'));
2673
 
2674
                $count = 0;
2675
                foreach ($userscourses as $usercourse) {
2676
                    if ($count === (int)$CFG->navcourselimit) {
2677
                        $url = new moodle_url('/user/profile.php', array('id' => $user->id, 'showallcourses' => 1));
2678
                        $userscoursesnode->add(get_string('showallcourses'), $url);
2679
                        break;
2680
                    }
2681
                    $count++;
2682
                    $usercoursecontext = context_course::instance($usercourse->id);
2683
                    $usercourseshortname = format_string($usercourse->shortname, true, array('context' => $usercoursecontext));
2684
                    $usercoursenode = $userscoursesnode->add($usercourseshortname, new moodle_url('/user/view.php',
2685
                            array('id' => $user->id, 'course' => $usercourse->id)), self::TYPE_CONTAINER);
2686
 
2687
                    $gradeavailable = has_capability('moodle/grade:view', $usercoursecontext);
2688
                    if (!$gradeavailable && !empty($usercourse->showgrades) && is_array($reports) && !empty($reports)) {
2689
                        foreach ($reports as $plugin => $plugindir) {
2690
                            if (has_capability('gradereport/'.$plugin.':view', $usercoursecontext)) {
2691
                                // Stop when the first visible plugin is found.
2692
                                $gradeavailable = true;
2693
                                break;
2694
                            }
2695
                        }
2696
                    }
2697
 
2698
                    if ($gradeavailable) {
2699
                        $url = new moodle_url('/grade/report/index.php', array('id' => $usercourse->id));
2700
                        $usercoursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null, null,
2701
                                new pix_icon('i/grades', ''));
2702
                    }
2703
 
2704
                    // Add a node to view the users notes if permitted.
2705
                    if (!empty($CFG->enablenotes) &&
2706
                            has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $usercoursecontext)) {
2707
                        $url = new moodle_url('/notes/index.php', array('user' => $user->id, 'course' => $usercourse->id));
2708
                        $usercoursenode->add(get_string('notes', 'notes'), $url, self::TYPE_SETTING);
2709
                    }
2710
 
2711
                    if (can_access_course($usercourse, $user->id, '', true)) {
2712
                        $usercoursenode->add(get_string('entercourse'), new moodle_url('/course/view.php',
2713
                                array('id' => $usercourse->id)), self::TYPE_SETTING, null, null, new pix_icon('i/course', ''));
2714
                    }
2715
 
2716
                    $reporttab = $usercoursenode->add(get_string('activityreports'));
2717
 
2718
                    $reportfunctions = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php');
2719
                    foreach ($reportfunctions as $reportfunction) {
2720
                        $reportfunction($reporttab, $user, $usercourse);
2721
                    }
2722
 
2723
                    $reporttab->trim_if_empty();
2724
                }
2725
            }
2726
 
2727
            // Let plugins hook into user navigation.
2728
            $pluginsfunction = get_plugins_with_function('extend_navigation_user', 'lib.php');
2729
            foreach ($pluginsfunction as $plugintype => $plugins) {
2730
                if ($plugintype != 'report') {
2731
                    foreach ($plugins as $pluginfunction) {
2732
                        $pluginfunction($usernode, $user, $usercontext, $course, $coursecontext);
2733
                    }
2734
                }
2735
            }
2736
        }
2737
        return true;
2738
    }
2739
 
2740
    /**
2741
     * This method simply checks to see if a given module can extend the navigation.
2742
     *
2743
     * @todo (MDL-25290) A shared caching solution should be used to save details on what extends navigation.
2744
     *
2745
     * @param string $modname
2746
     * @return bool
2747
     */
2748
    public static function module_extends_navigation($modname) {
2749
        global $CFG;
2750
        static $extendingmodules = array();
2751
        if (!array_key_exists($modname, $extendingmodules)) {
2752
            $extendingmodules[$modname] = false;
2753
            $file = $CFG->dirroot.'/mod/'.$modname.'/lib.php';
2754
            if (file_exists($file)) {
2755
                $function = $modname.'_extend_navigation';
2756
                require_once($file);
2757
                $extendingmodules[$modname] = (function_exists($function));
2758
            }
2759
        }
2760
        return $extendingmodules[$modname];
2761
    }
2762
    /**
2763
     * Extends the navigation for the given user.
2764
     *
2765
     * @param stdClass $user A user from the database
2766
     */
2767
    public function extend_for_user($user) {
2768
        $this->extendforuser[] = $user;
2769
    }
2770
 
2771
    /**
2772
     * Returns all of the users the navigation is being extended for
2773
     *
2774
     * @return array An array of extending users.
2775
     */
2776
    public function get_extending_users() {
2777
        return $this->extendforuser;
2778
    }
2779
    /**
2780
     * Adds the given course to the navigation structure.
2781
     *
2782
     * @param stdClass $course
2783
     * @param bool $forcegeneric
2784
     * @param bool $ismycourse
2785
     * @return navigation_node
2786
     */
2787
    public function add_course(stdClass $course, $forcegeneric = false, $coursetype = self::COURSE_OTHER) {
2788
        global $CFG, $SITE;
2789
 
2790
        // We found the course... we can return it now :)
2791
        if (!$forcegeneric && array_key_exists($course->id, $this->addedcourses)) {
2792
            return $this->addedcourses[$course->id];
2793
        }
2794
 
2795
        $coursecontext = context_course::instance($course->id);
2796
 
2797
        if ($coursetype != self::COURSE_MY && $coursetype != self::COURSE_CURRENT && $course->id != $SITE->id) {
2798
            if (is_role_switched($course->id)) {
2799
                // user has to be able to access course in order to switch, let's skip the visibility test here
2800
            } else if (!core_course_category::can_view_course_info($course)) {
2801
                return false;
2802
            }
2803
        }
2804
 
2805
        $issite = ($course->id == $SITE->id);
2806
        $displaycontext = \context_helper::get_navigation_filter_context($coursecontext);
2807
        $shortname = format_string($course->shortname, true, ['context' => $displaycontext]);
2808
        $fullname = format_string($course->fullname, true, ['context' => $displaycontext]);
2809
        // This is the name that will be shown for the course.
2810
        $coursename = empty($CFG->navshowfullcoursenames) ? $shortname : $fullname;
2811
 
2812
        if ($coursetype == self::COURSE_CURRENT) {
2813
            if ($coursenode = $this->rootnodes['mycourses']->find($course->id, self::TYPE_COURSE)) {
2814
                return $coursenode;
2815
            } else {
2816
                $coursetype = self::COURSE_OTHER;
2817
            }
2818
        }
2819
 
2820
        // Can the user expand the course to see its content.
2821
        $canexpandcourse = true;
2822
        if ($issite) {
2823
            $parent = $this;
2824
            $url = null;
2825
            if (empty($CFG->usesitenameforsitepages)) {
2826
                $coursename = get_string('sitepages');
2827
            }
2828
        } else if ($coursetype == self::COURSE_CURRENT) {
2829
            $parent = $this->rootnodes['currentcourse'];
2830
            $url = new moodle_url('/course/view.php', array('id'=>$course->id));
2831
            $canexpandcourse = $this->can_expand_course($course);
2832
        } else if ($coursetype == self::COURSE_MY && !$forcegeneric) {
2833
            if (!empty($CFG->navshowmycoursecategories) && ($parent = $this->rootnodes['mycourses']->find($course->category, self::TYPE_MY_CATEGORY))) {
2834
                // Nothing to do here the above statement set $parent to the category within mycourses.
2835
            } else {
2836
                $parent = $this->rootnodes['mycourses'];
2837
            }
2838
            $url = new moodle_url('/course/view.php', array('id'=>$course->id));
2839
        } else {
2840
            $parent = $this->rootnodes['courses'];
2841
            $url = new moodle_url('/course/view.php', array('id'=>$course->id));
2842
            // They can only expand the course if they can access it.
2843
            $canexpandcourse = $this->can_expand_course($course);
2844
            if (!empty($course->category) && $this->show_categories($coursetype == self::COURSE_MY)) {
2845
                if (!$this->is_category_fully_loaded($course->category)) {
2846
                    // We need to load the category structure for this course
2847
                    $this->load_all_categories($course->category, false);
2848
                }
2849
                if (array_key_exists($course->category, $this->addedcategories)) {
2850
                    $parent = $this->addedcategories[$course->category];
2851
                    // This could lead to the course being created so we should check whether it is the case again
2852
                    if (!$forcegeneric && array_key_exists($course->id, $this->addedcourses)) {
2853
                        return $this->addedcourses[$course->id];
2854
                    }
2855
                }
2856
            }
2857
        }
2858
 
2859
        $coursenode = $parent->add($coursename, $url, self::TYPE_COURSE, $shortname, $course->id, new pix_icon('i/course', ''));
2860
        $coursenode->showinflatnavigation = $coursetype == self::COURSE_MY;
2861
 
2862
        $coursenode->hidden = (!$course->visible);
2863
        $coursenode->title(format_string($course->fullname, true, ['context' => $displaycontext, 'escape' => false]));
2864
        if ($canexpandcourse) {
2865
            // This course can be expanded by the user, make it a branch to make the system aware that its expandable by ajax.
2866
            $coursenode->nodetype = self::NODETYPE_BRANCH;
2867
            $coursenode->isexpandable = true;
2868
        } else {
2869
            $coursenode->nodetype = self::NODETYPE_LEAF;
2870
            $coursenode->isexpandable = false;
2871
        }
2872
        if (!$forcegeneric) {
2873
            $this->addedcourses[$course->id] = $coursenode;
2874
        }
2875
 
2876
        return $coursenode;
2877
    }
2878
 
2879
    /**
2880
     * Returns a cache instance to use for the expand course cache.
2881
     * @return cache_session
2882
     */
2883
    protected function get_expand_course_cache() {
2884
        if ($this->cacheexpandcourse === null) {
2885
            $this->cacheexpandcourse = cache::make('core', 'navigation_expandcourse');
2886
        }
2887
        return $this->cacheexpandcourse;
2888
    }
2889
 
2890
    /**
2891
     * Checks if a user can expand a course in the navigation.
2892
     *
2893
     * We use a cache here because in order to be accurate we need to call can_access_course which is a costly function.
2894
     * Because this functionality is basic + non-essential and because we lack good event triggering this cache
2895
     * permits stale data.
2896
     * In the situation the user is granted access to a course after we've initialised this session cache the cache
2897
     * will be stale.
2898
     * It is brought up to date in only one of two ways.
2899
     *   1. The user logs out and in again.
2900
     *   2. The user browses to the course they've just being given access to.
2901
     *
2902
     * Really all this controls is whether the node is shown as expandable or not. It is uber un-important.
2903
     *
2904
     * @param stdClass $course
2905
     * @return bool
2906
     */
2907
    protected function can_expand_course($course) {
2908
        $cache = $this->get_expand_course_cache();
2909
        $canexpand = $cache->get($course->id);
2910
        if ($canexpand === false) {
2911
            $canexpand = isloggedin() && can_access_course($course, null, '', true);
2912
            $canexpand = (int)$canexpand;
2913
            $cache->set($course->id, $canexpand);
2914
        }
2915
        return ($canexpand === 1);
2916
    }
2917
 
2918
    /**
2919
     * Returns true if the category has already been loaded as have any child categories
2920
     *
2921
     * @param int $categoryid
2922
     * @return bool
2923
     */
2924
    protected function is_category_fully_loaded($categoryid) {
2925
        return (array_key_exists($categoryid, $this->addedcategories) && ($this->allcategoriesloaded || $this->addedcategories[$categoryid]->children->count() > 0));
2926
    }
2927
 
2928
    /**
2929
     * Adds essential course nodes to the navigation for the given course.
2930
     *
2931
     * This method adds nodes such as reports, blogs and participants
2932
     *
2933
     * @param navigation_node $coursenode
2934
     * @param stdClass $course
2935
     * @return bool returns true on successful addition of a node.
2936
     */
2937
    public function add_course_essentials($coursenode, stdClass $course) {
2938
        global $CFG, $SITE;
2939
        require_once($CFG->dirroot . '/course/lib.php');
2940
 
2941
        if ($course->id == $SITE->id) {
2942
            return $this->add_front_page_course_essentials($coursenode, $course);
2943
        }
2944
 
2945
        if ($coursenode == false || !($coursenode instanceof navigation_node) || $coursenode->get('participants', navigation_node::TYPE_CONTAINER)) {
2946
            return true;
2947
        }
2948
 
2949
        $navoptions = course_get_user_navigation_options($this->page->context, $course);
2950
 
2951
        //Participants
2952
        if ($navoptions->participants) {
2953
            $participants = $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id),
2954
                self::TYPE_CONTAINER, get_string('participants'), 'participants', new pix_icon('i/users', ''));
2955
 
2956
            if ($navoptions->blogs) {
2957
                $blogsurls = new moodle_url('/blog/index.php');
2958
                if ($currentgroup = groups_get_course_group($course, true)) {
2959
                    $blogsurls->param('groupid', $currentgroup);
2960
                } else {
2961
                    $blogsurls->param('courseid', $course->id);
2962
                }
2963
                $participants->add(get_string('blogscourse', 'blog'), $blogsurls->out(), self::TYPE_SETTING, null, 'courseblogs');
2964
            }
2965
 
2966
            if ($navoptions->notes) {
2967
                $participants->add(get_string('notes', 'notes'), new moodle_url('/notes/index.php', array('filtertype' => 'course', 'filterselect' => $course->id)), self::TYPE_SETTING, null, 'currentcoursenotes');
2968
            }
2969
        } else if (count($this->extendforuser) > 0) {
2970
            $coursenode->add(get_string('participants'), null, self::TYPE_CONTAINER, get_string('participants'), 'participants');
2971
        } else if ($siteparticipantsnode = $this->rootnodes['site']->get('participants', self::TYPE_CUSTOM)) {
2972
            // The participants node was added for the site, but cannot be viewed inside the course itself, so remove.
2973
            $siteparticipantsnode->remove();
2974
        }
2975
 
2976
        // Badges.
2977
        if ($navoptions->badges) {
2978
            $url = new moodle_url('/badges/view.php', array('type' => 2, 'id' => $course->id));
2979
 
2980
            $coursenode->add(get_string('coursebadges', 'badges'), $url,
2981
                    navigation_node::TYPE_SETTING, null, 'badgesview',
2982
                    new pix_icon('i/badge', get_string('coursebadges', 'badges')));
2983
        }
2984
 
2985
        // Check access to the course and competencies page.
2986
        if ($navoptions->competencies) {
2987
            // Just a link to course competency.
2988
            $title = get_string('competencies', 'core_competency');
2989
            $path = new moodle_url("/admin/tool/lp/coursecompetencies.php", array('courseid' => $course->id));
2990
            $coursenode->add($title, $path, navigation_node::TYPE_SETTING, null, 'competencies',
2991
                    new pix_icon('i/competencies', ''));
2992
        }
2993
        if ($navoptions->grades) {
2994
            $url = new moodle_url('/grade/report/index.php', array('id'=>$course->id));
2995
            $gradenode = $coursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null,
2996
                'grades', new pix_icon('i/grades', ''));
2997
            // If the page type matches the grade part, then make the nav drawer grade node (incl. all sub pages) active.
2998
            if ($this->page->context->contextlevel < CONTEXT_MODULE && strpos($this->page->pagetype, 'grade-') === 0) {
2999
                $gradenode->make_active();
3000
            }
3001
        }
3002
 
3003
        // Add link for configuring communication.
3004
        if ($navoptions->communication) {
3005
            $url = new moodle_url('/communication/configure.php', [
3006
                'contextid' => \core\context\course::instance($course->id)->id,
3007
                'instanceid' => $course->id,
3008
                'instancetype' => 'coursecommunication',
3009
                'component' => 'core_course',
3010
            ]);
3011
            $coursenode->add(get_string('communication', 'communication'), $url,
3012
                navigation_node::TYPE_SETTING, null, 'communication');
3013
        }
3014
 
3015
        return true;
3016
    }
3017
    /**
3018
     * This generates the structure of the course that won't be generated when
3019
     * the modules and sections are added.
3020
     *
3021
     * Things such as the reports branch, the participants branch, blogs... get
3022
     * added to the course node by this method.
3023
     *
3024
     * @param navigation_node $coursenode
3025
     * @param stdClass $course
3026
     * @return bool True for successfull generation
3027
     */
3028
    public function add_front_page_course_essentials(navigation_node $coursenode, stdClass $course) {
3029
        global $CFG, $USER, $COURSE, $SITE;
3030
        require_once($CFG->dirroot . '/course/lib.php');
3031
 
3032
        if ($coursenode == false || $coursenode->get('frontpageloaded', navigation_node::TYPE_CUSTOM)) {
3033
            return true;
3034
        }
3035
 
3036
        $systemcontext = context_system::instance();
3037
        $navoptions = course_get_user_navigation_options($systemcontext, $course);
3038
 
3039
        // Hidden node that we use to determine if the front page navigation is loaded.
3040
        // This required as there are not other guaranteed nodes that may be loaded.
3041
        $coursenode->add('frontpageloaded', null, self::TYPE_CUSTOM, null, 'frontpageloaded')->display = false;
3042
 
3043
        // Add My courses to the site pages within the navigation structure so the block can read it.
3044
        $coursenode->add(get_string('mycourses'), new moodle_url('/my/courses.php'), self::TYPE_CUSTOM, null, 'mycourses');
3045
 
3046
        // Participants.
3047
        if ($navoptions->participants) {
3048
            $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id),
3049
                self::TYPE_CUSTOM, get_string('participants'), 'participants');
3050
        }
3051
 
3052
        // Blogs.
3053
        if ($navoptions->blogs) {
3054
            $blogsurls = new moodle_url('/blog/index.php');
3055
            $coursenode->add(get_string('blogssite', 'blog'), $blogsurls->out(), self::TYPE_SYSTEM, null, 'siteblog');
3056
        }
3057
 
3058
        $filterselect = 0;
3059
 
3060
        // Badges.
3061
        if ($navoptions->badges) {
3062
            $url = new moodle_url($CFG->wwwroot . '/badges/view.php', array('type' => 1));
3063
            $coursenode->add(get_string('sitebadges', 'badges'), $url, navigation_node::TYPE_CUSTOM);
3064
        }
3065
 
3066
        // Notes.
3067
        if ($navoptions->notes) {
3068
            $coursenode->add(get_string('notes', 'notes'), new moodle_url('/notes/index.php',
3069
                array('filtertype' => 'course', 'filterselect' => $filterselect)), self::TYPE_SETTING, null, 'notes');
3070
        }
3071
 
3072
        // Tags
3073
        if ($navoptions->tags) {
3074
            $node = $coursenode->add(get_string('tags', 'tag'), new moodle_url('/tag/search.php'),
3075
                    self::TYPE_SETTING, null, 'tags');
3076
        }
3077
 
3078
        // Search.
3079
        if ($navoptions->search) {
3080
            $node = $coursenode->add(get_string('search', 'search'), new moodle_url('/search/index.php'),
3081
                    self::TYPE_SETTING, null, 'search');
3082
        }
3083
 
3084
        if (isloggedin()) {
3085
            $usercontext = context_user::instance($USER->id);
3086
            if (has_capability('moodle/user:manageownfiles', $usercontext)) {
3087
                $url = new moodle_url('/user/files.php');
3088
                $node = $coursenode->add(get_string('privatefiles'), $url,
3089
                    self::TYPE_SETTING, null, 'privatefiles', new pix_icon('i/privatefiles', ''));
3090
                $node->display = false;
3091
                $node->showinflatnavigation = true;
3092
                $node->mainnavonly = true;
3093
            }
3094
        }
3095
 
3096
        if (isloggedin()) {
3097
            $context = $this->page->context;
3098
            switch ($context->contextlevel) {
3099
                case CONTEXT_COURSECAT:
3100
                    // OK, expected context level.
3101
                    break;
3102
                case CONTEXT_COURSE:
3103
                    // OK, expected context level if not on frontpage.
3104
                    if ($COURSE->id != $SITE->id) {
3105
                        break;
3106
                    }
3107
                default:
3108
                    // If this context is part of a course (excluding frontpage), use the course context.
3109
                    // Otherwise, use the system context.
3110
                    $coursecontext = $context->get_course_context(false);
3111
                    if ($coursecontext && $coursecontext->instanceid !== $SITE->id) {
3112
                        $context = $coursecontext;
3113
                    } else {
3114
                        $context = $systemcontext;
3115
                    }
3116
            }
3117
 
3118
            $params = ['contextid' => $context->id];
3119
            if (has_capability('moodle/contentbank:access', $context)) {
3120
                $url = new moodle_url('/contentbank/index.php', $params);
3121
                $node = $coursenode->add(get_string('contentbank'), $url,
3122
                    self::TYPE_CUSTOM, null, 'contentbank', new pix_icon('i/contentbank', ''));
3123
                $node->showinflatnavigation = true;
3124
            }
3125
        }
3126
 
3127
        return true;
3128
    }
3129
 
3130
    /**
3131
     * Clears the navigation cache
3132
     */
3133
    public function clear_cache() {
3134
        $this->cache->clear();
3135
    }
3136
 
3137
    /**
3138
     * Sets an expansion limit for the navigation
3139
     *
3140
     * The expansion limit is used to prevent the display of content that has a type
3141
     * greater than the provided $type.
3142
     *
3143
     * Can be used to ensure things such as activities or activity content don't get
3144
     * shown on the navigation.
3145
     * They are still generated in order to ensure the navbar still makes sense.
3146
     *
3147
     * @param int $type One of navigation_node::TYPE_*
3148
     * @return bool true when complete.
3149
     */
3150
    public function set_expansion_limit($type) {
3151
        global $SITE;
3152
        $nodes = $this->find_all_of_type($type);
3153
 
3154
        // We only want to hide specific types of nodes.
3155
        // Only nodes that represent "structure" in the navigation tree should be hidden.
3156
        // If we hide all nodes then we risk hiding vital information.
3157
        $typestohide = array(
3158
            self::TYPE_CATEGORY,
3159
            self::TYPE_COURSE,
3160
            self::TYPE_SECTION,
3161
            self::TYPE_ACTIVITY
3162
        );
3163
 
3164
        foreach ($nodes as $node) {
3165
            // We need to generate the full site node
3166
            if ($type == self::TYPE_COURSE && $node->key == $SITE->id) {
3167
                continue;
3168
            }
3169
            foreach ($node->children as $child) {
3170
                $child->hide($typestohide);
3171
            }
3172
        }
3173
        return true;
3174
    }
3175
    /**
3176
     * Attempts to get the navigation with the given key from this nodes children.
3177
     *
3178
     * This function only looks at this nodes children, it does NOT look recursivily.
3179
     * If the node can't be found then false is returned.
3180
     *
3181
     * If you need to search recursivily then use the {@link global_navigation::find()} method.
3182
     *
3183
     * Note: If you are trying to set the active node {@link navigation_node::override_active_url()}
3184
     * may be of more use to you.
3185
     *
3186
     * @param string|int $key The key of the node you wish to receive.
3187
     * @param int $type One of navigation_node::TYPE_*
3188
     * @return navigation_node|false
3189
     */
3190
    public function get($key, $type = null) {
3191
        if (!$this->initialised) {
3192
            $this->initialise();
3193
        }
3194
        return parent::get($key, $type);
3195
    }
3196
 
3197
    /**
3198
     * Searches this nodes children and their children to find a navigation node
3199
     * with the matching key and type.
3200
     *
3201
     * This method is recursive and searches children so until either a node is
3202
     * found or there are no more nodes to search.
3203
     *
3204
     * If you know that the node being searched for is a child of this node
3205
     * then use the {@link global_navigation::get()} method instead.
3206
     *
3207
     * Note: If you are trying to set the active node {@link navigation_node::override_active_url()}
3208
     * may be of more use to you.
3209
     *
3210
     * @param string|int $key The key of the node you wish to receive.
3211
     * @param ?int $type One of navigation_node::TYPE_*
3212
     * @return navigation_node|false
3213
     */
3214
    public function find($key, $type) {
3215
        if (!$this->initialised) {
3216
            $this->initialise();
3217
        }
3218
        if ($type == self::TYPE_ROOTNODE && array_key_exists($key, $this->rootnodes)) {
3219
            return $this->rootnodes[$key];
3220
        }
3221
        return parent::find($key, $type);
3222
    }
3223
 
3224
    /**
3225
     * They've expanded the 'my courses' branch.
3226
     */
3227
    protected function load_courses_enrolled() {
3228
        global $CFG;
3229
 
3230
        $limit = (int) $CFG->navcourselimit;
3231
 
3232
        $courses = enrol_get_my_courses('*');
3233
        $flatnavcourses = [];
3234
 
3235
        // Go through the courses and see which ones we want to display in the flatnav.
3236
        foreach ($courses as $course) {
3237
            $classify = course_classify_for_timeline($course);
3238
 
3239
            if ($classify == COURSE_TIMELINE_INPROGRESS) {
3240
                $flatnavcourses[$course->id] = $course;
3241
            }
3242
        }
3243
 
3244
        // Get the number of courses that can be displayed in the nav block and in the flatnav.
3245
        $numtotalcourses = count($courses);
3246
        $numtotalflatnavcourses = count($flatnavcourses);
3247
 
3248
        // Reduce the size of the arrays to abide by the 'navcourselimit' setting.
3249
        $courses = array_slice($courses, 0, $limit, true);
3250
        $flatnavcourses = array_slice($flatnavcourses, 0, $limit, true);
3251
 
3252
        // Get the number of courses we are going to show for each.
3253
        $numshowncourses = count($courses);
3254
        $numshownflatnavcourses = count($flatnavcourses);
3255
        if ($numshowncourses && $this->show_my_categories()) {
3256
            // Generate an array containing unique values of all the courses' categories.
3257
            $categoryids = array();
3258
            foreach ($courses as $course) {
3259
                if (in_array($course->category, $categoryids)) {
3260
                    continue;
3261
                }
3262
                $categoryids[] = $course->category;
3263
            }
3264
 
3265
            // Array of category IDs that include the categories of the user's courses and the related course categories.
3266
            $fullpathcategoryids = [];
3267
            // Get the course categories for the enrolled courses' category IDs.
3268
            $mycoursecategories = core_course_category::get_many($categoryids);
3269
            // Loop over each of these categories and build the category tree using each category's path.
3270
            foreach ($mycoursecategories as $mycoursecat) {
3271
                $pathcategoryids = explode('/', $mycoursecat->path);
3272
                // First element of the exploded path is empty since paths begin with '/'.
3273
                array_shift($pathcategoryids);
3274
                // Merge the exploded category IDs into the full list of category IDs that we will fetch.
3275
                $fullpathcategoryids = array_merge($fullpathcategoryids, $pathcategoryids);
3276
            }
3277
 
3278
            // Fetch all of the categories related to the user's courses.
3279
            $pathcategories = core_course_category::get_many($fullpathcategoryids);
3280
            // Loop over each of these categories and build the category tree.
3281
            foreach ($pathcategories as $coursecat) {
3282
                // No need to process categories that have already been added.
3283
                if (isset($this->addedcategories[$coursecat->id])) {
3284
                    continue;
3285
                }
3286
                // Skip categories that are not visible.
3287
                if (!$coursecat->is_uservisible()) {
3288
                    continue;
3289
                }
3290
 
3291
                // Get this course category's parent node.
3292
                $parent = null;
3293
                if ($coursecat->parent && isset($this->addedcategories[$coursecat->parent])) {
3294
                    $parent = $this->addedcategories[$coursecat->parent];
3295
                }
3296
                if (!$parent) {
3297
                    // If it has no parent, then it should be right under the My courses node.
3298
                    $parent = $this->rootnodes['mycourses'];
3299
                }
3300
 
3301
                // Build the category object based from the coursecat object.
3302
                $mycategory = new stdClass();
3303
                $mycategory->id = $coursecat->id;
3304
                $mycategory->name = $coursecat->name;
3305
                $mycategory->visible = $coursecat->visible;
3306
 
3307
                // Add this category to the nav tree.
3308
                $this->add_category($mycategory, $parent, self::TYPE_MY_CATEGORY);
3309
            }
3310
        }
3311
 
3312
        // Go through each course now and add it to the nav block, and the flatnav if applicable.
3313
        foreach ($courses as $course) {
3314
            $node = $this->add_course($course, false, self::COURSE_MY);
3315
            if ($node) {
3316
                $node->showinflatnavigation = false;
3317
                // Check if we should also add this to the flat nav as well.
3318
                if (isset($flatnavcourses[$course->id])) {
3319
                    $node->showinflatnavigation = true;
3320
                }
3321
            }
3322
        }
3323
 
3324
        // Go through each course in the flatnav now.
3325
        foreach ($flatnavcourses as $course) {
3326
            // Check if we haven't already added it.
3327
            if (!isset($courses[$course->id])) {
3328
                // Ok, add it to the flatnav only.
3329
                $node = $this->add_course($course, false, self::COURSE_MY);
3330
                $node->display = false;
3331
                $node->showinflatnavigation = true;
3332
            }
3333
        }
3334
 
3335
        $showmorelinkinnav = $numtotalcourses > $numshowncourses;
3336
        $showmorelinkinflatnav = $numtotalflatnavcourses > $numshownflatnavcourses;
3337
        // Show a link to the course page if there are more courses the user is enrolled in.
3338
        if ($showmorelinkinnav || $showmorelinkinflatnav) {
3339
            // Adding hash to URL so the link is not highlighted in the navigation when clicked.
3340
            $url = new moodle_url('/my/courses.php');
3341
            $parent = $this->rootnodes['mycourses'];
3342
            $coursenode = $parent->add(get_string('morenavigationlinks'), $url, self::TYPE_CUSTOM, null, self::COURSE_INDEX_PAGE);
3343
 
3344
            if ($showmorelinkinnav) {
3345
                $coursenode->display = true;
3346
            }
3347
 
3348
            if ($showmorelinkinflatnav) {
3349
                $coursenode->showinflatnavigation = true;
3350
            }
3351
        }
3352
    }
3353
}
3354
 
3355
/**
3356
 * The global navigation class used especially for AJAX requests.
3357
 *
3358
 * The primary methods that are used in the global navigation class have been overriden
3359
 * to ensure that only the relevant branch is generated at the root of the tree.
3360
 * This can be done because AJAX is only used when the backwards structure for the
3361
 * requested branch exists.
3362
 * This has been done only because it shortens the amounts of information that is generated
3363
 * which of course will speed up the response time.. because no one likes laggy AJAX.
3364
 *
3365
 * @package   core
3366
 * @category  navigation
3367
 * @copyright 2009 Sam Hemelryk
3368
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3369
 */
3370
class global_navigation_for_ajax extends global_navigation {
3371
 
3372
    /** @var int used for determining what type of navigation_node::TYPE_* is being used */
3373
    protected $branchtype;
3374
 
3375
    /** @var int the instance id */
3376
    protected $instanceid;
3377
 
3378
    /** @var array Holds an array of expandable nodes */
3379
    protected $expandable = array();
3380
 
3381
    /**
3382
     * Constructs the navigation for use in an AJAX request
3383
     *
3384
     * @param moodle_page $page moodle_page object
3385
     * @param int $branchtype
3386
     * @param int $id
3387
     */
3388
    public function __construct($page, $branchtype, $id) {
3389
        $this->page = $page;
3390
        $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME);
3391
        $this->children = new navigation_node_collection();
3392
        $this->branchtype = $branchtype;
3393
        $this->instanceid = $id;
3394
        $this->initialise();
3395
    }
3396
    /**
3397
     * Initialise the navigation given the type and id for the branch to expand.
3398
     *
3399
     * @return array An array of the expandable nodes
3400
     */
3401
    public function initialise() {
3402
        global $DB, $SITE;
3403
 
3404
        if ($this->initialised || during_initial_install()) {
3405
            return $this->expandable;
3406
        }
3407
        $this->initialised = true;
3408
 
3409
        $this->rootnodes = array();
3410
        $this->rootnodes['site']    = $this->add_course($SITE);
3411
        $this->rootnodes['mycourses'] = $this->add(
3412
            get_string('mycourses'),
3413
            new moodle_url('/my/courses.php'),
3414
            self::TYPE_ROOTNODE,
3415
            null,
3416
            'mycourses'
3417
        );
3418
        $this->rootnodes['courses'] = $this->add(get_string('courses'), null, self::TYPE_ROOTNODE, null, 'courses');
3419
        // The courses branch is always displayed, and is always expandable (although may be empty).
3420
        // This mimicks what is done during {@link global_navigation::initialise()}.
3421
        $this->rootnodes['courses']->isexpandable = true;
3422
 
3423
        // Branchtype will be one of navigation_node::TYPE_*
3424
        switch ($this->branchtype) {
3425
            case 0:
3426
                if ($this->instanceid === 'mycourses') {
3427
                    $this->load_courses_enrolled();
3428
                } else if ($this->instanceid === 'courses') {
3429
                    $this->load_courses_other();
3430
                }
3431
                break;
3432
            case self::TYPE_CATEGORY :
3433
                $this->load_category($this->instanceid);
3434
                break;
3435
            case self::TYPE_MY_CATEGORY :
3436
                $this->load_category($this->instanceid, self::TYPE_MY_CATEGORY);
3437
                break;
3438
            case self::TYPE_COURSE :
3439
                $course = $DB->get_record('course', array('id' => $this->instanceid), '*', MUST_EXIST);
3440
                if (!can_access_course($course, null, '', true)) {
3441
                    // Thats OK all courses are expandable by default. We don't need to actually expand it we can just
3442
                    // add the course node and break. This leads to an empty node.
3443
                    $this->add_course($course);
3444
                    break;
3445
                }
3446
                require_course_login($course, true, null, false, true);
3447
                $this->page->set_context(context_course::instance($course->id));
3448
                $coursenode = $this->add_course($course, false, self::COURSE_CURRENT);
3449
                $this->add_course_essentials($coursenode, $course);
3450
                $this->load_course_sections($course, $coursenode);
3451
                break;
3452
            case self::TYPE_SECTION :
3453
                $sql = 'SELECT c.*, cs.section AS sectionnumber
3454
                        FROM {course} c
3455
                        LEFT JOIN {course_sections} cs ON cs.course = c.id
3456
                        WHERE cs.id = ?';
3457
                $course = $DB->get_record_sql($sql, array($this->instanceid), MUST_EXIST);
3458
                require_course_login($course, true, null, false, true);
3459
                $this->page->set_context(context_course::instance($course->id));
3460
                $coursenode = $this->add_course($course, false, self::COURSE_CURRENT);
3461
                $this->add_course_essentials($coursenode, $course);
3462
                $this->load_course_sections($course, $coursenode, $course->sectionnumber);
3463
                break;
3464
            case self::TYPE_ACTIVITY :
3465
                $sql = "SELECT c.*
3466
                          FROM {course} c
3467
                          JOIN {course_modules} cm ON cm.course = c.id
3468
                         WHERE cm.id = :cmid";
3469
                $params = array('cmid' => $this->instanceid);
3470
                $course = $DB->get_record_sql($sql, $params, MUST_EXIST);
3471
                $modinfo = get_fast_modinfo($course);
3472
                $cm = $modinfo->get_cm($this->instanceid);
3473
                require_course_login($course, true, $cm, false, true);
3474
                $this->page->set_context(context_module::instance($cm->id));
3475
                $coursenode = $this->add_course($course, false, self::COURSE_CURRENT);
3476
                $this->load_course_sections($course, $coursenode, null, $cm);
3477
                $activitynode = $coursenode->find($cm->id, self::TYPE_ACTIVITY);
3478
                if ($activitynode) {
3479
                    $modulenode = $this->load_activity($cm, $course, $activitynode);
3480
                }
3481
                break;
3482
            default:
3483
                throw new Exception('Unknown type');
3484
                return $this->expandable;
3485
        }
3486
 
3487
        if ($this->page->context->contextlevel == CONTEXT_COURSE && $this->page->context->instanceid != $SITE->id) {
3488
            $this->load_for_user(null, true);
3489
        }
3490
 
3491
        // Give the local plugins a chance to include some navigation if they want.
3492
        $this->load_local_plugin_navigation();
3493
 
3494
        $this->find_expandable($this->expandable);
3495
        return $this->expandable;
3496
    }
3497
 
3498
    /**
3499
     * They've expanded the general 'courses' branch.
3500
     */
3501
    protected function load_courses_other() {
3502
        $this->load_all_courses();
3503
    }
3504
 
3505
    /**
3506
     * Loads a single category into the AJAX navigation.
3507
     *
3508
     * This function is special in that it doesn't concern itself with the parent of
3509
     * the requested category or its siblings.
3510
     * This is because with the AJAX navigation we know exactly what is wanted and only need to
3511
     * request that.
3512
     *
3513
     * @global moodle_database $DB
3514
     * @param int $categoryid id of category to load in navigation.
3515
     * @param int $nodetype type of node, if category is under MyHome then it's TYPE_MY_CATEGORY
3516
     * @return void.
3517
     */
3518
    protected function load_category($categoryid, $nodetype = self::TYPE_CATEGORY) {
3519
        global $CFG, $DB;
3520
 
3521
        $limit = 20;
3522
        if (!empty($CFG->navcourselimit)) {
3523
            $limit = (int)$CFG->navcourselimit;
3524
        }
3525
 
3526
        $catcontextsql = context_helper::get_preload_record_columns_sql('ctx');
3527
        $sql = "SELECT cc.*, $catcontextsql
3528
                  FROM {course_categories} cc
3529
                  JOIN {context} ctx ON cc.id = ctx.instanceid
3530
                 WHERE ctx.contextlevel = ".CONTEXT_COURSECAT." AND
3531
                       (cc.id = :categoryid1 OR cc.parent = :categoryid2)
3532
              ORDER BY cc.depth ASC, cc.sortorder ASC, cc.id ASC";
3533
        $params = array('categoryid1' => $categoryid, 'categoryid2' => $categoryid);
3534
        $categories = $DB->get_recordset_sql($sql, $params, 0, $limit);
3535
        $categorylist = array();
3536
        $subcategories = array();
3537
        $basecategory = null;
3538
        foreach ($categories as $category) {
3539
            $categorylist[] = $category->id;
3540
            context_helper::preload_from_record($category);
3541
            if ($category->id == $categoryid) {
3542
                $this->add_category($category, $this, $nodetype);
3543
                $basecategory = $this->addedcategories[$category->id];
3544
            } else {
3545
                $subcategories[$category->id] = $category;
3546
            }
3547
        }
3548
        $categories->close();
3549
 
3550
 
3551
        // If category is shown in MyHome then only show enrolled courses and hide empty subcategories,
3552
        // else show all courses.
3553
        if ($nodetype === self::TYPE_MY_CATEGORY) {
3554
            $courses = enrol_get_my_courses('*');
3555
            $categoryids = array();
3556
 
3557
            // Only search for categories if basecategory was found.
3558
            if (!is_null($basecategory)) {
3559
                // Get course parent category ids.
3560
                foreach ($courses as $course) {
3561
                    $categoryids[] = $course->category;
3562
                }
3563
 
3564
                // Get a unique list of category ids which a part of the path
3565
                // to user's courses.
3566
                $coursesubcategories = array();
3567
                $addedsubcategories = array();
3568
 
3569
                list($sql, $params) = $DB->get_in_or_equal($categoryids);
3570
                $categories = $DB->get_recordset_select('course_categories', 'id '.$sql, $params, 'sortorder, id', 'id, path');
3571
 
3572
                foreach ($categories as $category){
3573
                    $coursesubcategories = array_merge($coursesubcategories, explode('/', trim($category->path, "/")));
3574
                }
3575
                $categories->close();
3576
                $coursesubcategories = array_unique($coursesubcategories);
3577
 
3578
                // Only add a subcategory if it is part of the path to user's course and
3579
                // wasn't already added.
3580
                foreach ($subcategories as $subid => $subcategory) {
3581
                    if (in_array($subid, $coursesubcategories) &&
3582
                            !in_array($subid, $addedsubcategories)) {
3583
                            $this->add_category($subcategory, $basecategory, $nodetype);
3584
                            $addedsubcategories[] = $subid;
3585
                    }
3586
                }
3587
            }
3588
 
3589
            foreach ($courses as $course) {
3590
                // Add course if it's in category.
3591
                if (in_array($course->category, $categorylist)) {
3592
                    $this->add_course($course, true, self::COURSE_MY);
3593
                }
3594
            }
3595
        } else {
3596
            if (!is_null($basecategory)) {
3597
                foreach ($subcategories as $key=>$category) {
3598
                    $this->add_category($category, $basecategory, $nodetype);
3599
                }
3600
            }
3601
            $courses = $DB->get_recordset('course', array('category' => $categoryid), 'sortorder', '*' , 0, $limit);
3602
            foreach ($courses as $course) {
3603
                $this->add_course($course);
3604
            }
3605
            $courses->close();
3606
        }
3607
    }
3608
 
3609
    /**
3610
     * Returns an array of expandable nodes
3611
     * @return array
3612
     */
3613
    public function get_expandable() {
3614
        return $this->expandable;
3615
    }
3616
}
3617
 
3618
/**
3619
 * Navbar class
3620
 *
3621
 * This class is used to manage the navbar, which is initialised from the navigation
3622
 * object held by PAGE
3623
 *
3624
 * @package   core
3625
 * @category  navigation
3626
 * @copyright 2009 Sam Hemelryk
3627
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3628
 */
3629
class navbar extends navigation_node {
3630
    /** @var bool A switch for whether the navbar is initialised or not */
3631
    protected $initialised = false;
3632
    /** @var mixed keys used to reference the nodes on the navbar */
3633
    protected $keys = array();
3634
    /** @var null|string content of the navbar */
3635
    protected $content = null;
3636
    /** @var moodle_page object the moodle page that this navbar belongs to */
3637
    protected $page;
3638
    /** @var bool A switch for whether to ignore the active navigation information */
3639
    protected $ignoreactive = false;
3640
    /** @var bool A switch to let us know if we are in the middle of an install */
3641
    protected $duringinstall = false;
3642
    /** @var bool A switch for whether the navbar has items */
3643
    protected $hasitems = false;
3644
    /** @var array An array of navigation nodes for the navbar */
3645
    protected $items;
3646
    /** @var array An array of child node objects */
3647
    public $children = array();
3648
    /** @var bool A switch for whether we want to include the root node in the navbar */
3649
    public $includesettingsbase = false;
3650
    /** @var breadcrumb_navigation_node[] $prependchildren */
3651
    protected $prependchildren = array();
3652
 
3653
    /**
3654
     * The almighty constructor
3655
     *
3656
     * @param moodle_page $page
3657
     */
3658
    public function __construct(moodle_page $page) {
3659
        global $CFG;
3660
        if (during_initial_install()) {
3661
            $this->duringinstall = true;
3662
            return;
3663
        }
3664
        $this->page = $page;
3665
        $this->text = get_string('home');
3666
        $this->shorttext = get_string('home');
3667
        $this->action = new moodle_url($CFG->wwwroot);
3668
        $this->nodetype = self::NODETYPE_BRANCH;
3669
        $this->type = self::TYPE_SYSTEM;
3670
    }
3671
 
3672
    /**
3673
     * Quick check to see if the navbar will have items in.
3674
     *
3675
     * @return bool Returns true if the navbar will have items, false otherwise
3676
     */
3677
    public function has_items() {
3678
        if ($this->duringinstall) {
3679
            return false;
3680
        } else if ($this->hasitems !== false) {
3681
            return true;
3682
        }
3683
        $outcome = false;
3684
        if (count($this->children) > 0 || count($this->prependchildren) > 0) {
3685
            // There have been manually added items - there are definitely items.
3686
            $outcome = true;
3687
        } else if (!$this->ignoreactive) {
3688
            // We will need to initialise the navigation structure to check if there are active items.
3689
            $this->page->navigation->initialise($this->page);
3690
            $outcome = ($this->page->navigation->contains_active_node() || $this->page->settingsnav->contains_active_node());
3691
        }
3692
        $this->hasitems = $outcome;
3693
        return $outcome;
3694
    }
3695
 
3696
    /**
3697
     * Turn on/off ignore active
3698
     *
3699
     * @param bool $setting
3700
     */
3701
    public function ignore_active($setting=true) {
3702
        $this->ignoreactive = ($setting);
3703
    }
3704
 
3705
    /**
3706
     * Gets a navigation node
3707
     *
3708
     * @param string|int $key for referencing the navbar nodes
3709
     * @param int $type breadcrumb_navigation_node::TYPE_*
3710
     * @return breadcrumb_navigation_node|bool
3711
     */
3712
    public function get($key, $type = null) {
3713
        foreach ($this->children as &$child) {
3714
            if ($child->key === $key && ($type == null || $type == $child->type)) {
3715
                return $child;
3716
            }
3717
        }
3718
        foreach ($this->prependchildren as &$child) {
3719
            if ($child->key === $key && ($type == null || $type == $child->type)) {
3720
                return $child;
3721
            }
3722
        }
3723
        return false;
3724
    }
3725
    /**
3726
     * Returns an array of breadcrumb_navigation_nodes that make up the navbar.
3727
     *
3728
     * @return array
3729
     */
3730
    public function get_items() {
3731
        global $CFG;
3732
        $items = array();
3733
        // Make sure that navigation is initialised
3734
        if (!$this->has_items()) {
3735
            return $items;
3736
        }
3737
        if ($this->items !== null) {
3738
            return $this->items;
3739
        }
3740
 
3741
        if (count($this->children) > 0) {
3742
            // Add the custom children.
3743
            $items = array_reverse($this->children);
3744
        }
3745
 
3746
        // Check if navigation contains the active node
3747
        if (!$this->ignoreactive) {
3748
            // We will need to ensure the navigation has been initialised.
3749
            $this->page->navigation->initialise($this->page);
3750
            // Now find the active nodes on both the navigation and settings.
3751
            $navigationactivenode = $this->page->navigation->find_active_node();
3752
            $settingsactivenode = $this->page->settingsnav->find_active_node();
3753
 
3754
            if ($navigationactivenode && $settingsactivenode) {
3755
                // Parse a combined navigation tree
3756
                while ($settingsactivenode && $settingsactivenode->parent !== null) {
3757
                    if (!$settingsactivenode->mainnavonly) {
3758
                        $items[] = new breadcrumb_navigation_node($settingsactivenode);
3759
                    }
3760
                    $settingsactivenode = $settingsactivenode->parent;
3761
                }
3762
                if (!$this->includesettingsbase) {
3763
                    // Removes the first node from the settings (root node) from the list
3764
                    array_pop($items);
3765
                }
3766
                while ($navigationactivenode && $navigationactivenode->parent !== null) {
3767
                    if (!$navigationactivenode->mainnavonly) {
3768
                        $items[] = new breadcrumb_navigation_node($navigationactivenode);
3769
                    }
3770
                    if (!empty($CFG->navshowcategories) &&
3771
                            $navigationactivenode->type === self::TYPE_COURSE &&
3772
                            $navigationactivenode->parent->key === 'currentcourse') {
3773
                        foreach ($this->get_course_categories() as $item) {
3774
                            $items[] = new breadcrumb_navigation_node($item);
3775
                        }
3776
                    }
3777
                    $navigationactivenode = $navigationactivenode->parent;
3778
                }
3779
            } else if ($navigationactivenode) {
3780
                // Parse the navigation tree to get the active node
3781
                while ($navigationactivenode && $navigationactivenode->parent !== null) {
3782
                    if (!$navigationactivenode->mainnavonly) {
3783
                        $items[] = new breadcrumb_navigation_node($navigationactivenode);
3784
                    }
3785
                    if (!empty($CFG->navshowcategories) &&
3786
                            $navigationactivenode->type === self::TYPE_COURSE &&
3787
                            $navigationactivenode->parent->key === 'currentcourse') {
3788
                        foreach ($this->get_course_categories() as $item) {
3789
                            $items[] = new breadcrumb_navigation_node($item);
3790
                        }
3791
                    }
3792
                    $navigationactivenode = $navigationactivenode->parent;
3793
                }
3794
            } else if ($settingsactivenode) {
3795
                // Parse the settings navigation to get the active node
3796
                while ($settingsactivenode && $settingsactivenode->parent !== null) {
3797
                    if (!$settingsactivenode->mainnavonly) {
3798
                        $items[] = new breadcrumb_navigation_node($settingsactivenode);
3799
                    }
3800
                    $settingsactivenode = $settingsactivenode->parent;
3801
                }
3802
            }
3803
        }
3804
 
3805
        $items[] = new breadcrumb_navigation_node(array(
3806
            'text' => $this->page->navigation->text,
3807
            'shorttext' => $this->page->navigation->shorttext,
3808
            'key' => $this->page->navigation->key,
3809
            'action' => $this->page->navigation->action
3810
        ));
3811
 
3812
        if (count($this->prependchildren) > 0) {
3813
            // Add the custom children
3814
            $items = array_merge($items, array_reverse($this->prependchildren));
3815
        }
3816
 
3817
        $last = reset($items);
3818
        if ($last) {
3819
            $last->set_last(true);
3820
        }
3821
        $this->items = array_reverse($items);
3822
        return $this->items;
3823
    }
3824
 
3825
    /**
3826
     * Get the list of categories leading to this course.
3827
     *
3828
     * This function is used by {@link navbar::get_items()} to add back the "courses"
3829
     * node and category chain leading to the current course.  Note that this is only ever
3830
     * called for the current course, so we don't need to bother taking in any parameters.
3831
     *
3832
     * @return array
3833
     */
3834
    private function get_course_categories() {
3835
        global $CFG;
3836
        require_once($CFG->dirroot.'/course/lib.php');
3837
 
3838
        $categories = array();
3839
        $cap = 'moodle/category:viewhiddencategories';
3840
        $showcategories = !core_course_category::is_simple_site();
3841
 
3842
        if ($showcategories) {
3843
            foreach ($this->page->categories as $category) {
3844
                $context = context_coursecat::instance($category->id);
3845
                if (!core_course_category::can_view_category($category)) {
3846
                    continue;
3847
                }
3848
 
3849
                $displaycontext = \context_helper::get_navigation_filter_context($context);
3850
                $url = new moodle_url('/course/index.php', ['categoryid' => $category->id]);
3851
                $name = format_string($category->name, true, ['context' => $displaycontext]);
3852
                $categorynode = breadcrumb_navigation_node::create($name, $url, self::TYPE_CATEGORY, null, $category->id);
3853
                if (!$category->visible) {
3854
                    $categorynode->hidden = true;
3855
                }
3856
                $categories[] = $categorynode;
3857
            }
3858
        }
3859
 
3860
        // Don't show the 'course' node if enrolled in this course.
3861
        $coursecontext = context_course::instance($this->page->course->id);
3862
        if (!is_enrolled($coursecontext, null, '', true)) {
3863
            $courses = $this->page->navigation->get('courses');
3864
            if (!$courses) {
3865
                // Courses node may not be present.
3866
                $courses = breadcrumb_navigation_node::create(
3867
                    get_string('courses'),
3868
                    new moodle_url('/course/index.php'),
3869
                    self::TYPE_CONTAINER
3870
                );
3871
            }
3872
            $categories[] = $courses;
3873
        }
3874
 
3875
        return $categories;
3876
    }
3877
 
3878
    /**
3879
     * Add a new breadcrumb_navigation_node to the navbar, overrides parent::add
3880
     *
3881
     * This function overrides {@link breadcrumb_navigation_node::add()} so that we can change
3882
     * the way nodes get added to allow us to simply call add and have the node added to the
3883
     * end of the navbar
3884
     *
3885
     * @param string $text
3886
     * @param string|moodle_url|action_link $action An action to associate with this node.
3887
     * @param int $type One of navigation_node::TYPE_*
3888
     * @param string $shorttext
3889
     * @param string|int $key A key to identify this node with. Key + type is unique to a parent.
3890
     * @param pix_icon $icon An optional icon to use for this node.
3891
     * @return navigation_node
3892
     */
3893
    public function add($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=null, $key=null, pix_icon $icon=null) {
3894
        if ($this->content !== null) {
3895
            debugging('Nav bar items must be printed before $OUTPUT->header() has been called', DEBUG_DEVELOPER);
3896
        }
3897
 
3898
        // Properties array used when creating the new navigation node
3899
        $itemarray = array(
3900
            'text' => $text,
3901
            'type' => $type
3902
        );
3903
        // Set the action if one was provided
3904
        if ($action!==null) {
3905
            $itemarray['action'] = $action;
3906
        }
3907
        // Set the shorttext if one was provided
3908
        if ($shorttext!==null) {
3909
            $itemarray['shorttext'] = $shorttext;
3910
        }
3911
        // Set the icon if one was provided
3912
        if ($icon!==null) {
3913
            $itemarray['icon'] = $icon;
3914
        }
3915
        // Default the key to the number of children if not provided
3916
        if ($key === null) {
3917
            $key = count($this->children);
3918
        }
3919
        // Set the key
3920
        $itemarray['key'] = $key;
3921
        // Set the parent to this node
3922
        $itemarray['parent'] = $this;
3923
        // Add the child using the navigation_node_collections add method
3924
        $this->children[] = new breadcrumb_navigation_node($itemarray);
3925
        return $this;
3926
    }
3927
 
3928
    /**
3929
     * Prepends a new navigation_node to the start of the navbar
3930
     *
3931
     * @param string $text
3932
     * @param string|moodle_url|action_link $action An action to associate with this node.
3933
     * @param int $type One of navigation_node::TYPE_*
3934
     * @param string $shorttext
3935
     * @param string|int $key A key to identify this node with. Key + type is unique to a parent.
3936
     * @param pix_icon $icon An optional icon to use for this node.
3937
     * @return navigation_node
3938
     */
3939
    public function prepend($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=null, $key=null, pix_icon $icon=null) {
3940
        if ($this->content !== null) {
3941
            debugging('Nav bar items must be printed before $OUTPUT->header() has been called', DEBUG_DEVELOPER);
3942
        }
3943
        // Properties array used when creating the new navigation node.
3944
        $itemarray = array(
3945
            'text' => $text,
3946
            'type' => $type
3947
        );
3948
        // Set the action if one was provided.
3949
        if ($action!==null) {
3950
            $itemarray['action'] = $action;
3951
        }
3952
        // Set the shorttext if one was provided.
3953
        if ($shorttext!==null) {
3954
            $itemarray['shorttext'] = $shorttext;
3955
        }
3956
        // Set the icon if one was provided.
3957
        if ($icon!==null) {
3958
            $itemarray['icon'] = $icon;
3959
        }
3960
        // Default the key to the number of children if not provided.
3961
        if ($key === null) {
3962
            $key = count($this->children);
3963
        }
3964
        // Set the key.
3965
        $itemarray['key'] = $key;
3966
        // Set the parent to this node.
3967
        $itemarray['parent'] = $this;
3968
        // Add the child node to the prepend list.
3969
        $this->prependchildren[] = new breadcrumb_navigation_node($itemarray);
3970
        return $this;
3971
    }
3972
}
3973
 
3974
/**
3975
 * Subclass of navigation_node allowing different rendering for the breadcrumbs
3976
 * in particular adding extra metadata for search engine robots to leverage.
3977
 *
3978
 * @package   core
3979
 * @category  navigation
3980
 * @copyright 2015 Brendan Heywood
3981
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3982
 */
3983
class breadcrumb_navigation_node extends navigation_node {
3984
 
3985
    /** @var $last boolean A flag indicating this is the last item in the list of breadcrumbs. */
3986
    private $last = false;
3987
 
3988
    /**
3989
     * A proxy constructor
3990
     *
3991
     * @param mixed $navnode A navigation_node or an array
3992
     */
3993
    public function __construct($navnode) {
3994
        if (is_array($navnode)) {
3995
            parent::__construct($navnode);
3996
        } else if ($navnode instanceof navigation_node) {
3997
 
3998
            // Just clone everything.
3999
            $objvalues = get_object_vars($navnode);
4000
            foreach ($objvalues as $key => $value) {
4001
                 $this->$key = $value;
4002
            }
4003
        } else {
4004
            throw new coding_exception('Not a valid breadcrumb_navigation_node');
4005
        }
4006
    }
4007
 
4008
    /**
4009
     * Getter for "last"
4010
     * @return boolean
4011
     */
4012
    public function is_last() {
4013
        return $this->last;
4014
    }
4015
 
4016
    /**
4017
     * Setter for "last"
4018
     * @param $val boolean
4019
     */
4020
    public function set_last($val) {
4021
        $this->last = $val;
4022
    }
4023
}
4024
 
4025
/**
4026
 * Subclass of navigation_node allowing different rendering for the flat navigation
4027
 * in particular allowing dividers and indents.
4028
 *
4029
 * @deprecated since Moodle 4.0 - do not use any more. Leverage secondary/tertiary navigation concepts
4030
 * @package   core
4031
 * @category  navigation
4032
 * @copyright 2016 Damyon Wiese
4033
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4034
 */
4035
class flat_navigation_node extends navigation_node {
4036
 
4037
    /** @var $indent integer The indent level */
4038
    private $indent = 0;
4039
 
4040
    /** @var $showdivider bool Show a divider before this element */
4041
    private $showdivider = false;
4042
 
4043
    /** @var $collectionlabel string Label for a group of nodes */
4044
    private $collectionlabel = '';
4045
 
4046
    /**
4047
     * A proxy constructor
4048
     *
4049
     * @param mixed $navnode A navigation_node or an array
4050
     */
4051
    public function __construct($navnode, $indent) {
4052
        debugging("Flat nav has been deprecated in favour of primary/secondary navigation concepts");
4053
        if (is_array($navnode)) {
4054
            parent::__construct($navnode);
4055
        } else if ($navnode instanceof navigation_node) {
4056
 
4057
            // Just clone everything.
4058
            $objvalues = get_object_vars($navnode);
4059
            foreach ($objvalues as $key => $value) {
4060
                 $this->$key = $value;
4061
            }
4062
        } else {
4063
            throw new coding_exception('Not a valid flat_navigation_node');
4064
        }
4065
        $this->indent = $indent;
4066
    }
4067
 
4068
    /**
4069
     * Setter, a label is required for a flat navigation node that shows a divider.
4070
     *
4071
     * @param string $label
4072
     */
4073
    public function set_collectionlabel($label) {
4074
        $this->collectionlabel = $label;
4075
    }
4076
 
4077
    /**
4078
     * Getter, get the label for this flat_navigation node, or it's parent if it doesn't have one.
4079
     *
4080
     * @return string
4081
     */
4082
    public function get_collectionlabel() {
4083
        if (!empty($this->collectionlabel)) {
4084
            return $this->collectionlabel;
4085
        }
4086
        if ($this->parent && ($this->parent instanceof flat_navigation_node || $this->parent instanceof flat_navigation)) {
4087
            return $this->parent->get_collectionlabel();
4088
        }
4089
        debugging('Navigation region requires a label', DEBUG_DEVELOPER);
4090
        return '';
4091
    }
4092
 
4093
    /**
4094
     * Does this node represent a course section link.
4095
     * @return boolean
4096
     */
4097
    public function is_section() {
4098
        return $this->type == navigation_node::TYPE_SECTION;
4099
    }
4100
 
4101
    /**
4102
     * In flat navigation - sections are active if we are looking at activities in the section.
4103
     * @return boolean
4104
     */
4105
    public function isactive() {
4106
        global $PAGE;
4107
 
4108
        if ($this->is_section()) {
4109
            $active = $PAGE->navigation->find_active_node();
4110
            if ($active) {
4111
                while ($active = $active->parent) {
4112
                    if ($active->key == $this->key && $active->type == $this->type) {
4113
                        return true;
4114
                    }
4115
                }
4116
            }
4117
        }
4118
        return $this->isactive;
4119
    }
4120
 
4121
    /**
4122
     * Getter for "showdivider"
4123
     * @return boolean
4124
     */
4125
    public function showdivider() {
4126
        return $this->showdivider;
4127
    }
4128
 
4129
    /**
4130
     * Setter for "showdivider"
4131
     * @param $val boolean
4132
     * @param $label string Label for the group of nodes
4133
     */
4134
    public function set_showdivider($val, $label = '') {
4135
        $this->showdivider = $val;
4136
        if ($this->showdivider && empty($label)) {
4137
            debugging('Navigation region requires a label', DEBUG_DEVELOPER);
4138
        } else {
4139
            $this->set_collectionlabel($label);
4140
        }
4141
    }
4142
 
4143
    /**
4144
     * Getter for "indent"
4145
     * @return boolean
4146
     */
4147
    public function get_indent() {
4148
        return $this->indent;
4149
    }
4150
 
4151
    /**
4152
     * Setter for "indent"
4153
     * @param $val boolean
4154
     */
4155
    public function set_indent($val) {
4156
        $this->indent = $val;
4157
    }
4158
}
4159
 
4160
/**
4161
 * Class used to generate a collection of navigation nodes most closely related
4162
 * to the current page.
4163
 *
4164
 * @deprecated since Moodle 4.0 - do not use any more. Leverage secondary/tertiary navigation concepts
4165
 * @package core
4166
 * @copyright 2016 Damyon Wiese
4167
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4168
 */
4169
class flat_navigation extends navigation_node_collection {
4170
    /** @var moodle_page the moodle page that the navigation belongs to */
4171
    protected $page;
4172
 
4173
    /**
4174
     * Constructor.
4175
     *
4176
     * @param moodle_page $page
4177
     */
4178
    public function __construct(moodle_page &$page) {
4179
        if (during_initial_install()) {
4180
            return;
4181
        }
4182
        debugging("Flat navigation has been deprecated in favour of primary/secondary navigation concepts");
4183
        $this->page = $page;
4184
    }
4185
 
4186
    /**
4187
     * Build the list of navigation nodes based on the current navigation and settings trees.
4188
     *
4189
     */
4190
    public function initialise() {
4191
        global $PAGE, $USER, $OUTPUT, $CFG;
4192
        if (during_initial_install()) {
4193
            return;
4194
        }
4195
 
4196
        $current = false;
4197
 
4198
        $course = $PAGE->course;
4199
 
4200
        $this->page->navigation->initialise();
4201
 
4202
        // First walk the nav tree looking for "flat_navigation" nodes.
4203
        if ($course->id > 1) {
4204
            // It's a real course.
4205
            $url = new moodle_url('/course/view.php', array('id' => $course->id));
4206
 
4207
            $coursecontext = context_course::instance($course->id, MUST_EXIST);
4208
            $displaycontext = \context_helper::get_navigation_filter_context($coursecontext);
4209
            // This is the name that will be shown for the course.
4210
            $coursename = empty($CFG->navshowfullcoursenames) ?
4211
                format_string($course->shortname, true, ['context' => $displaycontext]) :
4212
                format_string($course->fullname, true, ['context' => $displaycontext]);
4213
 
4214
            $flat = new flat_navigation_node(navigation_node::create($coursename, $url), 0);
4215
            $flat->set_collectionlabel($coursename);
4216
            $flat->key = 'coursehome';
4217
            $flat->icon = new pix_icon('i/course', '');
4218
 
4219
            $courseformat = course_get_format($course);
4220
            $coursenode = $PAGE->navigation->find_active_node();
4221
            $targettype = navigation_node::TYPE_COURSE;
4222
 
4223
            // Single activity format has no course node - the course node is swapped for the activity node.
4224
            if (!$courseformat->has_view_page()) {
4225
                $targettype = navigation_node::TYPE_ACTIVITY;
4226
            }
4227
 
4228
            while (!empty($coursenode) && ($coursenode->type != $targettype)) {
4229
                $coursenode = $coursenode->parent;
4230
            }
4231
            // There is one very strange page in mod/feedback/view.php which thinks it is both site and course
4232
            // context at the same time. That page is broken but we need to handle it (hence the SITEID).
4233
            if ($coursenode && $coursenode->key != SITEID) {
4234
                $this->add($flat);
4235
                foreach ($coursenode->children as $child) {
4236
                    if ($child->action) {
4237
                        $flat = new flat_navigation_node($child, 0);
4238
                        $this->add($flat);
4239
                    }
4240
                }
4241
            }
4242
 
4243
            $this->page->navigation->build_flat_navigation_list($this, true, get_string('site'));
4244
        } else {
4245
            $this->page->navigation->build_flat_navigation_list($this, false, get_string('site'));
4246
        }
4247
 
4248
        $admin = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
4249
        if (!$admin) {
4250
            // Try again - crazy nav tree!
4251
            $admin = $PAGE->settingsnav->find('root', navigation_node::TYPE_SITE_ADMIN);
4252
        }
4253
        if ($admin) {
4254
            $flat = new flat_navigation_node($admin, 0);
4255
            $flat->set_showdivider(true, get_string('sitesettings'));
4256
            $flat->key = 'sitesettings';
4257
            $flat->icon = new pix_icon('t/preferences', '');
4258
            $this->add($flat);
4259
        }
4260
 
4261
        // Add-a-block in editing mode.
4262
        if (isset($this->page->theme->addblockposition) &&
4263
                $this->page->theme->addblockposition == BLOCK_ADDBLOCK_POSITION_FLATNAV &&
4264
                $PAGE->user_is_editing() && $PAGE->user_can_edit_blocks()) {
4265
            $url = new moodle_url($PAGE->url, ['bui_addblock' => '', 'sesskey' => sesskey()]);
4266
            $addablock = navigation_node::create(get_string('addblock'), $url);
4267
            $flat = new flat_navigation_node($addablock, 0);
4268
            $flat->set_showdivider(true, get_string('blocksaddedit'));
4269
            $flat->key = 'addblock';
4270
            $flat->icon = new pix_icon('i/addblock', '');
4271
            $this->add($flat);
4272
 
4273
            $addblockurl = "?{$url->get_query_string(false)}";
4274
 
4275
            $PAGE->requires->js_call_amd('core_block/add_modal', 'init',
4276
                [$addblockurl, $this->page->get_edited_page_hash()]);
4277
        }
4278
    }
4279
 
4280
    /**
4281
     * Override the parent so we can set a label for this collection if it has not been set yet.
4282
     *
4283
     * @param navigation_node $node Node to add
4284
     * @param string $beforekey If specified, adds before a node with this key,
4285
     *   otherwise adds at end
4286
     * @return navigation_node Added node
4287
     */
4288
    public function add(navigation_node $node, $beforekey=null) {
4289
        $result = parent::add($node, $beforekey);
4290
        // Extend the parent to get a name for the collection of nodes if required.
4291
        if (empty($this->collectionlabel)) {
4292
            if ($node instanceof flat_navigation_node) {
4293
                $this->set_collectionlabel($node->get_collectionlabel());
4294
            }
4295
        }
4296
 
4297
        return $result;
4298
    }
4299
}
4300
 
4301
/**
4302
 * Class used to manage the settings option for the current page
4303
 *
4304
 * This class is used to manage the settings options in a tree format (recursively)
4305
 * and was created initially for use with the settings blocks.
4306
 *
4307
 * @package   core
4308
 * @category  navigation
4309
 * @copyright 2009 Sam Hemelryk
4310
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4311
 */
4312
class settings_navigation extends navigation_node {
4313
    /** @var context the current context */
4314
    protected $context;
4315
    /** @var moodle_page the moodle page that the navigation belongs to */
4316
    protected $page;
4317
    /** @var string contains administration section navigation_nodes */
4318
    protected $adminsection;
4319
    /** @var bool A switch to see if the navigation node is initialised */
4320
    protected $initialised = false;
4321
    /** @var array An array of users that the nodes can extend for. */
4322
    protected $userstoextendfor = array();
4323
    /** @var navigation_cache **/
4324
    protected $cache;
4325
 
4326
    /**
4327
     * Sets up the object with basic settings and preparse it for use
4328
     *
4329
     * @param moodle_page $page
4330
     */
4331
    public function __construct(moodle_page &$page) {
4332
        if (during_initial_install()) {
4333
            return;
4334
        }
4335
        $this->page = $page;
4336
        // Initialise the main navigation. It is most important that this is done
4337
        // before we try anything
4338
        $this->page->navigation->initialise();
4339
        // Initialise the navigation cache
4340
        $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME);
4341
        $this->children = new navigation_node_collection();
4342
    }
4343
 
4344
    /**
4345
     * Initialise the settings navigation based on the current context
4346
     *
4347
     * This function initialises the settings navigation tree for a given context
4348
     * by calling supporting functions to generate major parts of the tree.
4349
     *
4350
     */
4351
    public function initialise() {
4352
        global $DB, $SESSION, $SITE;
4353
 
4354
        if (during_initial_install()) {
4355
            return false;
4356
        } else if ($this->initialised) {
4357
            return true;
4358
        }
4359
        $this->id = 'settingsnav';
4360
        $this->context = $this->page->context;
4361
 
4362
        $context = $this->context;
4363
        if ($context->contextlevel == CONTEXT_BLOCK) {
4364
            $this->load_block_settings();
4365
            $context = $context->get_parent_context();
4366
            $this->context = $context;
4367
        }
4368
        switch ($context->contextlevel) {
4369
            case CONTEXT_SYSTEM:
4370
                if ($this->page->url->compare(new moodle_url('/admin/settings.php', array('section'=>'frontpagesettings')))) {
4371
                    $this->load_front_page_settings(($context->id == $this->context->id));
4372
                }
4373
                break;
4374
            case CONTEXT_COURSECAT:
4375
                $this->load_category_settings();
4376
                break;
4377
            case CONTEXT_COURSE:
4378
                if ($this->page->course->id != $SITE->id) {
4379
                    $this->load_course_settings(($context->id == $this->context->id));
4380
                } else {
4381
                    $this->load_front_page_settings(($context->id == $this->context->id));
4382
                }
4383
                break;
4384
            case CONTEXT_MODULE:
4385
                $this->load_module_settings();
4386
                $this->load_course_settings();
4387
                break;
4388
            case CONTEXT_USER:
4389
                if ($this->page->course->id != $SITE->id) {
4390
                    $this->load_course_settings();
4391
                }
4392
                break;
4393
        }
4394
 
4395
        $usersettings = $this->load_user_settings($this->page->course->id);
4396
 
4397
        $adminsettings = false;
4398
        if (isloggedin() && !isguestuser() && (!isset($SESSION->load_navigation_admin) || $SESSION->load_navigation_admin)) {
4399
            $isadminpage = $this->is_admin_tree_needed();
4400
 
4401
            if (has_capability('moodle/site:configview', context_system::instance())) {
4402
                if (has_capability('moodle/site:config', context_system::instance())) {
4403
                    // Make sure this works even if config capability changes on the fly
4404
                    // and also make it fast for admin right after login.
4405
                    $SESSION->load_navigation_admin = 1;
4406
                    if ($isadminpage) {
4407
                        $adminsettings = $this->load_administration_settings();
4408
                    }
4409
 
4410
                } else if (!isset($SESSION->load_navigation_admin)) {
4411
                    $adminsettings = $this->load_administration_settings();
4412
                    $SESSION->load_navigation_admin = (int)($adminsettings->children->count() > 0);
4413
 
4414
                } else if ($SESSION->load_navigation_admin) {
4415
                    if ($isadminpage) {
4416
                        $adminsettings = $this->load_administration_settings();
4417
                    }
4418
                }
4419
 
4420
                // Print empty navigation node, if needed.
4421
                if ($SESSION->load_navigation_admin && !$isadminpage) {
4422
                    if ($adminsettings) {
4423
                        // Do not print settings tree on pages that do not need it, this helps with performance.
4424
                        $adminsettings->remove();
4425
                        $adminsettings = false;
4426
                    }
4427
                    $siteadminnode = $this->add(get_string('administrationsite'), new moodle_url('/admin/search.php'),
4428
                            self::TYPE_SITE_ADMIN, null, 'siteadministration');
4429
                    $siteadminnode->id = 'expandable_branch_' . $siteadminnode->type . '_' .
4430
                            clean_param($siteadminnode->key, PARAM_ALPHANUMEXT);
4431
                    $siteadminnode->requiresajaxloading = 'true';
4432
                }
4433
            }
4434
        }
4435
 
4436
        if ($context->contextlevel == CONTEXT_SYSTEM && $adminsettings) {
4437
            $adminsettings->force_open();
4438
        } else if ($context->contextlevel == CONTEXT_USER && $usersettings) {
4439
            $usersettings->force_open();
4440
        }
4441
 
4442
        // At this point we give any local plugins the ability to extend/tinker with the navigation settings.
4443
        $this->load_local_plugin_settings();
4444
 
4445
        foreach ($this->children as $key=>$node) {
4446
            if ($node->nodetype == self::NODETYPE_BRANCH && $node->children->count() == 0) {
4447
                // Site administration is shown as link.
4448
                if (!empty($SESSION->load_navigation_admin) && ($node->type === self::TYPE_SITE_ADMIN)) {
4449
                    continue;
4450
                }
4451
                $node->remove();
4452
            }
4453
        }
4454
        $this->initialised = true;
4455
    }
4456
    /**
4457
     * Override the parent function so that we can add preceeding hr's and set a
4458
     * root node class against all first level element
4459
     *
4460
     * It does this by first calling the parent's add method {@link navigation_node::add()}
4461
     * and then proceeds to use the key to set class and hr
4462
     *
4463
     * @param string $text text to be used for the link.
4464
     * @param string|moodle_url $url url for the new node
4465
     * @param int $type the type of node navigation_node::TYPE_*
4466
     * @param string $shorttext
4467
     * @param string|int $key a key to access the node by.
4468
     * @param pix_icon $icon An icon that appears next to the node.
4469
     * @return navigation_node with the new node added to it.
4470
     */
4471
    public function add($text, $url=null, $type=null, $shorttext=null, $key=null, pix_icon $icon=null) {
4472
        $node = parent::add($text, $url, $type, $shorttext, $key, $icon);
4473
        $node->add_class('root_node');
4474
        return $node;
4475
    }
4476
 
4477
    /**
4478
     * This function allows the user to add something to the start of the settings
4479
     * navigation, which means it will be at the top of the settings navigation block
4480
     *
4481
     * @param string $text text to be used for the link.
4482
     * @param string|moodle_url $url url for the new node
4483
     * @param int $type the type of node navigation_node::TYPE_*
4484
     * @param string $shorttext
4485
     * @param string|int $key a key to access the node by.
4486
     * @param pix_icon $icon An icon that appears next to the node.
4487
     * @return navigation_node $node with the new node added to it.
4488
     */
4489
    public function prepend($text, $url=null, $type=null, $shorttext=null, $key=null, pix_icon $icon=null) {
4490
        $children = $this->children;
4491
        $childrenclass = get_class($children);
4492
        $this->children = new $childrenclass;
4493
        $node = $this->add($text, $url, $type, $shorttext, $key, $icon);
4494
        foreach ($children as $child) {
4495
            $this->children->add($child);
4496
        }
4497
        return $node;
4498
    }
4499
 
4500
    /**
4501
     * Does this page require loading of full admin tree or is
4502
     * it enough rely on AJAX?
4503
     *
4504
     * @return bool
4505
     */
4506
    protected function is_admin_tree_needed() {
4507
        if (self::$loadadmintree) {
4508
            // Usually external admin page or settings page.
4509
            return true;
4510
        }
4511
 
4512
        if ($this->page->pagelayout === 'admin' or strpos($this->page->pagetype, 'admin-') === 0) {
4513
            // Admin settings tree is intended for system level settings and management only, use navigation for the rest!
4514
            if ($this->page->context->contextlevel != CONTEXT_SYSTEM) {
4515
                return false;
4516
            }
4517
            return true;
4518
        }
4519
 
4520
        return false;
4521
    }
4522
 
4523
    /**
4524
     * Load the site administration tree
4525
     *
4526
     * This function loads the site administration tree by using the lib/adminlib library functions
4527
     *
4528
     * @param navigation_node $referencebranch A reference to a branch in the settings
4529
     *      navigation tree
4530
     * @param part_of_admin_tree $adminbranch The branch to add, if null generate the admin
4531
     *      tree and start at the beginning
4532
     * @return mixed A key to access the admin tree by
4533
     */
4534
    protected function load_administration_settings(navigation_node $referencebranch=null, part_of_admin_tree $adminbranch=null) {
4535
        global $CFG;
4536
 
4537
        // Check if we are just starting to generate this navigation.
4538
        if ($referencebranch === null) {
4539
 
4540
            // Require the admin lib then get an admin structure
4541
            if (!function_exists('admin_get_root')) {
4542
                require_once($CFG->dirroot.'/lib/adminlib.php');
4543
            }
4544
            $adminroot = admin_get_root(false, false);
4545
            // This is the active section identifier
4546
            $this->adminsection = $this->page->url->param('section');
4547
 
4548
            // Disable the navigation from automatically finding the active node
4549
            navigation_node::$autofindactive = false;
4550
            $referencebranch = $this->add(get_string('administrationsite'), '/admin/search.php', self::TYPE_SITE_ADMIN, null, 'root');
4551
            foreach ($adminroot->children as $adminbranch) {
4552
                $this->load_administration_settings($referencebranch, $adminbranch);
4553
            }
4554
            navigation_node::$autofindactive = true;
4555
 
4556
            // Use the admin structure to locate the active page
4557
            if (!$this->contains_active_node() && $current = $adminroot->locate($this->adminsection, true)) {
4558
                $currentnode = $this;
4559
                while (($pathkey = array_pop($current->path))!==null && $currentnode) {
4560
                    $currentnode = $currentnode->get($pathkey);
4561
                }
4562
                if ($currentnode) {
4563
                    $currentnode->make_active();
4564
                }
4565
            } else {
4566
                $this->scan_for_active_node($referencebranch);
4567
            }
4568
            return $referencebranch;
4569
        } else if ($adminbranch->check_access()) {
4570
            // We have a reference branch that we can access and is not hidden `hurrah`
4571
            // Now we need to display it and any children it may have
4572
            $url = null;
4573
            $icon = null;
4574
 
4575
            if ($adminbranch instanceof \core_admin\local\settings\linkable_settings_page) {
4576
                if (empty($CFG->linkadmincategories) && $adminbranch instanceof admin_category) {
4577
                    $url = null;
4578
                } else {
4579
                    $url = $adminbranch->get_settings_page_url();
4580
                }
4581
            }
4582
 
4583
            // Add the branch
4584
            $reference = $referencebranch->add($adminbranch->visiblename, $url, self::TYPE_SETTING, null, $adminbranch->name, $icon);
4585
 
4586
            if ($adminbranch->is_hidden()) {
4587
                if (($adminbranch instanceof admin_externalpage || $adminbranch instanceof admin_settingpage) && $adminbranch->name == $this->adminsection) {
4588
                    $reference->add_class('hidden');
4589
                } else {
4590
                    $reference->display = false;
4591
                }
4592
            }
4593
 
4594
            // Check if we are generating the admin notifications and whether notificiations exist
4595
            if ($adminbranch->name === 'adminnotifications' && admin_critical_warnings_present()) {
4596
                $reference->add_class('criticalnotification');
4597
            }
4598
            // Check if this branch has children
4599
            if ($reference && isset($adminbranch->children) && is_array($adminbranch->children) && count($adminbranch->children)>0) {
4600
                foreach ($adminbranch->children as $branch) {
4601
                    // Generate the child branches as well now using this branch as the reference
4602
                    $this->load_administration_settings($reference, $branch);
4603
                }
4604
            } else {
4605
                $reference->icon = new pix_icon('i/settings', '');
4606
            }
4607
        }
4608
    }
4609
 
4610
    /**
4611
     * This function recursivily scans nodes until it finds the active node or there
4612
     * are no more nodes.
4613
     * @param navigation_node $node
4614
     */
4615
    protected function scan_for_active_node(navigation_node $node) {
4616
        if (!$node->check_if_active() && $node->children->count()>0) {
4617
            foreach ($node->children as &$child) {
4618
                $this->scan_for_active_node($child);
4619
            }
4620
        }
4621
    }
4622
 
4623
    /**
4624
     * Gets a navigation node given an array of keys that represent the path to
4625
     * the desired node.
4626
     *
4627
     * @param array $path
4628
     * @return navigation_node|false
4629
     */
4630
    protected function get_by_path(array $path) {
4631
        $node = $this->get(array_shift($path));
4632
        foreach ($path as $key) {
4633
            $node->get($key);
4634
        }
4635
        return $node;
4636
    }
4637
 
4638
    /**
4639
     * This function loads the course settings that are available for the user
4640
     *
4641
     * @param bool $forceopen If set to true the course node will be forced open
4642
     * @return navigation_node|false
4643
     */
4644
    protected function load_course_settings($forceopen = false) {
4645
        global $CFG, $USER;
4646
        require_once($CFG->dirroot . '/course/lib.php');
4647
 
4648
        $course = $this->page->course;
4649
        $coursecontext = context_course::instance($course->id);
4650
        $adminoptions = course_get_user_administration_options($course, $coursecontext);
4651
 
4652
        // note: do not test if enrolled or viewing here because we need the enrol link in Course administration section
4653
 
4654
        $coursenode = $this->add(get_string('courseadministration'), null, self::TYPE_COURSE, null, 'courseadmin');
4655
        if ($forceopen) {
4656
            $coursenode->force_open();
4657
        }
4658
 
4659
        // MoodleNet links.
4660
        if ($this->page->user_is_editing()) {
4661
            $this->page->requires->js_call_amd('core/moodlenet/mutations', 'init');
4662
        }
4663
        $usercanshare = utilities::can_user_share($coursecontext, $USER->id, 'course');
4664
        $issuerid = get_config('moodlenet', 'oauthservice');
4665
        try {
4666
            $issuer = \core\oauth2\api::get_issuer($issuerid);
4667
            $isvalidinstance = utilities::is_valid_instance($issuer);
4668
            if ($usercanshare && $isvalidinstance) {
4669
                $this->page->requires->js_call_amd('core/moodlenet/send_resource', 'init');
4670
                $action = new action_link(new moodle_url(''), '', null, [
4671
                    'data-action' => 'sendtomoodlenet',
4672
                    'data-type' => 'course',
4673
                ]);
4674
                // Share course to MoodleNet link.
4675
                $coursenode->add(get_string('moodlenet:sharetomoodlenet', 'moodle'),
4676
                    $action, self::TYPE_SETTING, null, 'exportcoursetomoodlenet')->set_force_into_more_menu(true);
4677
                // MoodleNet share progress link.
4678
                $url = new moodle_url('/moodlenet/shareprogress.php');
4679
                $coursenode->add(get_string('moodlenet:shareprogress'),
4680
                    $url, self::TYPE_SETTING, null, 'moodlenetshareprogress')->set_force_into_more_menu(true);
4681
            }
4682
        } catch (dml_missing_record_exception $e) {
4683
            debugging("Invalid MoodleNet OAuth 2 service set in site administration: 'moodlenet | oauthservice'. " .
4684
                "This must be a valid issuer.");
4685
        }
4686
 
4687
        if ($adminoptions->update) {
4688
            // Add the course settings link
4689
            $url = new moodle_url('/course/edit.php', array('id'=>$course->id));
4690
            $coursenode->add(get_string('settings'), $url, self::TYPE_SETTING, null,
4691
                'editsettings', new pix_icon('i/settings', ''));
4692
        }
4693
 
4694
        if ($adminoptions->editcompletion) {
4695
            // Add the course completion settings link
4696
            $url = new moodle_url('/course/completion.php', array('id' => $course->id));
4697
            $coursenode->add(get_string('coursecompletion', 'completion'), $url, self::TYPE_SETTING, null, 'coursecompletion',
4698
                             new pix_icon('i/settings', ''));
4699
        }
4700
 
4701
        if (!$adminoptions->update && $adminoptions->tags) {
4702
            $url = new moodle_url('/course/tags.php', array('id' => $course->id));
4703
            $coursenode->add(get_string('coursetags', 'tag'), $url, self::TYPE_SETTING, null, 'coursetags', new pix_icon('i/settings', ''));
4704
            $coursenode->get('coursetags')->set_force_into_more_menu();
4705
        }
4706
 
4707
        // add enrol nodes
4708
        enrol_add_course_navigation($coursenode, $course);
4709
 
4710
        // Manage filters
4711
        if ($adminoptions->filters) {
4712
            $url = new moodle_url('/filter/manage.php', array('contextid'=>$coursecontext->id));
4713
            $coursenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING,
4714
                null, 'filtermanagement', new pix_icon('i/filter', ''));
4715
        }
4716
 
4717
        // View course reports.
4718
        if ($adminoptions->reports) {
4719
            $reportnav = $coursenode->add(get_string('reports'),
4720
                new moodle_url('/report/view.php', ['courseid' => $coursecontext->instanceid]),
4721
                self::TYPE_CONTAINER, null, 'coursereports', new pix_icon('i/stats', ''));
4722
            $coursereports = core_component::get_plugin_list('coursereport');
4723
            foreach ($coursereports as $report => $dir) {
4724
                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
4725
                if (file_exists($libfile)) {
4726
                    require_once($libfile);
4727
                    $reportfunction = $report.'_report_extend_navigation';
4728
                    if (function_exists($report.'_report_extend_navigation')) {
4729
                        $reportfunction($reportnav, $course, $coursecontext);
4730
                    }
4731
                }
4732
            }
4733
 
4734
            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
4735
            foreach ($reports as $reportfunction) {
4736
                $reportfunction($reportnav, $course, $coursecontext);
4737
            }
4738
 
4739
            if (!$reportnav->has_children()) {
4740
                $reportnav->remove();
4741
            }
4742
        }
4743
 
4744
        // Check if we can view the gradebook's setup page.
4745
        if ($adminoptions->gradebook) {
4746
            $url = new moodle_url('/grade/edit/tree/index.php', array('id' => $course->id));
4747
            $coursenode->add(get_string('gradebooksetup', 'grades'), $url, self::TYPE_SETTING,
4748
                null, 'gradebooksetup', new pix_icon('i/settings', ''));
4749
        }
4750
 
4751
        // Add the context locking node.
4752
        $this->add_context_locking_node($coursenode, $coursecontext);
4753
 
4754
        //  Add outcome if permitted
4755
        if ($adminoptions->outcomes) {
4756
            $url = new moodle_url('/grade/edit/outcome/course.php', array('id'=>$course->id));
4757
            $coursenode->add(get_string('outcomes', 'grades'), $url, self::TYPE_SETTING, null, 'outcomes', new pix_icon('i/outcomes', ''));
4758
        }
4759
 
4760
        //Add badges navigation
4761
        if ($adminoptions->badges) {
4762
            require_once($CFG->libdir .'/badgeslib.php');
4763
            badges_add_course_navigation($coursenode, $course);
4764
        }
4765
 
4766
        // Questions
4767
        require_once($CFG->libdir . '/questionlib.php');
4768
        question_extend_settings_navigation($coursenode, $coursecontext)->trim_if_empty();
4769
 
4770
        if ($adminoptions->update) {
4771
            // Repository Instances
4772
            if (!$this->cache->cached('contexthasrepos'.$coursecontext->id)) {
4773
                require_once($CFG->dirroot . '/repository/lib.php');
4774
                $editabletypes = repository::get_editable_types($coursecontext);
4775
                $haseditabletypes = !empty($editabletypes);
4776
                unset($editabletypes);
4777
                $this->cache->set('contexthasrepos'.$coursecontext->id, $haseditabletypes);
4778
            } else {
4779
                $haseditabletypes = $this->cache->{'contexthasrepos'.$coursecontext->id};
4780
            }
4781
            if ($haseditabletypes) {
4782
                $url = new moodle_url('/repository/manage_instances.php', array('contextid' => $coursecontext->id));
4783
                $coursenode->add(get_string('repositories'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/repository', ''));
4784
            }
4785
        }
4786
 
4787
        // Manage files
4788
        if ($adminoptions->files) {
4789
            // hidden in new courses and courses where legacy files were turned off
4790
            $url = new moodle_url('/files/index.php', array('contextid'=>$coursecontext->id));
4791
            $coursenode->add(get_string('courselegacyfiles'), $url, self::TYPE_SETTING, null, 'coursefiles', new pix_icon('i/folder', ''));
4792
 
4793
        }
4794
 
4795
        // Let plugins hook into course navigation.
4796
        $pluginsfunction = get_plugins_with_function('extend_navigation_course', 'lib.php');
4797
        foreach ($pluginsfunction as $plugintype => $plugins) {
4798
            // Ignore the report plugin as it was already loaded above.
4799
            if ($plugintype == 'report') {
4800
                continue;
4801
            }
4802
            foreach ($plugins as $pluginfunction) {
4803
                $pluginfunction($coursenode, $course, $coursecontext);
4804
            }
4805
        }
4806
 
4807
        // Prepare data for course content download functionality if it is enabled.
4808
        if (\core\content::can_export_context($coursecontext, $USER)) {
4809
            $linkattr = \core_course\output\content_export_link::get_attributes($coursecontext);
4810
            $actionlink = new action_link($linkattr->url, $linkattr->displaystring, null, $linkattr->elementattributes);
4811
 
4812
            $coursenode->add($linkattr->displaystring, $actionlink, self::TYPE_SETTING, null, 'download',
4813
                    new pix_icon('t/download', ''));
4814
            $coursenode->get('download')->set_force_into_more_menu(true);
4815
        }
4816
 
4817
        // Course reuse options.
4818
        if ($adminoptions->import
4819
                || $adminoptions->backup
4820
                || $adminoptions->restore
4821
                || $adminoptions->copy
4822
                || $adminoptions->reset) {
4823
            $coursereusenav = $coursenode->add(
4824
                get_string('coursereuse'),
4825
                new moodle_url('/backup/view.php', ['id' => $course->id]),
4826
                self::TYPE_CONTAINER, null, 'coursereuse', new pix_icon('t/edit', ''),
4827
            );
4828
 
4829
            // Import data from other courses.
4830
            if ($adminoptions->import) {
4831
                $url = new moodle_url('/backup/import.php', ['id' => $course->id]);
4832
                $coursereusenav->add(get_string('import'), $url, self::TYPE_SETTING, null, 'import', new pix_icon('i/import', ''));
4833
            }
4834
 
4835
            // Backup this course.
4836
            if ($adminoptions->backup) {
4837
                $url = new moodle_url('/backup/backup.php', ['id' => $course->id]);
4838
                $coursereusenav->add(get_string('backup'), $url, self::TYPE_SETTING, null, 'backup', new pix_icon('i/backup', ''));
4839
            }
4840
 
4841
            // Restore to this course.
4842
            if ($adminoptions->restore) {
4843
                $url = new moodle_url('/backup/restorefile.php', ['contextid' => $coursecontext->id]);
4844
                $coursereusenav->add(
4845
                    get_string('restore'),
4846
                    $url,
4847
                    self::TYPE_SETTING,
4848
                    null,
4849
                    'restore',
4850
                    new pix_icon('i/restore', ''),
4851
                );
4852
            }
4853
 
4854
            // Copy this course.
4855
            if ($adminoptions->copy) {
4856
                $url = new moodle_url('/backup/copy.php', ['id' => $course->id]);
4857
                $coursereusenav->add(get_string('copycourse'), $url, self::TYPE_SETTING, null, 'copy', new pix_icon('t/copy', ''));
4858
            }
4859
 
4860
            // Reset this course.
4861
            if ($adminoptions->reset) {
4862
                $url = new moodle_url('/course/reset.php', ['id' => $course->id]);
4863
                $coursereusenav->add(get_string('reset'), $url, self::TYPE_SETTING, null, 'reset', new pix_icon('i/return', ''));
4864
            }
4865
        }
4866
 
4867
        // Return we are done
4868
        return $coursenode;
4869
    }
4870
 
4871
    /**
4872
     * Get the moodle_page object associated to the current settings navigation.
4873
     *
4874
     * @return moodle_page
4875
     */
4876
    public function get_page(): moodle_page {
4877
        return $this->page;
4878
    }
4879
 
4880
    /**
4881
     * This function calls the module function to inject module settings into the
4882
     * settings navigation tree.
4883
     *
4884
     * This only gets called if there is a corrosponding function in the modules
4885
     * lib file.
4886
     *
4887
     * For examples mod/forum/lib.php {@link forum_extend_settings_navigation()}
4888
     *
4889
     * @return navigation_node|false
4890
     */
4891
    protected function load_module_settings() {
4892
        global $CFG, $USER;
4893
 
4894
        if (!$this->page->cm && $this->context->contextlevel == CONTEXT_MODULE && $this->context->instanceid) {
4895
            $cm = get_coursemodule_from_id(false, $this->context->instanceid, 0, false, MUST_EXIST);
4896
            $this->page->set_cm($cm, $this->page->course);
4897
        }
4898
 
4899
        $file = $CFG->dirroot.'/mod/'.$this->page->activityname.'/lib.php';
4900
        if (file_exists($file)) {
4901
            require_once($file);
4902
        }
4903
 
4904
        $modulenode = $this->add(get_string('pluginadministration', $this->page->activityname), null, self::TYPE_SETTING, null, 'modulesettings');
4905
        $modulenode->nodetype = navigation_node::NODETYPE_BRANCH;
4906
        $modulenode->force_open();
4907
 
4908
        // Settings for the module
4909
        if (has_capability('moodle/course:manageactivities', $this->page->cm->context)) {
4910
            $url = new moodle_url('/course/modedit.php', array('update' => $this->page->cm->id, 'return' => 1));
4911
            $modulenode->add(get_string('settings'), $url, self::TYPE_SETTING, null, 'modedit', new pix_icon('i/settings', ''));
4912
        }
4913
        // Assign local roles
4914
        if (count(get_assignable_roles($this->page->cm->context))>0) {
4915
            $url = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid'=>$this->page->cm->context->id));
4916
            $modulenode->add(get_string('localroles', 'role'), $url, self::TYPE_SETTING, null, 'roleassign',
4917
                new pix_icon('i/role', ''));
4918
        }
4919
        // Override roles
4920
        if (has_capability('moodle/role:review', $this->page->cm->context) or count(get_overridable_roles($this->page->cm->context))>0) {
4921
            $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid'=>$this->page->cm->context->id));
4922
            $modulenode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null, 'roleoverride',
4923
                new pix_icon('i/permissions', ''));
4924
        }
4925
        // Check role permissions
4926
        if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $this->page->cm->context)) {
4927
            $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid'=>$this->page->cm->context->id));
4928
            $modulenode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 'rolecheck',
4929
                new pix_icon('i/checkpermissions', ''));
4930
        }
4931
 
4932
        // Add the context locking node.
4933
        $this->add_context_locking_node($modulenode, $this->page->cm->context);
4934
 
4935
        // Manage filters
4936
        if (has_capability('moodle/filter:manage', $this->page->cm->context) && count(filter_get_available_in_context($this->page->cm->context))>0) {
4937
            $url = new moodle_url('/filter/manage.php', array('contextid'=>$this->page->cm->context->id));
4938
            $modulenode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, 'filtermanage',
4939
                new pix_icon('i/filter', ''));
4940
        }
4941
        // Add reports
4942
        $reports = get_plugin_list_with_function('report', 'extend_navigation_module', 'lib.php');
4943
        foreach ($reports as $reportfunction) {
4944
            $reportfunction($modulenode, $this->page->cm);
4945
        }
4946
        // Add a backup link
4947
        $featuresfunc = $this->page->activityname.'_supports';
4948
        if (function_exists($featuresfunc) && $featuresfunc(FEATURE_BACKUP_MOODLE2) && has_capability('moodle/backup:backupactivity', $this->page->cm->context)) {
4949
            $url = new moodle_url('/backup/backup.php', array('id'=>$this->page->cm->course, 'cm'=>$this->page->cm->id));
4950
            $modulenode->add(get_string('backup'), $url, self::TYPE_SETTING, null, 'backup', new pix_icon('i/backup', ''));
4951
        }
4952
 
4953
        // Restore this activity
4954
        $featuresfunc = $this->page->activityname.'_supports';
4955
        if (function_exists($featuresfunc) && $featuresfunc(FEATURE_BACKUP_MOODLE2) && has_capability('moodle/restore:restoreactivity', $this->page->cm->context)) {
4956
            $url = new moodle_url('/backup/restorefile.php', array('contextid'=>$this->page->cm->context->id));
4957
            $modulenode->add(get_string('restore'), $url, self::TYPE_SETTING, null, 'restore', new pix_icon('i/restore', ''));
4958
        }
4959
 
4960
        // Allow the active advanced grading method plugin to append its settings
4961
        $featuresfunc = $this->page->activityname.'_supports';
4962
        if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING) && has_capability('moodle/grade:managegradingforms', $this->page->cm->context)) {
4963
            require_once($CFG->dirroot.'/grade/grading/lib.php');
4964
            $gradingman = get_grading_manager($this->page->cm->context, 'mod_'.$this->page->activityname);
4965
            $gradingman->extend_settings_navigation($this, $modulenode);
4966
        }
4967
 
4968
        $function = $this->page->activityname.'_extend_settings_navigation';
4969
        if (function_exists($function)) {
4970
            $function($this, $modulenode);
4971
        }
4972
 
4973
        // Send activity to MoodleNet.
4974
        $usercanshare = utilities::can_user_share($this->context->get_course_context(), $USER->id);
4975
        $issuerid = get_config('moodlenet', 'oauthservice');
4976
        try {
4977
            $issuer = \core\oauth2\api::get_issuer($issuerid);
4978
            $isvalidinstance = utilities::is_valid_instance($issuer);
4979
            if ($usercanshare && $isvalidinstance) {
4980
                $this->page->requires->js_call_amd('core/moodlenet/send_resource', 'init');
4981
                $action = new action_link(new moodle_url(''), '', null, [
4982
                    'data-action' => 'sendtomoodlenet',
4983
                    'data-type' => 'activity',
4984
                ]);
4985
                $modulenode->add(get_string('moodlenet:sharetomoodlenet', 'moodle'),
4986
                    $action, self::TYPE_SETTING, null, 'exportmoodlenet')->set_force_into_more_menu(true);
4987
            }
4988
        } catch (dml_missing_record_exception $e) {
4989
            debugging("Invalid MoodleNet OAuth 2 service set in site administration: 'moodlenet | oauthservice'. " .
4990
                "This must be a valid issuer.");
4991
        }
4992
 
4993
        // Remove the module node if there are no children.
4994
        if ($modulenode->children->count() <= 0) {
4995
            $modulenode->remove();
4996
        }
4997
 
4998
        return $modulenode;
4999
    }
5000
 
5001
    /**
5002
     * Loads the user settings block of the settings nav
5003
     *
5004
     * This function is simply works out the userid and whether we need to load
5005
     * just the current users profile settings, or the current user and the user the
5006
     * current user is viewing.
5007
     *
5008
     * This function has some very ugly code to work out the user, if anyone has
5009
     * any bright ideas please feel free to intervene.
5010
     *
5011
     * @param int $courseid The course id of the current course
5012
     * @return navigation_node|false
5013
     */
5014
    protected function load_user_settings($courseid = SITEID) {
5015
        global $USER, $CFG;
5016
 
5017
        if (isguestuser() || !isloggedin()) {
5018
            return false;
5019
        }
5020
 
5021
        $navusers = $this->page->navigation->get_extending_users();
5022
 
5023
        if (count($this->userstoextendfor) > 0 || count($navusers) > 0) {
5024
            $usernode = null;
5025
            foreach ($this->userstoextendfor as $userid) {
5026
                if ($userid == $USER->id) {
5027
                    continue;
5028
                }
5029
                $node = $this->generate_user_settings($courseid, $userid, 'userviewingsettings');
5030
                if (is_null($usernode)) {
5031
                    $usernode = $node;
5032
                }
5033
            }
5034
            foreach ($navusers as $user) {
5035
                if ($user->id == $USER->id) {
5036
                    continue;
5037
                }
5038
                $node = $this->generate_user_settings($courseid, $user->id, 'userviewingsettings');
5039
                if (is_null($usernode)) {
5040
                    $usernode = $node;
5041
                }
5042
            }
5043
            $this->generate_user_settings($courseid, $USER->id);
5044
        } else {
5045
            $usernode = $this->generate_user_settings($courseid, $USER->id);
5046
        }
5047
        return $usernode;
5048
    }
5049
 
5050
    /**
5051
     * Extends the settings navigation for the given user.
5052
     *
5053
     * Note: This method gets called automatically if you call
5054
     * $PAGE->navigation->extend_for_user($userid)
5055
     *
5056
     * @param int $userid
5057
     */
5058
    public function extend_for_user($userid) {
5059
        global $CFG;
5060
 
5061
        if (!in_array($userid, $this->userstoextendfor)) {
5062
            $this->userstoextendfor[] = $userid;
5063
            if ($this->initialised) {
5064
                $this->generate_user_settings($this->page->course->id, $userid, 'userviewingsettings');
5065
                $children = array();
5066
                foreach ($this->children as $child) {
5067
                    $children[] = $child;
5068
                }
5069
                array_unshift($children, array_pop($children));
5070
                $this->children = new navigation_node_collection();
5071
                foreach ($children as $child) {
5072
                    $this->children->add($child);
5073
                }
5074
            }
5075
        }
5076
    }
5077
 
5078
    /**
5079
     * This function gets called by {@link settings_navigation::load_user_settings()} and actually works out
5080
     * what can be shown/done
5081
     *
5082
     * @param int $courseid The current course' id
5083
     * @param int $userid The user id to load for
5084
     * @param string $gstitle The string to pass to get_string for the branch title
5085
     * @return navigation_node|false
5086
     */
5087
    protected function generate_user_settings($courseid, $userid, $gstitle='usercurrentsettings') {
5088
        global $DB, $CFG, $USER, $SITE;
5089
 
5090
        if ($courseid != $SITE->id) {
5091
            if (!empty($this->page->course->id) && $this->page->course->id == $courseid) {
5092
                $course = $this->page->course;
5093
            } else {
5094
                $select = context_helper::get_preload_record_columns_sql('ctx');
5095
                $sql = "SELECT c.*, $select
5096
                          FROM {course} c
5097
                          JOIN {context} ctx ON c.id = ctx.instanceid
5098
                         WHERE c.id = :courseid AND ctx.contextlevel = :contextlevel";
5099
                $params = array('courseid' => $courseid, 'contextlevel' => CONTEXT_COURSE);
5100
                $course = $DB->get_record_sql($sql, $params, MUST_EXIST);
5101
                context_helper::preload_from_record($course);
5102
            }
5103
        } else {
5104
            $course = $SITE;
5105
        }
5106
 
5107
        $coursecontext = context_course::instance($course->id);   // Course context
5108
        $systemcontext   = context_system::instance();
5109
        $currentuser = ($USER->id == $userid);
5110
 
5111
        if ($currentuser) {
5112
            $user = $USER;
5113
            $usercontext = context_user::instance($user->id);       // User context
5114
        } else {
5115
            $select = context_helper::get_preload_record_columns_sql('ctx');
5116
            $sql = "SELECT u.*, $select
5117
                      FROM {user} u
5118
                      JOIN {context} ctx ON u.id = ctx.instanceid
5119
                     WHERE u.id = :userid AND ctx.contextlevel = :contextlevel";
5120
            $params = array('userid' => $userid, 'contextlevel' => CONTEXT_USER);
5121
            $user = $DB->get_record_sql($sql, $params, IGNORE_MISSING);
5122
            if (!$user) {
5123
                return false;
5124
            }
5125
            context_helper::preload_from_record($user);
5126
 
5127
            // Check that the user can view the profile
5128
            $usercontext = context_user::instance($user->id); // User context
5129
            $canviewuser = has_capability('moodle/user:viewdetails', $usercontext);
5130
 
5131
            if ($course->id == $SITE->id) {
5132
                if ($CFG->forceloginforprofiles && !has_coursecontact_role($user->id) && !$canviewuser) {  // Reduce possibility of "browsing" userbase at site level
5133
                    // Teachers can browse and be browsed at site level. If not forceloginforprofiles, allow access (bug #4366)
5134
                    return false;
5135
                }
5136
            } else {
5137
                $canviewusercourse = has_capability('moodle/user:viewdetails', $coursecontext);
5138
                $userisenrolled = is_enrolled($coursecontext, $user->id, '', true);
5139
                if ((!$canviewusercourse && !$canviewuser) || !$userisenrolled) {
5140
                    return false;
5141
                }
5142
                $canaccessallgroups = has_capability('moodle/site:accessallgroups', $coursecontext);
5143
                if (!$canaccessallgroups && groups_get_course_groupmode($course) == SEPARATEGROUPS && !$canviewuser) {
5144
                    // If groups are in use, make sure we can see that group (MDL-45874). That does not apply to parents.
5145
                    if ($courseid == $this->page->course->id) {
5146
                        $mygroups = get_fast_modinfo($this->page->course)->groups;
5147
                    } else {
5148
                        $mygroups = groups_get_user_groups($courseid);
5149
                    }
5150
                    $usergroups = groups_get_user_groups($courseid, $userid);
5151
                    if (!array_intersect_key($mygroups[0], $usergroups[0])) {
5152
                        return false;
5153
                    }
5154
                }
5155
            }
5156
        }
5157
 
5158
        $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $this->page->context));
5159
 
5160
        $key = $gstitle;
5161
        $prefurl = new moodle_url('/user/preferences.php');
5162
        if ($gstitle != 'usercurrentsettings') {
5163
            $key .= $userid;
5164
            $prefurl->param('userid', $userid);
5165
        }
5166
 
5167
        // Add a user setting branch.
5168
        if ($gstitle == 'usercurrentsettings') {
5169
            $mainpage = $this->add(get_string('home'), new moodle_url('/'), self::TYPE_CONTAINER, null, 'site');
5170
 
5171
            // This should be set to false as we don't want to show this to the user. It's only for generating the correct
5172
            // breadcrumb.
5173
            $mainpage->display = false;
5174
            $homepage = get_home_page();
5175
            if (($homepage == HOMEPAGE_MY || $homepage == HOMEPAGE_MYCOURSES)) {
5176
                $mainpage->mainnavonly = true;
5177
            }
5178
 
5179
            $iscurrentuser = ($user->id == $USER->id);
5180
 
5181
            $baseargs = array('id' => $user->id);
5182
            if ($course->id != $SITE->id && !$iscurrentuser) {
5183
                $baseargs['course'] = $course->id;
5184
                $issitecourse = false;
5185
            } else {
5186
                // Load all categories and get the context for the system.
5187
                $issitecourse = true;
5188
            }
5189
 
5190
            // Add the user profile to the dashboard.
5191
            $profilenode = $mainpage->add(get_string('profile'), new moodle_url('/user/profile.php',
5192
                    array('id' => $user->id)), self::TYPE_SETTING, null, 'myprofile');
5193
 
5194
            if (!empty($CFG->navadduserpostslinks)) {
5195
                // Add nodes for forum posts and discussions if the user can view either or both
5196
                // There are no capability checks here as the content of the page is based
5197
                // purely on the forums the current user has access too.
5198
                $forumtab = $profilenode->add(get_string('forumposts', 'forum'));
5199
                $forumtab->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', $baseargs), null, 'myposts');
5200
                $forumtab->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php',
5201
                        array_merge($baseargs, array('mode' => 'discussions'))), null, 'mydiscussions');
5202
            }
5203
 
5204
            // Add blog nodes.
5205
            if (!empty($CFG->enableblogs)) {
5206
                if (!$this->cache->cached('userblogoptions'.$user->id)) {
5207
                    require_once($CFG->dirroot.'/blog/lib.php');
5208
                    // Get all options for the user.
5209
                    $options = blog_get_options_for_user($user);
5210
                    $this->cache->set('userblogoptions'.$user->id, $options);
5211
                } else {
5212
                    $options = $this->cache->{'userblogoptions'.$user->id};
5213
                }
5214
 
5215
                if (count($options) > 0) {
5216
                    $blogs = $profilenode->add(get_string('blogs', 'blog'), null, navigation_node::TYPE_CONTAINER);
5217
                    foreach ($options as $type => $option) {
5218
                        if ($type == "rss") {
5219
                            $blogs->add($option['string'], $option['link'], self::TYPE_SETTING, null, null,
5220
                                    new pix_icon('i/rss', ''));
5221
                        } else {
5222
                            $blogs->add($option['string'], $option['link'], self::TYPE_SETTING, null, 'blog' . $type);
5223
                        }
5224
                    }
5225
                }
5226
            }
5227
 
5228
            // Add the messages link.
5229
            // It is context based so can appear in the user's profile and in course participants information.
5230
            if (!empty($CFG->messaging)) {
5231
                $messageargs = array('user1' => $USER->id);
5232
                if ($USER->id != $user->id) {
5233
                    $messageargs['user2'] = $user->id;
5234
                }
5235
                $url = new moodle_url('/message/index.php', $messageargs);
5236
                $mainpage->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages');
5237
            }
5238
 
5239
            // Add the "My private files" link.
5240
            // This link doesn't have a unique display for course context so only display it under the user's profile.
5241
            if ($issitecourse && $iscurrentuser && has_capability('moodle/user:manageownfiles', $usercontext)) {
5242
                $url = new moodle_url('/user/files.php');
5243
                $mainpage->add(get_string('privatefiles'), $url, self::TYPE_SETTING, null, 'privatefiles');
5244
            }
5245
 
5246
            // Add a node to view the users notes if permitted.
5247
            if (!empty($CFG->enablenotes) &&
5248
                    has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $coursecontext)) {
5249
                $url = new moodle_url('/notes/index.php', array('user' => $user->id));
5250
                if ($coursecontext->instanceid != SITEID) {
5251
                    $url->param('course', $coursecontext->instanceid);
5252
                }
5253
                $profilenode->add(get_string('notes', 'notes'), $url);
5254
            }
5255
 
5256
            // Show the grades node.
5257
            if (($issitecourse && $iscurrentuser) || has_capability('moodle/user:viewdetails', $usercontext)) {
5258
                require_once($CFG->dirroot . '/user/lib.php');
5259
                // Set the grades node to link to the "Grades" page.
5260
                if ($course->id == SITEID) {
5261
                    $url = user_mygrades_url($user->id, $course->id);
5262
                } else { // Otherwise we are in a course and should redirect to the user grade report (Activity report version).
5263
                    $url = new moodle_url('/course/user.php', array('mode' => 'grade', 'id' => $course->id, 'user' => $user->id));
5264
                }
5265
                $mainpage->add(get_string('grades', 'grades'), $url, self::TYPE_SETTING, null, 'mygrades');
5266
            }
5267
 
5268
            // Let plugins hook into user navigation.
5269
            $pluginsfunction = get_plugins_with_function('extend_navigation_user', 'lib.php');
5270
            foreach ($pluginsfunction as $plugintype => $plugins) {
5271
                if ($plugintype != 'report') {
5272
                    foreach ($plugins as $pluginfunction) {
5273
                        $pluginfunction($profilenode, $user, $usercontext, $course, $coursecontext);
5274
                    }
5275
                }
5276
            }
5277
 
5278
            $usersetting = navigation_node::create(get_string('preferences', 'moodle'), $prefurl, self::TYPE_CONTAINER, null, $key);
5279
            $mainpage->add_node($usersetting);
5280
        } else {
5281
            $usersetting = $this->add(get_string('preferences', 'moodle'), $prefurl, self::TYPE_CONTAINER, null, $key);
5282
            $usersetting->display = false;
5283
        }
5284
        $usersetting->id = 'usersettings';
5285
 
5286
        // Check if the user has been deleted.
5287
        if ($user->deleted) {
5288
            if (!has_capability('moodle/user:update', $coursecontext)) {
5289
                // We can't edit the user so just show the user deleted message.
5290
                $usersetting->add(get_string('userdeleted'), null, self::TYPE_SETTING);
5291
            } else {
5292
                // We can edit the user so show the user deleted message and link it to the profile.
5293
                if ($course->id == $SITE->id) {
5294
                    $profileurl = new moodle_url('/user/profile.php', array('id'=>$user->id));
5295
                } else {
5296
                    $profileurl = new moodle_url('/user/view.php', array('id'=>$user->id, 'course'=>$course->id));
5297
                }
5298
                $usersetting->add(get_string('userdeleted'), $profileurl, self::TYPE_SETTING);
5299
            }
5300
            return true;
5301
        }
5302
 
5303
        $userauthplugin = false;
5304
        if (!empty($user->auth)) {
5305
            $userauthplugin = get_auth_plugin($user->auth);
5306
        }
5307
 
5308
        $useraccount = $usersetting->add(get_string('useraccount'), null, self::TYPE_CONTAINER, null, 'useraccount');
5309
 
5310
        // Add the profile edit link.
5311
        if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) {
5312
            if (($currentuser || is_siteadmin($USER) || !is_siteadmin($user)) &&
5313
                    has_capability('moodle/user:update', $systemcontext)) {
5314
                $url = new moodle_url('/user/editadvanced.php', array('id'=>$user->id, 'course'=>$course->id));
5315
                $useraccount->add(get_string('editmyprofile'), $url, self::TYPE_SETTING, null, 'editprofile');
5316
            } else if ((has_capability('moodle/user:editprofile', $usercontext) && !is_siteadmin($user)) ||
5317
                    ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext))) {
5318
                if ($userauthplugin && $userauthplugin->can_edit_profile()) {
5319
                    $url = $userauthplugin->edit_profile_url();
5320
                    if (empty($url)) {
5321
                        $url = new moodle_url('/user/edit.php', array('id'=>$user->id, 'course'=>$course->id));
5322
                    }
5323
                    $useraccount->add(get_string('editmyprofile'), $url, self::TYPE_SETTING, null, 'editprofile');
5324
                }
5325
            }
5326
        }
5327
 
5328
        // Change password link.
5329
        if ($userauthplugin && $currentuser && !\core\session\manager::is_loggedinas() && !isguestuser() &&
5330
                has_capability('moodle/user:changeownpassword', $systemcontext) && $userauthplugin->can_change_password()) {
5331
            $passwordchangeurl = $userauthplugin->change_password_url();
5332
            if (empty($passwordchangeurl)) {
5333
                $passwordchangeurl = new moodle_url('/login/change_password.php', array('id'=>$course->id));
5334
            }
5335
            $useraccount->add(get_string("changepassword"), $passwordchangeurl, self::TYPE_SETTING, null, 'changepassword');
5336
        }
5337
 
5338
        // Default homepage.
5339
        $defaulthomepageuser = (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
5340
        if (isloggedin() && !isguestuser($user) && $defaulthomepageuser) {
5341
            if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) ||
5342
                    has_capability('moodle/user:editprofile', $usercontext)) {
5343
                $url = new moodle_url('/user/defaulthomepage.php', ['id' => $user->id]);
5344
                $useraccount->add(get_string('defaulthomepageuser'), $url, self::TYPE_SETTING, null, 'defaulthomepageuser');
5345
            }
5346
        }
5347
 
5348
        if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) {
5349
            if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) ||
5350
                    has_capability('moodle/user:editprofile', $usercontext)) {
5351
                $url = new moodle_url('/user/language.php', array('id' => $user->id, 'course' => $course->id));
5352
                $useraccount->add(get_string('preferredlanguage'), $url, self::TYPE_SETTING, null, 'preferredlanguage');
5353
            }
5354
        }
5355
        $pluginmanager = core_plugin_manager::instance();
5356
        $enabled = $pluginmanager->get_enabled_plugins('mod');
5357
        if (isset($enabled['forum']) && isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) {
5358
            if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) ||
5359
                    has_capability('moodle/user:editprofile', $usercontext)) {
5360
                $url = new moodle_url('/user/forum.php', array('id' => $user->id, 'course' => $course->id));
5361
                $useraccount->add(get_string('forumpreferences'), $url, self::TYPE_SETTING);
5362
            }
5363
        }
5364
        $editors = editors_get_enabled();
5365
        if (count($editors) > 1) {
5366
            if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) {
5367
                if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) ||
5368
                        has_capability('moodle/user:editprofile', $usercontext)) {
5369
                    $url = new moodle_url('/user/editor.php', array('id' => $user->id, 'course' => $course->id));
5370
                    $useraccount->add(get_string('editorpreferences'), $url, self::TYPE_SETTING);
5371
                }
5372
            }
5373
        }
5374
 
5375
        // Add "Calendar preferences" link.
5376
        if (isloggedin() && !isguestuser($user)) {
5377
            if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) ||
5378
                    has_capability('moodle/user:editprofile', $usercontext)) {
5379
                $url = new moodle_url('/user/calendar.php', array('id' => $user->id));
5380
                $useraccount->add(get_string('calendarpreferences', 'calendar'), $url, self::TYPE_SETTING, null, 'preferredcalendar');
5381
            }
5382
        }
5383
 
5384
        // Add "Content bank preferences" link.
5385
        if (isloggedin() && !isguestuser($user)) {
5386
            if ($currentuser && has_capability('moodle/user:editownprofile', $systemcontext) ||
5387
                has_capability('moodle/user:editprofile', $usercontext)) {
5388
                $url = new moodle_url('/user/contentbank.php', ['id' => $user->id]);
5389
                $useraccount->add(get_string('contentbankpreferences', 'core_contentbank'), $url, self::TYPE_SETTING,
5390
                        null, 'contentbankpreferences');
5391
            }
5392
        }
5393
 
5394
        // View the roles settings.
5395
        if (has_any_capability(['moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override',
5396
                'moodle/role:manage'], $usercontext)) {
5397
            $roles = $usersetting->add(get_string('roles'), null, self::TYPE_SETTING);
5398
 
5399
            $url = new moodle_url('/admin/roles/usersroles.php', ['userid' => $user->id, 'courseid' => $course->id]);
5400
            $roles->add(get_string('thisusersroles', 'role'), $url, self::TYPE_SETTING);
5401
 
5402
            $assignableroles = get_assignable_roles($usercontext, ROLENAME_BOTH);
5403
 
5404
            if (!empty($assignableroles)) {
5405
                $url = new moodle_url('/admin/roles/assign.php',
5406
                        array('contextid' => $usercontext->id, 'userid' => $user->id, 'courseid' => $course->id));
5407
                $roles->add(get_string('assignrolesrelativetothisuser', 'role'), $url, self::TYPE_SETTING);
5408
            }
5409
 
5410
            if (has_capability('moodle/role:review', $usercontext) || count(get_overridable_roles($usercontext, ROLENAME_BOTH))>0) {
5411
                $url = new moodle_url('/admin/roles/permissions.php',
5412
                        array('contextid' => $usercontext->id, 'userid' => $user->id, 'courseid' => $course->id));
5413
                $roles->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING);
5414
            }
5415
 
5416
            $url = new moodle_url('/admin/roles/check.php',
5417
                    array('contextid' => $usercontext->id, 'userid' => $user->id, 'courseid' => $course->id));
5418
            $roles->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING);
5419
        }
5420
 
5421
        // Repositories.
5422
        if (!$this->cache->cached('contexthasrepos'.$usercontext->id)) {
5423
            require_once($CFG->dirroot . '/repository/lib.php');
5424
            $editabletypes = repository::get_editable_types($usercontext);
5425
            $haseditabletypes = !empty($editabletypes);
5426
            unset($editabletypes);
5427
            $this->cache->set('contexthasrepos'.$usercontext->id, $haseditabletypes);
5428
        } else {
5429
            $haseditabletypes = $this->cache->{'contexthasrepos'.$usercontext->id};
5430
        }
5431
        if ($haseditabletypes) {
5432
            $repositories = $usersetting->add(get_string('repositories', 'repository'), null, self::TYPE_SETTING);
5433
            $repositories->add(get_string('manageinstances', 'repository'), new moodle_url('/repository/manage_instances.php',
5434
                array('contextid' => $usercontext->id)));
5435
        }
5436
 
5437
        // Portfolio.
5438
        if ($currentuser && !empty($CFG->enableportfolios) && has_capability('moodle/portfolio:export', $systemcontext)) {
5439
            require_once($CFG->libdir . '/portfoliolib.php');
5440
            if (portfolio_has_visible_instances()) {
5441
                $portfolio = $usersetting->add(get_string('portfolios', 'portfolio'), null, self::TYPE_SETTING);
5442
 
5443
                $url = new moodle_url('/user/portfolio.php', array('courseid'=>$course->id));
5444
                $portfolio->add(get_string('configure', 'portfolio'), $url, self::TYPE_SETTING);
5445
 
5446
                $url = new moodle_url('/user/portfoliologs.php', array('courseid'=>$course->id));
5447
                $portfolio->add(get_string('logs', 'portfolio'), $url, self::TYPE_SETTING);
5448
            }
5449
        }
5450
 
5451
        $enablemanagetokens = false;
5452
        if (!empty($CFG->enablerssfeeds)) {
5453
            $enablemanagetokens = true;
5454
        } else if (!is_siteadmin($USER->id)
5455
             && !empty($CFG->enablewebservices)
5456
             && has_capability('moodle/webservice:createtoken', context_system::instance()) ) {
5457
            $enablemanagetokens = true;
5458
        }
5459
        // Security keys.
5460
        if ($currentuser && $enablemanagetokens) {
5461
            $url = new moodle_url('/user/managetoken.php');
5462
            $useraccount->add(get_string('securitykeys', 'webservice'), $url, self::TYPE_SETTING);
5463
        }
5464
 
5465
        // Messaging.
5466
        if (($currentuser && has_capability('moodle/user:editownmessageprofile', $systemcontext)) || (!isguestuser($user) &&
5467
                has_capability('moodle/user:editmessageprofile', $usercontext) && !is_primary_admin($user->id))) {
5468
            $messagingurl = new moodle_url('/message/edit.php', array('id' => $user->id));
5469
            $notificationsurl = new moodle_url('/message/notificationpreferences.php', array('userid' => $user->id));
5470
            $useraccount->add(get_string('messagepreferences', 'message'), $messagingurl, self::TYPE_SETTING);
5471
            $useraccount->add(get_string('notificationpreferences', 'message'), $notificationsurl, self::TYPE_SETTING);
5472
        }
5473
 
5474
        // Blogs.
5475
        if ($currentuser && !empty($CFG->enableblogs)) {
5476
            $blog = $usersetting->add(get_string('blogs', 'blog'), null, navigation_node::TYPE_CONTAINER, null, 'blogs');
5477
            if (has_capability('moodle/blog:view', $systemcontext)) {
5478
                $blog->add(get_string('preferences', 'blog'), new moodle_url('/blog/preferences.php'),
5479
                        navigation_node::TYPE_SETTING);
5480
            }
5481
            if (!empty($CFG->useexternalblogs) && $CFG->maxexternalblogsperuser > 0 &&
5482
                    has_capability('moodle/blog:manageexternal', $systemcontext)) {
5483
                $blog->add(get_string('externalblogs', 'blog'), new moodle_url('/blog/external_blogs.php'),
5484
                        navigation_node::TYPE_SETTING);
5485
                $blog->add(get_string('addnewexternalblog', 'blog'), new moodle_url('/blog/external_blog_edit.php'),
5486
                        navigation_node::TYPE_SETTING);
5487
            }
5488
            // Remove the blog node if empty.
5489
            $blog->trim_if_empty();
5490
        }
5491
 
5492
        // Badges.
5493
        if ($currentuser && !empty($CFG->enablebadges)) {
5494
            $badges = $usersetting->add(get_string('badges'), null, navigation_node::TYPE_CONTAINER, null, 'badges');
5495
            if (has_capability('moodle/badges:manageownbadges', $usercontext)) {
5496
                $url = new moodle_url('/badges/mybadges.php');
5497
                $badges->add(get_string('managebadges', 'badges'), $url, self::TYPE_SETTING);
5498
            }
5499
            $badges->add(get_string('preferences', 'badges'), new moodle_url('/badges/preferences.php'),
5500
                    navigation_node::TYPE_SETTING);
5501
            if (!empty($CFG->badges_allowexternalbackpack)) {
5502
                $badges->add(get_string('backpackdetails', 'badges'), new moodle_url('/badges/mybackpack.php'),
5503
                        navigation_node::TYPE_SETTING);
5504
            }
5505
        }
5506
 
5507
        // Let plugins hook into user settings navigation.
5508
        $pluginsfunction = get_plugins_with_function('extend_navigation_user_settings', 'lib.php');
5509
        foreach ($pluginsfunction as $plugintype => $plugins) {
5510
            foreach ($plugins as $pluginfunction) {
5511
                $pluginfunction($usersetting, $user, $usercontext, $course, $coursecontext);
5512
            }
5513
        }
5514
 
5515
        return $usersetting;
5516
    }
5517
 
5518
    /**
5519
     * Loads block specific settings in the navigation
5520
     *
5521
     * @return navigation_node
5522
     */
5523
    protected function load_block_settings() {
5524
        global $CFG;
5525
 
5526
        $blocknode = $this->add($this->context->get_context_name(), null, self::TYPE_SETTING, null, 'blocksettings');
5527
        $blocknode->force_open();
5528
 
5529
        // Assign local roles
5530
        if (get_assignable_roles($this->context, ROLENAME_ORIGINAL)) {
5531
            $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $this->context->id));
5532
            $blocknode->add(get_string('assignroles', 'role'), $assignurl, self::TYPE_SETTING, null,
5533
                'roles', new pix_icon('i/assignroles', ''));
5534
        }
5535
 
5536
        // Override roles
5537
        if (has_capability('moodle/role:review', $this->context) or  count(get_overridable_roles($this->context))>0) {
5538
            $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid'=>$this->context->id));
5539
            $blocknode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null,
5540
                'permissions', new pix_icon('i/permissions', ''));
5541
        }
5542
        // Check role permissions
5543
        if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $this->context)) {
5544
            $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid'=>$this->context->id));
5545
            $blocknode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null,
5546
                'checkpermissions', new pix_icon('i/checkpermissions', ''));
5547
        }
5548
 
5549
        // Add the context locking node.
5550
        $this->add_context_locking_node($blocknode, $this->context);
5551
 
5552
        return $blocknode;
5553
    }
5554
 
5555
    /**
5556
     * Loads category specific settings in the navigation
5557
     *
5558
     * @return navigation_node
5559
     */
5560
    protected function load_category_settings() {
5561
        global $CFG;
5562
 
5563
        // We can land here while being in the context of a block, in which case we
5564
        // should get the parent context which should be the category one. See self::initialise().
5565
        if ($this->context->contextlevel == CONTEXT_BLOCK) {
5566
            $catcontext = $this->context->get_parent_context();
5567
        } else {
5568
            $catcontext = $this->context;
5569
        }
5570
 
5571
        // Let's make sure that we always have the right context when getting here.
5572
        if ($catcontext->contextlevel != CONTEXT_COURSECAT) {
5573
            throw new coding_exception('Unexpected context while loading category settings.');
5574
        }
5575
 
5576
        $categorynodetype = navigation_node::TYPE_CONTAINER;
5577
        $categorynode = $this->add($catcontext->get_context_name(), null, $categorynodetype, null, 'categorysettings');
5578
        $categorynode->nodetype = navigation_node::NODETYPE_BRANCH;
5579
        $categorynode->force_open();
5580
 
5581
        if (can_edit_in_category($catcontext->instanceid)) {
5582
            $url = new moodle_url('/course/management.php', array('categoryid' => $catcontext->instanceid));
5583
            $editstring = get_string('managecategorythis');
5584
            $node = $categorynode->add($editstring, $url, self::TYPE_SETTING, null, 'managecategory', new pix_icon('i/edit', ''));
5585
            $node->set_show_in_secondary_navigation(false);
5586
        }
5587
 
5588
        if (has_capability('moodle/category:manage', $catcontext)) {
5589
            $editurl = new moodle_url('/course/editcategory.php', array('id' => $catcontext->instanceid));
5590
            $categorynode->add(get_string('settings'), $editurl, self::TYPE_SETTING, null, 'edit', new pix_icon('i/edit', ''));
5591
 
5592
            $addsubcaturl = new moodle_url('/course/editcategory.php', array('parent' => $catcontext->instanceid));
5593
            $categorynode->add(get_string('addsubcategory'), $addsubcaturl, self::TYPE_SETTING, null,
5594
                'addsubcat', new pix_icon('i/withsubcat', ''))->set_show_in_secondary_navigation(false);
5595
        }
5596
 
5597
        // Assign local roles
5598
        $assignableroles = get_assignable_roles($catcontext);
5599
        if (!empty($assignableroles)) {
5600
            $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $catcontext->id));
5601
            $categorynode->add(get_string('assignroles', 'role'), $assignurl, self::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
5602
        }
5603
 
5604
        // Override roles
5605
        if (has_capability('moodle/role:review', $catcontext) or count(get_overridable_roles($catcontext)) > 0) {
5606
            $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid' => $catcontext->id));
5607
            $categorynode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null, 'permissions', new pix_icon('i/permissions', ''));
5608
        }
5609
        // Check role permissions
5610
        if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride',
5611
                'moodle/role:override', 'moodle/role:assign'), $catcontext)) {
5612
            $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid' => $catcontext->id));
5613
            $categorynode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 'rolecheck', new pix_icon('i/checkpermissions', ''));
5614
        }
5615
 
5616
        // Add the context locking node.
5617
        $this->add_context_locking_node($categorynode, $catcontext);
5618
 
5619
        // Cohorts
5620
        if (has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $catcontext)) {
5621
            $categorynode->add(get_string('cohorts', 'cohort'), new moodle_url('/cohort/index.php',
5622
                array('contextid' => $catcontext->id)), self::TYPE_SETTING, null, 'cohort', new pix_icon('i/cohort', ''));
5623
        }
5624
 
5625
        // Manage filters
5626
        if (has_capability('moodle/filter:manage', $catcontext) && count(filter_get_available_in_context($catcontext)) > 0) {
5627
            $url = new moodle_url('/filter/manage.php', array('contextid' => $catcontext->id));
5628
            $categorynode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, 'filters', new pix_icon('i/filter', ''));
5629
        }
5630
 
5631
        // Restore.
5632
        if (has_capability('moodle/restore:restorecourse', $catcontext)) {
5633
            $url = new moodle_url('/backup/restorefile.php', array('contextid' => $catcontext->id));
5634
            $categorynode->add(get_string('restorecourse', 'admin'), $url, self::TYPE_SETTING, null, 'restorecourse', new pix_icon('i/restore', ''));
5635
        }
5636
 
5637
        // Let plugins hook into category settings navigation.
5638
        $pluginsfunction = get_plugins_with_function('extend_navigation_category_settings', 'lib.php');
5639
        foreach ($pluginsfunction as $plugintype => $plugins) {
5640
            foreach ($plugins as $pluginfunction) {
5641
                $pluginfunction($categorynode, $catcontext);
5642
            }
5643
        }
5644
 
5645
        $cb = new contentbank();
5646
        if ($cb->is_context_allowed($catcontext)
5647
            && has_capability('moodle/contentbank:access', $catcontext)) {
5648
            $url = new \moodle_url('/contentbank/index.php', ['contextid' => $catcontext->id]);
5649
            $categorynode->add(get_string('contentbank'), $url, self::TYPE_CUSTOM, null,
5650
                'contentbank', new \pix_icon('i/contentbank', ''));
5651
        }
5652
 
5653
        return $categorynode;
5654
    }
5655
 
5656
    /**
5657
     * Determine whether the user is assuming another role
5658
     *
5659
     * This function checks to see if the user is assuming another role by means of
5660
     * role switching. In doing this we compare each RSW key (context path) against
5661
     * the current context path. This ensures that we can provide the switching
5662
     * options against both the course and any page shown under the course.
5663
     *
5664
     * @return bool|int The role(int) if the user is in another role, false otherwise
5665
     */
5666
    protected function in_alternative_role() {
5667
        global $USER;
5668
        if (!empty($USER->access['rsw']) && is_array($USER->access['rsw'])) {
5669
            if (!empty($this->page->context) && !empty($USER->access['rsw'][$this->page->context->path])) {
5670
                return $USER->access['rsw'][$this->page->context->path];
5671
            }
5672
            foreach ($USER->access['rsw'] as $key=>$role) {
5673
                if (strpos($this->context->path,$key)===0) {
5674
                    return $role;
5675
                }
5676
            }
5677
        }
5678
        return false;
5679
    }
5680
 
5681
    /**
5682
     * This function loads all of the front page settings into the settings navigation.
5683
     * This function is called when the user is on the front page, or $COURSE==$SITE
5684
     * @param bool $forceopen (optional)
5685
     * @return navigation_node
5686
     */
5687
    protected function load_front_page_settings($forceopen = false) {
5688
        global $SITE, $CFG;
5689
        require_once($CFG->dirroot . '/course/lib.php');
5690
 
5691
        $course = clone($SITE);
5692
        $coursecontext = context_course::instance($course->id);   // Course context
5693
        $adminoptions = course_get_user_administration_options($course, $coursecontext);
5694
 
5695
        $frontpage = $this->add(get_string('frontpagesettings'), null, self::TYPE_SETTING, null, 'frontpage');
5696
        if ($forceopen) {
5697
            $frontpage->force_open();
5698
        }
5699
        $frontpage->id = 'frontpagesettings';
5700
 
5701
        if ($this->page->user_allowed_editing() && !$this->page->theme->haseditswitch) {
5702
 
5703
            // Add the turn on/off settings
5704
            $url = new moodle_url('/course/view.php', array('id'=>$course->id, 'sesskey'=>sesskey()));
5705
            if ($this->page->user_is_editing()) {
5706
                $url->param('edit', 'off');
5707
                $editstring = get_string('turneditingoff');
5708
            } else {
5709
                $url->param('edit', 'on');
5710
                $editstring = get_string('turneditingon');
5711
            }
5712
            $frontpage->add($editstring, $url, self::TYPE_SETTING, null, null, new pix_icon('i/edit', ''));
5713
        }
5714
 
5715
        if ($adminoptions->update) {
5716
            // Add the course settings link
5717
            $url = new moodle_url('/admin/settings.php', array('section'=>'frontpagesettings'));
5718
            $frontpage->add(get_string('settings'), $url, self::TYPE_SETTING, null,
5719
                'editsettings', new pix_icon('i/settings', ''));
5720
        }
5721
 
5722
        // add enrol nodes
5723
        enrol_add_course_navigation($frontpage, $course);
5724
 
5725
        // Manage filters
5726
        if ($adminoptions->filters) {
5727
            $url = new moodle_url('/filter/manage.php', array('contextid'=>$coursecontext->id));
5728
            $frontpage->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING,
5729
                null, 'filtermanagement', new pix_icon('i/filter', ''));
5730
        }
5731
 
5732
        // View course reports.
5733
        if ($adminoptions->reports) {
5734
            $frontpagenav = $frontpage->add(get_string('reports'), new moodle_url('/report/view.php',
5735
                ['courseid' => $coursecontext->instanceid]),
5736
                self::TYPE_CONTAINER, null, 'coursereports',
5737
                    new pix_icon('i/stats', ''));
5738
            $coursereports = core_component::get_plugin_list('coursereport');
5739
            foreach ($coursereports as $report=>$dir) {
5740
                $libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
5741
                if (file_exists($libfile)) {
5742
                    require_once($libfile);
5743
                    $reportfunction = $report.'_report_extend_navigation';
5744
                    if (function_exists($report.'_report_extend_navigation')) {
5745
                        $reportfunction($frontpagenav, $course, $coursecontext);
5746
                    }
5747
                }
5748
            }
5749
 
5750
            $reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
5751
            foreach ($reports as $reportfunction) {
5752
                $reportfunction($frontpagenav, $course, $coursecontext);
5753
            }
5754
 
5755
            if (!$frontpagenav->has_children()) {
5756
                $frontpagenav->remove();
5757
            }
5758
        }
5759
 
5760
        // Questions
5761
        require_once($CFG->libdir . '/questionlib.php');
5762
        question_extend_settings_navigation($frontpage, $coursecontext)->trim_if_empty();
5763
 
5764
        // Manage files
5765
        if ($adminoptions->files) {
5766
            //hiden in new installs
5767
            $url = new moodle_url('/files/index.php', array('contextid'=>$coursecontext->id));
5768
            $frontpage->add(get_string('sitelegacyfiles'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/folder', ''));
5769
        }
5770
 
5771
        // Let plugins hook into frontpage navigation.
5772
        $pluginsfunction = get_plugins_with_function('extend_navigation_frontpage', 'lib.php');
5773
        foreach ($pluginsfunction as $plugintype => $plugins) {
5774
            foreach ($plugins as $pluginfunction) {
5775
                $pluginfunction($frontpage, $course, $coursecontext);
5776
            }
5777
        }
5778
 
5779
        // Course reuse options.
5780
        if ($adminoptions->backup || $adminoptions->restore) {
5781
            $coursereusenav = $frontpage->add(
5782
                get_string('coursereuse'),
5783
                new moodle_url('/backup/view.php', ['id' => $course->id]),
5784
                self::TYPE_CONTAINER, null, 'coursereuse', new pix_icon('t/edit', ''),
5785
            );
5786
 
5787
            // Backup this course.
5788
            if ($adminoptions->backup) {
5789
                $url = new moodle_url('/backup/backup.php', ['id' => $course->id]);
5790
                $coursereusenav->add(get_string('backup'), $url, self::TYPE_SETTING, null, 'backup', new pix_icon('i/backup', ''));
5791
            }
5792
 
5793
            // Restore to this course.
5794
            if ($adminoptions->restore) {
5795
                $url = new moodle_url('/backup/restorefile.php', ['contextid' => $coursecontext->id]);
5796
                $coursereusenav->add(
5797
                    get_string('restore'),
5798
                    $url,
5799
                    self::TYPE_SETTING,
5800
                    null,
5801
                    'restore',
5802
                    new pix_icon('i/restore', ''),
5803
                );
5804
            }
5805
        }
5806
 
5807
        return $frontpage;
5808
    }
5809
 
5810
    /**
5811
     * This function gives local plugins an opportunity to modify the settings navigation.
5812
     */
5813
    protected function load_local_plugin_settings() {
5814
 
5815
        foreach (get_plugin_list_with_function('local', 'extend_settings_navigation') as $function) {
5816
            $function($this, $this->context);
5817
        }
5818
    }
5819
 
5820
    /**
5821
     * This function marks the cache as volatile so it is cleared during shutdown
5822
     */
5823
    public function clear_cache() {
5824
        $this->cache->volatile();
5825
    }
5826
 
5827
    /**
5828
     * Checks to see if there are child nodes available in the specific user's preference node.
5829
     * If so, then they have the appropriate permissions view this user's preferences.
5830
     *
5831
     * @since Moodle 2.9.3
5832
     * @param int $userid The user's ID.
5833
     * @return bool True if child nodes exist to view, otherwise false.
5834
     */
5835
    public function can_view_user_preferences($userid) {
5836
        if (is_siteadmin()) {
5837
            return true;
5838
        }
5839
        // See if any nodes are present in the preferences section for this user.
5840
        $preferencenode = $this->find('userviewingsettings' . $userid, null);
5841
        if ($preferencenode && $preferencenode->has_children()) {
5842
            // Run through each child node.
5843
            foreach ($preferencenode->children as $childnode) {
5844
                // If the child node has children then this user has access to a link in the preferences page.
5845
                if ($childnode->has_children()) {
5846
                    return true;
5847
                }
5848
            }
5849
        }
5850
        // No links found for the user to access on the preferences page.
5851
        return false;
5852
    }
5853
}
5854
 
5855
/**
5856
 * Class used to populate site admin navigation for ajax.
5857
 *
5858
 * @package   core
5859
 * @category  navigation
5860
 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com>
5861
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5862
 */
5863
class settings_navigation_ajax extends settings_navigation {
5864
    /**
5865
     * Constructs the navigation for use in an AJAX request
5866
     *
5867
     * @param moodle_page $page
5868
     */
5869
    public function __construct(moodle_page &$page) {
5870
        $this->page = $page;
5871
        $this->cache = new navigation_cache(NAVIGATION_CACHE_NAME);
5872
        $this->children = new navigation_node_collection();
5873
        $this->initialise();
5874
    }
5875
 
5876
    /**
5877
     * Initialise the site admin navigation.
5878
     */
5879
    public function initialise() {
5880
        if ($this->initialised || during_initial_install()) {
5881
            return false;
5882
        }
5883
        $this->context = $this->page->context;
5884
        $this->load_administration_settings();
5885
 
5886
        // Check if local plugins is adding node to site admin.
5887
        $this->load_local_plugin_settings();
5888
 
5889
        $this->initialised = true;
5890
    }
5891
}
5892
 
5893
/**
5894
 * Simple class used to output a navigation branch in XML
5895
 *
5896
 * @package   core
5897
 * @category  navigation
5898
 * @copyright 2009 Sam Hemelryk
5899
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5900
 */
5901
class navigation_json {
5902
    /** @var array An array of different node types */
5903
    protected $nodetype = array('node','branch');
5904
    /** @var array An array of node keys and types */
5905
    protected $expandable = array();
5906
    /**
5907
     * Turns a branch and all of its children into XML
5908
     *
5909
     * @param navigation_node $branch
5910
     * @return string XML string
5911
     */
5912
    public function convert($branch) {
5913
        $xml = $this->convert_child($branch);
5914
        return $xml;
5915
    }
5916
    /**
5917
     * Set the expandable items in the array so that we have enough information
5918
     * to attach AJAX events
5919
     * @param array $expandable
5920
     */
5921
    public function set_expandable($expandable) {
5922
        foreach ($expandable as $node) {
5923
            $this->expandable[$node['key'].':'.$node['type']] = $node;
5924
        }
5925
    }
5926
    /**
5927
     * Recusively converts a child node and its children to XML for output
5928
     *
5929
     * @param navigation_node $child The child to convert
5930
     * @param int $depth Pointlessly used to track the depth of the XML structure
5931
     * @return string JSON
5932
     */
5933
    protected function convert_child($child, $depth=1) {
5934
        if (!$child->display) {
5935
            return '';
5936
        }
5937
        $attributes = array();
5938
        $attributes['id'] = $child->id;
5939
        $attributes['name'] = (string)$child->text; // This can be lang_string object so typecast it.
5940
        $attributes['type'] = $child->type;
5941
        $attributes['key'] = $child->key;
5942
        $attributes['class'] = $child->get_css_type();
5943
        $attributes['requiresajaxloading'] = $child->requiresajaxloading;
5944
 
5945
        if ($child->icon instanceof pix_icon) {
5946
            $attributes['icon'] = array(
5947
                'component' => $child->icon->component,
5948
                'pix' => $child->icon->pix,
5949
            );
5950
            foreach ($child->icon->attributes as $key=>$value) {
5951
                if ($key == 'class') {
5952
                    $attributes['icon']['classes'] = explode(' ', $value);
5953
                } else if (!array_key_exists($key, $attributes['icon'])) {
5954
                    $attributes['icon'][$key] = $value;
5955
                }
5956
 
5957
            }
5958
        } else if (!empty($child->icon)) {
5959
            $attributes['icon'] = (string)$child->icon;
5960
        }
5961
 
5962
        if ($child->forcetitle || $child->title !== $child->text) {
5963
            $attributes['title'] = htmlentities($child->title ?? '', ENT_QUOTES, 'UTF-8');
5964
        }
5965
        if (array_key_exists($child->key.':'.$child->type, $this->expandable)) {
5966
            $attributes['expandable'] = $child->key;
5967
            $child->add_class($this->expandable[$child->key.':'.$child->type]['id']);
5968
        }
5969
 
5970
        if (count($child->classes)>0) {
5971
            $attributes['class'] .= ' '.join(' ',$child->classes);
5972
        }
5973
        if (is_string($child->action)) {
5974
            $attributes['link'] = $child->action;
5975
        } else if ($child->action instanceof moodle_url) {
5976
            $attributes['link'] = $child->action->out();
5977
        } else if ($child->action instanceof action_link) {
5978
            $attributes['link'] = $child->action->url->out();
5979
        }
5980
        $attributes['hidden'] = ($child->hidden);
5981
        $attributes['haschildren'] = ($child->children->count()>0 || $child->type == navigation_node::TYPE_CATEGORY);
5982
        $attributes['haschildren'] = $attributes['haschildren'] || $child->type == navigation_node::TYPE_MY_CATEGORY;
5983
 
5984
        if ($child->children->count() > 0) {
5985
            $attributes['children'] = array();
5986
            foreach ($child->children as $subchild) {
5987
                $attributes['children'][] = $this->convert_child($subchild, $depth+1);
5988
            }
5989
        }
5990
 
5991
        if ($depth > 1) {
5992
            return $attributes;
5993
        } else {
5994
            return json_encode($attributes);
5995
        }
5996
    }
5997
}
5998
 
5999
/**
6000
 * The cache class used by global navigation and settings navigation.
6001
 *
6002
 * It is basically an easy access point to session with a bit of smarts to make
6003
 * sure that the information that is cached is valid still.
6004
 *
6005
 * Example use:
6006
 * <code php>
6007
 * if (!$cache->viewdiscussion()) {
6008
 *     // Code to do stuff and produce cachable content
6009
 *     $cache->viewdiscussion = has_capability('mod/forum:viewdiscussion', $coursecontext);
6010
 * }
6011
 * $content = $cache->viewdiscussion;
6012
 * </code>
6013
 *
6014
 * @package   core
6015
 * @category  navigation
6016
 * @copyright 2009 Sam Hemelryk
6017
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6018
 */
6019
class navigation_cache {
6020
    /** @var int represents the time created */
6021
    protected $creation;
6022
    /** @var array An array of session keys */
6023
    protected $session;
6024
    /**
6025
     * The string to use to segregate this particular cache. It can either be
6026
     * unique to start a fresh cache or if you want to share a cache then make
6027
     * it the string used in the original cache.
6028
     * @var string
6029
     */
6030
    protected $area;
6031
    /** @var int a time that the information will time out */
6032
    protected $timeout;
6033
    /** @var stdClass The current context */
6034
    protected $currentcontext;
6035
    /** @var int cache time information */
6036
    const CACHETIME = 0;
6037
    /** @var int cache user id */
6038
    const CACHEUSERID = 1;
6039
    /** @var int cache value */
6040
    const CACHEVALUE = 2;
6041
    /** @var null|array An array of navigation cache areas to expire on shutdown */
6042
    public static $volatilecaches;
6043
 
6044
    /**
6045
     * Contructor for the cache. Requires two arguments
6046
     *
6047
     * @param string $area The string to use to segregate this particular cache
6048
     *                it can either be unique to start a fresh cache or if you want
6049
     *                to share a cache then make it the string used in the original
6050
     *                cache
6051
     * @param int $timeout The number of seconds to time the information out after
6052
     */
6053
    public function __construct($area, $timeout=1800) {
6054
        $this->creation = time();
6055
        $this->area = $area;
6056
        $this->timeout = time() - $timeout;
6057
        if (rand(0,100) === 0) {
6058
            $this->garbage_collection();
6059
        }
6060
    }
6061
 
6062
    /**
6063
     * Used to set up the cache within the SESSION.
6064
     *
6065
     * This is called for each access and ensure that we don't put anything into the session before
6066
     * it is required.
6067
     */
6068
    protected function ensure_session_cache_initialised() {
6069
        global $SESSION;
6070
        if (empty($this->session)) {
6071
            if (!isset($SESSION->navcache)) {
6072
                $SESSION->navcache = new stdClass;
6073
            }
6074
            if (!isset($SESSION->navcache->{$this->area})) {
6075
                $SESSION->navcache->{$this->area} = array();
6076
            }
6077
            $this->session = &$SESSION->navcache->{$this->area}; // pointer to array, =& is correct here
6078
        }
6079
    }
6080
 
6081
    /**
6082
     * Magic Method to retrieve something by simply calling using = cache->key
6083
     *
6084
     * @param mixed $key The identifier for the information you want out again
6085
     * @return void|mixed Either void or what ever was put in
6086
     */
6087
    public function __get($key) {
6088
        if (!$this->cached($key)) {
6089
            return;
6090
        }
6091
        $information = $this->session[$key][self::CACHEVALUE];
6092
        return unserialize($information);
6093
    }
6094
 
6095
    /**
6096
     * Magic method that simply uses {@link set();} to store something in the cache
6097
     *
6098
     * @param string|int $key
6099
     * @param mixed $information
6100
     */
6101
    public function __set($key, $information) {
6102
        $this->set($key, $information);
6103
    }
6104
 
6105
    /**
6106
     * Sets some information against the cache (session) for later retrieval
6107
     *
6108
     * @param string|int $key
6109
     * @param mixed $information
6110
     */
6111
    public function set($key, $information) {
6112
        global $USER;
6113
        $this->ensure_session_cache_initialised();
6114
        $information = serialize($information);
6115
        $this->session[$key]= array(self::CACHETIME=>time(), self::CACHEUSERID=>$USER->id, self::CACHEVALUE=>$information);
6116
    }
6117
    /**
6118
     * Check the existence of the identifier in the cache
6119
     *
6120
     * @param string|int $key
6121
     * @return bool
6122
     */
6123
    public function cached($key) {
6124
        global $USER;
6125
        $this->ensure_session_cache_initialised();
6126
        if (!array_key_exists($key, $this->session) || !is_array($this->session[$key]) || $this->session[$key][self::CACHEUSERID]!=$USER->id || $this->session[$key][self::CACHETIME] < $this->timeout) {
6127
            return false;
6128
        }
6129
        return true;
6130
    }
6131
    /**
6132
     * Compare something to it's equivilant in the cache
6133
     *
6134
     * @param string $key
6135
     * @param mixed $value
6136
     * @param bool $serialise Whether to serialise the value before comparison
6137
     *              this should only be set to false if the value is already
6138
     *              serialised
6139
     * @return bool If the value is the same false if it is not set or doesn't match
6140
     */
6141
    public function compare($key, $value, $serialise = true) {
6142
        if ($this->cached($key)) {
6143
            if ($serialise) {
6144
                $value = serialize($value);
6145
            }
6146
            if ($this->session[$key][self::CACHEVALUE] === $value) {
6147
                return true;
6148
            }
6149
        }
6150
        return false;
6151
    }
6152
    /**
6153
     * Wipes the entire cache, good to force regeneration
6154
     */
6155
    public function clear() {
6156
        global $SESSION;
6157
        unset($SESSION->navcache);
6158
        $this->session = null;
6159
    }
6160
    /**
6161
     * Checks all cache entries and removes any that have expired, good ole cleanup
6162
     */
6163
    protected function garbage_collection() {
6164
        if (empty($this->session)) {
6165
            return true;
6166
        }
6167
        foreach ($this->session as $key=>$cachedinfo) {
6168
            if (is_array($cachedinfo) && $cachedinfo[self::CACHETIME]<$this->timeout) {
6169
                unset($this->session[$key]);
6170
            }
6171
        }
6172
    }
6173
 
6174
    /**
6175
     * Marks the cache as being volatile (likely to change)
6176
     *
6177
     * Any caches marked as volatile will be destroyed at the on shutdown by
6178
     * {@link navigation_node::destroy_volatile_caches()} which is registered
6179
     * as a shutdown function if any caches are marked as volatile.
6180
     *
6181
     * @param bool $setting True to destroy the cache false not too
6182
     */
6183
    public function volatile($setting = true) {
6184
        if (self::$volatilecaches===null) {
6185
            self::$volatilecaches = array();
6186
            core_shutdown_manager::register_function(array('navigation_cache','destroy_volatile_caches'));
6187
        }
6188
 
6189
        if ($setting) {
6190
            self::$volatilecaches[$this->area] = $this->area;
6191
        } else if (array_key_exists($this->area, self::$volatilecaches)) {
6192
            unset(self::$volatilecaches[$this->area]);
6193
        }
6194
    }
6195
 
6196
    /**
6197
     * Destroys all caches marked as volatile
6198
     *
6199
     * This function is static and works in conjunction with the static volatilecaches
6200
     * property of navigation cache.
6201
     * Because this function is static it manually resets the cached areas back to an
6202
     * empty array.
6203
     */
6204
    public static function destroy_volatile_caches() {
6205
        global $SESSION;
6206
        if (is_array(self::$volatilecaches) && count(self::$volatilecaches)>0) {
6207
            foreach (self::$volatilecaches as $area) {
6208
                $SESSION->navcache->{$area} = array();
6209
            }
6210
        } else {
6211
            $SESSION->navcache = new stdClass;
6212
        }
6213
    }
6214
}