MDL-76843 quiz: add test to verify random essay stats now work
[moodle.git] / lib / accesslib.php
blob8bc42803a371d07d1377fc705e3cffa02d281349
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * This file contains functions for managing user access
20 * <b>Public API vs internals</b>
22 * General users probably only care about
24 * Context handling
25 * - context_course::instance($courseid), context_module::instance($cm->id), context_coursecat::instance($catid)
26 * - context::instance_by_id($contextid)
27 * - $context->get_parent_contexts();
28 * - $context->get_child_contexts();
30 * Whether the user can do something...
31 * - has_capability()
32 * - has_any_capability()
33 * - has_all_capabilities()
34 * - require_capability()
35 * - require_login() (from moodlelib)
36 * - is_enrolled()
37 * - is_viewing()
38 * - is_guest()
39 * - is_siteadmin()
40 * - isguestuser()
41 * - isloggedin()
43 * What courses has this user access to?
44 * - get_enrolled_users()
46 * What users can do X in this context?
47 * - get_enrolled_users() - at and bellow course context
48 * - get_users_by_capability() - above course context
50 * Modify roles
51 * - role_assign()
52 * - role_unassign()
53 * - role_unassign_all()
55 * Advanced - for internal use only
56 * - load_all_capabilities()
57 * - reload_all_capabilities()
58 * - has_capability_in_accessdata()
59 * - get_user_roles_sitewide_accessdata()
60 * - etc.
62 * <b>Name conventions</b>
64 * "ctx" means context
65 * "ra" means role assignment
66 * "rdef" means role definition
68 * <b>accessdata</b>
70 * Access control data is held in the "accessdata" array
71 * which - for the logged-in user, will be in $USER->access
73 * For other users can be generated and passed around (but may also be cached
74 * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser).
76 * $accessdata is a multidimensional array, holding
77 * role assignments (RAs), role switches and initialization time.
79 * Things are keyed on "contextpaths" (the path field of
80 * the context table) for fast walking up/down the tree.
81 * <code>
82 * $accessdata['ra'][$contextpath] = array($roleid=>$roleid)
83 * [$contextpath] = array($roleid=>$roleid)
84 * [$contextpath] = array($roleid=>$roleid)
85 * </code>
87 * <b>Stale accessdata</b>
89 * For the logged-in user, accessdata is long-lived.
91 * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
92 * context paths affected by changes. Any check at-or-below
93 * a dirty context will trigger a transparent reload of accessdata.
95 * Changes at the system level will force the reload for everyone.
97 * <b>Default role caps</b>
98 * The default role assignment is not in the DB, so we
99 * add it manually to accessdata.
101 * This means that functions that work directly off the
102 * DB need to ensure that the default role caps
103 * are dealt with appropriately.
105 * @package core_access
106 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
107 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
110 defined('MOODLE_INTERNAL') || die();
112 /** No capability change */
113 define('CAP_INHERIT', 0);
114 /** Allow permission, overrides CAP_PREVENT defined in parent contexts */
115 define('CAP_ALLOW', 1);
116 /** Prevent permission, overrides CAP_ALLOW defined in parent contexts */
117 define('CAP_PREVENT', -1);
118 /** Prohibit permission, overrides everything in current and child contexts */
119 define('CAP_PROHIBIT', -1000);
121 /** System context level - only one instance in every system */
122 define('CONTEXT_SYSTEM', 10);
123 /** User context level - one instance for each user describing what others can do to user */
124 define('CONTEXT_USER', 30);
125 /** Course category context level - one instance for each category */
126 define('CONTEXT_COURSECAT', 40);
127 /** Course context level - one instances for each course */
128 define('CONTEXT_COURSE', 50);
129 /** Course module context level - one instance for each course module */
130 define('CONTEXT_MODULE', 70);
132 * Block context level - one instance for each block, sticky blocks are tricky
133 * because ppl think they should be able to override them at lower contexts.
134 * Any other context level instance can be parent of block context.
136 define('CONTEXT_BLOCK', 80);
138 /** Capability allow management of trusts - NOT IMPLEMENTED YET - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
139 define('RISK_MANAGETRUST', 0x0001);
140 /** Capability allows changes in system configuration - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
141 define('RISK_CONFIG', 0x0002);
142 /** Capability allows user to add scripted content - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
143 define('RISK_XSS', 0x0004);
144 /** Capability allows access to personal user information - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
145 define('RISK_PERSONAL', 0x0008);
146 /** Capability allows users to add content others may see - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
147 define('RISK_SPAM', 0x0010);
148 /** capability allows mass delete of data belonging to other users - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
149 define('RISK_DATALOSS', 0x0020);
151 /** rolename displays - the name as defined in the role definition, localised if name empty */
152 define('ROLENAME_ORIGINAL', 0);
153 /** rolename displays - the name as defined by a role alias at the course level, falls back to ROLENAME_ORIGINAL if alias not present */
154 define('ROLENAME_ALIAS', 1);
155 /** rolename displays - Both, like this: Role alias (Original) */
156 define('ROLENAME_BOTH', 2);
157 /** rolename displays - the name as defined in the role definition and the shortname in brackets */
158 define('ROLENAME_ORIGINALANDSHORT', 3);
159 /** rolename displays - the name as defined by a role alias, in raw form suitable for editing */
160 define('ROLENAME_ALIAS_RAW', 4);
161 /** rolename displays - the name is simply short role name */
162 define('ROLENAME_SHORT', 5);
164 if (!defined('CONTEXT_CACHE_MAX_SIZE')) {
165 /** maximum size of context cache - it is possible to tweak this config.php or in any script before inclusion of context.php */
166 define('CONTEXT_CACHE_MAX_SIZE', 2500);
169 /** Performance hint for assign_capability: the contextid is known to exist */
170 define('ACCESSLIB_HINT_CONTEXT_EXISTS', 'contextexists');
171 /** Performance hint for assign_capability: there is no existing entry in role_capabilities */
172 define('ACCESSLIB_HINT_NO_EXISTING', 'notexists');
175 * Although this looks like a global variable, it isn't really.
177 * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
178 * It is used to cache various bits of data between function calls for performance reasons.
179 * Sadly, a PHP global variable is the only way to implement this, without rewriting everything
180 * as methods of a class, instead of functions.
182 * @access private
183 * @global stdClass $ACCESSLIB_PRIVATE
184 * @name $ACCESSLIB_PRIVATE
186 global $ACCESSLIB_PRIVATE;
187 $ACCESSLIB_PRIVATE = new stdClass();
188 $ACCESSLIB_PRIVATE->cacheroledefs = array(); // Holds site-wide role definitions.
189 $ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache, loaded from DB once per page
190 $ACCESSLIB_PRIVATE->dirtyusers = null; // Dirty users cache, loaded from DB once per $USER->id
191 $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
194 * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
196 * This method should ONLY BE USED BY UNIT TESTS. It clears all of
197 * accesslib's private caches. You need to do this before setting up test data,
198 * and also at the end of the tests.
200 * @access private
201 * @return void
203 function accesslib_clear_all_caches_for_unit_testing() {
204 global $USER;
205 if (!PHPUNIT_TEST) {
206 throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
209 accesslib_clear_all_caches(true);
210 accesslib_reset_role_cache();
212 unset($USER->access);
216 * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
218 * This reset does not touch global $USER.
220 * @access private
221 * @param bool $resetcontexts
222 * @return void
224 function accesslib_clear_all_caches($resetcontexts) {
225 global $ACCESSLIB_PRIVATE;
227 $ACCESSLIB_PRIVATE->dirtycontexts = null;
228 $ACCESSLIB_PRIVATE->dirtyusers = null;
229 $ACCESSLIB_PRIVATE->accessdatabyuser = array();
231 if ($resetcontexts) {
232 context_helper::reset_caches();
237 * Full reset of accesslib's private role cache. ONLY TO BE USED FROM THIS LIBRARY FILE!
239 * This reset does not touch global $USER.
241 * Note: Only use this when the roles that need a refresh are unknown.
243 * @see accesslib_clear_role_cache()
245 * @access private
246 * @return void
248 function accesslib_reset_role_cache() {
249 global $ACCESSLIB_PRIVATE;
251 $ACCESSLIB_PRIVATE->cacheroledefs = array();
252 $cache = cache::make('core', 'roledefs');
253 $cache->purge();
257 * Clears accesslib's private cache of a specific role or roles. ONLY BE USED FROM THIS LIBRARY FILE!
259 * This reset does not touch global $USER.
261 * @access private
262 * @param int|array $roles
263 * @return void
265 function accesslib_clear_role_cache($roles) {
266 global $ACCESSLIB_PRIVATE;
268 if (!is_array($roles)) {
269 $roles = [$roles];
272 foreach ($roles as $role) {
273 if (isset($ACCESSLIB_PRIVATE->cacheroledefs[$role])) {
274 unset($ACCESSLIB_PRIVATE->cacheroledefs[$role]);
278 $cache = cache::make('core', 'roledefs');
279 $cache->delete_many($roles);
283 * Role is assigned at system context.
285 * @access private
286 * @param int $roleid
287 * @return array
289 function get_role_access($roleid) {
290 $accessdata = get_empty_accessdata();
291 $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
292 return $accessdata;
296 * Fetch raw "site wide" role definitions.
297 * Even MUC static acceleration cache appears a bit slow for this.
298 * Important as can be hit hundreds of times per page.
300 * @param array $roleids List of role ids to fetch definitions for.
301 * @return array Complete definition for each requested role.
303 function get_role_definitions(array $roleids) {
304 global $ACCESSLIB_PRIVATE;
306 if (empty($roleids)) {
307 return array();
310 // Grab all keys we have not yet got in our static cache.
311 if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
312 $cache = cache::make('core', 'roledefs');
313 foreach ($cache->get_many($uncached) as $roleid => $cachedroledef) {
314 if (is_array($cachedroledef)) {
315 $ACCESSLIB_PRIVATE->cacheroledefs[$roleid] = $cachedroledef;
319 // Check we have the remaining keys from the MUC.
320 if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
321 $uncached = get_role_definitions_uncached($uncached);
322 $ACCESSLIB_PRIVATE->cacheroledefs += $uncached;
323 $cache->set_many($uncached);
327 // Return just the roles we need.
328 return array_intersect_key($ACCESSLIB_PRIVATE->cacheroledefs, array_flip($roleids));
332 * Query raw "site wide" role definitions.
334 * @param array $roleids List of role ids to fetch definitions for.
335 * @return array Complete definition for each requested role.
337 function get_role_definitions_uncached(array $roleids) {
338 global $DB;
340 if (empty($roleids)) {
341 return array();
344 // Create a blank results array: even if a role has no capabilities,
345 // we need to ensure it is included in the results to show we have
346 // loaded all the capabilities that there are.
347 $rdefs = array();
348 foreach ($roleids as $roleid) {
349 $rdefs[$roleid] = array();
352 // Load all the capabilities for these roles in all contexts.
353 list($sql, $params) = $DB->get_in_or_equal($roleids);
354 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
355 FROM {role_capabilities} rc
356 JOIN {context} ctx ON rc.contextid = ctx.id
357 JOIN {capabilities} cap ON rc.capability = cap.name
358 WHERE rc.roleid $sql";
359 $rs = $DB->get_recordset_sql($sql, $params);
361 // Store the capabilities into the expected data structure.
362 foreach ($rs as $rd) {
363 if (!isset($rdefs[$rd->roleid][$rd->path])) {
364 $rdefs[$rd->roleid][$rd->path] = array();
366 $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
369 $rs->close();
371 // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
372 // we process role definitinons in a way that requires we see parent contexts
373 // before child contexts. This sort ensures that works (and is faster than
374 // sorting in the SQL query).
375 foreach ($rdefs as $roleid => $rdef) {
376 ksort($rdefs[$roleid]);
379 return $rdefs;
383 * Get the default guest role, this is used for guest account,
384 * search engine spiders, etc.
386 * @return stdClass role record
388 function get_guest_role() {
389 global $CFG, $DB;
391 if (empty($CFG->guestroleid)) {
392 if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
393 $guestrole = array_shift($roles); // Pick the first one
394 set_config('guestroleid', $guestrole->id);
395 return $guestrole;
396 } else {
397 debugging('Can not find any guest role!');
398 return false;
400 } else {
401 if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
402 return $guestrole;
403 } else {
404 // somebody is messing with guest roles, remove incorrect setting and try to find a new one
405 set_config('guestroleid', '');
406 return get_guest_role();
412 * Check whether a user has a particular capability in a given context.
414 * For example:
415 * $context = context_module::instance($cm->id);
416 * has_capability('mod/forum:replypost', $context)
418 * By default checks the capabilities of the current user, but you can pass a
419 * different userid. By default will return true for admin users, but you can override that with the fourth argument.
421 * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
422 * or capabilities with XSS, config or data loss risks.
424 * @category access
426 * @param string $capability the name of the capability to check. For example mod/forum:view
427 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
428 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
429 * @param boolean $doanything If false, ignores effect of admin role assignment
430 * @return boolean true if the user has this capability. Otherwise false.
432 function has_capability($capability, context $context, $user = null, $doanything = true) {
433 global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
435 if (during_initial_install()) {
436 if ($SCRIPT === "/$CFG->admin/index.php"
437 or $SCRIPT === "/$CFG->admin/cli/install.php"
438 or $SCRIPT === "/$CFG->admin/cli/install_database.php"
439 or (defined('BEHAT_UTIL') and BEHAT_UTIL)
440 or (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL)) {
441 // we are in an installer - roles can not work yet
442 return true;
443 } else {
444 return false;
448 if (strpos($capability, 'moodle/legacy:') === 0) {
449 throw new coding_exception('Legacy capabilities can not be used any more!');
452 if (!is_bool($doanything)) {
453 throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
456 // capability must exist
457 if (!$capinfo = get_capability_info($capability)) {
458 debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
459 return false;
462 if (!isset($USER->id)) {
463 // should never happen
464 $USER->id = 0;
465 debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
468 // make sure there is a real user specified
469 if ($user === null) {
470 $userid = $USER->id;
471 } else {
472 $userid = is_object($user) ? $user->id : $user;
475 // make sure forcelogin cuts off not-logged-in users if enabled
476 if (!empty($CFG->forcelogin) and $userid == 0) {
477 return false;
480 // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
481 if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
482 if (isguestuser($userid) or $userid == 0) {
483 return false;
487 // Check whether context locking is enabled.
488 if (!empty($CFG->contextlocking)) {
489 if ($capinfo->captype === 'write' && $context->locked) {
490 // Context locking applies to any write capability in a locked context.
491 // It does not apply to moodle/site:managecontextlocks - this is to allow context locking to be unlocked.
492 if ($capinfo->name !== 'moodle/site:managecontextlocks') {
493 // It applies to all users who are not site admins.
494 // It also applies to site admins when contextlockappliestoadmin is set.
495 if (!is_siteadmin($userid) || !empty($CFG->contextlockappliestoadmin)) {
496 return false;
502 // somehow make sure the user is not deleted and actually exists
503 if ($userid != 0) {
504 if ($userid == $USER->id and isset($USER->deleted)) {
505 // this prevents one query per page, it is a bit of cheating,
506 // but hopefully session is terminated properly once user is deleted
507 if ($USER->deleted) {
508 return false;
510 } else {
511 if (!context_user::instance($userid, IGNORE_MISSING)) {
512 // no user context == invalid userid
513 return false;
518 // context path/depth must be valid
519 if (empty($context->path) or $context->depth == 0) {
520 // this should not happen often, each upgrade tries to rebuild the context paths
521 debugging('Context id '.$context->id.' does not have valid path, please use context_helper::build_all_paths()');
522 if (is_siteadmin($userid)) {
523 return true;
524 } else {
525 return false;
529 if (!empty($USER->loginascontext)) {
530 // The current user is logged in as another user and can assume their identity at or below the `loginascontext`
531 // defined in the USER session.
532 // The user may not assume their identity at any other location.
533 if (!$USER->loginascontext->is_parent_of($context, true)) {
534 // The context being checked is not the specified context, or one of its children.
535 return false;
539 // Find out if user is admin - it is not possible to override the doanything in any way
540 // and it is not possible to switch to admin role either.
541 if ($doanything) {
542 if (is_siteadmin($userid)) {
543 if ($userid != $USER->id) {
544 return true;
546 // make sure switchrole is not used in this context
547 if (empty($USER->access['rsw'])) {
548 return true;
550 $parts = explode('/', trim($context->path, '/'));
551 $path = '';
552 $switched = false;
553 foreach ($parts as $part) {
554 $path .= '/' . $part;
555 if (!empty($USER->access['rsw'][$path])) {
556 $switched = true;
557 break;
560 if (!$switched) {
561 return true;
563 //ok, admin switched role in this context, let's use normal access control rules
567 // Careful check for staleness...
568 $context->reload_if_dirty();
570 if ($USER->id == $userid) {
571 if (!isset($USER->access)) {
572 load_all_capabilities();
574 $access =& $USER->access;
576 } else {
577 // make sure user accessdata is really loaded
578 get_user_accessdata($userid, true);
579 $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
582 return has_capability_in_accessdata($capability, $context, $access);
586 * Check if the user has any one of several capabilities from a list.
588 * This is just a utility method that calls has_capability in a loop. Try to put
589 * the capabilities that most users are likely to have first in the list for best
590 * performance.
592 * @category access
593 * @see has_capability()
595 * @param array $capabilities an array of capability names.
596 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
597 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
598 * @param boolean $doanything If false, ignore effect of admin role assignment
599 * @return boolean true if the user has any of these capabilities. Otherwise false.
601 function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
602 foreach ($capabilities as $capability) {
603 if (has_capability($capability, $context, $user, $doanything)) {
604 return true;
607 return false;
611 * Check if the user has all the capabilities in a list.
613 * This is just a utility method that calls has_capability in a loop. Try to put
614 * the capabilities that fewest users are likely to have first in the list for best
615 * performance.
617 * @category access
618 * @see has_capability()
620 * @param array $capabilities an array of capability names.
621 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
622 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
623 * @param boolean $doanything If false, ignore effect of admin role assignment
624 * @return boolean true if the user has all of these capabilities. Otherwise false.
626 function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
627 foreach ($capabilities as $capability) {
628 if (!has_capability($capability, $context, $user, $doanything)) {
629 return false;
632 return true;
636 * Is course creator going to have capability in a new course?
638 * This is intended to be used in enrolment plugins before or during course creation,
639 * do not use after the course is fully created.
641 * @category access
643 * @param string $capability the name of the capability to check.
644 * @param context $context course or category context where is course going to be created
645 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
646 * @return boolean true if the user will have this capability.
648 * @throws coding_exception if different type of context submitted
650 function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
651 global $CFG;
653 if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
654 throw new coding_exception('Only course or course category context expected');
657 if (has_capability($capability, $context, $user)) {
658 // User already has the capability, it could be only removed if CAP_PROHIBIT
659 // was involved here, but we ignore that.
660 return true;
663 if (!has_capability('moodle/course:create', $context, $user)) {
664 return false;
667 if (!enrol_is_enabled('manual')) {
668 return false;
671 if (empty($CFG->creatornewroleid)) {
672 return false;
675 if ($context->contextlevel == CONTEXT_COURSE) {
676 if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
677 return false;
679 } else {
680 if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
681 return false;
685 // Most likely they will be enrolled after the course creation is finished,
686 // does the new role have the required capability?
687 list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
688 return isset($neededroles[$CFG->creatornewroleid]);
692 * Check if the user is an admin at the site level.
694 * Please note that use of proper capabilities is always encouraged,
695 * this function is supposed to be used from core or for temporary hacks.
697 * @category access
699 * @param int|stdClass $user_or_id user id or user object
700 * @return bool true if user is one of the administrators, false otherwise
702 function is_siteadmin($user_or_id = null) {
703 global $CFG, $USER;
705 if ($user_or_id === null) {
706 $user_or_id = $USER;
709 if (empty($user_or_id)) {
710 return false;
712 if (!empty($user_or_id->id)) {
713 $userid = $user_or_id->id;
714 } else {
715 $userid = $user_or_id;
718 // Because this script is called many times (150+ for course page) with
719 // the same parameters, it is worth doing minor optimisations. This static
720 // cache stores the value for a single userid, saving about 2ms from course
721 // page load time without using significant memory. As the static cache
722 // also includes the value it depends on, this cannot break unit tests.
723 static $knownid, $knownresult, $knownsiteadmins;
724 if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
725 return $knownresult;
727 $knownid = $userid;
728 $knownsiteadmins = $CFG->siteadmins;
730 $siteadmins = explode(',', $CFG->siteadmins);
731 $knownresult = in_array($userid, $siteadmins);
732 return $knownresult;
736 * Returns true if user has at least one role assign
737 * of 'coursecontact' role (is potentially listed in some course descriptions).
739 * @param int $userid
740 * @return bool
742 function has_coursecontact_role($userid) {
743 global $DB, $CFG;
745 if (empty($CFG->coursecontact)) {
746 return false;
748 $sql = "SELECT 1
749 FROM {role_assignments}
750 WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
751 return $DB->record_exists_sql($sql, array('userid'=>$userid));
755 * Does the user have a capability to do something?
757 * Walk the accessdata array and return true/false.
758 * Deals with prohibits, role switching, aggregating
759 * capabilities, etc.
761 * The main feature of here is being FAST and with no
762 * side effects.
764 * Notes:
766 * Switch Role merges with default role
767 * ------------------------------------
768 * If you are a teacher in course X, you have at least
769 * teacher-in-X + defaultloggedinuser-sitewide. So in the
770 * course you'll have techer+defaultloggedinuser.
771 * We try to mimic that in switchrole.
773 * Permission evaluation
774 * ---------------------
775 * Originally there was an extremely complicated way
776 * to determine the user access that dealt with
777 * "locality" or role assignments and role overrides.
778 * Now we simply evaluate access for each role separately
779 * and then verify if user has at least one role with allow
780 * and at the same time no role with prohibit.
782 * @access private
783 * @param string $capability
784 * @param context $context
785 * @param array $accessdata
786 * @return bool
788 function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
789 global $CFG;
791 // Build $paths as a list of current + all parent "paths" with order bottom-to-top
792 $path = $context->path;
793 $paths = array($path);
794 while ($path = rtrim($path, '0123456789')) {
795 $path = rtrim($path, '/');
796 if ($path === '') {
797 break;
799 $paths[] = $path;
802 $roles = array();
803 $switchedrole = false;
805 // Find out if role switched
806 if (!empty($accessdata['rsw'])) {
807 // From the bottom up...
808 foreach ($paths as $path) {
809 if (isset($accessdata['rsw'][$path])) {
810 // Found a switchrole assignment - check for that role _plus_ the default user role
811 $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
812 $switchedrole = true;
813 break;
818 if (!$switchedrole) {
819 // get all users roles in this context and above
820 foreach ($paths as $path) {
821 if (isset($accessdata['ra'][$path])) {
822 foreach ($accessdata['ra'][$path] as $roleid) {
823 $roles[$roleid] = null;
829 // Now find out what access is given to each role, going bottom-->up direction
830 $rdefs = get_role_definitions(array_keys($roles));
831 $allowed = false;
833 foreach ($roles as $roleid => $ignored) {
834 foreach ($paths as $path) {
835 if (isset($rdefs[$roleid][$path][$capability])) {
836 $perm = (int)$rdefs[$roleid][$path][$capability];
837 if ($perm === CAP_PROHIBIT) {
838 // any CAP_PROHIBIT found means no permission for the user
839 return false;
841 if (is_null($roles[$roleid])) {
842 $roles[$roleid] = $perm;
846 // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
847 $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
850 return $allowed;
854 * A convenience function that tests has_capability, and displays an error if
855 * the user does not have that capability.
857 * NOTE before Moodle 2.0, this function attempted to make an appropriate
858 * require_login call before checking the capability. This is no longer the case.
859 * You must call require_login (or one of its variants) if you want to check the
860 * user is logged in, before you call this function.
862 * @see has_capability()
864 * @param string $capability the name of the capability to check. For example mod/forum:view
865 * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
866 * @param int $userid A user id. By default (null) checks the permissions of the current user.
867 * @param bool $doanything If false, ignore effect of admin role assignment
868 * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
869 * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
870 * @return void terminates with an error if the user does not have the given capability.
872 function require_capability($capability, context $context, $userid = null, $doanything = true,
873 $errormessage = 'nopermissions', $stringfile = '') {
874 if (!has_capability($capability, $context, $userid, $doanything)) {
875 throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
880 * A convenience function that tests has_capability for a list of capabilities, and displays an error if
881 * the user does not have that capability.
883 * This is just a utility method that calls has_capability in a loop. Try to put
884 * the capabilities that fewest users are likely to have first in the list for best
885 * performance.
887 * @category access
888 * @see has_capability()
890 * @param array $capabilities an array of capability names.
891 * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
892 * @param int $userid A user id. By default (null) checks the permissions of the current user.
893 * @param bool $doanything If false, ignore effect of admin role assignment
894 * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
895 * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
896 * @return void terminates with an error if the user does not have the given capability.
898 function require_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true,
899 $errormessage = 'nopermissions', $stringfile = ''): void {
900 foreach ($capabilities as $capability) {
901 if (!has_capability($capability, $context, $userid, $doanything)) {
902 throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
908 * Return a nested array showing all role assignments for the user.
909 * [ra] => [contextpath][roleid] = roleid
911 * @access private
912 * @param int $userid - the id of the user
913 * @return array access info array
915 function get_user_roles_sitewide_accessdata($userid) {
916 global $CFG, $DB;
918 $accessdata = get_empty_accessdata();
920 // start with the default role
921 if (!empty($CFG->defaultuserroleid)) {
922 $syscontext = context_system::instance();
923 $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
926 // load the "default frontpage role"
927 if (!empty($CFG->defaultfrontpageroleid)) {
928 $frontpagecontext = context_course::instance(get_site()->id);
929 if ($frontpagecontext->path) {
930 $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
934 // Preload every assigned role.
935 $sql = "SELECT ctx.path, ra.roleid, ra.contextid
936 FROM {role_assignments} ra
937 JOIN {context} ctx ON ctx.id = ra.contextid
938 WHERE ra.userid = :userid";
940 $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
942 foreach ($rs as $ra) {
943 // RAs leafs are arrays to support multi-role assignments...
944 $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
947 $rs->close();
949 return $accessdata;
953 * Returns empty accessdata structure.
955 * @access private
956 * @return array empt accessdata
958 function get_empty_accessdata() {
959 $accessdata = array(); // named list
960 $accessdata['ra'] = array();
961 $accessdata['time'] = time();
962 $accessdata['rsw'] = array();
964 return $accessdata;
968 * Get accessdata for a given user.
970 * @access private
971 * @param int $userid
972 * @param bool $preloadonly true means do not return access array
973 * @return array accessdata
975 function get_user_accessdata($userid, $preloadonly=false) {
976 global $CFG, $ACCESSLIB_PRIVATE, $USER;
978 if (isset($USER->access)) {
979 $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
982 if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
983 if (empty($userid)) {
984 if (!empty($CFG->notloggedinroleid)) {
985 $accessdata = get_role_access($CFG->notloggedinroleid);
986 } else {
987 // weird
988 return get_empty_accessdata();
991 } else if (isguestuser($userid)) {
992 if ($guestrole = get_guest_role()) {
993 $accessdata = get_role_access($guestrole->id);
994 } else {
995 //weird
996 return get_empty_accessdata();
999 } else {
1000 // Includes default role and frontpage role.
1001 $accessdata = get_user_roles_sitewide_accessdata($userid);
1004 $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
1007 if ($preloadonly) {
1008 return;
1009 } else {
1010 return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
1015 * A convenience function to completely load all the capabilities
1016 * for the current user. It is called from has_capability() and functions change permissions.
1018 * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
1019 * @see check_enrolment_plugins()
1021 * @access private
1022 * @return void
1024 function load_all_capabilities() {
1025 global $USER;
1027 // roles not installed yet - we are in the middle of installation
1028 if (during_initial_install()) {
1029 return;
1032 if (!isset($USER->id)) {
1033 // this should not happen
1034 $USER->id = 0;
1037 unset($USER->access);
1038 $USER->access = get_user_accessdata($USER->id);
1040 // Clear to force a refresh
1041 unset($USER->mycourses);
1043 // init/reset internal enrol caches - active course enrolments and temp access
1044 $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
1048 * A convenience function to completely reload all the capabilities
1049 * for the current user when roles have been updated in a relevant
1050 * context -- but PRESERVING switchroles and loginas.
1051 * This function resets all accesslib and context caches.
1053 * That is - completely transparent to the user.
1055 * Note: reloads $USER->access completely.
1057 * @access private
1058 * @return void
1060 function reload_all_capabilities() {
1061 global $USER, $DB, $ACCESSLIB_PRIVATE;
1063 // copy switchroles
1064 $sw = array();
1065 if (!empty($USER->access['rsw'])) {
1066 $sw = $USER->access['rsw'];
1069 accesslib_clear_all_caches(true);
1070 unset($USER->access);
1072 // Prevent dirty flags refetching on this page.
1073 $ACCESSLIB_PRIVATE->dirtycontexts = array();
1074 $ACCESSLIB_PRIVATE->dirtyusers = array($USER->id => false);
1076 load_all_capabilities();
1078 foreach ($sw as $path => $roleid) {
1079 if ($record = $DB->get_record('context', array('path'=>$path))) {
1080 $context = context::instance_by_id($record->id);
1081 if (has_capability('moodle/role:switchroles', $context)) {
1082 role_switch($roleid, $context);
1089 * Adds a temp role to current USER->access array.
1091 * Useful for the "temporary guest" access we grant to logged-in users.
1092 * This is useful for enrol plugins only.
1094 * @since Moodle 2.2
1095 * @param context_course $coursecontext
1096 * @param int $roleid
1097 * @return void
1099 function load_temp_course_role(context_course $coursecontext, $roleid) {
1100 global $USER, $SITE;
1102 if (empty($roleid)) {
1103 debugging('invalid role specified in load_temp_course_role()');
1104 return;
1107 if ($coursecontext->instanceid == $SITE->id) {
1108 debugging('Can not use temp roles on the frontpage');
1109 return;
1112 if (!isset($USER->access)) {
1113 load_all_capabilities();
1116 $coursecontext->reload_if_dirty();
1118 if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
1119 return;
1122 $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
1126 * Removes any extra guest roles from current USER->access array.
1127 * This is useful for enrol plugins only.
1129 * @since Moodle 2.2
1130 * @param context_course $coursecontext
1131 * @return void
1133 function remove_temp_course_roles(context_course $coursecontext) {
1134 global $DB, $USER, $SITE;
1136 if ($coursecontext->instanceid == $SITE->id) {
1137 debugging('Can not use temp roles on the frontpage');
1138 return;
1141 if (empty($USER->access['ra'][$coursecontext->path])) {
1142 //no roles here, weird
1143 return;
1146 $sql = "SELECT DISTINCT ra.roleid AS id
1147 FROM {role_assignments} ra
1148 WHERE ra.contextid = :contextid AND ra.userid = :userid";
1149 $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
1151 $USER->access['ra'][$coursecontext->path] = array();
1152 foreach ($ras as $r) {
1153 $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
1158 * Returns array of all role archetypes.
1160 * @return array
1162 function get_role_archetypes() {
1163 return array(
1164 'manager' => 'manager',
1165 'coursecreator' => 'coursecreator',
1166 'editingteacher' => 'editingteacher',
1167 'teacher' => 'teacher',
1168 'student' => 'student',
1169 'guest' => 'guest',
1170 'user' => 'user',
1171 'frontpage' => 'frontpage'
1176 * Assign the defaults found in this capability definition to roles that have
1177 * the corresponding legacy capabilities assigned to them.
1179 * @param string $capability
1180 * @param array $legacyperms an array in the format (example):
1181 * 'guest' => CAP_PREVENT,
1182 * 'student' => CAP_ALLOW,
1183 * 'teacher' => CAP_ALLOW,
1184 * 'editingteacher' => CAP_ALLOW,
1185 * 'coursecreator' => CAP_ALLOW,
1186 * 'manager' => CAP_ALLOW
1187 * @return boolean success or failure.
1189 function assign_legacy_capabilities($capability, $legacyperms) {
1191 $archetypes = get_role_archetypes();
1193 foreach ($legacyperms as $type => $perm) {
1195 $systemcontext = context_system::instance();
1196 if ($type === 'admin') {
1197 debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1198 $type = 'manager';
1201 if (!array_key_exists($type, $archetypes)) {
1202 throw new \moodle_exception('invalidlegacy', '', '', $type);
1205 if ($roles = get_archetype_roles($type)) {
1206 foreach ($roles as $role) {
1207 // Assign a site level capability.
1208 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1209 return false;
1214 return true;
1218 * Verify capability risks.
1220 * @param stdClass $capability a capability - a row from the capabilities table.
1221 * @return boolean whether this capability is safe - that is, whether people with the
1222 * safeoverrides capability should be allowed to change it.
1224 function is_safe_capability($capability) {
1225 return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1229 * Get the local override (if any) for a given capability in a role in a context
1231 * @param int $roleid
1232 * @param int $contextid
1233 * @param string $capability
1234 * @return stdClass local capability override
1236 function get_local_override($roleid, $contextid, $capability) {
1237 global $DB;
1239 return $DB->get_record_sql("
1240 SELECT rc.*
1241 FROM {role_capabilities} rc
1242 JOIN {capability} cap ON rc.capability = cap.name
1243 WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
1244 'roleid' => $roleid,
1245 'contextid' => $contextid,
1246 'capability' => $capability,
1252 * Returns context instance plus related course and cm instances
1254 * @param int $contextid
1255 * @return array of ($context, $course, $cm)
1257 function get_context_info_array($contextid) {
1258 global $DB;
1260 $context = context::instance_by_id($contextid, MUST_EXIST);
1261 $course = null;
1262 $cm = null;
1264 if ($context->contextlevel == CONTEXT_COURSE) {
1265 $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
1267 } else if ($context->contextlevel == CONTEXT_MODULE) {
1268 $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
1269 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1271 } else if ($context->contextlevel == CONTEXT_BLOCK) {
1272 $parent = $context->get_parent_context();
1274 if ($parent->contextlevel == CONTEXT_COURSE) {
1275 $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
1276 } else if ($parent->contextlevel == CONTEXT_MODULE) {
1277 $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
1278 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1282 return array($context, $course, $cm);
1286 * Function that creates a role
1288 * @param string $name role name
1289 * @param string $shortname role short name
1290 * @param string $description role description
1291 * @param string $archetype
1292 * @return int id or dml_exception
1294 function create_role($name, $shortname, $description, $archetype = '') {
1295 global $DB;
1297 if (strpos($archetype, 'moodle/legacy:') !== false) {
1298 throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
1301 // verify role archetype actually exists
1302 $archetypes = get_role_archetypes();
1303 if (empty($archetypes[$archetype])) {
1304 $archetype = '';
1307 // Insert the role record.
1308 $role = new stdClass();
1309 $role->name = $name;
1310 $role->shortname = $shortname;
1311 $role->description = $description;
1312 $role->archetype = $archetype;
1314 //find free sortorder number
1315 $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
1316 if (empty($role->sortorder)) {
1317 $role->sortorder = 1;
1319 $id = $DB->insert_record('role', $role);
1321 return $id;
1325 * Function that deletes a role and cleanups up after it
1327 * @param int $roleid id of role to delete
1328 * @return bool always true
1330 function delete_role($roleid) {
1331 global $DB;
1333 // first unssign all users
1334 role_unassign_all(array('roleid'=>$roleid));
1336 // cleanup all references to this role, ignore errors
1337 $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
1338 $DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
1339 $DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
1340 $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
1341 $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
1342 $DB->delete_records('role_names', array('roleid'=>$roleid));
1343 $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
1345 // Get role record before it's deleted.
1346 $role = $DB->get_record('role', array('id'=>$roleid));
1348 // Finally delete the role itself.
1349 $DB->delete_records('role', array('id'=>$roleid));
1351 // Trigger event.
1352 $event = \core\event\role_deleted::create(
1353 array(
1354 'context' => context_system::instance(),
1355 'objectid' => $roleid,
1356 'other' =>
1357 array(
1358 'shortname' => $role->shortname,
1359 'description' => $role->description,
1360 'archetype' => $role->archetype
1364 $event->add_record_snapshot('role', $role);
1365 $event->trigger();
1367 // Reset any cache of this role, including MUC.
1368 accesslib_clear_role_cache($roleid);
1370 return true;
1374 * Function to write context specific overrides, or default capabilities.
1376 * The $performancehints array can currently contain two values intended to make this faster when
1377 * this function is being called in a loop, if you have already checked certain details:
1378 * 'contextexists' - if we already know the contextid exists in context table
1379 * ASSIGN_HINT_NO_EXISTING - if we already know there is no entry in role_capabilities matching
1380 * contextid, roleid, and capability
1382 * @param string $capability string name
1383 * @param int $permission CAP_ constants
1384 * @param int $roleid role id
1385 * @param int|context $contextid context id
1386 * @param bool $overwrite
1387 * @param string[] $performancehints Performance hints - leave blank unless needed
1388 * @return bool always true or exception
1390 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false, array $performancehints = []) {
1391 global $USER, $DB;
1393 if ($contextid instanceof context) {
1394 $context = $contextid;
1395 } else {
1396 $context = context::instance_by_id($contextid);
1399 // Capability must exist.
1400 if (!$capinfo = get_capability_info($capability)) {
1401 throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1404 if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
1405 unassign_capability($capability, $roleid, $context->id);
1406 return true;
1409 if (in_array(ACCESSLIB_HINT_NO_EXISTING, $performancehints)) {
1410 $existing = false;
1411 } else {
1412 $existing = $DB->get_record('role_capabilities',
1413 ['contextid' => $context->id, 'roleid' => $roleid, 'capability' => $capability]);
1416 if ($existing and !$overwrite) { // We want to keep whatever is there already
1417 return true;
1420 $cap = new stdClass();
1421 $cap->contextid = $context->id;
1422 $cap->roleid = $roleid;
1423 $cap->capability = $capability;
1424 $cap->permission = $permission;
1425 $cap->timemodified = time();
1426 $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
1428 if ($existing) {
1429 $cap->id = $existing->id;
1430 $DB->update_record('role_capabilities', $cap);
1431 } else {
1432 if (in_array(ACCESSLIB_HINT_CONTEXT_EXISTS, $performancehints) ||
1433 $DB->record_exists('context', ['id' => $context->id])) {
1434 $DB->insert_record('role_capabilities', $cap);
1438 // Trigger capability_assigned event.
1439 \core\event\capability_assigned::create([
1440 'userid' => $cap->modifierid,
1441 'context' => $context,
1442 'objectid' => $roleid,
1443 'other' => [
1444 'capability' => $capability,
1445 'oldpermission' => $existing->permission ?? CAP_INHERIT,
1446 'permission' => $permission
1448 ])->trigger();
1450 // Reset any cache of this role, including MUC.
1451 accesslib_clear_role_cache($roleid);
1453 return true;
1457 * Unassign a capability from a role.
1459 * @param string $capability the name of the capability
1460 * @param int $roleid the role id
1461 * @param int|context $contextid null means all contexts
1462 * @return boolean true or exception
1464 function unassign_capability($capability, $roleid, $contextid = null) {
1465 global $DB, $USER;
1467 // Capability must exist.
1468 if (!$capinfo = get_capability_info($capability)) {
1469 throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1472 if (!empty($contextid)) {
1473 if ($contextid instanceof context) {
1474 $context = $contextid;
1475 } else {
1476 $context = context::instance_by_id($contextid);
1478 // delete from context rel, if this is the last override in this context
1479 $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
1480 } else {
1481 $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
1484 // Trigger capability_assigned event.
1485 \core\event\capability_unassigned::create([
1486 'userid' => $USER->id,
1487 'context' => $context ?? context_system::instance(),
1488 'objectid' => $roleid,
1489 'other' => [
1490 'capability' => $capability,
1492 ])->trigger();
1494 // Reset any cache of this role, including MUC.
1495 accesslib_clear_role_cache($roleid);
1497 return true;
1501 * Get the roles that have a given capability assigned to it
1503 * This function does not resolve the actual permission of the capability.
1504 * It just checks for permissions and overrides.
1505 * Use get_roles_with_cap_in_context() if resolution is required.
1507 * @param string $capability capability name (string)
1508 * @param string $permission optional, the permission defined for this capability
1509 * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
1510 * @param stdClass $context null means any
1511 * @return array of role records
1513 function get_roles_with_capability($capability, $permission = null, $context = null) {
1514 global $DB;
1516 if ($context) {
1517 $contexts = $context->get_parent_context_ids(true);
1518 list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
1519 $contextsql = "AND rc.contextid $insql";
1520 } else {
1521 $params = array();
1522 $contextsql = '';
1525 if ($permission) {
1526 $permissionsql = " AND rc.permission = :permission";
1527 $params['permission'] = $permission;
1528 } else {
1529 $permissionsql = '';
1532 $sql = "SELECT r.*
1533 FROM {role} r
1534 WHERE r.id IN (SELECT rc.roleid
1535 FROM {role_capabilities} rc
1536 JOIN {capabilities} cap ON rc.capability = cap.name
1537 WHERE rc.capability = :capname
1538 $contextsql
1539 $permissionsql)";
1540 $params['capname'] = $capability;
1543 return $DB->get_records_sql($sql, $params);
1547 * This function makes a role-assignment (a role for a user in a particular context)
1549 * @param int $roleid the role of the id
1550 * @param int $userid userid
1551 * @param int|context $contextid id of the context
1552 * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
1553 * @param int $itemid id of enrolment/auth plugin
1554 * @param string $timemodified defaults to current time
1555 * @return int new/existing id of the assignment
1557 function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
1558 global $USER, $DB;
1560 // first of all detect if somebody is using old style parameters
1561 if ($contextid === 0 or is_numeric($component)) {
1562 throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
1565 // now validate all parameters
1566 if (empty($roleid)) {
1567 throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
1570 if (empty($userid)) {
1571 throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
1574 if ($itemid) {
1575 if (strpos($component, '_') === false) {
1576 throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
1578 } else {
1579 $itemid = 0;
1580 if ($component !== '' and strpos($component, '_') === false) {
1581 throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1585 if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
1586 throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
1589 if ($contextid instanceof context) {
1590 $context = $contextid;
1591 } else {
1592 $context = context::instance_by_id($contextid, MUST_EXIST);
1595 if (!$timemodified) {
1596 $timemodified = time();
1599 // Check for existing entry
1600 $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
1602 if ($ras) {
1603 // role already assigned - this should not happen
1604 if (count($ras) > 1) {
1605 // very weird - remove all duplicates!
1606 $ra = array_shift($ras);
1607 foreach ($ras as $r) {
1608 $DB->delete_records('role_assignments', array('id'=>$r->id));
1610 } else {
1611 $ra = reset($ras);
1614 // actually there is no need to update, reset anything or trigger any event, so just return
1615 return $ra->id;
1618 // Create a new entry
1619 $ra = new stdClass();
1620 $ra->roleid = $roleid;
1621 $ra->contextid = $context->id;
1622 $ra->userid = $userid;
1623 $ra->component = $component;
1624 $ra->itemid = $itemid;
1625 $ra->timemodified = $timemodified;
1626 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
1627 $ra->sortorder = 0;
1629 $ra->id = $DB->insert_record('role_assignments', $ra);
1631 // Role assignments have changed, so mark user as dirty.
1632 mark_user_dirty($userid);
1634 core_course_category::role_assignment_changed($roleid, $context);
1636 $event = \core\event\role_assigned::create(array(
1637 'context' => $context,
1638 'objectid' => $ra->roleid,
1639 'relateduserid' => $ra->userid,
1640 'other' => array(
1641 'id' => $ra->id,
1642 'component' => $ra->component,
1643 'itemid' => $ra->itemid
1646 $event->add_record_snapshot('role_assignments', $ra);
1647 $event->trigger();
1649 return $ra->id;
1653 * Removes one role assignment
1655 * @param int $roleid
1656 * @param int $userid
1657 * @param int $contextid
1658 * @param string $component
1659 * @param int $itemid
1660 * @return void
1662 function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
1663 // first make sure the params make sense
1664 if ($roleid == 0 or $userid == 0 or $contextid == 0) {
1665 throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
1668 if ($itemid) {
1669 if (strpos($component, '_') === false) {
1670 throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
1672 } else {
1673 $itemid = 0;
1674 if ($component !== '' and strpos($component, '_') === false) {
1675 throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1679 role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
1683 * Removes multiple role assignments, parameters may contain:
1684 * 'roleid', 'userid', 'contextid', 'component', 'enrolid'.
1686 * @param array $params role assignment parameters
1687 * @param bool $subcontexts unassign in subcontexts too
1688 * @param bool $includemanual include manual role assignments too
1689 * @return void
1691 function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
1692 global $USER, $CFG, $DB;
1694 if (!$params) {
1695 throw new coding_exception('Missing parameters in role_unsassign_all() call');
1698 $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
1699 foreach ($params as $key=>$value) {
1700 if (!in_array($key, $allowed)) {
1701 throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
1705 if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
1706 throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
1709 if ($includemanual) {
1710 if (!isset($params['component']) or $params['component'] === '') {
1711 throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
1715 if ($subcontexts) {
1716 if (empty($params['contextid'])) {
1717 throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
1721 $ras = $DB->get_records('role_assignments', $params);
1722 foreach ($ras as $ra) {
1723 $DB->delete_records('role_assignments', array('id'=>$ra->id));
1724 if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
1725 // Role assignments have changed, so mark user as dirty.
1726 mark_user_dirty($ra->userid);
1728 $event = \core\event\role_unassigned::create(array(
1729 'context' => $context,
1730 'objectid' => $ra->roleid,
1731 'relateduserid' => $ra->userid,
1732 'other' => array(
1733 'id' => $ra->id,
1734 'component' => $ra->component,
1735 'itemid' => $ra->itemid
1738 $event->add_record_snapshot('role_assignments', $ra);
1739 $event->trigger();
1740 core_course_category::role_assignment_changed($ra->roleid, $context);
1743 unset($ras);
1745 // process subcontexts
1746 if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
1747 if ($params['contextid'] instanceof context) {
1748 $context = $params['contextid'];
1749 } else {
1750 $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
1753 if ($context) {
1754 $contexts = $context->get_child_contexts();
1755 $mparams = $params;
1756 foreach ($contexts as $context) {
1757 $mparams['contextid'] = $context->id;
1758 $ras = $DB->get_records('role_assignments', $mparams);
1759 foreach ($ras as $ra) {
1760 $DB->delete_records('role_assignments', array('id'=>$ra->id));
1761 // Role assignments have changed, so mark user as dirty.
1762 mark_user_dirty($ra->userid);
1764 $event = \core\event\role_unassigned::create(
1765 array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1766 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1767 $event->add_record_snapshot('role_assignments', $ra);
1768 $event->trigger();
1769 core_course_category::role_assignment_changed($ra->roleid, $context);
1775 // do this once more for all manual role assignments
1776 if ($includemanual) {
1777 $params['component'] = '';
1778 role_unassign_all($params, $subcontexts, false);
1783 * Mark a user as dirty (with timestamp) so as to force reloading of the user session.
1785 * @param int $userid
1786 * @return void
1788 function mark_user_dirty($userid) {
1789 global $CFG, $ACCESSLIB_PRIVATE;
1791 if (during_initial_install()) {
1792 return;
1795 // Throw exception if invalid userid is provided.
1796 if (empty($userid)) {
1797 throw new coding_exception('Invalid user parameter supplied for mark_user_dirty() function!');
1800 // Set dirty flag in database, set dirty field locally, and clear local accessdata cache.
1801 set_cache_flag('accesslib/dirtyusers', $userid, 1, time() + $CFG->sessiontimeout);
1802 $ACCESSLIB_PRIVATE->dirtyusers[$userid] = 1;
1803 unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
1807 * Determines if a user is currently logged in
1809 * @category access
1811 * @return bool
1813 function isloggedin() {
1814 global $USER;
1816 return (!empty($USER->id));
1820 * Determines if a user is logged in as real guest user with username 'guest'.
1822 * @category access
1824 * @param int|object $user mixed user object or id, $USER if not specified
1825 * @return bool true if user is the real guest user, false if not logged in or other user
1827 function isguestuser($user = null) {
1828 global $USER, $DB, $CFG;
1830 // make sure we have the user id cached in config table, because we are going to use it a lot
1831 if (empty($CFG->siteguest)) {
1832 if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
1833 // guest does not exist yet, weird
1834 return false;
1836 set_config('siteguest', $guestid);
1838 if ($user === null) {
1839 $user = $USER;
1842 if ($user === null) {
1843 // happens when setting the $USER
1844 return false;
1846 } else if (is_numeric($user)) {
1847 return ($CFG->siteguest == $user);
1849 } else if (is_object($user)) {
1850 if (empty($user->id)) {
1851 return false; // not logged in means is not be guest
1852 } else {
1853 return ($CFG->siteguest == $user->id);
1856 } else {
1857 throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
1862 * Does user have a (temporary or real) guest access to course?
1864 * @category access
1866 * @param context $context
1867 * @param stdClass|int $user
1868 * @return bool
1870 function is_guest(context $context, $user = null) {
1871 global $USER;
1873 // first find the course context
1874 $coursecontext = $context->get_course_context();
1876 // make sure there is a real user specified
1877 if ($user === null) {
1878 $userid = isset($USER->id) ? $USER->id : 0;
1879 } else {
1880 $userid = is_object($user) ? $user->id : $user;
1883 if (isguestuser($userid)) {
1884 // can not inspect or be enrolled
1885 return true;
1888 if (has_capability('moodle/course:view', $coursecontext, $user)) {
1889 // viewing users appear out of nowhere, they are neither guests nor participants
1890 return false;
1893 // consider only real active enrolments here
1894 if (is_enrolled($coursecontext, $user, '', true)) {
1895 return false;
1898 return true;
1902 * Returns true if the user has moodle/course:view capability in the course,
1903 * this is intended for admins, managers (aka small admins), inspectors, etc.
1905 * @category access
1907 * @param context $context
1908 * @param int|stdClass $user if null $USER is used
1909 * @param string $withcapability extra capability name
1910 * @return bool
1912 function is_viewing(context $context, $user = null, $withcapability = '') {
1913 // first find the course context
1914 $coursecontext = $context->get_course_context();
1916 if (isguestuser($user)) {
1917 // can not inspect
1918 return false;
1921 if (!has_capability('moodle/course:view', $coursecontext, $user)) {
1922 // admins are allowed to inspect courses
1923 return false;
1926 if ($withcapability and !has_capability($withcapability, $context, $user)) {
1927 // site admins always have the capability, but the enrolment above blocks
1928 return false;
1931 return true;
1935 * Returns true if the user is able to access the course.
1937 * This function is in no way, shape, or form a substitute for require_login.
1938 * It should only be used in circumstances where it is not possible to call require_login
1939 * such as the navigation.
1941 * This function checks many of the methods of access to a course such as the view
1942 * capability, enrollments, and guest access. It also makes use of the cache
1943 * generated by require_login for guest access.
1945 * The flags within the $USER object that are used here should NEVER be used outside
1946 * of this function can_access_course and require_login. Doing so WILL break future
1947 * versions.
1949 * @param stdClass $course record
1950 * @param stdClass|int|null $user user record or id, current user if null
1951 * @param string $withcapability Check for this capability as well.
1952 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1953 * @return boolean Returns true if the user is able to access the course
1955 function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
1956 global $DB, $USER;
1958 // this function originally accepted $coursecontext parameter
1959 if ($course instanceof context) {
1960 if ($course instanceof context_course) {
1961 debugging('deprecated context parameter, please use $course record');
1962 $coursecontext = $course;
1963 $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
1964 } else {
1965 debugging('Invalid context parameter, please use $course record');
1966 return false;
1968 } else {
1969 $coursecontext = context_course::instance($course->id);
1972 if (!isset($USER->id)) {
1973 // should never happen
1974 $USER->id = 0;
1975 debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
1978 // make sure there is a user specified
1979 if ($user === null) {
1980 $userid = $USER->id;
1981 } else {
1982 $userid = is_object($user) ? $user->id : $user;
1984 unset($user);
1986 if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
1987 return false;
1990 if ($userid == $USER->id) {
1991 if (!empty($USER->access['rsw'][$coursecontext->path])) {
1992 // the fact that somebody switched role means they can access the course no matter to what role they switched
1993 return true;
1997 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
1998 return false;
2001 if (is_viewing($coursecontext, $userid)) {
2002 return true;
2005 if ($userid != $USER->id) {
2006 // for performance reasons we do not verify temporary guest access for other users, sorry...
2007 return is_enrolled($coursecontext, $userid, '', $onlyactive);
2010 // === from here we deal only with $USER ===
2012 $coursecontext->reload_if_dirty();
2014 if (isset($USER->enrol['enrolled'][$course->id])) {
2015 if ($USER->enrol['enrolled'][$course->id] > time()) {
2016 return true;
2019 if (isset($USER->enrol['tempguest'][$course->id])) {
2020 if ($USER->enrol['tempguest'][$course->id] > time()) {
2021 return true;
2025 if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
2026 return true;
2029 if (!core_course_category::can_view_course_info($course)) {
2030 // No guest access if user does not have capability to browse courses.
2031 return false;
2034 // if not enrolled try to gain temporary guest access
2035 $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
2036 $enrols = enrol_get_plugins(true);
2037 foreach ($instances as $instance) {
2038 if (!isset($enrols[$instance->enrol])) {
2039 continue;
2041 // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
2042 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2043 if ($until !== false and $until > time()) {
2044 $USER->enrol['tempguest'][$course->id] = $until;
2045 return true;
2048 if (isset($USER->enrol['tempguest'][$course->id])) {
2049 unset($USER->enrol['tempguest'][$course->id]);
2050 remove_temp_course_roles($coursecontext);
2053 return false;
2057 * Loads the capability definitions for the component (from file).
2059 * Loads the capability definitions for the component (from file). If no
2060 * capabilities are defined for the component, we simply return an empty array.
2062 * @access private
2063 * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
2064 * @return array array of capabilities
2066 function load_capability_def($component) {
2067 $defpath = core_component::get_component_directory($component).'/db/access.php';
2069 $capabilities = array();
2070 if (file_exists($defpath)) {
2071 require($defpath);
2072 if (!empty(${$component.'_capabilities'})) {
2073 // BC capability array name
2074 // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
2075 debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
2076 $capabilities = ${$component.'_capabilities'};
2080 return $capabilities;
2084 * Gets the capabilities that have been cached in the database for this component.
2086 * @access private
2087 * @param string $component - examples: 'moodle', 'mod_forum'
2088 * @return array array of capabilities
2090 function get_cached_capabilities($component = 'moodle') {
2091 global $DB;
2092 $caps = get_all_capabilities();
2093 $componentcaps = array();
2094 foreach ($caps as $cap) {
2095 if ($cap['component'] == $component) {
2096 $componentcaps[] = (object) $cap;
2099 return $componentcaps;
2103 * Returns default capabilities for given role archetype.
2105 * @param string $archetype role archetype
2106 * @return array
2108 function get_default_capabilities($archetype) {
2109 global $DB;
2111 if (!$archetype) {
2112 return array();
2115 $alldefs = array();
2116 $defaults = array();
2117 $components = array();
2118 $allcaps = get_all_capabilities();
2120 foreach ($allcaps as $cap) {
2121 if (!in_array($cap['component'], $components)) {
2122 $components[] = $cap['component'];
2123 $alldefs = array_merge($alldefs, load_capability_def($cap['component']));
2126 foreach ($alldefs as $name=>$def) {
2127 // Use array 'archetypes if available. Only if not specified, use 'legacy'.
2128 if (isset($def['archetypes'])) {
2129 if (isset($def['archetypes'][$archetype])) {
2130 $defaults[$name] = $def['archetypes'][$archetype];
2132 // 'legacy' is for backward compatibility with 1.9 access.php
2133 } else {
2134 if (isset($def['legacy'][$archetype])) {
2135 $defaults[$name] = $def['legacy'][$archetype];
2140 return $defaults;
2144 * Return default roles that can be assigned, overridden or switched
2145 * by give role archetype.
2147 * @param string $type assign|override|switch|view
2148 * @param string $archetype
2149 * @return array of role ids
2151 function get_default_role_archetype_allows($type, $archetype) {
2152 global $DB;
2154 if (empty($archetype)) {
2155 return array();
2158 $roles = $DB->get_records('role');
2159 $archetypemap = array();
2160 foreach ($roles as $role) {
2161 if ($role->archetype) {
2162 $archetypemap[$role->archetype][$role->id] = $role->id;
2166 $defaults = array(
2167 'assign' => array(
2168 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
2169 'coursecreator' => array(),
2170 'editingteacher' => array('teacher', 'student'),
2171 'teacher' => array(),
2172 'student' => array(),
2173 'guest' => array(),
2174 'user' => array(),
2175 'frontpage' => array(),
2177 'override' => array(
2178 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2179 'coursecreator' => array(),
2180 'editingteacher' => array('teacher', 'student', 'guest'),
2181 'teacher' => array(),
2182 'student' => array(),
2183 'guest' => array(),
2184 'user' => array(),
2185 'frontpage' => array(),
2187 'switch' => array(
2188 'manager' => array('editingteacher', 'teacher', 'student', 'guest'),
2189 'coursecreator' => array(),
2190 'editingteacher' => array('teacher', 'student', 'guest'),
2191 'teacher' => array('student', 'guest'),
2192 'student' => array(),
2193 'guest' => array(),
2194 'user' => array(),
2195 'frontpage' => array(),
2197 'view' => array(
2198 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2199 'coursecreator' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2200 'editingteacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2201 'teacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2202 'student' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2203 'guest' => array(),
2204 'user' => array(),
2205 'frontpage' => array(),
2209 if (!isset($defaults[$type][$archetype])) {
2210 debugging("Unknown type '$type'' or archetype '$archetype''");
2211 return array();
2214 $return = array();
2215 foreach ($defaults[$type][$archetype] as $at) {
2216 if (isset($archetypemap[$at])) {
2217 foreach ($archetypemap[$at] as $roleid) {
2218 $return[$roleid] = $roleid;
2223 return $return;
2227 * Reset role capabilities to default according to selected role archetype.
2228 * If no archetype selected, removes all capabilities.
2230 * This applies to capabilities that are assigned to the role (that you could
2231 * edit in the 'define roles' interface), and not to any capability overrides
2232 * in different locations.
2234 * @param int $roleid ID of role to reset capabilities for
2236 function reset_role_capabilities($roleid) {
2237 global $DB;
2239 $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
2240 $defaultcaps = get_default_capabilities($role->archetype);
2242 $systemcontext = context_system::instance();
2244 $DB->delete_records('role_capabilities',
2245 array('roleid' => $roleid, 'contextid' => $systemcontext->id));
2247 foreach ($defaultcaps as $cap=>$permission) {
2248 assign_capability($cap, $permission, $roleid, $systemcontext->id);
2251 // Reset any cache of this role, including MUC.
2252 accesslib_clear_role_cache($roleid);
2256 * Updates the capabilities table with the component capability definitions.
2257 * If no parameters are given, the function updates the core moodle
2258 * capabilities.
2260 * Note that the absence of the db/access.php capabilities definition file
2261 * will cause any stored capabilities for the component to be removed from
2262 * the database.
2264 * @access private
2265 * @param string $component examples: 'moodle', 'mod_forum', 'block_activity_results'
2266 * @return boolean true if success, exception in case of any problems
2268 function update_capabilities($component = 'moodle') {
2269 global $DB, $OUTPUT;
2271 // Allow temporary caches to be used during install, dramatically boosting performance.
2272 $token = new \core_cache\allow_temporary_caches();
2274 $storedcaps = array();
2276 $filecaps = load_capability_def($component);
2277 foreach ($filecaps as $capname=>$unused) {
2278 if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
2279 debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
2283 // It is possible somebody directly modified the DB (according to accesslib_test anyway).
2284 // So ensure our updating is based on fresh data.
2285 cache::make('core', 'capabilities')->delete('core_capabilities');
2287 $cachedcaps = get_cached_capabilities($component);
2288 if ($cachedcaps) {
2289 foreach ($cachedcaps as $cachedcap) {
2290 array_push($storedcaps, $cachedcap->name);
2291 // update risk bitmasks and context levels in existing capabilities if needed
2292 if (array_key_exists($cachedcap->name, $filecaps)) {
2293 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
2294 $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
2296 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
2297 $updatecap = new stdClass();
2298 $updatecap->id = $cachedcap->id;
2299 $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
2300 $DB->update_record('capabilities', $updatecap);
2302 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
2303 $updatecap = new stdClass();
2304 $updatecap->id = $cachedcap->id;
2305 $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
2306 $DB->update_record('capabilities', $updatecap);
2309 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
2310 $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
2312 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
2313 $updatecap = new stdClass();
2314 $updatecap->id = $cachedcap->id;
2315 $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
2316 $DB->update_record('capabilities', $updatecap);
2322 // Flush the cached again, as we have changed DB.
2323 cache::make('core', 'capabilities')->delete('core_capabilities');
2325 // Are there new capabilities in the file definition?
2326 $newcaps = array();
2328 foreach ($filecaps as $filecap => $def) {
2329 if (!$storedcaps ||
2330 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
2331 if (!array_key_exists('riskbitmask', $def)) {
2332 $def['riskbitmask'] = 0; // no risk if not specified
2334 $newcaps[$filecap] = $def;
2337 // Add new capabilities to the stored definition.
2338 $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
2339 $capabilityobjects = [];
2340 foreach ($newcaps as $capname => $capdef) {
2341 $capability = new stdClass();
2342 $capability->name = $capname;
2343 $capability->captype = $capdef['captype'];
2344 $capability->contextlevel = $capdef['contextlevel'];
2345 $capability->component = $component;
2346 $capability->riskbitmask = $capdef['riskbitmask'];
2347 $capabilityobjects[] = $capability;
2349 $DB->insert_records('capabilities', $capabilityobjects);
2351 // Flush the cache, as we have changed DB.
2352 cache::make('core', 'capabilities')->delete('core_capabilities');
2354 foreach ($newcaps as $capname => $capdef) {
2355 if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
2356 if ($rolecapabilities = $DB->get_records_sql('
2357 SELECT rc.*,
2358 CASE WHEN EXISTS(SELECT 1
2359 FROM {role_capabilities} rc2
2360 WHERE rc2.capability = ?
2361 AND rc2.contextid = rc.contextid
2362 AND rc2.roleid = rc.roleid) THEN 1 ELSE 0 END AS entryexists,
2363 ' . context_helper::get_preload_record_columns_sql('x') .'
2364 FROM {role_capabilities} rc
2365 JOIN {context} x ON x.id = rc.contextid
2366 WHERE rc.capability = ?',
2367 [$capname, $capdef['clonepermissionsfrom']])) {
2368 foreach ($rolecapabilities as $rolecapability) {
2369 // Preload the context and add performance hints based on the SQL query above.
2370 context_helper::preload_from_record($rolecapability);
2371 $performancehints = [ACCESSLIB_HINT_CONTEXT_EXISTS];
2372 if (!$rolecapability->entryexists) {
2373 $performancehints[] = ACCESSLIB_HINT_NO_EXISTING;
2375 //assign_capability will update rather than insert if capability exists
2376 if (!assign_capability($capname, $rolecapability->permission,
2377 $rolecapability->roleid, $rolecapability->contextid, true, $performancehints)) {
2378 echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
2382 // we ignore archetype key if we have cloned permissions
2383 } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
2384 assign_legacy_capabilities($capname, $capdef['archetypes']);
2385 // 'legacy' is for backward compatibility with 1.9 access.php
2386 } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
2387 assign_legacy_capabilities($capname, $capdef['legacy']);
2390 // Are there any capabilities that have been removed from the file
2391 // definition that we need to delete from the stored capabilities and
2392 // role assignments?
2393 capabilities_cleanup($component, $filecaps);
2395 // reset static caches
2396 accesslib_reset_role_cache();
2398 // Flush the cached again, as we have changed DB.
2399 cache::make('core', 'capabilities')->delete('core_capabilities');
2401 return true;
2405 * Deletes cached capabilities that are no longer needed by the component.
2406 * Also unassigns these capabilities from any roles that have them.
2407 * NOTE: this function is called from lib/db/upgrade.php
2409 * @access private
2410 * @param string $component examples: 'moodle', 'mod_forum', 'block_activity_results'
2411 * @param array $newcapdef array of the new capability definitions that will be
2412 * compared with the cached capabilities
2413 * @return int number of deprecated capabilities that have been removed
2415 function capabilities_cleanup($component, $newcapdef = null) {
2416 global $DB;
2418 $removedcount = 0;
2420 if ($cachedcaps = get_cached_capabilities($component)) {
2421 foreach ($cachedcaps as $cachedcap) {
2422 if (empty($newcapdef) ||
2423 array_key_exists($cachedcap->name, $newcapdef) === false) {
2425 // Delete from roles.
2426 if ($roles = get_roles_with_capability($cachedcap->name)) {
2427 foreach ($roles as $role) {
2428 if (!unassign_capability($cachedcap->name, $role->id)) {
2429 throw new \moodle_exception('cannotunassigncap', 'error', '',
2430 (object)array('cap' => $cachedcap->name, 'role' => $role->name));
2435 // Remove from role_capabilities for any old ones.
2436 $DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
2438 // Remove from capabilities cache.
2439 $DB->delete_records('capabilities', array('name' => $cachedcap->name));
2440 $removedcount++;
2441 } // End if.
2444 if ($removedcount) {
2445 cache::make('core', 'capabilities')->delete('core_capabilities');
2447 return $removedcount;
2451 * Returns an array of all the known types of risk
2452 * The array keys can be used, for example as CSS class names, or in calls to
2453 * print_risk_icon. The values are the corresponding RISK_ constants.
2455 * @return array all the known types of risk.
2457 function get_all_risks() {
2458 return array(
2459 'riskmanagetrust' => RISK_MANAGETRUST,
2460 'riskconfig' => RISK_CONFIG,
2461 'riskxss' => RISK_XSS,
2462 'riskpersonal' => RISK_PERSONAL,
2463 'riskspam' => RISK_SPAM,
2464 'riskdataloss' => RISK_DATALOSS,
2469 * Return a link to moodle docs for a given capability name
2471 * @param stdClass $capability a capability - a row from the mdl_capabilities table.
2472 * @return string the human-readable capability name as a link to Moodle Docs.
2474 function get_capability_docs_link($capability) {
2475 $url = get_docs_url('Capabilities/' . $capability->name);
2476 return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
2480 * This function pulls out all the resolved capabilities (overrides and
2481 * defaults) of a role used in capability overrides in contexts at a given
2482 * context.
2484 * @param int $roleid
2485 * @param context $context
2486 * @param string $cap capability, optional, defaults to ''
2487 * @return array Array of capabilities
2489 function role_context_capabilities($roleid, context $context, $cap = '') {
2490 global $DB;
2492 $contexts = $context->get_parent_context_ids(true);
2493 $contexts = '('.implode(',', $contexts).')';
2495 $params = array($roleid);
2497 if ($cap) {
2498 $search = " AND rc.capability = ? ";
2499 $params[] = $cap;
2500 } else {
2501 $search = '';
2504 $sql = "SELECT rc.*
2505 FROM {role_capabilities} rc
2506 JOIN {context} c ON rc.contextid = c.id
2507 JOIN {capabilities} cap ON rc.capability = cap.name
2508 WHERE rc.contextid in $contexts
2509 AND rc.roleid = ?
2510 $search
2511 ORDER BY c.contextlevel DESC, rc.capability DESC";
2513 $capabilities = array();
2515 if ($records = $DB->get_records_sql($sql, $params)) {
2516 // We are traversing via reverse order.
2517 foreach ($records as $record) {
2518 // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
2519 if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
2520 $capabilities[$record->capability] = $record->permission;
2524 return $capabilities;
2528 * Constructs array with contextids as first parameter and context paths,
2529 * in both cases bottom top including self.
2531 * @access private
2532 * @param context $context
2533 * @return array
2535 function get_context_info_list(context $context) {
2536 $contextids = explode('/', ltrim($context->path, '/'));
2537 $contextpaths = array();
2538 $contextids2 = $contextids;
2539 while ($contextids2) {
2540 $contextpaths[] = '/' . implode('/', $contextids2);
2541 array_pop($contextids2);
2543 return array($contextids, $contextpaths);
2547 * Check if context is the front page context or a context inside it
2549 * Returns true if this context is the front page context, or a context inside it,
2550 * otherwise false.
2552 * @param context $context a context object.
2553 * @return bool
2555 function is_inside_frontpage(context $context) {
2556 $frontpagecontext = context_course::instance(SITEID);
2557 return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
2561 * Returns capability information (cached)
2563 * @param string $capabilityname
2564 * @return stdClass or null if capability not found
2566 function get_capability_info($capabilityname) {
2567 $caps = get_all_capabilities();
2569 // Check for deprecated capability.
2570 if ($deprecatedinfo = get_deprecated_capability_info($capabilityname)) {
2571 if (!empty($deprecatedinfo['replacement'])) {
2572 // Let's try again with this capability if it exists.
2573 if (isset($caps[$deprecatedinfo['replacement']])) {
2574 $capabilityname = $deprecatedinfo['replacement'];
2575 } else {
2576 debugging("Capability '{$capabilityname}' was supposed to be replaced with ".
2577 "'{$deprecatedinfo['replacement']}', which does not exist !");
2580 $fullmessage = $deprecatedinfo['fullmessage'];
2581 debugging($fullmessage, DEBUG_DEVELOPER);
2583 if (!isset($caps[$capabilityname])) {
2584 return null;
2587 return (object) $caps[$capabilityname];
2591 * Returns deprecation info for this particular capabilty (cached)
2593 * Do not use this function except in the get_capability_info
2595 * @param string $capabilityname
2596 * @return stdClass|null with deprecation message and potential replacement if not null
2598 function get_deprecated_capability_info($capabilityname) {
2599 // Here if we do like get_all_capabilities, we run into performance issues as the full array is unserialised each time.
2600 // We could have used an adhoc task but this also had performance issue. Last solution was to create a cache using
2601 // the official caches.php file. The performance issue shows in test_permission_evaluation.
2602 $cache = cache::make('core', 'deprecatedcapabilities');
2603 // Cache has not be initialised.
2604 if (!$cache->get('deprecated_capabilities_initialised')) {
2605 // Look for deprecated capabilities in each components.
2606 $allcaps = get_all_capabilities();
2607 $components = [];
2608 $alldeprecatedcaps = [];
2609 foreach ($allcaps as $cap) {
2610 if (!in_array($cap['component'], $components)) {
2611 $components[] = $cap['component'];
2612 $defpath = core_component::get_component_directory($cap['component']).'/db/access.php';
2613 if (file_exists($defpath)) {
2614 $deprecatedcapabilities = [];
2615 require($defpath);
2616 if (!empty($deprecatedcapabilities)) {
2617 foreach ($deprecatedcapabilities as $cname => $cdef) {
2618 $cache->set($cname, $cdef);
2624 $cache->set('deprecated_capabilities_initialised', true);
2626 if (!$cache->has($capabilityname)) {
2627 return null;
2629 $deprecatedinfo = $cache->get($capabilityname);
2630 $deprecatedinfo['fullmessage'] = "The capability '{$capabilityname}' is deprecated.";
2631 if (!empty($deprecatedinfo['message'])) {
2632 $deprecatedinfo['fullmessage'] .= $deprecatedinfo['message'];
2634 if (!empty($deprecatedinfo['replacement'])) {
2635 $deprecatedinfo['fullmessage'] .=
2636 "It will be replaced by '{$deprecatedinfo['replacement']}'.";
2638 return $deprecatedinfo;
2642 * Returns all capabilitiy records, preferably from MUC and not database.
2644 * @return array All capability records indexed by capability name
2646 function get_all_capabilities() {
2647 global $DB;
2648 $cache = cache::make('core', 'capabilities');
2649 if (!$allcaps = $cache->get('core_capabilities')) {
2650 $rs = $DB->get_recordset('capabilities');
2651 $allcaps = array();
2652 foreach ($rs as $capability) {
2653 $capability->riskbitmask = (int) $capability->riskbitmask;
2654 $allcaps[$capability->name] = (array) $capability;
2656 $rs->close();
2657 $cache->set('core_capabilities', $allcaps);
2659 return $allcaps;
2663 * Returns the human-readable, translated version of the capability.
2664 * Basically a big switch statement.
2666 * @param string $capabilityname e.g. mod/choice:readresponses
2667 * @return string
2669 function get_capability_string($capabilityname) {
2671 // Typical capability name is 'plugintype/pluginname:capabilityname'
2672 list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
2674 if ($type === 'moodle') {
2675 $component = 'core_role';
2676 } else if ($type === 'quizreport') {
2677 //ugly hack!!
2678 $component = 'quiz_'.$name;
2679 } else {
2680 $component = $type.'_'.$name;
2683 $stringname = $name.':'.$capname;
2685 if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
2686 return get_string($stringname, $component);
2689 $dir = core_component::get_component_directory($component);
2690 if (!file_exists($dir)) {
2691 // plugin broken or does not exist, do not bother with printing of debug message
2692 return $capabilityname.' ???';
2695 // something is wrong in plugin, better print debug
2696 return get_string($stringname, $component);
2700 * This gets the mod/block/course/core etc strings.
2702 * @param string $component
2703 * @param int $contextlevel
2704 * @return string|bool String is success, false if failed
2706 function get_component_string($component, $contextlevel) {
2708 if ($component === 'moodle' || $component === 'core') {
2709 return context_helper::get_level_name($contextlevel);
2712 list($type, $name) = core_component::normalize_component($component);
2713 $dir = core_component::get_plugin_directory($type, $name);
2714 if (!file_exists($dir)) {
2715 // plugin not installed, bad luck, there is no way to find the name
2716 return $component . ' ???';
2719 // Some plugin types need an extra prefix to make the name easy to understand.
2720 switch ($type) {
2721 case 'quiz':
2722 $prefix = get_string('quizreport', 'quiz') . ': ';
2723 break;
2724 case 'repository':
2725 $prefix = get_string('repository', 'repository') . ': ';
2726 break;
2727 case 'gradeimport':
2728 $prefix = get_string('gradeimport', 'grades') . ': ';
2729 break;
2730 case 'gradeexport':
2731 $prefix = get_string('gradeexport', 'grades') . ': ';
2732 break;
2733 case 'gradereport':
2734 $prefix = get_string('gradereport', 'grades') . ': ';
2735 break;
2736 case 'webservice':
2737 $prefix = get_string('webservice', 'webservice') . ': ';
2738 break;
2739 case 'block':
2740 $prefix = get_string('block') . ': ';
2741 break;
2742 case 'mod':
2743 $prefix = get_string('activity') . ': ';
2744 break;
2746 // Default case, just use the plugin name.
2747 default:
2748 $prefix = '';
2750 return $prefix . get_string('pluginname', $component);
2754 * Gets the list of roles assigned to this context and up (parents)
2755 * from the aggregation of:
2756 * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
2757 * b) if applicable, those roles that are assigned in the context.
2759 * @param context $context
2760 * @return array
2762 function get_profile_roles(context $context) {
2763 global $CFG, $DB;
2764 // If the current user can assign roles, then they can see all roles on the profile and participants page,
2765 // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2766 if (has_capability('moodle/role:assign', $context)) {
2767 $rolesinscope = array_keys(get_all_roles($context));
2768 } else {
2769 $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2772 if (empty($rolesinscope)) {
2773 return [];
2776 list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2777 list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2778 $params = array_merge($params, $cparams);
2780 if ($coursecontext = $context->get_course_context(false)) {
2781 $params['coursecontext'] = $coursecontext->id;
2782 } else {
2783 $params['coursecontext'] = 0;
2786 $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2787 FROM {role_assignments} ra, {role} r
2788 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2789 WHERE r.id = ra.roleid
2790 AND ra.contextid $contextlist
2791 AND r.id $rallowed
2792 ORDER BY r.sortorder ASC";
2794 return $DB->get_records_sql($sql, $params);
2798 * Gets the list of roles assigned to this context and up (parents)
2800 * @param context $context
2801 * @param boolean $includeparents, false means without parents.
2802 * @return array
2804 function get_roles_used_in_context(context $context, $includeparents = true) {
2805 global $DB;
2807 if ($includeparents === true) {
2808 list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
2809 } else {
2810 list($contextlist, $params) = $DB->get_in_or_equal($context->id, SQL_PARAMS_NAMED, 'cl');
2813 if ($coursecontext = $context->get_course_context(false)) {
2814 $params['coursecontext'] = $coursecontext->id;
2815 } else {
2816 $params['coursecontext'] = 0;
2819 $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2820 FROM {role_assignments} ra, {role} r
2821 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2822 WHERE r.id = ra.roleid
2823 AND ra.contextid $contextlist
2824 ORDER BY r.sortorder ASC";
2826 return $DB->get_records_sql($sql, $params);
2830 * This function is used to print roles column in user profile page.
2831 * It is using the CFG->profileroles to limit the list to only interesting roles.
2832 * (The permission tab has full details of user role assignments.)
2834 * @param int $userid
2835 * @param int $courseid
2836 * @return string
2838 function get_user_roles_in_course($userid, $courseid) {
2839 global $CFG, $DB;
2840 if ($courseid == SITEID) {
2841 $context = context_system::instance();
2842 } else {
2843 $context = context_course::instance($courseid);
2845 // If the current user can assign roles, then they can see all roles on the profile and participants page,
2846 // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2847 if (has_capability('moodle/role:assign', $context)) {
2848 $rolesinscope = array_keys(get_all_roles($context));
2849 } else {
2850 $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2852 if (empty($rolesinscope)) {
2853 return '';
2856 list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2857 list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2858 $params = array_merge($params, $cparams);
2860 if ($coursecontext = $context->get_course_context(false)) {
2861 $params['coursecontext'] = $coursecontext->id;
2862 } else {
2863 $params['coursecontext'] = 0;
2866 $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2867 FROM {role_assignments} ra, {role} r
2868 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2869 WHERE r.id = ra.roleid
2870 AND ra.contextid $contextlist
2871 AND r.id $rallowed
2872 AND ra.userid = :userid
2873 ORDER BY r.sortorder ASC";
2874 $params['userid'] = $userid;
2876 $rolestring = '';
2878 if ($roles = $DB->get_records_sql($sql, $params)) {
2879 $viewableroles = get_viewable_roles($context, $userid);
2881 $rolenames = array();
2882 foreach ($roles as $roleid => $unused) {
2883 if (isset($viewableroles[$roleid])) {
2884 $url = new moodle_url('/user/index.php', ['contextid' => $context->id, 'roleid' => $roleid]);
2885 $rolenames[] = '<a href="' . $url . '">' . $viewableroles[$roleid] . '</a>';
2888 $rolestring = implode(', ', $rolenames);
2891 return $rolestring;
2895 * Checks if a user can assign users to a particular role in this context
2897 * @param context $context
2898 * @param int $targetroleid - the id of the role you want to assign users to
2899 * @return boolean
2901 function user_can_assign(context $context, $targetroleid) {
2902 global $DB;
2904 // First check to see if the user is a site administrator.
2905 if (is_siteadmin()) {
2906 return true;
2909 // Check if user has override capability.
2910 // If not return false.
2911 if (!has_capability('moodle/role:assign', $context)) {
2912 return false;
2914 // pull out all active roles of this user from this context(or above)
2915 if ($userroles = get_user_roles($context)) {
2916 foreach ($userroles as $userrole) {
2917 // if any in the role_allow_override table, then it's ok
2918 if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
2919 return true;
2924 return false;
2928 * Returns all site roles in correct sort order.
2930 * Note: this method does not localise role names or descriptions,
2931 * use role_get_names() if you need role names.
2933 * @param context $context optional context for course role name aliases
2934 * @return array of role records with optional coursealias property
2936 function get_all_roles(context $context = null) {
2937 global $DB;
2939 if (!$context or !$coursecontext = $context->get_course_context(false)) {
2940 $coursecontext = null;
2943 if ($coursecontext) {
2944 $sql = "SELECT r.*, rn.name AS coursealias
2945 FROM {role} r
2946 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2947 ORDER BY r.sortorder ASC";
2948 return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
2950 } else {
2951 return $DB->get_records('role', array(), 'sortorder ASC');
2956 * Returns roles of a specified archetype
2958 * @param string $archetype
2959 * @return array of full role records
2961 function get_archetype_roles($archetype) {
2962 global $DB;
2963 return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
2967 * Gets all the user roles assigned in this context, or higher contexts for a list of users.
2969 * If you try using the combination $userids = [], $checkparentcontexts = true then this is likely
2970 * to cause an out-of-memory error on large Moodle sites, so this combination is deprecated and
2971 * outputs a warning, even though it is the default.
2973 * @param context $context
2974 * @param array $userids. An empty list means fetch all role assignments for the context.
2975 * @param bool $checkparentcontexts defaults to true
2976 * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2977 * @return array
2979 function get_users_roles(context $context, $userids = [], $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2980 global $DB;
2982 if (!$userids && $checkparentcontexts) {
2983 debugging('Please do not call get_users_roles() with $checkparentcontexts = true ' .
2984 'and $userids array not set. This combination causes large Moodle sites ' .
2985 'with lots of site-wide role assignemnts to run out of memory.', DEBUG_DEVELOPER);
2988 if ($checkparentcontexts) {
2989 $contextids = $context->get_parent_context_ids();
2990 } else {
2991 $contextids = array();
2993 $contextids[] = $context->id;
2995 list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
2997 // If userids was passed as an empty array, we fetch all role assignments for the course.
2998 if (empty($userids)) {
2999 $useridlist = ' IS NOT NULL ';
3000 $uparams = [];
3001 } else {
3002 list($useridlist, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uids');
3005 $sql = "SELECT ra.*, r.name, r.shortname, ra.userid
3006 FROM {role_assignments} ra, {role} r, {context} c
3007 WHERE ra.userid $useridlist
3008 AND ra.roleid = r.id
3009 AND ra.contextid = c.id
3010 AND ra.contextid $contextids
3011 ORDER BY $order";
3013 $all = $DB->get_records_sql($sql , array_merge($params, $uparams));
3015 // Return results grouped by userid.
3016 $result = [];
3017 foreach ($all as $id => $record) {
3018 if (!isset($result[$record->userid])) {
3019 $result[$record->userid] = [];
3021 $result[$record->userid][$record->id] = $record;
3024 // Make sure all requested users are included in the result, even if they had no role assignments.
3025 foreach ($userids as $id) {
3026 if (!isset($result[$id])) {
3027 $result[$id] = [];
3031 return $result;
3036 * Gets all the user roles assigned in this context, or higher contexts
3037 * this is mainly used when checking if a user can assign a role, or overriding a role
3038 * i.e. we need to know what this user holds, in order to verify against allow_assign and
3039 * allow_override tables
3041 * @param context $context
3042 * @param int $userid
3043 * @param bool $checkparentcontexts defaults to true
3044 * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
3045 * @return array
3047 function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
3048 global $USER, $DB;
3050 if (empty($userid)) {
3051 if (empty($USER->id)) {
3052 return array();
3054 $userid = $USER->id;
3057 if ($checkparentcontexts) {
3058 $contextids = $context->get_parent_context_ids();
3059 } else {
3060 $contextids = array();
3062 $contextids[] = $context->id;
3064 list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
3066 array_unshift($params, $userid);
3068 $sql = "SELECT ra.*, r.name, r.shortname
3069 FROM {role_assignments} ra, {role} r, {context} c
3070 WHERE ra.userid = ?
3071 AND ra.roleid = r.id
3072 AND ra.contextid = c.id
3073 AND ra.contextid $contextids
3074 ORDER BY $order";
3076 return $DB->get_records_sql($sql ,$params);
3080 * Like get_user_roles, but adds in the authenticated user role, and the front
3081 * page roles, if applicable.
3083 * @param context $context the context.
3084 * @param int $userid optional. Defaults to $USER->id
3085 * @return array of objects with fields ->userid, ->contextid and ->roleid.
3087 function get_user_roles_with_special(context $context, $userid = 0) {
3088 global $CFG, $USER;
3090 if (empty($userid)) {
3091 if (empty($USER->id)) {
3092 return array();
3094 $userid = $USER->id;
3097 $ras = get_user_roles($context, $userid);
3099 // Add front-page role if relevant.
3100 $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3101 $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
3102 is_inside_frontpage($context);
3103 if ($defaultfrontpageroleid && $isfrontpage) {
3104 $frontpagecontext = context_course::instance(SITEID);
3105 $ra = new stdClass();
3106 $ra->userid = $userid;
3107 $ra->contextid = $frontpagecontext->id;
3108 $ra->roleid = $defaultfrontpageroleid;
3109 $ras[] = $ra;
3112 // Add authenticated user role if relevant.
3113 $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3114 if ($defaultuserroleid && !isguestuser($userid)) {
3115 $systemcontext = context_system::instance();
3116 $ra = new stdClass();
3117 $ra->userid = $userid;
3118 $ra->contextid = $systemcontext->id;
3119 $ra->roleid = $defaultuserroleid;
3120 $ras[] = $ra;
3123 return $ras;
3127 * Creates a record in the role_allow_override table
3129 * @param int $fromroleid source roleid
3130 * @param int $targetroleid target roleid
3131 * @return void
3133 function core_role_set_override_allowed($fromroleid, $targetroleid) {
3134 global $DB;
3136 $record = new stdClass();
3137 $record->roleid = $fromroleid;
3138 $record->allowoverride = $targetroleid;
3139 $DB->insert_record('role_allow_override', $record);
3143 * Creates a record in the role_allow_assign table
3145 * @param int $fromroleid source roleid
3146 * @param int $targetroleid target roleid
3147 * @return void
3149 function core_role_set_assign_allowed($fromroleid, $targetroleid) {
3150 global $DB;
3152 $record = new stdClass();
3153 $record->roleid = $fromroleid;
3154 $record->allowassign = $targetroleid;
3155 $DB->insert_record('role_allow_assign', $record);
3159 * Creates a record in the role_allow_switch table
3161 * @param int $fromroleid source roleid
3162 * @param int $targetroleid target roleid
3163 * @return void
3165 function core_role_set_switch_allowed($fromroleid, $targetroleid) {
3166 global $DB;
3168 $record = new stdClass();
3169 $record->roleid = $fromroleid;
3170 $record->allowswitch = $targetroleid;
3171 $DB->insert_record('role_allow_switch', $record);
3175 * Creates a record in the role_allow_view table
3177 * @param int $fromroleid source roleid
3178 * @param int $targetroleid target roleid
3179 * @return void
3181 function core_role_set_view_allowed($fromroleid, $targetroleid) {
3182 global $DB;
3184 $record = new stdClass();
3185 $record->roleid = $fromroleid;
3186 $record->allowview = $targetroleid;
3187 $DB->insert_record('role_allow_view', $record);
3191 * Gets a list of roles that this user can assign in this context
3193 * @param context $context the context.
3194 * @param int $rolenamedisplay the type of role name to display. One of the
3195 * ROLENAME_X constants. Default ROLENAME_ALIAS.
3196 * @param bool $withusercounts if true, count the number of users with each role.
3197 * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
3198 * @return array if $withusercounts is false, then an array $roleid => $rolename.
3199 * if $withusercounts is true, returns a list of three arrays,
3200 * $rolenames, $rolecounts, and $nameswithcounts.
3202 function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
3203 global $USER, $DB;
3205 // make sure there is a real user specified
3206 if ($user === null) {
3207 $userid = isset($USER->id) ? $USER->id : 0;
3208 } else {
3209 $userid = is_object($user) ? $user->id : $user;
3212 if (!has_capability('moodle/role:assign', $context, $userid)) {
3213 if ($withusercounts) {
3214 return array(array(), array(), array());
3215 } else {
3216 return array();
3220 $params = array();
3221 $extrafields = '';
3223 if ($withusercounts) {
3224 $extrafields = ', (SELECT COUNT(DISTINCT u.id)
3225 FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
3226 WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
3227 ) AS usercount';
3228 $params['conid'] = $context->id;
3231 if (is_siteadmin($userid)) {
3232 // show all roles allowed in this context to admins
3233 $assignrestriction = "";
3234 } else {
3235 $parents = $context->get_parent_context_ids(true);
3236 $contexts = implode(',' , $parents);
3237 $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
3238 FROM {role_allow_assign} raa
3239 JOIN {role_assignments} ra ON ra.roleid = raa.roleid
3240 WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3241 ) ar ON ar.id = r.id";
3242 $params['userid'] = $userid;
3244 $params['contextlevel'] = $context->contextlevel;
3246 if ($coursecontext = $context->get_course_context(false)) {
3247 $params['coursecontext'] = $coursecontext->id;
3248 } else {
3249 $params['coursecontext'] = 0; // no course aliases
3250 $coursecontext = null;
3252 $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
3253 FROM {role} r
3254 $assignrestriction
3255 JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
3256 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3257 ORDER BY r.sortorder ASC";
3258 $roles = $DB->get_records_sql($sql, $params);
3260 $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
3262 if (!$withusercounts) {
3263 return $rolenames;
3266 $rolecounts = array();
3267 $nameswithcounts = array();
3268 foreach ($roles as $role) {
3269 $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
3270 $rolecounts[$role->id] = $roles[$role->id]->usercount;
3272 return array($rolenames, $rolecounts, $nameswithcounts);
3276 * Gets a list of roles that this user can switch to in a context
3278 * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
3279 * This function just process the contents of the role_allow_switch table. You also need to
3280 * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
3282 * @param context $context a context.
3283 * @param int $rolenamedisplay the type of role name to display. One of the
3284 * ROLENAME_X constants. Default ROLENAME_ALIAS.
3285 * @return array an array $roleid => $rolename.
3287 function get_switchable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS) {
3288 global $USER, $DB;
3290 // You can't switch roles without this capability.
3291 if (!has_capability('moodle/role:switchroles', $context)) {
3292 return [];
3295 $params = array();
3296 $extrajoins = '';
3297 $extrawhere = '';
3298 if (!is_siteadmin()) {
3299 // Admins are allowed to switch to any role with.
3300 // Others are subject to the additional constraint that the switch-to role must be allowed by
3301 // 'role_allow_switch' for some role they have assigned in this context or any parent.
3302 $parents = $context->get_parent_context_ids(true);
3303 $contexts = implode(',' , $parents);
3305 $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
3306 JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3307 $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
3308 $params['userid'] = $USER->id;
3311 if ($coursecontext = $context->get_course_context(false)) {
3312 $params['coursecontext'] = $coursecontext->id;
3313 } else {
3314 $params['coursecontext'] = 0; // no course aliases
3315 $coursecontext = null;
3318 $query = "
3319 SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3320 FROM (SELECT DISTINCT rc.roleid
3321 FROM {role_capabilities} rc
3323 $extrajoins
3324 $extrawhere) idlist
3325 JOIN {role} r ON r.id = idlist.roleid
3326 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3327 ORDER BY r.sortorder";
3328 $roles = $DB->get_records_sql($query, $params);
3330 return role_fix_names($roles, $context, $rolenamedisplay, true);
3334 * Gets a list of roles that this user can view in a context
3336 * @param context $context a context.
3337 * @param int $userid id of user.
3338 * @param int $rolenamedisplay the type of role name to display. One of the
3339 * ROLENAME_X constants. Default ROLENAME_ALIAS.
3340 * @return array an array $roleid => $rolename.
3342 function get_viewable_roles(context $context, $userid = null, $rolenamedisplay = ROLENAME_ALIAS) {
3343 global $USER, $DB;
3345 if ($userid == null) {
3346 $userid = $USER->id;
3349 $params = array();
3350 $extrajoins = '';
3351 $extrawhere = '';
3352 if (!is_siteadmin()) {
3353 // Admins are allowed to view any role.
3354 // Others are subject to the additional constraint that the view role must be allowed by
3355 // 'role_allow_view' for some role they have assigned in this context or any parent.
3356 $contexts = $context->get_parent_context_ids(true);
3357 list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
3359 $extrajoins = "JOIN {role_allow_view} ras ON ras.allowview = r.id
3360 JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3361 $extrawhere = "WHERE ra.userid = :userid AND ra.contextid $insql";
3363 $params += $inparams;
3364 $params['userid'] = $userid;
3367 if ($coursecontext = $context->get_course_context(false)) {
3368 $params['coursecontext'] = $coursecontext->id;
3369 } else {
3370 $params['coursecontext'] = 0; // No course aliases.
3371 $coursecontext = null;
3374 $query = "
3375 SELECT r.id, r.name, r.shortname, rn.name AS coursealias, r.sortorder
3376 FROM {role} r
3377 $extrajoins
3378 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3379 $extrawhere
3380 GROUP BY r.id, r.name, r.shortname, rn.name, r.sortorder
3381 ORDER BY r.sortorder";
3382 $roles = $DB->get_records_sql($query, $params);
3384 return role_fix_names($roles, $context, $rolenamedisplay, true);
3388 * Gets a list of roles that this user can override in this context.
3390 * @param context $context the context.
3391 * @param int $rolenamedisplay the type of role name to display. One of the
3392 * ROLENAME_X constants. Default ROLENAME_ALIAS.
3393 * @param bool $withcounts if true, count the number of overrides that are set for each role.
3394 * @return array if $withcounts is false, then an array $roleid => $rolename.
3395 * if $withusercounts is true, returns a list of three arrays,
3396 * $rolenames, $rolecounts, and $nameswithcounts.
3398 function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
3399 global $USER, $DB;
3401 if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
3402 if ($withcounts) {
3403 return array(array(), array(), array());
3404 } else {
3405 return array();
3409 $parents = $context->get_parent_context_ids(true);
3410 $contexts = implode(',' , $parents);
3412 $params = array();
3413 $extrafields = '';
3415 $params['userid'] = $USER->id;
3416 if ($withcounts) {
3417 $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
3418 WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
3419 $params['conid'] = $context->id;
3422 if ($coursecontext = $context->get_course_context(false)) {
3423 $params['coursecontext'] = $coursecontext->id;
3424 } else {
3425 $params['coursecontext'] = 0; // no course aliases
3426 $coursecontext = null;
3429 if (is_siteadmin()) {
3430 // show all roles to admins
3431 $roles = $DB->get_records_sql("
3432 SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3433 FROM {role} ro
3434 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3435 ORDER BY ro.sortorder ASC", $params);
3437 } else {
3438 $roles = $DB->get_records_sql("
3439 SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3440 FROM {role} ro
3441 JOIN (SELECT DISTINCT r.id
3442 FROM {role} r
3443 JOIN {role_allow_override} rao ON r.id = rao.allowoverride
3444 JOIN {role_assignments} ra ON rao.roleid = ra.roleid
3445 WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3446 ) inline_view ON ro.id = inline_view.id
3447 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3448 ORDER BY ro.sortorder ASC", $params);
3451 $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
3453 if (!$withcounts) {
3454 return $rolenames;
3457 $rolecounts = array();
3458 $nameswithcounts = array();
3459 foreach ($roles as $role) {
3460 $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
3461 $rolecounts[$role->id] = $roles[$role->id]->overridecount;
3463 return array($rolenames, $rolecounts, $nameswithcounts);
3467 * Create a role menu suitable for default role selection in enrol plugins.
3469 * @package core_enrol
3471 * @param context $context
3472 * @param int $addroleid current or default role - always added to list
3473 * @return array roleid=>localised role name
3475 function get_default_enrol_roles(context $context, $addroleid = null) {
3476 global $DB;
3478 $params = array('contextlevel'=>CONTEXT_COURSE);
3480 if ($coursecontext = $context->get_course_context(false)) {
3481 $params['coursecontext'] = $coursecontext->id;
3482 } else {
3483 $params['coursecontext'] = 0; // no course names
3484 $coursecontext = null;
3487 if ($addroleid) {
3488 $addrole = "OR r.id = :addroleid";
3489 $params['addroleid'] = $addroleid;
3490 } else {
3491 $addrole = "";
3494 $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3495 FROM {role} r
3496 LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
3497 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3498 WHERE rcl.id IS NOT NULL $addrole
3499 ORDER BY sortorder DESC";
3501 $roles = $DB->get_records_sql($sql, $params);
3503 return role_fix_names($roles, $context, ROLENAME_BOTH, true);
3507 * Return context levels where this role is assignable.
3509 * @param integer $roleid the id of a role.
3510 * @return array list of the context levels at which this role may be assigned.
3512 function get_role_contextlevels($roleid) {
3513 global $DB;
3514 return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
3515 'contextlevel', 'id,contextlevel');
3519 * Return roles suitable for assignment at the specified context level.
3521 * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
3523 * @param integer $contextlevel a contextlevel.
3524 * @return array list of role ids that are assignable at this context level.
3526 function get_roles_for_contextlevels($contextlevel) {
3527 global $DB;
3528 return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
3529 '', 'id,roleid');
3533 * Returns default context levels where roles can be assigned.
3535 * @param string $rolearchetype one of the role archetypes - that is, one of the keys
3536 * from the array returned by get_role_archetypes();
3537 * @return array list of the context levels at which this type of role may be assigned by default.
3539 function get_default_contextlevels($rolearchetype) {
3540 static $defaults = array(
3541 'manager' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
3542 'coursecreator' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
3543 'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3544 'teacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3545 'student' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3546 'guest' => array(),
3547 'user' => array(),
3548 'frontpage' => array());
3550 if (isset($defaults[$rolearchetype])) {
3551 return $defaults[$rolearchetype];
3552 } else {
3553 return array();
3558 * Set the context levels at which a particular role can be assigned.
3559 * Throws exceptions in case of error.
3561 * @param integer $roleid the id of a role.
3562 * @param array $contextlevels the context levels at which this role should be assignable,
3563 * duplicate levels are removed.
3564 * @return void
3566 function set_role_contextlevels($roleid, array $contextlevels) {
3567 global $DB;
3568 $DB->delete_records('role_context_levels', array('roleid' => $roleid));
3569 $rcl = new stdClass();
3570 $rcl->roleid = $roleid;
3571 $contextlevels = array_unique($contextlevels);
3572 foreach ($contextlevels as $level) {
3573 $rcl->contextlevel = $level;
3574 $DB->insert_record('role_context_levels', $rcl, false, true);
3579 * Gets sql joins for finding users with capability in the given context.
3581 * @param context $context Context for the join.
3582 * @param string|array $capability Capability name or array of names.
3583 * If an array is provided then this is the equivalent of a logical 'OR',
3584 * i.e. the user needs to have one of these capabilities.
3585 * @param string $useridcolumn e.g. 'u.id'.
3586 * @return \core\dml\sql_join Contains joins, wheres, params.
3587 * This function will set ->cannotmatchanyrows if applicable.
3588 * This may let you skip doing a DB query.
3590 function get_with_capability_join(context $context, $capability, $useridcolumn) {
3591 global $CFG, $DB;
3593 // Add a unique prefix to param names to ensure they are unique.
3594 static $i = 0;
3595 $i++;
3596 $paramprefix = 'eu' . $i . '_';
3598 $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3599 $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3601 $ctxids = trim($context->path, '/');
3602 $ctxids = str_replace('/', ',', $ctxids);
3604 // Context is the frontpage
3605 $isfrontpage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID;
3606 $isfrontpage = $isfrontpage || is_inside_frontpage($context);
3608 $caps = (array) $capability;
3610 // Construct list of context paths bottom --> top.
3611 list($contextids, $paths) = get_context_info_list($context);
3613 // We need to find out all roles that have these capabilities either in definition or in overrides.
3614 $defs = [];
3615 list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, $paramprefix . 'con');
3616 list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, $paramprefix . 'cap');
3618 // Check whether context locking is enabled.
3619 // Filter out any write capability if this is the case.
3620 $excludelockedcaps = '';
3621 $excludelockedcapsparams = [];
3622 if (!empty($CFG->contextlocking) && $context->locked) {
3623 $excludelockedcaps = 'AND (cap.captype = :capread OR cap.name = :managelockscap)';
3624 $excludelockedcapsparams['capread'] = 'read';
3625 $excludelockedcapsparams['managelockscap'] = 'moodle/site:managecontextlocks';
3628 $params = array_merge($params, $params2, $excludelockedcapsparams);
3629 $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3630 FROM {role_capabilities} rc
3631 JOIN {capabilities} cap ON rc.capability = cap.name
3632 JOIN {context} ctx on rc.contextid = ctx.id
3633 WHERE rc.contextid $incontexts AND rc.capability $incaps $excludelockedcaps";
3635 $rcs = $DB->get_records_sql($sql, $params);
3636 foreach ($rcs as $rc) {
3637 $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3640 // Go through the permissions bottom-->top direction to evaluate the current permission,
3641 // first one wins (prohibit is an exception that always wins).
3642 $access = [];
3643 foreach ($caps as $cap) {
3644 foreach ($paths as $path) {
3645 if (empty($defs[$cap][$path])) {
3646 continue;
3648 foreach ($defs[$cap][$path] as $roleid => $perm) {
3649 if ($perm == CAP_PROHIBIT) {
3650 $access[$cap][$roleid] = CAP_PROHIBIT;
3651 continue;
3653 if (!isset($access[$cap][$roleid])) {
3654 $access[$cap][$roleid] = (int)$perm;
3660 // Make lists of roles that are needed and prohibited in this context.
3661 $needed = []; // One of these is enough.
3662 $prohibited = []; // Must not have any of these.
3663 foreach ($caps as $cap) {
3664 if (empty($access[$cap])) {
3665 continue;
3667 foreach ($access[$cap] as $roleid => $perm) {
3668 if ($perm == CAP_PROHIBIT) {
3669 unset($needed[$cap][$roleid]);
3670 $prohibited[$cap][$roleid] = true;
3671 } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
3672 $needed[$cap][$roleid] = true;
3675 if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
3676 // Easy, nobody has the permission.
3677 unset($needed[$cap]);
3678 unset($prohibited[$cap]);
3679 } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
3680 // Everybody is disqualified on the frontpage.
3681 unset($needed[$cap]);
3682 unset($prohibited[$cap]);
3684 if (empty($prohibited[$cap])) {
3685 unset($prohibited[$cap]);
3689 if (empty($needed)) {
3690 // There can not be anybody if no roles match this request.
3691 return new \core\dml\sql_join('', '1 = 2', [], true);
3694 if (empty($prohibited)) {
3695 // We can compact the needed roles.
3696 $n = [];
3697 foreach ($needed as $cap) {
3698 foreach ($cap as $roleid => $unused) {
3699 $n[$roleid] = true;
3702 $needed = ['any' => $n];
3703 unset($n);
3706 // Prepare query clauses.
3707 $wherecond = [];
3708 $params = [];
3709 $joins = [];
3710 $cannotmatchanyrows = false;
3712 // We never return deleted users or guest account.
3713 // Use a hack to get the deleted user column without an API change.
3714 $deletedusercolumn = substr($useridcolumn, 0, -2) . 'deleted';
3715 $wherecond[] = "$deletedusercolumn = 0 AND $useridcolumn <> :{$paramprefix}guestid";
3716 $params[$paramprefix . 'guestid'] = $CFG->siteguest;
3718 // Now add the needed and prohibited roles conditions as joins.
3719 if (!empty($needed['any'])) {
3720 // Simple case - there are no prohibits involved.
3721 if (!empty($needed['any'][$defaultuserroleid]) ||
3722 ($isfrontpage && !empty($needed['any'][$defaultfrontpageroleid]))) {
3723 // Everybody.
3724 } else {
3725 $joins[] = "JOIN (SELECT DISTINCT userid
3726 FROM {role_assignments}
3727 WHERE contextid IN ($ctxids)
3728 AND roleid IN (" . implode(',', array_keys($needed['any'])) . ")
3729 ) ra ON ra.userid = $useridcolumn";
3731 } else {
3732 $unions = [];
3733 $everybody = false;
3734 foreach ($needed as $cap => $unused) {
3735 if (empty($prohibited[$cap])) {
3736 if (!empty($needed[$cap][$defaultuserroleid]) ||
3737 ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3738 $everybody = true;
3739 break;
3740 } else {
3741 $unions[] = "SELECT userid
3742 FROM {role_assignments}
3743 WHERE contextid IN ($ctxids)
3744 AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
3746 } else {
3747 if (!empty($prohibited[$cap][$defaultuserroleid]) ||
3748 ($isfrontpage && !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
3749 // Nobody can have this cap because it is prohibited in default roles.
3750 continue;
3752 } else if (!empty($needed[$cap][$defaultuserroleid]) ||
3753 ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3754 // Everybody except the prohibited - hiding does not matter.
3755 $unions[] = "SELECT id AS userid
3756 FROM {user}
3757 WHERE id NOT IN (SELECT userid
3758 FROM {role_assignments}
3759 WHERE contextid IN ($ctxids)
3760 AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3762 } else {
3763 $unions[] = "SELECT userid
3764 FROM {role_assignments}
3765 WHERE contextid IN ($ctxids) AND roleid IN (" . implode(',', array_keys($needed[$cap])) . ")
3766 AND userid NOT IN (
3767 SELECT userid
3768 FROM {role_assignments}
3769 WHERE contextid IN ($ctxids)
3770 AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3775 if (!$everybody) {
3776 if ($unions) {
3777 $joins[] = "JOIN (
3778 SELECT DISTINCT userid
3779 FROM (
3780 " . implode("\n UNION \n", $unions) . "
3781 ) us
3782 ) ra ON ra.userid = $useridcolumn";
3783 } else {
3784 // Only prohibits found - nobody can be matched.
3785 $wherecond[] = "1 = 2";
3786 $cannotmatchanyrows = true;
3791 return new \core\dml\sql_join(implode("\n", $joins), implode(" AND ", $wherecond), $params, $cannotmatchanyrows);
3795 * Who has this capability in this context?
3797 * This can be a very expensive call - use sparingly and keep
3798 * the results if you are going to need them again soon.
3800 * Note if $fields is empty this function attempts to get u.*
3801 * which can get rather large - and has a serious perf impact
3802 * on some DBs.
3804 * @param context $context
3805 * @param string|array $capability - capability name(s)
3806 * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
3807 * @param string $sort - the sort order. Default is lastaccess time.
3808 * @param mixed $limitfrom - number of records to skip (offset)
3809 * @param mixed $limitnum - number of records to fetch
3810 * @param string|array $groups - single group or array of groups - only return
3811 * users who are in one of these group(s).
3812 * @param string|array $exceptions - list of users to exclude, comma separated or array
3813 * @param bool $notuseddoanything not used any more, admin accounts are never returned
3814 * @param bool $notusedview - use get_enrolled_sql() instead
3815 * @param bool $useviewallgroups if $groups is set the return users who
3816 * have capability both $capability and moodle/site:accessallgroups
3817 * in this context, as well as users who have $capability and who are
3818 * in $groups.
3819 * @return array of user records
3821 function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
3822 $groups = '', $exceptions = '', $notuseddoanything = null, $notusedview = null, $useviewallgroups = false) {
3823 global $CFG, $DB;
3825 // Context is a course page other than the frontpage.
3826 $iscoursepage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid != SITEID;
3828 // Set up default fields list if necessary.
3829 if (empty($fields)) {
3830 if ($iscoursepage) {
3831 $fields = 'u.*, ul.timeaccess AS lastaccess';
3832 } else {
3833 $fields = 'u.*';
3835 } else {
3836 if ($CFG->debugdeveloper && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
3837 debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
3841 // Set up default sort if necessary.
3842 if (empty($sort)) { // default to course lastaccess or just lastaccess
3843 if ($iscoursepage) {
3844 $sort = 'ul.timeaccess';
3845 } else {
3846 $sort = 'u.lastaccess';
3850 // Get the bits of SQL relating to capabilities.
3851 $sqljoin = get_with_capability_join($context, $capability, 'u.id');
3852 if ($sqljoin->cannotmatchanyrows) {
3853 return [];
3856 // Prepare query clauses.
3857 $wherecond = [$sqljoin->wheres];
3858 $params = $sqljoin->params;
3859 $joins = [$sqljoin->joins];
3861 // Add user lastaccess JOIN, if required.
3862 if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
3863 // Here user_lastaccess is not required MDL-13810.
3864 } else {
3865 if ($iscoursepage) {
3866 $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
3867 } else {
3868 throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
3872 // Groups.
3873 if ($groups) {
3874 $groups = (array)$groups;
3875 list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
3876 $joins[] = "LEFT OUTER JOIN (SELECT DISTINCT userid
3877 FROM {groups_members}
3878 WHERE groupid $grouptest
3879 ) gm ON gm.userid = u.id";
3881 $params = array_merge($params, $grpparams);
3883 $grouptest = 'gm.userid IS NOT NULL';
3884 if ($useviewallgroups) {
3885 $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
3886 if (!empty($viewallgroupsusers)) {
3887 $grouptest .= ' OR u.id IN (' . implode(',', array_keys($viewallgroupsusers)) . ')';
3890 $wherecond[] = "($grouptest)";
3893 // User exceptions.
3894 if (!empty($exceptions)) {
3895 $exceptions = (array)$exceptions;
3896 list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc', false);
3897 $params = array_merge($params, $exparams);
3898 $wherecond[] = "u.id $exsql";
3901 // Collect WHERE conditions and needed joins.
3902 $where = implode(' AND ', $wherecond);
3903 if ($where !== '') {
3904 $where = 'WHERE ' . $where;
3906 $joins = implode("\n", $joins);
3908 // Finally! we have all the bits, run the query.
3909 $sql = "SELECT $fields
3910 FROM {user} u
3911 $joins
3912 $where
3913 ORDER BY $sort";
3915 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3919 * Re-sort a users array based on a sorting policy
3921 * Will re-sort a $users results array (from get_users_by_capability(), usually)
3922 * based on a sorting policy. This is to support the odd practice of
3923 * sorting teachers by 'authority', where authority was "lowest id of the role
3924 * assignment".
3926 * Will execute 1 database query. Only suitable for small numbers of users, as it
3927 * uses an u.id IN() clause.
3929 * Notes about the sorting criteria.
3931 * As a default, we cannot rely on role.sortorder because then
3932 * admins/coursecreators will always win. That is why the sane
3933 * rule "is locality matters most", with sortorder as 2nd
3934 * consideration.
3936 * If you want role.sortorder, use the 'sortorder' policy, and
3937 * name explicitly what roles you want to cover. It's probably
3938 * a good idea to see what roles have the capabilities you want
3939 * (array_diff() them against roiles that have 'can-do-anything'
3940 * to weed out admin-ish roles. Or fetch a list of roles from
3941 * variables like $CFG->coursecontact .
3943 * @param array $users Users array, keyed on userid
3944 * @param context $context
3945 * @param array $roles ids of the roles to include, optional
3946 * @param string $sortpolicy defaults to locality, more about
3947 * @return array sorted copy of the array
3949 function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') {
3950 global $DB;
3952 $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
3953 $contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
3954 if (empty($roles)) {
3955 $roleswhere = '';
3956 } else {
3957 $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
3960 $sql = "SELECT ra.userid
3961 FROM {role_assignments} ra
3962 JOIN {role} r
3963 ON ra.roleid=r.id
3964 JOIN {context} ctx
3965 ON ra.contextid=ctx.id
3966 WHERE $userswhere
3967 $contextwhere
3968 $roleswhere";
3970 // Default 'locality' policy -- read PHPDoc notes
3971 // about sort policies...
3972 $orderby = 'ORDER BY '
3973 .'ctx.depth DESC, ' /* locality wins */
3974 .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3975 .'ra.id'; /* role assignment order tie-breaker */
3976 if ($sortpolicy === 'sortorder') {
3977 $orderby = 'ORDER BY '
3978 .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3979 .'ra.id'; /* role assignment order tie-breaker */
3982 $sortedids = $DB->get_fieldset_sql($sql . $orderby);
3983 $sortedusers = array();
3984 $seen = array();
3986 foreach ($sortedids as $id) {
3987 // Avoid duplicates
3988 if (isset($seen[$id])) {
3989 continue;
3991 $seen[$id] = true;
3993 // assign
3994 $sortedusers[$id] = $users[$id];
3996 return $sortedusers;
4000 * Gets all the users assigned this role in this context or higher
4002 * Note that moodle is based on capabilities and it is usually better
4003 * to check permissions than to check role ids as the capabilities
4004 * system is more flexible. If you really need, you can to use this
4005 * function but consider has_capability() as a possible substitute.
4007 * All $sort fields are added into $fields if not present there yet.
4009 * If $roleid is an array or is empty (all roles) you need to set $fields
4010 * (and $sort by extension) params according to it, as the first field
4011 * returned by the database should be unique (ra.id is the best candidate).
4013 * @param int $roleid (can also be an array of ints!)
4014 * @param context $context
4015 * @param bool $parent if true, get list of users assigned in higher context too
4016 * @param string $fields fields from user (u.) , role assignment (ra) or role (r.)
4017 * @param string $sort sort from user (u.) , role assignment (ra.) or role (r.).
4018 * null => use default sort from users_order_by_sql.
4019 * @param bool $all true means all, false means limit to enrolled users
4020 * @param string $group defaults to ''
4021 * @param mixed $limitfrom defaults to ''
4022 * @param mixed $limitnum defaults to ''
4023 * @param string $extrawheretest defaults to ''
4024 * @param array $whereorsortparams any paramter values used by $sort or $extrawheretest.
4025 * @return array
4027 function get_role_users($roleid, context $context, $parent = false, $fields = '',
4028 $sort = null, $all = true, $group = '',
4029 $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereorsortparams = array()) {
4030 global $DB;
4032 if (empty($fields)) {
4033 $userfieldsapi = \core_user\fields::for_name();
4034 $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
4035 $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
4036 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
4037 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
4038 'u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name AS rolename, r.sortorder, '.
4039 'r.shortname AS roleshortname, rn.name AS rolecoursealias';
4042 // Prevent wrong function uses.
4043 if ((empty($roleid) || is_array($roleid)) && strpos($fields, 'ra.id') !== 0) {
4044 debugging('get_role_users() without specifying one single roleid needs to be called prefixing ' .
4045 'role assignments id (ra.id) as unique field, you can use $fields param for it.');
4047 if (!empty($roleid)) {
4048 // Solving partially the issue when specifying multiple roles.
4049 $users = array();
4050 foreach ($roleid as $id) {
4051 // Ignoring duplicated keys keeping the first user appearance.
4052 $users = $users + get_role_users($id, $context, $parent, $fields, $sort, $all, $group,
4053 $limitfrom, $limitnum, $extrawheretest, $whereorsortparams);
4055 return $users;
4059 $parentcontexts = '';
4060 if ($parent) {
4061 $parentcontexts = substr($context->path, 1); // kill leading slash
4062 $parentcontexts = str_replace('/', ',', $parentcontexts);
4063 if ($parentcontexts !== '') {
4064 $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
4068 if ($roleid) {
4069 list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'r');
4070 $roleselect = "AND ra.roleid $rids";
4071 } else {
4072 $params = array();
4073 $roleselect = '';
4076 if ($coursecontext = $context->get_course_context(false)) {
4077 $params['coursecontext'] = $coursecontext->id;
4078 } else {
4079 $params['coursecontext'] = 0;
4082 if ($group) {
4083 $groupjoin = "JOIN {groups_members} gm ON gm.userid = u.id";
4084 $groupselect = " AND gm.groupid = :groupid ";
4085 $params['groupid'] = $group;
4086 } else {
4087 $groupjoin = '';
4088 $groupselect = '';
4091 $params['contextid'] = $context->id;
4093 if ($extrawheretest) {
4094 $extrawheretest = ' AND ' . $extrawheretest;
4097 if ($whereorsortparams) {
4098 $params = array_merge($params, $whereorsortparams);
4101 if (!$sort) {
4102 list($sort, $sortparams) = users_order_by_sql('u');
4103 $params = array_merge($params, $sortparams);
4106 // Adding the fields from $sort that are not present in $fields.
4107 $sortarray = preg_split('/,\s*/', $sort);
4108 $fieldsarray = preg_split('/,\s*/', $fields);
4110 // Discarding aliases from the fields.
4111 $fieldnames = array();
4112 foreach ($fieldsarray as $key => $field) {
4113 list($fieldnames[$key]) = explode(' ', $field);
4116 $addedfields = array();
4117 foreach ($sortarray as $sortfield) {
4118 // Throw away any additional arguments to the sort (e.g. ASC/DESC).
4119 list($sortfield) = explode(' ', $sortfield);
4120 list($tableprefix) = explode('.', $sortfield);
4121 $fieldpresent = false;
4122 foreach ($fieldnames as $fieldname) {
4123 if ($fieldname === $sortfield || $fieldname === $tableprefix.'.*') {
4124 $fieldpresent = true;
4125 break;
4129 if (!$fieldpresent) {
4130 $fieldsarray[] = $sortfield;
4131 $addedfields[] = $sortfield;
4135 $fields = implode(', ', $fieldsarray);
4136 if (!empty($addedfields)) {
4137 $addedfields = implode(', ', $addedfields);
4138 debugging('get_role_users() adding '.$addedfields.' to the query result because they were required by $sort but missing in $fields');
4141 if ($all === null) {
4142 // Previously null was used to indicate that parameter was not used.
4143 $all = true;
4145 if (!$all and $coursecontext) {
4146 // Do not use get_enrolled_sql() here for performance reasons.
4147 $ejoin = "JOIN {user_enrolments} ue ON ue.userid = u.id
4148 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :ecourseid)";
4149 $params['ecourseid'] = $coursecontext->instanceid;
4150 } else {
4151 $ejoin = "";
4154 $sql = "SELECT DISTINCT $fields, ra.roleid
4155 FROM {role_assignments} ra
4156 JOIN {user} u ON u.id = ra.userid
4157 JOIN {role} r ON ra.roleid = r.id
4158 $ejoin
4159 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
4160 $groupjoin
4161 WHERE (ra.contextid = :contextid $parentcontexts)
4162 $roleselect
4163 $groupselect
4164 $extrawheretest
4165 ORDER BY $sort"; // join now so that we can just use fullname() later
4167 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
4171 * Counts all the users assigned this role in this context or higher
4173 * @param int|array $roleid either int or an array of ints
4174 * @param context $context
4175 * @param bool $parent if true, get list of users assigned in higher context too
4176 * @return int Returns the result count
4178 function count_role_users($roleid, context $context, $parent = false) {
4179 global $DB;
4181 if ($parent) {
4182 if ($contexts = $context->get_parent_context_ids()) {
4183 $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
4184 } else {
4185 $parentcontexts = '';
4187 } else {
4188 $parentcontexts = '';
4191 if ($roleid) {
4192 list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
4193 $roleselect = "AND r.roleid $rids";
4194 } else {
4195 $params = array();
4196 $roleselect = '';
4199 array_unshift($params, $context->id);
4201 $sql = "SELECT COUNT(DISTINCT u.id)
4202 FROM {role_assignments} r
4203 JOIN {user} u ON u.id = r.userid
4204 WHERE (r.contextid = ? $parentcontexts)
4205 $roleselect
4206 AND u.deleted = 0";
4208 return $DB->count_records_sql($sql, $params);
4212 * This function gets the list of course and course category contexts that this user has a particular capability in.
4214 * It is now reasonably efficient, but bear in mind that if there are users who have the capability
4215 * everywhere, it may return an array of all contexts.
4217 * @param string $capability Capability in question
4218 * @param int $userid User ID or null for current user
4219 * @param bool $getcategories Wether to return also course_categories
4220 * @param bool $doanything True if 'doanything' is permitted (default)
4221 * @param string $coursefieldsexceptid Leave blank if you only need 'id' in the course records;
4222 * otherwise use a comma-separated list of the fields you require, not including id.
4223 * Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
4224 * @param string $categoryfieldsexceptid Leave blank if you only need 'id' in the course records;
4225 * otherwise use a comma-separated list of the fields you require, not including id.
4226 * Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
4227 * @param string $courseorderby If set, use a comma-separated list of fields from course
4228 * table with sql modifiers (DESC) if needed
4229 * @param string $categoryorderby If set, use a comma-separated list of fields from course_category
4230 * table with sql modifiers (DESC) if needed
4231 * @param int $limit Limit the number of courses to return on success. Zero equals all entries.
4232 * @return array Array of categories and courses.
4234 function get_user_capability_contexts(string $capability, bool $getcategories, $userid = null, $doanything = true,
4235 $coursefieldsexceptid = '', $categoryfieldsexceptid = '', $courseorderby = '',
4236 $categoryorderby = '', $limit = 0): array {
4237 global $DB, $USER;
4239 // Default to current user.
4240 if (!$userid) {
4241 $userid = $USER->id;
4244 if (!$capinfo = get_capability_info($capability)) {
4245 debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
4246 return [false, false];
4249 if ($doanything && is_siteadmin($userid)) {
4250 // If the user is a site admin and $doanything is enabled then there is no need to restrict
4251 // the list of courses.
4252 $contextlimitsql = '';
4253 $contextlimitparams = [];
4254 } else {
4255 // Gets SQL to limit contexts ('x' table) to those where the user has this capability.
4256 list ($contextlimitsql, $contextlimitparams) = \core\access\get_user_capability_course_helper::get_sql(
4257 $userid, $capinfo->name);
4258 if (!$contextlimitsql) {
4259 // If the does not have this capability in any context, return false without querying.
4260 return [false, false];
4263 $contextlimitsql = 'WHERE' . $contextlimitsql;
4266 $categories = [];
4267 if ($getcategories) {
4268 $fieldlist = \core\access\get_user_capability_course_helper::map_fieldnames($categoryfieldsexceptid);
4269 if ($categoryorderby) {
4270 $fields = explode(',', $categoryorderby);
4271 $orderby = '';
4272 foreach ($fields as $field) {
4273 if ($orderby) {
4274 $orderby .= ',';
4276 $orderby .= 'c.'.$field;
4278 $orderby = 'ORDER BY '.$orderby;
4280 $rs = $DB->get_recordset_sql("
4281 SELECT c.id $fieldlist
4282 FROM {course_categories} c
4283 JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ?
4284 $contextlimitsql
4285 $orderby", array_merge([CONTEXT_COURSECAT], $contextlimitparams));
4286 $basedlimit = $limit;
4287 foreach ($rs as $category) {
4288 $categories[] = $category;
4289 $basedlimit--;
4290 if ($basedlimit == 0) {
4291 break;
4296 $courses = [];
4297 $fieldlist = \core\access\get_user_capability_course_helper::map_fieldnames($coursefieldsexceptid);
4298 if ($courseorderby) {
4299 $fields = explode(',', $courseorderby);
4300 $courseorderby = '';
4301 foreach ($fields as $field) {
4302 if ($courseorderby) {
4303 $courseorderby .= ',';
4305 $courseorderby .= 'c.'.$field;
4307 $courseorderby = 'ORDER BY '.$courseorderby;
4309 $rs = $DB->get_recordset_sql("
4310 SELECT c.id $fieldlist
4311 FROM {course} c
4312 JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ?
4313 $contextlimitsql
4314 $courseorderby", array_merge([CONTEXT_COURSE], $contextlimitparams));
4315 foreach ($rs as $course) {
4316 $courses[] = $course;
4317 $limit--;
4318 if ($limit == 0) {
4319 break;
4322 $rs->close();
4323 return [$categories, $courses];
4327 * This function gets the list of courses that this user has a particular capability in.
4329 * It is now reasonably efficient, but bear in mind that if there are users who have the capability
4330 * everywhere, it may return an array of all courses.
4332 * @param string $capability Capability in question
4333 * @param int $userid User ID or null for current user
4334 * @param bool $doanything True if 'doanything' is permitted (default)
4335 * @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
4336 * otherwise use a comma-separated list of the fields you require, not including id.
4337 * Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
4338 * @param string $orderby If set, use a comma-separated list of fields from course
4339 * table with sql modifiers (DESC) if needed
4340 * @param int $limit Limit the number of courses to return on success. Zero equals all entries.
4341 * @return array|bool Array of courses, if none found false is returned.
4343 function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '',
4344 $orderby = '', $limit = 0) {
4345 list($categories, $courses) = get_user_capability_contexts(
4346 $capability,
4347 false,
4348 $userid,
4349 $doanything,
4350 $fieldsexceptid,
4352 $orderby,
4354 $limit
4356 return $courses;
4360 * Switches the current user to another role for the current session and only
4361 * in the given context.
4363 * The caller *must* check
4364 * - that this op is allowed
4365 * - that the requested role can be switched to in this context (use get_switchable_roles)
4366 * - that the requested role is NOT $CFG->defaultuserroleid
4368 * To "unswitch" pass 0 as the roleid.
4370 * This function *will* modify $USER->access - beware
4372 * @param integer $roleid the role to switch to.
4373 * @param context $context the context in which to perform the switch.
4374 * @return bool success or failure.
4376 function role_switch($roleid, context $context) {
4377 global $USER;
4379 // Add the ghost RA to $USER->access as $USER->access['rsw'][$path] = $roleid.
4380 // To un-switch just unset($USER->access['rsw'][$path]).
4382 // Note: it is not possible to switch to roles that do not have course:view
4384 if (!isset($USER->access)) {
4385 load_all_capabilities();
4388 // Add the switch RA
4389 if ($roleid == 0) {
4390 unset($USER->access['rsw'][$context->path]);
4391 return true;
4394 $USER->access['rsw'][$context->path] = $roleid;
4396 return true;
4400 * Checks if the user has switched roles within the given course.
4402 * Note: You can only switch roles within the course, hence it takes a course id
4403 * rather than a context. On that note Petr volunteered to implement this across
4404 * all other contexts, all requests for this should be forwarded to him ;)
4406 * @param int $courseid The id of the course to check
4407 * @return bool True if the user has switched roles within the course.
4409 function is_role_switched($courseid) {
4410 global $USER;
4411 $context = context_course::instance($courseid, MUST_EXIST);
4412 return (!empty($USER->access['rsw'][$context->path]));
4416 * Get any role that has an override on exact context
4418 * @param context $context The context to check
4419 * @return array An array of roles
4421 function get_roles_with_override_on_context(context $context) {
4422 global $DB;
4424 return $DB->get_records_sql("SELECT r.*
4425 FROM {role_capabilities} rc, {role} r
4426 WHERE rc.roleid = r.id AND rc.contextid = ?",
4427 array($context->id));
4431 * Get all capabilities for this role on this context (overrides)
4433 * @param stdClass $role
4434 * @param context $context
4435 * @return array
4437 function get_capabilities_from_role_on_context($role, context $context) {
4438 global $DB;
4440 return $DB->get_records_sql("SELECT *
4441 FROM {role_capabilities}
4442 WHERE contextid = ? AND roleid = ?",
4443 array($context->id, $role->id));
4447 * Find all user assignment of users for this role, on this context
4449 * @param stdClass $role
4450 * @param context $context
4451 * @return array
4453 function get_users_from_role_on_context($role, context $context) {
4454 global $DB;
4456 return $DB->get_records_sql("SELECT *
4457 FROM {role_assignments}
4458 WHERE contextid = ? AND roleid = ?",
4459 array($context->id, $role->id));
4463 * Simple function returning a boolean true if user has roles
4464 * in context or parent contexts, otherwise false.
4466 * @param int $userid
4467 * @param int $roleid
4468 * @param int $contextid empty means any context
4469 * @return bool
4471 function user_has_role_assignment($userid, $roleid, $contextid = 0) {
4472 global $DB;
4474 if ($contextid) {
4475 if (!$context = context::instance_by_id($contextid, IGNORE_MISSING)) {
4476 return false;
4478 $parents = $context->get_parent_context_ids(true);
4479 list($contexts, $params) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED, 'r');
4480 $params['userid'] = $userid;
4481 $params['roleid'] = $roleid;
4483 $sql = "SELECT COUNT(ra.id)
4484 FROM {role_assignments} ra
4485 WHERE ra.userid = :userid AND ra.roleid = :roleid AND ra.contextid $contexts";
4487 $count = $DB->get_field_sql($sql, $params);
4488 return ($count > 0);
4490 } else {
4491 return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid));
4496 * Get localised role name or alias if exists and format the text.
4498 * @param stdClass $role role object
4499 * - optional 'coursealias' property should be included for performance reasons if course context used
4500 * - description property is not required here
4501 * @param context|bool $context empty means system context
4502 * @param int $rolenamedisplay type of role name
4503 * @return string localised role name or course role name alias
4505 function role_get_name(stdClass $role, $context = null, $rolenamedisplay = ROLENAME_ALIAS) {
4506 global $DB;
4508 if ($rolenamedisplay == ROLENAME_SHORT) {
4509 return $role->shortname;
4512 if (!$context or !$coursecontext = $context->get_course_context(false)) {
4513 $coursecontext = null;
4516 if ($coursecontext and !property_exists($role, 'coursealias') and ($rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH or $rolenamedisplay == ROLENAME_ALIAS_RAW)) {
4517 $role = clone($role); // Do not modify parameters.
4518 if ($r = $DB->get_record('role_names', array('roleid'=>$role->id, 'contextid'=>$coursecontext->id))) {
4519 $role->coursealias = $r->name;
4520 } else {
4521 $role->coursealias = null;
4525 if ($rolenamedisplay == ROLENAME_ALIAS_RAW) {
4526 if ($coursecontext) {
4527 return $role->coursealias;
4528 } else {
4529 return null;
4533 if (trim($role->name) !== '') {
4534 // For filtering always use context where was the thing defined - system for roles here.
4535 $original = format_string($role->name, true, array('context'=>context_system::instance()));
4537 } else {
4538 // Empty role->name means we want to see localised role name based on shortname,
4539 // only default roles are supposed to be localised.
4540 switch ($role->shortname) {
4541 case 'manager': $original = get_string('manager', 'role'); break;
4542 case 'coursecreator': $original = get_string('coursecreators'); break;
4543 case 'editingteacher': $original = get_string('defaultcourseteacher'); break;
4544 case 'teacher': $original = get_string('noneditingteacher'); break;
4545 case 'student': $original = get_string('defaultcoursestudent'); break;
4546 case 'guest': $original = get_string('guest'); break;
4547 case 'user': $original = get_string('authenticateduser'); break;
4548 case 'frontpage': $original = get_string('frontpageuser', 'role'); break;
4549 // We should not get here, the role UI should require the name for custom roles!
4550 default: $original = $role->shortname; break;
4554 if ($rolenamedisplay == ROLENAME_ORIGINAL) {
4555 return $original;
4558 if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
4559 return "$original ($role->shortname)";
4562 if ($rolenamedisplay == ROLENAME_ALIAS) {
4563 if ($coursecontext && $role->coursealias && trim($role->coursealias) !== '') {
4564 return format_string($role->coursealias, true, array('context'=>$coursecontext));
4565 } else {
4566 return $original;
4570 if ($rolenamedisplay == ROLENAME_BOTH) {
4571 if ($coursecontext && $role->coursealias && trim($role->coursealias) !== '') {
4572 return format_string($role->coursealias, true, array('context'=>$coursecontext)) . " ($original)";
4573 } else {
4574 return $original;
4578 throw new coding_exception('Invalid $rolenamedisplay parameter specified in role_get_name()');
4582 * Returns localised role description if available.
4583 * If the name is empty it tries to find the default role name using
4584 * hardcoded list of default role names or other methods in the future.
4586 * @param stdClass $role
4587 * @return string localised role name
4589 function role_get_description(stdClass $role) {
4590 if (!html_is_blank($role->description)) {
4591 return format_text($role->description, FORMAT_HTML, array('context'=>context_system::instance()));
4594 switch ($role->shortname) {
4595 case 'manager': return get_string('managerdescription', 'role');
4596 case 'coursecreator': return get_string('coursecreatorsdescription');
4597 case 'editingteacher': return get_string('defaultcourseteacherdescription');
4598 case 'teacher': return get_string('noneditingteacherdescription');
4599 case 'student': return get_string('defaultcoursestudentdescription');
4600 case 'guest': return get_string('guestdescription');
4601 case 'user': return get_string('authenticateduserdescription');
4602 case 'frontpage': return get_string('frontpageuserdescription', 'role');
4603 default: return '';
4608 * Get all the localised role names for a context.
4610 * In new installs default roles have empty names, this function
4611 * add localised role names using current language pack.
4613 * @param context $context the context, null means system context
4614 * @param array of role objects with a ->localname field containing the context-specific role name.
4615 * @param int $rolenamedisplay
4616 * @param bool $returnmenu true means id=>localname, false means id=>rolerecord
4617 * @return array Array of context-specific role names, or role objects with a ->localname field added.
4619 function role_get_names(context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
4620 return role_fix_names(get_all_roles($context), $context, $rolenamedisplay, $returnmenu);
4624 * Prepare list of roles for display, apply aliases and localise default role names.
4626 * @param array $roleoptions array roleid => roleobject (with optional coursealias), strings are accepted for backwards compatibility only
4627 * @param context $context the context, null means system context
4628 * @param int $rolenamedisplay
4629 * @param bool $returnmenu null means keep the same format as $roleoptions, true means id=>localname, false means id=>rolerecord
4630 * @return array Array of context-specific role names, or role objects with a ->localname field added.
4632 function role_fix_names($roleoptions, context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
4633 global $DB;
4635 if (empty($roleoptions)) {
4636 return array();
4639 if (!$context or !$coursecontext = $context->get_course_context(false)) {
4640 $coursecontext = null;
4643 // We usually need all role columns...
4644 $first = reset($roleoptions);
4645 if ($returnmenu === null) {
4646 $returnmenu = !is_object($first);
4649 if (!is_object($first) or !property_exists($first, 'shortname')) {
4650 $allroles = get_all_roles($context);
4651 foreach ($roleoptions as $rid => $unused) {
4652 $roleoptions[$rid] = $allroles[$rid];
4656 // Inject coursealias if necessary.
4657 if ($coursecontext and ($rolenamedisplay == ROLENAME_ALIAS_RAW or $rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH)) {
4658 $first = reset($roleoptions);
4659 if (!property_exists($first, 'coursealias')) {
4660 $aliasnames = $DB->get_records('role_names', array('contextid'=>$coursecontext->id));
4661 foreach ($aliasnames as $alias) {
4662 if (isset($roleoptions[$alias->roleid])) {
4663 $roleoptions[$alias->roleid]->coursealias = $alias->name;
4669 // Add localname property.
4670 foreach ($roleoptions as $rid => $role) {
4671 $roleoptions[$rid]->localname = role_get_name($role, $coursecontext, $rolenamedisplay);
4674 if (!$returnmenu) {
4675 return $roleoptions;
4678 $menu = array();
4679 foreach ($roleoptions as $rid => $role) {
4680 $menu[$rid] = $role->localname;
4683 return $menu;
4687 * Aids in detecting if a new line is required when reading a new capability
4689 * This function helps admin/roles/manage.php etc to detect if a new line should be printed
4690 * when we read in a new capability.
4691 * Most of the time, if the 2 components are different we should print a new line, (e.g. course system->rss client)
4692 * but when we are in grade, all reports/import/export capabilities should be together
4694 * @param string $cap component string a
4695 * @param string $comp component string b
4696 * @param int $contextlevel
4697 * @return bool whether 2 component are in different "sections"
4699 function component_level_changed($cap, $comp, $contextlevel) {
4701 if (strstr($cap->component, '/') && strstr($comp, '/')) {
4702 $compsa = explode('/', $cap->component);
4703 $compsb = explode('/', $comp);
4705 // list of system reports
4706 if (($compsa[0] == 'report') && ($compsb[0] == 'report')) {
4707 return false;
4710 // we are in gradebook, still
4711 if (($compsa[0] == 'gradeexport' || $compsa[0] == 'gradeimport' || $compsa[0] == 'gradereport') &&
4712 ($compsb[0] == 'gradeexport' || $compsb[0] == 'gradeimport' || $compsb[0] == 'gradereport')) {
4713 return false;
4716 if (($compsa[0] == 'coursereport') && ($compsb[0] == 'coursereport')) {
4717 return false;
4721 return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
4725 * Fix the roles.sortorder field in the database, so it contains sequential integers,
4726 * and return an array of roleids in order.
4728 * @param array $allroles array of roles, as returned by get_all_roles();
4729 * @return array $role->sortorder =-> $role->id with the keys in ascending order.
4731 function fix_role_sortorder($allroles) {
4732 global $DB;
4734 $rolesort = array();
4735 $i = 0;
4736 foreach ($allroles as $role) {
4737 $rolesort[$i] = $role->id;
4738 if ($role->sortorder != $i) {
4739 $r = new stdClass();
4740 $r->id = $role->id;
4741 $r->sortorder = $i;
4742 $DB->update_record('role', $r);
4743 $allroles[$role->id]->sortorder = $i;
4745 $i++;
4747 return $rolesort;
4751 * Switch the sort order of two roles (used in admin/roles/manage.php).
4753 * @param stdClass $first The first role. Actually, only ->sortorder is used.
4754 * @param stdClass $second The second role. Actually, only ->sortorder is used.
4755 * @return boolean success or failure
4757 function switch_roles($first, $second) {
4758 global $DB;
4759 $temp = $DB->get_field('role', 'MAX(sortorder) + 1', array());
4760 $result = $DB->set_field('role', 'sortorder', $temp, array('sortorder' => $first->sortorder));
4761 $result = $result && $DB->set_field('role', 'sortorder', $first->sortorder, array('sortorder' => $second->sortorder));
4762 $result = $result && $DB->set_field('role', 'sortorder', $second->sortorder, array('sortorder' => $temp));
4763 return $result;
4767 * Duplicates all the base definitions of a role
4769 * @param stdClass $sourcerole role to copy from
4770 * @param int $targetrole id of role to copy to
4772 function role_cap_duplicate($sourcerole, $targetrole) {
4773 global $DB;
4775 $systemcontext = context_system::instance();
4776 $caps = $DB->get_records_sql("SELECT *
4777 FROM {role_capabilities}
4778 WHERE roleid = ? AND contextid = ?",
4779 array($sourcerole->id, $systemcontext->id));
4780 // adding capabilities
4781 foreach ($caps as $cap) {
4782 unset($cap->id);
4783 $cap->roleid = $targetrole;
4784 $DB->insert_record('role_capabilities', $cap);
4787 // Reset any cache of this role, including MUC.
4788 accesslib_clear_role_cache($targetrole);
4792 * Returns two lists, this can be used to find out if user has capability.
4793 * Having any needed role and no forbidden role in this context means
4794 * user has this capability in this context.
4795 * Use get_role_names_with_cap_in_context() if you need role names to display in the UI
4797 * @param stdClass $context
4798 * @param string $capability
4799 * @return array($neededroles, $forbiddenroles)
4801 function get_roles_with_cap_in_context($context, $capability) {
4802 global $DB;
4804 $ctxids = trim($context->path, '/'); // kill leading slash
4805 $ctxids = str_replace('/', ',', $ctxids);
4807 $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
4808 FROM {role_capabilities} rc
4809 JOIN {context} ctx ON ctx.id = rc.contextid
4810 JOIN {capabilities} cap ON rc.capability = cap.name
4811 WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
4812 ORDER BY rc.roleid ASC, ctx.depth DESC";
4813 $params = array('cap'=>$capability);
4815 if (!$capdefs = $DB->get_records_sql($sql, $params)) {
4816 // no cap definitions --> no capability
4817 return array(array(), array());
4820 $forbidden = array();
4821 $needed = array();
4822 foreach ($capdefs as $def) {
4823 if (isset($forbidden[$def->roleid])) {
4824 continue;
4826 if ($def->permission == CAP_PROHIBIT) {
4827 $forbidden[$def->roleid] = $def->roleid;
4828 unset($needed[$def->roleid]);
4829 continue;
4831 if (!isset($needed[$def->roleid])) {
4832 if ($def->permission == CAP_ALLOW) {
4833 $needed[$def->roleid] = true;
4834 } else if ($def->permission == CAP_PREVENT) {
4835 $needed[$def->roleid] = false;
4839 unset($capdefs);
4841 // remove all those roles not allowing
4842 foreach ($needed as $key=>$value) {
4843 if (!$value) {
4844 unset($needed[$key]);
4845 } else {
4846 $needed[$key] = $key;
4850 return array($needed, $forbidden);
4854 * Returns an array of role IDs that have ALL of the the supplied capabilities
4855 * Uses get_roles_with_cap_in_context(). Returns $allowed minus $forbidden
4857 * @param stdClass $context
4858 * @param array $capabilities An array of capabilities
4859 * @return array of roles with all of the required capabilities
4861 function get_roles_with_caps_in_context($context, $capabilities) {
4862 $neededarr = array();
4863 $forbiddenarr = array();
4864 foreach ($capabilities as $caprequired) {
4865 list($neededarr[], $forbiddenarr[]) = get_roles_with_cap_in_context($context, $caprequired);
4868 $rolesthatcanrate = array();
4869 if (!empty($neededarr)) {
4870 foreach ($neededarr as $needed) {
4871 if (empty($rolesthatcanrate)) {
4872 $rolesthatcanrate = $needed;
4873 } else {
4874 //only want roles that have all caps
4875 $rolesthatcanrate = array_intersect_key($rolesthatcanrate,$needed);
4879 if (!empty($forbiddenarr) && !empty($rolesthatcanrate)) {
4880 foreach ($forbiddenarr as $forbidden) {
4881 //remove any roles that are forbidden any of the caps
4882 $rolesthatcanrate = array_diff($rolesthatcanrate, $forbidden);
4885 return $rolesthatcanrate;
4889 * Returns an array of role names that have ALL of the the supplied capabilities
4890 * Uses get_roles_with_caps_in_context(). Returns $allowed minus $forbidden
4892 * @param stdClass $context
4893 * @param array $capabilities An array of capabilities
4894 * @return array of roles with all of the required capabilities
4896 function get_role_names_with_caps_in_context($context, $capabilities) {
4897 global $DB;
4899 $rolesthatcanrate = get_roles_with_caps_in_context($context, $capabilities);
4900 $allroles = $DB->get_records('role', null, 'sortorder DESC');
4902 $roles = array();
4903 foreach ($rolesthatcanrate as $r) {
4904 $roles[$r] = $allroles[$r];
4907 return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
4911 * This function verifies the prohibit comes from this context
4912 * and there are no more prohibits in parent contexts.
4914 * @param int $roleid
4915 * @param context $context
4916 * @param string $capability name
4917 * @return bool
4919 function prohibit_is_removable($roleid, context $context, $capability) {
4920 global $DB;
4922 $ctxids = trim($context->path, '/'); // kill leading slash
4923 $ctxids = str_replace('/', ',', $ctxids);
4925 $params = array('roleid'=>$roleid, 'cap'=>$capability, 'prohibit'=>CAP_PROHIBIT);
4927 $sql = "SELECT ctx.id
4928 FROM {role_capabilities} rc
4929 JOIN {context} ctx ON ctx.id = rc.contextid
4930 JOIN {capabilities} cap ON rc.capability = cap.name
4931 WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
4932 ORDER BY ctx.depth DESC";
4934 if (!$prohibits = $DB->get_records_sql($sql, $params)) {
4935 // no prohibits == nothing to remove
4936 return true;
4939 if (count($prohibits) > 1) {
4940 // more prohibits can not be removed
4941 return false;
4944 return !empty($prohibits[$context->id]);
4948 * More user friendly role permission changing,
4949 * it should produce as few overrides as possible.
4951 * @param int $roleid
4952 * @param stdClass $context
4953 * @param string $capname capability name
4954 * @param int $permission
4955 * @return void
4957 function role_change_permission($roleid, $context, $capname, $permission) {
4958 global $DB;
4960 if ($permission == CAP_INHERIT) {
4961 unassign_capability($capname, $roleid, $context->id);
4962 return;
4965 $ctxids = trim($context->path, '/'); // kill leading slash
4966 $ctxids = str_replace('/', ',', $ctxids);
4968 $params = array('roleid'=>$roleid, 'cap'=>$capname);
4970 $sql = "SELECT ctx.id, rc.permission, ctx.depth
4971 FROM {role_capabilities} rc
4972 JOIN {context} ctx ON ctx.id = rc.contextid
4973 JOIN {capabilities} cap ON rc.capability = cap.name
4974 WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
4975 ORDER BY ctx.depth DESC";
4977 if ($existing = $DB->get_records_sql($sql, $params)) {
4978 foreach ($existing as $e) {
4979 if ($e->permission == CAP_PROHIBIT) {
4980 // prohibit can not be overridden, no point in changing anything
4981 return;
4984 $lowest = array_shift($existing);
4985 if ($lowest->permission == $permission) {
4986 // permission already set in this context or parent - nothing to do
4987 return;
4989 if ($existing) {
4990 $parent = array_shift($existing);
4991 if ($parent->permission == $permission) {
4992 // permission already set in parent context or parent - just unset in this context
4993 // we do this because we want as few overrides as possible for performance reasons
4994 unassign_capability($capname, $roleid, $context->id);
4995 return;
4999 } else {
5000 if ($permission == CAP_PREVENT) {
5001 // nothing means role does not have permission
5002 return;
5006 // assign the needed capability
5007 assign_capability($capname, $permission, $roleid, $context->id, true);
5012 * Basic moodle context abstraction class.
5014 * Google confirms that no other important framework is using "context" class,
5015 * we could use something else like mcontext or moodle_context, but we need to type
5016 * this very often which would be annoying and it would take too much space...
5018 * This class is derived from stdClass for backwards compatibility with
5019 * odl $context record that was returned from DML $DB->get_record()
5021 * @package core_access
5022 * @category access
5023 * @copyright Petr Skoda {@link http://skodak.org}
5024 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5025 * @since Moodle 2.2
5027 * @property-read int $id context id
5028 * @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc.
5029 * @property-read int $instanceid id of related instance in each context
5030 * @property-read string $path path to context, starts with system context
5031 * @property-read int $depth
5033 abstract class context extends stdClass implements IteratorAggregate {
5035 /** @var string Default sorting of capabilities in {@see get_capabilities} */
5036 protected const DEFAULT_CAPABILITY_SORT = 'contextlevel, component, name';
5039 * The context id
5040 * Can be accessed publicly through $context->id
5041 * @var int
5043 protected $_id;
5046 * The context level
5047 * Can be accessed publicly through $context->contextlevel
5048 * @var int One of CONTEXT_* e.g. CONTEXT_COURSE, CONTEXT_MODULE
5050 protected $_contextlevel;
5053 * Id of the item this context is related to e.g. COURSE_CONTEXT => course.id
5054 * Can be accessed publicly through $context->instanceid
5055 * @var int
5057 protected $_instanceid;
5060 * The path to the context always starting from the system context
5061 * Can be accessed publicly through $context->path
5062 * @var string
5064 protected $_path;
5067 * The depth of the context in relation to parent contexts
5068 * Can be accessed publicly through $context->depth
5069 * @var int
5071 protected $_depth;
5074 * Whether this context is locked or not.
5076 * Can be accessed publicly through $context->locked.
5078 * @var int
5080 protected $_locked;
5083 * @var array Context caching info
5085 private static $cache_contextsbyid = array();
5088 * @var array Context caching info
5090 private static $cache_contexts = array();
5093 * Context count
5094 * Why do we do count contexts? Because count($array) is horribly slow for large arrays
5095 * @var int
5097 protected static $cache_count = 0;
5100 * @var array Context caching info
5102 protected static $cache_preloaded = array();
5105 * @var context_system The system context once initialised
5107 protected static $systemcontext = null;
5110 * Resets the cache to remove all data.
5111 * @static
5113 protected static function reset_caches() {
5114 self::$cache_contextsbyid = array();
5115 self::$cache_contexts = array();
5116 self::$cache_count = 0;
5117 self::$cache_preloaded = array();
5119 self::$systemcontext = null;
5123 * Adds a context to the cache. If the cache is full, discards a batch of
5124 * older entries.
5126 * @static
5127 * @param context $context New context to add
5128 * @return void
5130 protected static function cache_add(context $context) {
5131 if (isset(self::$cache_contextsbyid[$context->id])) {
5132 // already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow
5133 return;
5136 if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) {
5137 $i = 0;
5138 foreach (self::$cache_contextsbyid as $ctx) {
5139 $i++;
5140 if ($i <= 100) {
5141 // we want to keep the first contexts to be loaded on this page, hopefully they will be needed again later
5142 continue;
5144 if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
5145 // we remove oldest third of the contexts to make room for more contexts
5146 break;
5148 unset(self::$cache_contextsbyid[$ctx->id]);
5149 unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
5150 self::$cache_count--;
5154 self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
5155 self::$cache_contextsbyid[$context->id] = $context;
5156 self::$cache_count++;
5160 * Removes a context from the cache.
5162 * @static
5163 * @param context $context Context object to remove
5164 * @return void
5166 protected static function cache_remove(context $context) {
5167 if (!isset(self::$cache_contextsbyid[$context->id])) {
5168 // not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow
5169 return;
5171 unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
5172 unset(self::$cache_contextsbyid[$context->id]);
5174 self::$cache_count--;
5176 if (self::$cache_count < 0) {
5177 self::$cache_count = 0;
5182 * Gets a context from the cache.
5184 * @static
5185 * @param int $contextlevel Context level
5186 * @param int $instance Instance ID
5187 * @return context|bool Context or false if not in cache
5189 protected static function cache_get($contextlevel, $instance) {
5190 if (isset(self::$cache_contexts[$contextlevel][$instance])) {
5191 return self::$cache_contexts[$contextlevel][$instance];
5193 return false;
5197 * Gets a context from the cache based on its id.
5199 * @static
5200 * @param int $id Context ID
5201 * @return context|bool Context or false if not in cache
5203 protected static function cache_get_by_id($id) {
5204 if (isset(self::$cache_contextsbyid[$id])) {
5205 return self::$cache_contextsbyid[$id];
5207 return false;
5211 * Preloads context information from db record and strips the cached info.
5213 * @static
5214 * @param stdClass $rec
5215 * @return void (modifies $rec)
5217 protected static function preload_from_record(stdClass $rec) {
5218 $notenoughdata = false;
5219 $notenoughdata = $notenoughdata || empty($rec->ctxid);
5220 $notenoughdata = $notenoughdata || empty($rec->ctxlevel);
5221 $notenoughdata = $notenoughdata || !isset($rec->ctxinstance);
5222 $notenoughdata = $notenoughdata || empty($rec->ctxpath);
5223 $notenoughdata = $notenoughdata || empty($rec->ctxdepth);
5224 $notenoughdata = $notenoughdata || !isset($rec->ctxlocked);
5225 if ($notenoughdata) {
5226 // The record does not have enough data, passed here repeatedly or context does not exist yet.
5227 if (isset($rec->ctxid) && !isset($rec->ctxlocked)) {
5228 debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
5230 return;
5233 $record = (object) [
5234 'id' => $rec->ctxid,
5235 'contextlevel' => $rec->ctxlevel,
5236 'instanceid' => $rec->ctxinstance,
5237 'path' => $rec->ctxpath,
5238 'depth' => $rec->ctxdepth,
5239 'locked' => $rec->ctxlocked,
5242 unset($rec->ctxid);
5243 unset($rec->ctxlevel);
5244 unset($rec->ctxinstance);
5245 unset($rec->ctxpath);
5246 unset($rec->ctxdepth);
5247 unset($rec->ctxlocked);
5249 return context::create_instance_from_record($record);
5253 // ====== magic methods =======
5256 * Magic setter method, we do not want anybody to modify properties from the outside
5257 * @param string $name
5258 * @param mixed $value
5260 public function __set($name, $value) {
5261 debugging('Can not change context instance properties!');
5265 * Magic method getter, redirects to read only values.
5266 * @param string $name
5267 * @return mixed
5269 public function __get($name) {
5270 switch ($name) {
5271 case 'id':
5272 return $this->_id;
5273 case 'contextlevel':
5274 return $this->_contextlevel;
5275 case 'instanceid':
5276 return $this->_instanceid;
5277 case 'path':
5278 return $this->_path;
5279 case 'depth':
5280 return $this->_depth;
5281 case 'locked':
5282 return $this->is_locked();
5284 default:
5285 debugging('Invalid context property accessed! '.$name);
5286 return null;
5291 * Full support for isset on our magic read only properties.
5292 * @param string $name
5293 * @return bool
5295 public function __isset($name) {
5296 switch ($name) {
5297 case 'id':
5298 return isset($this->_id);
5299 case 'contextlevel':
5300 return isset($this->_contextlevel);
5301 case 'instanceid':
5302 return isset($this->_instanceid);
5303 case 'path':
5304 return isset($this->_path);
5305 case 'depth':
5306 return isset($this->_depth);
5307 case 'locked':
5308 // Locked is always set.
5309 return true;
5310 default:
5311 return false;
5316 * All properties are read only, sorry.
5317 * @param string $name
5319 public function __unset($name) {
5320 debugging('Can not unset context instance properties!');
5323 // ====== implementing method from interface IteratorAggregate ======
5326 * Create an iterator because magic vars can't be seen by 'foreach'.
5328 * Now we can convert context object to array using convert_to_array(),
5329 * and feed it properly to json_encode().
5331 public function getIterator(): Traversable {
5332 $ret = array(
5333 'id' => $this->id,
5334 'contextlevel' => $this->contextlevel,
5335 'instanceid' => $this->instanceid,
5336 'path' => $this->path,
5337 'depth' => $this->depth,
5338 'locked' => $this->locked,
5340 return new ArrayIterator($ret);
5343 // ====== general context methods ======
5346 * Constructor is protected so that devs are forced to
5347 * use context_xxx::instance() or context::instance_by_id().
5349 * @param stdClass $record
5351 protected function __construct(stdClass $record) {
5352 $this->_id = (int)$record->id;
5353 $this->_contextlevel = (int)$record->contextlevel;
5354 $this->_instanceid = $record->instanceid;
5355 $this->_path = $record->path;
5356 $this->_depth = $record->depth;
5358 if (isset($record->locked)) {
5359 $this->_locked = $record->locked;
5360 } else if (!during_initial_install() && !moodle_needs_upgrading()) {
5361 debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
5366 * This function is also used to work around 'protected' keyword problems in context_helper.
5367 * @static
5368 * @param stdClass $record
5369 * @return context instance
5371 protected static function create_instance_from_record(stdClass $record) {
5372 $classname = context_helper::get_class_for_level($record->contextlevel);
5374 if ($context = context::cache_get_by_id($record->id)) {
5375 return $context;
5378 $context = new $classname($record);
5379 context::cache_add($context);
5381 return $context;
5385 * Copy prepared new contexts from temp table to context table,
5386 * we do this in db specific way for perf reasons only.
5387 * @static
5389 protected static function merge_context_temp_table() {
5390 global $DB;
5392 /* MDL-11347:
5393 * - mysql does not allow to use FROM in UPDATE statements
5394 * - using two tables after UPDATE works in mysql, but might give unexpected
5395 * results in pg 8 (depends on configuration)
5396 * - using table alias in UPDATE does not work in pg < 8.2
5398 * Different code for each database - mostly for performance reasons
5401 $dbfamily = $DB->get_dbfamily();
5402 if ($dbfamily == 'mysql') {
5403 $updatesql = "UPDATE {context} ct, {context_temp} temp
5404 SET ct.path = temp.path,
5405 ct.depth = temp.depth,
5406 ct.locked = temp.locked
5407 WHERE ct.id = temp.id";
5408 } else if ($dbfamily == 'oracle') {
5409 $updatesql = "UPDATE {context} ct
5410 SET (ct.path, ct.depth, ct.locked) =
5411 (SELECT temp.path, temp.depth, temp.locked
5412 FROM {context_temp} temp
5413 WHERE temp.id=ct.id)
5414 WHERE EXISTS (SELECT 'x'
5415 FROM {context_temp} temp
5416 WHERE temp.id = ct.id)";
5417 } else if ($dbfamily == 'postgres' or $dbfamily == 'mssql') {
5418 $updatesql = "UPDATE {context}
5419 SET path = temp.path,
5420 depth = temp.depth,
5421 locked = temp.locked
5422 FROM {context_temp} temp
5423 WHERE temp.id={context}.id";
5424 } else {
5425 // sqlite and others
5426 $updatesql = "UPDATE {context}
5427 SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
5428 depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id),
5429 locked = (SELECT locked FROM {context_temp} WHERE id = {context}.id)
5430 WHERE id IN (SELECT id FROM {context_temp})";
5433 $DB->execute($updatesql);
5437 * Get a context instance as an object, from a given context id.
5439 * @static
5440 * @param int $id context id
5441 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
5442 * MUST_EXIST means throw exception if no record found
5443 * @return context|bool the context object or false if not found
5445 public static function instance_by_id($id, $strictness = MUST_EXIST) {
5446 global $DB;
5448 if (get_called_class() !== 'context' and get_called_class() !== 'context_helper') {
5449 // some devs might confuse context->id and instanceid, better prevent these mistakes completely
5450 throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
5453 if ($id == SYSCONTEXTID) {
5454 return context_system::instance(0, $strictness);
5457 if (is_array($id) or is_object($id) or empty($id)) {
5458 throw new coding_exception('Invalid context id specified context::instance_by_id()');
5461 if ($context = context::cache_get_by_id($id)) {
5462 return $context;
5465 if ($record = $DB->get_record('context', array('id'=>$id), '*', $strictness)) {
5466 return context::create_instance_from_record($record);
5469 return false;
5473 * Update context info after moving context in the tree structure.
5475 * @param context $newparent
5476 * @return void
5478 public function update_moved(context $newparent) {
5479 global $DB;
5481 $frompath = $this->_path;
5482 $newpath = $newparent->path . '/' . $this->_id;
5484 $trans = $DB->start_delegated_transaction();
5486 $setdepth = '';
5487 if (($newparent->depth +1) != $this->_depth) {
5488 $diff = $newparent->depth - $this->_depth + 1;
5489 $setdepth = ", depth = depth + $diff";
5491 $sql = "UPDATE {context}
5492 SET path = ?
5493 $setdepth
5494 WHERE id = ?";
5495 $params = array($newpath, $this->_id);
5496 $DB->execute($sql, $params);
5498 $this->_path = $newpath;
5499 $this->_depth = $newparent->depth + 1;
5501 $sql = "UPDATE {context}
5502 SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath)+1))."
5503 $setdepth
5504 WHERE path LIKE ?";
5505 $params = array($newpath, "{$frompath}/%");
5506 $DB->execute($sql, $params);
5508 $this->mark_dirty();
5510 context::reset_caches();
5512 $trans->allow_commit();
5516 * Set whether this context has been locked or not.
5518 * @param bool $locked
5519 * @return $this
5521 public function set_locked(bool $locked) {
5522 global $DB;
5524 if ($this->_locked == $locked) {
5525 return $this;
5528 $this->_locked = $locked;
5529 $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
5530 $this->mark_dirty();
5532 if ($locked) {
5533 $eventname = '\\core\\event\\context_locked';
5534 } else {
5535 $eventname = '\\core\\event\\context_unlocked';
5537 $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
5538 $event->trigger();
5540 self::reset_caches();
5542 return $this;
5546 * Remove all context path info and optionally rebuild it.
5548 * @param bool $rebuild
5549 * @return void
5551 public function reset_paths($rebuild = true) {
5552 global $DB;
5554 if ($this->_path) {
5555 $this->mark_dirty();
5557 $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
5558 $DB->set_field_select('context', 'path', NULL, "path LIKE '%/$this->_id/%'");
5559 if ($this->_contextlevel != CONTEXT_SYSTEM) {
5560 $DB->set_field('context', 'depth', 0, array('id'=>$this->_id));
5561 $DB->set_field('context', 'path', NULL, array('id'=>$this->_id));
5562 $this->_depth = 0;
5563 $this->_path = null;
5566 if ($rebuild) {
5567 context_helper::build_all_paths(false);
5570 context::reset_caches();
5574 * Delete all data linked to content, do not delete the context record itself
5576 public function delete_content() {
5577 global $CFG, $DB;
5579 blocks_delete_all_for_context($this->_id);
5580 filter_delete_all_for_context($this->_id);
5582 require_once($CFG->dirroot . '/comment/lib.php');
5583 comment::delete_comments(array('contextid'=>$this->_id));
5585 require_once($CFG->dirroot.'/rating/lib.php');
5586 $delopt = new stdclass();
5587 $delopt->contextid = $this->_id;
5588 $rm = new rating_manager();
5589 $rm->delete_ratings($delopt);
5591 // delete all files attached to this context
5592 $fs = get_file_storage();
5593 $fs->delete_area_files($this->_id);
5595 // Delete all repository instances attached to this context.
5596 require_once($CFG->dirroot . '/repository/lib.php');
5597 repository::delete_all_for_context($this->_id);
5599 // delete all advanced grading data attached to this context
5600 require_once($CFG->dirroot.'/grade/grading/lib.php');
5601 grading_manager::delete_all_for_context($this->_id);
5603 // now delete stuff from role related tables, role_unassign_all
5604 // and unenrol should be called earlier to do proper cleanup
5605 $DB->delete_records('role_assignments', array('contextid'=>$this->_id));
5606 $DB->delete_records('role_names', array('contextid'=>$this->_id));
5607 $this->delete_capabilities();
5611 * Unassign all capabilities from a context.
5613 public function delete_capabilities() {
5614 global $DB;
5616 $ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
5617 if ($ids) {
5618 $DB->delete_records('role_capabilities', array('contextid' => $this->_id));
5620 // Reset any cache of these roles, including MUC.
5621 accesslib_clear_role_cache($ids);
5626 * Delete the context content and the context record itself
5628 public function delete() {
5629 global $DB;
5631 if ($this->_contextlevel <= CONTEXT_SYSTEM) {
5632 throw new coding_exception('Cannot delete system context');
5635 // double check the context still exists
5636 if (!$DB->record_exists('context', array('id'=>$this->_id))) {
5637 context::cache_remove($this);
5638 return;
5641 $this->delete_content();
5642 $DB->delete_records('context', array('id'=>$this->_id));
5643 // purge static context cache if entry present
5644 context::cache_remove($this);
5646 // Inform search engine to delete data related to this context.
5647 \core_search\manager::context_deleted($this);
5650 // ====== context level related methods ======
5653 * Utility method for context creation
5655 * @static
5656 * @param int $contextlevel
5657 * @param int $instanceid
5658 * @param string $parentpath
5659 * @return stdClass context record
5661 protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
5662 global $DB;
5664 $record = new stdClass();
5665 $record->contextlevel = $contextlevel;
5666 $record->instanceid = $instanceid;
5667 $record->depth = 0;
5668 $record->path = null; //not known before insert
5669 $record->locked = 0;
5671 $record->id = $DB->insert_record('context', $record);
5673 // now add path if known - it can be added later
5674 if (!is_null($parentpath)) {
5675 $record->path = $parentpath.'/'.$record->id;
5676 $record->depth = substr_count($record->path, '/');
5677 $DB->update_record('context', $record);
5680 return $record;
5684 * Returns human readable context identifier.
5686 * @param boolean $withprefix whether to prefix the name of the context with the
5687 * type of context, e.g. User, Course, Forum, etc.
5688 * @param boolean $short whether to use the short name of the thing. Only applies
5689 * to course contexts
5690 * @param boolean $escape Whether the returned name of the thing is to be
5691 * HTML escaped or not.
5692 * @return string the human readable context name.
5694 public function get_context_name($withprefix = true, $short = false, $escape = true) {
5695 // must be implemented in all context levels
5696 throw new coding_exception('can not get name of abstract context');
5700 * Whether the current context is locked.
5702 * @return bool
5704 public function is_locked() {
5705 if ($this->_locked) {
5706 return true;
5709 if ($parent = $this->get_parent_context()) {
5710 return $parent->is_locked();
5713 return false;
5717 * Returns the most relevant URL for this context.
5719 * @return moodle_url
5721 public abstract function get_url();
5724 * Returns array of relevant context capability records.
5726 * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display
5727 * @return array
5729 public abstract function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT);
5732 * Recursive function which, given a context, find all its children context ids.
5734 * For course category contexts it will return immediate children and all subcategory contexts.
5735 * It will NOT recurse into courses or subcategories categories.
5736 * If you want to do that, call it on the returned courses/categories.
5738 * When called for a course context, it will return the modules and blocks
5739 * displayed in the course page and blocks displayed on the module pages.
5741 * If called on a user/course/module context it _will_ populate the cache with the appropriate
5742 * contexts ;-)
5744 * @return array Array of child records
5746 public function get_child_contexts() {
5747 global $DB;
5749 if (empty($this->_path) or empty($this->_depth)) {
5750 debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
5751 return array();
5754 $sql = "SELECT ctx.*
5755 FROM {context} ctx
5756 WHERE ctx.path LIKE ?";
5757 $params = array($this->_path.'/%');
5758 $records = $DB->get_records_sql($sql, $params);
5760 $result = array();
5761 foreach ($records as $record) {
5762 $result[$record->id] = context::create_instance_from_record($record);
5765 return $result;
5769 * Determine if the current context is a parent of the possible child.
5771 * @param context $possiblechild
5772 * @param bool $includeself Whether to check the current context
5773 * @return bool
5775 public function is_parent_of(context $possiblechild, bool $includeself): bool {
5776 // A simple substring check is used on the context path.
5777 // The possible child's path is used as a haystack, with the current context as the needle.
5778 // The path is prefixed with '+' to ensure that the parent always starts at the top.
5779 // It is suffixed with '+' to ensure that parents are not included.
5780 // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
5781 // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
5782 // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
5783 $haystacksuffix = $includeself ? '/+' : '+';
5785 $strpos = strpos(
5786 "+{$possiblechild->path}{$haystacksuffix}",
5787 "+{$this->path}/"
5789 return $strpos === 0;
5793 * Returns parent contexts of this context in reversed order, i.e. parent first,
5794 * then grand parent, etc.
5796 * @param bool $includeself true means include self too
5797 * @return array of context instances
5799 public function get_parent_contexts($includeself = false) {
5800 if (!$contextids = $this->get_parent_context_ids($includeself)) {
5801 return array();
5804 // Preload the contexts to reduce DB calls.
5805 context_helper::preload_contexts_by_id($contextids);
5807 $result = array();
5808 foreach ($contextids as $contextid) {
5809 $parent = context::instance_by_id($contextid, MUST_EXIST);
5810 $result[$parent->id] = $parent;
5813 return $result;
5817 * Determine if the current context is a child of the possible parent.
5819 * @param context $possibleparent
5820 * @param bool $includeself Whether to check the current context
5821 * @return bool
5823 public function is_child_of(context $possibleparent, bool $includeself): bool {
5824 // A simple substring check is used on the context path.
5825 // The current context is used as a haystack, with the possible parent as the needle.
5826 // The path is prefixed with '+' to ensure that the parent always starts at the top.
5827 // It is suffixed with '+' to ensure that children are not included.
5828 // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
5829 // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
5830 // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
5831 $haystacksuffix = $includeself ? '/+' : '+';
5833 $strpos = strpos(
5834 "+{$this->path}{$haystacksuffix}",
5835 "+{$possibleparent->path}/"
5837 return $strpos === 0;
5841 * Returns parent context ids of this context in reversed order, i.e. parent first,
5842 * then grand parent, etc.
5844 * @param bool $includeself true means include self too
5845 * @return array of context ids
5847 public function get_parent_context_ids($includeself = false) {
5848 if (empty($this->_path)) {
5849 return array();
5852 $parentcontexts = trim($this->_path, '/'); // kill leading slash
5853 $parentcontexts = explode('/', $parentcontexts);
5854 if (!$includeself) {
5855 array_pop($parentcontexts); // and remove its own id
5858 return array_reverse($parentcontexts);
5862 * Returns parent context paths of this context.
5864 * @param bool $includeself true means include self too
5865 * @return array of context paths
5867 public function get_parent_context_paths($includeself = false) {
5868 if (empty($this->_path)) {
5869 return array();
5872 $contextids = explode('/', $this->_path);
5874 $path = '';
5875 $paths = array();
5876 foreach ($contextids as $contextid) {
5877 if ($contextid) {
5878 $path .= '/' . $contextid;
5879 $paths[$contextid] = $path;
5883 if (!$includeself) {
5884 unset($paths[$this->_id]);
5887 return $paths;
5891 * Returns parent context
5893 * @return context
5895 public function get_parent_context() {
5896 if (empty($this->_path) or $this->_id == SYSCONTEXTID) {
5897 return false;
5900 $parentcontexts = trim($this->_path, '/'); // kill leading slash
5901 $parentcontexts = explode('/', $parentcontexts);
5902 array_pop($parentcontexts); // self
5903 $contextid = array_pop($parentcontexts); // immediate parent
5905 return context::instance_by_id($contextid, MUST_EXIST);
5909 * Is this context part of any course? If yes return course context.
5911 * @param bool $strict true means throw exception if not found, false means return false if not found
5912 * @return context_course context of the enclosing course, null if not found or exception
5914 public function get_course_context($strict = true) {
5915 if ($strict) {
5916 throw new coding_exception('Context does not belong to any course.');
5917 } else {
5918 return false;
5923 * Returns sql necessary for purging of stale context instances.
5925 * @static
5926 * @return string cleanup SQL
5928 protected static function get_cleanup_sql() {
5929 throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
5933 * Rebuild context paths and depths at context level.
5935 * @static
5936 * @param bool $force
5937 * @return void
5939 protected static function build_paths($force) {
5940 throw new coding_exception('build_paths() method must be implemented in all context levels');
5944 * Create missing context instances at given level
5946 * @static
5947 * @return void
5949 protected static function create_level_instances() {
5950 throw new coding_exception('create_level_instances() method must be implemented in all context levels');
5954 * Reset all cached permissions and definitions if the necessary.
5955 * @return void
5957 public function reload_if_dirty() {
5958 global $ACCESSLIB_PRIVATE, $USER;
5960 // Load dirty contexts list if needed
5961 if (CLI_SCRIPT) {
5962 if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
5963 // we do not load dirty flags in CLI and cron
5964 $ACCESSLIB_PRIVATE->dirtycontexts = array();
5966 } else {
5967 if (!isset($USER->access['time'])) {
5968 // Nothing has been loaded yet, so we do not need to check dirty flags now.
5969 return;
5972 // From skodak: No idea why -2 is there, server cluster time difference maybe...
5973 $changedsince = $USER->access['time'] - 2;
5975 if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
5976 $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
5979 if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
5980 $ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
5984 $dirty = false;
5986 if (!empty($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
5987 $dirty = true;
5988 } else if (!empty($ACCESSLIB_PRIVATE->dirtycontexts)) {
5989 $paths = $this->get_parent_context_paths(true);
5991 foreach ($paths as $path) {
5992 if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
5993 $dirty = true;
5994 break;
5999 if ($dirty) {
6000 // Reload all capabilities of USER and others - preserving loginas, roleswitches, etc.
6001 // Then cleanup any marks of dirtyness... at least from our short term memory!
6002 reload_all_capabilities();
6007 * Mark a context as dirty (with timestamp) so as to force reloading of the context.
6009 public function mark_dirty() {
6010 global $CFG, $USER, $ACCESSLIB_PRIVATE;
6012 if (during_initial_install()) {
6013 return;
6016 // only if it is a non-empty string
6017 if (is_string($this->_path) && $this->_path !== '') {
6018 set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time()+$CFG->sessiontimeout);
6019 if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
6020 $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
6021 } else {
6022 if (CLI_SCRIPT) {
6023 $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
6024 } else {
6025 if (isset($USER->access['time'])) {
6026 $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time']-2);
6027 } else {
6028 $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
6030 // flags not loaded yet, it will be done later in $context->reload_if_dirty()
6039 * Context maintenance and helper methods.
6041 * This is "extends context" is a bloody hack that tires to work around the deficiencies
6042 * in the "protected" keyword in PHP, this helps us to hide all the internals of context
6043 * level implementation from the rest of code, the code completion returns what developers need.
6045 * Thank you Tim Hunt for helping me with this nasty trick.
6047 * @package core_access
6048 * @category access
6049 * @copyright Petr Skoda {@link http://skodak.org}
6050 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6051 * @since Moodle 2.2
6053 class context_helper extends context {
6056 * @var array An array mapping context levels to classes
6058 private static $alllevels;
6061 * Instance does not make sense here, only static use
6063 protected function __construct() {
6067 * Reset internal context levels array.
6069 public static function reset_levels() {
6070 self::$alllevels = null;
6074 * Initialise context levels, call before using self::$alllevels.
6076 private static function init_levels() {
6077 global $CFG;
6079 if (isset(self::$alllevels)) {
6080 return;
6082 self::$alllevels = array(
6083 CONTEXT_SYSTEM => 'context_system',
6084 CONTEXT_USER => 'context_user',
6085 CONTEXT_COURSECAT => 'context_coursecat',
6086 CONTEXT_COURSE => 'context_course',
6087 CONTEXT_MODULE => 'context_module',
6088 CONTEXT_BLOCK => 'context_block',
6091 if (empty($CFG->custom_context_classes)) {
6092 return;
6095 $levels = $CFG->custom_context_classes;
6096 if (!is_array($levels)) {
6097 $levels = @unserialize($levels);
6099 if (!is_array($levels)) {
6100 debugging('Invalid $CFG->custom_context_classes detected, value ignored.', DEBUG_DEVELOPER);
6101 return;
6104 // Unsupported custom levels, use with care!!!
6105 foreach ($levels as $level => $classname) {
6106 self::$alllevels[$level] = $classname;
6108 ksort(self::$alllevels);
6112 * Returns a class name of the context level class
6114 * @static
6115 * @param int $contextlevel (CONTEXT_SYSTEM, etc.)
6116 * @return string class name of the context class
6118 public static function get_class_for_level($contextlevel) {
6119 self::init_levels();
6120 if (isset(self::$alllevels[$contextlevel])) {
6121 return self::$alllevels[$contextlevel];
6122 } else {
6123 throw new coding_exception('Invalid context level specified');
6128 * Returns a list of all context levels
6130 * @static
6131 * @return array int=>string (level=>level class name)
6133 public static function get_all_levels() {
6134 self::init_levels();
6135 return self::$alllevels;
6139 * Remove stale contexts that belonged to deleted instances.
6140 * Ideally all code should cleanup contexts properly, unfortunately accidents happen...
6142 * @static
6143 * @return void
6145 public static function cleanup_instances() {
6146 global $DB;
6147 self::init_levels();
6149 $sqls = array();
6150 foreach (self::$alllevels as $level=>$classname) {
6151 $sqls[] = $classname::get_cleanup_sql();
6154 $sql = implode(" UNION ", $sqls);
6156 // it is probably better to use transactions, it might be faster too
6157 $transaction = $DB->start_delegated_transaction();
6159 $rs = $DB->get_recordset_sql($sql);
6160 foreach ($rs as $record) {
6161 $context = context::create_instance_from_record($record);
6162 $context->delete();
6164 $rs->close();
6166 $transaction->allow_commit();
6170 * Create all context instances at the given level and above.
6172 * @static
6173 * @param int $contextlevel null means all levels
6174 * @param bool $buildpaths
6175 * @return void
6177 public static function create_instances($contextlevel = null, $buildpaths = true) {
6178 self::init_levels();
6179 foreach (self::$alllevels as $level=>$classname) {
6180 if ($contextlevel and $level > $contextlevel) {
6181 // skip potential sub-contexts
6182 continue;
6184 $classname::create_level_instances();
6185 if ($buildpaths) {
6186 $classname::build_paths(false);
6192 * Rebuild paths and depths in all context levels.
6194 * @static
6195 * @param bool $force false means add missing only
6196 * @return void
6198 public static function build_all_paths($force = false) {
6199 self::init_levels();
6200 foreach (self::$alllevels as $classname) {
6201 $classname::build_paths($force);
6204 // reset static course cache - it might have incorrect cached data
6205 accesslib_clear_all_caches(true);
6209 * Resets the cache to remove all data.
6210 * @static
6212 public static function reset_caches() {
6213 context::reset_caches();
6217 * Returns all fields necessary for context preloading from user $rec.
6219 * This helps with performance when dealing with hundreds of contexts.
6221 * @static
6222 * @param string $tablealias context table alias in the query
6223 * @return array (table.column=>alias, ...)
6225 public static function get_preload_record_columns($tablealias) {
6226 return [
6227 "$tablealias.id" => "ctxid",
6228 "$tablealias.path" => "ctxpath",
6229 "$tablealias.depth" => "ctxdepth",
6230 "$tablealias.contextlevel" => "ctxlevel",
6231 "$tablealias.instanceid" => "ctxinstance",
6232 "$tablealias.locked" => "ctxlocked",
6237 * Returns all fields necessary for context preloading from user $rec.
6239 * This helps with performance when dealing with hundreds of contexts.
6241 * @static
6242 * @param string $tablealias context table alias in the query
6243 * @return string
6245 public static function get_preload_record_columns_sql($tablealias) {
6246 return "$tablealias.id AS ctxid, " .
6247 "$tablealias.path AS ctxpath, " .
6248 "$tablealias.depth AS ctxdepth, " .
6249 "$tablealias.contextlevel AS ctxlevel, " .
6250 "$tablealias.instanceid AS ctxinstance, " .
6251 "$tablealias.locked AS ctxlocked";
6255 * Preloads context cache with information from db record and strips the cached info.
6257 * The db request has to contain all columns from context_helper::get_preload_record_columns().
6259 * @static
6260 * @param stdClass $rec
6261 * @return void This is intentional. See MDL-37115. You will need to get the context
6262 * in the normal way, but it is now cached, so that will be fast.
6264 public static function preload_from_record(stdClass $rec) {
6265 context::preload_from_record($rec);
6269 * Preload a set of contexts using their contextid.
6271 * @param array $contextids
6273 public static function preload_contexts_by_id(array $contextids) {
6274 global $DB;
6276 // Determine which contexts are not already cached.
6277 $tofetch = [];
6278 foreach ($contextids as $contextid) {
6279 if (!self::cache_get_by_id($contextid)) {
6280 $tofetch[] = $contextid;
6284 if (count($tofetch) > 1) {
6285 // There are at least two to fetch.
6286 // There is no point only fetching a single context as this would be no more efficient than calling the existing code.
6287 list($insql, $inparams) = $DB->get_in_or_equal($tofetch, SQL_PARAMS_NAMED);
6288 $ctxs = $DB->get_records_select('context', "id {$insql}", $inparams, '',
6289 \context_helper::get_preload_record_columns_sql('{context}'));
6290 foreach ($ctxs as $ctx) {
6291 self::preload_from_record($ctx);
6297 * Preload all contexts instances from course.
6299 * To be used if you expect multiple queries for course activities...
6301 * @static
6302 * @param int $courseid
6304 public static function preload_course($courseid) {
6305 // Users can call this multiple times without doing any harm
6306 if (isset(context::$cache_preloaded[$courseid])) {
6307 return;
6309 $coursecontext = context_course::instance($courseid);
6310 $coursecontext->get_child_contexts();
6312 context::$cache_preloaded[$courseid] = true;
6316 * Delete context instance
6318 * @static
6319 * @param int $contextlevel
6320 * @param int $instanceid
6321 * @return void
6323 public static function delete_instance($contextlevel, $instanceid) {
6324 global $DB;
6326 // double check the context still exists
6327 if ($record = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
6328 $context = context::create_instance_from_record($record);
6329 $context->delete();
6330 } else {
6331 // we should try to purge the cache anyway
6336 * Returns the name of specified context level
6338 * @static
6339 * @param int $contextlevel
6340 * @return string name of the context level
6342 public static function get_level_name($contextlevel) {
6343 $classname = context_helper::get_class_for_level($contextlevel);
6344 return $classname::get_level_name();
6348 * Gets the current context to be used for navigation tree filtering.
6350 * @param context|null $context The current context to be checked against.
6351 * @return context|null the context that navigation tree filtering should use.
6353 public static function get_navigation_filter_context(?context $context): ?context {
6354 global $CFG;
6355 if (!empty($CFG->filternavigationwithsystemcontext)) {
6356 return context_system::instance();
6357 } else {
6358 return $context;
6363 * not used
6365 public function get_url() {
6369 * not used
6371 * @param string $sort
6373 public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
6379 * System context class
6381 * @package core_access
6382 * @category access
6383 * @copyright Petr Skoda {@link http://skodak.org}
6384 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6385 * @since Moodle 2.2
6387 class context_system extends context {
6389 * Please use context_system::instance() if you need the instance of context.
6391 * @param stdClass $record
6393 protected function __construct(stdClass $record) {
6394 parent::__construct($record);
6395 if ($record->contextlevel != CONTEXT_SYSTEM) {
6396 throw new coding_exception('Invalid $record->contextlevel in context_system constructor.');
6401 * Returns human readable context level name.
6403 * @static
6404 * @return string the human readable context level name.
6406 public static function get_level_name() {
6407 return get_string('coresystem');
6411 * Returns human readable context identifier.
6413 * @param boolean $withprefix does not apply to system context
6414 * @param boolean $short does not apply to system context
6415 * @param boolean $escape does not apply to system context
6416 * @return string the human readable context name.
6418 public function get_context_name($withprefix = true, $short = false, $escape = true) {
6419 return self::get_level_name();
6423 * Returns the most relevant URL for this context.
6425 * @return moodle_url
6427 public function get_url() {
6428 return new moodle_url('/');
6432 * Returns array of relevant context capability records.
6434 * @param string $sort
6435 * @return array
6437 public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
6438 global $DB;
6440 return $DB->get_records('capabilities', [], $sort);
6444 * Create missing context instances at system context
6445 * @static
6447 protected static function create_level_instances() {
6448 // nothing to do here, the system context is created automatically in installer
6449 self::instance(0);
6453 * Returns system context instance.
6455 * @static
6456 * @param int $instanceid should be 0
6457 * @param int $strictness
6458 * @param bool $cache
6459 * @return context_system context instance
6461 public static function instance($instanceid = 0, $strictness = MUST_EXIST, $cache = true) {
6462 global $DB;
6464 if ($instanceid != 0) {
6465 debugging('context_system::instance(): invalid $id parameter detected, should be 0');
6468 // SYSCONTEXTID is cached in local cache to eliminate 1 query per page.
6469 if (defined('SYSCONTEXTID') and $cache) {
6470 if (!isset(context::$systemcontext)) {
6471 $record = new stdClass();
6472 $record->id = SYSCONTEXTID;
6473 $record->contextlevel = CONTEXT_SYSTEM;
6474 $record->instanceid = 0;
6475 $record->path = '/'.SYSCONTEXTID;
6476 $record->depth = 1;
6477 $record->locked = 0;
6478 context::$systemcontext = new context_system($record);
6480 return context::$systemcontext;
6483 try {
6484 // We ignore the strictness completely because system context must exist except during install.
6485 $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST);
6486 } catch (dml_exception $e) {
6487 //table or record does not exist
6488 if (!during_initial_install()) {
6489 // do not mess with system context after install, it simply must exist
6490 throw $e;
6492 $record = null;
6495 if (!$record) {
6496 $record = new stdClass();
6497 $record->contextlevel = CONTEXT_SYSTEM;
6498 $record->instanceid = 0;
6499 $record->depth = 1;
6500 $record->path = null; // Not known before insert.
6501 $record->locked = 0;
6503 try {
6504 if ($DB->count_records('context')) {
6505 // contexts already exist, this is very weird, system must be first!!!
6506 return null;
6508 if (defined('SYSCONTEXTID')) {
6509 // this would happen only in unittest on sites that went through weird 1.7 upgrade
6510 $record->id = SYSCONTEXTID;
6511 $DB->import_record('context', $record);
6512 $DB->get_manager()->reset_sequence('context');
6513 } else {
6514 $record->id = $DB->insert_record('context', $record);
6516 } catch (dml_exception $e) {
6517 // can not create context - table does not exist yet, sorry
6518 return null;
6522 if ($record->instanceid != 0) {
6523 // this is very weird, somebody must be messing with context table
6524 debugging('Invalid system context detected');
6527 if ($record->depth != 1 or $record->path != '/'.$record->id) {
6528 // fix path if necessary, initial install or path reset
6529 $record->depth = 1;
6530 $record->path = '/'.$record->id;
6531 $DB->update_record('context', $record);
6534 if (empty($record->locked)) {
6535 $record->locked = 0;
6538 if (!defined('SYSCONTEXTID')) {
6539 define('SYSCONTEXTID', $record->id);
6542 context::$systemcontext = new context_system($record);
6543 return context::$systemcontext;
6547 * Returns all site contexts except the system context, DO NOT call on production servers!!
6549 * Contexts are not cached.
6551 * @return array
6553 public function get_child_contexts() {
6554 global $DB;
6556 debugging('Fetching of system context child courses is strongly discouraged on production servers (it may eat all available memory)!');
6558 // Just get all the contexts except for CONTEXT_SYSTEM level
6559 // and hope we don't OOM in the process - don't cache
6560 $sql = "SELECT c.*
6561 FROM {context} c
6562 WHERE contextlevel > ".CONTEXT_SYSTEM;
6563 $records = $DB->get_records_sql($sql);
6565 $result = array();
6566 foreach ($records as $record) {
6567 $result[$record->id] = context::create_instance_from_record($record);
6570 return $result;
6574 * Returns sql necessary for purging of stale context instances.
6576 * @static
6577 * @return string cleanup SQL
6579 protected static function get_cleanup_sql() {
6580 $sql = "
6581 SELECT c.*
6582 FROM {context} c
6583 WHERE 1=2
6586 return $sql;
6590 * Rebuild context paths and depths at system context level.
6592 * @static
6593 * @param bool $force
6595 protected static function build_paths($force) {
6596 global $DB;
6598 /* note: ignore $force here, we always do full test of system context */
6600 // exactly one record must exist
6601 $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST);
6603 if ($record->instanceid != 0) {
6604 debugging('Invalid system context detected');
6607 if (defined('SYSCONTEXTID') and $record->id != SYSCONTEXTID) {
6608 debugging('Invalid SYSCONTEXTID detected');
6611 if ($record->depth != 1 or $record->path != '/'.$record->id) {
6612 // fix path if necessary, initial install or path reset
6613 $record->depth = 1;
6614 $record->path = '/'.$record->id;
6615 $DB->update_record('context', $record);
6620 * Set whether this context has been locked or not.
6622 * @param bool $locked
6623 * @return $this
6625 public function set_locked(bool $locked) {
6626 throw new \coding_exception('It is not possible to lock the system context');
6628 return $this;
6634 * User context class
6636 * @package core_access
6637 * @category access
6638 * @copyright Petr Skoda {@link http://skodak.org}
6639 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6640 * @since Moodle 2.2
6642 class context_user extends context {
6644 * Please use context_user::instance($userid) if you need the instance of context.
6645 * Alternatively if you know only the context id use context::instance_by_id($contextid)
6647 * @param stdClass $record
6649 protected function __construct(stdClass $record) {
6650 parent::__construct($record);
6651 if ($record->contextlevel != CONTEXT_USER) {
6652 throw new coding_exception('Invalid $record->contextlevel in context_user constructor.');
6657 * Returns human readable context level name.
6659 * @static
6660 * @return string the human readable context level name.
6662 public static function get_level_name() {
6663 return get_string('user');
6667 * Returns human readable context identifier.
6669 * @param boolean $withprefix whether to prefix the name of the context with User
6670 * @param boolean $short does not apply to user context
6671 * @param boolean $escape does not apply to user context
6672 * @return string the human readable context name.
6674 public function get_context_name($withprefix = true, $short = false, $escape = true) {
6675 global $DB;
6677 $name = '';
6678 if ($user = $DB->get_record('user', array('id'=>$this->_instanceid, 'deleted'=>0))) {
6679 if ($withprefix){
6680 $name = get_string('user').': ';
6682 $name .= fullname($user);
6684 return $name;
6688 * Returns the most relevant URL for this context.
6690 * @return moodle_url
6692 public function get_url() {
6693 global $COURSE;
6695 if ($COURSE->id == SITEID) {
6696 $url = new moodle_url('/user/profile.php', array('id'=>$this->_instanceid));
6697 } else {
6698 $url = new moodle_url('/user/view.php', array('id'=>$this->_instanceid, 'courseid'=>$COURSE->id));
6700 return $url;
6704 * Returns array of relevant context capability records.
6706 * @param string $sort
6707 * @return array
6709 public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
6710 global $DB;
6712 $extracaps = array('moodle/grade:viewall');
6713 list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
6715 return $DB->get_records_select('capabilities', "contextlevel = :level OR name {$extra}",
6716 $params + ['level' => CONTEXT_USER], $sort);
6720 * Returns user context instance.
6722 * @static
6723 * @param int $userid id from {user} table
6724 * @param int $strictness
6725 * @return context_user context instance
6727 public static function instance($userid, $strictness = MUST_EXIST) {
6728 global $DB;
6730 if ($context = context::cache_get(CONTEXT_USER, $userid)) {
6731 return $context;
6734 if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_USER, 'instanceid' => $userid))) {
6735 if ($user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), 'id', $strictness)) {
6736 $record = context::insert_context_record(CONTEXT_USER, $user->id, '/'.SYSCONTEXTID, 0);
6740 if ($record) {
6741 $context = new context_user($record);
6742 context::cache_add($context);
6743 return $context;
6746 return false;
6750 * Create missing context instances at user context level
6751 * @static
6753 protected static function create_level_instances() {
6754 global $DB;
6756 $sql = "SELECT ".CONTEXT_USER.", u.id
6757 FROM {user} u
6758 WHERE u.deleted = 0
6759 AND NOT EXISTS (SELECT 'x'
6760 FROM {context} cx
6761 WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
6762 $contextdata = $DB->get_recordset_sql($sql);
6763 foreach ($contextdata as $context) {
6764 context::insert_context_record(CONTEXT_USER, $context->id, null);
6766 $contextdata->close();
6770 * Returns sql necessary for purging of stale context instances.
6772 * @static
6773 * @return string cleanup SQL
6775 protected static function get_cleanup_sql() {
6776 $sql = "
6777 SELECT c.*
6778 FROM {context} c
6779 LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0)
6780 WHERE u.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
6783 return $sql;
6787 * Rebuild context paths and depths at user context level.
6789 * @static
6790 * @param bool $force
6792 protected static function build_paths($force) {
6793 global $DB;
6795 // First update normal users.
6796 $path = $DB->sql_concat('?', 'id');
6797 $pathstart = '/' . SYSCONTEXTID . '/';
6798 $params = array($pathstart);
6800 if ($force) {
6801 $where = "depth <> 2 OR path IS NULL OR path <> ({$path})";
6802 $params[] = $pathstart;
6803 } else {
6804 $where = "depth = 0 OR path IS NULL";
6807 $sql = "UPDATE {context}
6808 SET depth = 2,
6809 path = {$path}
6810 WHERE contextlevel = " . CONTEXT_USER . "
6811 AND ($where)";
6812 $DB->execute($sql, $params);
6818 * Course category context class
6820 * @package core_access
6821 * @category access
6822 * @copyright Petr Skoda {@link http://skodak.org}
6823 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6824 * @since Moodle 2.2
6826 class context_coursecat extends context {
6828 * Please use context_coursecat::instance($coursecatid) if you need the instance of context.
6829 * Alternatively if you know only the context id use context::instance_by_id($contextid)
6831 * @param stdClass $record
6833 protected function __construct(stdClass $record) {
6834 parent::__construct($record);
6835 if ($record->contextlevel != CONTEXT_COURSECAT) {
6836 throw new coding_exception('Invalid $record->contextlevel in context_coursecat constructor.');
6841 * Returns human readable context level name.
6843 * @static
6844 * @return string the human readable context level name.
6846 public static function get_level_name() {
6847 return get_string('category');
6851 * Returns human readable context identifier.
6853 * @param boolean $withprefix whether to prefix the name of the context with Category
6854 * @param boolean $short does not apply to course categories
6855 * @param boolean $escape Whether the returned name of the context is to be HTML escaped or not.
6856 * @return string the human readable context name.
6858 public function get_context_name($withprefix = true, $short = false, $escape = true) {
6859 global $DB;
6861 $name = '';
6862 if ($category = $DB->get_record('course_categories', array('id'=>$this->_instanceid))) {
6863 if ($withprefix){
6864 $name = get_string('category').': ';
6866 if (!$escape) {
6867 $name .= format_string($category->name, true, array('context' => $this, 'escape' => false));
6868 } else {
6869 $name .= format_string($category->name, true, array('context' => $this));
6872 return $name;
6876 * Returns the most relevant URL for this context.
6878 * @return moodle_url
6880 public function get_url() {
6881 return new moodle_url('/course/index.php', array('categoryid' => $this->_instanceid));
6885 * Returns array of relevant context capability records.
6887 * @param string $sort
6888 * @return array
6890 public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
6891 global $DB;
6893 return $DB->get_records_list('capabilities', 'contextlevel', [
6894 CONTEXT_COURSECAT,
6895 CONTEXT_COURSE,
6896 CONTEXT_MODULE,
6897 CONTEXT_BLOCK,
6898 ], $sort);
6902 * Returns course category context instance.
6904 * @static
6905 * @param int $categoryid id from {course_categories} table
6906 * @param int $strictness
6907 * @return context_coursecat context instance
6909 public static function instance($categoryid, $strictness = MUST_EXIST) {
6910 global $DB;
6912 if ($context = context::cache_get(CONTEXT_COURSECAT, $categoryid)) {
6913 return $context;
6916 if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSECAT, 'instanceid' => $categoryid))) {
6917 if ($category = $DB->get_record('course_categories', array('id' => $categoryid), 'id,parent', $strictness)) {
6918 if ($category->parent) {
6919 $parentcontext = context_coursecat::instance($category->parent);
6920 $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, $parentcontext->path);
6921 } else {
6922 $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, '/'.SYSCONTEXTID, 0);
6927 if ($record) {
6928 $context = new context_coursecat($record);
6929 context::cache_add($context);
6930 return $context;
6933 return false;
6937 * Returns immediate child contexts of category and all subcategories,
6938 * children of subcategories and courses are not returned.
6940 * @return array
6942 public function get_child_contexts() {
6943 global $DB;
6945 if (empty($this->_path) or empty($this->_depth)) {
6946 debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
6947 return array();
6950 $sql = "SELECT ctx.*
6951 FROM {context} ctx
6952 WHERE ctx.path LIKE ? AND (ctx.depth = ? OR ctx.contextlevel = ?)";
6953 $params = array($this->_path.'/%', $this->depth+1, CONTEXT_COURSECAT);
6954 $records = $DB->get_records_sql($sql, $params);
6956 $result = array();
6957 foreach ($records as $record) {
6958 $result[$record->id] = context::create_instance_from_record($record);
6961 return $result;
6965 * Create missing context instances at course category context level
6966 * @static
6968 protected static function create_level_instances() {
6969 global $DB;
6971 $sql = "SELECT ".CONTEXT_COURSECAT.", cc.id
6972 FROM {course_categories} cc
6973 WHERE NOT EXISTS (SELECT 'x'
6974 FROM {context} cx
6975 WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
6976 $contextdata = $DB->get_recordset_sql($sql);
6977 foreach ($contextdata as $context) {
6978 context::insert_context_record(CONTEXT_COURSECAT, $context->id, null);
6980 $contextdata->close();
6984 * Returns sql necessary for purging of stale context instances.
6986 * @static
6987 * @return string cleanup SQL
6989 protected static function get_cleanup_sql() {
6990 $sql = "
6991 SELECT c.*
6992 FROM {context} c
6993 LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id
6994 WHERE cc.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
6997 return $sql;
7001 * Rebuild context paths and depths at course category context level.
7003 * @static
7004 * @param bool $force
7006 protected static function build_paths($force) {
7007 global $DB;
7009 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSECAT." AND (depth = 0 OR path IS NULL)")) {
7010 if ($force) {
7011 $ctxemptyclause = $emptyclause = '';
7012 } else {
7013 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
7014 $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
7017 $base = '/'.SYSCONTEXTID;
7019 // Normal top level categories
7020 $sql = "UPDATE {context}
7021 SET depth=2,
7022 path=".$DB->sql_concat("'$base/'", 'id')."
7023 WHERE contextlevel=".CONTEXT_COURSECAT."
7024 AND EXISTS (SELECT 'x'
7025 FROM {course_categories} cc
7026 WHERE cc.id = {context}.instanceid AND cc.depth=1)
7027 $emptyclause";
7028 $DB->execute($sql);
7030 // Deeper categories - one query per depthlevel
7031 $maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}");
7032 for ($n=2; $n<=$maxdepth; $n++) {
7033 $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
7034 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
7035 FROM {context} ctx
7036 JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT." AND cc.depth = $n)
7037 JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".CONTEXT_COURSECAT.")
7038 WHERE pctx.path IS NOT NULL AND pctx.depth > 0
7039 $ctxemptyclause";
7040 $trans = $DB->start_delegated_transaction();
7041 $DB->delete_records('context_temp');
7042 $DB->execute($sql);
7043 context::merge_context_temp_table();
7044 $DB->delete_records('context_temp');
7045 $trans->allow_commit();
7054 * Course context class
7056 * @package core_access
7057 * @category access
7058 * @copyright Petr Skoda {@link http://skodak.org}
7059 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7060 * @since Moodle 2.2
7062 class context_course extends context {
7064 * Please use context_course::instance($courseid) if you need the instance of context.
7065 * Alternatively if you know only the context id use context::instance_by_id($contextid)
7067 * @param stdClass $record
7069 protected function __construct(stdClass $record) {
7070 parent::__construct($record);
7071 if ($record->contextlevel != CONTEXT_COURSE) {
7072 throw new coding_exception('Invalid $record->contextlevel in context_course constructor.');
7077 * Returns human readable context level name.
7079 * @static
7080 * @return string the human readable context level name.
7082 public static function get_level_name() {
7083 return get_string('course');
7087 * Returns human readable context identifier.
7089 * @param boolean $withprefix whether to prefix the name of the context with Course
7090 * @param boolean $short whether to use the short name of the thing.
7091 * @param bool $escape Whether the returned category name is to be HTML escaped or not.
7092 * @return string the human readable context name.
7094 public function get_context_name($withprefix = true, $short = false, $escape = true) {
7095 global $DB;
7097 $name = '';
7098 if ($this->_instanceid == SITEID) {
7099 $name = get_string('frontpage', 'admin');
7100 } else {
7101 if ($course = $DB->get_record('course', array('id'=>$this->_instanceid))) {
7102 if ($withprefix){
7103 $name = get_string('course').': ';
7105 if ($short){
7106 if (!$escape) {
7107 $name .= format_string($course->shortname, true, array('context' => $this, 'escape' => false));
7108 } else {
7109 $name .= format_string($course->shortname, true, array('context' => $this));
7111 } else {
7112 if (!$escape) {
7113 $name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this,
7114 'escape' => false));
7115 } else {
7116 $name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this));
7121 return $name;
7125 * Returns the most relevant URL for this context.
7127 * @return moodle_url
7129 public function get_url() {
7130 if ($this->_instanceid != SITEID) {
7131 return new moodle_url('/course/view.php', array('id'=>$this->_instanceid));
7134 return new moodle_url('/');
7138 * Returns array of relevant context capability records.
7140 * @param string $sort
7141 * @return array
7143 public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
7144 global $DB;
7146 return $DB->get_records_list('capabilities', 'contextlevel', [
7147 CONTEXT_COURSE,
7148 CONTEXT_MODULE,
7149 CONTEXT_BLOCK,
7150 ], $sort);
7154 * Is this context part of any course? If yes return course context.
7156 * @param bool $strict true means throw exception if not found, false means return false if not found
7157 * @return context_course context of the enclosing course, null if not found or exception
7159 public function get_course_context($strict = true) {
7160 return $this;
7164 * Returns course context instance.
7166 * @static
7167 * @param int $courseid id from {course} table
7168 * @param int $strictness
7169 * @return context_course context instance
7171 public static function instance($courseid, $strictness = MUST_EXIST) {
7172 global $DB;
7174 if ($context = context::cache_get(CONTEXT_COURSE, $courseid)) {
7175 return $context;
7178 if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSE, 'instanceid' => $courseid))) {
7179 if ($course = $DB->get_record('course', array('id' => $courseid), 'id,category', $strictness)) {
7180 if ($course->category) {
7181 $parentcontext = context_coursecat::instance($course->category);
7182 $record = context::insert_context_record(CONTEXT_COURSE, $course->id, $parentcontext->path);
7183 } else {
7184 $record = context::insert_context_record(CONTEXT_COURSE, $course->id, '/'.SYSCONTEXTID, 0);
7189 if ($record) {
7190 $context = new context_course($record);
7191 context::cache_add($context);
7192 return $context;
7195 return false;
7199 * Create missing context instances at course context level
7200 * @static
7202 protected static function create_level_instances() {
7203 global $DB;
7205 $sql = "SELECT ".CONTEXT_COURSE.", c.id
7206 FROM {course} c
7207 WHERE NOT EXISTS (SELECT 'x'
7208 FROM {context} cx
7209 WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
7210 $contextdata = $DB->get_recordset_sql($sql);
7211 foreach ($contextdata as $context) {
7212 context::insert_context_record(CONTEXT_COURSE, $context->id, null);
7214 $contextdata->close();
7218 * Returns sql necessary for purging of stale context instances.
7220 * @static
7221 * @return string cleanup SQL
7223 protected static function get_cleanup_sql() {
7224 $sql = "
7225 SELECT c.*
7226 FROM {context} c
7227 LEFT OUTER JOIN {course} co ON c.instanceid = co.id
7228 WHERE co.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
7231 return $sql;
7235 * Rebuild context paths and depths at course context level.
7237 * @static
7238 * @param bool $force
7240 protected static function build_paths($force) {
7241 global $DB;
7243 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSE." AND (depth = 0 OR path IS NULL)")) {
7244 if ($force) {
7245 $ctxemptyclause = $emptyclause = '';
7246 } else {
7247 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
7248 $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
7251 $base = '/'.SYSCONTEXTID;
7253 // Standard frontpage
7254 $sql = "UPDATE {context}
7255 SET depth = 2,
7256 path = ".$DB->sql_concat("'$base/'", 'id')."
7257 WHERE contextlevel = ".CONTEXT_COURSE."
7258 AND EXISTS (SELECT 'x'
7259 FROM {course} c
7260 WHERE c.id = {context}.instanceid AND c.category = 0)
7261 $emptyclause";
7262 $DB->execute($sql);
7264 // standard courses
7265 $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
7266 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
7267 FROM {context} ctx
7268 JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE." AND c.category <> 0)
7269 JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".CONTEXT_COURSECAT.")
7270 WHERE pctx.path IS NOT NULL AND pctx.depth > 0
7271 $ctxemptyclause";
7272 $trans = $DB->start_delegated_transaction();
7273 $DB->delete_records('context_temp');
7274 $DB->execute($sql);
7275 context::merge_context_temp_table();
7276 $DB->delete_records('context_temp');
7277 $trans->allow_commit();
7284 * Course module context class
7286 * @package core_access
7287 * @category access
7288 * @copyright Petr Skoda {@link http://skodak.org}
7289 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7290 * @since Moodle 2.2
7292 class context_module extends context {
7294 * Please use context_module::instance($cmid) if you need the instance of context.
7295 * Alternatively if you know only the context id use context::instance_by_id($contextid)
7297 * @param stdClass $record
7299 protected function __construct(stdClass $record) {
7300 parent::__construct($record);
7301 if ($record->contextlevel != CONTEXT_MODULE) {
7302 throw new coding_exception('Invalid $record->contextlevel in context_module constructor.');
7307 * Returns human readable context level name.
7309 * @static
7310 * @return string the human readable context level name.
7312 public static function get_level_name() {
7313 return get_string('activitymodule');
7317 * Returns human readable context identifier.
7319 * @param boolean $withprefix whether to prefix the name of the context with the
7320 * module name, e.g. Forum, Glossary, etc.
7321 * @param boolean $short does not apply to module context
7322 * @param boolean $escape Whether the returned name of the context is to be HTML escaped or not.
7323 * @return string the human readable context name.
7325 public function get_context_name($withprefix = true, $short = false, $escape = true) {
7326 global $DB;
7328 $name = '';
7329 if ($cm = $DB->get_record_sql("SELECT cm.*, md.name AS modname
7330 FROM {course_modules} cm
7331 JOIN {modules} md ON md.id = cm.module
7332 WHERE cm.id = ?", array($this->_instanceid))) {
7333 if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) {
7334 if ($withprefix){
7335 $name = get_string('modulename', $cm->modname).': ';
7337 if (!$escape) {
7338 $name .= format_string($mod->name, true, array('context' => $this, 'escape' => false));
7339 } else {
7340 $name .= format_string($mod->name, true, array('context' => $this));
7344 return $name;
7348 * Returns the most relevant URL for this context.
7350 * @return moodle_url
7352 public function get_url() {
7353 global $DB;
7355 if ($modname = $DB->get_field_sql("SELECT md.name AS modname
7356 FROM {course_modules} cm
7357 JOIN {modules} md ON md.id = cm.module
7358 WHERE cm.id = ?", array($this->_instanceid))) {
7359 return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$this->_instanceid));
7362 return new moodle_url('/');
7366 * Returns array of relevant context capability records.
7368 * @param string $sort
7369 * @return array
7371 public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
7372 global $DB, $CFG;
7374 $cm = $DB->get_record('course_modules', array('id'=>$this->_instanceid));
7375 $module = $DB->get_record('modules', array('id'=>$cm->module));
7377 $subcaps = array();
7379 $modulepath = "{$CFG->dirroot}/mod/{$module->name}";
7380 if (file_exists("{$modulepath}/db/subplugins.json")) {
7381 $subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes;
7382 } else if (file_exists("{$modulepath}/db/subplugins.php")) {
7383 debugging('Use of subplugins.php has been deprecated. ' .
7384 'Please update your plugin to provide a subplugins.json file instead.',
7385 DEBUG_DEVELOPER);
7386 $subplugins = array(); // should be redefined in the file
7387 include("{$modulepath}/db/subplugins.php");
7390 if (!empty($subplugins)) {
7391 foreach (array_keys($subplugins) as $subplugintype) {
7392 foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) {
7393 $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
7398 $modfile = "{$modulepath}/lib.php";
7399 $extracaps = array();
7400 if (file_exists($modfile)) {
7401 include_once($modfile);
7402 $modfunction = $module->name.'_get_extra_capabilities';
7403 if (function_exists($modfunction)) {
7404 $extracaps = $modfunction();
7408 $extracaps = array_merge($subcaps, $extracaps);
7409 $extra = '';
7410 list($extra, $params) = $DB->get_in_or_equal(
7411 $extracaps, SQL_PARAMS_NAMED, 'cap0', true, '');
7412 if (!empty($extra)) {
7413 $extra = "OR name $extra";
7416 // Fetch the list of modules, and remove this one.
7417 $components = \core_component::get_component_list();
7418 $componentnames = $components['mod'];
7419 unset($componentnames["mod_{$module->name}"]);
7420 $componentnames = array_keys($componentnames);
7422 // Exclude all other modules.
7423 list($notcompsql, $notcompparams) = $DB->get_in_or_equal($componentnames, SQL_PARAMS_NAMED, 'notcomp', false);
7424 $params = array_merge($params, $notcompparams);
7427 // Exclude other component submodules.
7428 $i = 0;
7429 $ignorecomponents = [];
7430 foreach ($componentnames as $mod) {
7431 if ($subplugins = \core_component::get_subplugins($mod)) {
7432 foreach (array_keys($subplugins) as $subplugintype) {
7433 $paramname = "notlike{$i}";
7434 $ignorecomponents[] = $DB->sql_like('component', ":{$paramname}", true, true, true);
7435 $params[$paramname] = "{$subplugintype}_%";
7436 $i++;
7440 $notlikesql = "(" . implode(' AND ', $ignorecomponents) . ")";
7442 $sql = "SELECT *
7443 FROM {capabilities}
7444 WHERE (contextlevel = ".CONTEXT_MODULE."
7445 AND component {$notcompsql}
7446 AND {$notlikesql})
7447 $extra
7448 ORDER BY $sort";
7450 return $DB->get_records_sql($sql, $params);
7454 * Is this context part of any course? If yes return course context.
7456 * @param bool $strict true means throw exception if not found, false means return false if not found
7457 * @return context_course context of the enclosing course, null if not found or exception
7459 public function get_course_context($strict = true) {
7460 return $this->get_parent_context();
7464 * Returns module context instance.
7466 * @static
7467 * @param int $cmid id of the record from {course_modules} table; pass cmid there, NOT id in the instance column
7468 * @param int $strictness
7469 * @return context_module context instance
7471 public static function instance($cmid, $strictness = MUST_EXIST) {
7472 global $DB;
7474 if ($context = context::cache_get(CONTEXT_MODULE, $cmid)) {
7475 return $context;
7478 if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_MODULE, 'instanceid' => $cmid))) {
7479 if ($cm = $DB->get_record('course_modules', array('id' => $cmid), 'id,course', $strictness)) {
7480 $parentcontext = context_course::instance($cm->course);
7481 $record = context::insert_context_record(CONTEXT_MODULE, $cm->id, $parentcontext->path);
7485 if ($record) {
7486 $context = new context_module($record);
7487 context::cache_add($context);
7488 return $context;
7491 return false;
7495 * Create missing context instances at module context level
7496 * @static
7498 protected static function create_level_instances() {
7499 global $DB;
7501 $sql = "SELECT ".CONTEXT_MODULE.", cm.id
7502 FROM {course_modules} cm
7503 WHERE NOT EXISTS (SELECT 'x'
7504 FROM {context} cx
7505 WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
7506 $contextdata = $DB->get_recordset_sql($sql);
7507 foreach ($contextdata as $context) {
7508 context::insert_context_record(CONTEXT_MODULE, $context->id, null);
7510 $contextdata->close();
7514 * Returns sql necessary for purging of stale context instances.
7516 * @static
7517 * @return string cleanup SQL
7519 protected static function get_cleanup_sql() {
7520 $sql = "
7521 SELECT c.*
7522 FROM {context} c
7523 LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id
7524 WHERE cm.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
7527 return $sql;
7531 * Rebuild context paths and depths at module context level.
7533 * @static
7534 * @param bool $force
7536 protected static function build_paths($force) {
7537 global $DB;
7539 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_MODULE." AND (depth = 0 OR path IS NULL)")) {
7540 if ($force) {
7541 $ctxemptyclause = '';
7542 } else {
7543 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
7546 $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
7547 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
7548 FROM {context} ctx
7549 JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_MODULE.")
7550 JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = ".CONTEXT_COURSE.")
7551 WHERE pctx.path IS NOT NULL AND pctx.depth > 0
7552 $ctxemptyclause";
7553 $trans = $DB->start_delegated_transaction();
7554 $DB->delete_records('context_temp');
7555 $DB->execute($sql);
7556 context::merge_context_temp_table();
7557 $DB->delete_records('context_temp');
7558 $trans->allow_commit();
7565 * Block context class
7567 * @package core_access
7568 * @category access
7569 * @copyright Petr Skoda {@link http://skodak.org}
7570 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7571 * @since Moodle 2.2
7573 class context_block extends context {
7575 * Please use context_block::instance($blockinstanceid) if you need the instance of context.
7576 * Alternatively if you know only the context id use context::instance_by_id($contextid)
7578 * @param stdClass $record
7580 protected function __construct(stdClass $record) {
7581 parent::__construct($record);
7582 if ($record->contextlevel != CONTEXT_BLOCK) {
7583 throw new coding_exception('Invalid $record->contextlevel in context_block constructor');
7588 * Returns human readable context level name.
7590 * @static
7591 * @return string the human readable context level name.
7593 public static function get_level_name() {
7594 return get_string('block');
7598 * Returns human readable context identifier.
7600 * @param boolean $withprefix whether to prefix the name of the context with Block
7601 * @param boolean $short does not apply to block context
7602 * @param boolean $escape does not apply to block context
7603 * @return string the human readable context name.
7605 public function get_context_name($withprefix = true, $short = false, $escape = true) {
7606 global $DB, $CFG;
7608 $name = '';
7609 if ($blockinstance = $DB->get_record('block_instances', array('id'=>$this->_instanceid))) {
7610 global $CFG;
7611 require_once("$CFG->dirroot/blocks/moodleblock.class.php");
7612 require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php");
7613 $blockname = "block_$blockinstance->blockname";
7614 if ($blockobject = new $blockname()) {
7615 if ($withprefix){
7616 $name = get_string('block').': ';
7618 $name .= $blockobject->title;
7622 return $name;
7626 * Returns the most relevant URL for this context.
7628 * @return moodle_url
7630 public function get_url() {
7631 $parentcontexts = $this->get_parent_context();
7632 return $parentcontexts->get_url();
7636 * Returns array of relevant context capability records.
7638 * @param string $sort
7639 * @return array
7641 public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
7642 global $DB;
7644 $bi = $DB->get_record('block_instances', array('id' => $this->_instanceid));
7646 $select = '(contextlevel = :level AND component = :component)';
7647 $params = [
7648 'level' => CONTEXT_BLOCK,
7649 'component' => 'block_' . $bi->blockname,
7652 $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities');
7653 if ($extracaps) {
7654 list($extra, $extraparams) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
7655 $select .= " OR name $extra";
7656 $params = array_merge($params, $extraparams);
7659 return $DB->get_records_select('capabilities', $select, $params, $sort);
7663 * Is this context part of any course? If yes return course context.
7665 * @param bool $strict true means throw exception if not found, false means return false if not found
7666 * @return context_course context of the enclosing course, null if not found or exception
7668 public function get_course_context($strict = true) {
7669 $parentcontext = $this->get_parent_context();
7670 return $parentcontext->get_course_context($strict);
7674 * Returns block context instance.
7676 * @static
7677 * @param int $blockinstanceid id from {block_instances} table.
7678 * @param int $strictness
7679 * @return context_block context instance
7681 public static function instance($blockinstanceid, $strictness = MUST_EXIST) {
7682 global $DB;
7684 if ($context = context::cache_get(CONTEXT_BLOCK, $blockinstanceid)) {
7685 return $context;
7688 if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_BLOCK, 'instanceid' => $blockinstanceid))) {
7689 if ($bi = $DB->get_record('block_instances', array('id' => $blockinstanceid), 'id,parentcontextid', $strictness)) {
7690 $parentcontext = context::instance_by_id($bi->parentcontextid);
7691 $record = context::insert_context_record(CONTEXT_BLOCK, $bi->id, $parentcontext->path);
7695 if ($record) {
7696 $context = new context_block($record);
7697 context::cache_add($context);
7698 return $context;
7701 return false;
7705 * Block do not have child contexts...
7706 * @return array
7708 public function get_child_contexts() {
7709 return array();
7713 * Create missing context instances at block context level
7714 * @static
7716 protected static function create_level_instances() {
7717 global $DB;
7719 $sql = <<<EOF
7720 INSERT INTO {context} (
7721 contextlevel,
7722 instanceid
7723 ) SELECT
7724 :contextlevel,
7725 bi.id as instanceid
7726 FROM {block_instances} bi
7727 WHERE NOT EXISTS (
7728 SELECT 'x' FROM {context} cx WHERE bi.id = cx.instanceid AND cx.contextlevel = :existingcontextlevel
7730 EOF;
7732 $DB->execute($sql, [
7733 'contextlevel' => CONTEXT_BLOCK,
7734 'existingcontextlevel' => CONTEXT_BLOCK,
7739 * Returns sql necessary for purging of stale context instances.
7741 * @static
7742 * @return string cleanup SQL
7744 protected static function get_cleanup_sql() {
7745 $sql = "
7746 SELECT c.*
7747 FROM {context} c
7748 LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id
7749 WHERE bi.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
7752 return $sql;
7756 * Rebuild context paths and depths at block context level.
7758 * @static
7759 * @param bool $force
7761 protected static function build_paths($force) {
7762 global $DB;
7764 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_BLOCK." AND (depth = 0 OR path IS NULL)")) {
7765 if ($force) {
7766 $ctxemptyclause = '';
7767 } else {
7768 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
7771 // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent
7772 $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
7773 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
7774 FROM {context} ctx
7775 JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.")
7776 JOIN {context} pctx ON (pctx.id = bi.parentcontextid)
7777 WHERE (pctx.path IS NOT NULL AND pctx.depth > 0)
7778 $ctxemptyclause";
7779 $trans = $DB->start_delegated_transaction();
7780 $DB->delete_records('context_temp');
7781 $DB->execute($sql);
7782 context::merge_context_temp_table();
7783 $DB->delete_records('context_temp');
7784 $trans->allow_commit();
7790 // ============== DEPRECATED FUNCTIONS ==========================================
7791 // Old context related functions were deprecated in 2.0, it is recommended
7792 // to use context classes in new code. Old function can be used when
7793 // creating patches that are supposed to be backported to older stable branches.
7794 // These deprecated functions will not be removed in near future,
7795 // before removing devs will be warned with a debugging message first,
7796 // then we will add error message and only after that we can remove the functions
7797 // completely.
7800 * Runs get_records select on context table and returns the result
7801 * Does get_records_select on the context table, and returns the results ordered
7802 * by contextlevel, and then the natural sort order within each level.
7803 * for the purpose of $select, you need to know that the context table has been
7804 * aliased to ctx, so for example, you can call get_sorted_contexts('ctx.depth = 3');
7806 * @param string $select the contents of the WHERE clause. Remember to do ctx.fieldname.
7807 * @param array $params any parameters required by $select.
7808 * @return array the requested context records.
7810 function get_sorted_contexts($select, $params = array()) {
7812 //TODO: we should probably rewrite all the code that is using this thing, the trouble is we MUST NOT modify the context instances...
7814 global $DB;
7815 if ($select) {
7816 $select = 'WHERE ' . $select;
7818 return $DB->get_records_sql("
7819 SELECT ctx.*
7820 FROM {context} ctx
7821 LEFT JOIN {user} u ON ctx.contextlevel = " . CONTEXT_USER . " AND u.id = ctx.instanceid
7822 LEFT JOIN {course_categories} cat ON ctx.contextlevel = " . CONTEXT_COURSECAT . " AND cat.id = ctx.instanceid
7823 LEFT JOIN {course} c ON ctx.contextlevel = " . CONTEXT_COURSE . " AND c.id = ctx.instanceid
7824 LEFT JOIN {course_modules} cm ON ctx.contextlevel = " . CONTEXT_MODULE . " AND cm.id = ctx.instanceid
7825 LEFT JOIN {block_instances} bi ON ctx.contextlevel = " . CONTEXT_BLOCK . " AND bi.id = ctx.instanceid
7826 $select
7827 ORDER BY ctx.contextlevel, bi.defaultregion, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.defaultweight), u.lastname, u.firstname, cm.id
7828 ", $params);
7832 * Given context and array of users, returns array of users whose enrolment status is suspended,
7833 * or enrolment has expired or has not started. Also removes those users from the given array
7835 * @param context $context context in which suspended users should be extracted.
7836 * @param array $users list of users.
7837 * @param array $ignoreusers array of user ids to ignore, e.g. guest
7838 * @return array list of suspended users.
7840 function extract_suspended_users($context, &$users, $ignoreusers=array()) {
7841 global $DB;
7843 // Get active enrolled users.
7844 list($sql, $params) = get_enrolled_sql($context, null, null, true);
7845 $activeusers = $DB->get_records_sql($sql, $params);
7847 // Move suspended users to a separate array & remove from the initial one.
7848 $susers = array();
7849 if (sizeof($activeusers)) {
7850 foreach ($users as $userid => $user) {
7851 if (!array_key_exists($userid, $activeusers) && !in_array($userid, $ignoreusers)) {
7852 $susers[$userid] = $user;
7853 unset($users[$userid]);
7857 return $susers;
7861 * Given context and array of users, returns array of user ids whose enrolment status is suspended,
7862 * or enrolment has expired or not started.
7864 * @param context $context context in which user enrolment is checked.
7865 * @param bool $usecache Enable or disable (default) the request cache
7866 * @return array list of suspended user id's.
7868 function get_suspended_userids(context $context, $usecache = false) {
7869 global $DB;
7871 if ($usecache) {
7872 $cache = cache::make('core', 'suspended_userids');
7873 $susers = $cache->get($context->id);
7874 if ($susers !== false) {
7875 return $susers;
7879 $coursecontext = $context->get_course_context();
7880 $susers = array();
7882 // Front page users are always enrolled, so suspended list is empty.
7883 if ($coursecontext->instanceid != SITEID) {
7884 list($sql, $params) = get_enrolled_sql($context, null, null, false, true);
7885 $susers = $DB->get_fieldset_sql($sql, $params);
7886 $susers = array_combine($susers, $susers);
7889 // Cache results for the remainder of this request.
7890 if ($usecache) {
7891 $cache->set($context->id, $susers);
7894 return $susers;
7898 * Gets sql for finding users with capability in the given context
7900 * @param context $context
7901 * @param string|array $capability Capability name or array of names.
7902 * If an array is provided then this is the equivalent of a logical 'OR',
7903 * i.e. the user needs to have one of these capabilities.
7904 * @return array($sql, $params)
7906 function get_with_capability_sql(context $context, $capability) {
7907 static $i = 0;
7908 $i++;
7909 $prefix = 'cu' . $i . '_';
7911 $capjoin = get_with_capability_join($context, $capability, $prefix . 'u.id');
7913 $sql = "SELECT DISTINCT {$prefix}u.id
7914 FROM {user} {$prefix}u
7915 $capjoin->joins
7916 WHERE {$prefix}u.deleted = 0 AND $capjoin->wheres";
7918 return array($sql, $capjoin->params);