Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

4747 lines (4372 sloc) 209.701 kb
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains classes used to manage the navigation structures within Moodle.
*
* @since 2.0
* @package core
* @copyright 2009 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* The name that will be used to separate the navigation cache within SESSION
*/
define('NAVIGATION_CACHE_NAME', 'navigation');
/**
* This class is used to represent a node in a navigation tree
*
* This class is used to represent a node in a navigation tree within Moodle,
* the tree could be one of global navigation, settings navigation, or the navbar.
* Each node can be one of two types either a Leaf (default) or a branch.
* When a node is first created it is created as a leaf, when/if children are added
* the node then becomes a branch.
*
* @package core
* @category navigation
* @copyright 2009 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class navigation_node implements renderable {
/** @var int Used to identify this node a leaf (default) 0 */
const NODETYPE_LEAF = 0;
/** @var int Used to identify this node a branch, happens with children 1 */
const NODETYPE_BRANCH = 1;
/** @var null Unknown node type null */
const TYPE_UNKNOWN = null;
/** @var int System node type 0 */
const TYPE_ROOTNODE = 0;
/** @var int System node type 1 */
const TYPE_SYSTEM = 1;
/** @var int Category node type 10 */
const TYPE_CATEGORY = 10;
/** @var int Course node type 20 */
const TYPE_COURSE = 20;
/** @var int Course Structure node type 30 */
const TYPE_SECTION = 30;
/** @var int Activity node type, e.g. Forum, Quiz 40 */
const TYPE_ACTIVITY = 40;
/** @var int Resource node type, e.g. Link to a file, or label 50 */
const TYPE_RESOURCE = 50;
/** @var int A custom node type, default when adding without specifing type 60 */
const TYPE_CUSTOM = 60;
/** @var int Setting node type, used only within settings nav 70 */
const TYPE_SETTING = 70;
/** @var int Setting node type, used only within settings nav 80 */
const TYPE_USER = 80;
/** @var int Setting node type, used for containers of no importance 90 */
const TYPE_CONTAINER = 90;
/** @var int Parameter to aid the coder in tracking [optional] */
public $id = null;
/** @var string|int The identifier for the node, used to retrieve the node */
public $key = null;
/** @var string The text to use for the node */
public $text = null;
/** @var string Short text to use if requested [optional] */
public $shorttext = null;
/** @var string The title attribute for an action if one is defined */
public $title = null;
/** @var string A string that can be used to build a help button */
public $helpbutton = null;
/** @var moodle_url|action_link|null An action for the node (link) */
public $action = null;
/** @var pix_icon The path to an icon to use for this node */
public $icon = null;
/** @var int See TYPE_* constants defined for this class */
public $type = self::TYPE_UNKNOWN;
/** @var int See NODETYPE_* constants defined for this class */
public $nodetype = self::NODETYPE_LEAF;
/** @var bool If set to true the node will be collapsed by default */
public $collapse = false;
/** @var bool If set to true the node will be expanded by default */
public $forceopen = false;
/** @var array An array of CSS classes for the node */
public $classes = array();
/** @var navigation_node_collection An array of child nodes */
public $children = array();
/** @var bool If set to true the node will be recognised as active */
public $isactive = false;
/** @var bool If set to true the node will be dimmed */
public $hidden = false;
/** @var bool If set to false the node will not be displayed */
public $display = true;
/** @var bool If set to true then an HR will be printed before the node */
public $preceedwithhr = false;
/** @var bool If set to true the the navigation bar should ignore this node */
public $mainnavonly = false;
/** @var bool If set to true a title will be added to the action no matter what */
public $forcetitle = false;
/** @var navigation_node A reference to the node parent, you should never set this directly you should always call set_parent */
public $parent = null;
/** @var bool Override to not display the icon even if one is provided **/
public $hideicon = false;
/** @var array */
protected $namedtypes = array(0=>'system',10=>'category',20=>'course',30=>'structure',40=>'activity',50=>'resource',60=>'custom',70=>'setting', 80=>'user');
/** @var moodle_url */
protected static $fullmeurl = null;
/** @var bool toogles auto matching of active node */
public static $autofindactive = true;
/** @var mixed If set to an int, that section will be included even if it has no activities */
public $includesectionnum = false;
/**
* Constructs a new navigation_node
*
* @param array|string $properties Either an array of properties or a string to use
* as the text for the node
*/
public function __construct($properties) {
if (is_array($properties)) {
// Check the array for each property that we allow to set at construction.
// text - The main content for the node
// shorttext - A short text if required for the node
// icon - The icon to display for the node
// type - The type of the node
// key - The key to use to identify the node
// parent - A reference to the nodes parent
// action - The action to attribute to this node, usually a URL to link to
if (array_key_exists('text', $properties)) {
$this->text = $properties['text'];
}
if (array_key_exists('shorttext', $properties)) {
$this->shorttext = $properties['shorttext'];
}
if (!array_key_exists('icon', $properties)) {
$properties['icon'] = new pix_icon('i/navigationitem', '');
}
$this->icon = $properties['icon'];
if ($this->icon instanceof pix_icon) {
if (empty($this->icon->attributes['class'])) {
$this->icon->attributes['class'] = 'navicon';
} else {
$this->icon->attributes['class'] .= ' navicon';
}
}
if (array_key_exists('type', $properties)) {
$this->type = $properties['type'];
} else {
$this->type = self::TYPE_CUSTOM;
}
if (array_key_exists('key', $properties)) {
$this->key = $properties['key'];
}
// This needs to happen last because of the check_if_active call that occurs
if (array_key_exists('action', $properties)) {
$this->action = $properties['action'];
if (is_string($this->action)) {
$this->action = new moodle_url($this->action);
}
if (self::$autofindactive) {
$this->check_if_active();
}
}
if (array_key_exists('parent', $properties)) {
$this->set_parent($properties['parent']);
}
} else if (is_string($properties)) {
$this->text = $properties;
}
if ($this->text === null) {
throw new coding_exception('You must set the text for the node when you create it.');
}
// Default the title to the text
$this->title = $this->text;
// Instantiate a new navigation node collection for this nodes children
$this->children = new navigation_node_collection();
}
/**
* Checks if this node is the active node.
*
* This is determined by comparing the action for the node against the
* defined URL for the page. A match will see this node marked as active.
*
* @param int $strength One of URL_MATCH_EXACT, URL_MATCH_PARAMS, or URL_MATCH_BASE
* @return bool
*/
public function check_if_active($strength=URL_MATCH_EXACT) {
global $FULLME, $PAGE;
// Set fullmeurl if it hasn't already been set
if (self::$fullmeurl == null) {
if ($PAGE->has_set_url()) {
self::override_active_url(new moodle_url($PAGE->url));
} else {
self::override_active_url(new moodle_url($FULLME));
}
}
// Compare the action of this node against the fullmeurl
if ($this->action instanceof moodle_url && $this->action->compare(self::$fullmeurl, $strength)) {
$this->make_active();
return true;
}
return false;
}
/**
* This sets the URL that the URL of new nodes get compared to when locating
* the active node.
*
* The active node is the node that matches the URL set here. By default this
* is either $PAGE->url or if that hasn't been set $FULLME.
*
* @param moodle_url $url The url to use for the fullmeurl.
*/
public static function override_active_url(moodle_url $url) {
// Clone the URL, in case the calling script changes their URL later.
self::$fullmeurl = new moodle_url($url);
}
/**
* Creates a navigation node, ready to add it as a child using add_node
* function. (The created node needs to be added before you can use it.)
* @param string $text
* @param moodle_url|action_link $action
* @param int $type
* @param string $shorttext
* @param string|int $key
* @param pix_icon $icon
* @return navigation_node
*/
public static function create($text, $action=null, $type=self::TYPE_CUSTOM,
$shorttext=null, $key=null, pix_icon $icon=null) {
// Properties array used when creating the new navigation node
$itemarray = array(
'text' => $text,
'type' => $type
);
// Set the action if one was provided
if ($action!==null) {
$itemarray['action'] = $action;
}
// Set the shorttext if one was provided
if ($shorttext!==null) {
$itemarray['shorttext'] = $shorttext;
}
// Set the icon if one was provided
if ($icon!==null) {
$itemarray['icon'] = $icon;
}
// Set the key
$itemarray['key'] = $key;
// Construct and return
return new navigation_node($itemarray);
}
/**
* Adds a navigation node as a child of this node.
*
* @param string $text
* @param moodle_url|action_link $action
* @param int $type
* @param string $shorttext
* @param string|int $key
* @param pix_icon $icon
* @return navigation_node
*/
public function add($text, $action=null, $type=self::TYPE_CUSTOM, $shorttext=null, $key=null, pix_icon $icon=null) {
// Create child node
$childnode = self::create($text, $action, $type, $shorttext, $key, $icon);
// Add the child to end and return
return $this->add_node($childnode);
}
/**
* Adds a navigation node as a child of this one, given a $node object
* created using the create function.
* @param navigation_node $childnode Node to add
* @param string $beforekey
* @return navigation_node The added node
*/
public function add_node(navigation_node $childnode, $beforekey=null) {
// First convert the nodetype for this node to a branch as it will now have children
if ($this->nodetype !== self::NODETYPE_BRANCH) {
$this->nodetype = self::NODETYPE_BRANCH;
}
// Set the parent to this node
$childnode->set_parent($this);
// Default the key to the number of children if not provided
if ($childnode->key === null) {
$childnode->key = $this->children->count();
}
// Add the child using the navigation_node_collections add method
$node = $this->children->add($childnode, $beforekey);
// If added node is a category node or the user is logged in and it's a course
// then mark added node as a branch (makes it expandable by AJAX)
$type = $childnode->type;
if (($type==self::TYPE_CATEGORY) || (isloggedin() && $type==self::TYPE_COURSE)) {
$node->nodetype = self::NODETYPE_BRANCH;
}
// If this node is hidden mark it's children as hidden also
if ($this->hidden) {
$node->hidden = true;
}
// Return added node (reference returned by $this->children->add()
return $node;
}
/**
* Return a list of all the keys of all the child nodes.
* @return array the keys.
*/
public function get_children_key_list() {
return $this->children->get_key_list();
}
/**
* Searches for a node of the given type with the given key.
*
* This searches this node plus all of its children, and their children....
* If you know the node you are looking for is a child of this node then please
* use the get method instead.
*
* @param int|string $key The key of the node we are looking for
* @param int $type One of navigation_node::TYPE_*
* @return navigation_node|false
*/
public function find($key, $type) {
return $this->children->find($key, $type);
}
/**
* Get the child of this node that has the given key + (optional) type.
*
* If you are looking for a node and want to search all children + thier children
* then please use the find method instead.
*
* @param int|string $key The key of the node we are looking for
* @param int $type One of navigation_node::TYPE_*
* @return navigation_node|false
*/
public function get($key, $type=null) {
return $this->children->get($key, $type);
}
/**
* Removes this node.
*
* @return bool
*/
public function remove() {
return $this->parent->children->remove($this->key, $this->type);
}
/**
* Checks if this node has or could have any children
*
* @return bool Returns true if it has children or could have (by AJAX expansion)
*/
public function has_children() {
return ($this->nodetype === navigation_node::NODETYPE_BRANCH || $this->children->count()>0);
}
/**
* Marks this node as active and forces it open.
*
* Important: If you are here because you need to mark a node active to get
* the navigation to do what you want have you looked at {@link navigation_node::override_active_url()}?
* You can use it to specify a different URL to match the active navigation node on
* rather than having to locate and manually mark a node active.
*/
public function make_active() {
$this->isactive = true;
$this->add_class('active_tree_node');
$this->force_open();
if ($this->parent !== null) {
$this->parent->make_inactive();
}
}
/**
* Marks a node as inactive and recusised back to the base of the tree
* doing the same to all parents.
*/
public function make_inactive() {
$this->isactive = false;
$this->remove_class('active_tree_node');
if ($this->parent !== null) {
$this->parent->make_inactive();
}
}
/**
* Forces this node to be open and at the same time forces open all
* parents until the root node.
*
* Recursive.
*/
public function force_open() {
$this->forceopen = true;
if ($this->parent !== null) {
$this->parent->force_open();
}
}
/**
* Adds a CSS class to this node.
*
* @param string $class
* @return bool
*/
public function add_class($class) {
if (!in_array($class, $this->classes)) {
$this->classes[] = $class;
}
return true;
}
/**
* Removes a CSS class from this node.
*
* @param string $class
* @return bool True if the class was successfully removed.
*/
public function remove_class($class) {
if (in_array($class, $this->classes)) {
$key = array_search($class,$this->classes);
if ($key!==false) {
unset($this->classes[$key]);
return true;
}
}
return false;
}
/**
* Sets the title for this node and forces Moodle to utilise it.
* @param string $title
*/
public function title($title) {
$this->title = $title;
$this->forcetitle = true;
}
/**
* Resets the page specific information on this node if it is being unserialised.
*/
public function __wakeup(){
$this->forceopen = false;
$this->isactive = false;
$this->remove_class('active_tree_node');
}
/**
* Checks if this node or any of its children contain the active node.
*
* Recursive.
*
* @return bool
*/
public function contains_active_node() {
if ($this->isactive) {
return true;
} else {
foreach ($this->children as $child) {
if ($child->isactive || $child->contains_active_node()) {
return true;
}
}
}
return false;
}
/**
* Finds the active node.
*
* Searches this nodes children plus all of the children for the active node
* and returns it if found.
*
* Recursive.
*
* @return navigation_node|false
*/
public function find_active_node() {
if ($this->isactive) {
return $this;
} else {
foreach ($this->children as &$child) {
$outcome = $child->find_active_node();
if ($outcome !== false) {
return $outcome;
}
}
}
return false;
}
/**
* Searches all children for the best matching active node
* @return navigation_node|false
*/
public function search_for_active_node() {
if ($this->check_if_active(URL_MATCH_BASE)) {
return $this;
} else {
foreach ($this->children as &$child) {
$outcome = $child->search_for_active_node();
if ($outcome !== false) {
return $outcome;
}
}
}
return false;
}
/**
* Gets the content for this node.
*
* @param bool $shorttext If true shorttext is used rather than the normal text
* @return string
*/
public function get_content($shorttext=false) {
if ($shorttext && $this->shorttext!==null) {
return format_string($this->shorttext);
} else {
return format_string($this->text);
}
}
/**
* Gets the title to use for this node.
*
* @return string
*/
public function get_title() {
if ($this->forcetitle || $this->action != null){
return $this->title;
} else {
return '';
}
}
/**
* Gets the CSS class to add to this node to describe its type
*
* @return string
*/
public function get_css_type() {
if (array_key_exists($this->type, $this->namedtypes)) {
return 'type_'.$this->namedtypes[$this->type];
}
return 'type_unknown';
}
/**
* Finds all nodes that are expandable by AJAX
*
* @param array $expandable An array by reference to populate with expandable nodes.
*/
public function find_expandable(array &$expandable) {
foreach ($this->children as &$child) {
if ($child->nodetype == self::NODETYPE_BRANCH && $child->children->count() == 0 && $child->display) {
$child->id = 'expandable_branch_'.(count($expandable)+1);
$this->add_class('canexpand');
$expandable[] = array('id' => $child->id, 'key' => $child->key, 'type' => $child->type);
}
$child->find_expandable($expandable);
}
}
/**
* Finds all nodes of a given type (recursive)
*
* @param int $type One of navigation_node::TYPE_*
* @return array
*/
public function find_all_of_type($type) {
$nodes = $this->children->type($type);
foreach ($this->children as &$node) {
$childnodes = $node->find_all_of_type($type);
$nodes = array_merge($nodes, $childnodes);
}
return $nodes;
}
/**
* Removes this node if it is empty
*/
public function trim_if_empty() {
if ($this->children->count() == 0) {
$this->remove();
}
}
/**
* Creates a tab representation of this nodes children that can be used
* with print_tabs to produce the tabs on a page.
*
* call_user_func_array('print_tabs', $node->get_tabs_array());
*
* @param array $inactive
* @param bool $return
* @return array Array (tabs, selected, inactive, activated, return)
*/
public function get_tabs_array(array $inactive=array(), $return=false) {
$tabs = array();
$rows = array();
$selected = null;
$activated = array();
foreach ($this->children as $node) {
$tabs[] = new tabobject($node->key, $node->action, $node->get_content(), $node->get_title());
if ($node->contains_active_node()) {
if ($node->children->count() > 0) {
$activated[] = $node->key;
foreach ($node->children as $child) {
if ($child->contains_active_node()) {
$selected = $child->key;
}
$rows[] = new tabobject($child->key, $child->action, $child->get_content(), $child->get_title());
}
} else {
$selected = $node->key;
}
}
}
return array(array($tabs, $rows), $selected, $inactive, $activated, $return);
}
/**
* Sets the parent for this node and if this node is active ensures that the tree is properly
* adjusted as well.
*
* @param navigation_node $parent
*/
public function set_parent(navigation_node $parent) {
// Set the parent (thats the easy part)
$this->parent = $parent;
// Check if this node is active (this is checked during construction)
if ($this->isactive) {
// Force all of the parent nodes open so you can see this node
$this->parent->force_open();
// Make all parents inactive so that its clear where we are.
$this->parent->make_inactive();
}
}
}
/**
* Navigation node collection
*
* This class is responsible for managing a collection of navigation nodes.
* It is required because a node's unique identifier is a combination of both its
* key and its type.
*
* Originally an array was used with a string key that was a combination of the two
* however it was decided that a better solution would be to use a class that
* implements the standard IteratorAggregate interface.
*
* @package core
* @category navigation
* @copyright 2010 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class navigation_node_collection implements IteratorAggregate {
/**
* A multidimensional array to where the first key is the type and the second
* key is the nodes key.
* @var array
*/
protected $collection = array();
/**
* An array that contains references to nodes in the same order they were added.
* This is maintained as a progressive array.
* @var array
*/
protected $orderedcollection = array();
/**
* A reference to the last node that was added to the collection
* @var navigation_node
*/
protected $last = null;
/**
* The total number of items added to this array.
* @var int
*/
protected $count = 0;
/**
* Adds a navigation node to the collection
*
* @param navigation_node $node Node to add
* @param string $beforekey If specified, adds before a node with this key,
* otherwise adds at end
* @return navigation_node Added node
*/
public function add(navigation_node $node, $beforekey=null) {
global $CFG;
$key = $node->key;
$type = $node->type;
// First check we have a 2nd dimension for this type
if (!array_key_exists($type, $this->orderedcollection)) {
$this->orderedcollection[$type] = array();
}
// Check for a collision and report if debugging is turned on
if ($CFG->debug && array_key_exists($key, $this->orderedcollection[$type])) {
debugging('Navigation node intersect: Adding a node that already exists '.$key, DEBUG_DEVELOPER);
}
// Find the key to add before
$newindex = $this->count;
$last = true;
if ($beforekey !== null) {
foreach ($this->collection as $index => $othernode) {
if ($othernode->key === $beforekey) {
$newindex = $index;
$last = false;
break;
}
}
if ($newindex === $this->count) {
debugging('Navigation node add_before: Reference node not found ' . $beforekey .
', options: ' . implode(' ', $this->get_key_list()), DEBUG_DEVELOPER);
}
}
// Add the node to the appropriate place in the by-type structure (which
// is not ordered, despite the variable name)
$this->orderedcollection[$type][$key] = $node;
if (!$last) {
// Update existing references in the ordered collection (which is the
// one that isn't called 'ordered') to shuffle them along if required
for ($oldindex = $this->count; $oldindex > $newindex; $oldindex--) {
$this->collection[$oldindex] = $this->collection[$oldindex - 1];
}
}
// Add a reference to the node to the progressive collection.
$this->collection[$newindex] = $this->orderedcollection[$type][$key];
// Update the last property to a reference to this new node.
$this->last = $this->orderedcollection[$type][$key];
// Reorder the array by index if needed
if (!$last) {
ksort($this->collection);
}
$this->count++;
// Return the reference to the now added node
return $node;
}
/**
* Return a list of all the keys of all the nodes.
* @return array the keys.
*/
public function get_key_list() {
$keys = array();
foreach ($this->collection as $node) {
$keys[] = $node->key;
}
return $keys;
}
/**
* Fetches a node from this collection.
*
* @param string|int $key The key of the node we want to find.
* @param int $type One of navigation_node::TYPE_*.
* @return navigation_node|null
*/
public function get($key, $type=null) {
if ($type !== null) {
// If the type is known then we can simply check and fetch
if (!empty($this->orderedcollection[$type][$key])) {
return $this->orderedcollection[$type][$key];
}
} else {
// Because we don't know the type we look in the progressive array
foreach ($this->collection as $node) {
if ($node->key === $key) {
return $node;
}
}
}
return false;
}
/**
* Searches for a node with matching key and type.
*
* This function searches both the nodes in this collection and all of
* the nodes in each collection belonging to the nodes in this collection.
*
* Recursive.
*
* @param string|int $key The key of the node we want to find.
* @param int $type One of navigation_node::TYPE_*.
* @return navigation_node|null
*/
public function find($key, $type=null) {
if ($type !== null && array_key_exists($type, $this->orderedcollection) && array_key_exists($key, $this->orderedcollection[$type])) {
return $this->orderedcollection[$type][$key];
} else {
$nodes = $this->getIterator();
// Search immediate children first
foreach ($nodes as &$node) {
if ($node->key === $key && ($type === null || $type === $node->type)) {
return $node;
}
}
// Now search each childs children
foreach ($nodes as &$node) {
$result = $node->children->find($key, $type);
if ($result !== false) {
return $result;
}
}
}
return false;
}
/**
* Fetches the last node that was added to this collection
*
* @return navigation_node
*/
public function last() {
return $this->last;
}
/**
* Fetches all nodes of a given type from this collection
*
* @param string|int $type node type being searched for.
* @return array ordered collection
*/
public function type($type) {
if (!array_key_exists($type, $this->orderedcollection)) {
$this->orderedcollection[$type] = array();
}
return $this->orderedcollection[$type];
}
/**
* Removes the node with the given key and type from the collection
*
* @param string|int $key The key of the node we want to find.
* @param int $type
* @return bool
*/
public function remove($key, $type=null) {
$child = $this->get($key, $type);
if ($child !== false) {
foreach ($this->collection as $colkey => $node) {
if ($node->key == $key && $node->type == $type) {
unset($this->collection[$colkey]);
break;
}
}
unset($this->orderedcollection[$child->type][$child->key]);
$this->count--;
return true;
}
return false;
}
/**
* Gets the number of nodes in this collection
*
* This option uses an internal count rather than counting the actual options to avoid
* a performance hit through the count function.
*
* @return int
*/
public function count() {
return $this->count;
}
/**
* Gets an array iterator for the collection.
*
* This is required by the IteratorAggregator interface and is used by routines
* such as the foreach loop.
*
* @return ArrayIterator
*/
public function getIterator() {
return new ArrayIterator($this->collection);
}
}
/**
* The global navigation class used for... the global navigation
*
* This class is used by PAGE to store the global navigation for the site
* and is then used by the settings nav and navbar to save on processing and DB calls
*
* See
* {@link lib/pagelib.php} {@link moodle_page::initialise_theme_and_output()}
* {@link lib/ajax/getnavbranch.php} Called by ajax
*
* @package core
* @category navigation
* @copyright 2009 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class global_navigation extends navigation_node {
/** @var moodle_page The Moodle page this navigation object belongs to. */
protected $page;
/** @var bool switch to let us know if the navigation object is initialised*/
protected $initialised = false;
/** @var array An array of course information */
protected $mycourses = array();
/** @var array An array for containing root navigation nodes */
protected $rootnodes = array();
/** @var bool A switch for whether to show empty sections in the navigation */
protected $showemptysections = true;
/** @var bool A switch for whether courses should be shown within categories on the navigation. */
protected $showcategories = null;
/** @var array An array of stdClasses for users that the navigation is extended for */
protected $extendforuser = array();
/** @var navigation_cache */
protected $cache;
/** @var array An array of course ids that are present in the navigation */
protected $addedcourses = array();
/** @var bool */
protected $allcategoriesloaded = false;
/** @var array An array of category ids that are included in the navigation */
protected $addedcategories = array();
/** @var int expansion limit */
protected $expansionlimit = 0;
/** @var int userid to allow parent to see child's profile page navigation */
protected $useridtouseforparentchecks = 0;
/** Used when loading categories to load all top level categories [parent = 0] **/
const LOAD_ROOT_CATEGORIES = 0;
/** Used when loading categories to load all categories **/
const LOAD_ALL_CATEGORIES = -1;
/**
* Constructs a new global navigation
*
* @param moodle_page $page The page this navigation object belongs to
*/
public function __construct(moodle_page $page) {
global $CFG, $SITE, $USER;
if (during_initial_install()) {
return;
}
if (get_home_page() == HOMEPAGE_SITE) {
// We are using the site home for the root element
$properties = array(
'key' => 'home',
'type' => navigation_node::TYPE_SYSTEM,
'text' => get_string('home'),
'action' => new moodle_url('/')
);
} else {
// We are using the users my moodle for the root element
$properties = array(
'key' => 'myhome',
'type' => navigation_node::TYPE_SYSTEM,
'text' => get_string('myhome'),
'action' => new moodle_url('/my/')
);
}
// Use the parents constructor.... good good reuse
parent::__construct($properties);
// Initalise and set defaults
$this->page = $page;
$this->forceopen = true;
$this->cache = new navigation_cache(NAVIGATION_CACHE_NAME);
}
/**
* Mutator to set userid to allow parent to see child's profile
* page navigation. See MDL-25805 for initial issue. Linked to it
* is an issue explaining why this is a REALLY UGLY HACK thats not
* for you to use!
*
* @param int $userid userid of profile page that parent wants to navigate around.
*/
public function set_userid_for_parent_checks($userid) {
$this->useridtouseforparentchecks = $userid;
}
/**
* Initialises the navigation object.
*
* This causes the navigation object to look at the current state of the page
* that it is associated with and then load the appropriate content.
*
* This should only occur the first time that the navigation structure is utilised
* which will normally be either when the navbar is called to be displayed or
* when a block makes use of it.
*
* @return bool
*/
public function initialise() {
global $CFG, $SITE, $USER, $DB;
// Check if it has alread been initialised
if ($this->initialised || during_initial_install()) {
return true;
}
$this->initialised = true;
// Set up the five base root nodes. These are nodes where we will put our
// content and are as follows:
// site: Navigation for the front page.
// myprofile: User profile information goes here.
// mycourses: The users courses get added here.
// courses: Additional courses are added here.
// users: Other users information loaded here.
$this->rootnodes = array();
if (get_home_page() == HOMEPAGE_SITE) {
// The home element should be my moodle because the root element is the site
if (isloggedin() && !isguestuser()) { // Makes no sense if you aren't logged in
$this->rootnodes['home'] = $this->add(get_string('myhome'), new moodle_url('/my/'), self::TYPE_SETTING, null, 'home');
}
} else {
// The home element should be the site because the root node is my moodle
$this->rootnodes['home'] = $this->add(get_string('sitehome'), new moodle_url('/'), self::TYPE_SETTING, null, 'home');
if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY)) {
// We need to stop automatic redirection
$this->rootnodes['home']->action->param('redirect', '0');
}
}
$this->rootnodes['site'] = $this->add_course($SITE);
$this->rootnodes['myprofile'] = $this->add(get_string('myprofile'), null, self::TYPE_USER, null, 'myprofile');
$this->rootnodes['mycourses'] = $this->add(get_string('mycourses'), null, self::TYPE_ROOTNODE, null, 'mycourses');
$this->rootnodes['courses'] = $this->add(get_string('courses'), new moodle_url('/course/index.php'), self::TYPE_ROOTNODE, null, 'courses');
$this->rootnodes['users'] = $this->add(get_string('users'), null, self::TYPE_ROOTNODE, null, 'users');
// We always load the frontpage course to ensure it is available without
// JavaScript enabled.
$this->add_front_page_course_essentials($this->rootnodes['site'], $SITE);
$this->load_course_sections($SITE, $this->rootnodes['site']);
// Fetch all of the users courses.
$mycourses = enrol_get_my_courses();
// We need to show categories if we can show categories and the user isn't enrolled in any courses or we're not showing all courses
$showcategories = ($this->show_categories() && (count($mycourses) == 0 || !empty($CFG->navshowallcourses)));
// $issite gets set to true if the current pages course is the sites frontpage course
$issite = ($this->page->course->id == $SITE->id);
// $ismycourse gets set to true if the user is enrolled in the current pages course.
$ismycourse = !$issite && (array_key_exists($this->page->course->id, $mycourses));
// Check if any courses were returned.
if (count($mycourses) > 0) {
// Check if categories should be displayed within the my courses branch
if (!empty($CFG->navshowmycoursecategories)) {
// Find the category of each mycourse
$categories = array();
foreach ($mycourses as $course) {
$categories[] = $course->category;
}
// Do a single DB query to get the categories immediately associated with
// courses the user is enrolled in.
$categories = $DB->get_records_list('course_categories', 'id', array_unique($categories), 'depth ASC, sortorder ASC');
// Work out the parent categories that we need to load that we havn't
// already got.
$categoryids = array();
foreach ($categories as $category) {
$categoryids = array_merge($categoryids, explode('/', trim($category->path, '/')));
}
$categoryids = array_unique($categoryids);
$categoryids = array_diff($categoryids, array_keys($categories));
if (count($categoryids)) {
// Fetch any other categories we need.
$allcategories = $DB->get_records_list('course_categories', 'id', $categoryids, 'depth ASC, sortorder ASC');
if (is_array($allcategories) && count($allcategories) > 0) {
$categories = array_merge($categories, $allcategories);
}
}
// We ONLY want the categories, we need to get rid of the keys
$categories = array_values($categories);
$addedcategories = array();
while (($category = array_shift($categories)) !== null) {
if ($category->parent == '0') {
$categoryparent = $this->rootnodes['mycourses'];
} else if (array_key_exists($category->parent, $addedcategories)) {
$categoryparent = $addedcategories[$category->parent];
} else {
// Prepare to count iterations. We don't want to loop forever
// accidentally if for some reason a category can't be placed.
if (!isset($category->loopcount)) {
$category->loopcount = 0;
}
$category->loopcount++;
if ($category->loopcount > 5) {
// This is a pretty serious problem and this should never happen.
// If it does then for some reason a category has been loaded but
// its parents have now. It could be data corruption.
debugging('Category '.$category->id.' could not be placed within the navigation', DEBUG_DEVELOPER);
} else {
// Add it back to the end of the categories array
array_push($categories, $category);
}
continue;
}
$url = new moodle_url('/course/category.php', array('id' => $category->id));
$addedcategories[$category->id] = $categoryparent->add($category->name, $url, self::TYPE_CATEGORY, $category->name, $category->id);
if (!$category->visible) {
// Let's decide the context where viewhidden cap checks will happen.
if ($category->parent == '0') {
$contexttocheck = context_system::instance();
} else {
$contexttocheck = context_coursecat::instance($category->parent);
}
if (!has_capability('moodle/category:viewhiddencategories', $contexttocheck)) {
$addedcategories[$category->id]->display = false;
} else {
$addedcategories[$category->id]->hidden = true;
}
}
}
}
// Add all of the users courses to the navigation.
// First up we need to add to the mycourses section.
foreach ($mycourses as $course) {
$course->coursenode = $this->add_course($course, false, true);
}
if (!empty($CFG->navshowallcourses)) {
// Load all courses
$this->load_all_courses();
}
// Next if nasvshowallcourses is enabled then we need to add courses
// to the courses branch as well.
if (!empty($CFG->navshowallcourses)) {
foreach ($mycourses as $course) {
if (!empty($course->category) && !$this->can_add_more_courses_to_category($course->category)) {
continue;
}
$genericcoursenode = $this->add_course($course, true);
if ($genericcoursenode->isactive) {
// We don't want this node to be active because we want the
// node in the mycourses branch to be active.
$genericcoursenode->make_inactive();
$genericcoursenode->collapse = true;
if ($genericcoursenode->parent && $genericcoursenode->parent->type == self::TYPE_CATEGORY) {
$parent = $genericcoursenode->parent;
while ($parent && $parent->type == self::TYPE_CATEGORY) {
$parent->collapse = true;
$parent = $parent->parent;
}
}
}
}
}
} else if (!empty($CFG->navshowallcourses) || !$this->show_categories()) {
// Load all courses
$this->load_all_courses();
}
$canviewcourseprofile = true;
// Next load context specific content into the navigation
switch ($this->page->context->contextlevel) {
case CONTEXT_SYSTEM :
// This has already been loaded we just need to map the variable
if ($showcategories) {
$this->load_all_categories(self::LOAD_ROOT_CATEGORIES, true);
}
break;
case CONTEXT_COURSECAT :
// This has already been loaded we just need to map the variable
if ($showcategories) {
$this->load_all_categories($this->page->context->instanceid, true);
}
break;
case CONTEXT_BLOCK :
case CONTEXT_COURSE :
if ($issite) {
// If it is the front page course, or a block on it then
// all we need to do is load the root categories if required
if ($showcategories) {
$this->load_all_categories(self::LOAD_ROOT_CATEGORIES, true);
}
break;
}
// Load the course associated with the page into the navigation
$course = $this->page->course;
if ($this->show_categories() && !$ismycourse) {
// The user isn't enrolled in the course and we need to show categories in which case we need
// to load the category relating to the course and depending up $showcategories all of the root categories as well.
$this->load_all_categories($course->category, $showcategories);
}
$coursenode = $this->load_course($course);
// If the course wasn't added then don't try going any further.
if (!$coursenode) {
$canviewcourseprofile = false;
break;
}
// If the user is not enrolled then we only want to show the
// course node and not populate it.
// Not enrolled, can't view, and hasn't switched roles
if (!can_access_course($course)) {
// TODO: very ugly hack - do not force "parents" to enrol into course their child is enrolled in,
// this hack has been propagated from user/view.php to display the navigation node. (MDL-25805)
$isparent = false;
if ($this->useridtouseforparentchecks) {
if ($this->useridtouseforparentchecks != $USER->id) {
$usercontext = get_context_instance(CONTEXT_USER, $this->useridtouseforparentchecks, MUST_EXIST);
if ($DB->record_exists('role_assignments', array('userid' => $USER->id, 'contextid' => $usercontext->id))
and has_capability('moodle/user:viewdetails', $usercontext)) {
$isparent = true;
}
}
}
if (!$isparent) {
$coursenode->make_active();
$canviewcourseprofile = false;
break;
}
}
// Add the essentials such as reports etc...
$this->add_course_essentials($coursenode, $course);
if ($this->format_display_course_content($course->format)) {
// Load the course sections
$sections = $this->load_course_sections($course, $coursenode);
}
if (!$coursenode->contains_active_node() && !$coursenode->search_for_active_node()) {
$coursenode->make_active();
}
break;
case CONTEXT_MODULE :
if ($issite) {
// If this is the site course then most information will have
// already been loaded.
// However we need to check if there is more content that can
// yet be loaded for the specific module instance.
$activitynode = $this->rootnodes['site']->get($this->page->cm->id, navigation_node::TYPE_ACTIVITY);
if ($activitynode) {
$this->load_activity($this->page->cm, $this->page->course, $activitynode);
}
break;
}
$course = $this->page->course;
$cm = $this->page->cm;
if ($this->show_categories() && !$ismycourse) {
$this->load_all_categories($course->category, $showcategories);
}
// Load the course associated with the page into the navigation
$coursenode = $this->load_course($course);
// If the course wasn't added then don't try going any further.
if (!$coursenode) {
$canviewcourseprofile = false;
break;
}
// If the user is not enrolled then we only want to show the
// course node and not populate it.
if (!can_access_course($course)) {
$coursenode->make_active();
$canviewcourseprofile = false;
break;
}
$this->add_course_essentials($coursenode, $course);
// Get section number from $cm (if provided) - we need this
// before loading sections in order to tell it to load this section
// even if it would not normally display (=> it contains only
// a label, which we are now editing)
$sectionnum = isset($cm->sectionnum) ? $cm->sectionnum : 0;
if ($sectionnum) {
// This value has to be stored in a member variable because
// otherwise we would have to pass it through a public API
// to course formats and they would need to change their
// functions to pass it along again...
$this->includesectionnum = $sectionnum;
} else {
$this->includesectionnum = false;
}
// Load the course sections into the page
$sections = $this->load_course_sections($course, $coursenode);
if ($course->id != $SITE->id) {
// Find the section for the $CM associated with the page and collect
// its section number.
if ($sectionnum) {
$cm->sectionnumber = $sectionnum;
} else {
foreach ($sections as $section) {
if ($section->id == $cm->section) {
$cm->sectionnumber = $section->section;
break;
}
}
}
// Load all of the section activities for the section the cm belongs to.
if (isset($cm->sectionnumber) and !empty($sections[$cm->sectionnumber])) {
list($sectionarray, $activityarray) = $this->generate_sections_and_activities($course);
$activities = $this->load_section_activities($sections[$cm->sectionnumber]->sectionnode, $cm->sectionnumber, $activityarray);
} else {
$activities = array();
if ($activity = $this->load_stealth_activity($coursenode, get_fast_modinfo($course))) {
// "stealth" activity from unavailable section
$activities[$cm->id] = $activity;
}
}
} else {
$activities = array();
$activities[$cm->id] = $coursenode->get($cm->id, navigation_node::TYPE_ACTIVITY);
}
if (!empty($activities[$cm->id])) {
// Finally load the cm specific navigaton information
$this->load_activity($cm, $course, $activities[$cm->id]);
// Check if we have an active ndoe
if (!$activities[$cm->id]->contains_active_node() && !$activities[$cm->id]->search_for_active_node()) {
// And make the activity node active.
$activities[$cm->id]->make_active();
}
} else {
//TODO: something is wrong, what to do? (Skodak)
}
break;
case CONTEXT_USER :
if ($issite) {
// The users profile information etc is already loaded
// for the front page.
break;
}
$course = $this->page->course;
if ($this->show_categories() && !$ismycourse) {
$this->load_all_categories($course->category, $showcategories);
}
// Load the course associated with the user into the navigation
$coursenode = $this->load_course($course);
// If the course wasn't added then don't try going any further.
if (!$coursenode) {
$canviewcourseprofile = false;
break;
}
// If the user is not enrolled then we only want to show the
// course node and not populate it.
if (!can_access_course($course)) {
$coursenode->make_active();
$canviewcourseprofile = false;
break;
}
$this->add_course_essentials($coursenode, $course);
$sections = $this->load_course_sections($course, $coursenode);
break;
}
// Look for all categories which have been loaded
if ($showcategories) {
$categories = $this->find_all_of_type(self::TYPE_CATEGORY);
if (count($categories) !== 0) {
$categoryids = array();
foreach ($categories as $category) {
$categoryids[] = $category->key;
}
list($categoriessql, $params) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED);
$params['limit'] = (!empty($CFG->navcourselimit))?$CFG->navcourselimit:20;
$sql = "SELECT cc.id, COUNT(c.id) AS coursecount
FROM {course_categories} cc
JOIN {course} c ON c.category = cc.id
WHERE cc.id {$categoriessql}
GROUP BY cc.id
HAVING COUNT(c.id) > :limit";
$excessivecategories = $DB->get_records_sql($sql, $params);
foreach ($categories as &$category) {
if (array_key_exists($category->key, $excessivecategories) && !$this->can_add_more_courses_to_category($category)) {
$url = new moodle_url('/course/category.php', array('id'=>$category->key));
$category->add(get_string('viewallcourses'), $url, self::TYPE_SETTING);
}
}
}
} else if ((!empty($CFG->navshowallcourses) || empty($mycourses)) && !$this->can_add_more_courses_to_category($this->rootnodes['courses'])) {
$this->rootnodes['courses']->add(get_string('viewallcoursescategories'), new moodle_url('/course/index.php'), self::TYPE_SETTING);
}
// Load for the current user
$this->load_for_user();
if ($this->page->context->contextlevel >= CONTEXT_COURSE && $this->page->context->instanceid != $SITE->id && $canviewcourseprofile) {
$this->load_for_user(null, true);
}
// Load each extending user into the navigation.
foreach ($this->extendforuser as $user) {
if ($user->id != $USER->id) {
$this->load_for_user($user);
}
}
// Give the local plugins a chance to include some navigation if they want.
foreach (get_plugin_list_with_file('local', 'lib.php', true) as $plugin => $file) {
$function = "local_{$plugin}_extends_navigation";
$oldfunction = "{$plugin}_extends_navigation";
if (function_exists($function)) {
// This is the preferred function name as there is less chance of conflicts
$function($this);
} else if (function_exists($oldfunction)) {
// We continue to support the old function name to ensure backwards compatability
$oldfunction($this);
}
}
// Remove any empty root nodes
foreach ($this->rootnodes as $node) {
// Dont remove the home node
if ($node->key !== 'home' && !$node->has_children()) {
$node->remove();
}
}
if (!$this->contains_active_node()) {
$this->search_for_active_node();
}
// If the user is not logged in modify the navigation structure as detailed
// in {@link http://docs.moodle.org/dev/Navigation_2.0_structure}
if (!isloggedin()) {
$activities = clone($this->rootnodes['site']->children);
$this->rootnodes['site']->remove();
$children = clone($this->children);
$this->children = new navigation_node_collection();
foreach ($activities as $child) {
$this->children->add($child);
}
foreach ($children as $child) {
$this->children->add($child);
}
}
return true;
}
/**
* Returns true if courses should be shown within categories on the navigation.
*
* @return bool
*/
protected function show_categories() {
global $CFG, $DB;
if ($this->showcategories === null) {
$show = $this->page->context->contextlevel == CONTEXT_COURSECAT;
$show = $show || (!empty($CFG->navshowcategories) && $DB->count_records('course_categories') > 1);
$this->showcategories = $show;
}
return $this->showcategories;
}
/**
* Checks the course format to see whether it wants the navigation to load
* additional information for the course.
*
* This function utilises a callback that can exist within the course format lib.php file
* The callback should be a function called:
* callback_{formatname}_display_content()
* It doesn't get any arguments and should return true if additional content is
* desired. If the callback doesn't exist we assume additional content is wanted.
*
* @param string $format The course format
* @return bool
*/
protected function format_display_course_content($format) {
global $CFG;
$formatlib = $CFG->dirroot.'/course/format/'.$format.'/lib.php';
if (file_exists($formatlib)) {
require_once($formatlib);
$displayfunc = 'callback_'.$format.'_display_content';
if (function_exists($displayfunc) && !$displayfunc()) {
return $displayfunc();
}
}
return true;
}
/**
* Loads the courses in Moodle into the navigation.
*
* @global moodle_database $DB
* @param string|array $categoryids An array containing categories to load courses
* for, OR null to load courses for all categories.
* @return array An array of navigation_nodes one for each course
*/
protected function load_all_courses($categoryids = null) {
global $CFG, $DB, $SITE;
// Work out the limit of courses.
$limit = 20;
if (!empty($CFG->navcourselimit)) {
$limit = $CFG->navcourselimit;
}
$toload = (empty($CFG->navshowallcourses))?self::LOAD_ROOT_CATEGORIES:self::LOAD_ALL_CATEGORIES;
// If we are going to show all courses AND we are showing categories then
// to save us repeated DB calls load all of the categories now
if ($this->show_categories()) {
$this->load_all_categories($toload);
}
// Will be the return of our efforts
$coursenodes = array();
// Check if we need to show categories.
if ($this->show_categories()) {
// Hmmm we need to show categories... this is going to be painful.
// We now need to fetch up to $limit courses for each category to
// be displayed.
if ($categoryids !== null) {
if (!is_array($categoryids)) {
$categoryids = array($categoryids);
}
list($categorywhere, $categoryparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'cc');
$categorywhere = 'WHERE cc.id '.$categorywhere;
} else if ($toload == self::LOAD_ROOT_CATEGORIES) {
$categorywhere = 'WHERE cc.depth = 1 OR cc.depth = 2';
$categoryparams = array();
} else {
$categorywhere = '';
$categoryparams = array();
}
// First up we are going to get the categories that we are going to
// need so that we can determine how best to load the courses from them.
$sql = "SELECT cc.id, COUNT(c.id) AS coursecount
FROM {course_categories} cc
LEFT JOIN {course} c ON c.category = cc.id
{$categorywhere}
GROUP BY cc.id";
$categories = $DB->get_recordset_sql($sql, $categoryparams);
$fullfetch = array();
$partfetch = array();
foreach ($categories as $category) {
if (!$this->can_add_more_courses_to_category($category->id)) {
continue;
}
if ($category->coursecount > $limit * 5) {
$partfetch[] = $category->id;
} else if ($category->coursecount > 0) {
$fullfetch[] = $category->id;
}
}
$categories->close();
if (count($fullfetch)) {
// First up fetch all of the courses in categories where we know that we are going to
// need the majority of courses.
list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
list($categoryids, $categoryparams) = $DB->get_in_or_equal($fullfetch, SQL_PARAMS_NAMED, 'lcategory');
$sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
FROM {course} c
$ccjoin
WHERE c.category {$categoryids}
ORDER BY c.sortorder ASC";
$coursesrs = $DB->get_recordset_sql($sql, $categoryparams);
foreach ($coursesrs as $course) {
if ($course->id == $SITE->id) {
// This should not be necessary, frontpage is not in any category.
continue;
}
if (array_key_exists($course->id, $this->addedcourses)) {
// It is probably better to not include the already loaded courses
// directly in SQL because inequalities may confuse query optimisers
// and may interfere with query caching.
continue;
}
if (!$this->can_add_more_courses_to_category($course->category)) {
continue;
}
context_instance_preload($course);
if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
continue;
}
$coursenodes[$course->id] = $this->add_course($course);
}
$coursesrs->close();
}
if (count($partfetch)) {
// Next we will work our way through the categories where we will likely only need a small
// proportion of the courses.
foreach ($partfetch as $categoryid) {
list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
$sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
FROM {course} c
$ccjoin
WHERE c.category = :categoryid
ORDER BY c.sortorder ASC";
$courseparams = array('categoryid' => $categoryid);
$coursesrs = $DB->get_recordset_sql($sql, $courseparams, 0, $limit * 5);
foreach ($coursesrs as $course) {
if ($course->id == $SITE->id) {
// This should not be necessary, frontpage is not in any category.
continue;
}
if (array_key_exists($course->id, $this->addedcourses)) {
// It is probably better to not include the already loaded courses
// directly in SQL because inequalities may confuse query optimisers
// and may interfere with query caching.
// This also helps to respect expected $limit on repeated executions.
continue;
}
if (!$this->can_add_more_courses_to_category($course->category)) {
break;
}
context_instance_preload($course);
if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
continue;
}
$coursenodes[$course->id] = $this->add_course($course);
}
$coursesrs->close();
}
}
} else {
// Prepare the SQL to load the courses and their contexts
list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
list($courseids, $courseparams) = $DB->get_in_or_equal(array_keys($this->addedcourses), SQL_PARAMS_NAMED, 'lc', false);
$sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
FROM {course} c
$ccjoin
WHERE c.id {$courseids}
ORDER BY c.sortorder ASC";
$coursesrs = $DB->get_recordset_sql($sql, $courseparams);
foreach ($coursesrs as $course) {
if ($course->id == $SITE->id) {
// frotpage is not wanted here
continue;
}
context_instance_preload($course);
if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
continue;
}
$coursenodes[$course->id] = $this->add_course($course);
if (count($coursenodes) >= $limit) {
break;
}
}
$coursesrs->close();
}
return $coursenodes;
}
/**
* Returns true if more courses can be added to the provided category.
*
* @param int|navigation_node|stdClass $category
* @return bool
*/
protected function can_add_more_courses_to_category($category) {
global $CFG;
$limit = 20;
if (!empty($CFG->navcourselimit)) {
$limit = (int)$CFG->navcourselimit;
}
if (is_numeric($category)) {
if (!array_key_exists($category, $this->addedcategories)) {
return true;
}
$coursecount = count($this->addedcategories[$category]->children->type(self::TYPE_COURSE));
} else if ($category instanceof navigation_node) {
if ($category->type != self::TYPE_CATEGORY) {
return false;
}
$coursecount = count($category->children->type(self::TYPE_COURSE));
} else if (is_object($category) && property_exists($category,'id')) {
$coursecount = count($this->addedcategories[$category->id]->children->type(self::TYPE_COURSE));
}
return ($coursecount <= $limit);
}
/**
* Loads all categories (top level or if an id is specified for that category)
*
* @param int $categoryid The category id to load or null/0 to load all base level categories
* @param bool $showbasecategories If set to true all base level categories will be loaded as well
* as the requested category and any parent categories.
* @return navigation_node|void returns a navigation node if a category has been loaded.
*/
protected function load_all_categories($categoryid = self::LOAD_ROOT_CATEGORIES, $showbasecategories = false) {
global $DB;
// Check if this category has already been loaded
if ($this->allcategoriesloaded || ($categoryid < 1 && $this->is_category_fully_loaded($categoryid))) {
return true;
}
$catcontextsql = context_helper::get_preload_record_columns_sql('ctx');
$sqlselect = "SELECT cc.*, $catcontextsql
FROM {course_categories} cc
JOIN {context} ctx ON cc.id = ctx.instanceid";
$sqlwhere = "WHERE ctx.contextlevel = ".CONTEXT_COURSECAT;
$sqlorder = "ORDER BY cc.depth ASC, cc.sortorder ASC, cc.id ASC";
$params = array();
$categoriestoload = array();
if ($categoryid == self::LOAD_ALL_CATEGORIES) {
// We are going to load all categories regardless... prepare to fire
// on the database server!
} else if ($categoryid == self::LOAD_ROOT_CATEGORIES) { // can be 0
// We are going to load all of the first level categories (categories without parents)
$sqlwhere .= " AND cc.parent = 0";
} else if (array_key_exists($categoryid, $this->addedcategories)) {
// The category itself has been loaded already so we just need to ensure its subcategories
// have been loaded
list($sql, $params) = $DB->get_in_or_equal(array_keys($this->addedcategories), SQL_PARAMS_NAMED, 'parent', false);
if ($showbasecategories) {
// We need to include categories with parent = 0 as well
$sqlwhere .= " AND (cc.parent = :categoryid OR cc.parent = 0) AND cc.parent {$sql}";
} else {
// All we need is categories that match the parent
$sqlwhere .= " AND cc.parent = :categoryid AND cc.parent {$sql}";
}
$params['categoryid'] = $categoryid;
} else {
// This category hasn't been loaded yet so we need to fetch it, work out its category path
// and load this category plus all its parents and subcategories
$category = $DB->get_record('course_categories', array('id' => $categoryid), 'path', MUST_EXIST);
$categoriestoload = explode('/', trim($category->path, '/'));
list($select, $params) = $DB->get_in_or_equal($categoriestoload);
// We are going to use select twice so double the params
$params = array_merge($params, $params);
$basecategorysql = ($showbasecategories)?' OR cc.depth = 1':'';
$sqlwhere .= " AND (cc.id {$select} OR cc.parent {$select}{$basecategorysql})";
}
$categoriesrs = $DB->get_recordset_sql("$sqlselect $sqlwhere $sqlorder", $params);
$categories = array();
foreach ($categoriesrs as $category) {
// Preload the context.. we'll need it when adding the category in order
// to format the category name.
context_helper::preload_from_record($category);
if (array_key_exists($category->id, $this->addedcategories)) {
// Do nothing, its already been added.
} else if ($category->parent == '0') {
// This is a root category lets add it immediately
$this->add_category($category, $this->rootnodes['courses']);
} else if (array_key_exists($category->parent, $this->addedcategories)) {
// This categories parent has already been added we can add this immediately
$this->add_category($category, $this->addedcategories[$category->parent]);
} else {
$categories[] = $category;
}
}
$categoriesrs->close();
// Now we have an array of categories we need to add them to the navigation.
while (!empty($categories)) {
$category = reset($categories);
if (array_key_exists($category->id, $this->addedcategories)) {
// Do nothing
} else if ($category->parent == '0') {
$this->add_category($category, $this->rootnodes['courses']);
} else if (array_key_exists($category->parent, $this->addedcategories)) {
$this->add_category($category, $this->addedcategories[$category->parent]);
} else {
// This category isn't in the navigation and niether is it's parent (yet).
// We need to go through the category path and add all of its components in order.
$path = explode('/', trim($category->path, '/'));
foreach ($path as $catid) {
if (!array_key_exists($catid, $this->addedcategories)) {
// This category isn't in the navigation yet so add it.
$subcategory = $categories[$catid];
if ($subcategory->parent == '0') {
// Yay we have a root category - this likely means we will now be able
// to add categories without problems.
$this->add_category($subcategory, $this->rootnodes['courses']);
} else if (array_key_exists($subcategory->parent, $this->addedcategories)) {
// The parent is in the category (as we'd expect) so add it now.
$this->add_category($subcategory, $this->addedcategories[$subcategory->parent]);
// Remove the category from the categories array.
unset($categories[$catid]);
} else {
// We should never ever arrive here - if we have then there is a bigger
// problem at hand.
throw new coding_exception('Category path order is incorrect and/or there are missing categories');
}
}
}
}
// Remove the category from the categories array now that we know it has been added.
unset($categories[$category->id]);
}
if ($categoryid === self::LOAD_ALL_CATEGORIES) {
$this->allcategoriesloaded = true;
}
// Check if there are any categories to load.
if (count($categoriestoload) > 0) {
$readytoloadcourses = array();
foreach ($categoriestoload as $category) {
if ($this->can_add_more_courses_to_category($category)) {
$readytoloadcourses[] = $category;
}
}
if (count($readytoloadcourses)) {
$this->load_all_courses($readytoloadcourses);
}
}
}
/**
* Adds a structured category to the navigation in the correct order/place
*
* @param stdClass $category
* @param navigation_node $parent
*/
protected function add_category(stdClass $category, navigation_node $parent) {
if (array_key_exists($category->id, $this->addedcategories)) {
return;
}
$url = new moodle_url('/course/category.php', array('id' => $category->id));
$context = get_context_instance(CONTEXT_COURSECAT, $category->id);
$categoryname = format_string($category->name, true, array('context' => $context));
$categorynode = $parent->add($categoryname, $url, self::TYPE_CATEGORY, $categoryname, $category->id);
if (empty($category->visible)) {
if (has_capability('moodle/category:viewhiddencategories', get_system_context())) {
$categorynode->hidden = true;
} else {
$categorynode->display = false;
}
}
$this->addedcategories[$category->id] = $categorynode;
}
/**
* Loads the given course into the navigation
*
* @param stdClass $course
* @return navigation_node
*/
protected function load_course(stdClass $course) {
global $SITE;
if ($course->id == $SITE->id) {
// This is always loaded during initialisation
return $this->rootnodes['site'];
} else if (array_key_exists($course->id, $this->addedcourses)) {
// The course has already been loaded so return a reference
return $this->addedcourses[$course->id];
} else {
// Add the course
return $this->add_course($course);
}
}
/**
* Loads all of the courses section into the navigation.
*
* This function utilisies a callback that can be implemented within the course
* formats lib.php file to customise the navigation that is generated at this
* point for the course.
*
* By default (if not defined) the method {@link global_navigation::load_generic_course_sections()} is
* called instead.
*
* @param stdClass $course Database record for the course
* @param navigation_node $coursenode The course node within the navigation
* @return array Array of navigation nodes for the section with key = section id
*/
protected function load_course_sections(stdClass $course, navigation_node $coursenode) {
global $CFG;
$structurefile = $CFG->dirroot.'/course/format/'.$course->format.'/lib.php';
$structurefunc = 'callback_'.$course->format.'_load_content';
if (function_exists($structurefunc)) {
return $structurefunc($this, $course, $coursenode);
} else if (file_exists($structurefile)) {
require_once $structurefile;
if (function_exists($structurefunc)) {
return $structurefunc($this, $course, $coursenode);
} else {
return $this->load_generic_course_sections($course, $coursenode);
}
} else {
return $this->load_generic_course_sections($course, $coursenode);
}
}
/**
* Generates an array of sections and an array of activities for the given course.
*
* This method uses the cache to improve performance and avoid the get_fast_modinfo call
*
* @param stdClass $course
* @return array Array($sections, $activities)
*/
protected function generate_sections_and_activities(stdClass $course) {
global $CFG;
require_once($CFG->dirroot.'/course/lib.php');
$modinfo = get_fast_modinfo($course);
$sections = array_slice($modinfo->get_section_info_all(), 0, $course->numsections+1, true);
$activities = array();
foreach ($sections as $key => $section) {
// Clone and unset summary to prevent $SESSION bloat (MDL-31802).
$sections[$key] = clone($section);
unset($sections[$key]->summary);
$sections[$key]->hasactivites = false;
if (!array_key_exists($section->section, $modinfo->sections)) {
continue;
}
foreach ($modinfo->sections[$section->section] as $cmid) {
$cm = $modinfo->cms[$cmid];
if (!$cm->uservisible) {
continue;
}
$activity = new stdClass;
$activity->id = $cm->id;
$activity->course = $course->id;
$activity->section = $section->section;
$activity->name = $cm->name;
$activity->icon = $cm->icon;
$activity->iconcomponent = $cm->iconcomponent;
$activity->hidden = (!$cm->visible);
$activity->modname = $cm->modname;
$activity->nodetype = navigation_node::NODETYPE_LEAF;
$activity->onclick = $cm->get_on_click();
$url = $cm->get_url();
if (!$url) {
$activity->url = null;
$activity->display = false;
} else {
$activity->url = $cm->get_url()->out();
$activity->display = true;
if (self::module_extends_navigation($cm->modname)) {
$activity->nodetype = navigation_node::NODETYPE_BRANCH;
}
}
$activities[$cmid] = $activity;
if ($activity->display) {
$sections[$key]->hasactivites = true;
}
}
}
return array($sections, $activities);
}
/**
* Generically loads the course sections into the course's navigation.
*
* @param stdClass $course
* @param navigation_node $coursenode
* @param string $courseformat The course format
* @return array An array of course section nodes
*/
public function load_generic_course_sections(stdClass $course, navigation_node $coursenode, $courseformat='unknown') {
global $CFG, $DB, $USER, $SITE;
require_once($CFG->dirroot.'/course/lib.php');
list($sections, $activities) = $this->generate_sections_and_activities($course);
$namingfunction = 'callback_'.$courseformat.'_get_section_name';
$namingfunctionexists = (function_exists($namingfunction));
$urlfunction = 'callback_'.$courseformat.'_get_section_url';
if (function_exists($urlfunction)) {
// This code path is deprecated but we decided not to warn developers as
// major changes are likely to follow in 2.4. See MDL-32504.
} else {
$urlfunction = null;
}
$key = 0;
if (defined('AJAX_SCRIPT') && AJAX_SCRIPT == '0' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
$key = optional_param('section', $key, PARAM_INT);
}
$navigationsections = array();
foreach ($sections as $sectionid => $section) {
$section = clone($section);
if ($course->id == $SITE->id) {
$this->load_section_activities($coursenode, $section->section, $activities);
} else {
if (!$section->uservisible || (!$this->showemptysections &&
!$section->hasactivites && $this->includesectionnum !== $section->section)) {
continue;
}
if ($namingfunctionexists) {
$sectionname = $namingfunction($course, $section, $sections);
} else {
$sectionname = get_string('section').' '.$section->section;
}
$url = null;
if ($urlfunction) {
// pre 2.3 style format url
$url = $urlfunction($course->id, $section->section);
}else{
if ($course->coursedisplay == COURSE_DISPLAY_MULTIPAGE) {
$url = course_get_url($course, $section->section);
}
}
$sectionnode = $coursenode->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id);
$sectionnode->nodetype = navigation_node::NODETYPE_BRANCH;
$sectionnode->hidden = (!$section->visible || !$section->available);
if ($key != '0' && $section->section != '0' && $section->section == $key && $this->page->context->contextlevel != CONTEXT_MODULE && $section->hasactivites) {
$sectionnode->make_active();
$this->load_section_activities($sectionnode, $section->section, $activities);
}
$section->sectionnode = $sectionnode;
$navigationsections[$sectionid] = $section;
}
}
return $navigationsections;
}
/**
* Loads all of the activities for a section into the navigation structure.
*
* @param navigation_node $sectionnode
* @param int $sectionnumber
* @param array $activities An array of activites as returned by {@link global_navigation::generate_sections_and_activities()}
* @param stdClass $course The course object the section and activities relate to.
* @return array Array of activity nodes
*/
protected function load_section_activities(navigation_node $sectionnode, $sectionnumber, array $activities, $course = null) {
global $CFG, $SITE;
// A static counter for JS function naming
static $legacyonclickcounter = 0;
$activitynodes = array();
if (empty($activities)) {
return $activitynodes;
}
if (!is_object($course)) {
$activity = reset($activities);
$courseid = $activity->course;
} else {
$courseid = $course->id;
}
$showactivities = ($courseid != $SITE->id || !empty($CFG->navshowfrontpagemods));
foreach ($activities as $activity) {
if ($activity->section != $sectionnumber) {
continue;
}
if ($activity->icon) {
$icon = new pix_icon($activity->icon, get_string('modulename', $activity->modname), $activity->iconcomponent);
} else {
$icon = new pix_icon('icon', get_string('modulename', $activity->modname), $activity->modname);
}
// Prepare the default name and url for the node
$activityname = format_string($activity->name, true, array('context' => get_context_instance(CONTEXT_MODULE, $activity->id)));
$action = new moodle_url($activity->url);
// Check if the onclick property is set (puke!)
if (!empty($activity->onclick)) {
// Increment the counter so that we have a unique number.
$legacyonclickcounter++;
// Generate the function name we will use
$functionname = 'legacy_activity_onclick_handler_'.$legacyonclickcounter;
$propogrationhandler = '';
// Check if we need to cancel propogation. Remember inline onclick
// events would return false if they wanted to prevent propogation and the
// default action.
if (strpos($activity->onclick, 'return false')) {
$propogrationhandler = 'e.halt();';
}
// Decode the onclick - it has already been encoded for display (puke)
$onclick = htmlspecialchars_decode($activity->onclick);
// Build the JS function the click event will call
$jscode = "function {$functionname}(e) { $propogrationhandler $onclick }";
$this->page->requires->js_init_code($jscode);
// Override the default url with the new action link
$action = new action_link($action, $activityname, new component_action('click', $functionname));
}
$activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon);
$activitynode->title(get_string('modulename', $activity->modname));
$activitynode->hidden = $activity->hidden;
$activitynode->display = $showactivities && $activity->display;
$activitynode->nodetype = $activity->nodetype;
$activitynodes[$activity->id] = $activitynode;
}
return $activitynodes;
}
/**
* Loads a stealth module from unavailable section
* @param navigation_node $coursenode
* @param stdClass $modinfo
* @return navigation_node or null if not accessible
*/
protected function load_stealth_activity(navigation_node $coursenode, $modinfo) {
if (empty($modinfo->cms[$this->page->cm->id])) {
return null;
}
$cm = $modinfo->cms[$this->page->cm->id];
if (!$cm->uservisible) {
return null;
}
if ($cm->icon) {
$icon = new pix_icon($cm->icon, get_string('modulename', $cm->modname), $cm->iconcomponent);
} else {
$icon = new pix_icon('icon', get_string('modulename', $cm->modname), $cm->modname);
}
$url = $cm->get_url();
$activitynode = $coursenode->add(format_string($cm->name), $url, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
$activitynode->title(get_string('modulename', $cm->modname));
$activitynode->hidden = (!$cm->visible);
if (!$url) {
// Don't show activities that don't have links!
$activitynode->display = false;
} else if (self::module_extends_navigation($cm->modname)) {
$activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
}
return $activitynode;
}
/**
* Loads the navigation structure for the given activity into the activities node.
*
* This method utilises a callback within the modules lib.php file to load the
* content specific to activity given.
*
* The callback is a method: {modulename}_extend_navigation()
* Examples:
* * {@link forum_extend_navigation()}
* * {@link workshop_extend_navigation()}
*
* @param cm_info|stdClass $cm
* @param stdClass $course
* @param navigation_node $activity
* @return bool
*/
protected function load_activity($cm, stdClass $course, navigation_node $activity) {
global $CFG, $DB;
// make sure we have a $cm from get_fast_modinfo as this contains activity access details
if (!($cm instanceof cm_info)) {
$modinfo = get_fast_modinfo($course);
$cm = $modinfo->get_cm($cm->id);
}
$activity->nodetype = navigation_node::NODETYPE_LEAF;
$activity->make_active();
$file = $CFG->dirroot.'/mod/'.$cm->modname.'/lib.php';
$function = $cm->modname.'_extend_navigation';
if (file_exists($file)) {
require_once($file);
if (function_exists($function)) {
$activtyrecord = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
$function($activity, $course, $activtyrecord, $cm);
}
}
// Allow the active advanced grading method plugin to append module navigation
$featuresfunc = $cm->modname.'_supports';
if (function_exists($featuresfunc) && $featuresfunc(FEATURE_ADVANCED_GRADING)) {
require_once($CFG->dirroot.'/grade/grading/lib.php');
$gradingman = get_grading_manager($cm->context, $cm->modname);
$gradingman->extend_navigation($this, $activity);
}
return $activity->has_children();
}
/**
* Loads user specific information into the navigation in the appropriate place.
*
* If no user is provided the current user is assumed.
*
* @param stdClass $user
* @param bool $forceforcontext probably force something to be loaded somewhere (ask SamH if not sure what this means)
* @return bool
*/
protected function load_for_user($user=null, $forceforcontext=false) {
global $DB, $CFG, $USER, $SITE;
if ($user === null) {
// We can't require login here but if the user isn't logged in we don't
// want to show anything
if (!isloggedin() || isguestuser()) {
return false;
}
$user = $USER;
} else if (!is_object($user)) {
// If the user is not an object then get them from the database
$select = context_helper::get_preload_record_columns_sql('ctx');
$sql = "SELECT u.*, $select
FROM {user} u
JOIN {context} ctx ON u.id = ctx.instanceid
WHERE u.id = :userid AND
ctx.contextlevel = :contextlevel";
$user = $DB->get_record_sql($sql, array('userid' => (int)$user, 'contextlevel' => CONTEXT_USER), MUST_EXIST);
context_helper::preload_from_record($user);
}
$iscurrentuser = ($user->id == $USER->id);
$usercontext = get_context_instance(CONTEXT_USER, $user->id);
// Get the course set against the page, by default this will be the site
$course = $this->page->course;
$baseargs = array('id'=>$user->id);
if ($course->id != $SITE->id && (!$iscurrentuser || $forceforcontext)) {
$coursenode = $this->load_course($course);
$baseargs['course'] = $course->id;
$coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
$issitecourse = false;
} else {
// Load all categories and get the context for the system
$coursecontext = get_context_instance(CONTEXT_SYSTEM);
$issitecourse = true;
}
// Create a node to add user information under.
if ($iscurrentuser && !$forceforcontext) {
// If it's the current user the information will go under the profile root node
$usernode = $this->rootnodes['myprofile'];
$course = get_site();
$coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
$issitecourse = true;
} else {
if (!$issitecourse) {
// Not the current user so add it to the participants node for the current course
$usersnode = $coursenode->get('participants', navigation_node::TYPE_CONTAINER);
$userviewurl = new moodle_url('/user/view.php', $baseargs);
} else {
// This is the site so add a users node to the root branch
$usersnode = $this->rootnodes['users'];
if (has_capability('moodle/course:viewparticipants', $coursecontext)) {
$usersnode->action = new moodle_url('/user/index.php', array('id'=>$course->id));
}
$userviewurl = new moodle_url('/user/profile.php', $baseargs);
}
if (!$usersnode) {
// We should NEVER get here, if the course hasn't been populated
// with a participants node then the navigaiton either wasn't generated
// for it (you are missing a require_login or set_context call) or
// you don't have access.... in the interests of no leaking informatin
// we simply quit...
return false;
}
// Add a branch for the current user
$canseefullname = has_capability('moodle/site:viewfullnames', $coursecontext);
$usernode = $usersnode->add(fullname($user, $canseefullname), $userviewurl, self::TYPE_USER, null, $user->id);
if ($this->page->context->contextlevel == CONTEXT_USER && $user->id == $this->page->context->instanceid) {
$usernode->make_active();
}
}
// If the user is the current user or has permission to view the details of the requested
// user than add a view profile link.
if ($iscurrentuser || has_capability('moodle/user:viewdetails', $coursecontext) || has_capability('moodle/user:viewdetails', $usercontext)) {
if ($issitecourse || ($iscurrentuser && !$forceforcontext)) {
$usernode->add(get_string('viewprofile'), new moodle_url('/user/profile.php',$baseargs));
} else {
$usernode->add(get_string('viewprofile'), new moodle_url('/user/view.php',$baseargs));
}
}
if (!empty($CFG->navadduserpostslinks)) {
// Add nodes for forum posts and discussions if the user can view either or both
// There are no capability checks here as the content of the page is based
// purely on the forums the current user has access too.
$forumtab = $usernode->add(get_string('forumposts', 'forum'));
$forumtab->add(get_string('posts', 'forum'), new moodle_url('/mod/forum/user.php', $baseargs));
$forumtab->add(get_string('discussions', 'forum'), new moodle_url('/mod/forum/user.php', array_merge($baseargs, array('mode'=>'discussions'))));
}
// Add blog nodes
if (!empty($CFG->bloglevel)) {
if (!$this->cache->cached('userblogoptions'.$user->id)) {
require_once($CFG->dirroot.'/blog/lib.php');
// Get all options for the user
$options = blog_get_options_for_user($user);
$this->cache->set('userblogoptions'.$user->id, $options);
} else {
$options = $this->cache->{'userblogoptions'.$user->id};
}
if (count($options) > 0) {
$blogs = $usernode->add(get_string('blogs', 'blog'), null, navigation_node::TYPE_CONTAINER);
foreach ($options as $type => $option) {
if ($type == "rss") {
$blogs->add($option['string'], $option['link'], settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', ''));
} else {
$blogs->add($option['string'], $option['link']);
}
}
}
}
if (!empty($CFG->messaging)) {
$messageargs = null;
if ($USER->id!=$user->id) {
$messageargs = array('id'=>$user->id);
}
$url = new moodle_url('/message/index.php',$messageargs);
$usernode->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages');
}
$context = get_context_instance(CONTEXT_USER, $USER->id);
if ($iscurrentuser && has_capability('moodle/user:manageownfiles', $context)) {
$url = new moodle_url('/user/files.php');
$usernode->add(get_string('myfiles'), $url, self::TYPE_SETTING);
}
// Add a node to view the users notes if permitted
if (!empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $coursecontext)) {
$url = new moodle_url('/notes/index.php',array('user'=>$user->id));
if ($coursecontext->instanceid) {
$url->param('course', $coursecontext->instanceid);
}
$usernode->add(get_string('notes', 'notes'), $url);
}
// Add reports node
$reporttab = $usernode->add(get_string('activityreports'));
$reports = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php');
foreach ($reports as $reportfunction) {
$reportfunction($reporttab, $user, $course);
}
$anyreport = has_capability('moodle/user:viewuseractivitiesreport', $usercontext);
if ($anyreport || ($course->showreports && $iscurrentuser && $forceforcontext)) {
// Add grade hardcoded grade report if necessary
$gradeaccess = false;
if (has_capability('moodle/grade:viewall', $coursecontext)) {
//ok - can view all course grades
$gradeaccess = true;
} else if ($course->showgrades) {
if ($iscurrentuser && has_capability('moodle/grade:view', $coursecontext)) {
//ok - can view own grades
$gradeaccess = true;
} else if (has_capability('moodle/grade:viewall', $usercontext)) {
// ok - can view grades of this user - parent most probably
$gradeaccess = true;
} else if ($anyreport) {
// ok - can view grades of this user - parent most probably
$gradeaccess = true;
}
}
if ($gradeaccess) {
$reporttab->add(get_string('grade'), new moodle_url('/course/user.php', array('mode'=>'grade', 'id'=>$course->id, 'user'=>$usercontext->instanceid)));
}
}
// Check the number of nodes in the report node... if there are none remove the node
$reporttab->trim_if_empty();
// If the user is the current user add the repositories for the current user
$hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
if ($iscurrentuser) {
if (!$this->cache->cached('contexthasrepos'.$usercontext->id)) {
require_once($CFG->dirroot . '/repository/lib.php');
$editabletypes = repository::get_editable_types($usercontext);
$haseditabletypes = !empty($editabletypes);
unset($editabletypes);
$this->cache->set('contexthasrepos'.$usercontext->id, $haseditabletypes);
} else {
$haseditabletypes = $this->cache->{'contexthasrepos'.$usercontext->id};
}
if ($haseditabletypes) {
$usernode->add(get_string('repositories', 'repository'), new moodle_url('/repository/manage_instances.php', array('contextid' => $usercontext->id)));
}
} else if ($course->id == $SITE->id && has_capability('moodle/user:viewdetails', $usercontext) && (!in_array('mycourses', $hiddenfields) || has_capability('moodle/user:viewhiddendetails', $coursecontext))) {
// Add view grade report is permitted
$reports = get_plugin_list('gradereport');
arsort($reports); // user is last, we want to test it first
$userscourses = enrol_get_users_courses($user->id);
$userscoursesnode = $usernode->add(get_string('courses'));
foreach ($userscourses as $usercourse) {
$usercoursecontext = get_context_instance(CONTEXT_COURSE, $usercourse->id);
$usercourseshortname = format_string($usercourse->shortname, true, array('context' => $usercoursecontext));
$usercoursenode = $userscoursesnode->add($usercourseshortname, new moodle_url('/user/view.php', array('id'=>$user->id, 'course'=>$usercourse->id)), self::TYPE_CONTAINER);
$gradeavailable = has_capability('moodle/grade:viewall', $usercoursecontext);
if (!$gradeavailable && !empty($usercourse->showgrades) && is_array($reports) && !empty($reports)) {
foreach ($reports as $plugin => $plugindir) {
if (has_capability('gradereport/'.$plugin.':view', $usercoursecontext)) {
//stop when the first visible plugin is found
$gradeavailable = true;
break;
}
}
}
if ($gradeavailable) {
$url = new moodle_url('/grade/report/index.php', array('id'=>$usercourse->id));
$usercoursenode->add(get_string('grades'), $url, self::TYPE_SETTING, null, null, new pix_icon('i/grades', ''));
}
// Add a node to view the users notes if permitted
if (!empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $usercoursecontext)) {
$url = new moodle_url('/notes/index.php',array('user'=>$user->id, 'course'=>$usercourse->id));
$usercoursenode->add(get_string('notes', 'notes'), $url, self::TYPE_SETTING);
}
if (can_access_course($usercourse, $user->id)) {
$usercoursenode->add(get_string('entercourse'), new moodle_url('/course/view.php', array('id'=>$usercourse->id)), self::TYPE_SETTING, null, null, new pix_icon('i/course', ''));
}
$reporttab = $usercoursenode->add(get_string('activityreports'));
$reports = get_plugin_list_with_function('report', 'extend_navigation_user', 'lib.php');
foreach ($reports as $reportfunction) {
$reportfunction($reporttab, $user, $usercourse);
}
$reporttab->trim_if_empty();
}
}
return true;
}
/**
* This method simply checks to see if a given module can extend the navigation.
*
* @todo (MDL-25290) A shared caching solution should be used to save details on what extends navigation.
*
* @param string $modname
* @return bool
*/
protected static function module_extends_navigation($modname) {
global $CFG;
static $extendingmodules = array();
if (!array_key_exists($modname, $extendingmodules)) {
$extendingmodules[$modname] = false;
$file = $CFG->dirroot.'/mod/'.$modname.'/lib.php';
if (file_exists($file)) {
$function = $modname.'_extend_navigation';
require_once($file);
$extendingmodules[$modname] = (function_exists($function));
}
}
return $extendingmodules[$modname];
}
/**
* Extends the navigation for the given user.
*
* @param stdClass $user A user from the database
*/
public function extend_for_user($user) {
$this->extendforuser[] = $user;
}
/**
* Returns all of the users the navigation is being extended for
*
* @return array An array of extending users.
*/
public function get_extending_users() {
return $this->extendforuser;
}
/**
* Adds the given course to the navigation structure.
*
* @param stdClass $course
* @param bool $forcegeneric
* @param bool $ismycourse
* @return navigation_node
*/
public function add_course(stdClass $course, $forcegeneric = false, $ismycourse = false) {
global $CFG, $SITE;
// We found the course... we can return it now :)
if (!$forcegeneric && array_key_exists($course->id, $this->addedcourses)) {
return $this->addedcourses[$course->id];
}
$coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
if ($course->id != $SITE->id && !$course->visible) {
if (is_role_switched($course->id)) {
// user has to be able to access course in order to switch, let's skip the visibility test here
} else if (!has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
return false;
}
}
$issite = ($course->id == $SITE->id);
$shortname = format_string($course->shortname, true, array('context' => $coursecontext));
if ($issite) {
$parent = $this;
$url = null;
if (empty($CFG->usesitenameforsitepages)) {
$shortname = get_string('sitepages');
}
} else if ($ismycourse && !$forcegeneric) {
if (!empty($CFG->navshowmycoursecategories) && ($parent = $this->rootnodes['mycourses']->find($course->category, self::TYPE_CATEGORY))) {
// Nothing to do here the above statement set $parent to the category within mycourses.
} else {
$parent = $this->rootnodes['mycourses'];
}
$url = new moodle_url('/course/view.php', array('id'=>$course->id));
} else {
$parent = $this->rootnodes['courses'];
$url = new moodle_url('/course/view.php', array('id'=>$course->id));
if (!empty($course->category) && $this->show_categories()) {
if ($this->show_categories() && !$this->is_category_fully_loaded($course->category)) {
// We need to load the category structure for this course
$this->load_all_categories($course->category, false);
}
if (array_key_exists($course->category, $this->addedcategories)) {
$parent = $this->addedcategories[$course->category];
// This could lead to the course being created so we should check whether it is the case again
if (!$forcegeneric && array_key_exists($course->id, $this->addedcourses)) {
return $this->addedcourses[$course->id];
}
}
}
}
$coursenode = $parent->add($shortname, $url, self::TYPE_COURSE, $shortname, $course->id);
$coursenode->nodetype = self::NODETYPE_BRANCH;
$coursenode->hidden = (!$course->visible);
$coursenode->title(format_string($course->fullname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id))));
if (!$forcegeneric) {
$this->addedcourses[$course->id] = $coursenode;
}
return $coursenode;
}
/**
* Returns true if the category has already been loaded as have any child categories
*
* @param int $categoryid
* @return bool
*/
protected function is_category_fully_loaded($categoryid) {
return (array_key_exists($categoryid, $this->addedcategories) && ($this->allcategoriesloaded || $this->addedcategories[$categoryid]->children->count() > 0));
}
/**
* Adds essential course nodes to the navigation for the given course.
*
* This method adds nodes such as reports, blogs and participants
*
* @param navigation_node $coursenode
* @param stdClass $course
* @return bool returns true on successful addition of a node.
*/
public function add_course_essentials($coursenode, stdClass $course) {
global $CFG, $SITE;
if ($course->id == $SITE->id) {
return $this->add_front_page_course_essentials($coursenode, $course);
}
if ($coursenode == false || !($coursenode instanceof navigation_node) || $coursenode->get('participants', navigation_node::TYPE_CONTAINER)) {
return true;
}
//Participants
if (has_capability('moodle/course:viewparticipants', $this->page->context)) {
$participants = $coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id), self::TYPE_CONTAINER, get_string('participants'), 'participants');
$currentgroup = groups_get_course_group($course, true);
if ($course->id == $SITE->id) {
$filterselect = '';
} else if ($course->id && !$currentgroup) {
$filterselect = $course->id;
} else {
$filterselect = $currentgroup;
}
$filterselect = clean_param($filterselect, PARAM_INT);
if (($CFG->bloglevel == BLOG_GLOBAL_LEVEL or ($CFG->bloglevel == BLOG_SITE_LEVEL and (isloggedin() and !isguestuser())))
and has_capability('moodle/blog:view', get_context_instance(CONTEXT_SYSTEM))) {
$blogsurls = new moodle_url('/blog/index.php', array('courseid' => $filterselect));
$participants->add(get_string('blogscourse','blog'), $blogsurls->out());
}
if (!empty($CFG->enablenotes) && (has_capability('moodle/notes:manage', $this->page->context) || has_capability('moodle/notes:view', $this->page->context))) {
$participants->add(get_string('notes','notes'), new moodle_url('/notes/index.php', array('filtertype'=>'course', 'filterselect'=>$course->id)));
}
} else if (count($this->extendforuser) > 0 || $this->page->course->id == $course->id) {
$participants = $coursenode->add(get_string('participants'), null, self::TYPE_CONTAINER, get_string('participants'), 'participants');
}
// View course reports
if (has_capability('moodle/site:viewreports', $this->page->context)) { // basic capability for listing of reports
$reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null, new pix_icon('i/stats', ''));
$coursereports = get_plugin_list('coursereport'); // deprecated
foreach ($coursereports as $report=>$dir) {
$libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
if (file_exists($libfile)) {
require_once($libfile);
$reportfunction = $report.'_report_extend_navigation';
if (function_exists($report.'_report_extend_navigation')) {
$reportfunction($reportnav, $course, $this->page->context);
}
}
}
$reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
foreach ($reports as $reportfunction) {
$reportfunction($reportnav, $course, $this->page->context);
}
}
return true;
}
/**
* This generates the structure of the course that won't be generated when
* the modules and sections are added.
*
* Things such as the reports branch, the participants branch, blogs... get
* added to the course node by this method.
*
* @param navigation_node $coursenode
* @param stdClass $course
* @return bool True for successfull generation
*/
public function add_front_page_course_essentials(navigation_node $coursenode, stdClass $course) {
global $CFG;
if ($coursenode == false || $coursenode->get('frontpageloaded', navigation_node::TYPE_CUSTOM)) {
return true;
}
// Hidden node that we use to determine if the front page navigation is loaded.
// This required as there are not other guaranteed nodes that may be loaded.
$coursenode->add('frontpageloaded', null, self::TYPE_CUSTOM, null, 'frontpageloaded')->display = false;
//Participants
if (has_capability('moodle/course:viewparticipants', get_system_context())) {
$coursenode->add(get_string('participants'), new moodle_url('/user/index.php?id='.$course->id), self::TYPE_CUSTOM, get_string('participants'), 'participants');
}
$filterselect = 0;
// Blogs
if (!empty($CFG->bloglevel)
and ($CFG->bloglevel == BLOG_GLOBAL_LEVEL or ($CFG->bloglevel == BLOG_SITE_LEVEL and (isloggedin() and !isguestuser())))
and has_capability('moodle/blog:view', get_context_instance(CONTEXT_SYSTEM))) {
$blogsurls = new moodle_url('/blog/index.php', array('courseid' => $filterselect));
$coursenode->add(get_string('blogssite','blog'), $blogsurls->out());
}
// Notes
if (!empty($CFG->enablenotes) && (has_capability('moodle/notes:manage', $this->page->context) || has_capability('moodle/notes:view', $this->page->context))) {
$coursenode->add(get_string('notes','notes'), new moodle_url('/notes/index.php', array('filtertype'=>'course', 'filterselect'=>$filterselect)));
}
// Tags
if (!empty($CFG->usetags) && isloggedin()) {
$coursenode->add(get_string('tags', 'tag'), new moodle_url('/tag/search.php'));
}
if (isloggedin()) {
// Calendar
$calendarurl = new moodle_url('/calendar/view.php', array('view' => 'month'));
$coursenode->add(get_string('calendar', 'calendar'), $calendarurl, self::TYPE_CUSTOM, null, 'calendar');
}
// View course reports
if (has_capability('moodle/site:viewreports', $this->page->context)) { // basic capability for listing of reports
$reportnav = $coursenode->add(get_string('reports'), null, self::TYPE_CONTAINER, null, null, new pix_icon('i/stats', ''));
$coursereports = get_plugin_list('coursereport'); // deprecated
foreach ($coursereports as $report=>$dir) {
$libfile = $CFG->dirroot.'/course/report/'.$report.'/lib.php';
if (file_exists($libfile)) {
require_once($libfile);
$reportfunction = $report.'_report_extend_navigation';
if (function_exists($report.'_report_extend_navigation')) {
$reportfunction($reportnav, $course, $this->page->context);
}
}
}
$reports = get_plugin_list_with_function('report', 'extend_navigation_course', 'lib.php');
foreach ($reports as $reportfunction) {
$reportfunction($reportnav, $course, $this->page->context);
}
}
return true;
}
/**
* Clears the navigation cache
*/
public function clear_cache() {
$this->cache->clear();
}
/**
* Sets an expansion limit for the navigation
*
* The expansion limit is used to prevent the display of content that has a type
* greater than the provided $type.
*
* Can be used to ensure things such as activities or activity content don't get
* shown on the navigation.
* They are still generated in order to ensure the navbar still makes sense.
*
* @param int $type One of navigation_node::TYPE_*
* @return bool true when complete.
*/
public function set_expansion_limit($type) {
global $SITE;
$nodes = $this->find_all_of_type($type);
foreach ($nodes as &$node) {
// We need to generate the full site node
if ($type == self::TYPE_COURSE && $node->key == $SITE->id) {
continue;
}
foreach ($node->children as &$child) {
// We still want to show course reports and participants containers
// or there will be navigation missing.
if ($type == self::TYPE_COURSE && $child->type === self::TYPE_CONTAINER) {
continue;
}
$child->display = false;
}
}
return true;
}
/**
* Attempts to get the navigation with the given key from this nodes children.
*
* This function only looks at this nodes children, it does NOT look recursivily.
* If the node can't be found then false is returned.
*
* If you need to search recursivily then use the {@link global_navigation::find()} method.
*
* Note: If you are trying to set the active node {@link navigation_node::override_active_url()}
* may be of more use to you.
*
* @param string|int $key The key of the node you wish to receive.
* @param int $type One of navigation_node::TYPE_*
* @return navigation_node|false
*/
public function get($key, $type = null) {
if (!$this->initialised) {
$this->initialise();
}
return parent::get($key, $type);
}
/**
* Searches this nodes children and their children to find a navigation node
* with the matching key and type.
*
* This method is recursive and searches children so until either a node is
* found or there are no more nodes to search.
*
* If you know that the node being searched for is a child of this node
* then use the {@link global_navigation::get()} method instead.
*
* Note: If you are trying to set the active node {@link navigation_node::override_active_url()}
* may be of more use to you.
*
* @param string|int $key The key of the node you wish to receive.
* @param int $type One of navigation_node::TYPE_*
* @return navigation_node|false
*/
public function find($key, $type) {
if (!$this->initialised) {
$this->initialise();
}
return parent::find($key, $type);
}
}
/**
* The global navigation class used especially for AJAX requests.
*
* The primary methods that are used in the global navigation class have been overriden
* to ensure that only the relevant branch is generated at the root of the tree.
* This can be done because AJAX is only used when the backwards structure for the
* requested branch exists.
* This has been done only because it shortens the amounts of information that is generated
* which of course will speed up the response time.. because no one likes laggy AJAX.
*
* @package core