ab6f09eff00526d4dd80253dc82ec36c74c95df6
[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     // Find out if user is admin - it is not possible to override the doanything in any way
525     // and it is not possible to switch to admin role either.
526     if ($doanything) {
527         if (is_siteadmin($userid)) {
528             if ($userid != $USER->id) {
529                 return true;
530             }
531             // make sure switchrole is not used in this context
532             if (empty($USER->access['rsw'])) {
533                 return true;
534             }
535             $parts = explode('/', trim($context->path, '/'));
536             $path = '';
537             $switched = false;
538             foreach ($parts as $part) {
539                 $path .= '/' . $part;
540                 if (!empty($USER->access['rsw'][$path])) {
541                     $switched = true;
542                     break;
543                 }
544             }
545             if (!$switched) {
546                 return true;
547             }
548             //ok, admin switched role in this context, let's use normal access control rules
549         }
550     }
552     // Careful check for staleness...
553     $context->reload_if_dirty();
555     if ($USER->id == $userid) {
556         if (!isset($USER->access)) {
557             load_all_capabilities();
558         }
559         $access =& $USER->access;
561     } else {
562         // make sure user accessdata is really loaded
563         get_user_accessdata($userid, true);
564         $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
565     }
567     return has_capability_in_accessdata($capability, $context, $access);
570 /**
571  * Check if the user has any one of several capabilities from a list.
572  *
573  * This is just a utility method that calls has_capability in a loop. Try to put
574  * the capabilities that most users are likely to have first in the list for best
575  * performance.
576  *
577  * @category access
578  * @see has_capability()
579  *
580  * @param array $capabilities an array of capability names.
581  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
582  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
583  * @param boolean $doanything If false, ignore effect of admin role assignment
584  * @return boolean true if the user has any of these capabilities. Otherwise false.
585  */
586 function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
587     foreach ($capabilities as $capability) {
588         if (has_capability($capability, $context, $user, $doanything)) {
589             return true;
590         }
591     }
592     return false;
595 /**
596  * Check if the user has all the capabilities in a list.
597  *
598  * This is just a utility method that calls has_capability in a loop. Try to put
599  * the capabilities that fewest users are likely to have first in the list for best
600  * performance.
601  *
602  * @category access
603  * @see has_capability()
604  *
605  * @param array $capabilities an array of capability names.
606  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
607  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
608  * @param boolean $doanything If false, ignore effect of admin role assignment
609  * @return boolean true if the user has all of these capabilities. Otherwise false.
610  */
611 function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
612     foreach ($capabilities as $capability) {
613         if (!has_capability($capability, $context, $user, $doanything)) {
614             return false;
615         }
616     }
617     return true;
620 /**
621  * Is course creator going to have capability in a new course?
622  *
623  * This is intended to be used in enrolment plugins before or during course creation,
624  * do not use after the course is fully created.
625  *
626  * @category access
627  *
628  * @param string $capability the name of the capability to check.
629  * @param context $context course or category context where is course going to be created
630  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
631  * @return boolean true if the user will have this capability.
632  *
633  * @throws coding_exception if different type of context submitted
634  */
635 function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
636     global $CFG;
638     if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
639         throw new coding_exception('Only course or course category context expected');
640     }
642     if (has_capability($capability, $context, $user)) {
643         // User already has the capability, it could be only removed if CAP_PROHIBIT
644         // was involved here, but we ignore that.
645         return true;
646     }
648     if (!has_capability('moodle/course:create', $context, $user)) {
649         return false;
650     }
652     if (!enrol_is_enabled('manual')) {
653         return false;
654     }
656     if (empty($CFG->creatornewroleid)) {
657         return false;
658     }
660     if ($context->contextlevel == CONTEXT_COURSE) {
661         if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
662             return false;
663         }
664     } else {
665         if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
666             return false;
667         }
668     }
670     // Most likely they will be enrolled after the course creation is finished,
671     // does the new role have the required capability?
672     list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
673     return isset($neededroles[$CFG->creatornewroleid]);
676 /**
677  * Check if the user is an admin at the site level.
678  *
679  * Please note that use of proper capabilities is always encouraged,
680  * this function is supposed to be used from core or for temporary hacks.
681  *
682  * @category access
683  *
684  * @param  int|stdClass  $user_or_id user id or user object
685  * @return bool true if user is one of the administrators, false otherwise
686  */
687 function is_siteadmin($user_or_id = null) {
688     global $CFG, $USER;
690     if ($user_or_id === null) {
691         $user_or_id = $USER;
692     }
694     if (empty($user_or_id)) {
695         return false;
696     }
697     if (!empty($user_or_id->id)) {
698         $userid = $user_or_id->id;
699     } else {
700         $userid = $user_or_id;
701     }
703     // Because this script is called many times (150+ for course page) with
704     // the same parameters, it is worth doing minor optimisations. This static
705     // cache stores the value for a single userid, saving about 2ms from course
706     // page load time without using significant memory. As the static cache
707     // also includes the value it depends on, this cannot break unit tests.
708     static $knownid, $knownresult, $knownsiteadmins;
709     if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
710         return $knownresult;
711     }
712     $knownid = $userid;
713     $knownsiteadmins = $CFG->siteadmins;
715     $siteadmins = explode(',', $CFG->siteadmins);
716     $knownresult = in_array($userid, $siteadmins);
717     return $knownresult;
720 /**
721  * Returns true if user has at least one role assign
722  * of 'coursecontact' role (is potentially listed in some course descriptions).
723  *
724  * @param int $userid
725  * @return bool
726  */
727 function has_coursecontact_role($userid) {
728     global $DB, $CFG;
730     if (empty($CFG->coursecontact)) {
731         return false;
732     }
733     $sql = "SELECT 1
734               FROM {role_assignments}
735              WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
736     return $DB->record_exists_sql($sql, array('userid'=>$userid));
739 /**
740  * Does the user have a capability to do something?
741  *
742  * Walk the accessdata array and return true/false.
743  * Deals with prohibits, role switching, aggregating
744  * capabilities, etc.
745  *
746  * The main feature of here is being FAST and with no
747  * side effects.
748  *
749  * Notes:
750  *
751  * Switch Role merges with default role
752  * ------------------------------------
753  * If you are a teacher in course X, you have at least
754  * teacher-in-X + defaultloggedinuser-sitewide. So in the
755  * course you'll have techer+defaultloggedinuser.
756  * We try to mimic that in switchrole.
757  *
758  * Permission evaluation
759  * ---------------------
760  * Originally there was an extremely complicated way
761  * to determine the user access that dealt with
762  * "locality" or role assignments and role overrides.
763  * Now we simply evaluate access for each role separately
764  * and then verify if user has at least one role with allow
765  * and at the same time no role with prohibit.
766  *
767  * @access private
768  * @param string $capability
769  * @param context $context
770  * @param array $accessdata
771  * @return bool
772  */
773 function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
774     global $CFG;
776     // Build $paths as a list of current + all parent "paths" with order bottom-to-top
777     $path = $context->path;
778     $paths = array($path);
779     while ($path = rtrim($path, '0123456789')) {
780         $path = rtrim($path, '/');
781         if ($path === '') {
782             break;
783         }
784         $paths[] = $path;
785     }
787     $roles = array();
788     $switchedrole = false;
790     // Find out if role switched
791     if (!empty($accessdata['rsw'])) {
792         // From the bottom up...
793         foreach ($paths as $path) {
794             if (isset($accessdata['rsw'][$path])) {
795                 // Found a switchrole assignment - check for that role _plus_ the default user role
796                 $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
797                 $switchedrole = true;
798                 break;
799             }
800         }
801     }
803     if (!$switchedrole) {
804         // get all users roles in this context and above
805         foreach ($paths as $path) {
806             if (isset($accessdata['ra'][$path])) {
807                 foreach ($accessdata['ra'][$path] as $roleid) {
808                     $roles[$roleid] = null;
809                 }
810             }
811         }
812     }
814     // Now find out what access is given to each role, going bottom-->up direction
815     $rdefs = get_role_definitions(array_keys($roles));
816     $allowed = false;
818     foreach ($roles as $roleid => $ignored) {
819         foreach ($paths as $path) {
820             if (isset($rdefs[$roleid][$path][$capability])) {
821                 $perm = (int)$rdefs[$roleid][$path][$capability];
822                 if ($perm === CAP_PROHIBIT) {
823                     // any CAP_PROHIBIT found means no permission for the user
824                     return false;
825                 }
826                 if (is_null($roles[$roleid])) {
827                     $roles[$roleid] = $perm;
828                 }
829             }
830         }
831         // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
832         $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
833     }
835     return $allowed;
838 /**
839  * A convenience function that tests has_capability, and displays an error if
840  * the user does not have that capability.
841  *
842  * NOTE before Moodle 2.0, this function attempted to make an appropriate
843  * require_login call before checking the capability. This is no longer the case.
844  * You must call require_login (or one of its variants) if you want to check the
845  * user is logged in, before you call this function.
846  *
847  * @see has_capability()
848  *
849  * @param string $capability the name of the capability to check. For example mod/forum:view
850  * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
851  * @param int $userid A user id. By default (null) checks the permissions of the current user.
852  * @param bool $doanything If false, ignore effect of admin role assignment
853  * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
854  * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
855  * @return void terminates with an error if the user does not have the given capability.
856  */
857 function require_capability($capability, context $context, $userid = null, $doanything = true,
858                             $errormessage = 'nopermissions', $stringfile = '') {
859     if (!has_capability($capability, $context, $userid, $doanything)) {
860         throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
861     }
864 /**
865  * A convenience function that tests has_capability for a list of capabilities, and displays an error if
866  * the user does not have that capability.
867  *
868  * This is just a utility method that calls has_capability in a loop. Try to put
869  * the capabilities that fewest users are likely to have first in the list for best
870  * performance.
871  *
872  * @category access
873  * @see has_capability()
874  *
875  * @param array $capabilities an array of capability names.
876  * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
877  * @param int $userid A user id. By default (null) checks the permissions of the current user.
878  * @param bool $doanything If false, ignore effect of admin role assignment
879  * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
880  * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
881  * @return void terminates with an error if the user does not have the given capability.
882  */
883 function require_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true,
884                                   $errormessage = 'nopermissions', $stringfile = ''): void {
885     foreach ($capabilities as $capability) {
886         if (!has_capability($capability, $context, $userid, $doanything)) {
887             throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
888         }
889     }
892 /**
893  * Return a nested array showing all role assignments for the user.
894  * [ra] => [contextpath][roleid] = roleid
895  *
896  * @access private
897  * @param int $userid - the id of the user
898  * @return array access info array
899  */
900 function get_user_roles_sitewide_accessdata($userid) {
901     global $CFG, $DB;
903     $accessdata = get_empty_accessdata();
905     // start with the default role
906     if (!empty($CFG->defaultuserroleid)) {
907         $syscontext = context_system::instance();
908         $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
909     }
911     // load the "default frontpage role"
912     if (!empty($CFG->defaultfrontpageroleid)) {
913         $frontpagecontext = context_course::instance(get_site()->id);
914         if ($frontpagecontext->path) {
915             $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
916         }
917     }
919     // Preload every assigned role.
920     $sql = "SELECT ctx.path, ra.roleid, ra.contextid
921               FROM {role_assignments} ra
922               JOIN {context} ctx ON ctx.id = ra.contextid
923              WHERE ra.userid = :userid";
925     $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
927     foreach ($rs as $ra) {
928         // RAs leafs are arrays to support multi-role assignments...
929         $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
930     }
932     $rs->close();
934     return $accessdata;
937 /**
938  * Returns empty accessdata structure.
939  *
940  * @access private
941  * @return array empt accessdata
942  */
943 function get_empty_accessdata() {
944     $accessdata               = array(); // named list
945     $accessdata['ra']         = array();
946     $accessdata['time']       = time();
947     $accessdata['rsw']        = array();
949     return $accessdata;
952 /**
953  * Get accessdata for a given user.
954  *
955  * @access private
956  * @param int $userid
957  * @param bool $preloadonly true means do not return access array
958  * @return array accessdata
959  */
960 function get_user_accessdata($userid, $preloadonly=false) {
961     global $CFG, $ACCESSLIB_PRIVATE, $USER;
963     if (isset($USER->access)) {
964         $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
965     }
967     if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
968         if (empty($userid)) {
969             if (!empty($CFG->notloggedinroleid)) {
970                 $accessdata = get_role_access($CFG->notloggedinroleid);
971             } else {
972                 // weird
973                 return get_empty_accessdata();
974             }
976         } else if (isguestuser($userid)) {
977             if ($guestrole = get_guest_role()) {
978                 $accessdata = get_role_access($guestrole->id);
979             } else {
980                 //weird
981                 return get_empty_accessdata();
982             }
984         } else {
985             // Includes default role and frontpage role.
986             $accessdata = get_user_roles_sitewide_accessdata($userid);
987         }
989         $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
990     }
992     if ($preloadonly) {
993         return;
994     } else {
995         return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
996     }
999 /**
1000  * A convenience function to completely load all the capabilities
1001  * for the current user. It is called from has_capability() and functions change permissions.
1002  *
1003  * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
1004  * @see check_enrolment_plugins()
1005  *
1006  * @access private
1007  * @return void
1008  */
1009 function load_all_capabilities() {
1010     global $USER;
1012     // roles not installed yet - we are in the middle of installation
1013     if (during_initial_install()) {
1014         return;
1015     }
1017     if (!isset($USER->id)) {
1018         // this should not happen
1019         $USER->id = 0;
1020     }
1022     unset($USER->access);
1023     $USER->access = get_user_accessdata($USER->id);
1025     // Clear to force a refresh
1026     unset($USER->mycourses);
1028     // init/reset internal enrol caches - active course enrolments and temp access
1029     $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
1032 /**
1033  * A convenience function to completely reload all the capabilities
1034  * for the current user when roles have been updated in a relevant
1035  * context -- but PRESERVING switchroles and loginas.
1036  * This function resets all accesslib and context caches.
1037  *
1038  * That is - completely transparent to the user.
1039  *
1040  * Note: reloads $USER->access completely.
1041  *
1042  * @access private
1043  * @return void
1044  */
1045 function reload_all_capabilities() {
1046     global $USER, $DB, $ACCESSLIB_PRIVATE;
1048     // copy switchroles
1049     $sw = array();
1050     if (!empty($USER->access['rsw'])) {
1051         $sw = $USER->access['rsw'];
1052     }
1054     accesslib_clear_all_caches(true);
1055     unset($USER->access);
1057     // Prevent dirty flags refetching on this page.
1058     $ACCESSLIB_PRIVATE->dirtycontexts = array();
1059     $ACCESSLIB_PRIVATE->dirtyusers    = array($USER->id => false);
1061     load_all_capabilities();
1063     foreach ($sw as $path => $roleid) {
1064         if ($record = $DB->get_record('context', array('path'=>$path))) {
1065             $context = context::instance_by_id($record->id);
1066             if (has_capability('moodle/role:switchroles', $context)) {
1067                 role_switch($roleid, $context);
1068             }
1069         }
1070     }
1073 /**
1074  * Adds a temp role to current USER->access array.
1075  *
1076  * Useful for the "temporary guest" access we grant to logged-in users.
1077  * This is useful for enrol plugins only.
1078  *
1079  * @since Moodle 2.2
1080  * @param context_course $coursecontext
1081  * @param int $roleid
1082  * @return void
1083  */
1084 function load_temp_course_role(context_course $coursecontext, $roleid) {
1085     global $USER, $SITE;
1087     if (empty($roleid)) {
1088         debugging('invalid role specified in load_temp_course_role()');
1089         return;
1090     }
1092     if ($coursecontext->instanceid == $SITE->id) {
1093         debugging('Can not use temp roles on the frontpage');
1094         return;
1095     }
1097     if (!isset($USER->access)) {
1098         load_all_capabilities();
1099     }
1101     $coursecontext->reload_if_dirty();
1103     if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
1104         return;
1105     }
1107     $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
1110 /**
1111  * Removes any extra guest roles from current USER->access array.
1112  * This is useful for enrol plugins only.
1113  *
1114  * @since Moodle 2.2
1115  * @param context_course $coursecontext
1116  * @return void
1117  */
1118 function remove_temp_course_roles(context_course $coursecontext) {
1119     global $DB, $USER, $SITE;
1121     if ($coursecontext->instanceid == $SITE->id) {
1122         debugging('Can not use temp roles on the frontpage');
1123         return;
1124     }
1126     if (empty($USER->access['ra'][$coursecontext->path])) {
1127         //no roles here, weird
1128         return;
1129     }
1131     $sql = "SELECT DISTINCT ra.roleid AS id
1132               FROM {role_assignments} ra
1133              WHERE ra.contextid = :contextid AND ra.userid = :userid";
1134     $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
1136     $USER->access['ra'][$coursecontext->path] = array();
1137     foreach ($ras as $r) {
1138         $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
1139     }
1142 /**
1143  * Returns array of all role archetypes.
1144  *
1145  * @return array
1146  */
1147 function get_role_archetypes() {
1148     return array(
1149         'manager'        => 'manager',
1150         'coursecreator'  => 'coursecreator',
1151         'editingteacher' => 'editingteacher',
1152         'teacher'        => 'teacher',
1153         'student'        => 'student',
1154         'guest'          => 'guest',
1155         'user'           => 'user',
1156         'frontpage'      => 'frontpage'
1157     );
1160 /**
1161  * Assign the defaults found in this capability definition to roles that have
1162  * the corresponding legacy capabilities assigned to them.
1163  *
1164  * @param string $capability
1165  * @param array $legacyperms an array in the format (example):
1166  *                      'guest' => CAP_PREVENT,
1167  *                      'student' => CAP_ALLOW,
1168  *                      'teacher' => CAP_ALLOW,
1169  *                      'editingteacher' => CAP_ALLOW,
1170  *                      'coursecreator' => CAP_ALLOW,
1171  *                      'manager' => CAP_ALLOW
1172  * @return boolean success or failure.
1173  */
1174 function assign_legacy_capabilities($capability, $legacyperms) {
1176     $archetypes = get_role_archetypes();
1178     foreach ($legacyperms as $type => $perm) {
1180         $systemcontext = context_system::instance();
1181         if ($type === 'admin') {
1182             debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1183             $type = 'manager';
1184         }
1186         if (!array_key_exists($type, $archetypes)) {
1187             print_error('invalidlegacy', '', '', $type);
1188         }
1190         if ($roles = get_archetype_roles($type)) {
1191             foreach ($roles as $role) {
1192                 // Assign a site level capability.
1193                 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1194                     return false;
1195                 }
1196             }
1197         }
1198     }
1199     return true;
1202 /**
1203  * Verify capability risks.
1204  *
1205  * @param stdClass $capability a capability - a row from the capabilities table.
1206  * @return boolean whether this capability is safe - that is, whether people with the
1207  *      safeoverrides capability should be allowed to change it.
1208  */
1209 function is_safe_capability($capability) {
1210     return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1213 /**
1214  * Get the local override (if any) for a given capability in a role in a context
1215  *
1216  * @param int $roleid
1217  * @param int $contextid
1218  * @param string $capability
1219  * @return stdClass local capability override
1220  */
1221 function get_local_override($roleid, $contextid, $capability) {
1222     global $DB;
1224     return $DB->get_record_sql("
1225         SELECT rc.*
1226           FROM {role_capabilities} rc
1227           JOIN {capability} cap ON rc.capability = cap.name
1228          WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
1229             'roleid' => $roleid,
1230             'contextid' => $contextid,
1231             'capability' => $capability,
1233         ]);
1236 /**
1237  * Returns context instance plus related course and cm instances
1238  *
1239  * @param int $contextid
1240  * @return array of ($context, $course, $cm)
1241  */
1242 function get_context_info_array($contextid) {
1243     global $DB;
1245     $context = context::instance_by_id($contextid, MUST_EXIST);
1246     $course  = null;
1247     $cm      = null;
1249     if ($context->contextlevel == CONTEXT_COURSE) {
1250         $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
1252     } else if ($context->contextlevel == CONTEXT_MODULE) {
1253         $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
1254         $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1256     } else if ($context->contextlevel == CONTEXT_BLOCK) {
1257         $parent = $context->get_parent_context();
1259         if ($parent->contextlevel == CONTEXT_COURSE) {
1260             $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
1261         } else if ($parent->contextlevel == CONTEXT_MODULE) {
1262             $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
1263             $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1264         }
1265     }
1267     return array($context, $course, $cm);
1270 /**
1271  * Function that creates a role
1272  *
1273  * @param string $name role name
1274  * @param string $shortname role short name
1275  * @param string $description role description
1276  * @param string $archetype
1277  * @return int id or dml_exception
1278  */
1279 function create_role($name, $shortname, $description, $archetype = '') {
1280     global $DB;
1282     if (strpos($archetype, 'moodle/legacy:') !== false) {
1283         throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
1284     }
1286     // verify role archetype actually exists
1287     $archetypes = get_role_archetypes();
1288     if (empty($archetypes[$archetype])) {
1289         $archetype = '';
1290     }
1292     // Insert the role record.
1293     $role = new stdClass();
1294     $role->name        = $name;
1295     $role->shortname   = $shortname;
1296     $role->description = $description;
1297     $role->archetype   = $archetype;
1299     //find free sortorder number
1300     $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
1301     if (empty($role->sortorder)) {
1302         $role->sortorder = 1;
1303     }
1304     $id = $DB->insert_record('role', $role);
1306     return $id;
1309 /**
1310  * Function that deletes a role and cleanups up after it
1311  *
1312  * @param int $roleid id of role to delete
1313  * @return bool always true
1314  */
1315 function delete_role($roleid) {
1316     global $DB;
1318     // first unssign all users
1319     role_unassign_all(array('roleid'=>$roleid));
1321     // cleanup all references to this role, ignore errors
1322     $DB->delete_records('role_capabilities',   array('roleid'=>$roleid));
1323     $DB->delete_records('role_allow_assign',   array('roleid'=>$roleid));
1324     $DB->delete_records('role_allow_assign',   array('allowassign'=>$roleid));
1325     $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
1326     $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
1327     $DB->delete_records('role_names',          array('roleid'=>$roleid));
1328     $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
1330     // Get role record before it's deleted.
1331     $role = $DB->get_record('role', array('id'=>$roleid));
1333     // Finally delete the role itself.
1334     $DB->delete_records('role', array('id'=>$roleid));
1336     // Trigger event.
1337     $event = \core\event\role_deleted::create(
1338         array(
1339             'context' => context_system::instance(),
1340             'objectid' => $roleid,
1341             'other' =>
1342                 array(
1343                     'shortname' => $role->shortname,
1344                     'description' => $role->description,
1345                     'archetype' => $role->archetype
1346                 )
1347             )
1348         );
1349     $event->add_record_snapshot('role', $role);
1350     $event->trigger();
1352     // Reset any cache of this role, including MUC.
1353     accesslib_clear_role_cache($roleid);
1355     return true;
1358 /**
1359  * Function to write context specific overrides, or default capabilities.
1360  *
1361  * @param string $capability string name
1362  * @param int $permission CAP_ constants
1363  * @param int $roleid role id
1364  * @param int|context $contextid context id
1365  * @param bool $overwrite
1366  * @return bool always true or exception
1367  */
1368 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
1369     global $USER, $DB;
1371     if ($contextid instanceof context) {
1372         $context = $contextid;
1373     } else {
1374         $context = context::instance_by_id($contextid);
1375     }
1377     // Capability must exist.
1378     if (!$capinfo = get_capability_info($capability)) {
1379         throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1380     }
1382     if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
1383         unassign_capability($capability, $roleid, $context->id);
1384         return true;
1385     }
1387     $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
1389     if ($existing and !$overwrite) {   // We want to keep whatever is there already
1390         return true;
1391     }
1393     $cap = new stdClass();
1394     $cap->contextid    = $context->id;
1395     $cap->roleid       = $roleid;
1396     $cap->capability   = $capability;
1397     $cap->permission   = $permission;
1398     $cap->timemodified = time();
1399     $cap->modifierid   = empty($USER->id) ? 0 : $USER->id;
1401     if ($existing) {
1402         $cap->id = $existing->id;
1403         $DB->update_record('role_capabilities', $cap);
1404     } else {
1405         if ($DB->record_exists('context', array('id'=>$context->id))) {
1406             $DB->insert_record('role_capabilities', $cap);
1407         }
1408     }
1410     // Trigger capability_assigned event.
1411     \core\event\capability_assigned::create([
1412         'userid' => $cap->modifierid,
1413         'context' => $context,
1414         'objectid' => $roleid,
1415         'other' => [
1416             'capability' => $capability,
1417             'oldpermission' => $existing->permission ?? CAP_INHERIT,
1418             'permission' => $permission
1419         ]
1420     ])->trigger();
1422     // Reset any cache of this role, including MUC.
1423     accesslib_clear_role_cache($roleid);
1425     return true;
1428 /**
1429  * Unassign a capability from a role.
1430  *
1431  * @param string $capability the name of the capability
1432  * @param int $roleid the role id
1433  * @param int|context $contextid null means all contexts
1434  * @return boolean true or exception
1435  */
1436 function unassign_capability($capability, $roleid, $contextid = null) {
1437     global $DB, $USER;
1439     // Capability must exist.
1440     if (!$capinfo = get_capability_info($capability)) {
1441         throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1442     }
1444     if (!empty($contextid)) {
1445         if ($contextid instanceof context) {
1446             $context = $contextid;
1447         } else {
1448             $context = context::instance_by_id($contextid);
1449         }
1450         // delete from context rel, if this is the last override in this context
1451         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
1452     } else {
1453         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
1454     }
1456     // Trigger capability_assigned event.
1457     \core\event\capability_unassigned::create([
1458         'userid' => $USER->id,
1459         'context' => $context ?? context_system::instance(),
1460         'objectid' => $roleid,
1461         'other' => [
1462             'capability' => $capability,
1463         ]
1464     ])->trigger();
1466     // Reset any cache of this role, including MUC.
1467     accesslib_clear_role_cache($roleid);
1469     return true;
1472 /**
1473  * Get the roles that have a given capability assigned to it
1474  *
1475  * This function does not resolve the actual permission of the capability.
1476  * It just checks for permissions and overrides.
1477  * Use get_roles_with_cap_in_context() if resolution is required.
1478  *
1479  * @param string $capability capability name (string)
1480  * @param string $permission optional, the permission defined for this capability
1481  *                      either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
1482  * @param stdClass $context null means any
1483  * @return array of role records
1484  */
1485 function get_roles_with_capability($capability, $permission = null, $context = null) {
1486     global $DB;
1488     if ($context) {
1489         $contexts = $context->get_parent_context_ids(true);
1490         list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
1491         $contextsql = "AND rc.contextid $insql";
1492     } else {
1493         $params = array();
1494         $contextsql = '';
1495     }
1497     if ($permission) {
1498         $permissionsql = " AND rc.permission = :permission";
1499         $params['permission'] = $permission;
1500     } else {
1501         $permissionsql = '';
1502     }
1504     $sql = "SELECT r.*
1505               FROM {role} r
1506              WHERE r.id IN (SELECT rc.roleid
1507                               FROM {role_capabilities} rc
1508                               JOIN {capabilities} cap ON rc.capability = cap.name
1509                              WHERE rc.capability = :capname
1510                                    $contextsql
1511                                    $permissionsql)";
1512     $params['capname'] = $capability;
1515     return $DB->get_records_sql($sql, $params);
1518 /**
1519  * This function makes a role-assignment (a role for a user in a particular context)
1520  *
1521  * @param int $roleid the role of the id
1522  * @param int $userid userid
1523  * @param int|context $contextid id of the context
1524  * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
1525  * @param int $itemid id of enrolment/auth plugin
1526  * @param string $timemodified defaults to current time
1527  * @return int new/existing id of the assignment
1528  */
1529 function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
1530     global $USER, $DB;
1532     // first of all detect if somebody is using old style parameters
1533     if ($contextid === 0 or is_numeric($component)) {
1534         throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
1535     }
1537     // now validate all parameters
1538     if (empty($roleid)) {
1539         throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
1540     }
1542     if (empty($userid)) {
1543         throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
1544     }
1546     if ($itemid) {
1547         if (strpos($component, '_') === false) {
1548             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
1549         }
1550     } else {
1551         $itemid = 0;
1552         if ($component !== '' and strpos($component, '_') === false) {
1553             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1554         }
1555     }
1557     if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
1558         throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
1559     }
1561     if ($contextid instanceof context) {
1562         $context = $contextid;
1563     } else {
1564         $context = context::instance_by_id($contextid, MUST_EXIST);
1565     }
1567     if (!$timemodified) {
1568         $timemodified = time();
1569     }
1571     // Check for existing entry
1572     $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
1574     if ($ras) {
1575         // role already assigned - this should not happen
1576         if (count($ras) > 1) {
1577             // very weird - remove all duplicates!
1578             $ra = array_shift($ras);
1579             foreach ($ras as $r) {
1580                 $DB->delete_records('role_assignments', array('id'=>$r->id));
1581             }
1582         } else {
1583             $ra = reset($ras);
1584         }
1586         // actually there is no need to update, reset anything or trigger any event, so just return
1587         return $ra->id;
1588     }
1590     // Create a new entry
1591     $ra = new stdClass();
1592     $ra->roleid       = $roleid;
1593     $ra->contextid    = $context->id;
1594     $ra->userid       = $userid;
1595     $ra->component    = $component;
1596     $ra->itemid       = $itemid;
1597     $ra->timemodified = $timemodified;
1598     $ra->modifierid   = empty($USER->id) ? 0 : $USER->id;
1599     $ra->sortorder    = 0;
1601     $ra->id = $DB->insert_record('role_assignments', $ra);
1603     // Role assignments have changed, so mark user as dirty.
1604     mark_user_dirty($userid);
1606     core_course_category::role_assignment_changed($roleid, $context);
1608     $event = \core\event\role_assigned::create(array(
1609         'context' => $context,
1610         'objectid' => $ra->roleid,
1611         'relateduserid' => $ra->userid,
1612         'other' => array(
1613             'id' => $ra->id,
1614             'component' => $ra->component,
1615             'itemid' => $ra->itemid
1616         )
1617     ));
1618     $event->add_record_snapshot('role_assignments', $ra);
1619     $event->trigger();
1621     return $ra->id;
1624 /**
1625  * Removes one role assignment
1626  *
1627  * @param int $roleid
1628  * @param int  $userid
1629  * @param int  $contextid
1630  * @param string $component
1631  * @param int  $itemid
1632  * @return void
1633  */
1634 function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
1635     // first make sure the params make sense
1636     if ($roleid == 0 or $userid == 0 or $contextid == 0) {
1637         throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
1638     }
1640     if ($itemid) {
1641         if (strpos($component, '_') === false) {
1642             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
1643         }
1644     } else {
1645         $itemid = 0;
1646         if ($component !== '' and strpos($component, '_') === false) {
1647             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1648         }
1649     }
1651     role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
1654 /**
1655  * Removes multiple role assignments, parameters may contain:
1656  *   'roleid', 'userid', 'contextid', 'component', 'enrolid'.
1657  *
1658  * @param array $params role assignment parameters
1659  * @param bool $subcontexts unassign in subcontexts too
1660  * @param bool $includemanual include manual role assignments too
1661  * @return void
1662  */
1663 function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
1664     global $USER, $CFG, $DB;
1666     if (!$params) {
1667         throw new coding_exception('Missing parameters in role_unsassign_all() call');
1668     }
1670     $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
1671     foreach ($params as $key=>$value) {
1672         if (!in_array($key, $allowed)) {
1673             throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
1674         }
1675     }
1677     if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
1678         throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
1679     }
1681     if ($includemanual) {
1682         if (!isset($params['component']) or $params['component'] === '') {
1683             throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
1684         }
1685     }
1687     if ($subcontexts) {
1688         if (empty($params['contextid'])) {
1689             throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
1690         }
1691     }
1693     $ras = $DB->get_records('role_assignments', $params);
1694     foreach ($ras as $ra) {
1695         $DB->delete_records('role_assignments', array('id'=>$ra->id));
1696         if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
1697             // Role assignments have changed, so mark user as dirty.
1698             mark_user_dirty($ra->userid);
1700             $event = \core\event\role_unassigned::create(array(
1701                 'context' => $context,
1702                 'objectid' => $ra->roleid,
1703                 'relateduserid' => $ra->userid,
1704                 'other' => array(
1705                     'id' => $ra->id,
1706                     'component' => $ra->component,
1707                     'itemid' => $ra->itemid
1708                 )
1709             ));
1710             $event->add_record_snapshot('role_assignments', $ra);
1711             $event->trigger();
1712             core_course_category::role_assignment_changed($ra->roleid, $context);
1713         }
1714     }
1715     unset($ras);
1717     // process subcontexts
1718     if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
1719         if ($params['contextid'] instanceof context) {
1720             $context = $params['contextid'];
1721         } else {
1722             $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
1723         }
1725         if ($context) {
1726             $contexts = $context->get_child_contexts();
1727             $mparams = $params;
1728             foreach ($contexts as $context) {
1729                 $mparams['contextid'] = $context->id;
1730                 $ras = $DB->get_records('role_assignments', $mparams);
1731                 foreach ($ras as $ra) {
1732                     $DB->delete_records('role_assignments', array('id'=>$ra->id));
1733                     // Role assignments have changed, so mark user as dirty.
1734                     mark_user_dirty($ra->userid);
1736                     $event = \core\event\role_unassigned::create(
1737                         array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1738                             'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1739                     $event->add_record_snapshot('role_assignments', $ra);
1740                     $event->trigger();
1741                     core_course_category::role_assignment_changed($ra->roleid, $context);
1742                 }
1743             }
1744         }
1745     }
1747     // do this once more for all manual role assignments
1748     if ($includemanual) {
1749         $params['component'] = '';
1750         role_unassign_all($params, $subcontexts, false);
1751     }
1754 /**
1755  * Mark a user as dirty (with timestamp) so as to force reloading of the user session.
1756  *
1757  * @param int $userid
1758  * @return void
1759  */
1760 function mark_user_dirty($userid) {
1761     global $CFG, $ACCESSLIB_PRIVATE;
1763     if (during_initial_install()) {
1764         return;
1765     }
1767     // Throw exception if invalid userid is provided.
1768     if (empty($userid)) {
1769         throw new coding_exception('Invalid user parameter supplied for mark_user_dirty() function!');
1770     }
1772     // Set dirty flag in database, set dirty field locally, and clear local accessdata cache.
1773     set_cache_flag('accesslib/dirtyusers', $userid, 1, time() + $CFG->sessiontimeout);
1774     $ACCESSLIB_PRIVATE->dirtyusers[$userid] = 1;
1775     unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
1778 /**
1779  * Determines if a user is currently logged in
1780  *
1781  * @category   access
1782  *
1783  * @return bool
1784  */
1785 function isloggedin() {
1786     global $USER;
1788     return (!empty($USER->id));
1791 /**
1792  * Determines if a user is logged in as real guest user with username 'guest'.
1793  *
1794  * @category   access
1795  *
1796  * @param int|object $user mixed user object or id, $USER if not specified
1797  * @return bool true if user is the real guest user, false if not logged in or other user
1798  */
1799 function isguestuser($user = null) {
1800     global $USER, $DB, $CFG;
1802     // make sure we have the user id cached in config table, because we are going to use it a lot
1803     if (empty($CFG->siteguest)) {
1804         if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
1805             // guest does not exist yet, weird
1806             return false;
1807         }
1808         set_config('siteguest', $guestid);
1809     }
1810     if ($user === null) {
1811         $user = $USER;
1812     }
1814     if ($user === null) {
1815         // happens when setting the $USER
1816         return false;
1818     } else if (is_numeric($user)) {
1819         return ($CFG->siteguest == $user);
1821     } else if (is_object($user)) {
1822         if (empty($user->id)) {
1823             return false; // not logged in means is not be guest
1824         } else {
1825             return ($CFG->siteguest == $user->id);
1826         }
1828     } else {
1829         throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
1830     }
1833 /**
1834  * Does user have a (temporary or real) guest access to course?
1835  *
1836  * @category   access
1837  *
1838  * @param context $context
1839  * @param stdClass|int $user
1840  * @return bool
1841  */
1842 function is_guest(context $context, $user = null) {
1843     global $USER;
1845     // first find the course context
1846     $coursecontext = $context->get_course_context();
1848     // make sure there is a real user specified
1849     if ($user === null) {
1850         $userid = isset($USER->id) ? $USER->id : 0;
1851     } else {
1852         $userid = is_object($user) ? $user->id : $user;
1853     }
1855     if (isguestuser($userid)) {
1856         // can not inspect or be enrolled
1857         return true;
1858     }
1860     if (has_capability('moodle/course:view', $coursecontext, $user)) {
1861         // viewing users appear out of nowhere, they are neither guests nor participants
1862         return false;
1863     }
1865     // consider only real active enrolments here
1866     if (is_enrolled($coursecontext, $user, '', true)) {
1867         return false;
1868     }
1870     return true;
1873 /**
1874  * Returns true if the user has moodle/course:view capability in the course,
1875  * this is intended for admins, managers (aka small admins), inspectors, etc.
1876  *
1877  * @category   access
1878  *
1879  * @param context $context
1880  * @param int|stdClass $user if null $USER is used
1881  * @param string $withcapability extra capability name
1882  * @return bool
1883  */
1884 function is_viewing(context $context, $user = null, $withcapability = '') {
1885     // first find the course context
1886     $coursecontext = $context->get_course_context();
1888     if (isguestuser($user)) {
1889         // can not inspect
1890         return false;
1891     }
1893     if (!has_capability('moodle/course:view', $coursecontext, $user)) {
1894         // admins are allowed to inspect courses
1895         return false;
1896     }
1898     if ($withcapability and !has_capability($withcapability, $context, $user)) {
1899         // site admins always have the capability, but the enrolment above blocks
1900         return false;
1901     }
1903     return true;
1906 /**
1907  * Returns true if the user is able to access the course.
1908  *
1909  * This function is in no way, shape, or form a substitute for require_login.
1910  * It should only be used in circumstances where it is not possible to call require_login
1911  * such as the navigation.
1912  *
1913  * This function checks many of the methods of access to a course such as the view
1914  * capability, enrollments, and guest access. It also makes use of the cache
1915  * generated by require_login for guest access.
1916  *
1917  * The flags within the $USER object that are used here should NEVER be used outside
1918  * of this function can_access_course and require_login. Doing so WILL break future
1919  * versions.
1920  *
1921  * @param stdClass $course record
1922  * @param stdClass|int|null $user user record or id, current user if null
1923  * @param string $withcapability Check for this capability as well.
1924  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1925  * @return boolean Returns true if the user is able to access the course
1926  */
1927 function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
1928     global $DB, $USER;
1930     // this function originally accepted $coursecontext parameter
1931     if ($course instanceof context) {
1932         if ($course instanceof context_course) {
1933             debugging('deprecated context parameter, please use $course record');
1934             $coursecontext = $course;
1935             $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
1936         } else {
1937             debugging('Invalid context parameter, please use $course record');
1938             return false;
1939         }
1940     } else {
1941         $coursecontext = context_course::instance($course->id);
1942     }
1944     if (!isset($USER->id)) {
1945         // should never happen
1946         $USER->id = 0;
1947         debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
1948     }
1950     // make sure there is a user specified
1951     if ($user === null) {
1952         $userid = $USER->id;
1953     } else {
1954         $userid = is_object($user) ? $user->id : $user;
1955     }
1956     unset($user);
1958     if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
1959         return false;
1960     }
1962     if ($userid == $USER->id) {
1963         if (!empty($USER->access['rsw'][$coursecontext->path])) {
1964             // the fact that somebody switched role means they can access the course no matter to what role they switched
1965             return true;
1966         }
1967     }
1969     if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
1970         return false;
1971     }
1973     if (is_viewing($coursecontext, $userid)) {
1974         return true;
1975     }
1977     if ($userid != $USER->id) {
1978         // for performance reasons we do not verify temporary guest access for other users, sorry...
1979         return is_enrolled($coursecontext, $userid, '', $onlyactive);
1980     }
1982     // === from here we deal only with $USER ===
1984     $coursecontext->reload_if_dirty();
1986     if (isset($USER->enrol['enrolled'][$course->id])) {
1987         if ($USER->enrol['enrolled'][$course->id] > time()) {
1988             return true;
1989         }
1990     }
1991     if (isset($USER->enrol['tempguest'][$course->id])) {
1992         if ($USER->enrol['tempguest'][$course->id] > time()) {
1993             return true;
1994         }
1995     }
1997     if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
1998         return true;
1999     }
2001     if (!core_course_category::can_view_course_info($course)) {
2002         // No guest access if user does not have capability to browse courses.
2003         return false;
2004     }
2006     // if not enrolled try to gain temporary guest access
2007     $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
2008     $enrols = enrol_get_plugins(true);
2009     foreach ($instances as $instance) {
2010         if (!isset($enrols[$instance->enrol])) {
2011             continue;
2012         }
2013         // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
2014         $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2015         if ($until !== false and $until > time()) {
2016             $USER->enrol['tempguest'][$course->id] = $until;
2017             return true;
2018         }
2019     }
2020     if (isset($USER->enrol['tempguest'][$course->id])) {
2021         unset($USER->enrol['tempguest'][$course->id]);
2022         remove_temp_course_roles($coursecontext);
2023     }
2025     return false;
2028 /**
2029  * Loads the capability definitions for the component (from file).
2030  *
2031  * Loads the capability definitions for the component (from file). If no
2032  * capabilities are defined for the component, we simply return an empty array.
2033  *
2034  * @access private
2035  * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
2036  * @return array array of capabilities
2037  */
2038 function load_capability_def($component) {
2039     $defpath = core_component::get_component_directory($component).'/db/access.php';
2041     $capabilities = array();
2042     if (file_exists($defpath)) {
2043         require($defpath);
2044         if (!empty(${$component.'_capabilities'})) {
2045             // BC capability array name
2046             // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
2047             debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
2048             $capabilities = ${$component.'_capabilities'};
2049         }
2050     }
2052     return $capabilities;
2055 /**
2056  * Gets the capabilities that have been cached in the database for this component.
2057  *
2058  * @access private
2059  * @param string $component - examples: 'moodle', 'mod_forum'
2060  * @return array array of capabilities
2061  */
2062 function get_cached_capabilities($component = 'moodle') {
2063     global $DB;
2064     $caps = get_all_capabilities();
2065     $componentcaps = array();
2066     foreach ($caps as $cap) {
2067         if ($cap['component'] == $component) {
2068             $componentcaps[] = (object) $cap;
2069         }
2070     }
2071     return $componentcaps;
2074 /**
2075  * Returns default capabilities for given role archetype.
2076  *
2077  * @param string $archetype role archetype
2078  * @return array
2079  */
2080 function get_default_capabilities($archetype) {
2081     global $DB;
2083     if (!$archetype) {
2084         return array();
2085     }
2087     $alldefs = array();
2088     $defaults = array();
2089     $components = array();
2090     $allcaps = get_all_capabilities();
2092     foreach ($allcaps as $cap) {
2093         if (!in_array($cap['component'], $components)) {
2094             $components[] = $cap['component'];
2095             $alldefs = array_merge($alldefs, load_capability_def($cap['component']));
2096         }
2097     }
2098     foreach ($alldefs as $name=>$def) {
2099         // Use array 'archetypes if available. Only if not specified, use 'legacy'.
2100         if (isset($def['archetypes'])) {
2101             if (isset($def['archetypes'][$archetype])) {
2102                 $defaults[$name] = $def['archetypes'][$archetype];
2103             }
2104         // 'legacy' is for backward compatibility with 1.9 access.php
2105         } else {
2106             if (isset($def['legacy'][$archetype])) {
2107                 $defaults[$name] = $def['legacy'][$archetype];
2108             }
2109         }
2110     }
2112     return $defaults;
2115 /**
2116  * Return default roles that can be assigned, overridden or switched
2117  * by give role archetype.
2118  *
2119  * @param string $type  assign|override|switch|view
2120  * @param string $archetype
2121  * @return array of role ids
2122  */
2123 function get_default_role_archetype_allows($type, $archetype) {
2124     global $DB;
2126     if (empty($archetype)) {
2127         return array();
2128     }
2130     $roles = $DB->get_records('role');
2131     $archetypemap = array();
2132     foreach ($roles as $role) {
2133         if ($role->archetype) {
2134             $archetypemap[$role->archetype][$role->id] = $role->id;
2135         }
2136     }
2138     $defaults = array(
2139         'assign' => array(
2140             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
2141             'coursecreator'  => array(),
2142             'editingteacher' => array('teacher', 'student'),
2143             'teacher'        => array(),
2144             'student'        => array(),
2145             'guest'          => array(),
2146             'user'           => array(),
2147             'frontpage'      => array(),
2148         ),
2149         'override' => array(
2150             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2151             'coursecreator'  => array(),
2152             'editingteacher' => array('teacher', 'student', 'guest'),
2153             'teacher'        => array(),
2154             'student'        => array(),
2155             'guest'          => array(),
2156             'user'           => array(),
2157             'frontpage'      => array(),
2158         ),
2159         'switch' => array(
2160             'manager'        => array('editingteacher', 'teacher', 'student', 'guest'),
2161             'coursecreator'  => array(),
2162             'editingteacher' => array('teacher', 'student', 'guest'),
2163             'teacher'        => array('student', 'guest'),
2164             'student'        => array(),
2165             'guest'          => array(),
2166             'user'           => array(),
2167             'frontpage'      => array(),
2168         ),
2169         'view' => array(
2170             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2171             'coursecreator'  => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2172             'editingteacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2173             'teacher'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2174             'student'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2175             'guest'          => array(),
2176             'user'           => array(),
2177             'frontpage'      => array(),
2178         ),
2179     );
2181     if (!isset($defaults[$type][$archetype])) {
2182         debugging("Unknown type '$type'' or archetype '$archetype''");
2183         return array();
2184     }
2186     $return = array();
2187     foreach ($defaults[$type][$archetype] as $at) {
2188         if (isset($archetypemap[$at])) {
2189             foreach ($archetypemap[$at] as $roleid) {
2190                 $return[$roleid] = $roleid;
2191             }
2192         }
2193     }
2195     return $return;
2198 /**
2199  * Reset role capabilities to default according to selected role archetype.
2200  * If no archetype selected, removes all capabilities.
2201  *
2202  * This applies to capabilities that are assigned to the role (that you could
2203  * edit in the 'define roles' interface), and not to any capability overrides
2204  * in different locations.
2205  *
2206  * @param int $roleid ID of role to reset capabilities for
2207  */
2208 function reset_role_capabilities($roleid) {
2209     global $DB;
2211     $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
2212     $defaultcaps = get_default_capabilities($role->archetype);
2214     $systemcontext = context_system::instance();
2216     $DB->delete_records('role_capabilities',
2217             array('roleid' => $roleid, 'contextid' => $systemcontext->id));
2219     foreach ($defaultcaps as $cap=>$permission) {
2220         assign_capability($cap, $permission, $roleid, $systemcontext->id);
2221     }
2223     // Reset any cache of this role, including MUC.
2224     accesslib_clear_role_cache($roleid);
2227 /**
2228  * Updates the capabilities table with the component capability definitions.
2229  * If no parameters are given, the function updates the core moodle
2230  * capabilities.
2231  *
2232  * Note that the absence of the db/access.php capabilities definition file
2233  * will cause any stored capabilities for the component to be removed from
2234  * the database.
2235  *
2236  * @access private
2237  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
2238  * @return boolean true if success, exception in case of any problems
2239  */
2240 function update_capabilities($component = 'moodle') {
2241     global $DB, $OUTPUT;
2243     $storedcaps = array();
2245     $filecaps = load_capability_def($component);
2246     foreach ($filecaps as $capname=>$unused) {
2247         if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
2248             debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
2249         }
2250     }
2252     // It is possible somebody directly modified the DB (according to accesslib_test anyway).
2253     // So ensure our updating is based on fresh data.
2254     cache::make('core', 'capabilities')->delete('core_capabilities');
2256     $cachedcaps = get_cached_capabilities($component);
2257     if ($cachedcaps) {
2258         foreach ($cachedcaps as $cachedcap) {
2259             array_push($storedcaps, $cachedcap->name);
2260             // update risk bitmasks and context levels in existing capabilities if needed
2261             if (array_key_exists($cachedcap->name, $filecaps)) {
2262                 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
2263                     $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
2264                 }
2265                 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
2266                     $updatecap = new stdClass();
2267                     $updatecap->id = $cachedcap->id;
2268                     $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
2269                     $DB->update_record('capabilities', $updatecap);
2270                 }
2271                 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
2272                     $updatecap = new stdClass();
2273                     $updatecap->id = $cachedcap->id;
2274                     $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
2275                     $DB->update_record('capabilities', $updatecap);
2276                 }
2278                 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
2279                     $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
2280                 }
2281                 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
2282                     $updatecap = new stdClass();
2283                     $updatecap->id = $cachedcap->id;
2284                     $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
2285                     $DB->update_record('capabilities', $updatecap);
2286                 }
2287             }
2288         }
2289     }
2291     // Flush the cached again, as we have changed DB.
2292     cache::make('core', 'capabilities')->delete('core_capabilities');
2294     // Are there new capabilities in the file definition?
2295     $newcaps = array();
2297     foreach ($filecaps as $filecap => $def) {
2298         if (!$storedcaps ||
2299                 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
2300             if (!array_key_exists('riskbitmask', $def)) {
2301                 $def['riskbitmask'] = 0; // no risk if not specified
2302             }
2303             $newcaps[$filecap] = $def;
2304         }
2305     }
2306     // Add new capabilities to the stored definition.
2307     $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
2308     foreach ($newcaps as $capname => $capdef) {
2309         $capability = new stdClass();
2310         $capability->name         = $capname;
2311         $capability->captype      = $capdef['captype'];
2312         $capability->contextlevel = $capdef['contextlevel'];
2313         $capability->component    = $component;
2314         $capability->riskbitmask  = $capdef['riskbitmask'];
2316         $DB->insert_record('capabilities', $capability, false);
2318         // Flush the cached, as we have changed DB.
2319         cache::make('core', 'capabilities')->delete('core_capabilities');
2321         if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
2322             if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
2323                 foreach ($rolecapabilities as $rolecapability){
2324                     //assign_capability will update rather than insert if capability exists
2325                     if (!assign_capability($capname, $rolecapability->permission,
2326                                             $rolecapability->roleid, $rolecapability->contextid, true)){
2327                          echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
2328                     }
2329                 }
2330             }
2331         // we ignore archetype key if we have cloned permissions
2332         } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
2333             assign_legacy_capabilities($capname, $capdef['archetypes']);
2334         // 'legacy' is for backward compatibility with 1.9 access.php
2335         } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
2336             assign_legacy_capabilities($capname, $capdef['legacy']);
2337         }
2338     }
2339     // Are there any capabilities that have been removed from the file
2340     // definition that we need to delete from the stored capabilities and
2341     // role assignments?
2342     capabilities_cleanup($component, $filecaps);
2344     // reset static caches
2345     accesslib_reset_role_cache();
2347     // Flush the cached again, as we have changed DB.
2348     cache::make('core', 'capabilities')->delete('core_capabilities');
2350     return true;
2353 /**
2354  * Deletes cached capabilities that are no longer needed by the component.
2355  * Also unassigns these capabilities from any roles that have them.
2356  * NOTE: this function is called from lib/db/upgrade.php
2357  *
2358  * @access private
2359  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
2360  * @param array $newcapdef array of the new capability definitions that will be
2361  *                     compared with the cached capabilities
2362  * @return int number of deprecated capabilities that have been removed
2363  */
2364 function capabilities_cleanup($component, $newcapdef = null) {
2365     global $DB;
2367     $removedcount = 0;
2369     if ($cachedcaps = get_cached_capabilities($component)) {
2370         foreach ($cachedcaps as $cachedcap) {
2371             if (empty($newcapdef) ||
2372                         array_key_exists($cachedcap->name, $newcapdef) === false) {
2374                 // Delete from roles.
2375                 if ($roles = get_roles_with_capability($cachedcap->name)) {
2376                     foreach ($roles as $role) {
2377                         if (!unassign_capability($cachedcap->name, $role->id)) {
2378                             print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
2379                         }
2380                     }
2381                 }
2383                 // Remove from role_capabilities for any old ones.
2384                 $DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
2386                 // Remove from capabilities cache.
2387                 $DB->delete_records('capabilities', array('name' => $cachedcap->name));
2388                 $removedcount++;
2389             } // End if.
2390         }
2391     }
2392     if ($removedcount) {
2393         cache::make('core', 'capabilities')->delete('core_capabilities');
2394     }
2395     return $removedcount;
2398 /**
2399  * Returns an array of all the known types of risk
2400  * The array keys can be used, for example as CSS class names, or in calls to
2401  * print_risk_icon. The values are the corresponding RISK_ constants.
2402  *
2403  * @return array all the known types of risk.
2404  */
2405 function get_all_risks() {
2406     return array(
2407         'riskmanagetrust' => RISK_MANAGETRUST,
2408         'riskconfig'      => RISK_CONFIG,
2409         'riskxss'         => RISK_XSS,
2410         'riskpersonal'    => RISK_PERSONAL,
2411         'riskspam'        => RISK_SPAM,
2412         'riskdataloss'    => RISK_DATALOSS,
2413     );
2416 /**
2417  * Return a link to moodle docs for a given capability name
2418  *
2419  * @param stdClass $capability a capability - a row from the mdl_capabilities table.
2420  * @return string the human-readable capability name as a link to Moodle Docs.
2421  */
2422 function get_capability_docs_link($capability) {
2423     $url = get_docs_url('Capabilities/' . $capability->name);
2424     return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
2427 /**
2428  * This function pulls out all the resolved capabilities (overrides and
2429  * defaults) of a role used in capability overrides in contexts at a given
2430  * context.
2431  *
2432  * @param int $roleid
2433  * @param context $context
2434  * @param string $cap capability, optional, defaults to ''
2435  * @return array Array of capabilities
2436  */
2437 function role_context_capabilities($roleid, context $context, $cap = '') {
2438     global $DB;
2440     $contexts = $context->get_parent_context_ids(true);
2441     $contexts = '('.implode(',', $contexts).')';
2443     $params = array($roleid);
2445     if ($cap) {
2446         $search = " AND rc.capability = ? ";
2447         $params[] = $cap;
2448     } else {
2449         $search = '';
2450     }
2452     $sql = "SELECT rc.*
2453               FROM {role_capabilities} rc
2454               JOIN {context} c ON rc.contextid = c.id
2455               JOIN {capabilities} cap ON rc.capability = cap.name
2456              WHERE rc.contextid in $contexts
2457                    AND rc.roleid = ?
2458                    $search
2459           ORDER BY c.contextlevel DESC, rc.capability DESC";
2461     $capabilities = array();
2463     if ($records = $DB->get_records_sql($sql, $params)) {
2464         // We are traversing via reverse order.
2465         foreach ($records as $record) {
2466             // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
2467             if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
2468                 $capabilities[$record->capability] = $record->permission;
2469             }
2470         }
2471     }
2472     return $capabilities;
2475 /**
2476  * Constructs array with contextids as first parameter and context paths,
2477  * in both cases bottom top including self.
2478  *
2479  * @access private
2480  * @param context $context
2481  * @return array
2482  */
2483 function get_context_info_list(context $context) {
2484     $contextids = explode('/', ltrim($context->path, '/'));
2485     $contextpaths = array();
2486     $contextids2 = $contextids;
2487     while ($contextids2) {
2488         $contextpaths[] = '/' . implode('/', $contextids2);
2489         array_pop($contextids2);
2490     }
2491     return array($contextids, $contextpaths);
2494 /**
2495  * Check if context is the front page context or a context inside it
2496  *
2497  * Returns true if this context is the front page context, or a context inside it,
2498  * otherwise false.
2499  *
2500  * @param context $context a context object.
2501  * @return bool
2502  */
2503 function is_inside_frontpage(context $context) {
2504     $frontpagecontext = context_course::instance(SITEID);
2505     return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
2508 /**
2509  * Returns capability information (cached)
2510  *
2511  * @param string $capabilityname
2512  * @return stdClass or null if capability not found
2513  */
2514 function get_capability_info($capabilityname) {
2515     $caps = get_all_capabilities();
2517     if (!isset($caps[$capabilityname])) {
2518         return null;
2519     }
2521     return (object) $caps[$capabilityname];
2524 /**
2525  * Returns all capabilitiy records, preferably from MUC and not database.
2526  *
2527  * @return array All capability records indexed by capability name
2528  */
2529 function get_all_capabilities() {
2530     global $DB;
2531     $cache = cache::make('core', 'capabilities');
2532     if (!$allcaps = $cache->get('core_capabilities')) {
2533         $rs = $DB->get_recordset('capabilities');
2534         $allcaps = array();
2535         foreach ($rs as $capability) {
2536             $capability->riskbitmask = (int) $capability->riskbitmask;
2537             $allcaps[$capability->name] = (array) $capability;
2538         }
2539         $rs->close();
2540         $cache->set('core_capabilities', $allcaps);
2541     }
2542     return $allcaps;
2545 /**
2546  * Returns the human-readable, translated version of the capability.
2547  * Basically a big switch statement.
2548  *
2549  * @param string $capabilityname e.g. mod/choice:readresponses
2550  * @return string
2551  */
2552 function get_capability_string($capabilityname) {
2554     // Typical capability name is 'plugintype/pluginname:capabilityname'
2555     list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
2557     if ($type === 'moodle') {
2558         $component = 'core_role';
2559     } else if ($type === 'quizreport') {
2560         //ugly hack!!
2561         $component = 'quiz_'.$name;
2562     } else {
2563         $component = $type.'_'.$name;
2564     }
2566     $stringname = $name.':'.$capname;
2568     if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
2569         return get_string($stringname, $component);
2570     }
2572     $dir = core_component::get_component_directory($component);
2573     if (!file_exists($dir)) {
2574         // plugin broken or does not exist, do not bother with printing of debug message
2575         return $capabilityname.' ???';
2576     }
2578     // something is wrong in plugin, better print debug
2579     return get_string($stringname, $component);
2582 /**
2583  * This gets the mod/block/course/core etc strings.
2584  *
2585  * @param string $component
2586  * @param int $contextlevel
2587  * @return string|bool String is success, false if failed
2588  */
2589 function get_component_string($component, $contextlevel) {
2591     if ($component === 'moodle' || $component === 'core') {
2592         return context_helper::get_level_name($contextlevel);
2593     }
2595     list($type, $name) = core_component::normalize_component($component);
2596     $dir = core_component::get_plugin_directory($type, $name);
2597     if (!file_exists($dir)) {
2598         // plugin not installed, bad luck, there is no way to find the name
2599         return $component . ' ???';
2600     }
2602     // Some plugin types need an extra prefix to make the name easy to understand.
2603     switch ($type) {
2604         case 'quiz':
2605             $prefix = get_string('quizreport', 'quiz') . ': ';
2606             break;
2607         case 'repository':
2608             $prefix = get_string('repository', 'repository') . ': ';
2609             break;
2610         case 'gradeimport':
2611             $prefix = get_string('gradeimport', 'grades') . ': ';
2612             break;
2613         case 'gradeexport':
2614             $prefix = get_string('gradeexport', 'grades') . ': ';
2615             break;
2616         case 'gradereport':
2617             $prefix = get_string('gradereport', 'grades') . ': ';
2618             break;
2619         case 'webservice':
2620             $prefix = get_string('webservice', 'webservice') . ': ';
2621             break;
2622         case 'block':
2623             $prefix = get_string('block') . ': ';
2624             break;
2625         case 'mod':
2626             $prefix = get_string('activity') . ': ';
2627             break;
2629         // Default case, just use the plugin name.
2630         default:
2631             $prefix = '';
2632     }
2633     return $prefix . get_string('pluginname', $component);
2636 /**
2637  * Gets the list of roles assigned to this context and up (parents)
2638  * from the aggregation of:
2639  * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
2640  * b) if applicable, those roles that are assigned in the context.
2641  *
2642  * @param context $context
2643  * @return array
2644  */
2645 function get_profile_roles(context $context) {
2646     global $CFG, $DB;
2647     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2648     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2649     if (has_capability('moodle/role:assign', $context)) {
2650         $rolesinscope = array_keys(get_all_roles($context));
2651     } else {
2652         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2653     }
2655     if (empty($rolesinscope)) {
2656         return [];
2657     }
2659     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2660     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2661     $params = array_merge($params, $cparams);
2663     if ($coursecontext = $context->get_course_context(false)) {
2664         $params['coursecontext'] = $coursecontext->id;
2665     } else {
2666         $params['coursecontext'] = 0;
2667     }
2669     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2670               FROM {role_assignments} ra, {role} r
2671          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2672              WHERE r.id = ra.roleid
2673                    AND ra.contextid $contextlist
2674                    AND r.id $rallowed
2675           ORDER BY r.sortorder ASC";
2677     return $DB->get_records_sql($sql, $params);
2680 /**
2681  * Gets the list of roles assigned to this context and up (parents)
2682  *
2683  * @param context $context
2684  * @param boolean $includeparents, false means without parents.
2685  * @return array
2686  */
2687 function get_roles_used_in_context(context $context, $includeparents = true) {
2688     global $DB;
2690     if ($includeparents === true) {
2691         list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
2692     } else {
2693         list($contextlist, $params) = $DB->get_in_or_equal($context->id, SQL_PARAMS_NAMED, 'cl');
2694     }
2696     if ($coursecontext = $context->get_course_context(false)) {
2697         $params['coursecontext'] = $coursecontext->id;
2698     } else {
2699         $params['coursecontext'] = 0;
2700     }
2702     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2703               FROM {role_assignments} ra, {role} r
2704          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2705              WHERE r.id = ra.roleid
2706                    AND ra.contextid $contextlist
2707           ORDER BY r.sortorder ASC";
2709     return $DB->get_records_sql($sql, $params);
2712 /**
2713  * This function is used to print roles column in user profile page.
2714  * It is using the CFG->profileroles to limit the list to only interesting roles.
2715  * (The permission tab has full details of user role assignments.)
2716  *
2717  * @param int $userid
2718  * @param int $courseid
2719  * @return string
2720  */
2721 function get_user_roles_in_course($userid, $courseid) {
2722     global $CFG, $DB;
2723     if ($courseid == SITEID) {
2724         $context = context_system::instance();
2725     } else {
2726         $context = context_course::instance($courseid);
2727     }
2728     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2729     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2730     if (has_capability('moodle/role:assign', $context)) {
2731         $rolesinscope = array_keys(get_all_roles($context));
2732     } else {
2733         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2734     }
2735     if (empty($rolesinscope)) {
2736         return '';
2737     }
2739     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2740     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2741     $params = array_merge($params, $cparams);
2743     if ($coursecontext = $context->get_course_context(false)) {
2744         $params['coursecontext'] = $coursecontext->id;
2745     } else {
2746         $params['coursecontext'] = 0;
2747     }
2749     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2750               FROM {role_assignments} ra, {role} r
2751          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2752              WHERE r.id = ra.roleid
2753                    AND ra.contextid $contextlist
2754                    AND r.id $rallowed
2755                    AND ra.userid = :userid
2756           ORDER BY r.sortorder ASC";
2757     $params['userid'] = $userid;
2759     $rolestring = '';
2761     if ($roles = $DB->get_records_sql($sql, $params)) {
2762         $viewableroles = get_viewable_roles($context, $userid);
2764         $rolenames = array();
2765         foreach ($roles as $roleid => $unused) {
2766             if (isset($viewableroles[$roleid])) {
2767                 $url = new moodle_url('/user/index.php', ['contextid' => $context->id, 'roleid' => $roleid]);
2768                 $rolenames[] = '<a href="' . $url . '">' . $viewableroles[$roleid] . '</a>';
2769             }
2770         }
2771         $rolestring = implode(',', $rolenames);
2772     }
2774     return $rolestring;
2777 /**
2778  * Checks if a user can assign users to a particular role in this context
2779  *
2780  * @param context $context
2781  * @param int $targetroleid - the id of the role you want to assign users to
2782  * @return boolean
2783  */
2784 function user_can_assign(context $context, $targetroleid) {
2785     global $DB;
2787     // First check to see if the user is a site administrator.
2788     if (is_siteadmin()) {
2789         return true;
2790     }
2792     // Check if user has override capability.
2793     // If not return false.
2794     if (!has_capability('moodle/role:assign', $context)) {
2795         return false;
2796     }
2797     // pull out all active roles of this user from this context(or above)
2798     if ($userroles = get_user_roles($context)) {
2799         foreach ($userroles as $userrole) {
2800             // if any in the role_allow_override table, then it's ok
2801             if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
2802                 return true;
2803             }
2804         }
2805     }
2807     return false;
2810 /**
2811  * Returns all site roles in correct sort order.
2812  *
2813  * Note: this method does not localise role names or descriptions,
2814  *       use role_get_names() if you need role names.
2815  *
2816  * @param context $context optional context for course role name aliases
2817  * @return array of role records with optional coursealias property
2818  */
2819 function get_all_roles(context $context = null) {
2820     global $DB;
2822     if (!$context or !$coursecontext = $context->get_course_context(false)) {
2823         $coursecontext = null;
2824     }
2826     if ($coursecontext) {
2827         $sql = "SELECT r.*, rn.name AS coursealias
2828                   FROM {role} r
2829              LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2830               ORDER BY r.sortorder ASC";
2831         return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
2833     } else {
2834         return $DB->get_records('role', array(), 'sortorder ASC');
2835     }
2838 /**
2839  * Returns roles of a specified archetype
2840  *
2841  * @param string $archetype
2842  * @return array of full role records
2843  */
2844 function get_archetype_roles($archetype) {
2845     global $DB;
2846     return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
2849 /**
2850  * Gets all the user roles assigned in this context, or higher contexts for a list of users.
2851  *
2852  * If you try using the combination $userids = [], $checkparentcontexts = true then this is likely
2853  * to cause an out-of-memory error on large Moodle sites, so this combination is deprecated and
2854  * outputs a warning, even though it is the default.
2855  *
2856  * @param context $context
2857  * @param array $userids. An empty list means fetch all role assignments for the context.
2858  * @param bool $checkparentcontexts defaults to true
2859  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2860  * @return array
2861  */
2862 function get_users_roles(context $context, $userids = [], $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2863     global $DB;
2865     if (!$userids && $checkparentcontexts) {
2866         debugging('Please do not call get_users_roles() with $checkparentcontexts = true ' .
2867                 'and $userids array not set. This combination causes large Moodle sites ' .
2868                 'with lots of site-wide role assignemnts to run out of memory.', DEBUG_DEVELOPER);
2869     }
2871     if ($checkparentcontexts) {
2872         $contextids = $context->get_parent_context_ids();
2873     } else {
2874         $contextids = array();
2875     }
2876     $contextids[] = $context->id;
2878     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
2880     // If userids was passed as an empty array, we fetch all role assignments for the course.
2881     if (empty($userids)) {
2882         $useridlist = ' IS NOT NULL ';
2883         $uparams = [];
2884     } else {
2885         list($useridlist, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uids');
2886     }
2888     $sql = "SELECT ra.*, r.name, r.shortname, ra.userid
2889               FROM {role_assignments} ra, {role} r, {context} c
2890              WHERE ra.userid $useridlist
2891                    AND ra.roleid = r.id
2892                    AND ra.contextid = c.id
2893                    AND ra.contextid $contextids
2894           ORDER BY $order";
2896     $all = $DB->get_records_sql($sql , array_merge($params, $uparams));
2898     // Return results grouped by userid.
2899     $result = [];
2900     foreach ($all as $id => $record) {
2901         if (!isset($result[$record->userid])) {
2902             $result[$record->userid] = [];
2903         }
2904         $result[$record->userid][$record->id] = $record;
2905     }
2907     // Make sure all requested users are included in the result, even if they had no role assignments.
2908     foreach ($userids as $id) {
2909         if (!isset($result[$id])) {
2910             $result[$id] = [];
2911         }
2912     }
2914     return $result;
2918 /**
2919  * Gets all the user roles assigned in this context, or higher contexts
2920  * this is mainly used when checking if a user can assign a role, or overriding a role
2921  * i.e. we need to know what this user holds, in order to verify against allow_assign and
2922  * allow_override tables
2923  *
2924  * @param context $context
2925  * @param int $userid
2926  * @param bool $checkparentcontexts defaults to true
2927  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2928  * @return array
2929  */
2930 function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2931     global $USER, $DB;
2933     if (empty($userid)) {
2934         if (empty($USER->id)) {
2935             return array();
2936         }
2937         $userid = $USER->id;
2938     }
2940     if ($checkparentcontexts) {
2941         $contextids = $context->get_parent_context_ids();
2942     } else {
2943         $contextids = array();
2944     }
2945     $contextids[] = $context->id;
2947     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
2949     array_unshift($params, $userid);
2951     $sql = "SELECT ra.*, r.name, r.shortname
2952               FROM {role_assignments} ra, {role} r, {context} c
2953              WHERE ra.userid = ?
2954                    AND ra.roleid = r.id
2955                    AND ra.contextid = c.id
2956                    AND ra.contextid $contextids
2957           ORDER BY $order";
2959     return $DB->get_records_sql($sql ,$params);
2962 /**
2963  * Like get_user_roles, but adds in the authenticated user role, and the front
2964  * page roles, if applicable.
2965  *
2966  * @param context $context the context.
2967  * @param int $userid optional. Defaults to $USER->id
2968  * @return array of objects with fields ->userid, ->contextid and ->roleid.
2969  */
2970 function get_user_roles_with_special(context $context, $userid = 0) {
2971     global $CFG, $USER;
2973     if (empty($userid)) {
2974         if (empty($USER->id)) {
2975             return array();
2976         }
2977         $userid = $USER->id;
2978     }
2980     $ras = get_user_roles($context, $userid);
2982     // Add front-page role if relevant.
2983     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
2984     $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
2985             is_inside_frontpage($context);
2986     if ($defaultfrontpageroleid && $isfrontpage) {
2987         $frontpagecontext = context_course::instance(SITEID);
2988         $ra = new stdClass();
2989         $ra->userid = $userid;
2990         $ra->contextid = $frontpagecontext->id;
2991         $ra->roleid = $defaultfrontpageroleid;
2992         $ras[] = $ra;
2993     }
2995     // Add authenticated user role if relevant.
2996     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
2997     if ($defaultuserroleid && !isguestuser($userid)) {
2998         $systemcontext = context_system::instance();
2999         $ra = new stdClass();
3000         $ra->userid = $userid;
3001         $ra->contextid = $systemcontext->id;
3002         $ra->roleid = $defaultuserroleid;
3003         $ras[] = $ra;
3004     }
3006     return $ras;
3009 /**
3010  * Creates a record in the role_allow_override table
3011  *
3012  * @param int $fromroleid source roleid
3013  * @param int $targetroleid target roleid
3014  * @return void
3015  */
3016 function core_role_set_override_allowed($fromroleid, $targetroleid) {
3017     global $DB;
3019     $record = new stdClass();
3020     $record->roleid        = $fromroleid;
3021     $record->allowoverride = $targetroleid;
3022     $DB->insert_record('role_allow_override', $record);
3025 /**
3026  * Creates a record in the role_allow_assign table
3027  *
3028  * @param int $fromroleid source roleid
3029  * @param int $targetroleid target roleid
3030  * @return void
3031  */
3032 function core_role_set_assign_allowed($fromroleid, $targetroleid) {
3033     global $DB;
3035     $record = new stdClass();
3036     $record->roleid      = $fromroleid;
3037     $record->allowassign = $targetroleid;
3038     $DB->insert_record('role_allow_assign', $record);
3041 /**
3042  * Creates a record in the role_allow_switch table
3043  *
3044  * @param int $fromroleid source roleid
3045  * @param int $targetroleid target roleid
3046  * @return void
3047  */
3048 function core_role_set_switch_allowed($fromroleid, $targetroleid) {
3049     global $DB;
3051     $record = new stdClass();
3052     $record->roleid      = $fromroleid;
3053     $record->allowswitch = $targetroleid;
3054     $DB->insert_record('role_allow_switch', $record);
3057 /**
3058  * Creates a record in the role_allow_view table
3059  *
3060  * @param int $fromroleid source roleid
3061  * @param int $targetroleid target roleid
3062  * @return void
3063  */
3064 function core_role_set_view_allowed($fromroleid, $targetroleid) {
3065     global $DB;
3067     $record = new stdClass();
3068     $record->roleid      = $fromroleid;
3069     $record->allowview = $targetroleid;
3070     $DB->insert_record('role_allow_view', $record);
3073 /**
3074  * Gets a list of roles that this user can assign in this context
3075  *
3076  * @param context $context the context.
3077  * @param int $rolenamedisplay the type of role name to display. One of the
3078  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3079  * @param bool $withusercounts if true, count the number of users with each role.
3080  * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
3081  * @return array if $withusercounts is false, then an array $roleid => $rolename.
3082  *      if $withusercounts is true, returns a list of three arrays,
3083  *      $rolenames, $rolecounts, and $nameswithcounts.
3084  */
3085 function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
3086     global $USER, $DB;
3088     // make sure there is a real user specified
3089     if ($user === null) {
3090         $userid = isset($USER->id) ? $USER->id : 0;
3091     } else {
3092         $userid = is_object($user) ? $user->id : $user;
3093     }
3095     if (!has_capability('moodle/role:assign', $context, $userid)) {
3096         if ($withusercounts) {
3097             return array(array(), array(), array());
3098         } else {
3099             return array();
3100         }
3101     }
3103     $params = array();
3104     $extrafields = '';
3106     if ($withusercounts) {
3107         $extrafields = ', (SELECT COUNT(DISTINCT u.id)
3108                              FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
3109                             WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
3110                           ) AS usercount';
3111         $params['conid'] = $context->id;
3112     }
3114     if (is_siteadmin($userid)) {
3115         // show all roles allowed in this context to admins
3116         $assignrestriction = "";
3117     } else {
3118         $parents = $context->get_parent_context_ids(true);
3119         $contexts = implode(',' , $parents);
3120         $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
3121                                       FROM {role_allow_assign} raa
3122                                       JOIN {role_assignments} ra ON ra.roleid = raa.roleid
3123                                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3124                                    ) ar ON ar.id = r.id";
3125         $params['userid'] = $userid;
3126     }
3127     $params['contextlevel'] = $context->contextlevel;
3129     if ($coursecontext = $context->get_course_context(false)) {
3130         $params['coursecontext'] = $coursecontext->id;
3131     } else {
3132         $params['coursecontext'] = 0; // no course aliases
3133         $coursecontext = null;
3134     }
3135     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
3136               FROM {role} r
3137               $assignrestriction
3138               JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
3139          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3140           ORDER BY r.sortorder ASC";
3141     $roles = $DB->get_records_sql($sql, $params);
3143     $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
3145     if (!$withusercounts) {
3146         return $rolenames;
3147     }
3149     $rolecounts = array();
3150     $nameswithcounts = array();
3151     foreach ($roles as $role) {
3152         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
3153         $rolecounts[$role->id] = $roles[$role->id]->usercount;
3154     }
3155     return array($rolenames, $rolecounts, $nameswithcounts);
3158 /**
3159  * Gets a list of roles that this user can switch to in a context
3160  *
3161  * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
3162  * This function just process the contents of the role_allow_switch table. You also need to
3163  * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
3164  *
3165  * @param context $context a context.
3166  * @param int $rolenamedisplay the type of role name to display. One of the
3167  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3168  * @return array an array $roleid => $rolename.
3169  */
3170 function get_switchable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS) {
3171     global $USER, $DB;
3173     // You can't switch roles without this capability.
3174     if (!has_capability('moodle/role:switchroles', $context)) {
3175         return [];
3176     }
3178     $params = array();
3179     $extrajoins = '';
3180     $extrawhere = '';
3181     if (!is_siteadmin()) {
3182         // Admins are allowed to switch to any role with.
3183         // Others are subject to the additional constraint that the switch-to role must be allowed by
3184         // 'role_allow_switch' for some role they have assigned in this context or any parent.
3185         $parents = $context->get_parent_context_ids(true);
3186         $contexts = implode(',' , $parents);
3188         $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
3189         JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3190         $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
3191         $params['userid'] = $USER->id;
3192     }
3194     if ($coursecontext = $context->get_course_context(false)) {
3195         $params['coursecontext'] = $coursecontext->id;
3196     } else {
3197         $params['coursecontext'] = 0; // no course aliases
3198         $coursecontext = null;
3199     }
3201     $query = "
3202         SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3203           FROM (SELECT DISTINCT rc.roleid
3204                   FROM {role_capabilities} rc
3206                   $extrajoins
3207                   $extrawhere) idlist
3208           JOIN {role} r ON r.id = idlist.roleid
3209      LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3210       ORDER BY r.sortorder";
3211     $roles = $DB->get_records_sql($query, $params);
3213     return role_fix_names($roles, $context, $rolenamedisplay, true);
3216 /**
3217  * Gets a list of roles that this user can view in a context
3218  *
3219  * @param context $context a context.
3220  * @param int $userid id of user.
3221  * @param int $rolenamedisplay the type of role name to display. One of the
3222  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3223  * @return array an array $roleid => $rolename.
3224  */
3225 function get_viewable_roles(context $context, $userid = null, $rolenamedisplay = ROLENAME_ALIAS) {
3226     global $USER, $DB;
3228     if ($userid == null) {
3229         $userid = $USER->id;
3230     }
3232     $params = array();
3233     $extrajoins = '';
3234     $extrawhere = '';
3235     if (!is_siteadmin()) {
3236         // Admins are allowed to view any role.
3237         // Others are subject to the additional constraint that the view role must be allowed by
3238         // 'role_allow_view' for some role they have assigned in this context or any parent.
3239         $contexts = $context->get_parent_context_ids(true);
3240         list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
3242         $extrajoins = "JOIN {role_allow_view} ras ON ras.allowview = r.id
3243                        JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3244         $extrawhere = "WHERE ra.userid = :userid AND ra.contextid $insql";
3246         $params += $inparams;
3247         $params['userid'] = $userid;
3248     }
3250     if ($coursecontext = $context->get_course_context(false)) {
3251         $params['coursecontext'] = $coursecontext->id;
3252     } else {
3253         $params['coursecontext'] = 0; // No course aliases.
3254         $coursecontext = null;
3255     }
3257     $query = "
3258         SELECT r.id, r.name, r.shortname, rn.name AS coursealias, r.sortorder
3259           FROM {role} r
3260           $extrajoins
3261      LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3262           $extrawhere
3263       GROUP BY r.id, r.name, r.shortname, rn.name, r.sortorder
3264       ORDER BY r.sortorder";
3265     $roles = $DB->get_records_sql($query, $params);
3267     return role_fix_names($roles, $context, $rolenamedisplay, true);
3270 /**
3271  * Gets a list of roles that this user can override in this context.
3272  *
3273  * @param context $context the context.
3274  * @param int $rolenamedisplay the type of role name to display. One of the
3275  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3276  * @param bool $withcounts if true, count the number of overrides that are set for each role.
3277  * @return array if $withcounts is false, then an array $roleid => $rolename.
3278  *      if $withusercounts is true, returns a list of three arrays,
3279  *      $rolenames, $rolecounts, and $nameswithcounts.
3280  */
3281 function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
3282     global $USER, $DB;
3284     if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
3285         if ($withcounts) {
3286             return array(array(), array(), array());
3287         } else {
3288             return array();
3289         }
3290     }
3292     $parents = $context->get_parent_context_ids(true);
3293     $contexts = implode(',' , $parents);
3295     $params = array();
3296     $extrafields = '';
3298     $params['userid'] = $USER->id;
3299     if ($withcounts) {
3300         $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
3301                 WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
3302         $params['conid'] = $context->id;
3303     }
3305     if ($coursecontext = $context->get_course_context(false)) {
3306         $params['coursecontext'] = $coursecontext->id;
3307     } else {
3308         $params['coursecontext'] = 0; // no course aliases
3309         $coursecontext = null;
3310     }
3312     if (is_siteadmin()) {
3313         // show all roles to admins
3314         $roles = $DB->get_records_sql("
3315             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3316               FROM {role} ro
3317          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3318           ORDER BY ro.sortorder ASC", $params);
3320     } else {
3321         $roles = $DB->get_records_sql("
3322             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3323               FROM {role} ro
3324               JOIN (SELECT DISTINCT r.id
3325                       FROM {role} r
3326                       JOIN {role_allow_override} rao ON r.id = rao.allowoverride
3327                       JOIN {role_assignments} ra ON rao.roleid = ra.roleid
3328                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3329                    ) inline_view ON ro.id = inline_view.id
3330          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3331           ORDER BY ro.sortorder ASC", $params);
3332     }
3334     $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
3336     if (!$withcounts) {
3337         return $rolenames;
3338     }
3340     $rolecounts = array();
3341     $nameswithcounts = array();
3342     foreach ($roles as $role) {
3343         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
3344         $rolecounts[$role->id] = $roles[$role->id]->overridecount;
3345     }
3346     return array($rolenames, $rolecounts, $nameswithcounts);
3349 /**
3350  * Create a role menu suitable for default role selection in enrol plugins.
3351  *
3352  * @package    core_enrol
3353  *
3354  * @param context $context
3355  * @param int $addroleid current or default role - always added to list
3356  * @return array roleid=>localised role name
3357  */
3358 function get_default_enrol_roles(context $context, $addroleid = null) {
3359     global $DB;
3361     $params = array('contextlevel'=>CONTEXT_COURSE);
3363     if ($coursecontext = $context->get_course_context(false)) {
3364         $params['coursecontext'] = $coursecontext->id;
3365     } else {
3366         $params['coursecontext'] = 0; // no course names
3367         $coursecontext = null;
3368     }
3370     if ($addroleid) {
3371         $addrole = "OR r.id = :addroleid";
3372         $params['addroleid'] = $addroleid;
3373     } else {
3374         $addrole = "";
3375     }
3377     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3378               FROM {role} r
3379          LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
3380          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3381              WHERE rcl.id IS NOT NULL $addrole
3382           ORDER BY sortorder DESC";
3384     $roles = $DB->get_records_sql($sql, $params);
3386     return role_fix_names($roles, $context, ROLENAME_BOTH, true);
3389 /**
3390  * Return context levels where this role is assignable.
3391  *
3392  * @param integer $roleid the id of a role.
3393  * @return array list of the context levels at which this role may be assigned.
3394  */
3395 function get_role_contextlevels($roleid) {
3396     global $DB;
3397     return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
3398             'contextlevel', 'id,contextlevel');
3401 /**
3402  * Return roles suitable for assignment at the specified context level.
3403  *
3404  * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
3405  *
3406  * @param integer $contextlevel a contextlevel.
3407  * @return array list of role ids that are assignable at this context level.
3408  */
3409 function get_roles_for_contextlevels($contextlevel) {
3410     global $DB;
3411     return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
3412             '', 'id,roleid');
3415 /**
3416  * Returns default context levels where roles can be assigned.
3417  *
3418  * @param string $rolearchetype one of the role archetypes - that is, one of the keys
3419  *      from the array returned by get_role_archetypes();
3420  * @return array list of the context levels at which this type of role may be assigned by default.
3421  */
3422 function get_default_contextlevels($rolearchetype) {
3423     static $defaults = array(
3424         'manager'        => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
3425         'coursecreator'  => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
3426         'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3427         'teacher'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3428         'student'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3429         'guest'          => array(),
3430         'user'           => array(),
3431         'frontpage'      => array());
3433     if (isset($defaults[$rolearchetype])) {
3434         return $defaults[$rolearchetype];
3435     } else {
3436         return array();
3437     }
3440 /**
3441  * Set the context levels at which a particular role can be assigned.
3442  * Throws exceptions in case of error.
3443  *
3444  * @param integer $roleid the id of a role.
3445  * @param array $contextlevels the context levels at which this role should be assignable,
3446  *      duplicate levels are removed.
3447  * @return void
3448  */
3449 function set_role_contextlevels($roleid, array $contextlevels) {
3450     global $DB;
3451     $DB->delete_records('role_context_levels', array('roleid' => $roleid));
3452     $rcl = new stdClass();
3453     $rcl->roleid = $roleid;
3454     $contextlevels = array_unique($contextlevels);
3455     foreach ($contextlevels as $level) {
3456         $rcl->contextlevel = $level;
3457         $DB->insert_record('role_context_levels', $rcl, false, true);
3458     }
3461 /**
3462  * Gets sql joins for finding users with capability in the given context.
3463  *
3464  * @param context $context Context for the join.
3465  * @param string|array $capability Capability name or array of names.
3466  *      If an array is provided then this is the equivalent of a logical 'OR',
3467  *      i.e. the user needs to have one of these capabilities.
3468  * @param string $useridcolumn e.g. 'u.id'.
3469  * @return \core\dml\sql_join Contains joins, wheres, params.
3470  *      This function will set ->cannotmatchanyrows if applicable.
3471  *      This may let you skip doing a DB query.
3472  */
3473 function get_with_capability_join(context $context, $capability, $useridcolumn) {
3474     global $CFG, $DB;
3476     // Add a unique prefix to param names to ensure they are unique.
3477     static $i = 0;
3478     $i++;
3479     $paramprefix = 'eu' . $i . '_';
3481     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3482     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3484     $ctxids = trim($context->path, '/');
3485     $ctxids = str_replace('/', ',', $ctxids);
3487     // Context is the frontpage
3488     $isfrontpage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID;
3489     $isfrontpage = $isfrontpage || is_inside_frontpage($context);
3491     $caps = (array) $capability;
3493     // Construct list of context paths bottom --> top.
3494     list($contextids, $paths) = get_context_info_list($context);
3496     // We need to find out all roles that have these capabilities either in definition or in overrides.
3497     $defs = [];
3498     list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, $paramprefix . 'con');
3499     list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, $paramprefix . 'cap');
3501     // Check whether context locking is enabled.
3502     // Filter out any write capability if this is the case.
3503     $excludelockedcaps = '';
3504     $excludelockedcapsparams = [];
3505     if (!empty($CFG->contextlocking) && $context->locked) {
3506         $excludelockedcaps = 'AND (cap.captype = :capread OR cap.name = :managelockscap)';
3507         $excludelockedcapsparams['capread'] = 'read';
3508         $excludelockedcapsparams['managelockscap'] = 'moodle/site:managecontextlocks';
3509     }
3511     $params = array_merge($params, $params2, $excludelockedcapsparams);
3512     $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3513               FROM {role_capabilities} rc
3514               JOIN {capabilities} cap ON rc.capability = cap.name
3515               JOIN {context} ctx on rc.contextid = ctx.id
3516              WHERE rc.contextid $incontexts AND rc.capability $incaps $excludelockedcaps";
3518     $rcs = $DB->get_records_sql($sql, $params);
3519     foreach ($rcs as $rc) {
3520         $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3521     }
3523     // Go through the permissions bottom-->top direction to evaluate the current permission,
3524     // first one wins (prohibit is an exception that always wins).
3525     $access = [];
3526     foreach ($caps as $cap) {
3527         foreach ($paths as $path) {
3528             if (empty($defs[$cap][$path])) {
3529                 continue;
3530             }
3531             foreach ($defs[$cap][$path] as $roleid => $perm) {
3532                 if ($perm == CAP_PROHIBIT) {
3533                     $access[$cap][$roleid] = CAP_PROHIBIT;
3534                     continue;
3535                 }
3536                 if (!isset($access[$cap][$roleid])) {
3537                     $access[$cap][$roleid] = (int)$perm;
3538                 }
3539             }
3540         }
3541     }
3543     // Make lists of roles that are needed and prohibited in this context.
3544     $needed = []; // One of these is enough.
3545     $prohibited = []; // Must not have any of these.
3546     foreach ($caps as $cap) {
3547         if (empty($access[$cap])) {
3548             continue;
3549         }
3550         foreach ($access[$cap] as $roleid => $perm) {
3551             if ($perm == CAP_PROHIBIT) {
3552                 unset($needed[$cap][$roleid]);
3553                 $prohibited[$cap][$roleid] = true;
3554             } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
3555                 $needed[$cap][$roleid] = true;
3556             }
3557         }
3558         if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
3559             // Easy, nobody has the permission.
3560             unset($needed[$cap]);
3561             unset($prohibited[$cap]);
3562         } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
3563             // Everybody is disqualified on the frontpage.
3564             unset($needed[$cap]);
3565             unset($prohibited[$cap]);
3566         }
3567         if (empty($prohibited[$cap])) {
3568             unset($prohibited[$cap]);
3569         }
3570     }
3572     if (empty($needed)) {
3573         // There can not be anybody if no roles match this request.
3574         return new \core\dml\sql_join('', '1 = 2', [], true);
3575     }
3577     if (empty($prohibited)) {
3578         // We can compact the needed roles.
3579         $n = [];
3580         foreach ($needed as $cap) {
3581             foreach ($cap as $roleid => $unused) {
3582                 $n[$roleid] = true;
3583             }
3584         }
3585         $needed = ['any' => $n];
3586         unset($n);
3587     }
3589     // Prepare query clauses.
3590     $wherecond = [];
3591     $params    = [];
3592     $joins     = [];
3593     $cannotmatchanyrows = false;
3595     // We never return deleted users or guest account.
3596     // Use a hack to get the deleted user column without an API change.
3597     $deletedusercolumn = substr($useridcolumn, 0, -2) . 'deleted';
3598     $wherecond[] = "$deletedusercolumn = 0 AND $useridcolumn <> :{$paramprefix}guestid";
3599     $params[$paramprefix . 'guestid'] = $CFG->siteguest;
3601     // Now add the needed and prohibited roles conditions as joins.
3602     if (!empty($needed['any'])) {
3603         // Simple case - there are no prohibits involved.
3604         if (!empty($needed['any'][$defaultuserroleid]) ||
3605                 ($isfrontpage && !empty($needed['any'][$defaultfrontpageroleid]))) {
3606             // Everybody.
3607         } else {
3608             $joins[] = "JOIN (SELECT DISTINCT userid
3609                                 FROM {role_assignments}
3610                                WHERE contextid IN ($ctxids)
3611                                      AND roleid IN (" . implode(',', array_keys($needed['any'])) . ")
3612                              ) ra ON ra.userid = $useridcolumn";
3613         }
3614     } else {
3615         $unions = [];
3616         $everybody = false;
3617         foreach ($needed as $cap => $unused) {
3618             if (empty($prohibited[$cap])) {
3619                 if (!empty($needed[$cap][$defaultuserroleid]) ||
3620                         ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3621                     $everybody = true;
3622                     break;
3623                 } else {
3624                     $unions[] = "SELECT userid
3625                                    FROM {role_assignments}
3626                                   WHERE contextid IN ($ctxids)
3627                                         AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
3628                 }
3629             } else {
3630                 if (!empty($prohibited[$cap][$defaultuserroleid]) ||
3631                         ($isfrontpage && !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
3632                     // Nobody can have this cap because it is prohibited in default roles.
3633                     continue;
3635                 } else if (!empty($needed[$cap][$defaultuserroleid]) ||
3636                         ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3637                     // Everybody except the prohibited - hiding does not matter.
3638                     $unions[] = "SELECT id AS userid
3639                                    FROM {user}
3640                                   WHERE id NOT IN (SELECT userid
3641                                                      FROM {role_assignments}
3642                                                     WHERE contextid IN ($ctxids)
3643                                                           AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3645                 } else {
3646                     $unions[] = "SELECT userid
3647                                    FROM {role_assignments}
3648                                   WHERE contextid IN ($ctxids) AND roleid IN (" . implode(',', array_keys($needed[$cap])) . ")
3649                                         AND userid NOT IN (
3650                                             SELECT userid
3651                                               FROM {role_assignments}
3652                                              WHERE contextid IN ($ctxids)
3653                                                    AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3654                 }
3655             }
3656         }
3658         if (!$everybody) {
3659             if ($unions) {
3660                 $joins[] = "JOIN (
3661                                   SELECT DISTINCT userid
3662                                     FROM (
3663                                             " . implode("\n UNION \n", $unions) . "
3664                                          ) us
3665                                  ) ra ON ra.userid = $useridcolumn";
3666             } else {
3667                 // Only prohibits found - nobody can be matched.
3668                 $wherecond[] = "1 = 2";
3669                 $cannotmatchanyrows = true;
3670             }
3671         }
3672     }
3674     return new \core\dml\sql_join(implode("\n", $joins), implode(" AND ", $wherecond), $params, $cannotmatchanyrows);
3677 /**
3678  * Who has this capability in this context?
3679  *
3680  * This can be a very expensive call - use sparingly and keep
3681  * the results if you are going to need them again soon.
3682  *
3683  * Note if $fields is empty this function attempts to get u.*
3684  * which can get rather large - and has a serious perf impact
3685  * on some DBs.
3686  *
3687  * @param context $context
3688  * @param string|array $capability - capability name(s)
3689  * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
3690  * @param string $sort - the sort order. Default is lastaccess time.
3691  * @param mixed $limitfrom - number of records to skip (offset)
3692  * @param mixed $limitnum - number of records to fetch
3693  * @param string|array $groups - single group or array of groups - only return
3694  *               users who are in one of these group(s).
3695  * @param string|array $exceptions - list of users to exclude, comma separated or array
3696  * @param bool $notuseddoanything not used any more, admin accounts are never returned
3697  * @param bool $notusedview - use get_enrolled_sql() instead
3698  * @param bool $useviewallgroups if $groups is set the return users who
3699  *               have capability both $capability and moodle/site:accessallgroups
3700  *               in this context, as well as users who have $capability and who are
3701  *               in $groups.
3702  * @return array of user records
3703  */
3704 function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
3705         $groups = '', $exceptions = '', $notuseddoanything = null, $notusedview = null, $useviewallgroups = false) {
3706     global $CFG, $DB;
3708     // Context is a course page other than the frontpage.
3709     $iscoursepage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid != SITEID;
3711     // Set up default fields list if necessary.
3712     if (empty($fields)) {
3713         if ($iscoursepage) {
3714             $fields = 'u.*, ul.timeaccess AS lastaccess';
3715         } else {
3716             $fields = 'u.*';
3717         }
3718     } else {
3719         if ($CFG->debugdeveloper && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
3720             debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
3721         }
3722     }
3724     // Set up default sort if necessary.
3725     if (empty($sort)) { // default to course lastaccess or just lastaccess
3726         if ($iscoursepage) {
3727             $sort = 'ul.timeaccess';
3728         } else {
3729             $sort = 'u.lastaccess';
3730         }
3731     }
3733     // Get the bits of SQL relating to capabilities.
3734     $sqljoin = get_with_capability_join($context, $capability, 'u.id');
3735     if ($sqljoin->cannotmatchanyrows) {
3736         return [];
3737     }
3739     // Prepare query clauses.
3740     $wherecond = [$sqljoin->wheres];
3741     $params    = $sqljoin->params;
3742     $joins     = [$sqljoin->joins];
3744     // Add user lastaccess JOIN, if required.
3745     if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
3746          // Here user_lastaccess is not required MDL-13810.
3747     } else {
3748         if ($iscoursepage) {
3749             $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
3750         } else {
3751             throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
3752         }
3753     }
3755     // Groups.
3756     if ($groups) {
3757         $groups = (array)$groups;
3758         list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
3759         $joins[] = "LEFT OUTER JOIN (SELECT DISTINCT userid
3760                                        FROM {groups_members}
3761                                       WHERE groupid $grouptest
3762                                     ) gm ON gm.userid = u.id";
3764         $params = array_merge($params, $grpparams);
3766         $grouptest = 'gm.userid IS NOT NULL';
3767         if ($useviewallgroups) {
3768             $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
3769             if (!empty($viewallgroupsusers)) {
3770                 $grouptest .= ' OR u.id IN (' . implode(',', array_keys($viewallgroupsusers)) . ')';
3771             }
3772         }
3773         $wherecond[] = "($grouptest)";
3774     }
3776     // User exceptions.
3777     if (!empty($exceptions)) {
3778         $exceptions = (array)$exceptions;
3779         list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc', false);
3780         $params = array_merge($params, $exparams);
3781         $wherecond[] = "u.id $exsql";
3782     }
3784     // Collect WHERE conditions and needed joins.
3785     $where = implode(' AND ', $wherecond);
3786     if ($where !== '') {
3787         $where = 'WHERE ' . $where;
3788     }
3789     $joins = implode("\n", $joins);
3791     // Finally! we have all the bits, run the query.
3792     $sql = "SELECT $fields
3793               FROM {user} u
3794             $joins
3795             $where
3796           ORDER BY $sort";
3798     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3801 /**
3802  * Re-sort a users array based on a sorting policy
3803  *
3804  * Will re-sort a $users results array (from get_users_by_capability(), usually)
3805  * based on a sorting policy. This is to support the odd practice of
3806  * sorting teachers by 'authority', where authority was "lowest id of the role
3807  * assignment".
3808  *
3809  * Will execute 1 database query. Only suitable for small numbers of users, as it
3810  * uses an u.id IN() clause.
3811  *
3812  * Notes about the sorting criteria.
3813  *
3814  * As a default, we cannot rely on role.sortorder because then
3815  * admins/coursecreators will always win. That is why the sane
3816  * rule "is locality matters most", with sortorder as 2nd
3817  * consideration.
3818  *
3819  * If you want role.sortorder, use the 'sortorder' policy, and
3820  * name explicitly what roles you want to cover. It's probably
3821  * a good idea to see what roles have the capabilities you want
3822  * (array_diff() them against roiles that have 'can-do-anything'
3823  * to weed out admin-ish roles. Or fetch a list of roles from
3824  * variables like $CFG->coursecontact .
3825  *
3826  * @param array $users Users array, keyed on userid
3827  * @param context $context
3828  * @param array $roles ids of the roles to include, optional
3829  * @param string $sortpolicy defaults to locality, more about
3830  * @return array sorted copy of the array
3831  */
3832 function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') {
3833     global $DB;
3835     $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
3836     $contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
3837     if (empty($roles)) {
3838         $roleswhere = '';
3839     } else {
3840         $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
3841     }
3843     $sql = "SELECT ra.userid
3844               FROM {role_assignments} ra
3845               JOIN {role} r
3846                    ON ra.roleid=r.id
3847               JOIN {context} ctx
3848                    ON ra.contextid=ctx.id
3849              WHERE $userswhere
3850                    $contextwhere
3851                    $roleswhere";
3853     // Default 'locality' policy -- read PHPDoc notes
3854     // about sort policies...
3855     $orderby = 'ORDER BY '
3856                     .'ctx.depth DESC, '  /* locality wins */
3857                     .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3858                     .'ra.id';            /* role assignment order tie-breaker */
3859     if ($sortpolicy === 'sortorder') {
3860         $orderby = 'ORDER BY '
3861                         .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3862                         .'ra.id';            /* role assignment order tie-breaker */
3863     }
3865     $sortedids = $DB->get_fieldset_sql($sql . $orderby);
3866     $sortedusers = array();
3867     $seen = array();
3869     foreach ($sortedids as $id) {
3870         // Avoid duplicates
3871         if (isset($seen[$id])) {
3872             continue;
3873         }
3874         $seen[$id] = true;
3876         // assign
3877         $sortedusers[$id] = $users[$id];
3878     }
3879     return $sortedusers;
3882 /**
3883  * Gets all the users assigned this role in this context or higher
3884  *
3885  * Note that moodle is based on capabilities and it is usually better
3886  * to check permissions than to check role ids as the capabilities
3887  * system is more flexible. If you really need, you can to use this
3888  * function but consider has_capability() as a possible substitute.
3889  *
3890  * All $sort fields are added into $fields if not present there yet.
3891  *
3892  * If $roleid is an array or is empty (all roles) you need to set $fields
3893  * (and $sort by extension) params according to it, as the first field
3894  * returned by the database should be unique (ra.id is the best candidate).
3895  *
3896  * @param int $roleid (can also be an array of ints!)
3897  * @param context $context
3898  * @param bool $parent if true, get list of users assigned in higher context too
3899  * @param string $fields fields from user (u.) , role assignment (ra) or role (r.)
3900  * @param string $sort sort from user (u.) , role assignment (ra.) or role (r.).
3901  *      null => use default sort from users_order_by_sql.
3902  * @param bool $all true means all, false means limit to enrolled users
3903  * @param string $group defaults to ''
3904  * @param mixed $limitfrom defaults to ''
3905  * @param mixed $limitnum defaults to ''
3906  * @param string $extrawheretest defaults to ''
3907  * @param array $whereorsortparams any paramter values used by $sort or $extrawheretest.
3908  * @return array
3909  */
3910 function get_role_users($roleid, context $context, $parent = false, $fields = '',
3911         $sort = null, $all = true, $group = '',
3912         $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereorsortparams = array()) {
3913     global $DB;
3915     if (empty($fields)) {
3916         $allnames = get_all_user_name_fields(true, 'u');
3917         $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
3918                   'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
3919                   'u.country, u.picture, u.idnumber, u.department, u.institution, '.
3920                   'u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name AS rolename, r.sortorder, '.
3921                   'r.shortname AS roleshortname, rn.name AS rolecoursealias';
3922     }
3924     // Prevent wrong function uses.
3925     if ((empty($roleid) || is_array($roleid)) && strpos($fields, 'ra.id') !== 0) {
3926         debugging('get_role_users() without specifying one single roleid needs to be called prefixing ' .
3927             'role assignments id (ra.id) as unique field, you can use $fields param for it.');
3929         if (!empty($roleid)) {
3930             // Solving partially the issue when specifying multiple roles.
3931             $users = array();
3932             foreach ($roleid as $id) {
3933                 // Ignoring duplicated keys keeping the first user appearance.
3934                 $users = $users + get_role_users($id, $context, $parent, $fields, $sort, $all, $group,
3935                     $limitfrom, $limitnum, $extrawheretest, $whereorsortparams);
3936             }
3937             return $users;
3938         }
3939     }
3941     $parentcontexts = '';
3942     if ($parent) {
3943         $parentcontexts = substr($context->path, 1); // kill leading slash
3944         $parentcontexts = str_replace('/', ',', $parentcontexts);
3945         if ($parentcontexts !== '') {
3946             $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
3947         }
3948     }
3950     if ($roleid) {
3951         list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'r');
3952         $roleselect = "AND ra.roleid $rids";
3953     } else {
3954         $params = array();
3955         $roleselect = '';
3956     }
3958     if ($coursecontext = $context->get_course_context(false)) {
3959         $params['coursecontext'] = $coursecontext->id;
3960     } else {
3961         $params['coursecontext'] = 0;
3962     }
3964     if ($group) {
3965         $groupjoin   = "JOIN {groups_members} gm ON gm.userid = u.id";
3966         $groupselect = " AND gm.groupid = :groupid ";
3967         $params['groupid'] = $group;
3968     } else {
3969         $groupjoin   = '';
3970         $groupselect = '';
3971     }
3973     $params['contextid'] = $context->id;
3975     if ($extrawheretest) {
3976         $extrawheretest = ' AND ' . $extrawheretest;
3977     }
3979     if ($whereorsortparams) {
3980         $params = array_merge($params, $whereorsortparams);
3981     }
3983     if (!$sort) {
3984         list($sort, $sortparams) = users_order_by_sql('u');
3985         $params = array_merge($params, $sortparams);
3986     }
3988     // Adding the fields from $sort that are not present in $fields.
3989     $sortarray = preg_split('/,\s*/', $sort);
3990     $fieldsarray = preg_split('/,\s*/', $fields);
3992     // Discarding aliases from the fields.
3993     $fieldnames = array();
3994     foreach ($fieldsarray as $key => $field) {
3995         list($fieldnames[$key]) = explode(' ', $field);
3996     }
3998     $addedfields = array();
3999     foreach ($sortarray as $sortfield) {
4000         // Throw away any additional arguments to the sort (e.g. ASC/DESC).
4001         list($sortfield) = explode(' ', $sortfield);
4002         list($tableprefix) = explode('.', $sortfield);
4003         $fieldpresent = false;
4004         foreach ($fieldnames as $fieldname) {
4005             if ($fieldname === $sortfield || $fieldname === $tableprefix.'.*') {
4006                 $fieldpresent = true;
4007                 break;
4008             }
4009         }
4011         if (!$fieldpresent) {
4012             $fieldsarray[] = $sortfield;
4013             $addedfields[] = $sortfield;
4014         }
4015     }
4017     $fields = implode(', ', $fieldsarray);
4018     if (!empty($addedfields)) {
4019         $addedfields = implode(', ', $addedfields);
4020         debugging('get_role_users() adding '.$addedfields.' to the query result because they were required by $sort but missing in $fields');
4021     }
4023     if ($all === null) {
4024         // Previously null was used to indicate that parameter was not used.
4025         $all = true;
4026     }
4027     if (!$all and $coursecontext) {
4028         // Do not use get_enrolled_sql() here for performance reasons.
4029         $ejoin = "JOIN {user_enrolments} ue ON ue.userid = u.id
4030                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :ecourseid)";
4031         $params['ecourseid'] = $coursecontext->instanceid;
4032     } else {
4033         $ejoin = "";
4034     }
4036     $sql = "SELECT DISTINCT $fields, ra.roleid
4037               FROM {role_assignments} ra
4038               JOIN {user} u ON u.id = ra.userid
4039               JOIN {role} r ON ra.roleid = r.id
4040             $ejoin
4041          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
4042         $groupjoin
4043              WHERE (ra.contextid = :contextid $parentcontexts)
4044                    $roleselect
4045                    $groupselect
4046                    $extrawheretest
4047           ORDER BY $sort";                  // join now so that we can just use fullname() later
4049     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
4052 /**
4053  * Counts all the users assigned this role in this context or higher
4054  *
4055  * @param int|array $roleid either int or an array of ints
4056  * @param context $context
4057  * @param bool $parent if true, get list of users assigned in higher context too
4058  * @return int Returns the result count
4059  */
4060 function count_role_users($roleid, context $context, $parent = false) {
4061     global $DB;
4063     if ($parent) {
4064         if ($contexts = $context->get_parent_context_ids()) {
4065             $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
4066         } else {
4067             $parentcontexts = '';
4068         }
4069     } else {
4070         $parentcontexts = '';
4071     }
4073     if ($roleid) {
4074         list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
4075         $roleselect = "AND r.roleid $rids";
4076     } else {
4077         $params = array();
4078         $roleselect = '';
4079     }
4081     array_unshift($params, $context->id);
4083     $sql = "SELECT COUNT(DISTINCT u.id)
4084               FROM {role_assignments} r
4085               JOIN {user} u ON u.id = r.userid
4086              WHERE (r.contextid = ? $parentcontexts)
4087                    $roleselect
4088                    AND u.deleted = 0";
4090     return $DB->count_records_sql($sql, $params);
4093 /**
4094  * This function gets the list of courses that this user has a particular capability in.
4095  *
4096  * It is now reasonably efficient, but bear in mind that if there are users who have the capability
4097  * everywhere, it may return an array of all courses.
4098  *
4099  * @param string $capability Capability in question
4100  * @param int $userid User ID or null for current user
4101  * @param bool $doanything True if 'doanything' is permitted (default)
4102  * @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
4103  *   otherwise use a comma-separated list of the fields you require, not including id.
4104  *   Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
4105  * @param string $orderby If set, use a comma-separated list of fields from course
4106  *   table with sql modifiers (DESC) if needed
4107  * @param int $limit Limit the number of courses to return on success. Zero equals all entries.
4108  * @return array|bool Array of courses, if none found false is returned.
4109  */
4110 function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', $orderby = '',
4111         $limit = 0) {
4112     global $DB, $USER;
4114     // Default to current user.
4115     if (!$userid) {
4116         $userid = $USER->id;
4117     }
4119     if ($doanything && is_siteadmin($userid)) {
4120         // If the user is a site admin and $doanything is enabled then there is no need to restrict
4121         // the list of courses.
4122         $contextlimitsql = '';
4123         $contextlimitparams = [];
4124     } else {
4125         // Gets SQL to limit contexts ('x' table) to those where the user has this capability.
4126         list ($contextlimitsql, $contextlimitparams) = \core\access\get_user_capability_course_helper::get_sql(
4127                 $userid, $capability);
4128         if (!$contextlimitsql) {
4129             // If the does not have this capability in any context, return false without querying.
4130             return false;
4131         }
4133         $contextlimitsql = 'WHERE' . $contextlimitsql;
4134     }
4136     // Convert fields list and ordering
4137     $fieldlist = '';
4138     if ($fieldsexceptid) {
4139         $fields = array_map('trim', explode(',', $fieldsexceptid));
4140         foreach ($fields as $field) {
4141             // Context fields have a different alias.
4142             if (strpos($field, 'ctx') === 0) {
4143                 switch($field) {
4144                     case 'ctxlevel' :
4145                         $realfield = 'contextlevel';
4146                         break;
4147                     case 'ctxinstance' :
4148                         $realfield = 'instanceid';
4149                         break;
4150                     default:
4151                         $realfield = substr($field, 3);
4152                         break;
4153                 }
4154                 $fieldlist .= ',x.' . $realfield . ' AS ' . $field;
4155             } else {
4156                 $fieldlist .= ',c.'.$field;
4157             }
4158         }
4159     }
4160     if ($orderby) {
4161         $fields = explode(',', $orderby);
4162         $orderby = '';
4163         foreach ($fields as $field) {
4164             if ($orderby) {
4165                 $orderby .= ',';
4166             }
4167             $orderby .= 'c.'.$field;
4168         }
4169         $orderby = 'ORDER BY '.$orderby;
4170     }
4172     $courses = array();
4173     $rs = $DB->get_recordset_sql("
4174             SELECT c.id $fieldlist
4175               FROM {course} c
4176               JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ?
4177             $contextlimitsql
4178             $orderby", array_merge([CONTEXT_COURSE], $contextlimitparams));
4179     foreach ($rs as $course) {
4180         $courses[] = $course;
4181         $limit--;
4182         if ($limit == 0) {
4183             break;
4184         }
4185     }
4186     $rs->close();
4187     return empty($courses) ? false : $courses;
4190 /**
4191  * Switches the current user to another role for the current session and only
4192  * in the given context.
4193  *
4194  * The caller *must* check
4195  * - that this op is allowed
4196  * - that the requested role can be switched to in this context (use get_switchable_roles)
4197  * - that the requested role is NOT $CFG->defaultuserroleid
4198  *
4199  * To "unswitch" pass 0 as the roleid.
4200  *
4201  * This function *will* modify $USER->access - beware
4202  *
4203  * @param integer $roleid the role to switch to.
4204  * @param context $context the context in which to perform the switch.
4205  * @return bool success or failure.
4206  */
4207 function role_switch($roleid, context $context) {
4208     global $USER;
4210     // Add the ghost RA to $USER->access as $USER->access['rsw'][$path] = $roleid.
4211     // To un-switch just unset($USER->access['rsw'][$path]).
4212     //
4213     // Note: it is not possible to switch to roles that do not have course:view
4215     if (!isset($USER->access)) {
4216         load_all_capabilities();
4217     }
4219     // Add the switch RA
4220     if ($roleid == 0) {
4221         unset($USER->access['rsw'][$context->path]);
4222         return true;
4223     }
4225     $USER->access['rsw'][$context->path] = $roleid;
4227     return true;
4230 /**
4231  * Checks if the user has switched roles within the given course.
4232  *
4233  * Note: You can only switch roles within the course, hence it takes a course id
4234  * rather than a context. On that note Petr volunteered to implement this across
4235  * all other contexts, all requests for this should be forwarded to him ;)
4236  *
4237  * @param int $courseid The id of the course to check
4238  * @return bool True if the user has switched roles within the course.
4239  */
4240 function is_role_switched($courseid) {
4241     global $USER;
4242     $context = context_course::instance($courseid, MUST_EXIST);
4243     return (!empty($USER->access['rsw'][$context->path]));
4246 /**
4247  * Get any role that has an override on exact context
4248  *
4249  * @param context $context The context to check
4250  * @return array An array of roles
4251  */
4252 function get_roles_with_override_on_context(context $context) {
4253     global $DB;
4255     return $DB->get_records_sql("SELECT r.*
4256                                    FROM {role_capabilities} rc, {role} r
4257                                   WHERE rc.roleid = r.id AND rc.contextid = ?",
4258                                 array($context->id));
4261 /**
4262  * Get all capabilities for this role on this context (overrides)
4263  *
4264  * @param stdClass $role
4265  * @param context $context
4266  * @return array
4267  */
4268 function get_capabilities_from_role_on_context($role, context $context) {
4269     global $DB;
4271     return $DB->get_records_sql("SELECT *
4272                                    FROM {role_capabilities}
4273                                   WHERE contextid = ? AND roleid = ?",
4274                                 array($context->id, $role->id));
4277 /**
4278  * Find all user assignment of users for this role, on this context
4279  *
4280  * @param stdClass $role
4281  * @param context $context
4282  * @return array
4283  */
4284 function get_users_from_role_on_context($role, context $context) {
4285     global $DB;
4287     return $DB->get_records_sql("SELECT *
4288                                    FROM {role_assignments}
4289                                   WHERE contextid = ? AND roleid = ?",
4290                                 array($context->id, $role->id));
4293 /**
4294  * Simple function returning a boolean true if user has roles
4295  * in context or parent contexts, otherwise false.
4296  *
4297  * @param int $userid
4298  * @param int $roleid
4299  * @param int $contextid empty means any context
4300  * @return bool
4301  */
4302 function user_has_role_assignment($userid, $roleid, $contextid = 0) {
4303     global $DB;
4305     if ($contextid) {
4306         if (!$context = context::instance_by_id($contextid, IGNORE_MISSING)) {
4307             return false;
4308         }
4309         $parents = $context->get_parent_context_ids(true);
4310         list($contexts, $params) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED, 'r');
4311         $params['userid'] = $userid;
4312         $params['roleid'] = $roleid;
4314         $sql = "SELECT COUNT(ra.id)
4315                   FROM {role_assignments} ra
4316                  WHERE ra.userid = :userid AND ra.roleid = :roleid AND ra.contextid $contexts";
4318         $count = $DB->get_field_sql($sql, $params);
4319         return ($count > 0);
4321     } else {
4322         return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid));
4323     }
4326 /**
4327  * Get localised role name or alias if exists and format the text.
4328  *
4329  * @param stdClass $role role object
<