MDL-68974 admin: Prevent login as outside of the desired context
[moodle.git] / lib / accesslib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * This file contains functions for managing user access
19  *
20  * <b>Public API vs internals</b>
21  *
22  * General users probably only care about
23  *
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();
29  *
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()
42  *
43  * What courses has this user access to?
44  * - get_enrolled_users()
45  *
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
49  *
50  * Modify roles
51  * - role_assign()
52  * - role_unassign()
53  * - role_unassign_all()
54  *
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.
61  *
62  * <b>Name conventions</b>
63  *
64  * "ctx" means context
65  * "ra" means role assignment
66  * "rdef" means role definition
67  *
68  * <b>accessdata</b>
69  *
70  * Access control data is held in the "accessdata" array
71  * which - for the logged-in user, will be in $USER->access
72  *
73  * For other users can be generated and passed around (but may also be cached
74  * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser).
75  *
76  * $accessdata is a multidimensional array, holding
77  * role assignments (RAs), role switches and initialization time.
78  *
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>
86  *
87  * <b>Stale accessdata</b>
88  *
89  * For the logged-in user, accessdata is long-lived.
90  *
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.
94  *
95  * Changes at the system level will force the reload for everyone.
96  *
97  * <b>Default role caps</b>
98  * The default role assignment is not in the DB, so we
99  * add it manually to accessdata.
100  *
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.
104  *
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
108  */
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);
131 /**
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.
135  */
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 /**
170  * Although this looks like a global variable, it isn't really.
171  *
172  * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
173  * It is used to cache various bits of data between function calls for performance reasons.
174  * Sadly, a PHP global variable is the only way to implement this, without rewriting everything
175  * as methods of a class, instead of functions.
176  *
177  * @access private
178  * @global stdClass $ACCESSLIB_PRIVATE
179  * @name $ACCESSLIB_PRIVATE
180  */
181 global $ACCESSLIB_PRIVATE;
182 $ACCESSLIB_PRIVATE = new stdClass();
183 $ACCESSLIB_PRIVATE->cacheroledefs    = array(); // Holds site-wide role definitions.
184 $ACCESSLIB_PRIVATE->dirtycontexts    = null;    // Dirty contexts cache, loaded from DB once per page
185 $ACCESSLIB_PRIVATE->dirtyusers       = null;    // Dirty users cache, loaded from DB once per $USER->id
186 $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
188 /**
189  * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
190  *
191  * This method should ONLY BE USED BY UNIT TESTS. It clears all of
192  * accesslib's private caches. You need to do this before setting up test data,
193  * and also at the end of the tests.
194  *
195  * @access private
196  * @return void
197  */
198 function accesslib_clear_all_caches_for_unit_testing() {
199     global $USER;
200     if (!PHPUNIT_TEST) {
201         throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
202     }
204     accesslib_clear_all_caches(true);
205     accesslib_reset_role_cache();
207     unset($USER->access);
210 /**
211  * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
212  *
213  * This reset does not touch global $USER.
214  *
215  * @access private
216  * @param bool $resetcontexts
217  * @return void
218  */
219 function accesslib_clear_all_caches($resetcontexts) {
220     global $ACCESSLIB_PRIVATE;
222     $ACCESSLIB_PRIVATE->dirtycontexts    = null;
223     $ACCESSLIB_PRIVATE->dirtyusers       = null;
224     $ACCESSLIB_PRIVATE->accessdatabyuser = array();
226     if ($resetcontexts) {
227         context_helper::reset_caches();
228     }
231 /**
232  * Full reset of accesslib's private role cache. ONLY TO BE USED FROM THIS LIBRARY FILE!
233  *
234  * This reset does not touch global $USER.
235  *
236  * Note: Only use this when the roles that need a refresh are unknown.
237  *
238  * @see accesslib_clear_role_cache()
239  *
240  * @access private
241  * @return void
242  */
243 function accesslib_reset_role_cache() {
244     global $ACCESSLIB_PRIVATE;
246     $ACCESSLIB_PRIVATE->cacheroledefs = array();
247     $cache = cache::make('core', 'roledefs');
248     $cache->purge();
251 /**
252  * Clears accesslib's private cache of a specific role or roles. ONLY BE USED FROM THIS LIBRARY FILE!
253  *
254  * This reset does not touch global $USER.
255  *
256  * @access private
257  * @param int|array $roles
258  * @return void
259  */
260 function accesslib_clear_role_cache($roles) {
261     global $ACCESSLIB_PRIVATE;
263     if (!is_array($roles)) {
264         $roles = [$roles];
265     }
267     foreach ($roles as $role) {
268         if (isset($ACCESSLIB_PRIVATE->cacheroledefs[$role])) {
269             unset($ACCESSLIB_PRIVATE->cacheroledefs[$role]);
270         }
271     }
273     $cache = cache::make('core', 'roledefs');
274     $cache->delete_many($roles);
277 /**
278  * Role is assigned at system context.
279  *
280  * @access private
281  * @param int $roleid
282  * @return array
283  */
284 function get_role_access($roleid) {
285     $accessdata = get_empty_accessdata();
286     $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
287     return $accessdata;
290 /**
291  * Fetch raw "site wide" role definitions.
292  * Even MUC static acceleration cache appears a bit slow for this.
293  * Important as can be hit hundreds of times per page.
294  *
295  * @param array $roleids List of role ids to fetch definitions for.
296  * @return array Complete definition for each requested role.
297  */
298 function get_role_definitions(array $roleids) {
299     global $ACCESSLIB_PRIVATE;
301     if (empty($roleids)) {
302         return array();
303     }
305     // Grab all keys we have not yet got in our static cache.
306     if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
307         $cache = cache::make('core', 'roledefs');
308         foreach ($cache->get_many($uncached) as $roleid => $cachedroledef) {
309             if (is_array($cachedroledef)) {
310                 $ACCESSLIB_PRIVATE->cacheroledefs[$roleid] = $cachedroledef;
311             }
312         }
314         // Check we have the remaining keys from the MUC.
315         if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
316             $uncached = get_role_definitions_uncached($uncached);
317             $ACCESSLIB_PRIVATE->cacheroledefs += $uncached;
318             $cache->set_many($uncached);
319         }
320     }
322     // Return just the roles we need.
323     return array_intersect_key($ACCESSLIB_PRIVATE->cacheroledefs, array_flip($roleids));
326 /**
327  * Query raw "site wide" role definitions.
328  *
329  * @param array $roleids List of role ids to fetch definitions for.
330  * @return array Complete definition for each requested role.
331  */
332 function get_role_definitions_uncached(array $roleids) {
333     global $DB;
335     if (empty($roleids)) {
336         return array();
337     }
339     // Create a blank results array: even if a role has no capabilities,
340     // we need to ensure it is included in the results to show we have
341     // loaded all the capabilities that there are.
342     $rdefs = array();
343     foreach ($roleids as $roleid) {
344         $rdefs[$roleid] = array();
345     }
347     // Load all the capabilities for these roles in all contexts.
348     list($sql, $params) = $DB->get_in_or_equal($roleids);
349     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
350               FROM {role_capabilities} rc
351               JOIN {context} ctx ON rc.contextid = ctx.id
352               JOIN {capabilities} cap ON rc.capability = cap.name
353              WHERE rc.roleid $sql";
354     $rs = $DB->get_recordset_sql($sql, $params);
356     // Store the capabilities into the expected data structure.
357     foreach ($rs as $rd) {
358         if (!isset($rdefs[$rd->roleid][$rd->path])) {
359             $rdefs[$rd->roleid][$rd->path] = array();
360         }
361         $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
362     }
364     $rs->close();
366     // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
367     // we process role definitinons in a way that requires we see parent contexts
368     // before child contexts. This sort ensures that works (and is faster than
369     // sorting in the SQL query).
370     foreach ($rdefs as $roleid => $rdef) {
371         ksort($rdefs[$roleid]);
372     }
374     return $rdefs;
377 /**
378  * Get the default guest role, this is used for guest account,
379  * search engine spiders, etc.
380  *
381  * @return stdClass role record
382  */
383 function get_guest_role() {
384     global $CFG, $DB;
386     if (empty($CFG->guestroleid)) {
387         if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
388             $guestrole = array_shift($roles);   // Pick the first one
389             set_config('guestroleid', $guestrole->id);
390             return $guestrole;
391         } else {
392             debugging('Can not find any guest role!');
393             return false;
394         }
395     } else {
396         if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
397             return $guestrole;
398         } else {
399             // somebody is messing with guest roles, remove incorrect setting and try to find a new one
400             set_config('guestroleid', '');
401             return get_guest_role();
402         }
403     }
406 /**
407  * Check whether a user has a particular capability in a given context.
408  *
409  * For example:
410  *      $context = context_module::instance($cm->id);
411  *      has_capability('mod/forum:replypost', $context)
412  *
413  * By default checks the capabilities of the current user, but you can pass a
414  * different userid. By default will return true for admin users, but you can override that with the fourth argument.
415  *
416  * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
417  * or capabilities with XSS, config or data loss risks.
418  *
419  * @category access
420  *
421  * @param string $capability the name of the capability to check. For example mod/forum:view
422  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
423  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
424  * @param boolean $doanything If false, ignores effect of admin role assignment
425  * @return boolean true if the user has this capability. Otherwise false.
426  */
427 function has_capability($capability, context $context, $user = null, $doanything = true) {
428     global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
430     if (during_initial_install()) {
431         if ($SCRIPT === "/$CFG->admin/index.php"
432                 or $SCRIPT === "/$CFG->admin/cli/install.php"
433                 or $SCRIPT === "/$CFG->admin/cli/install_database.php"
434                 or (defined('BEHAT_UTIL') and BEHAT_UTIL)
435                 or (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL)) {
436             // we are in an installer - roles can not work yet
437             return true;
438         } else {
439             return false;
440         }
441     }
443     if (strpos($capability, 'moodle/legacy:') === 0) {
444         throw new coding_exception('Legacy capabilities can not be used any more!');
445     }
447     if (!is_bool($doanything)) {
448         throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
449     }
451     // capability must exist
452     if (!$capinfo = get_capability_info($capability)) {
453         debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
454         return false;
455     }
457     if (!isset($USER->id)) {
458         // should never happen
459         $USER->id = 0;
460         debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
461     }
463     // make sure there is a real user specified
464     if ($user === null) {
465         $userid = $USER->id;
466     } else {
467         $userid = is_object($user) ? $user->id : $user;
468     }
470     // make sure forcelogin cuts off not-logged-in users if enabled
471     if (!empty($CFG->forcelogin) and $userid == 0) {
472         return false;
473     }
475     // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
476     if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
477         if (isguestuser($userid) or $userid == 0) {
478             return false;
479         }
480     }
482     // Check whether context locking is enabled.
483     if (!empty($CFG->contextlocking)) {
484         if ($capinfo->captype === 'write' && $context->locked) {
485             // Context locking applies to any write capability in a locked context.
486             // It does not apply to moodle/site:managecontextlocks - this is to allow context locking to be unlocked.
487             if ($capinfo->name !== 'moodle/site:managecontextlocks') {
488                 // It applies to all users who are not site admins.
489                 // It also applies to site admins when contextlockappliestoadmin is set.
490                 if (!is_siteadmin($userid) || !empty($CFG->contextlockappliestoadmin)) {
491                     return false;
492                 }
493             }
494         }
495     }
497     // somehow make sure the user is not deleted and actually exists
498     if ($userid != 0) {
499         if ($userid == $USER->id and isset($USER->deleted)) {
500             // this prevents one query per page, it is a bit of cheating,
501             // but hopefully session is terminated properly once user is deleted
502             if ($USER->deleted) {
503                 return false;
504             }
505         } else {
506             if (!context_user::instance($userid, IGNORE_MISSING)) {
507                 // no user context == invalid userid
508                 return false;
509             }
510         }
511     }
513     // context path/depth must be valid
514     if (empty($context->path) or $context->depth == 0) {
515         // this should not happen often, each upgrade tries to rebuild the context paths
516         debugging('Context id '.$context->id.' does not have valid path, please use context_helper::build_all_paths()');
517         if (is_siteadmin($userid)) {
518             return true;
519         } else {
520             return false;
521         }
522     }
524     if (!empty($USER->loginascontext)) {
525         // The current user is logged in as another user and can assume their identity at or below the `loginascontext`
526         // defined in the USER session.
527         // The user may not assume their identity at any other location.
528         if (!$USER->loginascontext->is_parent_of($context, true)) {
529             // The context being checked is not the specified context, or one of its children.
530             return false;
531         }
532     }
534     // Find out if user is admin - it is not possible to override the doanything in any way
535     // and it is not possible to switch to admin role either.
536     if ($doanything) {
537         if (is_siteadmin($userid)) {
538             if ($userid != $USER->id) {
539                 return true;
540             }
541             // make sure switchrole is not used in this context
542             if (empty($USER->access['rsw'])) {
543                 return true;
544             }
545             $parts = explode('/', trim($context->path, '/'));
546             $path = '';
547             $switched = false;
548             foreach ($parts as $part) {
549                 $path .= '/' . $part;
550                 if (!empty($USER->access['rsw'][$path])) {
551                     $switched = true;
552                     break;
553                 }
554             }
555             if (!$switched) {
556                 return true;
557             }
558             //ok, admin switched role in this context, let's use normal access control rules
559         }
560     }
562     // Careful check for staleness...
563     $context->reload_if_dirty();
565     if ($USER->id == $userid) {
566         if (!isset($USER->access)) {
567             load_all_capabilities();
568         }
569         $access =& $USER->access;
571     } else {
572         // make sure user accessdata is really loaded
573         get_user_accessdata($userid, true);
574         $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
575     }
577     return has_capability_in_accessdata($capability, $context, $access);
580 /**
581  * Check if the user has any one of several capabilities from a list.
582  *
583  * This is just a utility method that calls has_capability in a loop. Try to put
584  * the capabilities that most users are likely to have first in the list for best
585  * performance.
586  *
587  * @category access
588  * @see has_capability()
589  *
590  * @param array $capabilities an array of capability names.
591  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
592  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
593  * @param boolean $doanything If false, ignore effect of admin role assignment
594  * @return boolean true if the user has any of these capabilities. Otherwise false.
595  */
596 function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
597     foreach ($capabilities as $capability) {
598         if (has_capability($capability, $context, $user, $doanything)) {
599             return true;
600         }
601     }
602     return false;
605 /**
606  * Check if the user has all the capabilities in a list.
607  *
608  * This is just a utility method that calls has_capability in a loop. Try to put
609  * the capabilities that fewest users are likely to have first in the list for best
610  * performance.
611  *
612  * @category access
613  * @see has_capability()
614  *
615  * @param array $capabilities an array of capability names.
616  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
617  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
618  * @param boolean $doanything If false, ignore effect of admin role assignment
619  * @return boolean true if the user has all of these capabilities. Otherwise false.
620  */
621 function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
622     foreach ($capabilities as $capability) {
623         if (!has_capability($capability, $context, $user, $doanything)) {
624             return false;
625         }
626     }
627     return true;
630 /**
631  * Is course creator going to have capability in a new course?
632  *
633  * This is intended to be used in enrolment plugins before or during course creation,
634  * do not use after the course is fully created.
635  *
636  * @category access
637  *
638  * @param string $capability the name of the capability to check.
639  * @param context $context course or category context where is course going to be created
640  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
641  * @return boolean true if the user will have this capability.
642  *
643  * @throws coding_exception if different type of context submitted
644  */
645 function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
646     global $CFG;
648     if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
649         throw new coding_exception('Only course or course category context expected');
650     }
652     if (has_capability($capability, $context, $user)) {
653         // User already has the capability, it could be only removed if CAP_PROHIBIT
654         // was involved here, but we ignore that.
655         return true;
656     }
658     if (!has_capability('moodle/course:create', $context, $user)) {
659         return false;
660     }
662     if (!enrol_is_enabled('manual')) {
663         return false;
664     }
666     if (empty($CFG->creatornewroleid)) {
667         return false;
668     }
670     if ($context->contextlevel == CONTEXT_COURSE) {
671         if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
672             return false;
673         }
674     } else {
675         if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
676             return false;
677         }
678     }
680     // Most likely they will be enrolled after the course creation is finished,
681     // does the new role have the required capability?
682     list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
683     return isset($neededroles[$CFG->creatornewroleid]);
686 /**
687  * Check if the user is an admin at the site level.
688  *
689  * Please note that use of proper capabilities is always encouraged,
690  * this function is supposed to be used from core or for temporary hacks.
691  *
692  * @category access
693  *
694  * @param  int|stdClass  $user_or_id user id or user object
695  * @return bool true if user is one of the administrators, false otherwise
696  */
697 function is_siteadmin($user_or_id = null) {
698     global $CFG, $USER;
700     if ($user_or_id === null) {
701         $user_or_id = $USER;
702     }
704     if (empty($user_or_id)) {
705         return false;
706     }
707     if (!empty($user_or_id->id)) {
708         $userid = $user_or_id->id;
709     } else {
710         $userid = $user_or_id;
711     }
713     // Because this script is called many times (150+ for course page) with
714     // the same parameters, it is worth doing minor optimisations. This static
715     // cache stores the value for a single userid, saving about 2ms from course
716     // page load time without using significant memory. As the static cache
717     // also includes the value it depends on, this cannot break unit tests.
718     static $knownid, $knownresult, $knownsiteadmins;
719     if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
720         return $knownresult;
721     }
722     $knownid = $userid;
723     $knownsiteadmins = $CFG->siteadmins;
725     $siteadmins = explode(',', $CFG->siteadmins);
726     $knownresult = in_array($userid, $siteadmins);
727     return $knownresult;
730 /**
731  * Returns true if user has at least one role assign
732  * of 'coursecontact' role (is potentially listed in some course descriptions).
733  *
734  * @param int $userid
735  * @return bool
736  */
737 function has_coursecontact_role($userid) {
738     global $DB, $CFG;
740     if (empty($CFG->coursecontact)) {
741         return false;
742     }
743     $sql = "SELECT 1
744               FROM {role_assignments}
745              WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
746     return $DB->record_exists_sql($sql, array('userid'=>$userid));
749 /**
750  * Does the user have a capability to do something?
751  *
752  * Walk the accessdata array and return true/false.
753  * Deals with prohibits, role switching, aggregating
754  * capabilities, etc.
755  *
756  * The main feature of here is being FAST and with no
757  * side effects.
758  *
759  * Notes:
760  *
761  * Switch Role merges with default role
762  * ------------------------------------
763  * If you are a teacher in course X, you have at least
764  * teacher-in-X + defaultloggedinuser-sitewide. So in the
765  * course you'll have techer+defaultloggedinuser.
766  * We try to mimic that in switchrole.
767  *
768  * Permission evaluation
769  * ---------------------
770  * Originally there was an extremely complicated way
771  * to determine the user access that dealt with
772  * "locality" or role assignments and role overrides.
773  * Now we simply evaluate access for each role separately
774  * and then verify if user has at least one role with allow
775  * and at the same time no role with prohibit.
776  *
777  * @access private
778  * @param string $capability
779  * @param context $context
780  * @param array $accessdata
781  * @return bool
782  */
783 function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
784     global $CFG;
786     // Build $paths as a list of current + all parent "paths" with order bottom-to-top
787     $path = $context->path;
788     $paths = array($path);
789     while ($path = rtrim($path, '0123456789')) {
790         $path = rtrim($path, '/');
791         if ($path === '') {
792             break;
793         }
794         $paths[] = $path;
795     }
797     $roles = array();
798     $switchedrole = false;
800     // Find out if role switched
801     if (!empty($accessdata['rsw'])) {
802         // From the bottom up...
803         foreach ($paths as $path) {
804             if (isset($accessdata['rsw'][$path])) {
805                 // Found a switchrole assignment - check for that role _plus_ the default user role
806                 $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
807                 $switchedrole = true;
808                 break;
809             }
810         }
811     }
813     if (!$switchedrole) {
814         // get all users roles in this context and above
815         foreach ($paths as $path) {
816             if (isset($accessdata['ra'][$path])) {
817                 foreach ($accessdata['ra'][$path] as $roleid) {
818                     $roles[$roleid] = null;
819                 }
820             }
821         }
822     }
824     // Now find out what access is given to each role, going bottom-->up direction
825     $rdefs = get_role_definitions(array_keys($roles));
826     $allowed = false;
828     foreach ($roles as $roleid => $ignored) {
829         foreach ($paths as $path) {
830             if (isset($rdefs[$roleid][$path][$capability])) {
831                 $perm = (int)$rdefs[$roleid][$path][$capability];
832                 if ($perm === CAP_PROHIBIT) {
833                     // any CAP_PROHIBIT found means no permission for the user
834                     return false;
835                 }
836                 if (is_null($roles[$roleid])) {
837                     $roles[$roleid] = $perm;
838                 }
839             }
840         }
841         // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
842         $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
843     }
845     return $allowed;
848 /**
849  * A convenience function that tests has_capability, and displays an error if
850  * the user does not have that capability.
851  *
852  * NOTE before Moodle 2.0, this function attempted to make an appropriate
853  * require_login call before checking the capability. This is no longer the case.
854  * You must call require_login (or one of its variants) if you want to check the
855  * user is logged in, before you call this function.
856  *
857  * @see has_capability()
858  *
859  * @param string $capability the name of the capability to check. For example mod/forum:view
860  * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
861  * @param int $userid A user id. By default (null) checks the permissions of the current user.
862  * @param bool $doanything If false, ignore effect of admin role assignment
863  * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
864  * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
865  * @return void terminates with an error if the user does not have the given capability.
866  */
867 function require_capability($capability, context $context, $userid = null, $doanything = true,
868                             $errormessage = 'nopermissions', $stringfile = '') {
869     if (!has_capability($capability, $context, $userid, $doanything)) {
870         throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
871     }
874 /**
875  * A convenience function that tests has_capability for a list of capabilities, and displays an error if
876  * the user does not have that capability.
877  *
878  * This is just a utility method that calls has_capability in a loop. Try to put
879  * the capabilities that fewest users are likely to have first in the list for best
880  * performance.
881  *
882  * @category access
883  * @see has_capability()
884  *
885  * @param array $capabilities an array of capability names.
886  * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
887  * @param int $userid A user id. By default (null) checks the permissions of the current user.
888  * @param bool $doanything If false, ignore effect of admin role assignment
889  * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
890  * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
891  * @return void terminates with an error if the user does not have the given capability.
892  */
893 function require_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true,
894                                   $errormessage = 'nopermissions', $stringfile = ''): void {
895     foreach ($capabilities as $capability) {
896         if (!has_capability($capability, $context, $userid, $doanything)) {
897             throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
898         }
899     }
902 /**
903  * Return a nested array showing all role assignments for the user.
904  * [ra] => [contextpath][roleid] = roleid
905  *
906  * @access private
907  * @param int $userid - the id of the user
908  * @return array access info array
909  */
910 function get_user_roles_sitewide_accessdata($userid) {
911     global $CFG, $DB;
913     $accessdata = get_empty_accessdata();
915     // start with the default role
916     if (!empty($CFG->defaultuserroleid)) {
917         $syscontext = context_system::instance();
918         $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
919     }
921     // load the "default frontpage role"
922     if (!empty($CFG->defaultfrontpageroleid)) {
923         $frontpagecontext = context_course::instance(get_site()->id);
924         if ($frontpagecontext->path) {
925             $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
926         }
927     }
929     // Preload every assigned role.
930     $sql = "SELECT ctx.path, ra.roleid, ra.contextid
931               FROM {role_assignments} ra
932               JOIN {context} ctx ON ctx.id = ra.contextid
933              WHERE ra.userid = :userid";
935     $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
937     foreach ($rs as $ra) {
938         // RAs leafs are arrays to support multi-role assignments...
939         $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
940     }
942     $rs->close();
944     return $accessdata;
947 /**
948  * Returns empty accessdata structure.
949  *
950  * @access private
951  * @return array empt accessdata
952  */
953 function get_empty_accessdata() {
954     $accessdata               = array(); // named list
955     $accessdata['ra']         = array();
956     $accessdata['time']       = time();
957     $accessdata['rsw']        = array();
959     return $accessdata;
962 /**
963  * Get accessdata for a given user.
964  *
965  * @access private
966  * @param int $userid
967  * @param bool $preloadonly true means do not return access array
968  * @return array accessdata
969  */
970 function get_user_accessdata($userid, $preloadonly=false) {
971     global $CFG, $ACCESSLIB_PRIVATE, $USER;
973     if (isset($USER->access)) {
974         $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
975     }
977     if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
978         if (empty($userid)) {
979             if (!empty($CFG->notloggedinroleid)) {
980                 $accessdata = get_role_access($CFG->notloggedinroleid);
981             } else {
982                 // weird
983                 return get_empty_accessdata();
984             }
986         } else if (isguestuser($userid)) {
987             if ($guestrole = get_guest_role()) {
988                 $accessdata = get_role_access($guestrole->id);
989             } else {
990                 //weird
991                 return get_empty_accessdata();
992             }
994         } else {
995             // Includes default role and frontpage role.
996             $accessdata = get_user_roles_sitewide_accessdata($userid);
997         }
999         $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
1000     }
1002     if ($preloadonly) {
1003         return;
1004     } else {
1005         return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
1006     }
1009 /**
1010  * A convenience function to completely load all the capabilities
1011  * for the current user. It is called from has_capability() and functions change permissions.
1012  *
1013  * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
1014  * @see check_enrolment_plugins()
1015  *
1016  * @access private
1017  * @return void
1018  */
1019 function load_all_capabilities() {
1020     global $USER;
1022     // roles not installed yet - we are in the middle of installation
1023     if (during_initial_install()) {
1024         return;
1025     }
1027     if (!isset($USER->id)) {
1028         // this should not happen
1029         $USER->id = 0;
1030     }
1032     unset($USER->access);
1033     $USER->access = get_user_accessdata($USER->id);
1035     // Clear to force a refresh
1036     unset($USER->mycourses);
1038     // init/reset internal enrol caches - active course enrolments and temp access
1039     $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
1042 /**
1043  * A convenience function to completely reload all the capabilities
1044  * for the current user when roles have been updated in a relevant
1045  * context -- but PRESERVING switchroles and loginas.
1046  * This function resets all accesslib and context caches.
1047  *
1048  * That is - completely transparent to the user.
1049  *
1050  * Note: reloads $USER->access completely.
1051  *
1052  * @access private
1053  * @return void
1054  */
1055 function reload_all_capabilities() {
1056     global $USER, $DB, $ACCESSLIB_PRIVATE;
1058     // copy switchroles
1059     $sw = array();
1060     if (!empty($USER->access['rsw'])) {
1061         $sw = $USER->access['rsw'];
1062     }
1064     accesslib_clear_all_caches(true);
1065     unset($USER->access);
1067     // Prevent dirty flags refetching on this page.
1068     $ACCESSLIB_PRIVATE->dirtycontexts = array();
1069     $ACCESSLIB_PRIVATE->dirtyusers    = array($USER->id => false);
1071     load_all_capabilities();
1073     foreach ($sw as $path => $roleid) {
1074         if ($record = $DB->get_record('context', array('path'=>$path))) {
1075             $context = context::instance_by_id($record->id);
1076             if (has_capability('moodle/role:switchroles', $context)) {
1077                 role_switch($roleid, $context);
1078             }
1079         }
1080     }
1083 /**
1084  * Adds a temp role to current USER->access array.
1085  *
1086  * Useful for the "temporary guest" access we grant to logged-in users.
1087  * This is useful for enrol plugins only.
1088  *
1089  * @since Moodle 2.2
1090  * @param context_course $coursecontext
1091  * @param int $roleid
1092  * @return void
1093  */
1094 function load_temp_course_role(context_course $coursecontext, $roleid) {
1095     global $USER, $SITE;
1097     if (empty($roleid)) {
1098         debugging('invalid role specified in load_temp_course_role()');
1099         return;
1100     }
1102     if ($coursecontext->instanceid == $SITE->id) {
1103         debugging('Can not use temp roles on the frontpage');
1104         return;
1105     }
1107     if (!isset($USER->access)) {
1108         load_all_capabilities();
1109     }
1111     $coursecontext->reload_if_dirty();
1113     if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
1114         return;
1115     }
1117     $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
1120 /**
1121  * Removes any extra guest roles from current USER->access array.
1122  * This is useful for enrol plugins only.
1123  *
1124  * @since Moodle 2.2
1125  * @param context_course $coursecontext
1126  * @return void
1127  */
1128 function remove_temp_course_roles(context_course $coursecontext) {
1129     global $DB, $USER, $SITE;
1131     if ($coursecontext->instanceid == $SITE->id) {
1132         debugging('Can not use temp roles on the frontpage');
1133         return;
1134     }
1136     if (empty($USER->access['ra'][$coursecontext->path])) {
1137         //no roles here, weird
1138         return;
1139     }
1141     $sql = "SELECT DISTINCT ra.roleid AS id
1142               FROM {role_assignments} ra
1143              WHERE ra.contextid = :contextid AND ra.userid = :userid";
1144     $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
1146     $USER->access['ra'][$coursecontext->path] = array();
1147     foreach ($ras as $r) {
1148         $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
1149     }
1152 /**
1153  * Returns array of all role archetypes.
1154  *
1155  * @return array
1156  */
1157 function get_role_archetypes() {
1158     return array(
1159         'manager'        => 'manager',
1160         'coursecreator'  => 'coursecreator',
1161         'editingteacher' => 'editingteacher',
1162         'teacher'        => 'teacher',
1163         'student'        => 'student',
1164         'guest'          => 'guest',
1165         'user'           => 'user',
1166         'frontpage'      => 'frontpage'
1167     );
1170 /**
1171  * Assign the defaults found in this capability definition to roles that have
1172  * the corresponding legacy capabilities assigned to them.
1173  *
1174  * @param string $capability
1175  * @param array $legacyperms an array in the format (example):
1176  *                      'guest' => CAP_PREVENT,
1177  *                      'student' => CAP_ALLOW,
1178  *                      'teacher' => CAP_ALLOW,
1179  *                      'editingteacher' => CAP_ALLOW,
1180  *                      'coursecreator' => CAP_ALLOW,
1181  *                      'manager' => CAP_ALLOW
1182  * @return boolean success or failure.
1183  */
1184 function assign_legacy_capabilities($capability, $legacyperms) {
1186     $archetypes = get_role_archetypes();
1188     foreach ($legacyperms as $type => $perm) {
1190         $systemcontext = context_system::instance();
1191         if ($type === 'admin') {
1192             debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1193             $type = 'manager';
1194         }
1196         if (!array_key_exists($type, $archetypes)) {
1197             print_error('invalidlegacy', '', '', $type);
1198         }
1200         if ($roles = get_archetype_roles($type)) {
1201             foreach ($roles as $role) {
1202                 // Assign a site level capability.
1203                 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1204                     return false;
1205                 }
1206             }
1207         }
1208     }
1209     return true;
1212 /**
1213  * Verify capability risks.
1214  *
1215  * @param stdClass $capability a capability - a row from the capabilities table.
1216  * @return boolean whether this capability is safe - that is, whether people with the
1217  *      safeoverrides capability should be allowed to change it.
1218  */
1219 function is_safe_capability($capability) {
1220     return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1223 /**
1224  * Get the local override (if any) for a given capability in a role in a context
1225  *
1226  * @param int $roleid
1227  * @param int $contextid
1228  * @param string $capability
1229  * @return stdClass local capability override
1230  */
1231 function get_local_override($roleid, $contextid, $capability) {
1232     global $DB;
1234     return $DB->get_record_sql("
1235         SELECT rc.*
1236           FROM {role_capabilities} rc
1237           JOIN {capability} cap ON rc.capability = cap.name
1238          WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
1239             'roleid' => $roleid,
1240             'contextid' => $contextid,
1241             'capability' => $capability,
1243         ]);
1246 /**
1247  * Returns context instance plus related course and cm instances
1248  *
1249  * @param int $contextid
1250  * @return array of ($context, $course, $cm)
1251  */
1252 function get_context_info_array($contextid) {
1253     global $DB;
1255     $context = context::instance_by_id($contextid, MUST_EXIST);
1256     $course  = null;
1257     $cm      = null;
1259     if ($context->contextlevel == CONTEXT_COURSE) {
1260         $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
1262     } else if ($context->contextlevel == CONTEXT_MODULE) {
1263         $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
1264         $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1266     } else if ($context->contextlevel == CONTEXT_BLOCK) {
1267         $parent = $context->get_parent_context();
1269         if ($parent->contextlevel == CONTEXT_COURSE) {
1270             $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
1271         } else if ($parent->contextlevel == CONTEXT_MODULE) {
1272             $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
1273             $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1274         }
1275     }
1277     return array($context, $course, $cm);
1280 /**
1281  * Function that creates a role
1282  *
1283  * @param string $name role name
1284  * @param string $shortname role short name
1285  * @param string $description role description
1286  * @param string $archetype
1287  * @return int id or dml_exception
1288  */
1289 function create_role($name, $shortname, $description, $archetype = '') {
1290     global $DB;
1292     if (strpos($archetype, 'moodle/legacy:') !== false) {
1293         throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
1294     }
1296     // verify role archetype actually exists
1297     $archetypes = get_role_archetypes();
1298     if (empty($archetypes[$archetype])) {
1299         $archetype = '';
1300     }
1302     // Insert the role record.
1303     $role = new stdClass();
1304     $role->name        = $name;
1305     $role->shortname   = $shortname;
1306     $role->description = $description;
1307     $role->archetype   = $archetype;
1309     //find free sortorder number
1310     $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
1311     if (empty($role->sortorder)) {
1312         $role->sortorder = 1;
1313     }
1314     $id = $DB->insert_record('role', $role);
1316     return $id;
1319 /**
1320  * Function that deletes a role and cleanups up after it
1321  *
1322  * @param int $roleid id of role to delete
1323  * @return bool always true
1324  */
1325 function delete_role($roleid) {
1326     global $DB;
1328     // first unssign all users
1329     role_unassign_all(array('roleid'=>$roleid));
1331     // cleanup all references to this role, ignore errors
1332     $DB->delete_records('role_capabilities',   array('roleid'=>$roleid));
1333     $DB->delete_records('role_allow_assign',   array('roleid'=>$roleid));
1334     $DB->delete_records('role_allow_assign',   array('allowassign'=>$roleid));
1335     $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
1336     $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
1337     $DB->delete_records('role_names',          array('roleid'=>$roleid));
1338     $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
1340     // Get role record before it's deleted.
1341     $role = $DB->get_record('role', array('id'=>$roleid));
1343     // Finally delete the role itself.
1344     $DB->delete_records('role', array('id'=>$roleid));
1346     // Trigger event.
1347     $event = \core\event\role_deleted::create(
1348         array(
1349             'context' => context_system::instance(),
1350             'objectid' => $roleid,
1351             'other' =>
1352                 array(
1353                     'shortname' => $role->shortname,
1354                     'description' => $role->description,
1355                     'archetype' => $role->archetype
1356                 )
1357             )
1358         );
1359     $event->add_record_snapshot('role', $role);
1360     $event->trigger();
1362     // Reset any cache of this role, including MUC.
1363     accesslib_clear_role_cache($roleid);
1365     return true;
1368 /**
1369  * Function to write context specific overrides, or default capabilities.
1370  *
1371  * @param string $capability string name
1372  * @param int $permission CAP_ constants
1373  * @param int $roleid role id
1374  * @param int|context $contextid context id
1375  * @param bool $overwrite
1376  * @return bool always true or exception
1377  */
1378 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
1379     global $USER, $DB;
1381     if ($contextid instanceof context) {
1382         $context = $contextid;
1383     } else {
1384         $context = context::instance_by_id($contextid);
1385     }
1387     // Capability must exist.
1388     if (!$capinfo = get_capability_info($capability)) {
1389         throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1390     }
1392     if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
1393         unassign_capability($capability, $roleid, $context->id);
1394         return true;
1395     }
1397     $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
1399     if ($existing and !$overwrite) {   // We want to keep whatever is there already
1400         return true;
1401     }
1403     $cap = new stdClass();
1404     $cap->contextid    = $context->id;
1405     $cap->roleid       = $roleid;
1406     $cap->capability   = $capability;
1407     $cap->permission   = $permission;
1408     $cap->timemodified = time();
1409     $cap->modifierid   = empty($USER->id) ? 0 : $USER->id;
1411     if ($existing) {
1412         $cap->id = $existing->id;
1413         $DB->update_record('role_capabilities', $cap);
1414     } else {
1415         if ($DB->record_exists('context', array('id'=>$context->id))) {
1416             $DB->insert_record('role_capabilities', $cap);
1417         }
1418     }
1420     // Trigger capability_assigned event.
1421     \core\event\capability_assigned::create([
1422         'userid' => $cap->modifierid,
1423         'context' => $context,
1424         'objectid' => $roleid,
1425         'other' => [
1426             'capability' => $capability,
1427             'oldpermission' => $existing->permission ?? CAP_INHERIT,
1428             'permission' => $permission
1429         ]
1430     ])->trigger();
1432     // Reset any cache of this role, including MUC.
1433     accesslib_clear_role_cache($roleid);
1435     return true;
1438 /**
1439  * Unassign a capability from a role.
1440  *
1441  * @param string $capability the name of the capability
1442  * @param int $roleid the role id
1443  * @param int|context $contextid null means all contexts
1444  * @return boolean true or exception
1445  */
1446 function unassign_capability($capability, $roleid, $contextid = null) {
1447     global $DB, $USER;
1449     // Capability must exist.
1450     if (!$capinfo = get_capability_info($capability)) {
1451         throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1452     }
1454     if (!empty($contextid)) {
1455         if ($contextid instanceof context) {
1456             $context = $contextid;
1457         } else {
1458             $context = context::instance_by_id($contextid);
1459         }
1460         // delete from context rel, if this is the last override in this context
1461         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
1462     } else {
1463         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
1464     }
1466     // Trigger capability_assigned event.
1467     \core\event\capability_unassigned::create([
1468         'userid' => $USER->id,
1469         'context' => $context ?? context_system::instance(),
1470         'objectid' => $roleid,
1471         'other' => [
1472             'capability' => $capability,
1473         ]
1474     ])->trigger();
1476     // Reset any cache of this role, including MUC.
1477     accesslib_clear_role_cache($roleid);
1479     return true;
1482 /**
1483  * Get the roles that have a given capability assigned to it
1484  *
1485  * This function does not resolve the actual permission of the capability.
1486  * It just checks for permissions and overrides.
1487  * Use get_roles_with_cap_in_context() if resolution is required.
1488  *
1489  * @param string $capability capability name (string)
1490  * @param string $permission optional, the permission defined for this capability
1491  *                      either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
1492  * @param stdClass $context null means any
1493  * @return array of role records
1494  */
1495 function get_roles_with_capability($capability, $permission = null, $context = null) {
1496     global $DB;
1498     if ($context) {
1499         $contexts = $context->get_parent_context_ids(true);
1500         list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
1501         $contextsql = "AND rc.contextid $insql";
1502     } else {
1503         $params = array();
1504         $contextsql = '';
1505     }
1507     if ($permission) {
1508         $permissionsql = " AND rc.permission = :permission";
1509         $params['permission'] = $permission;
1510     } else {
1511         $permissionsql = '';
1512     }
1514     $sql = "SELECT r.*
1515               FROM {role} r
1516              WHERE r.id IN (SELECT rc.roleid
1517                               FROM {role_capabilities} rc
1518                               JOIN {capabilities} cap ON rc.capability = cap.name
1519                              WHERE rc.capability = :capname
1520                                    $contextsql
1521                                    $permissionsql)";
1522     $params['capname'] = $capability;
1525     return $DB->get_records_sql($sql, $params);
1528 /**
1529  * This function makes a role-assignment (a role for a user in a particular context)
1530  *
1531  * @param int $roleid the role of the id
1532  * @param int $userid userid
1533  * @param int|context $contextid id of the context
1534  * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
1535  * @param int $itemid id of enrolment/auth plugin
1536  * @param string $timemodified defaults to current time
1537  * @return int new/existing id of the assignment
1538  */
1539 function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
1540     global $USER, $DB;
1542     // first of all detect if somebody is using old style parameters
1543     if ($contextid === 0 or is_numeric($component)) {
1544         throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
1545     }
1547     // now validate all parameters
1548     if (empty($roleid)) {
1549         throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
1550     }
1552     if (empty($userid)) {
1553         throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
1554     }
1556     if ($itemid) {
1557         if (strpos($component, '_') === false) {
1558             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
1559         }
1560     } else {
1561         $itemid = 0;
1562         if ($component !== '' and strpos($component, '_') === false) {
1563             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1564         }
1565     }
1567     if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
1568         throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
1569     }
1571     if ($contextid instanceof context) {
1572         $context = $contextid;
1573     } else {
1574         $context = context::instance_by_id($contextid, MUST_EXIST);
1575     }
1577     if (!$timemodified) {
1578         $timemodified = time();
1579     }
1581     // Check for existing entry
1582     $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
1584     if ($ras) {
1585         // role already assigned - this should not happen
1586         if (count($ras) > 1) {
1587             // very weird - remove all duplicates!
1588             $ra = array_shift($ras);
1589             foreach ($ras as $r) {
1590                 $DB->delete_records('role_assignments', array('id'=>$r->id));
1591             }
1592         } else {
1593             $ra = reset($ras);
1594         }
1596         // actually there is no need to update, reset anything or trigger any event, so just return
1597         return $ra->id;
1598     }
1600     // Create a new entry
1601     $ra = new stdClass();
1602     $ra->roleid       = $roleid;
1603     $ra->contextid    = $context->id;
1604     $ra->userid       = $userid;
1605     $ra->component    = $component;
1606     $ra->itemid       = $itemid;
1607     $ra->timemodified = $timemodified;
1608     $ra->modifierid   = empty($USER->id) ? 0 : $USER->id;
1609     $ra->sortorder    = 0;
1611     $ra->id = $DB->insert_record('role_assignments', $ra);
1613     // Role assignments have changed, so mark user as dirty.
1614     mark_user_dirty($userid);
1616     core_course_category::role_assignment_changed($roleid, $context);
1618     $event = \core\event\role_assigned::create(array(
1619         'context' => $context,
1620         'objectid' => $ra->roleid,
1621         'relateduserid' => $ra->userid,
1622         'other' => array(
1623             'id' => $ra->id,
1624             'component' => $ra->component,
1625             'itemid' => $ra->itemid
1626         )
1627     ));
1628     $event->add_record_snapshot('role_assignments', $ra);
1629     $event->trigger();
1631     return $ra->id;
1634 /**
1635  * Removes one role assignment
1636  *
1637  * @param int $roleid
1638  * @param int  $userid
1639  * @param int  $contextid
1640  * @param string $component
1641  * @param int  $itemid
1642  * @return void
1643  */
1644 function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
1645     // first make sure the params make sense
1646     if ($roleid == 0 or $userid == 0 or $contextid == 0) {
1647         throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
1648     }
1650     if ($itemid) {
1651         if (strpos($component, '_') === false) {
1652             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
1653         }
1654     } else {
1655         $itemid = 0;
1656         if ($component !== '' and strpos($component, '_') === false) {
1657             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1658         }
1659     }
1661     role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
1664 /**
1665  * Removes multiple role assignments, parameters may contain:
1666  *   'roleid', 'userid', 'contextid', 'component', 'enrolid'.
1667  *
1668  * @param array $params role assignment parameters
1669  * @param bool $subcontexts unassign in subcontexts too
1670  * @param bool $includemanual include manual role assignments too
1671  * @return void
1672  */
1673 function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
1674     global $USER, $CFG, $DB;
1676     if (!$params) {
1677         throw new coding_exception('Missing parameters in role_unsassign_all() call');
1678     }
1680     $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
1681     foreach ($params as $key=>$value) {
1682         if (!in_array($key, $allowed)) {
1683             throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
1684         }
1685     }
1687     if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
1688         throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
1689     }
1691     if ($includemanual) {
1692         if (!isset($params['component']) or $params['component'] === '') {
1693             throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
1694         }
1695     }
1697     if ($subcontexts) {
1698         if (empty($params['contextid'])) {
1699             throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
1700         }
1701     }
1703     $ras = $DB->get_records('role_assignments', $params);
1704     foreach ($ras as $ra) {
1705         $DB->delete_records('role_assignments', array('id'=>$ra->id));
1706         if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
1707             // Role assignments have changed, so mark user as dirty.
1708             mark_user_dirty($ra->userid);
1710             $event = \core\event\role_unassigned::create(array(
1711                 'context' => $context,
1712                 'objectid' => $ra->roleid,
1713                 'relateduserid' => $ra->userid,
1714                 'other' => array(
1715                     'id' => $ra->id,
1716                     'component' => $ra->component,
1717                     'itemid' => $ra->itemid
1718                 )
1719             ));
1720             $event->add_record_snapshot('role_assignments', $ra);
1721             $event->trigger();
1722             core_course_category::role_assignment_changed($ra->roleid, $context);
1723         }
1724     }
1725     unset($ras);
1727     // process subcontexts
1728     if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
1729         if ($params['contextid'] instanceof context) {
1730             $context = $params['contextid'];
1731         } else {
1732             $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
1733         }
1735         if ($context) {
1736             $contexts = $context->get_child_contexts();
1737             $mparams = $params;
1738             foreach ($contexts as $context) {
1739                 $mparams['contextid'] = $context->id;
1740                 $ras = $DB->get_records('role_assignments', $mparams);
1741                 foreach ($ras as $ra) {
1742                     $DB->delete_records('role_assignments', array('id'=>$ra->id));
1743                     // Role assignments have changed, so mark user as dirty.
1744                     mark_user_dirty($ra->userid);
1746                     $event = \core\event\role_unassigned::create(
1747                         array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1748                             'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1749                     $event->add_record_snapshot('role_assignments', $ra);
1750                     $event->trigger();
1751                     core_course_category::role_assignment_changed($ra->roleid, $context);
1752                 }
1753             }
1754         }
1755     }
1757     // do this once more for all manual role assignments
1758     if ($includemanual) {
1759         $params['component'] = '';
1760         role_unassign_all($params, $subcontexts, false);
1761     }
1764 /**
1765  * Mark a user as dirty (with timestamp) so as to force reloading of the user session.
1766  *
1767  * @param int $userid
1768  * @return void
1769  */
1770 function mark_user_dirty($userid) {
1771     global $CFG, $ACCESSLIB_PRIVATE;
1773     if (during_initial_install()) {
1774         return;
1775     }
1777     // Throw exception if invalid userid is provided.
1778     if (empty($userid)) {
1779         throw new coding_exception('Invalid user parameter supplied for mark_user_dirty() function!');
1780     }
1782     // Set dirty flag in database, set dirty field locally, and clear local accessdata cache.
1783     set_cache_flag('accesslib/dirtyusers', $userid, 1, time() + $CFG->sessiontimeout);
1784     $ACCESSLIB_PRIVATE->dirtyusers[$userid] = 1;
1785     unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
1788 /**
1789  * Determines if a user is currently logged in
1790  *
1791  * @category   access
1792  *
1793  * @return bool
1794  */
1795 function isloggedin() {
1796     global $USER;
1798     return (!empty($USER->id));
1801 /**
1802  * Determines if a user is logged in as real guest user with username 'guest'.
1803  *
1804  * @category   access
1805  *
1806  * @param int|object $user mixed user object or id, $USER if not specified
1807  * @return bool true if user is the real guest user, false if not logged in or other user
1808  */
1809 function isguestuser($user = null) {
1810     global $USER, $DB, $CFG;
1812     // make sure we have the user id cached in config table, because we are going to use it a lot
1813     if (empty($CFG->siteguest)) {
1814         if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
1815             // guest does not exist yet, weird
1816             return false;
1817         }
1818         set_config('siteguest', $guestid);
1819     }
1820     if ($user === null) {
1821         $user = $USER;
1822     }
1824     if ($user === null) {
1825         // happens when setting the $USER
1826         return false;
1828     } else if (is_numeric($user)) {
1829         return ($CFG->siteguest == $user);
1831     } else if (is_object($user)) {
1832         if (empty($user->id)) {
1833             return false; // not logged in means is not be guest
1834         } else {
1835             return ($CFG->siteguest == $user->id);
1836         }
1838     } else {
1839         throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
1840     }
1843 /**
1844  * Does user have a (temporary or real) guest access to course?
1845  *
1846  * @category   access
1847  *
1848  * @param context $context
1849  * @param stdClass|int $user
1850  * @return bool
1851  */
1852 function is_guest(context $context, $user = null) {
1853     global $USER;
1855     // first find the course context
1856     $coursecontext = $context->get_course_context();
1858     // make sure there is a real user specified
1859     if ($user === null) {
1860         $userid = isset($USER->id) ? $USER->id : 0;
1861     } else {
1862         $userid = is_object($user) ? $user->id : $user;
1863     }
1865     if (isguestuser($userid)) {
1866         // can not inspect or be enrolled
1867         return true;
1868     }
1870     if (has_capability('moodle/course:view', $coursecontext, $user)) {
1871         // viewing users appear out of nowhere, they are neither guests nor participants
1872         return false;
1873     }
1875     // consider only real active enrolments here
1876     if (is_enrolled($coursecontext, $user, '', true)) {
1877         return false;
1878     }
1880     return true;
1883 /**
1884  * Returns true if the user has moodle/course:view capability in the course,
1885  * this is intended for admins, managers (aka small admins), inspectors, etc.
1886  *
1887  * @category   access
1888  *
1889  * @param context $context
1890  * @param int|stdClass $user if null $USER is used
1891  * @param string $withcapability extra capability name
1892  * @return bool
1893  */
1894 function is_viewing(context $context, $user = null, $withcapability = '') {
1895     // first find the course context
1896     $coursecontext = $context->get_course_context();
1898     if (isguestuser($user)) {
1899         // can not inspect
1900         return false;
1901     }
1903     if (!has_capability('moodle/course:view', $coursecontext, $user)) {
1904         // admins are allowed to inspect courses
1905         return false;
1906     }
1908     if ($withcapability and !has_capability($withcapability, $context, $user)) {
1909         // site admins always have the capability, but the enrolment above blocks
1910         return false;
1911     }
1913     return true;
1916 /**
1917  * Returns true if the user is able to access the course.
1918  *
1919  * This function is in no way, shape, or form a substitute for require_login.
1920  * It should only be used in circumstances where it is not possible to call require_login
1921  * such as the navigation.
1922  *
1923  * This function checks many of the methods of access to a course such as the view
1924  * capability, enrollments, and guest access. It also makes use of the cache
1925  * generated by require_login for guest access.
1926  *
1927  * The flags within the $USER object that are used here should NEVER be used outside
1928  * of this function can_access_course and require_login. Doing so WILL break future
1929  * versions.
1930  *
1931  * @param stdClass $course record
1932  * @param stdClass|int|null $user user record or id, current user if null
1933  * @param string $withcapability Check for this capability as well.
1934  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1935  * @return boolean Returns true if the user is able to access the course
1936  */
1937 function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
1938     global $DB, $USER;
1940     // this function originally accepted $coursecontext parameter
1941     if ($course instanceof context) {
1942         if ($course instanceof context_course) {
1943             debugging('deprecated context parameter, please use $course record');
1944             $coursecontext = $course;
1945             $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
1946         } else {
1947             debugging('Invalid context parameter, please use $course record');
1948             return false;
1949         }
1950     } else {
1951         $coursecontext = context_course::instance($course->id);
1952     }
1954     if (!isset($USER->id)) {
1955         // should never happen
1956         $USER->id = 0;
1957         debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
1958     }
1960     // make sure there is a user specified
1961     if ($user === null) {
1962         $userid = $USER->id;
1963     } else {
1964         $userid = is_object($user) ? $user->id : $user;
1965     }
1966     unset($user);
1968     if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
1969         return false;
1970     }
1972     if ($userid == $USER->id) {
1973         if (!empty($USER->access['rsw'][$coursecontext->path])) {
1974             // the fact that somebody switched role means they can access the course no matter to what role they switched
1975             return true;
1976         }
1977     }
1979     if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
1980         return false;
1981     }
1983     if (is_viewing($coursecontext, $userid)) {
1984         return true;
1985     }
1987     if ($userid != $USER->id) {
1988         // for performance reasons we do not verify temporary guest access for other users, sorry...
1989         return is_enrolled($coursecontext, $userid, '', $onlyactive);
1990     }
1992     // === from here we deal only with $USER ===
1994     $coursecontext->reload_if_dirty();
1996     if (isset($USER->enrol['enrolled'][$course->id])) {
1997         if ($USER->enrol['enrolled'][$course->id] > time()) {
1998             return true;
1999         }
2000     }
2001     if (isset($USER->enrol['tempguest'][$course->id])) {
2002         if ($USER->enrol['tempguest'][$course->id] > time()) {
2003             return true;
2004         }
2005     }
2007     if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
2008         return true;
2009     }
2011     if (!core_course_category::can_view_course_info($course)) {
2012         // No guest access if user does not have capability to browse courses.
2013         return false;
2014     }
2016     // if not enrolled try to gain temporary guest access
2017     $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
2018     $enrols = enrol_get_plugins(true);
2019     foreach ($instances as $instance) {
2020         if (!isset($enrols[$instance->enrol])) {
2021             continue;
2022         }
2023         // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
2024         $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2025         if ($until !== false and $until > time()) {
2026             $USER->enrol['tempguest'][$course->id] = $until;
2027             return true;
2028         }
2029     }
2030     if (isset($USER->enrol['tempguest'][$course->id])) {
2031         unset($USER->enrol['tempguest'][$course->id]);
2032         remove_temp_course_roles($coursecontext);
2033     }
2035     return false;
2038 /**
2039  * Loads the capability definitions for the component (from file).
2040  *
2041  * Loads the capability definitions for the component (from file). If no
2042  * capabilities are defined for the component, we simply return an empty array.
2043  *
2044  * @access private
2045  * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
2046  * @return array array of capabilities
2047  */
2048 function load_capability_def($component) {
2049     $defpath = core_component::get_component_directory($component).'/db/access.php';
2051     $capabilities = array();
2052     if (file_exists($defpath)) {
2053         require($defpath);
2054         if (!empty(${$component.'_capabilities'})) {
2055             // BC capability array name
2056             // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
2057             debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
2058             $capabilities = ${$component.'_capabilities'};
2059         }
2060     }
2062     return $capabilities;
2065 /**
2066  * Gets the capabilities that have been cached in the database for this component.
2067  *
2068  * @access private
2069  * @param string $component - examples: 'moodle', 'mod_forum'
2070  * @return array array of capabilities
2071  */
2072 function get_cached_capabilities($component = 'moodle') {
2073     global $DB;
2074     $caps = get_all_capabilities();
2075     $componentcaps = array();
2076     foreach ($caps as $cap) {
2077         if ($cap['component'] == $component) {
2078             $componentcaps[] = (object) $cap;
2079         }
2080     }
2081     return $componentcaps;
2084 /**
2085  * Returns default capabilities for given role archetype.
2086  *
2087  * @param string $archetype role archetype
2088  * @return array
2089  */
2090 function get_default_capabilities($archetype) {
2091     global $DB;
2093     if (!$archetype) {
2094         return array();
2095     }
2097     $alldefs = array();
2098     $defaults = array();
2099     $components = array();
2100     $allcaps = get_all_capabilities();
2102     foreach ($allcaps as $cap) {
2103         if (!in_array($cap['component'], $components)) {
2104             $components[] = $cap['component'];
2105             $alldefs = array_merge($alldefs, load_capability_def($cap['component']));
2106         }
2107     }
2108     foreach ($alldefs as $name=>$def) {
2109         // Use array 'archetypes if available. Only if not specified, use 'legacy'.
2110         if (isset($def['archetypes'])) {
2111             if (isset($def['archetypes'][$archetype])) {
2112                 $defaults[$name] = $def['archetypes'][$archetype];
2113             }
2114         // 'legacy' is for backward compatibility with 1.9 access.php
2115         } else {
2116             if (isset($def['legacy'][$archetype])) {
2117                 $defaults[$name] = $def['legacy'][$archetype];
2118             }
2119         }
2120     }
2122     return $defaults;
2125 /**
2126  * Return default roles that can be assigned, overridden or switched
2127  * by give role archetype.
2128  *
2129  * @param string $type  assign|override|switch|view
2130  * @param string $archetype
2131  * @return array of role ids
2132  */
2133 function get_default_role_archetype_allows($type, $archetype) {
2134     global $DB;
2136     if (empty($archetype)) {
2137         return array();
2138     }
2140     $roles = $DB->get_records('role');
2141     $archetypemap = array();
2142     foreach ($roles as $role) {
2143         if ($role->archetype) {
2144             $archetypemap[$role->archetype][$role->id] = $role->id;
2145         }
2146     }
2148     $defaults = array(
2149         'assign' => array(
2150             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
2151             'coursecreator'  => array(),
2152             'editingteacher' => array('teacher', 'student'),
2153             'teacher'        => array(),
2154             'student'        => array(),
2155             'guest'          => array(),
2156             'user'           => array(),
2157             'frontpage'      => array(),
2158         ),
2159         'override' => array(
2160             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2161             'coursecreator'  => array(),
2162             'editingteacher' => array('teacher', 'student', 'guest'),
2163             'teacher'        => array(),
2164             'student'        => array(),
2165             'guest'          => array(),
2166             'user'           => array(),
2167             'frontpage'      => array(),
2168         ),
2169         'switch' => array(
2170             'manager'        => array('editingteacher', 'teacher', 'student', 'guest'),
2171             'coursecreator'  => array(),
2172             'editingteacher' => array('teacher', 'student', 'guest'),
2173             'teacher'        => array('student', 'guest'),
2174             'student'        => array(),
2175             'guest'          => array(),
2176             'user'           => array(),
2177             'frontpage'      => array(),
2178         ),
2179         'view' => array(
2180             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2181             'coursecreator'  => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2182             'editingteacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2183             'teacher'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2184             'student'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2185             'guest'          => array(),
2186             'user'           => array(),
2187             'frontpage'      => array(),
2188         ),
2189     );
2191     if (!isset($defaults[$type][$archetype])) {
2192         debugging("Unknown type '$type'' or archetype '$archetype''");
2193         return array();
2194     }
2196     $return = array();
2197     foreach ($defaults[$type][$archetype] as $at) {
2198         if (isset($archetypemap[$at])) {
2199             foreach ($archetypemap[$at] as $roleid) {
2200                 $return[$roleid] = $roleid;
2201             }
2202         }
2203     }
2205     return $return;
2208 /**
2209  * Reset role capabilities to default according to selected role archetype.
2210  * If no archetype selected, removes all capabilities.
2211  *
2212  * This applies to capabilities that are assigned to the role (that you could
2213  * edit in the 'define roles' interface), and not to any capability overrides
2214  * in different locations.
2215  *
2216  * @param int $roleid ID of role to reset capabilities for
2217  */
2218 function reset_role_capabilities($roleid) {
2219     global $DB;
2221     $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
2222     $defaultcaps = get_default_capabilities($role->archetype);
2224     $systemcontext = context_system::instance();
2226     $DB->delete_records('role_capabilities',
2227             array('roleid' => $roleid, 'contextid' => $systemcontext->id));
2229     foreach ($defaultcaps as $cap=>$permission) {
2230         assign_capability($cap, $permission, $roleid, $systemcontext->id);
2231     }
2233     // Reset any cache of this role, including MUC.
2234     accesslib_clear_role_cache($roleid);
2237 /**
2238  * Updates the capabilities table with the component capability definitions.
2239  * If no parameters are given, the function updates the core moodle
2240  * capabilities.
2241  *
2242  * Note that the absence of the db/access.php capabilities definition file
2243  * will cause any stored capabilities for the component to be removed from
2244  * the database.
2245  *
2246  * @access private
2247  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
2248  * @return boolean true if success, exception in case of any problems
2249  */
2250 function update_capabilities($component = 'moodle') {
2251     global $DB, $OUTPUT;
2253     $storedcaps = array();
2255     $filecaps = load_capability_def($component);
2256     foreach ($filecaps as $capname=>$unused) {
2257         if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
2258             debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
2259         }
2260     }
2262     // It is possible somebody directly modified the DB (according to accesslib_test anyway).
2263     // So ensure our updating is based on fresh data.
2264     cache::make('core', 'capabilities')->delete('core_capabilities');
2266     $cachedcaps = get_cached_capabilities($component);
2267     if ($cachedcaps) {
2268         foreach ($cachedcaps as $cachedcap) {
2269             array_push($storedcaps, $cachedcap->name);
2270             // update risk bitmasks and context levels in existing capabilities if needed
2271             if (array_key_exists($cachedcap->name, $filecaps)) {
2272                 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
2273                     $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
2274                 }
2275                 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
2276                     $updatecap = new stdClass();
2277                     $updatecap->id = $cachedcap->id;
2278                     $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
2279                     $DB->update_record('capabilities', $updatecap);
2280                 }
2281                 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
2282                     $updatecap = new stdClass();
2283                     $updatecap->id = $cachedcap->id;
2284                     $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
2285                     $DB->update_record('capabilities', $updatecap);
2286                 }
2288                 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
2289                     $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
2290                 }
2291                 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
2292                     $updatecap = new stdClass();
2293                     $updatecap->id = $cachedcap->id;
2294                     $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
2295                     $DB->update_record('capabilities', $updatecap);
2296                 }
2297             }
2298         }
2299     }
2301     // Flush the cached again, as we have changed DB.
2302     cache::make('core', 'capabilities')->delete('core_capabilities');
2304     // Are there new capabilities in the file definition?
2305     $newcaps = array();
2307     foreach ($filecaps as $filecap => $def) {
2308         if (!$storedcaps ||
2309                 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
2310             if (!array_key_exists('riskbitmask', $def)) {
2311                 $def['riskbitmask'] = 0; // no risk if not specified
2312             }
2313             $newcaps[$filecap] = $def;
2314         }
2315     }
2316     // Add new capabilities to the stored definition.
2317     $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
2318     foreach ($newcaps as $capname => $capdef) {
2319         $capability = new stdClass();
2320         $capability->name         = $capname;
2321         $capability->captype      = $capdef['captype'];
2322         $capability->contextlevel = $capdef['contextlevel'];
2323         $capability->component    = $component;
2324         $capability->riskbitmask  = $capdef['riskbitmask'];
2326         $DB->insert_record('capabilities', $capability, false);
2328         // Flush the cached, as we have changed DB.
2329         cache::make('core', 'capabilities')->delete('core_capabilities');
2331         if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
2332             if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
2333                 foreach ($rolecapabilities as $rolecapability){
2334                     //assign_capability will update rather than insert if capability exists
2335                     if (!assign_capability($capname, $rolecapability->permission,
2336                                             $rolecapability->roleid, $rolecapability->contextid, true)){
2337                          echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
2338                     }
2339                 }
2340             }
2341         // we ignore archetype key if we have cloned permissions
2342         } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
2343             assign_legacy_capabilities($capname, $capdef['archetypes']);
2344         // 'legacy' is for backward compatibility with 1.9 access.php
2345         } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
2346             assign_legacy_capabilities($capname, $capdef['legacy']);
2347         }
2348     }
2349     // Are there any capabilities that have been removed from the file
2350     // definition that we need to delete from the stored capabilities and
2351     // role assignments?
2352     capabilities_cleanup($component, $filecaps);
2354     // reset static caches
2355     accesslib_reset_role_cache();
2357     // Flush the cached again, as we have changed DB.
2358     cache::make('core', 'capabilities')->delete('core_capabilities');
2360     return true;
2363 /**
2364  * Deletes cached capabilities that are no longer needed by the component.
2365  * Also unassigns these capabilities from any roles that have them.
2366  * NOTE: this function is called from lib/db/upgrade.php
2367  *
2368  * @access private
2369  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
2370  * @param array $newcapdef array of the new capability definitions that will be
2371  *                     compared with the cached capabilities
2372  * @return int number of deprecated capabilities that have been removed
2373  */
2374 function capabilities_cleanup($component, $newcapdef = null) {
2375     global $DB;
2377     $removedcount = 0;
2379     if ($cachedcaps = get_cached_capabilities($component)) {
2380         foreach ($cachedcaps as $cachedcap) {
2381             if (empty($newcapdef) ||
2382                         array_key_exists($cachedcap->name, $newcapdef) === false) {
2384                 // Delete from roles.
2385                 if ($roles = get_roles_with_capability($cachedcap->name)) {
2386                     foreach ($roles as $role) {
2387                         if (!unassign_capability($cachedcap->name, $role->id)) {
2388                             print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
2389                         }
2390                     }
2391                 }
2393                 // Remove from role_capabilities for any old ones.
2394                 $DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
2396                 // Remove from capabilities cache.
2397                 $DB->delete_records('capabilities', array('name' => $cachedcap->name));
2398                 $removedcount++;
2399             } // End if.
2400         }
2401     }
2402     if ($removedcount) {
2403         cache::make('core', 'capabilities')->delete('core_capabilities');
2404     }
2405     return $removedcount;
2408 /**
2409  * Returns an array of all the known types of risk
2410  * The array keys can be used, for example as CSS class names, or in calls to
2411  * print_risk_icon. The values are the corresponding RISK_ constants.
2412  *
2413  * @return array all the known types of risk.
2414  */
2415 function get_all_risks() {
2416     return array(
2417         'riskmanagetrust' => RISK_MANAGETRUST,
2418         'riskconfig'      => RISK_CONFIG,
2419         'riskxss'         => RISK_XSS,
2420         'riskpersonal'    => RISK_PERSONAL,
2421         'riskspam'        => RISK_SPAM,
2422         'riskdataloss'    => RISK_DATALOSS,
2423     );
2426 /**
2427  * Return a link to moodle docs for a given capability name
2428  *
2429  * @param stdClass $capability a capability - a row from the mdl_capabilities table.
2430  * @return string the human-readable capability name as a link to Moodle Docs.
2431  */
2432 function get_capability_docs_link($capability) {
2433     $url = get_docs_url('Capabilities/' . $capability->name);
2434     return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
2437 /**
2438  * This function pulls out all the resolved capabilities (overrides and
2439  * defaults) of a role used in capability overrides in contexts at a given
2440  * context.
2441  *
2442  * @param int $roleid
2443  * @param context $context
2444  * @param string $cap capability, optional, defaults to ''
2445  * @return array Array of capabilities
2446  */
2447 function role_context_capabilities($roleid, context $context, $cap = '') {
2448     global $DB;
2450     $contexts = $context->get_parent_context_ids(true);
2451     $contexts = '('.implode(',', $contexts).')';
2453     $params = array($roleid);
2455     if ($cap) {
2456         $search = " AND rc.capability = ? ";
2457         $params[] = $cap;
2458     } else {
2459         $search = '';
2460     }
2462     $sql = "SELECT rc.*
2463               FROM {role_capabilities} rc
2464               JOIN {context} c ON rc.contextid = c.id
2465               JOIN {capabilities} cap ON rc.capability = cap.name
2466              WHERE rc.contextid in $contexts
2467                    AND rc.roleid = ?
2468                    $search
2469           ORDER BY c.contextlevel DESC, rc.capability DESC";
2471     $capabilities = array();
2473     if ($records = $DB->get_records_sql($sql, $params)) {
2474         // We are traversing via reverse order.
2475         foreach ($records as $record) {
2476             // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
2477             if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
2478                 $capabilities[$record->capability] = $record->permission;
2479             }
2480         }
2481     }
2482     return $capabilities;
2485 /**
2486  * Constructs array with contextids as first parameter and context paths,
2487  * in both cases bottom top including self.
2488  *
2489  * @access private
2490  * @param context $context
2491  * @return array
2492  */
2493 function get_context_info_list(context $context) {
2494     $contextids = explode('/', ltrim($context->path, '/'));
2495     $contextpaths = array();
2496     $contextids2 = $contextids;
2497     while ($contextids2) {
2498         $contextpaths[] = '/' . implode('/', $contextids2);
2499         array_pop($contextids2);
2500     }
2501     return array($contextids, $contextpaths);
2504 /**
2505  * Check if context is the front page context or a context inside it
2506  *
2507  * Returns true if this context is the front page context, or a context inside it,
2508  * otherwise false.
2509  *
2510  * @param context $context a context object.
2511  * @return bool
2512  */
2513 function is_inside_frontpage(context $context) {
2514     $frontpagecontext = context_course::instance(SITEID);
2515     return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
2518 /**
2519  * Returns capability information (cached)
2520  *
2521  * @param string $capabilityname
2522  * @return stdClass or null if capability not found
2523  */
2524 function get_capability_info($capabilityname) {
2525     $caps = get_all_capabilities();
2527     if (!isset($caps[$capabilityname])) {
2528         return null;
2529     }
2531     return (object) $caps[$capabilityname];
2534 /**
2535  * Returns all capabilitiy records, preferably from MUC and not database.
2536  *
2537  * @return array All capability records indexed by capability name
2538  */
2539 function get_all_capabilities() {
2540     global $DB;
2541     $cache = cache::make('core', 'capabilities');
2542     if (!$allcaps = $cache->get('core_capabilities')) {
2543         $rs = $DB->get_recordset('capabilities');
2544         $allcaps = array();
2545         foreach ($rs as $capability) {
2546             $capability->riskbitmask = (int) $capability->riskbitmask;
2547             $allcaps[$capability->name] = (array) $capability;
2548         }
2549         $rs->close();
2550         $cache->set('core_capabilities', $allcaps);
2551     }
2552     return $allcaps;
2555 /**
2556  * Returns the human-readable, translated version of the capability.
2557  * Basically a big switch statement.
2558  *
2559  * @param string $capabilityname e.g. mod/choice:readresponses
2560  * @return string
2561  */
2562 function get_capability_string($capabilityname) {
2564     // Typical capability name is 'plugintype/pluginname:capabilityname'
2565     list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
2567     if ($type === 'moodle') {
2568         $component = 'core_role';
2569     } else if ($type === 'quizreport') {
2570         //ugly hack!!
2571         $component = 'quiz_'.$name;
2572     } else {
2573         $component = $type.'_'.$name;
2574     }
2576     $stringname = $name.':'.$capname;
2578     if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
2579         return get_string($stringname, $component);
2580     }
2582     $dir = core_component::get_component_directory($component);
2583     if (!file_exists($dir)) {
2584         // plugin broken or does not exist, do not bother with printing of debug message
2585         return $capabilityname.' ???';
2586     }
2588     // something is wrong in plugin, better print debug
2589     return get_string($stringname, $component);
2592 /**
2593  * This gets the mod/block/course/core etc strings.
2594  *
2595  * @param string $component
2596  * @param int $contextlevel
2597  * @return string|bool String is success, false if failed
2598  */
2599 function get_component_string($component, $contextlevel) {
2601     if ($component === 'moodle' || $component === 'core') {
2602         return context_helper::get_level_name($contextlevel);
2603     }
2605     list($type, $name) = core_component::normalize_component($component);
2606     $dir = core_component::get_plugin_directory($type, $name);
2607     if (!file_exists($dir)) {
2608         // plugin not installed, bad luck, there is no way to find the name
2609         return $component . ' ???';
2610     }
2612     // Some plugin types need an extra prefix to make the name easy to understand.
2613     switch ($type) {
2614         case 'quiz':
2615             $prefix = get_string('quizreport', 'quiz') . ': ';
2616             break;
2617         case 'repository':
2618             $prefix = get_string('repository', 'repository') . ': ';
2619             break;
2620         case 'gradeimport':
2621             $prefix = get_string('gradeimport', 'grades') . ': ';
2622             break;
2623         case 'gradeexport':
2624             $prefix = get_string('gradeexport', 'grades') . ': ';
2625             break;
2626         case 'gradereport':
2627             $prefix = get_string('gradereport', 'grades') . ': ';
2628             break;
2629         case 'webservice':
2630             $prefix = get_string('webservice', 'webservice') . ': ';
2631             break;
2632         case 'block':
2633             $prefix = get_string('block') . ': ';
2634             break;
2635         case 'mod':
2636             $prefix = get_string('activity') . ': ';
2637             break;
2639         // Default case, just use the plugin name.
2640         default:
2641             $prefix = '';
2642     }
2643     return $prefix . get_string('pluginname', $component);
2646 /**
2647  * Gets the list of roles assigned to this context and up (parents)
2648  * from the aggregation of:
2649  * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
2650  * b) if applicable, those roles that are assigned in the context.
2651  *
2652  * @param context $context
2653  * @return array
2654  */
2655 function get_profile_roles(context $context) {
2656     global $CFG, $DB;
2657     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2658     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2659     if (has_capability('moodle/role:assign', $context)) {
2660         $rolesinscope = array_keys(get_all_roles($context));
2661     } else {
2662         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2663     }
2665     if (empty($rolesinscope)) {
2666         return [];
2667     }
2669     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2670     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2671     $params = array_merge($params, $cparams);
2673     if ($coursecontext = $context->get_course_context(false)) {
2674         $params['coursecontext'] = $coursecontext->id;
2675     } else {
2676         $params['coursecontext'] = 0;
2677     }
2679     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2680               FROM {role_assignments} ra, {role} r
2681          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2682              WHERE r.id = ra.roleid
2683                    AND ra.contextid $contextlist
2684                    AND r.id $rallowed
2685           ORDER BY r.sortorder ASC";
2687     return $DB->get_records_sql($sql, $params);
2690 /**
2691  * Gets the list of roles assigned to this context and up (parents)
2692  *
2693  * @param context $context
2694  * @param boolean $includeparents, false means without parents.
2695  * @return array
2696  */
2697 function get_roles_used_in_context(context $context, $includeparents = true) {
2698     global $DB;
2700     if ($includeparents === true) {
2701         list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
2702     } else {
2703         list($contextlist, $params) = $DB->get_in_or_equal($context->id, SQL_PARAMS_NAMED, 'cl');
2704     }
2706     if ($coursecontext = $context->get_course_context(false)) {
2707         $params['coursecontext'] = $coursecontext->id;
2708     } else {
2709         $params['coursecontext'] = 0;
2710     }
2712     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2713               FROM {role_assignments} ra, {role} r
2714          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2715              WHERE r.id = ra.roleid
2716                    AND ra.contextid $contextlist
2717           ORDER BY r.sortorder ASC";
2719     return $DB->get_records_sql($sql, $params);
2722 /**
2723  * This function is used to print roles column in user profile page.
2724  * It is using the CFG->profileroles to limit the list to only interesting roles.
2725  * (The permission tab has full details of user role assignments.)
2726  *
2727  * @param int $userid
2728  * @param int $courseid
2729  * @return string
2730  */
2731 function get_user_roles_in_course($userid, $courseid) {
2732     global $CFG, $DB;
2733     if ($courseid == SITEID) {
2734         $context = context_system::instance();
2735     } else {
2736         $context = context_course::instance($courseid);
2737     }
2738     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2739     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2740     if (has_capability('moodle/role:assign', $context)) {
2741         $rolesinscope = array_keys(get_all_roles($context));
2742     } else {
2743         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2744     }
2745     if (empty($rolesinscope)) {
2746         return '';
2747     }
2749     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2750     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2751     $params = array_merge($params, $cparams);
2753     if ($coursecontext = $context->get_course_context(false)) {
2754         $params['coursecontext'] = $coursecontext->id;
2755     } else {
2756         $params['coursecontext'] = 0;
2757     }
2759     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2760               FROM {role_assignments} ra, {role} r
2761          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2762              WHERE r.id = ra.roleid
2763                    AND ra.contextid $contextlist
2764                    AND r.id $rallowed
2765                    AND ra.userid = :userid
2766           ORDER BY r.sortorder ASC";
2767     $params['userid'] = $userid;
2769     $rolestring = '';
2771     if ($roles = $DB->get_records_sql($sql, $params)) {
2772         $viewableroles = get_viewable_roles($context, $userid);
2774         $rolenames = array();
2775         foreach ($roles as $roleid => $unused) {
2776             if (isset($viewableroles[$roleid])) {
2777                 $url = new moodle_url('/user/index.php', ['contextid' => $context->id, 'roleid' => $roleid]);
2778                 $rolenames[] = '<a href="' . $url . '">' . $viewableroles[$roleid] . '</a>';
2779             }
2780         }
2781         $rolestring = implode(',', $rolenames);
2782     }
2784     return $rolestring;
2787 /**
2788  * Checks if a user can assign users to a particular role in this context
2789  *
2790  * @param context $context
2791  * @param int $targetroleid - the id of the role you want to assign users to
2792  * @return boolean
2793  */
2794 function user_can_assign(context $context, $targetroleid) {
2795     global $DB;
2797     // First check to see if the user is a site administrator.
2798     if (is_siteadmin()) {
2799         return true;
2800     }
2802     // Check if user has override capability.
2803     // If not return false.
2804     if (!has_capability('moodle/role:assign', $context)) {
2805         return false;
2806     }
2807     // pull out all active roles of this user from this context(or above)
2808     if ($userroles = get_user_roles($context)) {
2809         foreach ($userroles as $userrole) {
2810             // if any in the role_allow_override table, then it's ok
2811             if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
2812                 return true;
2813             }
2814         }
2815     }
2817     return false;
2820 /**
2821  * Returns all site roles in correct sort order.
2822  *
2823  * Note: this method does not localise role names or descriptions,
2824  *       use role_get_names() if you need role names.
2825  *
2826  * @param context $context optional context for course role name aliases
2827  * @return array of role records with optional coursealias property
2828  */
2829 function get_all_roles(context $context = null) {
2830     global $DB;
2832     if (!$context or !$coursecontext = $context->get_course_context(false)) {
2833         $coursecontext = null;
2834     }
2836     if ($coursecontext) {
2837         $sql = "SELECT r.*, rn.name AS coursealias
2838                   FROM {role} r
2839              LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2840               ORDER BY r.sortorder ASC";
2841         return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
2843     } else {
2844         return $DB->get_records('role', array(), 'sortorder ASC');
2845     }
2848 /**
2849  * Returns roles of a specified archetype
2850  *
2851  * @param string $archetype
2852  * @return array of full role records
2853  */
2854 function get_archetype_roles($archetype) {
2855     global $DB;
2856     return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
2859 /**
2860  * Gets all the user roles assigned in this context, or higher contexts for a list of users.
2861  *
2862  * If you try using the combination $userids = [], $checkparentcontexts = true then this is likely
2863  * to cause an out-of-memory error on large Moodle sites, so this combination is deprecated and
2864  * outputs a warning, even though it is the default.
2865  *
2866  * @param context $context
2867  * @param array $userids. An empty list means fetch all role assignments for the context.
2868  * @param bool $checkparentcontexts defaults to true
2869  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2870  * @return array
2871  */
2872 function get_users_roles(context $context, $userids = [], $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2873     global $DB;
2875     if (!$userids && $checkparentcontexts) {
2876         debugging('Please do not call get_users_roles() with $checkparentcontexts = true ' .
2877                 'and $userids array not set. This combination causes large Moodle sites ' .
2878                 'with lots of site-wide role assignemnts to run out of memory.', DEBUG_DEVELOPER);
2879     }
2881     if ($checkparentcontexts) {
2882         $contextids = $context->get_parent_context_ids();
2883     } else {
2884         $contextids = array();
2885     }
2886     $contextids[] = $context->id;
2888     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
2890     // If userids was passed as an empty array, we fetch all role assignments for the course.
2891     if (empty($userids)) {
2892         $useridlist = ' IS NOT NULL ';
2893         $uparams = [];
2894     } else {
2895         list($useridlist, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uids');
2896     }
2898     $sql = "SELECT ra.*, r.name, r.shortname, ra.userid
2899               FROM {role_assignments} ra, {role} r, {context} c
2900              WHERE ra.userid $useridlist
2901                    AND ra.roleid = r.id
2902                    AND ra.contextid = c.id
2903                    AND ra.contextid $contextids
2904           ORDER BY $order";
2906     $all = $DB->get_records_sql($sql , array_merge($params, $uparams));
2908     // Return results grouped by userid.
2909     $result = [];
2910     foreach ($all as $id => $record) {
2911         if (!isset($result[$record->userid])) {
2912             $result[$record->userid] = [];
2913         }
2914         $result[$record->userid][$record->id] = $record;
2915     }
2917     // Make sure all requested users are included in the result, even if they had no role assignments.
2918     foreach ($userids as $id) {
2919         if (!isset($result[$id])) {
2920             $result[$id] = [];
2921         }
2922     }
2924     return $result;
2928 /**
2929  * Gets all the user roles assigned in this context, or higher contexts
2930  * this is mainly used when checking if a user can assign a role, or overriding a role
2931  * i.e. we need to know what this user holds, in order to verify against allow_assign and
2932  * allow_override tables
2933  *
2934  * @param context $context
2935  * @param int $userid
2936  * @param bool $checkparentcontexts defaults to true
2937  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2938  * @return array
2939  */
2940 function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2941     global $USER, $DB;
2943     if (empty($userid)) {
2944         if (empty($USER->id)) {
2945             return array();
2946         }
2947         $userid = $USER->id;
2948     }
2950     if ($checkparentcontexts) {
2951         $contextids = $context->get_parent_context_ids();
2952     } else {
2953         $contextids = array();
2954     }
2955     $contextids[] = $context->id;
2957     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
2959     array_unshift($params, $userid);
2961     $sql = "SELECT ra.*, r.name, r.shortname
2962               FROM {role_assignments} ra, {role} r, {context} c
2963              WHERE ra.userid = ?
2964                    AND ra.roleid = r.id
2965                    AND ra.contextid = c.id
2966                    AND ra.contextid $contextids
2967           ORDER BY $order";
2969     return $DB->get_records_sql($sql ,$params);
2972 /**
2973  * Like get_user_roles, but adds in the authenticated user role, and the front
2974  * page roles, if applicable.
2975  *
2976  * @param context $context the context.
2977  * @param int $userid optional. Defaults to $USER->id
2978  * @return array of objects with fields ->userid, ->contextid and ->roleid.
2979  */
2980 function get_user_roles_with_special(context $context, $userid = 0) {
2981     global $CFG, $USER;
2983     if (empty($userid)) {
2984         if (empty($USER->id)) {
2985             return array();
2986         }
2987         $userid = $USER->id;
2988     }
2990     $ras = get_user_roles($context, $userid);
2992     // Add front-page role if relevant.
2993     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
2994     $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
2995             is_inside_frontpage($context);
2996     if ($defaultfrontpageroleid && $isfrontpage) {
2997         $frontpagecontext = context_course::instance(SITEID);
2998         $ra = new stdClass();
2999         $ra->userid = $userid;
3000         $ra->contextid = $frontpagecontext->id;
3001         $ra->roleid = $defaultfrontpageroleid;
3002         $ras[] = $ra;
3003     }
3005     // Add authenticated user role if relevant.
3006     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3007     if ($defaultuserroleid && !isguestuser($userid)) {
3008         $systemcontext = context_system::instance();
3009         $ra = new stdClass();
3010         $ra->userid = $userid;
3011         $ra->contextid = $systemcontext->id;
3012         $ra->roleid = $defaultuserroleid;
3013         $ras[] = $ra;
3014     }
3016     return $ras;
3019 /**
3020  * Creates a record in the role_allow_override table
3021  *
3022  * @param int $fromroleid source roleid
3023  * @param int $targetroleid target roleid
3024  * @return void
3025  */
3026 function core_role_set_override_allowed($fromroleid, $targetroleid) {
3027     global $DB;
3029     $record = new stdClass();
3030     $record->roleid        = $fromroleid;
3031     $record->allowoverride = $targetroleid;
3032     $DB->insert_record('role_allow_override', $record);
3035 /**
3036  * Creates a record in the role_allow_assign table
3037  *
3038  * @param int $fromroleid source roleid
3039  * @param int $targetroleid target roleid
3040  * @return void
3041  */
3042 function core_role_set_assign_allowed($fromroleid, $targetroleid) {
3043     global $DB;
3045     $record = new stdClass();
3046     $record->roleid      = $fromroleid;
3047     $record->allowassign = $targetroleid;
3048     $DB->insert_record('role_allow_assign', $record);
3051 /**
3052  * Creates a record in the role_allow_switch table
3053  *
3054  * @param int $fromroleid source roleid
3055  * @param int $targetroleid target roleid
3056  * @return void
3057  */
3058 function core_role_set_switch_allowed($fromroleid, $targetroleid) {
3059     global $DB;
3061     $record = new stdClass();
3062     $record->roleid      = $fromroleid;
3063     $record->allowswitch = $targetroleid;
3064     $DB->insert_record('role_allow_switch', $record);
3067 /**
3068  * Creates a record in the role_allow_view table
3069  *
3070  * @param int $fromroleid source roleid
3071  * @param int $targetroleid target roleid
3072  * @return void
3073  */
3074 function core_role_set_view_allowed($fromroleid, $targetroleid) {
3075     global $DB;
3077     $record = new stdClass();
3078     $record->roleid      = $fromroleid;
3079     $record->allowview = $targetroleid;
3080     $DB->insert_record('role_allow_view', $record);
3083 /**
3084  * Gets a list of roles that this user can assign in this context
3085  *
3086  * @param context $context the context.
3087  * @param int $rolenamedisplay the type of role name to display. One of the
3088  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3089  * @param bool $withusercounts if true, count the number of users with each role.
3090  * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
3091  * @return array if $withusercounts is false, then an array $roleid => $rolename.
3092  *      if $withusercounts is true, returns a list of three arrays,
3093  *      $rolenames, $rolecounts, and $nameswithcounts.
3094  */
3095 function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
3096     global $USER, $DB;
3098     // make sure there is a real user specified
3099     if ($user === null) {
3100         $userid = isset($USER->id) ? $USER->id : 0;
3101     } else {
3102         $userid = is_object($user) ? $user->id : $user;
3103     }
3105     if (!has_capability('moodle/role:assign', $context, $userid)) {
3106         if ($withusercounts) {
3107             return array(array(), array(), array());
3108         } else {
3109             return array();
3110         }
3111     }
3113     $params = array();
3114     $extrafields = '';
3116     if ($withusercounts) {
3117         $extrafields = ', (SELECT COUNT(DISTINCT u.id)
3118                              FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
3119                             WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
3120                           ) AS usercount';
3121         $params['conid'] = $context->id;
3122     }
3124     if (is_siteadmin($userid)) {
3125         // show all roles allowed in this context to admins
3126         $assignrestriction = "";
3127     } else {
3128         $parents = $context->get_parent_context_ids(true);
3129         $contexts = implode(',' , $parents);
3130         $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
3131                                       FROM {role_allow_assign} raa
3132                                       JOIN {role_assignments} ra ON ra.roleid = raa.roleid
3133                                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3134                                    ) ar ON ar.id = r.id";
3135         $params['userid'] = $userid;
3136     }
3137     $params['contextlevel'] = $context->contextlevel;
3139     if ($coursecontext = $context->get_course_context(false)) {
3140         $params['coursecontext'] = $coursecontext->id;
3141     } else {
3142         $params['coursecontext'] = 0; // no course aliases
3143         $coursecontext = null;
3144     }
3145     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
3146               FROM {role} r
3147               $assignrestriction
3148               JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
3149          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3150           ORDER BY r.sortorder ASC";
3151     $roles = $DB->get_records_sql($sql, $params);
3153     $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
3155     if (!$withusercounts) {
3156         return $rolenames;
3157     }
3159     $rolecounts = array();
3160     $nameswithcounts = array();
3161     foreach ($roles as $role) {
3162         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
3163         $rolecounts[$role->id] = $roles[$role->id]->usercount;
3164     }
3165     return array($rolenames, $rolecounts, $nameswithcounts);
3168 /**
3169  * Gets a list of roles that this user can switch to in a context
3170  *
3171  * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
3172  * This function just process the contents of the role_allow_switch table. You also need to
3173  * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
3174  *
3175  * @param context $context a context.
3176  * @param int $rolenamedisplay the type of role name to display. One of the
3177  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3178  * @return array an array $roleid => $rolename.
3179  */
3180 function get_switchable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS) {
3181     global $USER, $DB;
3183     // You can't switch roles without this capability.
3184     if (!has_capability('moodle/role:switchroles', $context)) {
3185         return [];
3186     }
3188     $params = array();
3189     $extrajoins = '';
3190     $extrawhere = '';
3191     if (!is_siteadmin()) {
3192         // Admins are allowed to switch to any role with.
3193         // Others are subject to the additional constraint that the switch-to role must be allowed by
3194         // 'role_allow_switch' for some role they have assigned in this context or any parent.
3195         $parents = $context->get_parent_context_ids(true);
3196         $contexts = implode(',' , $parents);
3198         $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
3199         JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3200         $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
3201         $params['userid'] = $USER->id;
3202     }
3204     if ($coursecontext = $context->get_course_context(false)) {
3205         $params['coursecontext'] = $coursecontext->id;
3206     } else {
3207         $params['coursecontext'] = 0; // no course aliases
3208         $coursecontext = null;
3209     }
3211     $query = "
3212         SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3213           FROM (SELECT DISTINCT rc.roleid
3214                   FROM {role_capabilities} rc
3216                   $extrajoins
3217                   $extrawhere) idlist
3218           JOIN {role} r ON r.id = idlist.roleid
3219      LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3220       ORDER BY r.sortorder";
3221     $roles = $DB->get_records_sql($query, $params);
3223     return role_fix_names($roles, $context, $rolenamedisplay, true);
3226 /**
3227  * Gets a list of roles that this user can view in a context
3228  *
3229  * @param context $context a context.
3230  * @param int $userid id of user.
3231  * @param int $rolenamedisplay the type of role name to display. One of the
3232  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3233  * @return array an array $roleid => $rolename.
3234  */
3235 function get_viewable_roles(context $context, $userid = null, $rolenamedisplay = ROLENAME_ALIAS) {
3236     global $USER, $DB;
3238     if ($userid == null) {
3239         $userid = $USER->id;
3240     }
3242     $params = array();
3243     $extrajoins = '';
3244     $extrawhere = '';
3245     if (!is_siteadmin()) {
3246         // Admins are allowed to view any role.
3247         // Others are subject to the additional constraint that the view role must be allowed by
3248         // 'role_allow_view' for some role they have assigned in this context or any parent.
3249         $contexts = $context->get_parent_context_ids(true);
3250         list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
3252         $extrajoins = "JOIN {role_allow_view} ras ON ras.allowview = r.id
3253                        JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3254         $extrawhere = "WHERE ra.userid = :userid AND ra.contextid $insql";
3256         $params += $inparams;
3257         $params['userid'] = $userid;
3258     }
3260     if ($coursecontext = $context->get_course_context(false)) {
3261         $params['coursecontext'] = $coursecontext->id;
3262     } else {
3263         $params['coursecontext'] = 0; // No course aliases.
3264         $coursecontext = null;
3265     }
3267     $query = "
3268         SELECT r.id, r.name, r.shortname, rn.name AS coursealias, r.sortorder
3269           FROM {role} r
3270           $extrajoins
3271      LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3272           $extrawhere
3273       GROUP BY r.id, r.name, r.shortname, rn.name, r.sortorder
3274       ORDER BY r.sortorder";
3275     $roles = $DB->get_records_sql($query, $params);
3277     return role_fix_names($roles, $context, $rolenamedisplay, true);
3280 /**
3281  * Gets a list of roles that this user can override in this context.
3282  *
3283  * @param context $context the context.
3284  * @param int $rolenamedisplay the type of role name to display. One of the
3285  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3286  * @param bool $withcounts if true, count the number of overrides that are set for each role.
3287  * @return array if $withcounts is false, then an array $roleid => $rolename.
3288  *      if $withusercounts is true, returns a list of three arrays,
3289  *      $rolenames, $rolecounts, and $nameswithcounts.
3290  */
3291 function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
3292     global $USER, $DB;
3294     if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
3295         if ($withcounts) {
3296             return array(array(), array(), array());
3297         } else {
3298             return array();
3299         }
3300     }
3302     $parents = $context->get_parent_context_ids(true);
3303     $contexts = implode(',' , $parents);
3305     $params = array();
3306     $extrafields = '';
3308     $params['userid'] = $USER->id;
3309     if ($withcounts) {
3310         $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
3311                 WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
3312         $params['conid'] = $context->id;
3313     }
3315     if ($coursecontext = $context->get_course_context(false)) {
3316         $params['coursecontext'] = $coursecontext->id;
3317     } else {
3318         $params['coursecontext'] = 0; // no course aliases
3319         $coursecontext = null;
3320     }
3322     if (is_siteadmin()) {
3323         // show all roles to admins
3324         $roles = $DB->get_records_sql("
3325             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3326               FROM {role} ro
3327          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3328           ORDER BY ro.sortorder ASC", $params);
3330     } else {
3331         $roles = $DB->get_records_sql("
3332             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3333               FROM {role} ro
3334               JOIN (SELECT DISTINCT r.id
3335                       FROM {role} r
3336                       JOIN {role_allow_override} rao ON r.id = rao.allowoverride
3337                       JOIN {role_assignments} ra ON rao.roleid = ra.roleid
3338                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3339                    ) inline_view ON ro.id = inline_view.id
3340          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3341           ORDER BY ro.sortorder ASC", $params);
3342     }
3344     $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
3346     if (!$withcounts) {
3347         return $rolenames;
3348     }
3350     $rolecounts = array();
3351     $nameswithcounts = array();
3352     foreach ($roles as $role) {
3353         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
3354         $rolecounts[$role->id] = $roles[$role->id]->overridecount;
3355     }
3356     return array($rolenames, $rolecounts, $nameswithcounts);
3359 /**
3360  * Create a role menu suitable for default role selection in enrol plugins.
3361  *
3362  * @package    core_enrol
3363  *
3364  * @param context $context
3365  * @param int $addroleid current or default role - always added to list
3366  * @return array roleid=>localised role name
3367  */
3368 function get_default_enrol_roles(context $context, $addroleid = null) {
3369     global $DB;
3371     $params = array('contextlevel'=>CONTEXT_COURSE);
3373     if ($coursecontext = $context->get_course_context(false)) {
3374         $params['coursecontext'] = $coursecontext->id;
3375     } else {
3376         $params['coursecontext'] = 0; // no course names
3377         $coursecontext = null;
3378     }
3380     if ($addroleid) {
3381         $addrole = "OR r.id = :addroleid";
3382         $params['addroleid'] = $addroleid;
3383     } else {
3384         $addrole = "";
3385     }
3387     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3388               FROM {role} r
3389          LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
3390          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3391              WHERE rcl.id IS NOT NULL $addrole
3392           ORDER BY sortorder DESC";
3394     $roles = $DB->get_records_sql($sql, $params);
3396     return role_fix_names($roles, $context, ROLENAME_BOTH, true);
3399 /**
3400  * Return context levels where this role is assignable.
3401  *
3402  * @param integer $roleid the id of a role.
3403  * @return array list of the context levels at which this role may be assigned.
3404  */
3405 function get_role_contextlevels($roleid) {
3406     global $DB;
3407     return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
3408             'contextlevel', 'id,contextlevel');
3411 /**
3412  * Return roles suitable for assignment at the specified context level.
3413  *
3414  * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
3415  *
3416  * @param integer $contextlevel a contextlevel.
3417  * @return array list of role ids that are assignable at this context level.
3418  */
3419 function get_roles_for_contextlevels($contextlevel) {
3420     global $DB;
3421     return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
3422             '', 'id,roleid');
3425 /**
3426  * Returns default context levels where roles can be assigned.
3427  *
3428  * @param string $rolearchetype one of the role archetypes - that is, one of the keys
3429  *      from the array returned by get_role_archetypes();
3430  * @return array list of the context levels at which this type of role may be assigned by default.
3431  */
3432 function get_default_contextlevels($rolearchetype) {
3433     static $defaults = array(
3434         'manager'        => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
3435         'coursecreator'  => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
3436         'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3437         'teacher'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3438         'student'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3439         'guest'          => array(),
3440         'user'           => array(),
3441         'frontpage'      => array());
3443     if (isset($defaults[$rolearchetype])) {
3444         return $defaults[$rolearchetype];
3445     } else {
3446         return array();
3447     }
3450 /**
3451  * Set the context levels at which a particular role can be assigned.
3452  * Throws exceptions in case of error.
3453  *
3454  * @param integer $roleid the id of a role.
3455  * @param array $contextlevels the context levels at which this role should be assignable,
3456  *      duplicate levels are removed.
3457  * @return void
3458  */
3459 function set_role_contextlevels($roleid, array $contextlevels) {
3460     global $DB;
3461     $DB->delete_records('role_context_levels', array('roleid' => $roleid));
3462     $rcl = new stdClass();
3463     $rcl->roleid = $roleid;
3464     $contextlevels = array_unique($contextlevels);
3465     foreach ($contextlevels as $level) {
3466         $rcl->contextlevel = $level;
3467         $DB->insert_record('role_context_levels', $rcl, false, true);
3468     }
3471 /**
3472  * Gets sql joins for finding users with capability in the given context.
3473  *
3474  * @param context $context Context for the join.
3475  * @param string|array $capability Capability name or array of names.
3476  *      If an array is provided then this is the equivalent of a logical 'OR',
3477  *      i.e. the user needs to have one of these capabilities.
3478  * @param string $useridcolumn e.g. 'u.id'.
3479  * @return \core\dml\sql_join Contains joins, wheres, params.
3480  *      This function will set ->cannotmatchanyrows if applicable.
3481  *      This may let you skip doing a DB query.
3482  */
3483 function get_with_capability_join(context $context, $capability, $useridcolumn) {
3484     global $CFG, $DB;
3486     // Add a unique prefix to param names to ensure they are unique.
3487     static $i = 0;
3488     $i++;
3489     $paramprefix = 'eu' . $i . '_';
3491     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3492     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3494     $ctxids = trim($context->path, '/');
3495     $ctxids = str_replace('/', ',', $ctxids);
3497     // Context is the frontpage
3498     $isfrontpage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID;
3499     $isfrontpage = $isfrontpage || is_inside_frontpage($context);
3501     $caps = (array) $capability;
3503     // Construct list of context paths bottom --> top.
3504     list($contextids, $paths) = get_context_info_list($context);
3506     // We need to find out all roles that have these capabilities either in definition or in overrides.
3507     $defs = [];
3508     list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, $paramprefix . 'con');
3509     list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, $paramprefix . 'cap');
3511     // Check whether context locking is enabled.
3512     // Filter out any write capability if this is the case.
3513     $excludelockedcaps = '';
3514     $excludelockedcapsparams = [];
3515     if (!empty($CFG->contextlocking) && $context->locked) {
3516         $excludelockedcaps = 'AND (cap.captype = :capread OR cap.name = :managelockscap)';
3517         $excludelockedcapsparams['capread'] = 'read';
3518         $excludelockedcapsparams['managelockscap'] = 'moodle/site:managecontextlocks';
3519     }
3521     $params = array_merge($params, $params2, $excludelockedcapsparams);
3522     $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3523               FROM {role_capabilities} rc
3524               JOIN {capabilities} cap ON rc.capability = cap.name
3525               JOIN {context} ctx on rc.contextid = ctx.id
3526              WHERE rc.contextid $incontexts AND rc.capability $incaps $excludelockedcaps";
3528     $rcs = $DB->get_records_sql($sql, $params);
3529     foreach ($rcs as $rc) {
3530         $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3531     }
3533     // Go through the permissions bottom-->top direction to evaluate the current permission,
3534     // first one wins (prohibit is an exception that always wins).
3535     $access = [];
3536     foreach ($caps as $cap) {
3537         foreach ($paths as $path) {
3538             if (empty($defs[$cap][$path])) {
3539                 continue;
3540             }
3541             foreach ($defs[$cap][$path] as $roleid => $perm) {
3542                 if ($perm == CAP_PROHIBIT) {
3543                     $access[$cap][$roleid] = CAP_PROHIBIT;
3544                     continue;
3545                 }
3546                 if (!isset($access[$cap][$roleid])) {
3547                     $access[$cap][$roleid] = (int)$perm;
3548                 }
3549             }
3550         }
3551     }
3553     // Make lists of roles that are needed and prohibited in this context.
3554     $needed = []; // One of these is enough.
3555     $prohibited = []; // Must not have any of these.
3556     foreach ($caps as $cap) {
3557         if (empty($access[$cap])) {
3558             continue;
3559         }
3560         foreach ($access[$cap] as $roleid => $perm) {
3561             if ($perm == CAP_PROHIBIT) {
3562                 unset($needed[$cap][$roleid]);
3563                 $prohibited[$cap][$roleid] = true;
3564             } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
3565                 $needed[$cap][$roleid] = true;
3566             }
3567         }
3568         if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
3569             // Easy, nobody has the permission.
3570             unset($needed[$cap]);
3571             unset($prohibited[$cap]);
3572         } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
3573             // Everybody is disqualified on the frontpage.
3574             unset($needed[$cap]);
3575             unset($prohibited[$cap]);
3576         }
3577         if (empty($prohibited[$cap])) {
3578             unset($prohibited[$cap]);
3579         }
3580     }
3582     if (empty($needed)) {
3583         // There can not be anybody if no roles match this request.
3584         return new \core\dml\sql_join('', '1 = 2', [], true);
3585     }
3587     if (empty($prohibited)) {
3588         // We can compact the needed roles.
3589         $n = [];
3590         foreach ($needed as $cap) {
3591             foreach ($cap as $roleid => $unused) {
3592                 $n[$roleid] = true;
3593             }
3594         }
3595         $needed = ['any' => $n];
3596         unset($n);
3597     }
3599     // Prepare query clauses.
3600     $wherecond = [];
3601     $params    = [];
3602     $joins     = [];
3603     $cannotmatchanyrows = false;
3605     // We never return deleted users or guest account.
3606     // Use a hack to get the deleted user column without an API change.
3607     $deletedusercolumn = substr($useridcolumn, 0, -2) . 'deleted';
3608     $wherecond[] = "$deletedusercolumn = 0 AND $useridcolumn <> :{$paramprefix}guestid";
3609     $params[$paramprefix . 'guestid'] = $CFG->siteguest;
3611     // Now add the needed and prohibited roles conditions as joins.
3612     if (!empty($needed['any'])) {
3613         // Simple case - there are no prohibits involved.
3614         if (!empty($needed['any'][$defaultuserroleid]) ||
3615                 ($isfrontpage && !empty($needed['any'][$defaultfrontpageroleid]))) {
3616             // Everybody.
3617         } else {
3618             $joins[] = "JOIN (SELECT DISTINCT userid
3619                                 FROM {role_assignments}
3620                                WHERE contextid IN ($ctxids)
3621                                      AND roleid IN (" . implode(',', array_keys($needed['any'])) . ")
3622                              ) ra ON ra.userid = $useridcolumn";
3623         }
3624     } else {
3625         $unions = [];
3626         $everybody = false;
3627         foreach ($needed as $cap => $unused) {
3628             if (empty($prohibited[$cap])) {
3629                 if (!empty($needed[$cap][$defaultuserroleid]) ||
3630                         ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3631                     $everybody = true;
3632                     break;
3633                 } else {
3634                     $unions[] = "SELECT userid
3635                                    FROM {role_assignments}
3636                                   WHERE contextid IN ($ctxids)
3637                                         AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
3638                 }
3639             } else {
3640                 if (!empty($prohibited[$cap][$defaultuserroleid]) ||
3641                         ($isfrontpage && !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
3642                     // Nobody can have this cap because it is prohibited in default roles.
3643                     continue;
3645                 } else if (!empty($needed[$cap][$defaultuserroleid]) ||
3646                         ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3647                     // Everybody except the prohibited - hiding does not matter.
3648                     $unions[] = "SELECT id AS userid
3649                                    FROM {user}
3650                                   WHERE id NOT IN (SELECT userid
3651                                                      FROM {role_assignments}
3652                                                     WHERE contextid IN ($ctxids)
3653                                                           AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3655                 } else {
3656                     $unions[] = "SELECT userid
3657                                    FROM {role_assignments}
3658                                   WHERE contextid IN ($ctxids) AND roleid IN (" . implode(',', array_keys($needed[$cap])) . ")
3659                                         AND userid NOT IN (
3660                                             SELECT userid
3661                                               FROM {role_assignments}
3662                                              WHERE contextid IN ($ctxids)
3663                                                    AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3664                 }
3665             }
3666         }
3668         if (!$everybody) {
3669             if ($unions) {
3670                 $joins[] = "JOIN (
3671                                   SELECT DISTINCT userid
3672                                     FROM (
3673