MDL-53566 core: Add support for context locking
[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              WHERE rc.roleid $sql";
353     $rs = $DB->get_recordset_sql($sql, $params);
355     // Store the capabilities into the expected data structure.
356     foreach ($rs as $rd) {
357         if (!isset($rdefs[$rd->roleid][$rd->path])) {
358             $rdefs[$rd->roleid][$rd->path] = array();
359         }
360         $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
361     }
363     $rs->close();
365     // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
366     // we process role definitinons in a way that requires we see parent contexts
367     // before child contexts. This sort ensures that works (and is faster than
368     // sorting in the SQL query).
369     foreach ($rdefs as $roleid => $rdef) {
370         ksort($rdefs[$roleid]);
371     }
373     return $rdefs;
376 /**
377  * Get the default guest role, this is used for guest account,
378  * search engine spiders, etc.
379  *
380  * @return stdClass role record
381  */
382 function get_guest_role() {
383     global $CFG, $DB;
385     if (empty($CFG->guestroleid)) {
386         if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
387             $guestrole = array_shift($roles);   // Pick the first one
388             set_config('guestroleid', $guestrole->id);
389             return $guestrole;
390         } else {
391             debugging('Can not find any guest role!');
392             return false;
393         }
394     } else {
395         if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
396             return $guestrole;
397         } else {
398             // somebody is messing with guest roles, remove incorrect setting and try to find a new one
399             set_config('guestroleid', '');
400             return get_guest_role();
401         }
402     }
405 /**
406  * Check whether a user has a particular capability in a given context.
407  *
408  * For example:
409  *      $context = context_module::instance($cm->id);
410  *      has_capability('mod/forum:replypost', $context)
411  *
412  * By default checks the capabilities of the current user, but you can pass a
413  * different userid. By default will return true for admin users, but you can override that with the fourth argument.
414  *
415  * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
416  * or capabilities with XSS, config or data loss risks.
417  *
418  * @category access
419  *
420  * @param string $capability the name of the capability to check. For example mod/forum:view
421  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
422  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
423  * @param boolean $doanything If false, ignores effect of admin role assignment
424  * @return boolean true if the user has this capability. Otherwise false.
425  */
426 function has_capability($capability, context $context, $user = null, $doanything = true) {
427     global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
429     if (during_initial_install()) {
430         if ($SCRIPT === "/$CFG->admin/index.php"
431                 or $SCRIPT === "/$CFG->admin/cli/install.php"
432                 or $SCRIPT === "/$CFG->admin/cli/install_database.php"
433                 or (defined('BEHAT_UTIL') and BEHAT_UTIL)
434                 or (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL)) {
435             // we are in an installer - roles can not work yet
436             return true;
437         } else {
438             return false;
439         }
440     }
442     if (strpos($capability, 'moodle/legacy:') === 0) {
443         throw new coding_exception('Legacy capabilities can not be used any more!');
444     }
446     if (!is_bool($doanything)) {
447         throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
448     }
450     // capability must exist
451     if (!$capinfo = get_capability_info($capability)) {
452         debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
453         return false;
454     }
456     if (!isset($USER->id)) {
457         // should never happen
458         $USER->id = 0;
459         debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
460     }
462     // make sure there is a real user specified
463     if ($user === null) {
464         $userid = $USER->id;
465     } else {
466         $userid = is_object($user) ? $user->id : $user;
467     }
469     // make sure forcelogin cuts off not-logged-in users if enabled
470     if (!empty($CFG->forcelogin) and $userid == 0) {
471         return false;
472     }
474     // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
475     if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
476         if (isguestuser($userid) or $userid == 0) {
477             return false;
478         }
479     }
481     // Check whether context locking is enabled.
482     if (!empty($CFG->contextlocking)) {
483         if ($capinfo->captype === 'write' && $context->locked && $capinfo->name !== 'moodle/site:managecontextlocks') {
484             return false;
485         }
486     }
488     // somehow make sure the user is not deleted and actually exists
489     if ($userid != 0) {
490         if ($userid == $USER->id and isset($USER->deleted)) {
491             // this prevents one query per page, it is a bit of cheating,
492             // but hopefully session is terminated properly once user is deleted
493             if ($USER->deleted) {
494                 return false;
495             }
496         } else {
497             if (!context_user::instance($userid, IGNORE_MISSING)) {
498                 // no user context == invalid userid
499                 return false;
500             }
501         }
502     }
504     // context path/depth must be valid
505     if (empty($context->path) or $context->depth == 0) {
506         // this should not happen often, each upgrade tries to rebuild the context paths
507         debugging('Context id '.$context->id.' does not have valid path, please use context_helper::build_all_paths()');
508         if (is_siteadmin($userid)) {
509             return true;
510         } else {
511             return false;
512         }
513     }
515     // Find out if user is admin - it is not possible to override the doanything in any way
516     // and it is not possible to switch to admin role either.
517     if ($doanything) {
518         if (is_siteadmin($userid)) {
519             if ($userid != $USER->id) {
520                 return true;
521             }
522             // make sure switchrole is not used in this context
523             if (empty($USER->access['rsw'])) {
524                 return true;
525             }
526             $parts = explode('/', trim($context->path, '/'));
527             $path = '';
528             $switched = false;
529             foreach ($parts as $part) {
530                 $path .= '/' . $part;
531                 if (!empty($USER->access['rsw'][$path])) {
532                     $switched = true;
533                     break;
534                 }
535             }
536             if (!$switched) {
537                 return true;
538             }
539             //ok, admin switched role in this context, let's use normal access control rules
540         }
541     }
543     // Careful check for staleness...
544     $context->reload_if_dirty();
546     if ($USER->id == $userid) {
547         if (!isset($USER->access)) {
548             load_all_capabilities();
549         }
550         $access =& $USER->access;
552     } else {
553         // make sure user accessdata is really loaded
554         get_user_accessdata($userid, true);
555         $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
556     }
558     return has_capability_in_accessdata($capability, $context, $access);
561 /**
562  * Check if the user has any one of several capabilities from a list.
563  *
564  * This is just a utility method that calls has_capability in a loop. Try to put
565  * the capabilities that most users are likely to have first in the list for best
566  * performance.
567  *
568  * @category access
569  * @see has_capability()
570  *
571  * @param array $capabilities an array of capability names.
572  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
573  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
574  * @param boolean $doanything If false, ignore effect of admin role assignment
575  * @return boolean true if the user has any of these capabilities. Otherwise false.
576  */
577 function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
578     foreach ($capabilities as $capability) {
579         if (has_capability($capability, $context, $user, $doanything)) {
580             return true;
581         }
582     }
583     return false;
586 /**
587  * Check if the user has all the capabilities in a list.
588  *
589  * This is just a utility method that calls has_capability in a loop. Try to put
590  * the capabilities that fewest users are likely to have first in the list for best
591  * performance.
592  *
593  * @category access
594  * @see has_capability()
595  *
596  * @param array $capabilities an array of capability names.
597  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
598  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
599  * @param boolean $doanything If false, ignore effect of admin role assignment
600  * @return boolean true if the user has all of these capabilities. Otherwise false.
601  */
602 function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
603     foreach ($capabilities as $capability) {
604         if (!has_capability($capability, $context, $user, $doanything)) {
605             return false;
606         }
607     }
608     return true;
611 /**
612  * Is course creator going to have capability in a new course?
613  *
614  * This is intended to be used in enrolment plugins before or during course creation,
615  * do not use after the course is fully created.
616  *
617  * @category access
618  *
619  * @param string $capability the name of the capability to check.
620  * @param context $context course or category context where is course going to be created
621  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
622  * @return boolean true if the user will have this capability.
623  *
624  * @throws coding_exception if different type of context submitted
625  */
626 function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
627     global $CFG;
629     if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
630         throw new coding_exception('Only course or course category context expected');
631     }
633     if (has_capability($capability, $context, $user)) {
634         // User already has the capability, it could be only removed if CAP_PROHIBIT
635         // was involved here, but we ignore that.
636         return true;
637     }
639     if (!has_capability('moodle/course:create', $context, $user)) {
640         return false;
641     }
643     if (!enrol_is_enabled('manual')) {
644         return false;
645     }
647     if (empty($CFG->creatornewroleid)) {
648         return false;
649     }
651     if ($context->contextlevel == CONTEXT_COURSE) {
652         if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
653             return false;
654         }
655     } else {
656         if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
657             return false;
658         }
659     }
661     // Most likely they will be enrolled after the course creation is finished,
662     // does the new role have the required capability?
663     list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
664     return isset($neededroles[$CFG->creatornewroleid]);
667 /**
668  * Check if the user is an admin at the site level.
669  *
670  * Please note that use of proper capabilities is always encouraged,
671  * this function is supposed to be used from core or for temporary hacks.
672  *
673  * @category access
674  *
675  * @param  int|stdClass  $user_or_id user id or user object
676  * @return bool true if user is one of the administrators, false otherwise
677  */
678 function is_siteadmin($user_or_id = null) {
679     global $CFG, $USER;
681     if ($user_or_id === null) {
682         $user_or_id = $USER;
683     }
685     if (empty($user_or_id)) {
686         return false;
687     }
688     if (!empty($user_or_id->id)) {
689         $userid = $user_or_id->id;
690     } else {
691         $userid = $user_or_id;
692     }
694     // Because this script is called many times (150+ for course page) with
695     // the same parameters, it is worth doing minor optimisations. This static
696     // cache stores the value for a single userid, saving about 2ms from course
697     // page load time without using significant memory. As the static cache
698     // also includes the value it depends on, this cannot break unit tests.
699     static $knownid, $knownresult, $knownsiteadmins;
700     if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
701         return $knownresult;
702     }
703     $knownid = $userid;
704     $knownsiteadmins = $CFG->siteadmins;
706     $siteadmins = explode(',', $CFG->siteadmins);
707     $knownresult = in_array($userid, $siteadmins);
708     return $knownresult;
711 /**
712  * Returns true if user has at least one role assign
713  * of 'coursecontact' role (is potentially listed in some course descriptions).
714  *
715  * @param int $userid
716  * @return bool
717  */
718 function has_coursecontact_role($userid) {
719     global $DB, $CFG;
721     if (empty($CFG->coursecontact)) {
722         return false;
723     }
724     $sql = "SELECT 1
725               FROM {role_assignments}
726              WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
727     return $DB->record_exists_sql($sql, array('userid'=>$userid));
730 /**
731  * Does the user have a capability to do something?
732  *
733  * Walk the accessdata array and return true/false.
734  * Deals with prohibits, role switching, aggregating
735  * capabilities, etc.
736  *
737  * The main feature of here is being FAST and with no
738  * side effects.
739  *
740  * Notes:
741  *
742  * Switch Role merges with default role
743  * ------------------------------------
744  * If you are a teacher in course X, you have at least
745  * teacher-in-X + defaultloggedinuser-sitewide. So in the
746  * course you'll have techer+defaultloggedinuser.
747  * We try to mimic that in switchrole.
748  *
749  * Permission evaluation
750  * ---------------------
751  * Originally there was an extremely complicated way
752  * to determine the user access that dealt with
753  * "locality" or role assignments and role overrides.
754  * Now we simply evaluate access for each role separately
755  * and then verify if user has at least one role with allow
756  * and at the same time no role with prohibit.
757  *
758  * @access private
759  * @param string $capability
760  * @param context $context
761  * @param array $accessdata
762  * @return bool
763  */
764 function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
765     global $CFG;
767     // Build $paths as a list of current + all parent "paths" with order bottom-to-top
768     $path = $context->path;
769     $paths = array($path);
770     while($path = rtrim($path, '0123456789')) {
771         $path = rtrim($path, '/');
772         if ($path === '') {
773             break;
774         }
775         $paths[] = $path;
776     }
778     $roles = array();
779     $switchedrole = false;
781     // Find out if role switched
782     if (!empty($accessdata['rsw'])) {
783         // From the bottom up...
784         foreach ($paths as $path) {
785             if (isset($accessdata['rsw'][$path])) {
786                 // Found a switchrole assignment - check for that role _plus_ the default user role
787                 $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
788                 $switchedrole = true;
789                 break;
790             }
791         }
792     }
794     if (!$switchedrole) {
795         // get all users roles in this context and above
796         foreach ($paths as $path) {
797             if (isset($accessdata['ra'][$path])) {
798                 foreach ($accessdata['ra'][$path] as $roleid) {
799                     $roles[$roleid] = null;
800                 }
801             }
802         }
803     }
805     // Now find out what access is given to each role, going bottom-->up direction
806     $rdefs = get_role_definitions(array_keys($roles));
807     $allowed = false;
809     foreach ($roles as $roleid => $ignored) {
810         foreach ($paths as $path) {
811             if (isset($rdefs[$roleid][$path][$capability])) {
812                 $perm = (int)$rdefs[$roleid][$path][$capability];
813                 if ($perm === CAP_PROHIBIT) {
814                     // any CAP_PROHIBIT found means no permission for the user
815                     return false;
816                 }
817                 if (is_null($roles[$roleid])) {
818                     $roles[$roleid] = $perm;
819                 }
820             }
821         }
822         // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
823         $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
824     }
826     return $allowed;
829 /**
830  * A convenience function that tests has_capability, and displays an error if
831  * the user does not have that capability.
832  *
833  * NOTE before Moodle 2.0, this function attempted to make an appropriate
834  * require_login call before checking the capability. This is no longer the case.
835  * You must call require_login (or one of its variants) if you want to check the
836  * user is logged in, before you call this function.
837  *
838  * @see has_capability()
839  *
840  * @param string $capability the name of the capability to check. For example mod/forum:view
841  * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
842  * @param int $userid A user id. By default (null) checks the permissions of the current user.
843  * @param bool $doanything If false, ignore effect of admin role assignment
844  * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
845  * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
846  * @return void terminates with an error if the user does not have the given capability.
847  */
848 function require_capability($capability, context $context, $userid = null, $doanything = true,
849                             $errormessage = 'nopermissions', $stringfile = '') {
850     if (!has_capability($capability, $context, $userid, $doanything)) {
851         throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
852     }
855 /**
856  * Return a nested array showing all role assignments for the user.
857  * [ra] => [contextpath][roleid] = roleid
858  *
859  * @access private
860  * @param int $userid - the id of the user
861  * @return array access info array
862  */
863 function get_user_roles_sitewide_accessdata($userid) {
864     global $CFG, $DB;
866     $accessdata = get_empty_accessdata();
868     // start with the default role
869     if (!empty($CFG->defaultuserroleid)) {
870         $syscontext = context_system::instance();
871         $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
872     }
874     // load the "default frontpage role"
875     if (!empty($CFG->defaultfrontpageroleid)) {
876         $frontpagecontext = context_course::instance(get_site()->id);
877         if ($frontpagecontext->path) {
878             $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
879         }
880     }
882     // Preload every assigned role.
883     $sql = "SELECT ctx.path, ra.roleid, ra.contextid
884               FROM {role_assignments} ra
885               JOIN {context} ctx ON ctx.id = ra.contextid
886              WHERE ra.userid = :userid";
888     $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
890     foreach ($rs as $ra) {
891         // RAs leafs are arrays to support multi-role assignments...
892         $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
893     }
895     $rs->close();
897     return $accessdata;
900 /**
901  * Returns empty accessdata structure.
902  *
903  * @access private
904  * @return array empt accessdata
905  */
906 function get_empty_accessdata() {
907     $accessdata               = array(); // named list
908     $accessdata['ra']         = array();
909     $accessdata['time']       = time();
910     $accessdata['rsw']        = array();
912     return $accessdata;
915 /**
916  * Get accessdata for a given user.
917  *
918  * @access private
919  * @param int $userid
920  * @param bool $preloadonly true means do not return access array
921  * @return array accessdata
922  */
923 function get_user_accessdata($userid, $preloadonly=false) {
924     global $CFG, $ACCESSLIB_PRIVATE, $USER;
926     if (isset($USER->access)) {
927         $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
928     }
930     if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
931         if (empty($userid)) {
932             if (!empty($CFG->notloggedinroleid)) {
933                 $accessdata = get_role_access($CFG->notloggedinroleid);
934             } else {
935                 // weird
936                 return get_empty_accessdata();
937             }
939         } else if (isguestuser($userid)) {
940             if ($guestrole = get_guest_role()) {
941                 $accessdata = get_role_access($guestrole->id);
942             } else {
943                 //weird
944                 return get_empty_accessdata();
945             }
947         } else {
948             // Includes default role and frontpage role.
949             $accessdata = get_user_roles_sitewide_accessdata($userid);
950         }
952         $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
953     }
955     if ($preloadonly) {
956         return;
957     } else {
958         return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
959     }
962 /**
963  * A convenience function to completely load all the capabilities
964  * for the current user. It is called from has_capability() and functions change permissions.
965  *
966  * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
967  * @see check_enrolment_plugins()
968  *
969  * @access private
970  * @return void
971  */
972 function load_all_capabilities() {
973     global $USER;
975     // roles not installed yet - we are in the middle of installation
976     if (during_initial_install()) {
977         return;
978     }
980     if (!isset($USER->id)) {
981         // this should not happen
982         $USER->id = 0;
983     }
985     unset($USER->access);
986     $USER->access = get_user_accessdata($USER->id);
988     // Clear to force a refresh
989     unset($USER->mycourses);
991     // init/reset internal enrol caches - active course enrolments and temp access
992     $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
995 /**
996  * A convenience function to completely reload all the capabilities
997  * for the current user when roles have been updated in a relevant
998  * context -- but PRESERVING switchroles and loginas.
999  * This function resets all accesslib and context caches.
1000  *
1001  * That is - completely transparent to the user.
1002  *
1003  * Note: reloads $USER->access completely.
1004  *
1005  * @access private
1006  * @return void
1007  */
1008 function reload_all_capabilities() {
1009     global $USER, $DB, $ACCESSLIB_PRIVATE;
1011     // copy switchroles
1012     $sw = array();
1013     if (!empty($USER->access['rsw'])) {
1014         $sw = $USER->access['rsw'];
1015     }
1017     accesslib_clear_all_caches(true);
1018     unset($USER->access);
1020     // Prevent dirty flags refetching on this page.
1021     $ACCESSLIB_PRIVATE->dirtycontexts = array();
1022     $ACCESSLIB_PRIVATE->dirtyusers    = array($USER->id => false);
1024     load_all_capabilities();
1026     foreach ($sw as $path => $roleid) {
1027         if ($record = $DB->get_record('context', array('path'=>$path))) {
1028             $context = context::instance_by_id($record->id);
1029             if (has_capability('moodle/role:switchroles', $context)) {
1030                 role_switch($roleid, $context);
1031             }
1032         }
1033     }
1036 /**
1037  * Adds a temp role to current USER->access array.
1038  *
1039  * Useful for the "temporary guest" access we grant to logged-in users.
1040  * This is useful for enrol plugins only.
1041  *
1042  * @since Moodle 2.2
1043  * @param context_course $coursecontext
1044  * @param int $roleid
1045  * @return void
1046  */
1047 function load_temp_course_role(context_course $coursecontext, $roleid) {
1048     global $USER, $SITE;
1050     if (empty($roleid)) {
1051         debugging('invalid role specified in load_temp_course_role()');
1052         return;
1053     }
1055     if ($coursecontext->instanceid == $SITE->id) {
1056         debugging('Can not use temp roles on the frontpage');
1057         return;
1058     }
1060     if (!isset($USER->access)) {
1061         load_all_capabilities();
1062     }
1064     $coursecontext->reload_if_dirty();
1066     if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
1067         return;
1068     }
1070     $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
1073 /**
1074  * Removes any extra guest roles from current USER->access array.
1075  * This is useful for enrol plugins only.
1076  *
1077  * @since Moodle 2.2
1078  * @param context_course $coursecontext
1079  * @return void
1080  */
1081 function remove_temp_course_roles(context_course $coursecontext) {
1082     global $DB, $USER, $SITE;
1084     if ($coursecontext->instanceid == $SITE->id) {
1085         debugging('Can not use temp roles on the frontpage');
1086         return;
1087     }
1089     if (empty($USER->access['ra'][$coursecontext->path])) {
1090         //no roles here, weird
1091         return;
1092     }
1094     $sql = "SELECT DISTINCT ra.roleid AS id
1095               FROM {role_assignments} ra
1096              WHERE ra.contextid = :contextid AND ra.userid = :userid";
1097     $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
1099     $USER->access['ra'][$coursecontext->path] = array();
1100     foreach($ras as $r) {
1101         $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
1102     }
1105 /**
1106  * Returns array of all role archetypes.
1107  *
1108  * @return array
1109  */
1110 function get_role_archetypes() {
1111     return array(
1112         'manager'        => 'manager',
1113         'coursecreator'  => 'coursecreator',
1114         'editingteacher' => 'editingteacher',
1115         'teacher'        => 'teacher',
1116         'student'        => 'student',
1117         'guest'          => 'guest',
1118         'user'           => 'user',
1119         'frontpage'      => 'frontpage'
1120     );
1123 /**
1124  * Assign the defaults found in this capability definition to roles that have
1125  * the corresponding legacy capabilities assigned to them.
1126  *
1127  * @param string $capability
1128  * @param array $legacyperms an array in the format (example):
1129  *                      'guest' => CAP_PREVENT,
1130  *                      'student' => CAP_ALLOW,
1131  *                      'teacher' => CAP_ALLOW,
1132  *                      'editingteacher' => CAP_ALLOW,
1133  *                      'coursecreator' => CAP_ALLOW,
1134  *                      'manager' => CAP_ALLOW
1135  * @return boolean success or failure.
1136  */
1137 function assign_legacy_capabilities($capability, $legacyperms) {
1139     $archetypes = get_role_archetypes();
1141     foreach ($legacyperms as $type => $perm) {
1143         $systemcontext = context_system::instance();
1144         if ($type === 'admin') {
1145             debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1146             $type = 'manager';
1147         }
1149         if (!array_key_exists($type, $archetypes)) {
1150             print_error('invalidlegacy', '', '', $type);
1151         }
1153         if ($roles = get_archetype_roles($type)) {
1154             foreach ($roles as $role) {
1155                 // Assign a site level capability.
1156                 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1157                     return false;
1158                 }
1159             }
1160         }
1161     }
1162     return true;
1165 /**
1166  * Verify capability risks.
1167  *
1168  * @param stdClass $capability a capability - a row from the capabilities table.
1169  * @return boolean whether this capability is safe - that is, whether people with the
1170  *      safeoverrides capability should be allowed to change it.
1171  */
1172 function is_safe_capability($capability) {
1173     return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1176 /**
1177  * Get the local override (if any) for a given capability in a role in a context
1178  *
1179  * @param int $roleid
1180  * @param int $contextid
1181  * @param string $capability
1182  * @return stdClass local capability override
1183  */
1184 function get_local_override($roleid, $contextid, $capability) {
1185     global $DB;
1186     return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
1189 /**
1190  * Returns context instance plus related course and cm instances
1191  *
1192  * @param int $contextid
1193  * @return array of ($context, $course, $cm)
1194  */
1195 function get_context_info_array($contextid) {
1196     global $DB;
1198     $context = context::instance_by_id($contextid, MUST_EXIST);
1199     $course  = null;
1200     $cm      = null;
1202     if ($context->contextlevel == CONTEXT_COURSE) {
1203         $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
1205     } else if ($context->contextlevel == CONTEXT_MODULE) {
1206         $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
1207         $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1209     } else if ($context->contextlevel == CONTEXT_BLOCK) {
1210         $parent = $context->get_parent_context();
1212         if ($parent->contextlevel == CONTEXT_COURSE) {
1213             $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
1214         } else if ($parent->contextlevel == CONTEXT_MODULE) {
1215             $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
1216             $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1217         }
1218     }
1220     return array($context, $course, $cm);
1223 /**
1224  * Function that creates a role
1225  *
1226  * @param string $name role name
1227  * @param string $shortname role short name
1228  * @param string $description role description
1229  * @param string $archetype
1230  * @return int id or dml_exception
1231  */
1232 function create_role($name, $shortname, $description, $archetype = '') {
1233     global $DB;
1235     if (strpos($archetype, 'moodle/legacy:') !== false) {
1236         throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
1237     }
1239     // verify role archetype actually exists
1240     $archetypes = get_role_archetypes();
1241     if (empty($archetypes[$archetype])) {
1242         $archetype = '';
1243     }
1245     // Insert the role record.
1246     $role = new stdClass();
1247     $role->name        = $name;
1248     $role->shortname   = $shortname;
1249     $role->description = $description;
1250     $role->archetype   = $archetype;
1252     //find free sortorder number
1253     $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
1254     if (empty($role->sortorder)) {
1255         $role->sortorder = 1;
1256     }
1257     $id = $DB->insert_record('role', $role);
1259     return $id;
1262 /**
1263  * Function that deletes a role and cleanups up after it
1264  *
1265  * @param int $roleid id of role to delete
1266  * @return bool always true
1267  */
1268 function delete_role($roleid) {
1269     global $DB;
1271     // first unssign all users
1272     role_unassign_all(array('roleid'=>$roleid));
1274     // cleanup all references to this role, ignore errors
1275     $DB->delete_records('role_capabilities',   array('roleid'=>$roleid));
1276     $DB->delete_records('role_allow_assign',   array('roleid'=>$roleid));
1277     $DB->delete_records('role_allow_assign',   array('allowassign'=>$roleid));
1278     $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
1279     $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
1280     $DB->delete_records('role_names',          array('roleid'=>$roleid));
1281     $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
1283     // Get role record before it's deleted.
1284     $role = $DB->get_record('role', array('id'=>$roleid));
1286     // Finally delete the role itself.
1287     $DB->delete_records('role', array('id'=>$roleid));
1289     // Trigger event.
1290     $event = \core\event\role_deleted::create(
1291         array(
1292             'context' => context_system::instance(),
1293             'objectid' => $roleid,
1294             'other' =>
1295                 array(
1296                     'shortname' => $role->shortname,
1297                     'description' => $role->description,
1298                     'archetype' => $role->archetype
1299                 )
1300             )
1301         );
1302     $event->add_record_snapshot('role', $role);
1303     $event->trigger();
1305     // Reset any cache of this role, including MUC.
1306     accesslib_clear_role_cache($roleid);
1308     return true;
1311 /**
1312  * Function to write context specific overrides, or default capabilities.
1313  *
1314  * @param string $capability string name
1315  * @param int $permission CAP_ constants
1316  * @param int $roleid role id
1317  * @param int|context $contextid context id
1318  * @param bool $overwrite
1319  * @return bool always true or exception
1320  */
1321 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
1322     global $USER, $DB;
1324     if ($contextid instanceof context) {
1325         $context = $contextid;
1326     } else {
1327         $context = context::instance_by_id($contextid);
1328     }
1330     if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
1331         unassign_capability($capability, $roleid, $context->id);
1332         return true;
1333     }
1335     $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
1337     if ($existing and !$overwrite) {   // We want to keep whatever is there already
1338         return true;
1339     }
1341     $cap = new stdClass();
1342     $cap->contextid    = $context->id;
1343     $cap->roleid       = $roleid;
1344     $cap->capability   = $capability;
1345     $cap->permission   = $permission;
1346     $cap->timemodified = time();
1347     $cap->modifierid   = empty($USER->id) ? 0 : $USER->id;
1349     if ($existing) {
1350         $cap->id = $existing->id;
1351         $DB->update_record('role_capabilities', $cap);
1352     } else {
1353         if ($DB->record_exists('context', array('id'=>$context->id))) {
1354             $DB->insert_record('role_capabilities', $cap);
1355         }
1356     }
1358     // Reset any cache of this role, including MUC.
1359     accesslib_clear_role_cache($roleid);
1361     return true;
1364 /**
1365  * Unassign a capability from a role.
1366  *
1367  * @param string $capability the name of the capability
1368  * @param int $roleid the role id
1369  * @param int|context $contextid null means all contexts
1370  * @return boolean true or exception
1371  */
1372 function unassign_capability($capability, $roleid, $contextid = null) {
1373     global $DB;
1375     if (!empty($contextid)) {
1376         if ($contextid instanceof context) {
1377             $context = $contextid;
1378         } else {
1379             $context = context::instance_by_id($contextid);
1380         }
1381         // delete from context rel, if this is the last override in this context
1382         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
1383     } else {
1384         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
1385     }
1387     // Reset any cache of this role, including MUC.
1388     accesslib_clear_role_cache($roleid);
1390     return true;
1393 /**
1394  * Get the roles that have a given capability assigned to it
1395  *
1396  * This function does not resolve the actual permission of the capability.
1397  * It just checks for permissions and overrides.
1398  * Use get_roles_with_cap_in_context() if resolution is required.
1399  *
1400  * @param string $capability capability name (string)
1401  * @param string $permission optional, the permission defined for this capability
1402  *                      either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
1403  * @param stdClass $context null means any
1404  * @return array of role records
1405  */
1406 function get_roles_with_capability($capability, $permission = null, $context = null) {
1407     global $DB;
1409     if ($context) {
1410         $contexts = $context->get_parent_context_ids(true);
1411         list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
1412         $contextsql = "AND rc.contextid $insql";
1413     } else {
1414         $params = array();
1415         $contextsql = '';
1416     }
1418     if ($permission) {
1419         $permissionsql = " AND rc.permission = :permission";
1420         $params['permission'] = $permission;
1421     } else {
1422         $permissionsql = '';
1423     }
1425     $sql = "SELECT r.*
1426               FROM {role} r
1427              WHERE r.id IN (SELECT rc.roleid
1428                               FROM {role_capabilities} rc
1429                              WHERE rc.capability = :capname
1430                                    $contextsql
1431                                    $permissionsql)";
1432     $params['capname'] = $capability;
1435     return $DB->get_records_sql($sql, $params);
1438 /**
1439  * This function makes a role-assignment (a role for a user in a particular context)
1440  *
1441  * @param int $roleid the role of the id
1442  * @param int $userid userid
1443  * @param int|context $contextid id of the context
1444  * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
1445  * @param int $itemid id of enrolment/auth plugin
1446  * @param string $timemodified defaults to current time
1447  * @return int new/existing id of the assignment
1448  */
1449 function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
1450     global $USER, $DB;
1452     // first of all detect if somebody is using old style parameters
1453     if ($contextid === 0 or is_numeric($component)) {
1454         throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
1455     }
1457     // now validate all parameters
1458     if (empty($roleid)) {
1459         throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
1460     }
1462     if (empty($userid)) {
1463         throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
1464     }
1466     if ($itemid) {
1467         if (strpos($component, '_') === false) {
1468             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
1469         }
1470     } else {
1471         $itemid = 0;
1472         if ($component !== '' and strpos($component, '_') === false) {
1473             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1474         }
1475     }
1477     if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
1478         throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
1479     }
1481     if ($contextid instanceof context) {
1482         $context = $contextid;
1483     } else {
1484         $context = context::instance_by_id($contextid, MUST_EXIST);
1485     }
1487     if (!$timemodified) {
1488         $timemodified = time();
1489     }
1491     // Check for existing entry
1492     $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
1494     if ($ras) {
1495         // role already assigned - this should not happen
1496         if (count($ras) > 1) {
1497             // very weird - remove all duplicates!
1498             $ra = array_shift($ras);
1499             foreach ($ras as $r) {
1500                 $DB->delete_records('role_assignments', array('id'=>$r->id));
1501             }
1502         } else {
1503             $ra = reset($ras);
1504         }
1506         // actually there is no need to update, reset anything or trigger any event, so just return
1507         return $ra->id;
1508     }
1510     // Create a new entry
1511     $ra = new stdClass();
1512     $ra->roleid       = $roleid;
1513     $ra->contextid    = $context->id;
1514     $ra->userid       = $userid;
1515     $ra->component    = $component;
1516     $ra->itemid       = $itemid;
1517     $ra->timemodified = $timemodified;
1518     $ra->modifierid   = empty($USER->id) ? 0 : $USER->id;
1519     $ra->sortorder    = 0;
1521     $ra->id = $DB->insert_record('role_assignments', $ra);
1523     // Role assignments have changed, so mark user as dirty.
1524     mark_user_dirty($userid);
1526     core_course_category::role_assignment_changed($roleid, $context);
1528     $event = \core\event\role_assigned::create(array(
1529         'context' => $context,
1530         'objectid' => $ra->roleid,
1531         'relateduserid' => $ra->userid,
1532         'other' => array(
1533             'id' => $ra->id,
1534             'component' => $ra->component,
1535             'itemid' => $ra->itemid
1536         )
1537     ));
1538     $event->add_record_snapshot('role_assignments', $ra);
1539     $event->trigger();
1541     return $ra->id;
1544 /**
1545  * Removes one role assignment
1546  *
1547  * @param int $roleid
1548  * @param int  $userid
1549  * @param int  $contextid
1550  * @param string $component
1551  * @param int  $itemid
1552  * @return void
1553  */
1554 function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
1555     // first make sure the params make sense
1556     if ($roleid == 0 or $userid == 0 or $contextid == 0) {
1557         throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
1558     }
1560     if ($itemid) {
1561         if (strpos($component, '_') === false) {
1562             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
1563         }
1564     } else {
1565         $itemid = 0;
1566         if ($component !== '' and strpos($component, '_') === false) {
1567             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1568         }
1569     }
1571     role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
1574 /**
1575  * Removes multiple role assignments, parameters may contain:
1576  *   'roleid', 'userid', 'contextid', 'component', 'enrolid'.
1577  *
1578  * @param array $params role assignment parameters
1579  * @param bool $subcontexts unassign in subcontexts too
1580  * @param bool $includemanual include manual role assignments too
1581  * @return void
1582  */
1583 function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
1584     global $USER, $CFG, $DB;
1586     if (!$params) {
1587         throw new coding_exception('Missing parameters in role_unsassign_all() call');
1588     }
1590     $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
1591     foreach ($params as $key=>$value) {
1592         if (!in_array($key, $allowed)) {
1593             throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
1594         }
1595     }
1597     if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
1598         throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
1599     }
1601     if ($includemanual) {
1602         if (!isset($params['component']) or $params['component'] === '') {
1603             throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
1604         }
1605     }
1607     if ($subcontexts) {
1608         if (empty($params['contextid'])) {
1609             throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
1610         }
1611     }
1613     $ras = $DB->get_records('role_assignments', $params);
1614     foreach($ras as $ra) {
1615         $DB->delete_records('role_assignments', array('id'=>$ra->id));
1616         if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
1617             // Role assignments have changed, so mark user as dirty.
1618             mark_user_dirty($ra->userid);
1620             $event = \core\event\role_unassigned::create(array(
1621                 'context' => $context,
1622                 'objectid' => $ra->roleid,
1623                 'relateduserid' => $ra->userid,
1624                 'other' => array(
1625                     'id' => $ra->id,
1626                     'component' => $ra->component,
1627                     'itemid' => $ra->itemid
1628                 )
1629             ));
1630             $event->add_record_snapshot('role_assignments', $ra);
1631             $event->trigger();
1632             core_course_category::role_assignment_changed($ra->roleid, $context);
1633         }
1634     }
1635     unset($ras);
1637     // process subcontexts
1638     if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
1639         if ($params['contextid'] instanceof context) {
1640             $context = $params['contextid'];
1641         } else {
1642             $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
1643         }
1645         if ($context) {
1646             $contexts = $context->get_child_contexts();
1647             $mparams = $params;
1648             foreach($contexts as $context) {
1649                 $mparams['contextid'] = $context->id;
1650                 $ras = $DB->get_records('role_assignments', $mparams);
1651                 foreach($ras as $ra) {
1652                     $DB->delete_records('role_assignments', array('id'=>$ra->id));
1653                     // Role assignments have changed, so mark user as dirty.
1654                     mark_user_dirty($ra->userid);
1656                     $event = \core\event\role_unassigned::create(
1657                         array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1658                             'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1659                     $event->add_record_snapshot('role_assignments', $ra);
1660                     $event->trigger();
1661                     core_course_category::role_assignment_changed($ra->roleid, $context);
1662                 }
1663             }
1664         }
1665     }
1667     // do this once more for all manual role assignments
1668     if ($includemanual) {
1669         $params['component'] = '';
1670         role_unassign_all($params, $subcontexts, false);
1671     }
1674 /**
1675  * Mark a user as dirty (with timestamp) so as to force reloading of the user session.
1676  *
1677  * @param int $userid
1678  * @return void
1679  */
1680 function mark_user_dirty($userid) {
1681     global $CFG, $ACCESSLIB_PRIVATE;
1683     if (during_initial_install()) {
1684         return;
1685     }
1687     // Throw exception if invalid userid is provided.
1688     if (empty($userid)) {
1689         throw new coding_exception('Invalid user parameter supplied for mark_user_dirty() function!');
1690     }
1692     // Set dirty flag in database, set dirty field locally, and clear local accessdata cache.
1693     set_cache_flag('accesslib/dirtyusers', $userid, 1, time() + $CFG->sessiontimeout);
1694     $ACCESSLIB_PRIVATE->dirtyusers[$userid] = 1;
1695     unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
1698 /**
1699  * Determines if a user is currently logged in
1700  *
1701  * @category   access
1702  *
1703  * @return bool
1704  */
1705 function isloggedin() {
1706     global $USER;
1708     return (!empty($USER->id));
1711 /**
1712  * Determines if a user is logged in as real guest user with username 'guest'.
1713  *
1714  * @category   access
1715  *
1716  * @param int|object $user mixed user object or id, $USER if not specified
1717  * @return bool true if user is the real guest user, false if not logged in or other user
1718  */
1719 function isguestuser($user = null) {
1720     global $USER, $DB, $CFG;
1722     // make sure we have the user id cached in config table, because we are going to use it a lot
1723     if (empty($CFG->siteguest)) {
1724         if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
1725             // guest does not exist yet, weird
1726             return false;
1727         }
1728         set_config('siteguest', $guestid);
1729     }
1730     if ($user === null) {
1731         $user = $USER;
1732     }
1734     if ($user === null) {
1735         // happens when setting the $USER
1736         return false;
1738     } else if (is_numeric($user)) {
1739         return ($CFG->siteguest == $user);
1741     } else if (is_object($user)) {
1742         if (empty($user->id)) {
1743             return false; // not logged in means is not be guest
1744         } else {
1745             return ($CFG->siteguest == $user->id);
1746         }
1748     } else {
1749         throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
1750     }
1753 /**
1754  * Does user have a (temporary or real) guest access to course?
1755  *
1756  * @category   access
1757  *
1758  * @param context $context
1759  * @param stdClass|int $user
1760  * @return bool
1761  */
1762 function is_guest(context $context, $user = null) {
1763     global $USER;
1765     // first find the course context
1766     $coursecontext = $context->get_course_context();
1768     // make sure there is a real user specified
1769     if ($user === null) {
1770         $userid = isset($USER->id) ? $USER->id : 0;
1771     } else {
1772         $userid = is_object($user) ? $user->id : $user;
1773     }
1775     if (isguestuser($userid)) {
1776         // can not inspect or be enrolled
1777         return true;
1778     }
1780     if (has_capability('moodle/course:view', $coursecontext, $user)) {
1781         // viewing users appear out of nowhere, they are neither guests nor participants
1782         return false;
1783     }
1785     // consider only real active enrolments here
1786     if (is_enrolled($coursecontext, $user, '', true)) {
1787         return false;
1788     }
1790     return true;
1793 /**
1794  * Returns true if the user has moodle/course:view capability in the course,
1795  * this is intended for admins, managers (aka small admins), inspectors, etc.
1796  *
1797  * @category   access
1798  *
1799  * @param context $context
1800  * @param int|stdClass $user if null $USER is used
1801  * @param string $withcapability extra capability name
1802  * @return bool
1803  */
1804 function is_viewing(context $context, $user = null, $withcapability = '') {
1805     // first find the course context
1806     $coursecontext = $context->get_course_context();
1808     if (isguestuser($user)) {
1809         // can not inspect
1810         return false;
1811     }
1813     if (!has_capability('moodle/course:view', $coursecontext, $user)) {
1814         // admins are allowed to inspect courses
1815         return false;
1816     }
1818     if ($withcapability and !has_capability($withcapability, $context, $user)) {
1819         // site admins always have the capability, but the enrolment above blocks
1820         return false;
1821     }
1823     return true;
1826 /**
1827  * Returns true if the user is able to access the course.
1828  *
1829  * This function is in no way, shape, or form a substitute for require_login.
1830  * It should only be used in circumstances where it is not possible to call require_login
1831  * such as the navigation.
1832  *
1833  * This function checks many of the methods of access to a course such as the view
1834  * capability, enrollments, and guest access. It also makes use of the cache
1835  * generated by require_login for guest access.
1836  *
1837  * The flags within the $USER object that are used here should NEVER be used outside
1838  * of this function can_access_course and require_login. Doing so WILL break future
1839  * versions.
1840  *
1841  * @param stdClass $course record
1842  * @param stdClass|int|null $user user record or id, current user if null
1843  * @param string $withcapability Check for this capability as well.
1844  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1845  * @return boolean Returns true if the user is able to access the course
1846  */
1847 function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
1848     global $DB, $USER;
1850     // this function originally accepted $coursecontext parameter
1851     if ($course instanceof context) {
1852         if ($course instanceof context_course) {
1853             debugging('deprecated context parameter, please use $course record');
1854             $coursecontext = $course;
1855             $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
1856         } else {
1857             debugging('Invalid context parameter, please use $course record');
1858             return false;
1859         }
1860     } else {
1861         $coursecontext = context_course::instance($course->id);
1862     }
1864     if (!isset($USER->id)) {
1865         // should never happen
1866         $USER->id = 0;
1867         debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
1868     }
1870     // make sure there is a user specified
1871     if ($user === null) {
1872         $userid = $USER->id;
1873     } else {
1874         $userid = is_object($user) ? $user->id : $user;
1875     }
1876     unset($user);
1878     if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
1879         return false;
1880     }
1882     if ($userid == $USER->id) {
1883         if (!empty($USER->access['rsw'][$coursecontext->path])) {
1884             // the fact that somebody switched role means they can access the course no matter to what role they switched
1885             return true;
1886         }
1887     }
1889     if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
1890         return false;
1891     }
1893     if (is_viewing($coursecontext, $userid)) {
1894         return true;
1895     }
1897     if ($userid != $USER->id) {
1898         // for performance reasons we do not verify temporary guest access for other users, sorry...
1899         return is_enrolled($coursecontext, $userid, '', $onlyactive);
1900     }
1902     // === from here we deal only with $USER ===
1904     $coursecontext->reload_if_dirty();
1906     if (isset($USER->enrol['enrolled'][$course->id])) {
1907         if ($USER->enrol['enrolled'][$course->id] > time()) {
1908             return true;
1909         }
1910     }
1911     if (isset($USER->enrol['tempguest'][$course->id])) {
1912         if ($USER->enrol['tempguest'][$course->id] > time()) {
1913             return true;
1914         }
1915     }
1917     if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
1918         return true;
1919     }
1921     // if not enrolled try to gain temporary guest access
1922     $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
1923     $enrols = enrol_get_plugins(true);
1924     foreach($instances as $instance) {
1925         if (!isset($enrols[$instance->enrol])) {
1926             continue;
1927         }
1928         // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
1929         $until = $enrols[$instance->enrol]->try_guestaccess($instance);
1930         if ($until !== false and $until > time()) {
1931             $USER->enrol['tempguest'][$course->id] = $until;
1932             return true;
1933         }
1934     }
1935     if (isset($USER->enrol['tempguest'][$course->id])) {
1936         unset($USER->enrol['tempguest'][$course->id]);
1937         remove_temp_course_roles($coursecontext);
1938     }
1940     return false;
1943 /**
1944  * Loads the capability definitions for the component (from file).
1945  *
1946  * Loads the capability definitions for the component (from file). If no
1947  * capabilities are defined for the component, we simply return an empty array.
1948  *
1949  * @access private
1950  * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
1951  * @return array array of capabilities
1952  */
1953 function load_capability_def($component) {
1954     $defpath = core_component::get_component_directory($component).'/db/access.php';
1956     $capabilities = array();
1957     if (file_exists($defpath)) {
1958         require($defpath);
1959         if (!empty(${$component.'_capabilities'})) {
1960             // BC capability array name
1961             // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
1962             debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
1963             $capabilities = ${$component.'_capabilities'};
1964         }
1965     }
1967     return $capabilities;
1970 /**
1971  * Gets the capabilities that have been cached in the database for this component.
1972  *
1973  * @access private
1974  * @param string $component - examples: 'moodle', 'mod_forum'
1975  * @return array array of capabilities
1976  */
1977 function get_cached_capabilities($component = 'moodle') {
1978     global $DB;
1979     $caps = get_all_capabilities();
1980     $componentcaps = array();
1981     foreach ($caps as $cap) {
1982         if ($cap['component'] == $component) {
1983             $componentcaps[] = (object) $cap;
1984         }
1985     }
1986     return $componentcaps;
1989 /**
1990  * Returns default capabilities for given role archetype.
1991  *
1992  * @param string $archetype role archetype
1993  * @return array
1994  */
1995 function get_default_capabilities($archetype) {
1996     global $DB;
1998     if (!$archetype) {
1999         return array();
2000     }
2002     $alldefs = array();
2003     $defaults = array();
2004     $components = array();
2005     $allcaps = get_all_capabilities();
2007     foreach ($allcaps as $cap) {
2008         if (!in_array($cap['component'], $components)) {
2009             $components[] = $cap['component'];
2010             $alldefs = array_merge($alldefs, load_capability_def($cap['component']));
2011         }
2012     }
2013     foreach($alldefs as $name=>$def) {
2014         // Use array 'archetypes if available. Only if not specified, use 'legacy'.
2015         if (isset($def['archetypes'])) {
2016             if (isset($def['archetypes'][$archetype])) {
2017                 $defaults[$name] = $def['archetypes'][$archetype];
2018             }
2019         // 'legacy' is for backward compatibility with 1.9 access.php
2020         } else {
2021             if (isset($def['legacy'][$archetype])) {
2022                 $defaults[$name] = $def['legacy'][$archetype];
2023             }
2024         }
2025     }
2027     return $defaults;
2030 /**
2031  * Return default roles that can be assigned, overridden or switched
2032  * by give role archetype.
2033  *
2034  * @param string $type  assign|override|switch|view
2035  * @param string $archetype
2036  * @return array of role ids
2037  */
2038 function get_default_role_archetype_allows($type, $archetype) {
2039     global $DB;
2041     if (empty($archetype)) {
2042         return array();
2043     }
2045     $roles = $DB->get_records('role');
2046     $archetypemap = array();
2047     foreach ($roles as $role) {
2048         if ($role->archetype) {
2049             $archetypemap[$role->archetype][$role->id] = $role->id;
2050         }
2051     }
2053     $defaults = array(
2054         'assign' => array(
2055             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
2056             'coursecreator'  => array(),
2057             'editingteacher' => array('teacher', 'student'),
2058             'teacher'        => array(),
2059             'student'        => array(),
2060             'guest'          => array(),
2061             'user'           => array(),
2062             'frontpage'      => array(),
2063         ),
2064         'override' => array(
2065             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2066             'coursecreator'  => array(),
2067             'editingteacher' => array('teacher', 'student', 'guest'),
2068             'teacher'        => array(),
2069             'student'        => array(),
2070             'guest'          => array(),
2071             'user'           => array(),
2072             'frontpage'      => array(),
2073         ),
2074         'switch' => array(
2075             'manager'        => array('editingteacher', 'teacher', 'student', 'guest'),
2076             'coursecreator'  => array(),
2077             'editingteacher' => array('teacher', 'student', 'guest'),
2078             'teacher'        => array('student', 'guest'),
2079             'student'        => array(),
2080             'guest'          => array(),
2081             'user'           => array(),
2082             'frontpage'      => array(),
2083         ),
2084         'view' => array(
2085             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2086             'coursecreator'  => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2087             'editingteacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2088             'teacher'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2089             'student'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2090             'guest'          => array(),
2091             'user'           => array(),
2092             'frontpage'      => array(),
2093         ),
2094     );
2096     if (!isset($defaults[$type][$archetype])) {
2097         debugging("Unknown type '$type'' or archetype '$archetype''");
2098         return array();
2099     }
2101     $return = array();
2102     foreach ($defaults[$type][$archetype] as $at) {
2103         if (isset($archetypemap[$at])) {
2104             foreach ($archetypemap[$at] as $roleid) {
2105                 $return[$roleid] = $roleid;
2106             }
2107         }
2108     }
2110     return $return;
2113 /**
2114  * Reset role capabilities to default according to selected role archetype.
2115  * If no archetype selected, removes all capabilities.
2116  *
2117  * This applies to capabilities that are assigned to the role (that you could
2118  * edit in the 'define roles' interface), and not to any capability overrides
2119  * in different locations.
2120  *
2121  * @param int $roleid ID of role to reset capabilities for
2122  */
2123 function reset_role_capabilities($roleid) {
2124     global $DB;
2126     $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
2127     $defaultcaps = get_default_capabilities($role->archetype);
2129     $systemcontext = context_system::instance();
2131     $DB->delete_records('role_capabilities',
2132             array('roleid' => $roleid, 'contextid' => $systemcontext->id));
2134     foreach($defaultcaps as $cap=>$permission) {
2135         assign_capability($cap, $permission, $roleid, $systemcontext->id);
2136     }
2138     // Reset any cache of this role, including MUC.
2139     accesslib_clear_role_cache($roleid);
2142 /**
2143  * Updates the capabilities table with the component capability definitions.
2144  * If no parameters are given, the function updates the core moodle
2145  * capabilities.
2146  *
2147  * Note that the absence of the db/access.php capabilities definition file
2148  * will cause any stored capabilities for the component to be removed from
2149  * the database.
2150  *
2151  * @access private
2152  * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
2153  * @return boolean true if success, exception in case of any problems
2154  */
2155 function update_capabilities($component = 'moodle') {
2156     global $DB, $OUTPUT;
2158     $storedcaps = array();
2160     $filecaps = load_capability_def($component);
2161     foreach($filecaps as $capname=>$unused) {
2162         if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
2163             debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
2164         }
2165     }
2167     // It is possible somebody directly modified the DB (according to accesslib_test anyway).
2168     // So ensure our updating is based on fresh data.
2169     cache::make('core', 'capabilities')->delete('core_capabilities');
2171     $cachedcaps = get_cached_capabilities($component);
2172     if ($cachedcaps) {
2173         foreach ($cachedcaps as $cachedcap) {
2174             array_push($storedcaps, $cachedcap->name);
2175             // update risk bitmasks and context levels in existing capabilities if needed
2176             if (array_key_exists($cachedcap->name, $filecaps)) {
2177                 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
2178                     $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
2179                 }
2180                 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
2181                     $updatecap = new stdClass();
2182                     $updatecap->id = $cachedcap->id;
2183                     $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
2184                     $DB->update_record('capabilities', $updatecap);
2185                 }
2186                 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
2187                     $updatecap = new stdClass();
2188                     $updatecap->id = $cachedcap->id;
2189                     $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
2190                     $DB->update_record('capabilities', $updatecap);
2191                 }
2193                 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
2194                     $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
2195                 }
2196                 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
2197                     $updatecap = new stdClass();
2198                     $updatecap->id = $cachedcap->id;
2199                     $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
2200                     $DB->update_record('capabilities', $updatecap);
2201                 }
2202             }
2203         }
2204     }
2206     // Flush the cached again, as we have changed DB.
2207     cache::make('core', 'capabilities')->delete('core_capabilities');
2209     // Are there new capabilities in the file definition?
2210     $newcaps = array();
2212     foreach ($filecaps as $filecap => $def) {
2213         if (!$storedcaps ||
2214                 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
2215             if (!array_key_exists('riskbitmask', $def)) {
2216                 $def['riskbitmask'] = 0; // no risk if not specified
2217             }
2218             $newcaps[$filecap] = $def;
2219         }
2220     }
2221     // Add new capabilities to the stored definition.
2222     $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
2223     foreach ($newcaps as $capname => $capdef) {
2224         $capability = new stdClass();
2225         $capability->name         = $capname;
2226         $capability->captype      = $capdef['captype'];
2227         $capability->contextlevel = $capdef['contextlevel'];
2228         $capability->component    = $component;
2229         $capability->riskbitmask  = $capdef['riskbitmask'];
2231         $DB->insert_record('capabilities', $capability, false);
2233         if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
2234             if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
2235                 foreach ($rolecapabilities as $rolecapability){
2236                     //assign_capability will update rather than insert if capability exists
2237                     if (!assign_capability($capname, $rolecapability->permission,
2238                                             $rolecapability->roleid, $rolecapability->contextid, true)){
2239                          echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
2240                     }
2241                 }
2242             }
2243         // we ignore archetype key if we have cloned permissions
2244         } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
2245             assign_legacy_capabilities($capname, $capdef['archetypes']);
2246         // 'legacy' is for backward compatibility with 1.9 access.php
2247         } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
2248             assign_legacy_capabilities($capname, $capdef['legacy']);
2249         }
2250     }
2251     // Are there any capabilities that have been removed from the file
2252     // definition that we need to delete from the stored capabilities and
2253     // role assignments?
2254     capabilities_cleanup($component, $filecaps);
2256     // reset static caches
2257     accesslib_reset_role_cache();
2259     // Flush the cached again, as we have changed DB.
2260     cache::make('core', 'capabilities')->delete('core_capabilities');
2262     return true;
2265 /**
2266  * Deletes cached capabilities that are no longer needed by the component.
2267  * Also unassigns these capabilities from any roles that have them.
2268  * NOTE: this function is called from lib/db/upgrade.php
2269  *
2270  * @access private
2271  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
2272  * @param array $newcapdef array of the new capability definitions that will be
2273  *                     compared with the cached capabilities
2274  * @return int number of deprecated capabilities that have been removed
2275  */
2276 function capabilities_cleanup($component, $newcapdef = null) {
2277     global $DB;
2279     $removedcount = 0;
2281     if ($cachedcaps = get_cached_capabilities($component)) {
2282         foreach ($cachedcaps as $cachedcap) {
2283             if (empty($newcapdef) ||
2284                         array_key_exists($cachedcap->name, $newcapdef) === false) {
2286                 // Remove from capabilities cache.
2287                 $DB->delete_records('capabilities', array('name'=>$cachedcap->name));
2288                 $removedcount++;
2289                 // Delete from roles.
2290                 if ($roles = get_roles_with_capability($cachedcap->name)) {
2291                     foreach($roles as $role) {
2292                         if (!unassign_capability($cachedcap->name, $role->id)) {
2293                             print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
2294                         }
2295                     }
2296                 }
2297             } // End if.
2298         }
2299     }
2300     if ($removedcount) {
2301         cache::make('core', 'capabilities')->delete('core_capabilities');
2302     }
2303     return $removedcount;
2306 /**
2307  * Returns an array of all the known types of risk
2308  * The array keys can be used, for example as CSS class names, or in calls to
2309  * print_risk_icon. The values are the corresponding RISK_ constants.
2310  *
2311  * @return array all the known types of risk.
2312  */
2313 function get_all_risks() {
2314     return array(
2315         'riskmanagetrust' => RISK_MANAGETRUST,
2316         'riskconfig'      => RISK_CONFIG,
2317         'riskxss'         => RISK_XSS,
2318         'riskpersonal'    => RISK_PERSONAL,
2319         'riskspam'        => RISK_SPAM,
2320         'riskdataloss'    => RISK_DATALOSS,
2321     );
2324 /**
2325  * Return a link to moodle docs for a given capability name
2326  *
2327  * @param stdClass $capability a capability - a row from the mdl_capabilities table.
2328  * @return string the human-readable capability name as a link to Moodle Docs.
2329  */
2330 function get_capability_docs_link($capability) {
2331     $url = get_docs_url('Capabilities/' . $capability->name);
2332     return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
2335 /**
2336  * This function pulls out all the resolved capabilities (overrides and
2337  * defaults) of a role used in capability overrides in contexts at a given
2338  * context.
2339  *
2340  * @param int $roleid
2341  * @param context $context
2342  * @param string $cap capability, optional, defaults to ''
2343  * @return array Array of capabilities
2344  */
2345 function role_context_capabilities($roleid, context $context, $cap = '') {
2346     global $DB;
2348     $contexts = $context->get_parent_context_ids(true);
2349     $contexts = '('.implode(',', $contexts).')';
2351     $params = array($roleid);
2353     if ($cap) {
2354         $search = " AND rc.capability = ? ";
2355         $params[] = $cap;
2356     } else {
2357         $search = '';
2358     }
2360     $sql = "SELECT rc.*
2361               FROM {role_capabilities} rc, {context} c
2362              WHERE rc.contextid in $contexts
2363                    AND rc.roleid = ?
2364                    AND rc.contextid = c.id $search
2365           ORDER BY c.contextlevel DESC, rc.capability DESC";
2367     $capabilities = array();
2369     if ($records = $DB->get_records_sql($sql, $params)) {
2370         // We are traversing via reverse order.
2371         foreach ($records as $record) {
2372             // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
2373             if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
2374                 $capabilities[$record->capability] = $record->permission;
2375             }
2376         }
2377     }
2378     return $capabilities;
2381 /**
2382  * Constructs array with contextids as first parameter and context paths,
2383  * in both cases bottom top including self.
2384  *
2385  * @access private
2386  * @param context $context
2387  * @return array
2388  */
2389 function get_context_info_list(context $context) {
2390     $contextids = explode('/', ltrim($context->path, '/'));
2391     $contextpaths = array();
2392     $contextids2 = $contextids;
2393     while ($contextids2) {
2394         $contextpaths[] = '/' . implode('/', $contextids2);
2395         array_pop($contextids2);
2396     }
2397     return array($contextids, $contextpaths);
2400 /**
2401  * Check if context is the front page context or a context inside it
2402  *
2403  * Returns true if this context is the front page context, or a context inside it,
2404  * otherwise false.
2405  *
2406  * @param context $context a context object.
2407  * @return bool
2408  */
2409 function is_inside_frontpage(context $context) {
2410     $frontpagecontext = context_course::instance(SITEID);
2411     return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
2414 /**
2415  * Returns capability information (cached)
2416  *
2417  * @param string $capabilityname
2418  * @return stdClass or null if capability not found
2419  */
2420 function get_capability_info($capabilityname) {
2421     $caps = get_all_capabilities();
2423     if (!isset($caps[$capabilityname])) {
2424         return null;
2425     }
2427     return (object) $caps[$capabilityname];
2430 /**
2431  * Returns all capabilitiy records, preferably from MUC and not database.
2432  *
2433  * @return array All capability records indexed by capability name
2434  */
2435 function get_all_capabilities() {
2436     global $DB;
2437     $cache = cache::make('core', 'capabilities');
2438     if (!$allcaps = $cache->get('core_capabilities')) {
2439         $rs = $DB->get_recordset('capabilities');
2440         $allcaps = array();
2441         foreach ($rs as $capability) {
2442             $capability->riskbitmask = (int) $capability->riskbitmask;
2443             $allcaps[$capability->name] = (array) $capability;
2444         }
2445         $rs->close();
2446         $cache->set('core_capabilities', $allcaps);
2447     }
2448     return $allcaps;
2451 /**
2452  * Returns the human-readable, translated version of the capability.
2453  * Basically a big switch statement.
2454  *
2455  * @param string $capabilityname e.g. mod/choice:readresponses
2456  * @return string
2457  */
2458 function get_capability_string($capabilityname) {
2460     // Typical capability name is 'plugintype/pluginname:capabilityname'
2461     list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
2463     if ($type === 'moodle') {
2464         $component = 'core_role';
2465     } else if ($type === 'quizreport') {
2466         //ugly hack!!
2467         $component = 'quiz_'.$name;
2468     } else {
2469         $component = $type.'_'.$name;
2470     }
2472     $stringname = $name.':'.$capname;
2474     if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
2475         return get_string($stringname, $component);
2476     }
2478     $dir = core_component::get_component_directory($component);
2479     if (!file_exists($dir)) {
2480         // plugin broken or does not exist, do not bother with printing of debug message
2481         return $capabilityname.' ???';
2482     }
2484     // something is wrong in plugin, better print debug
2485     return get_string($stringname, $component);
2488 /**
2489  * This gets the mod/block/course/core etc strings.
2490  *
2491  * @param string $component
2492  * @param int $contextlevel
2493  * @return string|bool String is success, false if failed
2494  */
2495 function get_component_string($component, $contextlevel) {
2497     if ($component === 'moodle' or $component === 'core') {
2498         switch ($contextlevel) {
2499             // TODO MDL-46123: this should probably use context level names instead
2500             case CONTEXT_SYSTEM:    return get_string('coresystem');
2501             case CONTEXT_USER:      return get_string('users');
2502             case CONTEXT_COURSECAT: return get_string('categories');
2503             case CONTEXT_COURSE:    return get_string('course');
2504             case CONTEXT_MODULE:    return get_string('activities');
2505             case CONTEXT_BLOCK:     return get_string('block');
2506             default:                print_error('unknowncontext');
2507         }
2508     }
2510     list($type, $name) = core_component::normalize_component($component);
2511     $dir = core_component::get_plugin_directory($type, $name);
2512     if (!file_exists($dir)) {
2513         // plugin not installed, bad luck, there is no way to find the name
2514         return $component.' ???';
2515     }
2517     switch ($type) {
2518         // TODO MDL-46123: this is really hacky and should be improved.
2519         case 'quiz':         return get_string($name.':componentname', $component);// insane hack!!!
2520         case 'repository':   return get_string('repository', 'repository').': '.get_string('pluginname', $component);
2521         case 'gradeimport':  return get_string('gradeimport', 'grades').': '.get_string('pluginname', $component);
2522         case 'gradeexport':  return get_string('gradeexport', 'grades').': '.get_string('pluginname', $component);
2523         case 'gradereport':  return get_string('gradereport', 'grades').': '.get_string('pluginname', $component);
2524         case 'webservice':   return get_string('webservice', 'webservice').': '.get_string('pluginname', $component);
2525         case 'block':        return get_string('block').': '.get_string('pluginname', basename($component));
2526         case 'mod':
2527             if (get_string_manager()->string_exists('pluginname', $component)) {
2528                 return get_string('activity').': '.get_string('pluginname', $component);
2529             } else {
2530                 return get_string('activity').': '.get_string('modulename', $component);
2531             }
2532         default: return get_string('pluginname', $component);
2533     }
2536 /**
2537  * Gets the list of roles assigned to this context and up (parents)
2538  * from the aggregation of:
2539  * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
2540  * b) if applicable, those roles that are assigned in the context.
2541  *
2542  * @param context $context
2543  * @return array
2544  */
2545 function get_profile_roles(context $context) {
2546     global $CFG, $DB;
2547     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2548     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2549     if (has_capability('moodle/role:assign', $context)) {
2550         $rolesinscope = array_keys(get_all_roles($context));
2551     } else {
2552         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2553     }
2555     if (empty($rolesinscope)) {
2556         return [];
2557     }
2559     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2560     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2561     $params = array_merge($params, $cparams);
2563     if ($coursecontext = $context->get_course_context(false)) {
2564         $params['coursecontext'] = $coursecontext->id;
2565     } else {
2566         $params['coursecontext'] = 0;
2567     }
2569     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2570               FROM {role_assignments} ra, {role} r
2571          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2572              WHERE r.id = ra.roleid
2573                    AND ra.contextid $contextlist
2574                    AND r.id $rallowed
2575           ORDER BY r.sortorder ASC";
2577     return $DB->get_records_sql($sql, $params);
2580 /**
2581  * Gets the list of roles assigned to this context and up (parents)
2582  *
2583  * @param context $context
2584  * @param boolean $includeparents, false means without parents.
2585  * @return array
2586  */
2587 function get_roles_used_in_context(context $context, $includeparents = true) {
2588     global $DB;
2590     if ($includeparents === true) {
2591         list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
2592     } else {
2593         list($contextlist, $params) = $DB->get_in_or_equal($context->id, SQL_PARAMS_NAMED, 'cl');
2594     }
2596     if ($coursecontext = $context->get_course_context(false)) {
2597         $params['coursecontext'] = $coursecontext->id;
2598     } else {
2599         $params['coursecontext'] = 0;
2600     }
2602     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2603               FROM {role_assignments} ra, {role} r
2604          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2605              WHERE r.id = ra.roleid
2606                    AND ra.contextid $contextlist
2607           ORDER BY r.sortorder ASC";
2609     return $DB->get_records_sql($sql, $params);
2612 /**
2613  * This function is used to print roles column in user profile page.
2614  * It is using the CFG->profileroles to limit the list to only interesting roles.
2615  * (The permission tab has full details of user role assignments.)
2616  *
2617  * @param int $userid
2618  * @param int $courseid
2619  * @return string
2620  */
2621 function get_user_roles_in_course($userid, $courseid) {
2622     global $CFG, $DB;
2623     if ($courseid == SITEID) {
2624         $context = context_system::instance();
2625     } else {
2626         $context = context_course::instance($courseid);
2627     }
2628     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2629     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2630     if (has_capability('moodle/role:assign', $context)) {
2631         $rolesinscope = array_keys(get_all_roles($context));
2632     } else {
2633         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2634     }
2635     if (empty($rolesinscope)) {
2636         return '';
2637     }
2639     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2640     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2641     $params = array_merge($params, $cparams);
2643     if ($coursecontext = $context->get_course_context(false)) {
2644         $params['coursecontext'] = $coursecontext->id;
2645     } else {
2646         $params['coursecontext'] = 0;
2647     }
2649     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2650               FROM {role_assignments} ra, {role} r
2651          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2652              WHERE r.id = ra.roleid
2653                    AND ra.contextid $contextlist
2654                    AND r.id $rallowed
2655                    AND ra.userid = :userid
2656           ORDER BY r.sortorder ASC";
2657     $params['userid'] = $userid;
2659     $rolestring = '';
2661     if ($roles = $DB->get_records_sql($sql, $params)) {
2662         $viewableroles = get_viewable_roles($context, $userid);
2664         $rolenames = array();
2665         foreach ($roles as $roleid => $unused) {
2666             if (isset($viewableroles[$roleid])) {
2667                 $url = new moodle_url('/user/index.php', ['contextid' => $context->id, 'roleid' => $roleid]);
2668                 $rolenames[] = '<a href="' . $url . '">' . $viewableroles[$roleid] . '</a>';
2669             }
2670         }
2671         $rolestring = implode(',', $rolenames);
2672     }
2674     return $rolestring;
2677 /**
2678  * Checks if a user can assign users to a particular role in this context
2679  *
2680  * @param context $context
2681  * @param int $targetroleid - the id of the role you want to assign users to
2682  * @return boolean
2683  */
2684 function user_can_assign(context $context, $targetroleid) {
2685     global $DB;
2687     // First check to see if the user is a site administrator.
2688     if (is_siteadmin()) {
2689         return true;
2690     }
2692     // Check if user has override capability.
2693     // If not return false.
2694     if (!has_capability('moodle/role:assign', $context)) {
2695         return false;
2696     }
2697     // pull out all active roles of this user from this context(or above)
2698     if ($userroles = get_user_roles($context)) {
2699         foreach ($userroles as $userrole) {
2700             // if any in the role_allow_override table, then it's ok
2701             if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
2702                 return true;
2703             }
2704         }
2705     }
2707     return false;
2710 /**
2711  * Returns all site roles in correct sort order.
2712  *
2713  * Note: this method does not localise role names or descriptions,
2714  *       use role_get_names() if you need role names.
2715  *
2716  * @param context $context optional context for course role name aliases
2717  * @return array of role records with optional coursealias property
2718  */
2719 function get_all_roles(context $context = null) {
2720     global $DB;
2722     if (!$context or !$coursecontext = $context->get_course_context(false)) {
2723         $coursecontext = null;
2724     }
2726     if ($coursecontext) {
2727         $sql = "SELECT r.*, rn.name AS coursealias
2728                   FROM {role} r
2729              LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2730               ORDER BY r.sortorder ASC";
2731         return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
2733     } else {
2734         return $DB->get_records('role', array(), 'sortorder ASC');
2735     }
2738 /**
2739  * Returns roles of a specified archetype
2740  *
2741  * @param string $archetype
2742  * @return array of full role records
2743  */
2744 function get_archetype_roles($archetype) {
2745     global $DB;
2746     return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
2749 /**
2750  * Gets all the user roles assigned in this context, or higher contexts for a list of users.
2751  *
2752  * If you try using the combination $userids = [], $checkparentcontexts = true then this is likely
2753  * to cause an out-of-memory error on large Moodle sites, so this combination is deprecated and
2754  * outputs a warning, even though it is the default.
2755  *
2756  * @param context $context
2757  * @param array $userids. An empty list means fetch all role assignments for the context.
2758  * @param bool $checkparentcontexts defaults to true
2759  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2760  * @return array
2761  */
2762 function get_users_roles(context $context, $userids = [], $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2763     global $DB;
2765     if (!$userids && $checkparentcontexts) {
2766         debugging('Please do not call get_users_roles() with $checkparentcontexts = true ' .
2767                 'and $userids array not set. This combination causes large Moodle sites ' .
2768                 'with lots of site-wide role assignemnts to run out of memory.', DEBUG_DEVELOPER);
2769     }
2771     if ($checkparentcontexts) {
2772         $contextids = $context->get_parent_context_ids();
2773     } else {
2774         $contextids = array();
2775     }
2776     $contextids[] = $context->id;
2778     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
2780     // If userids was passed as an empty array, we fetch all role assignments for the course.
2781     if (empty($userids)) {
2782         $useridlist = ' IS NOT NULL ';
2783         $uparams = [];
2784     } else {
2785         list($useridlist, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uids');
2786     }
2788     $sql = "SELECT ra.*, r.name, r.shortname, ra.userid
2789               FROM {role_assignments} ra, {role} r, {context} c
2790              WHERE ra.userid $useridlist
2791                    AND ra.roleid = r.id
2792                    AND ra.contextid = c.id
2793                    AND ra.contextid $contextids
2794           ORDER BY $order";
2796     $all = $DB->get_records_sql($sql , array_merge($params, $uparams));
2798     // Return results grouped by userid.
2799     $result = [];
2800     foreach ($all as $id => $record) {
2801         if (!isset($result[$record->userid])) {
2802             $result[$record->userid] = [];
2803         }
2804         $result[$record->userid][$record->id] = $record;
2805     }
2807     // Make sure all requested users are included in the result, even if they had no role assignments.
2808     foreach ($userids as $id) {
2809         if (!isset($result[$id])) {
2810             $result[$id] = [];
2811         }
2812     }
2814     return $result;
2818 /**
2819  * Gets all the user roles assigned in this context, or higher contexts
2820  * this is mainly used when checking if a user can assign a role, or overriding a role
2821  * i.e. we need to know what this user holds, in order to verify against allow_assign and
2822  * allow_override tables
2823  *
2824  * @param context $context
2825  * @param int $userid
2826  * @param bool $checkparentcontexts defaults to true
2827  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2828  * @return array
2829  */
2830 function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2831     global $USER, $DB;
2833     if (empty($userid)) {
2834         if (empty($USER->id)) {
2835             return array();
2836         }
2837         $userid = $USER->id;
2838     }
2840     if ($checkparentcontexts) {
2841         $contextids = $context->get_parent_context_ids();
2842     } else {
2843         $contextids = array();
2844     }
2845     $contextids[] = $context->id;
2847     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
2849     array_unshift($params, $userid);
2851     $sql = "SELECT ra.*, r.name, r.shortname
2852               FROM {role_assignments} ra, {role} r, {context} c
2853              WHERE ra.userid = ?
2854                    AND ra.roleid = r.id
2855                    AND ra.contextid = c.id
2856                    AND ra.contextid $contextids
2857           ORDER BY $order";
2859     return $DB->get_records_sql($sql ,$params);
2862 /**
2863  * Like get_user_roles, but adds in the authenticated user role, and the front
2864  * page roles, if applicable.
2865  *
2866  * @param context $context the context.
2867  * @param int $userid optional. Defaults to $USER->id
2868  * @return array of objects with fields ->userid, ->contextid and ->roleid.
2869  */
2870 function get_user_roles_with_special(context $context, $userid = 0) {
2871     global $CFG, $USER;
2873     if (empty($userid)) {
2874         if (empty($USER->id)) {
2875             return array();
2876         }
2877         $userid = $USER->id;
2878     }
2880     $ras = get_user_roles($context, $userid);
2882     // Add front-page role if relevant.
2883     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
2884     $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
2885             is_inside_frontpage($context);
2886     if ($defaultfrontpageroleid && $isfrontpage) {
2887         $frontpagecontext = context_course::instance(SITEID);
2888         $ra = new stdClass();
2889         $ra->userid = $userid;
2890         $ra->contextid = $frontpagecontext->id;
2891         $ra->roleid = $defaultfrontpageroleid;
2892         $ras[] = $ra;
2893     }
2895     // Add authenticated user role if relevant.
2896     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
2897     if ($defaultuserroleid && !isguestuser($userid)) {
2898         $systemcontext = context_system::instance();
2899         $ra = new stdClass();
2900         $ra->userid = $userid;
2901         $ra->contextid = $systemcontext->id;
2902         $ra->roleid = $defaultuserroleid;
2903         $ras[] = $ra;
2904     }
2906     return $ras;
2909 /**
2910  * Creates a record in the role_allow_override table
2911  *
2912  * @param int $fromroleid source roleid
2913  * @param int $targetroleid target roleid
2914  * @return void
2915  */
2916 function core_role_set_override_allowed($fromroleid, $targetroleid) {
2917     global $DB;
2919     $record = new stdClass();
2920     $record->roleid        = $fromroleid;
2921     $record->allowoverride = $targetroleid;
2922     $DB->insert_record('role_allow_override', $record);
2925 /**
2926  * Creates a record in the role_allow_assign table
2927  *
2928  * @param int $fromroleid source roleid
2929  * @param int $targetroleid target roleid
2930  * @return void
2931  */
2932 function core_role_set_assign_allowed($fromroleid, $targetroleid) {
2933     global $DB;
2935     $record = new stdClass();
2936     $record->roleid      = $fromroleid;
2937     $record->allowassign = $targetroleid;
2938     $DB->insert_record('role_allow_assign', $record);
2941 /**
2942  * Creates a record in the role_allow_switch table
2943  *
2944  * @param int $fromroleid source roleid
2945  * @param int $targetroleid target roleid
2946  * @return void
2947  */
2948 function core_role_set_switch_allowed($fromroleid, $targetroleid) {
2949     global $DB;
2951     $record = new stdClass();
2952     $record->roleid      = $fromroleid;
2953     $record->allowswitch = $targetroleid;
2954     $DB->insert_record('role_allow_switch', $record);
2957 /**
2958  * Creates a record in the role_allow_view table
2959  *
2960  * @param int $fromroleid source roleid
2961  * @param int $targetroleid target roleid
2962  * @return void
2963  */
2964 function core_role_set_view_allowed($fromroleid, $targetroleid) {
2965     global $DB;
2967     $record = new stdClass();
2968     $record->roleid      = $fromroleid;
2969     $record->allowview = $targetroleid;
2970     $DB->insert_record('role_allow_view', $record);
2973 /**
2974  * Gets a list of roles that this user can assign in this context
2975  *
2976  * @param context $context the context.
2977  * @param int $rolenamedisplay the type of role name to display. One of the
2978  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
2979  * @param bool $withusercounts if true, count the number of users with each role.
2980  * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
2981  * @return array if $withusercounts is false, then an array $roleid => $rolename.
2982  *      if $withusercounts is true, returns a list of three arrays,
2983  *      $rolenames, $rolecounts, and $nameswithcounts.
2984  */
2985 function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
2986     global $USER, $DB;
2988     // make sure there is a real user specified
2989     if ($user === null) {
2990         $userid = isset($USER->id) ? $USER->id : 0;
2991     } else {
2992         $userid = is_object($user) ? $user->id : $user;
2993     }
2995     if (!has_capability('moodle/role:assign', $context, $userid)) {
2996         if ($withusercounts) {
2997             return array(array(), array(), array());
2998         } else {
2999             return array();
3000         }
3001     }
3003     $params = array();
3004     $extrafields = '';
3006     if ($withusercounts) {
3007         $extrafields = ', (SELECT count(u.id)
3008                              FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
3009                             WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
3010                           ) AS usercount';
3011         $params['conid'] = $context->id;
3012     }
3014     if (is_siteadmin($userid)) {
3015         // show all roles allowed in this context to admins
3016         $assignrestriction = "";
3017     } else {
3018         $parents = $context->get_parent_context_ids(true);
3019         $contexts = implode(',' , $parents);
3020         $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
3021                                       FROM {role_allow_assign} raa
3022                                       JOIN {role_assignments} ra ON ra.roleid = raa.roleid
3023                                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3024                                    ) ar ON ar.id = r.id";
3025         $params['userid'] = $userid;
3026     }
3027     $params['contextlevel'] = $context->contextlevel;
3029     if ($coursecontext = $context->get_course_context(false)) {
3030         $params['coursecontext'] = $coursecontext->id;
3031     } else {
3032         $params['coursecontext'] = 0; // no course aliases
3033         $coursecontext = null;
3034     }
3035     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
3036               FROM {role} r
3037               $assignrestriction
3038               JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
3039          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3040           ORDER BY r.sortorder ASC";
3041     $roles = $DB->get_records_sql($sql, $params);
3043     $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
3045     if (!$withusercounts) {
3046         return $rolenames;
3047     }
3049     $rolecounts = array();
3050     $nameswithcounts = array();
3051     foreach ($roles as $role) {
3052         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
3053         $rolecounts[$role->id] = $roles[$role->id]->usercount;
3054     }
3055     return array($rolenames, $rolecounts, $nameswithcounts);
3058 /**
3059  * Gets a list of roles that this user can switch to in a context
3060  *
3061  * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
3062  * This function just process the contents of the role_allow_switch table. You also need to
3063  * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
3064  *
3065  * @param context $context a context.
3066  * @return array an array $roleid => $rolename.
3067  */
3068 function get_switchable_roles(context $context) {
3069     global $USER, $DB;
3071     // You can't switch roles without this capability.
3072     if (!has_capability('moodle/role:switchroles', $context)) {
3073         return [];
3074     }
3076     $params = array();
3077     $extrajoins = '';
3078     $extrawhere = '';
3079     if (!is_siteadmin()) {
3080         // Admins are allowed to switch to any role with.
3081         // Others are subject to the additional constraint that the switch-to role must be allowed by
3082         // 'role_allow_switch' for some role they have assigned in this context or any parent.
3083         $parents = $context->get_parent_context_ids(true);
3084         $contexts = implode(',' , $parents);
3086         $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
3087         JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3088         $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
3089         $params['userid'] = $USER->id;
3090     }
3092     if ($coursecontext = $context->get_course_context(false)) {
3093         $params['coursecontext'] = $coursecontext->id;
3094     } else {
3095         $params['coursecontext'] = 0; // no course aliases
3096         $coursecontext = null;
3097     }
3099     $query = "
3100         SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3101           FROM (SELECT DISTINCT rc.roleid
3102                   FROM {role_capabilities} rc
3103                   $extrajoins
3104                   $extrawhere) idlist
3105           JOIN {role} r ON r.id = idlist.roleid
3106      LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3107       ORDER BY r.sortorder";
3108     $roles = $DB->get_records_sql($query, $params);
3110     return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
3113 /**
3114  * Gets a list of roles that this user can view in a context
3115  *
3116  * @param context $context a context.
3117  * @param int $userid id of user.
3118  * @return array an array $roleid => $rolename.
3119  */
3120 function get_viewable_roles(context $context, $userid = null) {
3121     global $USER, $DB;
3123     if ($userid == null) {
3124         $userid = $USER->id;
3125     }
3127     $params = array();
3128     $extrajoins = '';
3129     $extrawhere = '';
3130     if (!is_siteadmin()) {
3131         // Admins are allowed to view any role.
3132         // Others are subject to the additional constraint that the view role must be allowed by
3133         // 'role_allow_view' for some role they have assigned in this context or any parent.
3134         $contexts = $context->get_parent_context_ids(true);
3135         list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
3137         $extrajoins = "JOIN {role_allow_view} ras ON ras.allowview = r.id
3138                        JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3139         $extrawhere = "WHERE ra.userid = :userid AND ra.contextid $insql";
3141         $params += $inparams;
3142         $params['userid'] = $userid;
3143     }
3145     if ($coursecontext = $context->get_course_context(false)) {
3146         $params['coursecontext'] = $coursecontext->id;
3147     } else {
3148         $params['coursecontext'] = 0; // No course aliases.
3149         $coursecontext = null;
3150     }
3152     $query = "
3153         SELECT r.id, r.name, r.shortname, rn.name AS coursealias, r.sortorder
3154           FROM {role} r
3155           $extrajoins
3156      LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3157           $extrawhere
3158       GROUP BY r.id, r.name, r.shortname, rn.name, r.sortorder
3159       ORDER BY r.sortorder";
3160     $roles = $DB->get_records_sql($query, $params);
3162     return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
3165 /**
3166  * Gets a list of roles that this user can override in this context.
3167  *
3168  * @param context $context the context.
3169  * @param int $rolenamedisplay the type of role name to display. One of the
3170  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3171  * @param bool $withcounts if true, count the number of overrides that are set for each role.
3172  * @return array if $withcounts is false, then an array $roleid => $rolename.
3173  *      if $withusercounts is true, returns a list of three arrays,
3174  *      $rolenames, $rolecounts, and $nameswithcounts.
3175  */
3176 function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
3177     global $USER, $DB;
3179     if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
3180         if ($withcounts) {
3181             return array(array(), array(), array());
3182         } else {
3183             return array();
3184         }
3185     }
3187     $parents = $context->get_parent_context_ids(true);
3188     $contexts = implode(',' , $parents);
3190     $params = array();
3191     $extrafields = '';
3193     $params['userid'] = $USER->id;
3194     if ($withcounts) {
3195         $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
3196                 WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
3197         $params['conid'] = $context->id;
3198     }
3200     if ($coursecontext = $context->get_course_context(false)) {
3201         $params['coursecontext'] = $coursecontext->id;
3202     } else {
3203         $params['coursecontext'] = 0; // no course aliases
3204         $coursecontext = null;
3205     }
3207     if (is_siteadmin()) {
3208         // show all roles to admins
3209         $roles = $DB->get_records_sql("
3210             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3211               FROM {role} ro
3212          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3213           ORDER BY ro.sortorder ASC", $params);
3215     } else {
3216         $roles = $DB->get_records_sql("
3217             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3218               FROM {role} ro
3219               JOIN (SELECT DISTINCT r.id
3220                       FROM {role} r
3221                       JOIN {role_allow_override} rao ON r.id = rao.allowoverride
3222                       JOIN {role_assignments} ra ON rao.roleid = ra.roleid
3223                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3224                    ) inline_view ON ro.id = inline_view.id
3225          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3226           ORDER BY ro.sortorder ASC", $params);
3227     }
3229     $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
3231     if (!$withcounts) {
3232         return $rolenames;
3233     }
3235     $rolecounts = array();
3236     $nameswithcounts = array();
3237     foreach ($roles as $role) {
3238         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
3239         $rolecounts[$role->id] = $roles[$role->id]->overridecount;
3240     }
3241     return array($rolenames, $rolecounts, $nameswithcounts);
3244 /**
3245  * Create a role menu suitable for default role selection in enrol plugins.
3246  *
3247  * @package    core_enrol
3248  *
3249  * @param context $context
3250  * @param int $addroleid current or default role - always added to list
3251  * @return array roleid=>localised role name
3252  */
3253 function get_default_enrol_roles(context $context, $addroleid = null) {
3254     global $DB;
3256     $params = array('contextlevel'=>CONTEXT_COURSE);
3258     if ($coursecontext = $context->get_course_context(false)) {
3259         $params['coursecontext'] = $coursecontext->id;
3260     } else {
3261         $params['coursecontext'] = 0; // no course names
3262         $coursecontext = null;
3263     }
3265     if ($addroleid) {
3266         $addrole = "OR r.id = :addroleid";
3267         $params['addroleid'] = $addroleid;
3268     } else {
3269         $addrole = "";
3270     }
3272     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3273               FROM {role} r
3274          LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
3275          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3276              WHERE rcl.id IS NOT NULL $addrole
3277           ORDER BY sortorder DESC";
3279     $roles = $DB->get_records_sql($sql, $params);
3281     return role_fix_names($roles, $context, ROLENAME_BOTH, true);
3284 /**
3285  * Return context levels where this role is assignable.
3286  *
3287  * @param integer $roleid the id of a role.
3288  * @return array list of the context levels at which this role may be assigned.
3289  */
3290 function get_role_contextlevels($roleid) {
3291     global $DB;
3292     return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
3293             'contextlevel', 'id,contextlevel');
3296 /**
3297  * Return roles suitable for assignment at the specified context level.
3298  *
3299  * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
3300  *
3301  * @param integer $contextlevel a contextlevel.
3302  * @return array list of role ids that are assignable at this context level.
3303  */
3304 function get_roles_for_contextlevels($contextlevel) {
3305     global $DB;
3306     return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
3307             '', 'id,roleid');
3310 /**
3311  * Returns default context levels where roles can be assigned.
3312  *
3313  * @param string $rolearchetype one of the role archetypes - that is, one of the keys
3314  *      from the array returned by get_role_archetypes();
3315  * @return array list of the context levels at which this type of role may be assigned by default.
3316  */
3317 function get_default_contextlevels($rolearchetype) {
3318     static $defaults = array(
3319         'manager'        => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
3320         'coursecreator'  => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
3321         'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3322         'teacher'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3323         'student'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3324         'guest'          => array(),
3325         'user'           => array(),
3326         'frontpage'      => array());
3328     if (isset($defaults[$rolearchetype])) {
3329         return $defaults[$rolearchetype];
3330     } else {
3331         return array();
3332     }
3335 /**
3336  * Set the context levels at which a particular role can be assigned.
3337  * Throws exceptions in case of error.
3338  *
3339  * @param integer $roleid the id of a role.
3340  * @param array $contextlevels the context levels at which this role should be assignable,
3341  *      duplicate levels are removed.
3342  * @return void
3343  */
3344 function set_role_contextlevels($roleid, array $contextlevels) {
3345     global $DB;
3346     $DB->delete_records('role_context_levels', array('roleid' => $roleid));
3347     $rcl = new stdClass();
3348     $rcl->roleid = $roleid;
3349     $contextlevels = array_unique($contextlevels);
3350     foreach ($contextlevels as $level) {
3351         $rcl->contextlevel = $level;
3352         $DB->insert_record('role_context_levels', $rcl, false, true);
3353     }
3356 /**
3357  * Who has this capability in this context?
3358  *
3359  * This can be a very expensive call - use sparingly and keep
3360  * the results if you are going to need them again soon.
3361  *
3362  * Note if $fields is empty this function attempts to get u.*
3363  * which can get rather large - and has a serious perf impact
3364  * on some DBs.
3365  *
3366  * @param context $context
3367  * @param string|array $capability - capability name(s)
3368  * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
3369  * @param string $sort - the sort order. Default is lastaccess time.
3370  * @param mixed $limitfrom - number of records to skip (offset)
3371  * @param mixed $limitnum - number of records to fetch
3372  * @param string|array $groups - single group or array of groups - only return
3373  *               users who are in one of these group(s).
3374  * @param string|array $exceptions - list of users to exclude, comma separated or array
3375  * @param bool $doanything_ignored not used any more, admin accounts are never returned
3376  * @param bool $view_ignored - use get_enrolled_sql() instead
3377  * @param bool $useviewallgroups if $groups is set the return users who
3378  *               have capability both $capability and moodle/site:accessallgroups
3379  *               in this context, as well as users who have $capability and who are
3380  *               in $groups.
3381  * @return array of user records
3382  */
3383 function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
3384                                  $groups = '', $exceptions = '', $doanything_ignored = null, $view_ignored = null, $useviewallgroups = false) {
3385     global $CFG, $DB;
3387     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3388     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3390     $ctxids = trim($context->path, '/');
3391     $ctxids = str_replace('/', ',', $ctxids);
3393     // Context is the frontpage
3394     $iscoursepage = false; // coursepage other than fp
3395     $isfrontpage = false;
3396     if ($context->contextlevel == CONTEXT_COURSE) {
3397         if ($context->instanceid == SITEID) {
3398             $isfrontpage = true;
3399         } else {
3400             $iscoursepage = true;
3401         }
3402     }
3403     $isfrontpage = ($isfrontpage || is_inside_frontpage($context));
3405     $caps = (array)$capability;
3407     // construct list of context paths bottom-->top
3408     list($contextids, $paths) = get_context_info_list($context);
3410     // we need to find out all roles that have these capabilities either in definition or in overrides
3411     $defs = array();
3412     list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
3413     list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, 'cap');
3414     $params = array_merge($params, $params2);
3415     $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3416               FROM {role_capabilities} rc
3417               JOIN {context} ctx on rc.contextid = ctx.id
3418              WHERE rc.contextid $incontexts AND rc.capability $incaps";
3420     $rcs = $DB->get_records_sql($sql, $params);
3421     foreach ($rcs as $rc) {
3422         $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3423     }
3425     // go through the permissions bottom-->top direction to evaluate the current permission,
3426     // first one wins (prohibit is an exception that always wins)
3427     $access = array();
3428     foreach ($caps as $cap) {
3429         foreach ($paths as $path) {
3430             if (empty($defs[$cap][$path])) {
3431                 continue;
3432             }
3433             foreach($defs[$cap][$path] as $roleid => $perm) {
3434                 if ($perm == CAP_PROHIBIT) {
3435                     $access[$cap][$roleid] = CAP_PROHIBIT;
3436                     continue;
3437                 }
3438                 if (!isset($access[$cap][$roleid])) {
3439                     $access[$cap][$roleid] = (int)$perm;
3440                 }
3441             }
3442         }
3443     }
3445     // make lists of roles that are needed and prohibited in this context
3446     $needed = array(); // one of these is enough
3447     $prohibited = array(); // must not have any of these
3448     foreach ($caps as $cap) {
3449         if (empty($access[$cap])) {
3450             continue;
3451         }
3452         foreach ($access[$cap] as $roleid => $perm) {
3453             if ($perm == CAP_PROHIBIT) {
3454                 unset($needed[$cap][$roleid]);
3455                 $prohibited[$cap][$roleid] = true;
3456             } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
3457                 $needed[$cap][$roleid] = true;
3458             }
3459         }
3460         if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
3461             // easy, nobody has the permission
3462             unset($needed[$cap]);
3463             unset($prohibited[$cap]);
3464         } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
3465             // everybody is disqualified on the frontpage
3466             unset($needed[$cap]);
3467             unset($prohibited[$cap]);
3468         }
3469         if (empty($prohibited[$cap])) {
3470             unset($prohibited[$cap]);
3471         }
3472     }
3474     if (empty($needed)) {
3475         // there can not be anybody if no roles match this request
3476         return array();
3477     }
3479     if (empty($prohibited)) {
3480         // we can compact the needed roles
3481         $n = array();
3482         foreach ($needed as $cap) {
3483             foreach ($cap as $roleid=>$unused) {
3484                 $n[$roleid] = true;
3485             }
3486         }
3487         $needed = array('any'=>$n);
3488         unset($n);
3489     }
3491     // ***** Set up default fields ******
3492     if (empty($fields)) {
3493         if ($iscoursepage) {
3494             $fields = 'u.*, ul.timeaccess AS lastaccess';
3495         } else {
3496             $fields = 'u.*';
3497         }
3498     } else {
3499         if ($CFG->debugdeveloper && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
3500             debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
3501         }
3502     }
3504     // Set up default sort
3505     if (empty($sort)) { // default to course lastaccess or just lastaccess
3506         if ($iscoursepage) {
3507             $sort = 'ul.timeaccess';
3508         } else {
3509             $sort = 'u.lastaccess';
3510         }
3511     }
3513     // Prepare query clauses
3514     $wherecond = array();
3515     $params    = array();
3516     $joins     = array();
3518     // User lastaccess JOIN
3519     if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
3520          // user_lastaccess is not required MDL-13810
3521     } else {
3522         if ($iscoursepage) {
3523             $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
3524         } else {
3525             throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
3526         }
3527     }
3529     // We never return deleted users or guest account.
3530     $wherecond[] = "u.deleted = 0 AND u.id <> :guestid";
3531     $params['guestid'] = $CFG->siteguest;
3533     // Groups
3534     if ($groups) {
3535         $groups = (array)$groups;
3536         list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
3537         $grouptest = "u.id IN (SELECT userid FROM {groups_members} gm WHERE gm.groupid $grouptest)";
3538         $params = array_merge($params, $grpparams);
3540         if ($useviewallgroups) {
3541             $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
3542             if (!empty($viewallgroupsusers)) {
3543                 $wherecond[] =  "($grouptest OR u.id IN (" . implode(',', array_keys($viewallgroupsusers)) . '))';
3544             } else {
3545                 $wherecond[] =  "($grouptest)";
3546             }
3547         } else {
3548             $wherecond[] =  "($grouptest)";
3549         }
3550     }
3552     // User exceptions
3553     if (!empty($exceptions)) {
3554         $exceptions = (array)$exceptions;
3555         list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc', false);
3556         $params = array_merge($params, $exparams);
3557         $wherecond[] = "u.id $exsql";
3558     }
3560     // now add the needed and prohibited roles conditions as joins
3561     if (!empty($needed['any'])) {
3562         // simple case - there are no prohibits involved
3563         if (!empty($needed['any'][$defaultuserroleid]) or ($isfrontpage and !empty($needed['any'][$defaultfrontpageroleid]))) {
3564             // everybody
3565         } else {
3566             $joins[] = "JOIN (SELECT DISTINCT userid
3567                                 FROM {role_assignments}
3568                                WHERE contextid IN ($ctxids)
3569                                      AND roleid IN (".implode(',', array_keys($needed['any'])) .")
3570                              ) ra ON ra.userid = u.id";
3571         }
3572     } else {
3573         $unions = array();
3574         $everybody = false;
3575         foreach ($needed as $cap=>$unused) {
3576             if (empty($prohibited[$cap])) {
3577                 if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
3578                     $everybody = true;
3579                     break;
3580                 } else {
3581                     $unions[] = "SELECT userid
3582                                    FROM {role_assignments}
3583                                   WHERE contextid IN ($ctxids)
3584                                         AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
3585                 }
3586             } else {
3587                 if (!empty($prohibited[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
3588                     // nobody can have this cap because it is prevented in default roles
3589                     continue;
3591                 } else if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
3592                     // everybody except the prohibitted - hiding does not matter
3593                     $unions[] = "SELECT id AS userid
3594                                    FROM {user}
3595                                   WHERE id NOT IN (SELECT userid
3596                                                      FROM {role_assignments}
3597                                                     WHERE contextid IN ($ctxids)
3598                                                           AND roleid IN (".implode(',', array_keys($prohibited[$cap])) ."))";
3600                 } else {
3601                     $unions[] = "SELECT userid
3602                                    FROM {role_assignments}
3603                                   WHERE contextid IN ($ctxids) AND roleid IN (".implode(',', array_keys($needed[$cap])) .")
3604                                         AND userid NOT IN (
3605                                             SELECT userid
3606                                               FROM {role_assignments}
3607                                              WHERE contextid IN ($ctxids)
3608                                                     AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . ")
3609                                                         )";
3610                 }
3611             }
3612         }
3613         if (!$everybody) {
3614             if ($unions) {
3615                 $joins[] = "JOIN (SELECT DISTINCT userid FROM ( ".implode(' UNION ', $unions)." ) us) ra ON ra.userid = u.id";
3616             } else {
3617                 // only prohibits found - nobody can be matched
3618                 $wherecond[] = "1 = 2";
3619             }
3620         }
3621     }
3623     // Collect WHERE conditions and needed joins
3624     $where = implode(' AND ', $wherecond);
3625     if ($where !== '') {
3626         $where = 'WHERE ' . $where;
3627     }
3628     $joins = implode("\n", $joins);
3630     // Ok, let's get the users!
3631     $sql = "SELECT $fields
3632               FROM {user} u
3633             $joins
3634             $where
3635           ORDER BY $sort";
3637     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3640 /**
3641  * Re-sort a users array based on a sorting policy
3642  *
3643  * Will re-sort a $users results array (from get_users_by_capability(), usually)
3644  * based on a sorting policy. This is to support the odd practice of
3645  * sorting teachers by 'authority', where authority was "lowest id of the role
3646  * assignment".
3647  *
3648  * Will execute 1 database query. Only suitable for small numbers of users, as it
3649  * uses an u.id IN() clause.
3650  *
3651  * Notes about the sorting criteria.
3652  *
3653  * As a default, we cannot rely on role.sortorder because then
3654  * admins/coursecreators will always win. That is why the sane
3655  * rule "is locality matters most", with sortorder as 2nd
3656  * consideration.
3657  *
3658  * If you want role.sortorder, use the 'sortorder' policy, and
3659  * name explicitly what roles you want to cover. It's probably
3660  * a good idea to see what roles have the capabilities you want
3661  * (array_diff() them against roiles that have 'can-do-anything'
3662  * to weed out admin-ish roles. Or fetch a list of roles from
3663  * variables like $CFG->coursecontact .
3664  *
3665  * @param array $users Users array, keyed on userid
3666  * @param context $context
3667  * @param array $roles ids of the roles to include, optional
3668  * @param string $sortpolicy defaults to locality, more about
3669  * @return array sorted copy of the array
3670  */
3671 function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') {
3672     global $DB;