Merge branch 'MDL-59811-master' of git://github.com/junpataleta/moodle
[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  *
66  * <b>accessdata</b>
67  *
68  * Access control data is held in the "accessdata" array
69  * which - for the logged-in user, will be in $USER->access
70  *
71  * For other users can be generated and passed around (but may also be cached
72  * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser).
73  *
74  * $accessdata is a multidimensional array, holding
75  * role assignments (RAs), role-capabilities-perm sets
76  * (role defs) and a list of courses we have loaded
77  * data for.
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->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
187 /**
188  * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
189  *
190  * This method should ONLY BE USED BY UNIT TESTS. It clears all of
191  * accesslib's private caches. You need to do this before setting up test data,
192  * and also at the end of the tests.
193  *
194  * @access private
195  * @return void
196  */
197 function accesslib_clear_all_caches_for_unit_testing() {
198     global $USER;
199     if (!PHPUNIT_TEST) {
200         throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
201     }
203     accesslib_clear_all_caches(true);
205     unset($USER->access);
208 /**
209  * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
210  *
211  * This reset does not touch global $USER.
212  *
213  * @access private
214  * @param bool $resetcontexts
215  * @return void
216  */
217 function accesslib_clear_all_caches($resetcontexts) {
218     global $ACCESSLIB_PRIVATE;
220     $ACCESSLIB_PRIVATE->dirtycontexts    = null;
221     $ACCESSLIB_PRIVATE->accessdatabyuser = array();
222     $ACCESSLIB_PRIVATE->cacheroledefs    = array();
224     $cache = cache::make('core', 'roledefs');
225     $cache->purge();
227     if ($resetcontexts) {
228         context_helper::reset_caches();
229     }
232 /**
233  * Clears accesslib's private cache of a specific role or roles. ONLY BE USED FROM THIS LIBRARY FILE!
234  *
235  * This reset does not touch global $USER.
236  *
237  * @access private
238  * @param int|array $roles
239  * @return void
240  */
241 function accesslib_clear_role_cache($roles) {
242     global $ACCESSLIB_PRIVATE;
244     if (!is_array($roles)) {
245         $roles = [$roles];
246     }
248     foreach ($roles as $role) {
249         if (isset($ACCESSLIB_PRIVATE->cacheroledefs[$role])) {
250             unset($ACCESSLIB_PRIVATE->cacheroledefs[$role]);
251         }
252     }
254     $cache = cache::make('core', 'roledefs');
255     $cache->delete_many($roles);
258 /**
259  * Role is assigned at system context.
260  *
261  * @access private
262  * @param int $roleid
263  * @return array
264  */
265 function get_role_access($roleid) {
266     $accessdata = get_empty_accessdata();
267     $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
268     return $accessdata;
271 /**
272  * Fetch raw "site wide" role definitions.
273  * Even MUC static acceleration cache appears a bit slow for this.
274  * Important as can be hit hundreds of times per page.
275  *
276  * @param array $roleids List of role ids to fetch definitions for.
277  * @return array Complete definition for each requested role.
278  */
279 function get_role_definitions(array $roleids) {
280     global $ACCESSLIB_PRIVATE;
282     if (empty($roleids)) {
283         return array();
284     }
286     // Grab all keys we have not yet got in our static cache.
287     if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
288         $cache = cache::make('core', 'roledefs');
289         $ACCESSLIB_PRIVATE->cacheroledefs += array_filter($cache->get_many($uncached));
291         // Check we have the remaining keys from the MUC.
292         if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
293             $uncached = get_role_definitions_uncached($uncached);
294             $ACCESSLIB_PRIVATE->cacheroledefs += $uncached;
295             $cache->set_many($uncached);
296         }
297     }
299     // Return just the roles we need.
300     return array_intersect_key($ACCESSLIB_PRIVATE->cacheroledefs, array_flip($roleids));
303 /**
304  * Query raw "site wide" role definitions.
305  *
306  * @param array $roleids List of role ids to fetch definitions for.
307  * @return array Complete definition for each requested role.
308  */
309 function get_role_definitions_uncached(array $roleids) {
310     global $DB;
312     if (empty($roleids)) {
313         return array();
314     }
316     list($sql, $params) = $DB->get_in_or_equal($roleids);
317     $rdefs = array();
319     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
320               FROM {role_capabilities} rc
321               JOIN {context} ctx ON rc.contextid = ctx.id
322              WHERE rc.roleid $sql
323           ORDER BY ctx.path, rc.roleid, rc.capability";
324     $rs = $DB->get_recordset_sql($sql, $params);
326     foreach ($rs as $rd) {
327         if (!isset($rdefs[$rd->roleid][$rd->path])) {
328             if (!isset($rdefs[$rd->roleid])) {
329                 $rdefs[$rd->roleid] = array();
330             }
331             $rdefs[$rd->roleid][$rd->path] = array();
332         }
333         $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
334     }
336     $rs->close();
337     return $rdefs;
340 /**
341  * Get the default guest role, this is used for guest account,
342  * search engine spiders, etc.
343  *
344  * @return stdClass role record
345  */
346 function get_guest_role() {
347     global $CFG, $DB;
349     if (empty($CFG->guestroleid)) {
350         if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
351             $guestrole = array_shift($roles);   // Pick the first one
352             set_config('guestroleid', $guestrole->id);
353             return $guestrole;
354         } else {
355             debugging('Can not find any guest role!');
356             return false;
357         }
358     } else {
359         if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
360             return $guestrole;
361         } else {
362             // somebody is messing with guest roles, remove incorrect setting and try to find a new one
363             set_config('guestroleid', '');
364             return get_guest_role();
365         }
366     }
369 /**
370  * Check whether a user has a particular capability in a given context.
371  *
372  * For example:
373  *      $context = context_module::instance($cm->id);
374  *      has_capability('mod/forum:replypost', $context)
375  *
376  * By default checks the capabilities of the current user, but you can pass a
377  * different userid. By default will return true for admin users, but you can override that with the fourth argument.
378  *
379  * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
380  * or capabilities with XSS, config or data loss risks.
381  *
382  * @category access
383  *
384  * @param string $capability the name of the capability to check. For example mod/forum:view
385  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
386  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
387  * @param boolean $doanything If false, ignores effect of admin role assignment
388  * @return boolean true if the user has this capability. Otherwise false.
389  */
390 function has_capability($capability, context $context, $user = null, $doanything = true) {
391     global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
393     if (during_initial_install()) {
394         if ($SCRIPT === "/$CFG->admin/index.php"
395                 or $SCRIPT === "/$CFG->admin/cli/install.php"
396                 or $SCRIPT === "/$CFG->admin/cli/install_database.php"
397                 or (defined('BEHAT_UTIL') and BEHAT_UTIL)
398                 or (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL)) {
399             // we are in an installer - roles can not work yet
400             return true;
401         } else {
402             return false;
403         }
404     }
406     if (strpos($capability, 'moodle/legacy:') === 0) {
407         throw new coding_exception('Legacy capabilities can not be used any more!');
408     }
410     if (!is_bool($doanything)) {
411         throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
412     }
414     // capability must exist
415     if (!$capinfo = get_capability_info($capability)) {
416         debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
417         return false;
418     }
420     if (!isset($USER->id)) {
421         // should never happen
422         $USER->id = 0;
423         debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
424     }
426     // make sure there is a real user specified
427     if ($user === null) {
428         $userid = $USER->id;
429     } else {
430         $userid = is_object($user) ? $user->id : $user;
431     }
433     // make sure forcelogin cuts off not-logged-in users if enabled
434     if (!empty($CFG->forcelogin) and $userid == 0) {
435         return false;
436     }
438     // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
439     if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
440         if (isguestuser($userid) or $userid == 0) {
441             return false;
442         }
443     }
445     // somehow make sure the user is not deleted and actually exists
446     if ($userid != 0) {
447         if ($userid == $USER->id and isset($USER->deleted)) {
448             // this prevents one query per page, it is a bit of cheating,
449             // but hopefully session is terminated properly once user is deleted
450             if ($USER->deleted) {
451                 return false;
452             }
453         } else {
454             if (!context_user::instance($userid, IGNORE_MISSING)) {
455                 // no user context == invalid userid
456                 return false;
457             }
458         }
459     }
461     // context path/depth must be valid
462     if (empty($context->path) or $context->depth == 0) {
463         // this should not happen often, each upgrade tries to rebuild the context paths
464         debugging('Context id '.$context->id.' does not have valid path, please use context_helper::build_all_paths()');
465         if (is_siteadmin($userid)) {
466             return true;
467         } else {
468             return false;
469         }
470     }
472     // Find out if user is admin - it is not possible to override the doanything in any way
473     // and it is not possible to switch to admin role either.
474     if ($doanything) {
475         if (is_siteadmin($userid)) {
476             if ($userid != $USER->id) {
477                 return true;
478             }
479             // make sure switchrole is not used in this context
480             if (empty($USER->access['rsw'])) {
481                 return true;
482             }
483             $parts = explode('/', trim($context->path, '/'));
484             $path = '';
485             $switched = false;
486             foreach ($parts as $part) {
487                 $path .= '/' . $part;
488                 if (!empty($USER->access['rsw'][$path])) {
489                     $switched = true;
490                     break;
491                 }
492             }
493             if (!$switched) {
494                 return true;
495             }
496             //ok, admin switched role in this context, let's use normal access control rules
497         }
498     }
500     // Careful check for staleness...
501     $context->reload_if_dirty();
503     if ($USER->id == $userid) {
504         if (!isset($USER->access)) {
505             load_all_capabilities();
506         }
507         $access =& $USER->access;
509     } else {
510         // make sure user accessdata is really loaded
511         get_user_accessdata($userid, true);
512         $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
513     }
515     return has_capability_in_accessdata($capability, $context, $access);
518 /**
519  * Check if the user has any one of several capabilities from a list.
520  *
521  * This is just a utility method that calls has_capability in a loop. Try to put
522  * the capabilities that most users are likely to have first in the list for best
523  * performance.
524  *
525  * @category access
526  * @see has_capability()
527  *
528  * @param array $capabilities an array of capability names.
529  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
530  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
531  * @param boolean $doanything If false, ignore effect of admin role assignment
532  * @return boolean true if the user has any of these capabilities. Otherwise false.
533  */
534 function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
535     foreach ($capabilities as $capability) {
536         if (has_capability($capability, $context, $user, $doanything)) {
537             return true;
538         }
539     }
540     return false;
543 /**
544  * Check if the user has all the capabilities in a list.
545  *
546  * This is just a utility method that calls has_capability in a loop. Try to put
547  * the capabilities that fewest users are likely to have first in the list for best
548  * performance.
549  *
550  * @category access
551  * @see has_capability()
552  *
553  * @param array $capabilities an array of capability names.
554  * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
555  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
556  * @param boolean $doanything If false, ignore effect of admin role assignment
557  * @return boolean true if the user has all of these capabilities. Otherwise false.
558  */
559 function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
560     foreach ($capabilities as $capability) {
561         if (!has_capability($capability, $context, $user, $doanything)) {
562             return false;
563         }
564     }
565     return true;
568 /**
569  * Is course creator going to have capability in a new course?
570  *
571  * This is intended to be used in enrolment plugins before or during course creation,
572  * do not use after the course is fully created.
573  *
574  * @category access
575  *
576  * @param string $capability the name of the capability to check.
577  * @param context $context course or category context where is course going to be created
578  * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
579  * @return boolean true if the user will have this capability.
580  *
581  * @throws coding_exception if different type of context submitted
582  */
583 function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
584     global $CFG;
586     if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
587         throw new coding_exception('Only course or course category context expected');
588     }
590     if (has_capability($capability, $context, $user)) {
591         // User already has the capability, it could be only removed if CAP_PROHIBIT
592         // was involved here, but we ignore that.
593         return true;
594     }
596     if (!has_capability('moodle/course:create', $context, $user)) {
597         return false;
598     }
600     if (!enrol_is_enabled('manual')) {
601         return false;
602     }
604     if (empty($CFG->creatornewroleid)) {
605         return false;
606     }
608     if ($context->contextlevel == CONTEXT_COURSE) {
609         if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
610             return false;
611         }
612     } else {
613         if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
614             return false;
615         }
616     }
618     // Most likely they will be enrolled after the course creation is finished,
619     // does the new role have the required capability?
620     list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
621     return isset($neededroles[$CFG->creatornewroleid]);
624 /**
625  * Check if the user is an admin at the site level.
626  *
627  * Please note that use of proper capabilities is always encouraged,
628  * this function is supposed to be used from core or for temporary hacks.
629  *
630  * @category access
631  *
632  * @param  int|stdClass  $user_or_id user id or user object
633  * @return bool true if user is one of the administrators, false otherwise
634  */
635 function is_siteadmin($user_or_id = null) {
636     global $CFG, $USER;
638     if ($user_or_id === null) {
639         $user_or_id = $USER;
640     }
642     if (empty($user_or_id)) {
643         return false;
644     }
645     if (!empty($user_or_id->id)) {
646         $userid = $user_or_id->id;
647     } else {
648         $userid = $user_or_id;
649     }
651     // Because this script is called many times (150+ for course page) with
652     // the same parameters, it is worth doing minor optimisations. This static
653     // cache stores the value for a single userid, saving about 2ms from course
654     // page load time without using significant memory. As the static cache
655     // also includes the value it depends on, this cannot break unit tests.
656     static $knownid, $knownresult, $knownsiteadmins;
657     if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
658         return $knownresult;
659     }
660     $knownid = $userid;
661     $knownsiteadmins = $CFG->siteadmins;
663     $siteadmins = explode(',', $CFG->siteadmins);
664     $knownresult = in_array($userid, $siteadmins);
665     return $knownresult;
668 /**
669  * Returns true if user has at least one role assign
670  * of 'coursecontact' role (is potentially listed in some course descriptions).
671  *
672  * @param int $userid
673  * @return bool
674  */
675 function has_coursecontact_role($userid) {
676     global $DB, $CFG;
678     if (empty($CFG->coursecontact)) {
679         return false;
680     }
681     $sql = "SELECT 1
682               FROM {role_assignments}
683              WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
684     return $DB->record_exists_sql($sql, array('userid'=>$userid));
687 /**
688  * Does the user have a capability to do something?
689  *
690  * Walk the accessdata array and return true/false.
691  * Deals with prohibits, role switching, aggregating
692  * capabilities, etc.
693  *
694  * The main feature of here is being FAST and with no
695  * side effects.
696  *
697  * Notes:
698  *
699  * Switch Role merges with default role
700  * ------------------------------------
701  * If you are a teacher in course X, you have at least
702  * teacher-in-X + defaultloggedinuser-sitewide. So in the
703  * course you'll have techer+defaultloggedinuser.
704  * We try to mimic that in switchrole.
705  *
706  * Permission evaluation
707  * ---------------------
708  * Originally there was an extremely complicated way
709  * to determine the user access that dealt with
710  * "locality" or role assignments and role overrides.
711  * Now we simply evaluate access for each role separately
712  * and then verify if user has at least one role with allow
713  * and at the same time no role with prohibit.
714  *
715  * @access private
716  * @param string $capability
717  * @param context $context
718  * @param array $accessdata
719  * @return bool
720  */
721 function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
722     global $CFG;
724     // Build $paths as a list of current + all parent "paths" with order bottom-to-top
725     $path = $context->path;
726     $paths = array($path);
727     while($path = rtrim($path, '0123456789')) {
728         $path = rtrim($path, '/');
729         if ($path === '') {
730             break;
731         }
732         $paths[] = $path;
733     }
735     $roles = array();
736     $switchedrole = false;
738     // Find out if role switched
739     if (!empty($accessdata['rsw'])) {
740         // From the bottom up...
741         foreach ($paths as $path) {
742             if (isset($accessdata['rsw'][$path])) {
743                 // Found a switchrole assignment - check for that role _plus_ the default user role
744                 $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
745                 $switchedrole = true;
746                 break;
747             }
748         }
749     }
751     if (!$switchedrole) {
752         // get all users roles in this context and above
753         foreach ($paths as $path) {
754             if (isset($accessdata['ra'][$path])) {
755                 foreach ($accessdata['ra'][$path] as $roleid) {
756                     $roles[$roleid] = null;
757                 }
758             }
759         }
760     }
762     // Now find out what access is given to each role, going bottom-->up direction
763     $rdefs = get_role_definitions(array_keys($roles));
764     $allowed = false;
766     foreach ($roles as $roleid => $ignored) {
767         foreach ($paths as $path) {
768             if (isset($rdefs[$roleid][$path][$capability])) {
769                 $perm = (int)$rdefs[$roleid][$path][$capability];
770                 if ($perm === CAP_PROHIBIT) {
771                     // any CAP_PROHIBIT found means no permission for the user
772                     return false;
773                 }
774                 if (is_null($roles[$roleid])) {
775                     $roles[$roleid] = $perm;
776                 }
777             }
778         }
779         // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
780         $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
781     }
783     return $allowed;
786 /**
787  * A convenience function that tests has_capability, and displays an error if
788  * the user does not have that capability.
789  *
790  * NOTE before Moodle 2.0, this function attempted to make an appropriate
791  * require_login call before checking the capability. This is no longer the case.
792  * You must call require_login (or one of its variants) if you want to check the
793  * user is logged in, before you call this function.
794  *
795  * @see has_capability()
796  *
797  * @param string $capability the name of the capability to check. For example mod/forum:view
798  * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
799  * @param int $userid A user id. By default (null) checks the permissions of the current user.
800  * @param bool $doanything If false, ignore effect of admin role assignment
801  * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
802  * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
803  * @return void terminates with an error if the user does not have the given capability.
804  */
805 function require_capability($capability, context $context, $userid = null, $doanything = true,
806                             $errormessage = 'nopermissions', $stringfile = '') {
807     if (!has_capability($capability, $context, $userid, $doanything)) {
808         throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
809     }
812 /**
813  * Return a nested array showing all role assignments for the user.
814  * [ra] => [contextpath][roleid] = roleid
815  *
816  * @access private
817  * @param int $userid - the id of the user
818  * @return array access info array
819  */
820 function get_user_roles_sitewide_accessdata($userid) {
821     global $CFG, $DB;
823     $accessdata = get_empty_accessdata();
825     // start with the default role
826     if (!empty($CFG->defaultuserroleid)) {
827         $syscontext = context_system::instance();
828         $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
829     }
831     // load the "default frontpage role"
832     if (!empty($CFG->defaultfrontpageroleid)) {
833         $frontpagecontext = context_course::instance(get_site()->id);
834         if ($frontpagecontext->path) {
835             $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
836         }
837     }
839     // Preload every assigned role.
840     $sql = "SELECT ctx.path, ra.roleid, ra.contextid
841               FROM {role_assignments} ra
842               JOIN {context} ctx ON ctx.id = ra.contextid
843              WHERE ra.userid = :userid";
845     $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
847     foreach ($rs as $ra) {
848         // RAs leafs are arrays to support multi-role assignments...
849         $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
850     }
852     $rs->close();
854     return $accessdata;
857 /**
858  * Returns empty accessdata structure.
859  *
860  * @access private
861  * @return array empt accessdata
862  */
863 function get_empty_accessdata() {
864     $accessdata               = array(); // named list
865     $accessdata['ra']         = array();
866     $accessdata['time']       = time();
867     $accessdata['rsw']        = array();
869     return $accessdata;
872 /**
873  * Get accessdata for a given user.
874  *
875  * @access private
876  * @param int $userid
877  * @param bool $preloadonly true means do not return access array
878  * @return array accessdata
879  */
880 function get_user_accessdata($userid, $preloadonly=false) {
881     global $CFG, $ACCESSLIB_PRIVATE, $USER;
883     if (isset($USER->access)) {
884         $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
885     }
887     if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
888         if (empty($userid)) {
889             if (!empty($CFG->notloggedinroleid)) {
890                 $accessdata = get_role_access($CFG->notloggedinroleid);
891             } else {
892                 // weird
893                 return get_empty_accessdata();
894             }
896         } else if (isguestuser($userid)) {
897             if ($guestrole = get_guest_role()) {
898                 $accessdata = get_role_access($guestrole->id);
899             } else {
900                 //weird
901                 return get_empty_accessdata();
902             }
904         } else {
905             // Includes default role and frontpage role.
906             $accessdata = get_user_roles_sitewide_accessdata($userid);
907         }
909         $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
910     }
912     if ($preloadonly) {
913         return;
914     } else {
915         return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
916     }
919 /**
920  * A convenience function to completely load all the capabilities
921  * for the current user. It is called from has_capability() and functions change permissions.
922  *
923  * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
924  * @see check_enrolment_plugins()
925  *
926  * @access private
927  * @return void
928  */
929 function load_all_capabilities() {
930     global $USER;
932     // roles not installed yet - we are in the middle of installation
933     if (during_initial_install()) {
934         return;
935     }
937     if (!isset($USER->id)) {
938         // this should not happen
939         $USER->id = 0;
940     }
942     unset($USER->access);
943     $USER->access = get_user_accessdata($USER->id);
945     // Clear to force a refresh
946     unset($USER->mycourses);
948     // init/reset internal enrol caches - active course enrolments and temp access
949     $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
952 /**
953  * A convenience function to completely reload all the capabilities
954  * for the current user when roles have been updated in a relevant
955  * context -- but PRESERVING switchroles and loginas.
956  * This function resets all accesslib and context caches.
957  *
958  * That is - completely transparent to the user.
959  *
960  * Note: reloads $USER->access completely.
961  *
962  * @access private
963  * @return void
964  */
965 function reload_all_capabilities() {
966     global $USER, $DB, $ACCESSLIB_PRIVATE;
968     // copy switchroles
969     $sw = array();
970     if (!empty($USER->access['rsw'])) {
971         $sw = $USER->access['rsw'];
972     }
974     accesslib_clear_all_caches(true);
975     unset($USER->access);
976     $ACCESSLIB_PRIVATE->dirtycontexts = array(); // prevent dirty flags refetching on this page
978     load_all_capabilities();
980     foreach ($sw as $path => $roleid) {
981         if ($record = $DB->get_record('context', array('path'=>$path))) {
982             $context = context::instance_by_id($record->id);
983             role_switch($roleid, $context);
984         }
985     }
988 /**
989  * Adds a temp role to current USER->access array.
990  *
991  * Useful for the "temporary guest" access we grant to logged-in users.
992  * This is useful for enrol plugins only.
993  *
994  * @since Moodle 2.2
995  * @param context_course $coursecontext
996  * @param int $roleid
997  * @return void
998  */
999 function load_temp_course_role(context_course $coursecontext, $roleid) {
1000     global $USER, $SITE;
1002     if (empty($roleid)) {
1003         debugging('invalid role specified in load_temp_course_role()');
1004         return;
1005     }
1007     if ($coursecontext->instanceid == $SITE->id) {
1008         debugging('Can not use temp roles on the frontpage');
1009         return;
1010     }
1012     if (!isset($USER->access)) {
1013         load_all_capabilities();
1014     }
1016     $coursecontext->reload_if_dirty();
1018     if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
1019         return;
1020     }
1022     $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
1025 /**
1026  * Removes any extra guest roles from current USER->access array.
1027  * This is useful for enrol plugins only.
1028  *
1029  * @since Moodle 2.2
1030  * @param context_course $coursecontext
1031  * @return void
1032  */
1033 function remove_temp_course_roles(context_course $coursecontext) {
1034     global $DB, $USER, $SITE;
1036     if ($coursecontext->instanceid == $SITE->id) {
1037         debugging('Can not use temp roles on the frontpage');
1038         return;
1039     }
1041     if (empty($USER->access['ra'][$coursecontext->path])) {
1042         //no roles here, weird
1043         return;
1044     }
1046     $sql = "SELECT DISTINCT ra.roleid AS id
1047               FROM {role_assignments} ra
1048              WHERE ra.contextid = :contextid AND ra.userid = :userid";
1049     $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
1051     $USER->access['ra'][$coursecontext->path] = array();
1052     foreach($ras as $r) {
1053         $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
1054     }
1057 /**
1058  * Returns array of all role archetypes.
1059  *
1060  * @return array
1061  */
1062 function get_role_archetypes() {
1063     return array(
1064         'manager'        => 'manager',
1065         'coursecreator'  => 'coursecreator',
1066         'editingteacher' => 'editingteacher',
1067         'teacher'        => 'teacher',
1068         'student'        => 'student',
1069         'guest'          => 'guest',
1070         'user'           => 'user',
1071         'frontpage'      => 'frontpage'
1072     );
1075 /**
1076  * Assign the defaults found in this capability definition to roles that have
1077  * the corresponding legacy capabilities assigned to them.
1078  *
1079  * @param string $capability
1080  * @param array $legacyperms an array in the format (example):
1081  *                      'guest' => CAP_PREVENT,
1082  *                      'student' => CAP_ALLOW,
1083  *                      'teacher' => CAP_ALLOW,
1084  *                      'editingteacher' => CAP_ALLOW,
1085  *                      'coursecreator' => CAP_ALLOW,
1086  *                      'manager' => CAP_ALLOW
1087  * @return boolean success or failure.
1088  */
1089 function assign_legacy_capabilities($capability, $legacyperms) {
1091     $archetypes = get_role_archetypes();
1093     foreach ($legacyperms as $type => $perm) {
1095         $systemcontext = context_system::instance();
1096         if ($type === 'admin') {
1097             debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1098             $type = 'manager';
1099         }
1101         if (!array_key_exists($type, $archetypes)) {
1102             print_error('invalidlegacy', '', '', $type);
1103         }
1105         if ($roles = get_archetype_roles($type)) {
1106             foreach ($roles as $role) {
1107                 // Assign a site level capability.
1108                 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1109                     return false;
1110                 }
1111             }
1112         }
1113     }
1114     return true;
1117 /**
1118  * Verify capability risks.
1119  *
1120  * @param stdClass $capability a capability - a row from the capabilities table.
1121  * @return boolean whether this capability is safe - that is, whether people with the
1122  *      safeoverrides capability should be allowed to change it.
1123  */
1124 function is_safe_capability($capability) {
1125     return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1128 /**
1129  * Get the local override (if any) for a given capability in a role in a context
1130  *
1131  * @param int $roleid
1132  * @param int $contextid
1133  * @param string $capability
1134  * @return stdClass local capability override
1135  */
1136 function get_local_override($roleid, $contextid, $capability) {
1137     global $DB;
1138     return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
1141 /**
1142  * Returns context instance plus related course and cm instances
1143  *
1144  * @param int $contextid
1145  * @return array of ($context, $course, $cm)
1146  */
1147 function get_context_info_array($contextid) {
1148     global $DB;
1150     $context = context::instance_by_id($contextid, MUST_EXIST);
1151     $course  = null;
1152     $cm      = null;
1154     if ($context->contextlevel == CONTEXT_COURSE) {
1155         $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
1157     } else if ($context->contextlevel == CONTEXT_MODULE) {
1158         $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
1159         $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1161     } else if ($context->contextlevel == CONTEXT_BLOCK) {
1162         $parent = $context->get_parent_context();
1164         if ($parent->contextlevel == CONTEXT_COURSE) {
1165             $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
1166         } else if ($parent->contextlevel == CONTEXT_MODULE) {
1167             $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
1168             $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1169         }
1170     }
1172     return array($context, $course, $cm);
1175 /**
1176  * Function that creates a role
1177  *
1178  * @param string $name role name
1179  * @param string $shortname role short name
1180  * @param string $description role description
1181  * @param string $archetype
1182  * @return int id or dml_exception
1183  */
1184 function create_role($name, $shortname, $description, $archetype = '') {
1185     global $DB;
1187     if (strpos($archetype, 'moodle/legacy:') !== false) {
1188         throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
1189     }
1191     // verify role archetype actually exists
1192     $archetypes = get_role_archetypes();
1193     if (empty($archetypes[$archetype])) {
1194         $archetype = '';
1195     }
1197     // Insert the role record.
1198     $role = new stdClass();
1199     $role->name        = $name;
1200     $role->shortname   = $shortname;
1201     $role->description = $description;
1202     $role->archetype   = $archetype;
1204     //find free sortorder number
1205     $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
1206     if (empty($role->sortorder)) {
1207         $role->sortorder = 1;
1208     }
1209     $id = $DB->insert_record('role', $role);
1211     return $id;
1214 /**
1215  * Function that deletes a role and cleanups up after it
1216  *
1217  * @param int $roleid id of role to delete
1218  * @return bool always true
1219  */
1220 function delete_role($roleid) {
1221     global $DB;
1223     // first unssign all users
1224     role_unassign_all(array('roleid'=>$roleid));
1226     // cleanup all references to this role, ignore errors
1227     $DB->delete_records('role_capabilities',   array('roleid'=>$roleid));
1228     $DB->delete_records('role_allow_assign',   array('roleid'=>$roleid));
1229     $DB->delete_records('role_allow_assign',   array('allowassign'=>$roleid));
1230     $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
1231     $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
1232     $DB->delete_records('role_names',          array('roleid'=>$roleid));
1233     $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
1235     // Get role record before it's deleted.
1236     $role = $DB->get_record('role', array('id'=>$roleid));
1238     // Finally delete the role itself.
1239     $DB->delete_records('role', array('id'=>$roleid));
1241     // Trigger event.
1242     $event = \core\event\role_deleted::create(
1243         array(
1244             'context' => context_system::instance(),
1245             'objectid' => $roleid,
1246             'other' =>
1247                 array(
1248                     'shortname' => $role->shortname,
1249                     'description' => $role->description,
1250                     'archetype' => $role->archetype
1251                 )
1252             )
1253         );
1254     $event->add_record_snapshot('role', $role);
1255     $event->trigger();
1257     // Reset any cache of this role, including MUC.
1258     accesslib_clear_role_cache($roleid);
1260     return true;
1263 /**
1264  * Function to write context specific overrides, or default capabilities.
1265  *
1266  * NOTE: use $context->mark_dirty() after this
1267  *
1268  * @param string $capability string name
1269  * @param int $permission CAP_ constants
1270  * @param int $roleid role id
1271  * @param int|context $contextid context id
1272  * @param bool $overwrite
1273  * @return bool always true or exception
1274  */
1275 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
1276     global $USER, $DB;
1278     if ($contextid instanceof context) {
1279         $context = $contextid;
1280     } else {
1281         $context = context::instance_by_id($contextid);
1282     }
1284     if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
1285         unassign_capability($capability, $roleid, $context->id);
1286         return true;
1287     }
1289     $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
1291     if ($existing and !$overwrite) {   // We want to keep whatever is there already
1292         return true;
1293     }
1295     $cap = new stdClass();
1296     $cap->contextid    = $context->id;
1297     $cap->roleid       = $roleid;
1298     $cap->capability   = $capability;
1299     $cap->permission   = $permission;
1300     $cap->timemodified = time();
1301     $cap->modifierid   = empty($USER->id) ? 0 : $USER->id;
1303     if ($existing) {
1304         $cap->id = $existing->id;
1305         $DB->update_record('role_capabilities', $cap);
1306     } else {
1307         if ($DB->record_exists('context', array('id'=>$context->id))) {
1308             $DB->insert_record('role_capabilities', $cap);
1309         }
1310     }
1312     // Reset any cache of this role, including MUC.
1313     accesslib_clear_role_cache($roleid);
1315     return true;
1318 /**
1319  * Unassign a capability from a role.
1320  *
1321  * NOTE: use $context->mark_dirty() after this
1322  *
1323  * @param string $capability the name of the capability
1324  * @param int $roleid the role id
1325  * @param int|context $contextid null means all contexts
1326  * @return boolean true or exception
1327  */
1328 function unassign_capability($capability, $roleid, $contextid = null) {
1329     global $DB;
1331     if (!empty($contextid)) {
1332         if ($contextid instanceof context) {
1333             $context = $contextid;
1334         } else {
1335             $context = context::instance_by_id($contextid);
1336         }
1337         // delete from context rel, if this is the last override in this context
1338         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
1339     } else {
1340         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
1341     }
1343     // Reset any cache of this role, including MUC.
1344     accesslib_clear_role_cache($roleid);
1346     return true;
1349 /**
1350  * Get the roles that have a given capability assigned to it
1351  *
1352  * This function does not resolve the actual permission of the capability.
1353  * It just checks for permissions and overrides.
1354  * Use get_roles_with_cap_in_context() if resolution is required.
1355  *
1356  * @param string $capability capability name (string)
1357  * @param string $permission optional, the permission defined for this capability
1358  *                      either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
1359  * @param stdClass $context null means any
1360  * @return array of role records
1361  */
1362 function get_roles_with_capability($capability, $permission = null, $context = null) {
1363     global $DB;
1365     if ($context) {
1366         $contexts = $context->get_parent_context_ids(true);
1367         list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
1368         $contextsql = "AND rc.contextid $insql";
1369     } else {
1370         $params = array();
1371         $contextsql = '';
1372     }
1374     if ($permission) {
1375         $permissionsql = " AND rc.permission = :permission";
1376         $params['permission'] = $permission;
1377     } else {
1378         $permissionsql = '';
1379     }
1381     $sql = "SELECT r.*
1382               FROM {role} r
1383              WHERE r.id IN (SELECT rc.roleid
1384                               FROM {role_capabilities} rc
1385                              WHERE rc.capability = :capname
1386                                    $contextsql
1387                                    $permissionsql)";
1388     $params['capname'] = $capability;
1391     return $DB->get_records_sql($sql, $params);
1394 /**
1395  * This function makes a role-assignment (a role for a user in a particular context)
1396  *
1397  * @param int $roleid the role of the id
1398  * @param int $userid userid
1399  * @param int|context $contextid id of the context
1400  * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
1401  * @param int $itemid id of enrolment/auth plugin
1402  * @param string $timemodified defaults to current time
1403  * @return int new/existing id of the assignment
1404  */
1405 function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
1406     global $USER, $DB, $CFG;
1408     // first of all detect if somebody is using old style parameters
1409     if ($contextid === 0 or is_numeric($component)) {
1410         throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
1411     }
1413     // now validate all parameters
1414     if (empty($roleid)) {
1415         throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
1416     }
1418     if (empty($userid)) {
1419         throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
1420     }
1422     if ($itemid) {
1423         if (strpos($component, '_') === false) {
1424             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
1425         }
1426     } else {
1427         $itemid = 0;
1428         if ($component !== '' and strpos($component, '_') === false) {
1429             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1430         }
1431     }
1433     if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
1434         throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
1435     }
1437     if ($contextid instanceof context) {
1438         $context = $contextid;
1439     } else {
1440         $context = context::instance_by_id($contextid, MUST_EXIST);
1441     }
1443     if (!$timemodified) {
1444         $timemodified = time();
1445     }
1447     // Check for existing entry
1448     $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
1450     if ($ras) {
1451         // role already assigned - this should not happen
1452         if (count($ras) > 1) {
1453             // very weird - remove all duplicates!
1454             $ra = array_shift($ras);
1455             foreach ($ras as $r) {
1456                 $DB->delete_records('role_assignments', array('id'=>$r->id));
1457             }
1458         } else {
1459             $ra = reset($ras);
1460         }
1462         // actually there is no need to update, reset anything or trigger any event, so just return
1463         return $ra->id;
1464     }
1466     // Create a new entry
1467     $ra = new stdClass();
1468     $ra->roleid       = $roleid;
1469     $ra->contextid    = $context->id;
1470     $ra->userid       = $userid;
1471     $ra->component    = $component;
1472     $ra->itemid       = $itemid;
1473     $ra->timemodified = $timemodified;
1474     $ra->modifierid   = empty($USER->id) ? 0 : $USER->id;
1475     $ra->sortorder    = 0;
1477     $ra->id = $DB->insert_record('role_assignments', $ra);
1479     // mark context as dirty - again expensive, but needed
1480     $context->mark_dirty();
1482     if (!empty($USER->id) && $USER->id == $userid) {
1483         // If the user is the current user, then do full reload of capabilities too.
1484         reload_all_capabilities();
1485     }
1487     require_once($CFG->libdir . '/coursecatlib.php');
1488     coursecat::role_assignment_changed($roleid, $context);
1490     $event = \core\event\role_assigned::create(array(
1491         'context' => $context,
1492         'objectid' => $ra->roleid,
1493         'relateduserid' => $ra->userid,
1494         'other' => array(
1495             'id' => $ra->id,
1496             'component' => $ra->component,
1497             'itemid' => $ra->itemid
1498         )
1499     ));
1500     $event->add_record_snapshot('role_assignments', $ra);
1501     $event->trigger();
1503     return $ra->id;
1506 /**
1507  * Removes one role assignment
1508  *
1509  * @param int $roleid
1510  * @param int  $userid
1511  * @param int  $contextid
1512  * @param string $component
1513  * @param int  $itemid
1514  * @return void
1515  */
1516 function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
1517     // first make sure the params make sense
1518     if ($roleid == 0 or $userid == 0 or $contextid == 0) {
1519         throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
1520     }
1522     if ($itemid) {
1523         if (strpos($component, '_') === false) {
1524             throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
1525         }
1526     } else {
1527         $itemid = 0;
1528         if ($component !== '' and strpos($component, '_') === false) {
1529             throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1530         }
1531     }
1533     role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
1536 /**
1537  * Removes multiple role assignments, parameters may contain:
1538  *   'roleid', 'userid', 'contextid', 'component', 'enrolid'.
1539  *
1540  * @param array $params role assignment parameters
1541  * @param bool $subcontexts unassign in subcontexts too
1542  * @param bool $includemanual include manual role assignments too
1543  * @return void
1544  */
1545 function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
1546     global $USER, $CFG, $DB;
1547     require_once($CFG->libdir . '/coursecatlib.php');
1549     if (!$params) {
1550         throw new coding_exception('Missing parameters in role_unsassign_all() call');
1551     }
1553     $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
1554     foreach ($params as $key=>$value) {
1555         if (!in_array($key, $allowed)) {
1556             throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
1557         }
1558     }
1560     if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
1561         throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
1562     }
1564     if ($includemanual) {
1565         if (!isset($params['component']) or $params['component'] === '') {
1566             throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
1567         }
1568     }
1570     if ($subcontexts) {
1571         if (empty($params['contextid'])) {
1572             throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
1573         }
1574     }
1576     $ras = $DB->get_records('role_assignments', $params);
1577     foreach($ras as $ra) {
1578         $DB->delete_records('role_assignments', array('id'=>$ra->id));
1579         if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
1580             // this is a bit expensive but necessary
1581             $context->mark_dirty();
1582             // If the user is the current user, then do full reload of capabilities too.
1583             if (!empty($USER->id) && $USER->id == $ra->userid) {
1584                 reload_all_capabilities();
1585             }
1586             $event = \core\event\role_unassigned::create(array(
1587                 'context' => $context,
1588                 'objectid' => $ra->roleid,
1589                 'relateduserid' => $ra->userid,
1590                 'other' => array(
1591                     'id' => $ra->id,
1592                     'component' => $ra->component,
1593                     'itemid' => $ra->itemid
1594                 )
1595             ));
1596             $event->add_record_snapshot('role_assignments', $ra);
1597             $event->trigger();
1598             coursecat::role_assignment_changed($ra->roleid, $context);
1599         }
1600     }
1601     unset($ras);
1603     // process subcontexts
1604     if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
1605         if ($params['contextid'] instanceof context) {
1606             $context = $params['contextid'];
1607         } else {
1608             $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
1609         }
1611         if ($context) {
1612             $contexts = $context->get_child_contexts();
1613             $mparams = $params;
1614             foreach($contexts as $context) {
1615                 $mparams['contextid'] = $context->id;
1616                 $ras = $DB->get_records('role_assignments', $mparams);
1617                 foreach($ras as $ra) {
1618                     $DB->delete_records('role_assignments', array('id'=>$ra->id));
1619                     // this is a bit expensive but necessary
1620                     $context->mark_dirty();
1621                     // If the user is the current user, then do full reload of capabilities too.
1622                     if (!empty($USER->id) && $USER->id == $ra->userid) {
1623                         reload_all_capabilities();
1624                     }
1625                     $event = \core\event\role_unassigned::create(
1626                         array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1627                             'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1628                     $event->add_record_snapshot('role_assignments', $ra);
1629                     $event->trigger();
1630                     coursecat::role_assignment_changed($ra->roleid, $context);
1631                 }
1632             }
1633         }
1634     }
1636     // do this once more for all manual role assignments
1637     if ($includemanual) {
1638         $params['component'] = '';
1639         role_unassign_all($params, $subcontexts, false);
1640     }
1643 /**
1644  * Determines if a user is currently logged in
1645  *
1646  * @category   access
1647  *
1648  * @return bool
1649  */
1650 function isloggedin() {
1651     global $USER;
1653     return (!empty($USER->id));
1656 /**
1657  * Determines if a user is logged in as real guest user with username 'guest'.
1658  *
1659  * @category   access
1660  *
1661  * @param int|object $user mixed user object or id, $USER if not specified
1662  * @return bool true if user is the real guest user, false if not logged in or other user
1663  */
1664 function isguestuser($user = null) {
1665     global $USER, $DB, $CFG;
1667     // make sure we have the user id cached in config table, because we are going to use it a lot
1668     if (empty($CFG->siteguest)) {
1669         if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
1670             // guest does not exist yet, weird
1671             return false;
1672         }
1673         set_config('siteguest', $guestid);
1674     }
1675     if ($user === null) {
1676         $user = $USER;
1677     }
1679     if ($user === null) {
1680         // happens when setting the $USER
1681         return false;
1683     } else if (is_numeric($user)) {
1684         return ($CFG->siteguest == $user);
1686     } else if (is_object($user)) {
1687         if (empty($user->id)) {
1688             return false; // not logged in means is not be guest
1689         } else {
1690             return ($CFG->siteguest == $user->id);
1691         }
1693     } else {
1694         throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
1695     }
1698 /**
1699  * Does user have a (temporary or real) guest access to course?
1700  *
1701  * @category   access
1702  *
1703  * @param context $context
1704  * @param stdClass|int $user
1705  * @return bool
1706  */
1707 function is_guest(context $context, $user = null) {
1708     global $USER;
1710     // first find the course context
1711     $coursecontext = $context->get_course_context();
1713     // make sure there is a real user specified
1714     if ($user === null) {
1715         $userid = isset($USER->id) ? $USER->id : 0;
1716     } else {
1717         $userid = is_object($user) ? $user->id : $user;
1718     }
1720     if (isguestuser($userid)) {
1721         // can not inspect or be enrolled
1722         return true;
1723     }
1725     if (has_capability('moodle/course:view', $coursecontext, $user)) {
1726         // viewing users appear out of nowhere, they are neither guests nor participants
1727         return false;
1728     }
1730     // consider only real active enrolments here
1731     if (is_enrolled($coursecontext, $user, '', true)) {
1732         return false;
1733     }
1735     return true;
1738 /**
1739  * Returns true if the user has moodle/course:view capability in the course,
1740  * this is intended for admins, managers (aka small admins), inspectors, etc.
1741  *
1742  * @category   access
1743  *
1744  * @param context $context
1745  * @param int|stdClass $user if null $USER is used
1746  * @param string $withcapability extra capability name
1747  * @return bool
1748  */
1749 function is_viewing(context $context, $user = null, $withcapability = '') {
1750     // first find the course context
1751     $coursecontext = $context->get_course_context();
1753     if (isguestuser($user)) {
1754         // can not inspect
1755         return false;
1756     }
1758     if (!has_capability('moodle/course:view', $coursecontext, $user)) {
1759         // admins are allowed to inspect courses
1760         return false;
1761     }
1763     if ($withcapability and !has_capability($withcapability, $context, $user)) {
1764         // site admins always have the capability, but the enrolment above blocks
1765         return false;
1766     }
1768     return true;
1771 /**
1772  * Returns true if the user is able to access the course.
1773  *
1774  * This function is in no way, shape, or form a substitute for require_login.
1775  * It should only be used in circumstances where it is not possible to call require_login
1776  * such as the navigation.
1777  *
1778  * This function checks many of the methods of access to a course such as the view
1779  * capability, enrollments, and guest access. It also makes use of the cache
1780  * generated by require_login for guest access.
1781  *
1782  * The flags within the $USER object that are used here should NEVER be used outside
1783  * of this function can_access_course and require_login. Doing so WILL break future
1784  * versions.
1785  *
1786  * @param stdClass $course record
1787  * @param stdClass|int|null $user user record or id, current user if null
1788  * @param string $withcapability Check for this capability as well.
1789  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1790  * @return boolean Returns true if the user is able to access the course
1791  */
1792 function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
1793     global $DB, $USER;
1795     // this function originally accepted $coursecontext parameter
1796     if ($course instanceof context) {
1797         if ($course instanceof context_course) {
1798             debugging('deprecated context parameter, please use $course record');
1799             $coursecontext = $course;
1800             $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
1801         } else {
1802             debugging('Invalid context parameter, please use $course record');
1803             return false;
1804         }
1805     } else {
1806         $coursecontext = context_course::instance($course->id);
1807     }
1809     if (!isset($USER->id)) {
1810         // should never happen
1811         $USER->id = 0;
1812         debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
1813     }
1815     // make sure there is a user specified
1816     if ($user === null) {
1817         $userid = $USER->id;
1818     } else {
1819         $userid = is_object($user) ? $user->id : $user;
1820     }
1821     unset($user);
1823     if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
1824         return false;
1825     }
1827     if ($userid == $USER->id) {
1828         if (!empty($USER->access['rsw'][$coursecontext->path])) {
1829             // the fact that somebody switched role means they can access the course no matter to what role they switched
1830             return true;
1831         }
1832     }
1834     if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
1835         return false;
1836     }
1838     if (is_viewing($coursecontext, $userid)) {
1839         return true;
1840     }
1842     if ($userid != $USER->id) {
1843         // for performance reasons we do not verify temporary guest access for other users, sorry...
1844         return is_enrolled($coursecontext, $userid, '', $onlyactive);
1845     }
1847     // === from here we deal only with $USER ===
1849     $coursecontext->reload_if_dirty();
1851     if (isset($USER->enrol['enrolled'][$course->id])) {
1852         if ($USER->enrol['enrolled'][$course->id] > time()) {
1853             return true;
1854         }
1855     }
1856     if (isset($USER->enrol['tempguest'][$course->id])) {
1857         if ($USER->enrol['tempguest'][$course->id] > time()) {
1858             return true;
1859         }
1860     }
1862     if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
1863         return true;
1864     }
1866     // if not enrolled try to gain temporary guest access
1867     $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
1868     $enrols = enrol_get_plugins(true);
1869     foreach($instances as $instance) {
1870         if (!isset($enrols[$instance->enrol])) {
1871             continue;
1872         }
1873         // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
1874         $until = $enrols[$instance->enrol]->try_guestaccess($instance);
1875         if ($until !== false and $until > time()) {
1876             $USER->enrol['tempguest'][$course->id] = $until;
1877             return true;
1878         }
1879     }
1880     if (isset($USER->enrol['tempguest'][$course->id])) {
1881         unset($USER->enrol['tempguest'][$course->id]);
1882         remove_temp_course_roles($coursecontext);
1883     }
1885     return false;
1888 /**
1889  * Loads the capability definitions for the component (from file).
1890  *
1891  * Loads the capability definitions for the component (from file). If no
1892  * capabilities are defined for the component, we simply return an empty array.
1893  *
1894  * @access private
1895  * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
1896  * @return array array of capabilities
1897  */
1898 function load_capability_def($component) {
1899     $defpath = core_component::get_component_directory($component).'/db/access.php';
1901     $capabilities = array();
1902     if (file_exists($defpath)) {
1903         require($defpath);
1904         if (!empty(${$component.'_capabilities'})) {
1905             // BC capability array name
1906             // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
1907             debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
1908             $capabilities = ${$component.'_capabilities'};
1909         }
1910     }
1912     return $capabilities;
1915 /**
1916  * Gets the capabilities that have been cached in the database for this component.
1917  *
1918  * @access private
1919  * @param string $component - examples: 'moodle', 'mod_forum'
1920  * @return array array of capabilities
1921  */
1922 function get_cached_capabilities($component = 'moodle') {
1923     global $DB;
1924     $caps = get_all_capabilities();
1925     $componentcaps = array();
1926     foreach ($caps as $cap) {
1927         if ($cap['component'] == $component) {
1928             $componentcaps[] = (object) $cap;
1929         }
1930     }
1931     return $componentcaps;
1934 /**
1935  * Returns default capabilities for given role archetype.
1936  *
1937  * @param string $archetype role archetype
1938  * @return array
1939  */
1940 function get_default_capabilities($archetype) {
1941     global $DB;
1943     if (!$archetype) {
1944         return array();
1945     }
1947     $alldefs = array();
1948     $defaults = array();
1949     $components = array();
1950     $allcaps = get_all_capabilities();
1952     foreach ($allcaps as $cap) {
1953         if (!in_array($cap['component'], $components)) {
1954             $components[] = $cap['component'];
1955             $alldefs = array_merge($alldefs, load_capability_def($cap['component']));
1956         }
1957     }
1958     foreach($alldefs as $name=>$def) {
1959         // Use array 'archetypes if available. Only if not specified, use 'legacy'.
1960         if (isset($def['archetypes'])) {
1961             if (isset($def['archetypes'][$archetype])) {
1962                 $defaults[$name] = $def['archetypes'][$archetype];
1963             }
1964         // 'legacy' is for backward compatibility with 1.9 access.php
1965         } else {
1966             if (isset($def['legacy'][$archetype])) {
1967                 $defaults[$name] = $def['legacy'][$archetype];
1968             }
1969         }
1970     }
1972     return $defaults;
1975 /**
1976  * Return default roles that can be assigned, overridden or switched
1977  * by give role archetype.
1978  *
1979  * @param string $type  assign|override|switch
1980  * @param string $archetype
1981  * @return array of role ids
1982  */
1983 function get_default_role_archetype_allows($type, $archetype) {
1984     global $DB;
1986     if (empty($archetype)) {
1987         return array();
1988     }
1990     $roles = $DB->get_records('role');
1991     $archetypemap = array();
1992     foreach ($roles as $role) {
1993         if ($role->archetype) {
1994             $archetypemap[$role->archetype][$role->id] = $role->id;
1995         }
1996     }
1998     $defaults = array(
1999         'assign' => array(
2000             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
2001             'coursecreator'  => array(),
2002             'editingteacher' => array('teacher', 'student'),
2003             'teacher'        => array(),
2004             'student'        => array(),
2005             'guest'          => array(),
2006             'user'           => array(),
2007             'frontpage'      => array(),
2008         ),
2009         'override' => array(
2010             'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2011             'coursecreator'  => array(),
2012             'editingteacher' => array('teacher', 'student', 'guest'),
2013             'teacher'        => array(),
2014             'student'        => array(),
2015             'guest'          => array(),
2016             'user'           => array(),
2017             'frontpage'      => array(),
2018         ),
2019         'switch' => array(
2020             'manager'        => array('editingteacher', 'teacher', 'student', 'guest'),
2021             'coursecreator'  => array(),
2022             'editingteacher' => array('teacher', 'student', 'guest'),
2023             'teacher'        => array('student', 'guest'),
2024             'student'        => array(),
2025             'guest'          => array(),
2026             'user'           => array(),
2027             'frontpage'      => array(),
2028         ),
2029     );
2031     if (!isset($defaults[$type][$archetype])) {
2032         debugging("Unknown type '$type'' or archetype '$archetype''");
2033         return array();
2034     }
2036     $return = array();
2037     foreach ($defaults[$type][$archetype] as $at) {
2038         if (isset($archetypemap[$at])) {
2039             foreach ($archetypemap[$at] as $roleid) {
2040                 $return[$roleid] = $roleid;
2041             }
2042         }
2043     }
2045     return $return;
2048 /**
2049  * Reset role capabilities to default according to selected role archetype.
2050  * If no archetype selected, removes all capabilities.
2051  *
2052  * This applies to capabilities that are assigned to the role (that you could
2053  * edit in the 'define roles' interface), and not to any capability overrides
2054  * in different locations.
2055  *
2056  * @param int $roleid ID of role to reset capabilities for
2057  */
2058 function reset_role_capabilities($roleid) {
2059     global $DB;
2061     $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
2062     $defaultcaps = get_default_capabilities($role->archetype);
2064     $systemcontext = context_system::instance();
2066     $DB->delete_records('role_capabilities',
2067             array('roleid' => $roleid, 'contextid' => $systemcontext->id));
2069     foreach($defaultcaps as $cap=>$permission) {
2070         assign_capability($cap, $permission, $roleid, $systemcontext->id);
2071     }
2073     // Reset any cache of this role, including MUC.
2074     accesslib_clear_role_cache($roleid);
2076     // Mark the system context dirty.
2077     context_system::instance()->mark_dirty();
2080 /**
2081  * Updates the capabilities table with the component capability definitions.
2082  * If no parameters are given, the function updates the core moodle
2083  * capabilities.
2084  *
2085  * Note that the absence of the db/access.php capabilities definition file
2086  * will cause any stored capabilities for the component to be removed from
2087  * the database.
2088  *
2089  * @access private
2090  * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
2091  * @return boolean true if success, exception in case of any problems
2092  */
2093 function update_capabilities($component = 'moodle') {
2094     global $DB, $OUTPUT;
2096     $storedcaps = array();
2098     $filecaps = load_capability_def($component);
2099     foreach($filecaps as $capname=>$unused) {
2100         if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
2101             debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
2102         }
2103     }
2105     // It is possible somebody directly modified the DB (according to accesslib_test anyway).
2106     // So ensure our updating is based on fresh data.
2107     cache::make('core', 'capabilities')->delete('core_capabilities');
2109     $cachedcaps = get_cached_capabilities($component);
2110     if ($cachedcaps) {
2111         foreach ($cachedcaps as $cachedcap) {
2112             array_push($storedcaps, $cachedcap->name);
2113             // update risk bitmasks and context levels in existing capabilities if needed
2114             if (array_key_exists($cachedcap->name, $filecaps)) {
2115                 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
2116                     $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
2117                 }
2118                 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
2119                     $updatecap = new stdClass();
2120                     $updatecap->id = $cachedcap->id;
2121                     $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
2122                     $DB->update_record('capabilities', $updatecap);
2123                 }
2124                 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
2125                     $updatecap = new stdClass();
2126                     $updatecap->id = $cachedcap->id;
2127                     $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
2128                     $DB->update_record('capabilities', $updatecap);
2129                 }
2131                 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
2132                     $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
2133                 }
2134                 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
2135                     $updatecap = new stdClass();
2136                     $updatecap->id = $cachedcap->id;
2137                     $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
2138                     $DB->update_record('capabilities', $updatecap);
2139                 }
2140             }
2141         }
2142     }
2144     // Flush the cached again, as we have changed DB.
2145     cache::make('core', 'capabilities')->delete('core_capabilities');
2147     // Are there new capabilities in the file definition?
2148     $newcaps = array();
2150     foreach ($filecaps as $filecap => $def) {
2151         if (!$storedcaps ||
2152                 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
2153             if (!array_key_exists('riskbitmask', $def)) {
2154                 $def['riskbitmask'] = 0; // no risk if not specified
2155             }
2156             $newcaps[$filecap] = $def;
2157         }
2158     }
2159     // Add new capabilities to the stored definition.
2160     $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
2161     foreach ($newcaps as $capname => $capdef) {
2162         $capability = new stdClass();
2163         $capability->name         = $capname;
2164         $capability->captype      = $capdef['captype'];
2165         $capability->contextlevel = $capdef['contextlevel'];
2166         $capability->component    = $component;
2167         $capability->riskbitmask  = $capdef['riskbitmask'];
2169         $DB->insert_record('capabilities', $capability, false);
2171         if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
2172             if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
2173                 foreach ($rolecapabilities as $rolecapability){
2174                     //assign_capability will update rather than insert if capability exists
2175                     if (!assign_capability($capname, $rolecapability->permission,
2176                                             $rolecapability->roleid, $rolecapability->contextid, true)){
2177                          echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
2178                     }
2179                 }
2180             }
2181         // we ignore archetype key if we have cloned permissions
2182         } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
2183             assign_legacy_capabilities($capname, $capdef['archetypes']);
2184         // 'legacy' is for backward compatibility with 1.9 access.php
2185         } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
2186             assign_legacy_capabilities($capname, $capdef['legacy']);
2187         }
2188     }
2189     // Are there any capabilities that have been removed from the file
2190     // definition that we need to delete from the stored capabilities and
2191     // role assignments?
2192     capabilities_cleanup($component, $filecaps);
2194     // reset static caches
2195     accesslib_clear_all_caches(false);
2197     // Flush the cached again, as we have changed DB.
2198     cache::make('core', 'capabilities')->delete('core_capabilities');
2200     return true;
2203 /**
2204  * Deletes cached capabilities that are no longer needed by the component.
2205  * Also unassigns these capabilities from any roles that have them.
2206  * NOTE: this function is called from lib/db/upgrade.php
2207  *
2208  * @access private
2209  * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
2210  * @param array $newcapdef array of the new capability definitions that will be
2211  *                     compared with the cached capabilities
2212  * @return int number of deprecated capabilities that have been removed
2213  */
2214 function capabilities_cleanup($component, $newcapdef = null) {
2215     global $DB;
2217     $removedcount = 0;
2219     if ($cachedcaps = get_cached_capabilities($component)) {
2220         foreach ($cachedcaps as $cachedcap) {
2221             if (empty($newcapdef) ||
2222                         array_key_exists($cachedcap->name, $newcapdef) === false) {
2224                 // Remove from capabilities cache.
2225                 $DB->delete_records('capabilities', array('name'=>$cachedcap->name));
2226                 $removedcount++;
2227                 // Delete from roles.
2228                 if ($roles = get_roles_with_capability($cachedcap->name)) {
2229                     foreach($roles as $role) {
2230                         if (!unassign_capability($cachedcap->name, $role->id)) {
2231                             print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
2232                         }
2233                     }
2234                 }
2235             } // End if.
2236         }
2237     }
2238     if ($removedcount) {
2239         cache::make('core', 'capabilities')->delete('core_capabilities');
2240     }
2241     return $removedcount;
2244 /**
2245  * Returns an array of all the known types of risk
2246  * The array keys can be used, for example as CSS class names, or in calls to
2247  * print_risk_icon. The values are the corresponding RISK_ constants.
2248  *
2249  * @return array all the known types of risk.
2250  */
2251 function get_all_risks() {
2252     return array(
2253         'riskmanagetrust' => RISK_MANAGETRUST,
2254         'riskconfig'      => RISK_CONFIG,
2255         'riskxss'         => RISK_XSS,
2256         'riskpersonal'    => RISK_PERSONAL,
2257         'riskspam'        => RISK_SPAM,
2258         'riskdataloss'    => RISK_DATALOSS,
2259     );
2262 /**
2263  * Return a link to moodle docs for a given capability name
2264  *
2265  * @param stdClass $capability a capability - a row from the mdl_capabilities table.
2266  * @return string the human-readable capability name as a link to Moodle Docs.
2267  */
2268 function get_capability_docs_link($capability) {
2269     $url = get_docs_url('Capabilities/' . $capability->name);
2270     return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
2273 /**
2274  * This function pulls out all the resolved capabilities (overrides and
2275  * defaults) of a role used in capability overrides in contexts at a given
2276  * context.
2277  *
2278  * @param int $roleid
2279  * @param context $context
2280  * @param string $cap capability, optional, defaults to ''
2281  * @return array Array of capabilities
2282  */
2283 function role_context_capabilities($roleid, context $context, $cap = '') {
2284     global $DB;
2286     $contexts = $context->get_parent_context_ids(true);
2287     $contexts = '('.implode(',', $contexts).')';
2289     $params = array($roleid);
2291     if ($cap) {
2292         $search = " AND rc.capability = ? ";
2293         $params[] = $cap;
2294     } else {
2295         $search = '';
2296     }
2298     $sql = "SELECT rc.*
2299               FROM {role_capabilities} rc, {context} c
2300              WHERE rc.contextid in $contexts
2301                    AND rc.roleid = ?
2302                    AND rc.contextid = c.id $search
2303           ORDER BY c.contextlevel DESC, rc.capability DESC";
2305     $capabilities = array();
2307     if ($records = $DB->get_records_sql($sql, $params)) {
2308         // We are traversing via reverse order.
2309         foreach ($records as $record) {
2310             // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
2311             if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
2312                 $capabilities[$record->capability] = $record->permission;
2313             }
2314         }
2315     }
2316     return $capabilities;
2319 /**
2320  * Constructs array with contextids as first parameter and context paths,
2321  * in both cases bottom top including self.
2322  *
2323  * @access private
2324  * @param context $context
2325  * @return array
2326  */
2327 function get_context_info_list(context $context) {
2328     $contextids = explode('/', ltrim($context->path, '/'));
2329     $contextpaths = array();
2330     $contextids2 = $contextids;
2331     while ($contextids2) {
2332         $contextpaths[] = '/' . implode('/', $contextids2);
2333         array_pop($contextids2);
2334     }
2335     return array($contextids, $contextpaths);
2338 /**
2339  * Check if context is the front page context or a context inside it
2340  *
2341  * Returns true if this context is the front page context, or a context inside it,
2342  * otherwise false.
2343  *
2344  * @param context $context a context object.
2345  * @return bool
2346  */
2347 function is_inside_frontpage(context $context) {
2348     $frontpagecontext = context_course::instance(SITEID);
2349     return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
2352 /**
2353  * Returns capability information (cached)
2354  *
2355  * @param string $capabilityname
2356  * @return stdClass or null if capability not found
2357  */
2358 function get_capability_info($capabilityname) {
2359     global $ACCESSLIB_PRIVATE, $DB; // one request per page only
2361     $caps = get_all_capabilities();
2363     if (!isset($caps[$capabilityname])) {
2364         return null;
2365     }
2367     return (object) $caps[$capabilityname];
2370 /**
2371  * Returns all capabilitiy records, preferably from MUC and not database.
2372  *
2373  * @return array All capability records indexed by capability name
2374  */
2375 function get_all_capabilities() {
2376     global $DB;
2377     $cache = cache::make('core', 'capabilities');
2378     if (!$allcaps = $cache->get('core_capabilities')) {
2379         $rs = $DB->get_recordset('capabilities');
2380         $allcaps = array();
2381         foreach ($rs as $capability) {
2382             $capability->riskbitmask = (int) $capability->riskbitmask;
2383             $allcaps[$capability->name] = (array) $capability;
2384         }
2385         $rs->close();
2386         $cache->set('core_capabilities', $allcaps);
2387     }
2388     return $allcaps;
2391 /**
2392  * Returns the human-readable, translated version of the capability.
2393  * Basically a big switch statement.
2394  *
2395  * @param string $capabilityname e.g. mod/choice:readresponses
2396  * @return string
2397  */
2398 function get_capability_string($capabilityname) {
2400     // Typical capability name is 'plugintype/pluginname:capabilityname'
2401     list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
2403     if ($type === 'moodle') {
2404         $component = 'core_role';
2405     } else if ($type === 'quizreport') {
2406         //ugly hack!!
2407         $component = 'quiz_'.$name;
2408     } else {
2409         $component = $type.'_'.$name;
2410     }
2412     $stringname = $name.':'.$capname;
2414     if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
2415         return get_string($stringname, $component);
2416     }
2418     $dir = core_component::get_component_directory($component);
2419     if (!file_exists($dir)) {
2420         // plugin broken or does not exist, do not bother with printing of debug message
2421         return $capabilityname.' ???';
2422     }
2424     // something is wrong in plugin, better print debug
2425     return get_string($stringname, $component);
2428 /**
2429  * This gets the mod/block/course/core etc strings.
2430  *
2431  * @param string $component
2432  * @param int $contextlevel
2433  * @return string|bool String is success, false if failed
2434  */
2435 function get_component_string($component, $contextlevel) {
2437     if ($component === 'moodle' or $component === 'core') {
2438         switch ($contextlevel) {
2439             // TODO MDL-46123: this should probably use context level names instead
2440             case CONTEXT_SYSTEM:    return get_string('coresystem');
2441             case CONTEXT_USER:      return get_string('users');
2442             case CONTEXT_COURSECAT: return get_string('categories');
2443             case CONTEXT_COURSE:    return get_string('course');
2444             case CONTEXT_MODULE:    return get_string('activities');
2445             case CONTEXT_BLOCK:     return get_string('block');
2446             default:                print_error('unknowncontext');
2447         }
2448     }
2450     list($type, $name) = core_component::normalize_component($component);
2451     $dir = core_component::get_plugin_directory($type, $name);
2452     if (!file_exists($dir)) {
2453         // plugin not installed, bad luck, there is no way to find the name
2454         return $component.' ???';
2455     }
2457     switch ($type) {
2458         // TODO MDL-46123: this is really hacky and should be improved.
2459         case 'quiz':         return get_string($name.':componentname', $component);// insane hack!!!
2460         case 'repository':   return get_string('repository', 'repository').': '.get_string('pluginname', $component);
2461         case 'gradeimport':  return get_string('gradeimport', 'grades').': '.get_string('pluginname', $component);
2462         case 'gradeexport':  return get_string('gradeexport', 'grades').': '.get_string('pluginname', $component);
2463         case 'gradereport':  return get_string('gradereport', 'grades').': '.get_string('pluginname', $component);
2464         case 'webservice':   return get_string('webservice', 'webservice').': '.get_string('pluginname', $component);
2465         case 'block':        return get_string('block').': '.get_string('pluginname', basename($component));
2466         case 'mod':
2467             if (get_string_manager()->string_exists('pluginname', $component)) {
2468                 return get_string('activity').': '.get_string('pluginname', $component);
2469             } else {
2470                 return get_string('activity').': '.get_string('modulename', $component);
2471             }
2472         default: return get_string('pluginname', $component);
2473     }
2476 /**
2477  * Gets the list of roles assigned to this context and up (parents)
2478  * from the aggregation of:
2479  * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
2480  * b) if applicable, those roles that are assigned in the context.
2481  *
2482  * @param context $context
2483  * @return array
2484  */
2485 function get_profile_roles(context $context) {
2486     global $CFG, $DB;
2487     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2488     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2489     if (has_capability('moodle/role:assign', $context)) {
2490         $rolesinscope = array_keys(get_all_roles($context));
2491     } else {
2492         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2493     }
2495     if (empty($rolesinscope)) {
2496         return [];
2497     }
2499     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2500     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2501     $params = array_merge($params, $cparams);
2503     if ($coursecontext = $context->get_course_context(false)) {
2504         $params['coursecontext'] = $coursecontext->id;
2505     } else {
2506         $params['coursecontext'] = 0;
2507     }
2509     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2510               FROM {role_assignments} ra, {role} r
2511          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2512              WHERE r.id = ra.roleid
2513                    AND ra.contextid $contextlist
2514                    AND r.id $rallowed
2515           ORDER BY r.sortorder ASC";
2517     return $DB->get_records_sql($sql, $params);
2520 /**
2521  * Gets the list of roles assigned to this context and up (parents)
2522  *
2523  * @param context $context
2524  * @return array
2525  */
2526 function get_roles_used_in_context(context $context) {
2527     global $DB;
2529     list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
2531     if ($coursecontext = $context->get_course_context(false)) {
2532         $params['coursecontext'] = $coursecontext->id;
2533     } else {
2534         $params['coursecontext'] = 0;
2535     }
2537     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2538               FROM {role_assignments} ra, {role} r
2539          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2540              WHERE r.id = ra.roleid
2541                    AND ra.contextid $contextlist
2542           ORDER BY r.sortorder ASC";
2544     return $DB->get_records_sql($sql, $params);
2547 /**
2548  * This function is used to print roles column in user profile page.
2549  * It is using the CFG->profileroles to limit the list to only interesting roles.
2550  * (The permission tab has full details of user role assignments.)
2551  *
2552  * @param int $userid
2553  * @param int $courseid
2554  * @return string
2555  */
2556 function get_user_roles_in_course($userid, $courseid) {
2557     global $CFG, $DB;
2558     if ($courseid == SITEID) {
2559         $context = context_system::instance();
2560     } else {
2561         $context = context_course::instance($courseid);
2562     }
2563     // If the current user can assign roles, then they can see all roles on the profile and participants page,
2564     // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2565     if (has_capability('moodle/role:assign', $context)) {
2566         $rolesinscope = array_keys(get_all_roles($context));
2567     } else {
2568         $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2569     }
2570     if (empty($rolesinscope)) {
2571         return '';
2572     }
2574     list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2575     list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2576     $params = array_merge($params, $cparams);
2578     if ($coursecontext = $context->get_course_context(false)) {
2579         $params['coursecontext'] = $coursecontext->id;
2580     } else {
2581         $params['coursecontext'] = 0;
2582     }
2584     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2585               FROM {role_assignments} ra, {role} r
2586          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2587              WHERE r.id = ra.roleid
2588                    AND ra.contextid $contextlist
2589                    AND r.id $rallowed
2590                    AND ra.userid = :userid
2591           ORDER BY r.sortorder ASC";
2592     $params['userid'] = $userid;
2594     $rolestring = '';
2596     if ($roles = $DB->get_records_sql($sql, $params)) {
2597         $rolenames = role_fix_names($roles, $context, ROLENAME_ALIAS, true);   // Substitute aliases
2599         foreach ($rolenames as $roleid => $rolename) {
2600             $rolenames[$roleid] = '<a href="'.$CFG->wwwroot.'/user/index.php?contextid='.$context->id.'&amp;roleid='.$roleid.'">'.$rolename.'</a>';
2601         }
2602         $rolestring = implode(',', $rolenames);
2603     }
2605     return $rolestring;
2608 /**
2609  * Checks if a user can assign users to a particular role in this context
2610  *
2611  * @param context $context
2612  * @param int $targetroleid - the id of the role you want to assign users to
2613  * @return boolean
2614  */
2615 function user_can_assign(context $context, $targetroleid) {
2616     global $DB;
2618     // First check to see if the user is a site administrator.
2619     if (is_siteadmin()) {
2620         return true;
2621     }
2623     // Check if user has override capability.
2624     // If not return false.
2625     if (!has_capability('moodle/role:assign', $context)) {
2626         return false;
2627     }
2628     // pull out all active roles of this user from this context(or above)
2629     if ($userroles = get_user_roles($context)) {
2630         foreach ($userroles as $userrole) {
2631             // if any in the role_allow_override table, then it's ok
2632             if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
2633                 return true;
2634             }
2635         }
2636     }
2638     return false;
2641 /**
2642  * Returns all site roles in correct sort order.
2643  *
2644  * Note: this method does not localise role names or descriptions,
2645  *       use role_get_names() if you need role names.
2646  *
2647  * @param context $context optional context for course role name aliases
2648  * @return array of role records with optional coursealias property
2649  */
2650 function get_all_roles(context $context = null) {
2651     global $DB;
2653     if (!$context or !$coursecontext = $context->get_course_context(false)) {
2654         $coursecontext = null;
2655     }
2657     if ($coursecontext) {
2658         $sql = "SELECT r.*, rn.name AS coursealias
2659                   FROM {role} r
2660              LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2661               ORDER BY r.sortorder ASC";
2662         return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
2664     } else {
2665         return $DB->get_records('role', array(), 'sortorder ASC');
2666     }
2669 /**
2670  * Returns roles of a specified archetype
2671  *
2672  * @param string $archetype
2673  * @return array of full role records
2674  */
2675 function get_archetype_roles($archetype) {
2676     global $DB;
2677     return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
2680 /**
2681  * Gets all the user roles assigned in this context, or higher contexts for a list of users.
2682  *
2683  * @param context $context
2684  * @param array $userids. An empty list means fetch all role assignments for the context.
2685  * @param bool $checkparentcontexts defaults to true
2686  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2687  * @return array
2688  */
2689 function get_users_roles(context $context, $userids = [], $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2690     global $USER, $DB;
2692     if ($checkparentcontexts) {
2693         $contextids = $context->get_parent_context_ids();
2694     } else {
2695         $contextids = array();
2696     }
2697     $contextids[] = $context->id;
2699     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
2701     // If userids was passed as an empty array, we fetch all role assignments for the course.
2702     if (empty($userids)) {
2703         $useridlist = ' IS NOT NULL ';
2704         $uparams = [];
2705     } else {
2706         list($useridlist, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uids');
2707     }
2709     $sql = "SELECT ra.*, r.name, r.shortname, ra.userid
2710               FROM {role_assignments} ra, {role} r, {context} c
2711              WHERE ra.userid $useridlist
2712                    AND ra.roleid = r.id
2713                    AND ra.contextid = c.id
2714                    AND ra.contextid $contextids
2715           ORDER BY $order";
2717     $all = $DB->get_records_sql($sql , array_merge($params, $uparams));
2719     // Return results grouped by userid.
2720     $result = [];
2721     foreach ($all as $id => $record) {
2722         if (!isset($result[$record->userid])) {
2723             $result[$record->userid] = [];
2724         }
2725         $result[$record->userid][$record->id] = $record;
2726     }
2728     // Make sure all requested users are included in the result, even if they had no role assignments.
2729     foreach ($userids as $id) {
2730         if (!isset($result[$id])) {
2731             $result[$id] = [];
2732         }
2733     }
2735     return $result;
2739 /**
2740  * Gets all the user roles assigned in this context, or higher contexts
2741  * this is mainly used when checking if a user can assign a role, or overriding a role
2742  * i.e. we need to know what this user holds, in order to verify against allow_assign and
2743  * allow_override tables
2744  *
2745  * @param context $context
2746  * @param int $userid
2747  * @param bool $checkparentcontexts defaults to true
2748  * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
2749  * @return array
2750  */
2751 function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
2752     global $USER, $DB;
2754     if (empty($userid)) {
2755         if (empty($USER->id)) {
2756             return array();
2757         }
2758         $userid = $USER->id;
2759     }
2761     if ($checkparentcontexts) {
2762         $contextids = $context->get_parent_context_ids();
2763     } else {
2764         $contextids = array();
2765     }
2766     $contextids[] = $context->id;
2768     list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
2770     array_unshift($params, $userid);
2772     $sql = "SELECT ra.*, r.name, r.shortname
2773               FROM {role_assignments} ra, {role} r, {context} c
2774              WHERE ra.userid = ?
2775                    AND ra.roleid = r.id
2776                    AND ra.contextid = c.id
2777                    AND ra.contextid $contextids
2778           ORDER BY $order";
2780     return $DB->get_records_sql($sql ,$params);
2783 /**
2784  * Like get_user_roles, but adds in the authenticated user role, and the front
2785  * page roles, if applicable.
2786  *
2787  * @param context $context the context.
2788  * @param int $userid optional. Defaults to $USER->id
2789  * @return array of objects with fields ->userid, ->contextid and ->roleid.
2790  */
2791 function get_user_roles_with_special(context $context, $userid = 0) {
2792     global $CFG, $USER;
2794     if (empty($userid)) {
2795         if (empty($USER->id)) {
2796             return array();
2797         }
2798         $userid = $USER->id;
2799     }
2801     $ras = get_user_roles($context, $userid);
2803     // Add front-page role if relevant.
2804     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
2805     $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
2806             is_inside_frontpage($context);
2807     if ($defaultfrontpageroleid && $isfrontpage) {
2808         $frontpagecontext = context_course::instance(SITEID);
2809         $ra = new stdClass();
2810         $ra->userid = $userid;
2811         $ra->contextid = $frontpagecontext->id;
2812         $ra->roleid = $defaultfrontpageroleid;
2813         $ras[] = $ra;
2814     }
2816     // Add authenticated user role if relevant.
2817     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
2818     if ($defaultuserroleid && !isguestuser($userid)) {
2819         $systemcontext = context_system::instance();
2820         $ra = new stdClass();
2821         $ra->userid = $userid;
2822         $ra->contextid = $systemcontext->id;
2823         $ra->roleid = $defaultuserroleid;
2824         $ras[] = $ra;
2825     }
2827     return $ras;
2830 /**
2831  * Creates a record in the role_allow_override table
2832  *
2833  * @param int $sroleid source roleid
2834  * @param int $troleid target roleid
2835  * @return void
2836  */
2837 function allow_override($sroleid, $troleid) {
2838     global $DB;
2840     $record = new stdClass();
2841     $record->roleid        = $sroleid;
2842     $record->allowoverride = $troleid;
2843     $DB->insert_record('role_allow_override', $record);
2846 /**
2847  * Creates a record in the role_allow_assign table
2848  *
2849  * @param int $fromroleid source roleid
2850  * @param int $targetroleid target roleid
2851  * @return void
2852  */
2853 function allow_assign($fromroleid, $targetroleid) {
2854     global $DB;
2856     $record = new stdClass();
2857     $record->roleid      = $fromroleid;
2858     $record->allowassign = $targetroleid;
2859     $DB->insert_record('role_allow_assign', $record);
2862 /**
2863  * Creates a record in the role_allow_switch table
2864  *
2865  * @param int $fromroleid source roleid
2866  * @param int $targetroleid target roleid
2867  * @return void
2868  */
2869 function allow_switch($fromroleid, $targetroleid) {
2870     global $DB;
2872     $record = new stdClass();
2873     $record->roleid      = $fromroleid;
2874     $record->allowswitch = $targetroleid;
2875     $DB->insert_record('role_allow_switch', $record);
2878 /**
2879  * Gets a list of roles that this user can assign in this context
2880  *
2881  * @param context $context the context.
2882  * @param int $rolenamedisplay the type of role name to display. One of the
2883  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
2884  * @param bool $withusercounts if true, count the number of users with each role.
2885  * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
2886  * @return array if $withusercounts is false, then an array $roleid => $rolename.
2887  *      if $withusercounts is true, returns a list of three arrays,
2888  *      $rolenames, $rolecounts, and $nameswithcounts.
2889  */
2890 function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
2891     global $USER, $DB;
2893     // make sure there is a real user specified
2894     if ($user === null) {
2895         $userid = isset($USER->id) ? $USER->id : 0;
2896     } else {
2897         $userid = is_object($user) ? $user->id : $user;
2898     }
2900     if (!has_capability('moodle/role:assign', $context, $userid)) {
2901         if ($withusercounts) {
2902             return array(array(), array(), array());
2903         } else {
2904             return array();
2905         }
2906     }
2908     $params = array();
2909     $extrafields = '';
2911     if ($withusercounts) {
2912         $extrafields = ', (SELECT count(u.id)
2913                              FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
2914                             WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
2915                           ) AS usercount';
2916         $params['conid'] = $context->id;
2917     }
2919     if (is_siteadmin($userid)) {
2920         // show all roles allowed in this context to admins
2921         $assignrestriction = "";
2922     } else {
2923         $parents = $context->get_parent_context_ids(true);
2924         $contexts = implode(',' , $parents);
2925         $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
2926                                       FROM {role_allow_assign} raa
2927                                       JOIN {role_assignments} ra ON ra.roleid = raa.roleid
2928                                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
2929                                    ) ar ON ar.id = r.id";
2930         $params['userid'] = $userid;
2931     }
2932     $params['contextlevel'] = $context->contextlevel;
2934     if ($coursecontext = $context->get_course_context(false)) {
2935         $params['coursecontext'] = $coursecontext->id;
2936     } else {
2937         $params['coursecontext'] = 0; // no course aliases
2938         $coursecontext = null;
2939     }
2940     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
2941               FROM {role} r
2942               $assignrestriction
2943               JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
2944          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2945           ORDER BY r.sortorder ASC";
2946     $roles = $DB->get_records_sql($sql, $params);
2948     $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
2950     if (!$withusercounts) {
2951         return $rolenames;
2952     }
2954     $rolecounts = array();
2955     $nameswithcounts = array();
2956     foreach ($roles as $role) {
2957         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
2958         $rolecounts[$role->id] = $roles[$role->id]->usercount;
2959     }
2960     return array($rolenames, $rolecounts, $nameswithcounts);
2963 /**
2964  * Gets a list of roles that this user can switch to in a context
2965  *
2966  * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
2967  * This function just process the contents of the role_allow_switch table. You also need to
2968  * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
2969  *
2970  * @param context $context a context.
2971  * @return array an array $roleid => $rolename.
2972  */
2973 function get_switchable_roles(context $context) {
2974     global $USER, $DB;
2976     // You can't switch roles without this capability.
2977     if (!has_capability('moodle/role:switchroles', $context)) {
2978         return [];
2979     }
2981     $params = array();
2982     $extrajoins = '';
2983     $extrawhere = '';
2984     if (!is_siteadmin()) {
2985         // Admins are allowed to switch to any role with.
2986         // Others are subject to the additional constraint that the switch-to role must be allowed by
2987         // 'role_allow_switch' for some role they have assigned in this context or any parent.
2988         $parents = $context->get_parent_context_ids(true);
2989         $contexts = implode(',' , $parents);
2991         $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
2992         JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
2993         $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
2994         $params['userid'] = $USER->id;
2995     }
2997     if ($coursecontext = $context->get_course_context(false)) {
2998         $params['coursecontext'] = $coursecontext->id;
2999     } else {
3000         $params['coursecontext'] = 0; // no course aliases
3001         $coursecontext = null;
3002     }
3004     $query = "
3005         SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3006           FROM (SELECT DISTINCT rc.roleid
3007                   FROM {role_capabilities} rc
3008                   $extrajoins
3009                   $extrawhere) idlist
3010           JOIN {role} r ON r.id = idlist.roleid
3011      LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3012       ORDER BY r.sortorder";
3013     $roles = $DB->get_records_sql($query, $params);
3015     return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
3018 /**
3019  * Gets a list of roles that this user can override in this context.
3020  *
3021  * @param context $context the context.
3022  * @param int $rolenamedisplay the type of role name to display. One of the
3023  *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3024  * @param bool $withcounts if true, count the number of overrides that are set for each role.
3025  * @return array if $withcounts is false, then an array $roleid => $rolename.
3026  *      if $withusercounts is true, returns a list of three arrays,
3027  *      $rolenames, $rolecounts, and $nameswithcounts.
3028  */
3029 function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
3030     global $USER, $DB;
3032     if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
3033         if ($withcounts) {
3034             return array(array(), array(), array());
3035         } else {
3036             return array();
3037         }
3038     }
3040     $parents = $context->get_parent_context_ids(true);
3041     $contexts = implode(',' , $parents);
3043     $params = array();
3044     $extrafields = '';
3046     $params['userid'] = $USER->id;
3047     if ($withcounts) {
3048         $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
3049                 WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
3050         $params['conid'] = $context->id;
3051     }
3053     if ($coursecontext = $context->get_course_context(false)) {
3054         $params['coursecontext'] = $coursecontext->id;
3055     } else {
3056         $params['coursecontext'] = 0; // no course aliases
3057         $coursecontext = null;
3058     }
3060     if (is_siteadmin()) {
3061         // show all roles to admins
3062         $roles = $DB->get_records_sql("
3063             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3064               FROM {role} ro
3065          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3066           ORDER BY ro.sortorder ASC", $params);
3068     } else {
3069         $roles = $DB->get_records_sql("
3070             SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3071               FROM {role} ro
3072               JOIN (SELECT DISTINCT r.id
3073                       FROM {role} r
3074                       JOIN {role_allow_override} rao ON r.id = rao.allowoverride
3075                       JOIN {role_assignments} ra ON rao.roleid = ra.roleid
3076                      WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3077                    ) inline_view ON ro.id = inline_view.id
3078          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3079           ORDER BY ro.sortorder ASC", $params);
3080     }
3082     $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
3084     if (!$withcounts) {
3085         return $rolenames;
3086     }
3088     $rolecounts = array();
3089     $nameswithcounts = array();
3090     foreach ($roles as $role) {
3091         $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
3092         $rolecounts[$role->id] = $roles[$role->id]->overridecount;
3093     }
3094     return array($rolenames, $rolecounts, $nameswithcounts);
3097 /**
3098  * Create a role menu suitable for default role selection in enrol plugins.
3099  *
3100  * @package    core_enrol
3101  *
3102  * @param context $context
3103  * @param int $addroleid current or default role - always added to list
3104  * @return array roleid=>localised role name
3105  */
3106 function get_default_enrol_roles(context $context, $addroleid = null) {
3107     global $DB;
3109     $params = array('contextlevel'=>CONTEXT_COURSE);
3111     if ($coursecontext = $context->get_course_context(false)) {
3112         $params['coursecontext'] = $coursecontext->id;
3113     } else {
3114         $params['coursecontext'] = 0; // no course names
3115         $coursecontext = null;
3116     }
3118     if ($addroleid) {
3119         $addrole = "OR r.id = :addroleid";
3120         $params['addroleid'] = $addroleid;
3121     } else {
3122         $addrole = "";
3123     }
3125     $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3126               FROM {role} r
3127          LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
3128          LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3129              WHERE rcl.id IS NOT NULL $addrole
3130           ORDER BY sortorder DESC";
3132     $roles = $DB->get_records_sql($sql, $params);
3134     return role_fix_names($roles, $context, ROLENAME_BOTH, true);
3137 /**
3138  * Return context levels where this role is assignable.
3139  *
3140  * @param integer $roleid the id of a role.
3141  * @return array list of the context levels at which this role may be assigned.
3142  */
3143 function get_role_contextlevels($roleid) {
3144     global $DB;
3145     return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
3146             'contextlevel', 'id,contextlevel');
3149 /**
3150  * Return roles suitable for assignment at the specified context level.
3151  *
3152  * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
3153  *
3154  * @param integer $contextlevel a contextlevel.
3155  * @return array list of role ids that are assignable at this context level.
3156  */
3157 function get_roles_for_contextlevels($contextlevel) {
3158     global $DB;
3159     return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
3160             '', 'id,roleid');
3163 /**
3164  * Returns default context levels where roles can be assigned.
3165  *
3166  * @param string $rolearchetype one of the role archetypes - that is, one of the keys
3167  *      from the array returned by get_role_archetypes();
3168  * @return array list of the context levels at which this type of role may be assigned by default.
3169  */
3170 function get_default_contextlevels($rolearchetype) {
3171     static $defaults = array(
3172         'manager'        => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
3173         'coursecreator'  => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
3174         'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3175         'teacher'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3176         'student'        => array(CONTEXT_COURSE, CONTEXT_MODULE),
3177         'guest'          => array(),
3178         'user'           => array(),
3179         'frontpage'      => array());
3181     if (isset($defaults[$rolearchetype])) {
3182         return $defaults[$rolearchetype];
3183     } else {
3184         return array();
3185     }
3188 /**
3189  * Set the context levels at which a particular role can be assigned.
3190  * Throws exceptions in case of error.
3191  *
3192  * @param integer $roleid the id of a role.
3193  * @param array $contextlevels the context levels at which this role should be assignable,
3194  *      duplicate levels are removed.
3195  * @return void
3196  */
3197 function set_role_contextlevels($roleid, array $contextlevels) {
3198     global $DB;
3199     $DB->delete_records('role_context_levels', array('roleid' => $roleid));
3200     $rcl = new stdClass();
3201     $rcl->roleid = $roleid;
3202     $contextlevels = array_unique($contextlevels);
3203     foreach ($contextlevels as $level) {
3204         $rcl->contextlevel = $level;
3205         $DB->insert_record('role_context_levels', $rcl, false, true);
3206     }
3209 /**
3210  * Who has this capability in this context?
3211  *
3212  * This can be a very expensive call - use sparingly and keep
3213  * the results if you are going to need them again soon.
3214  *
3215  * Note if $fields is empty this function attempts to get u.*
3216  * which can get rather large - and has a serious perf impact
3217  * on some DBs.
3218  *
3219  * @param context $context
3220  * @param string|array $capability - capability name(s)
3221  * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
3222  * @param string $sort - the sort order. Default is lastaccess time.
3223  * @param mixed $limitfrom - number of records to skip (offset)
3224  * @param mixed $limitnum - number of records to fetch
3225  * @param string|array $groups - single group or array of groups - only return
3226  *               users who are in one of these group(s).
3227  * @param string|array $exceptions - list of users to exclude, comma separated or array
3228  * @param bool $doanything_ignored not used any more, admin accounts are never returned
3229  * @param bool $view_ignored - use get_enrolled_sql() instead
3230  * @param bool $useviewallgroups if $groups is set the return users who
3231  *               have capability both $capability and moodle/site:accessallgroups
3232  *               in this context, as well as users who have $capability and who are
3233  *               in $groups.
3234  * @return array of user records
3235  */
3236 function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
3237                                  $groups = '', $exceptions = '', $doanything_ignored = null, $view_ignored = null, $useviewallgroups = false) {
3238     global $CFG, $DB;
3240     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3241     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3243     $ctxids = trim($context->path, '/');
3244     $ctxids = str_replace('/', ',', $ctxids);
3246     // Context is the frontpage
3247     $iscoursepage = false; // coursepage other than fp
3248     $isfrontpage = false;
3249     if ($context->contextlevel == CONTEXT_COURSE) {
3250         if ($context->instanceid == SITEID) {
3251             $isfrontpage = true;
3252         } else {
3253             $iscoursepage = true;
3254         }
3255     }
3256     $isfrontpage = ($isfrontpage || is_inside_frontpage($context));
3258     $caps = (array)$capability;
3260     // construct list of context paths bottom-->top
3261     list($contextids, $paths) = get_context_info_list($context);
3263     // we need to find out all roles that have these capabilities either in definition or in overrides
3264     $defs = array();
3265     list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
3266     list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, 'cap');
3267     $params = array_merge($params, $params2);
3268     $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3269               FROM {role_capabilities} rc
3270               JOIN {context} ctx on rc.contextid = ctx.id
3271              WHERE rc.contextid $incontexts AND rc.capability $incaps";
3273     $rcs = $DB->get_records_sql($sql, $params);
3274     foreach ($rcs as $rc) {
3275         $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3276     }
3278     // go through the permissions bottom-->top direction to evaluate the current permission,
3279     // first one wins (prohibit is an exception that always wins)
3280     $access = array();
3281     foreach ($caps as $cap) {
3282         foreach ($paths as $path) {
3283             if (empty($defs[$cap][$path])) {
3284                 continue;
3285             }
3286             foreach($defs[$cap][$path] as $roleid => $perm) {
3287                 if ($perm == CAP_PROHIBIT) {
3288                     $access[$cap][$roleid] = CAP_PROHIBIT;
3289                     continue;
3290                 }
3291                 if (!isset($access[$cap][$roleid])) {
3292                     $access[$cap][$roleid] = (int)$perm;
3293                 }
3294             }
3295         }
3296     }
3298     // make lists of roles that are needed and prohibited in this context
3299     $needed = array(); // one of these is enough
3300     $prohibited = array(); // must not have any of these
3301     foreach ($caps as $cap) {
3302         if (empty($access[$cap])) {
3303             continue;
3304         }
3305         foreach ($access[$cap] as $roleid => $perm) {
3306             if ($perm == CAP_PROHIBIT) {
3307                 unset($needed[$cap][$roleid]);
3308                 $prohibited[$cap][$roleid] = true;
3309             } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
3310                 $needed[$cap][$roleid] = true;
3311             }
3312         }
3313         if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
3314             // easy, nobody has the permission
3315             unset($needed[$cap]);
3316             unset($prohibited[$cap]);
3317         } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
3318             // everybody is disqualified on the frontpage
3319             unset($needed[$cap]);
3320             unset($prohibited[$cap]);
3321         }
3322         if (empty($prohibited[$cap])) {
3323             unset($prohibited[$cap]);
3324         }
3325     }
3327     if (empty($needed)) {
3328         // there can not be anybody if no roles match this request
3329         return array();
3330     }
3332     if (empty($prohibited)) {
3333         // we can compact the needed roles
3334         $n = array();
3335         foreach ($needed as $cap) {
3336             foreach ($cap as $roleid=>$unused) {
3337                 $n[$roleid] = true;
3338             }
3339         }
3340         $needed = array('any'=>$n);
3341         unset($n);
3342     }
3344     // ***** Set up default fields ******
3345     if (empty($fields)) {
3346         if ($iscoursepage) {
3347             $fields = 'u.*, ul.timeaccess AS lastaccess';
3348         } else {
3349             $fields = 'u.*';
3350         }
3351     } else {
3352         if ($CFG->debugdeveloper && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
3353             debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
3354         }
3355     }
3357     // Set up default sort
3358     if (empty($sort)) { // default to course lastaccess or just lastaccess
3359         if ($iscoursepage) {
3360             $sort = 'ul.timeaccess';
3361         } else {
3362             $sort = 'u.lastaccess';
3363         }
3364     }
3366     // Prepare query clauses
3367     $wherecond = array();
3368     $params    = array();
3369     $joins     = array();
3371     // User lastaccess JOIN
3372     if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
3373          // user_lastaccess is not required MDL-13810
3374     } else {
3375         if ($iscoursepage) {
3376             $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
3377         } else {
3378             throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
3379         }
3380     }
3382     // We never return deleted users or guest account.
3383     $wherecond[] = "u.deleted = 0 AND u.id <> :guestid";
3384     $params['guestid'] = $CFG->siteguest;
3386     // Groups
3387     if ($groups) {
3388         $groups = (array)$groups;
3389         list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
3390         $grouptest = "u.id IN (SELECT userid FROM {groups_members} gm WHERE gm.groupid $grouptest)";
3391         $params = array_merge($params, $grpparams);
3393         if ($useviewallgroups) {
3394             $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
3395             if (!empty($viewallgroupsusers)) {
3396                 $wherecond[] =  "($grouptest OR u.id IN (" . implode(',', array_keys($viewallgroupsusers)) . '))';
3397             } else {
3398                 $wherecond[] =  "($grouptest)";
3399             }
3400         } else {
3401             $wherecond[] =  "($grouptest)";
3402         }
3403     }
3405     // User exceptions
3406     if (!empty($exceptions)) {
3407         $exceptions = (array)$exceptions;
3408         list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc', false);
3409         $params = array_merge($params, $exparams);
3410         $wherecond[] = "u.id $exsql";
3411     }
3413     // now add the needed and prohibited roles conditions as joins
3414     if (!empty($needed['any'])) {
3415         // simple case - there are no prohibits involved
3416         if (!empty($needed['any'][$defaultuserroleid]) or ($isfrontpage and !empty($needed['any'][$defaultfrontpageroleid]))) {
3417             // everybody
3418         } else {
3419             $joins[] = "JOIN (SELECT DISTINCT userid
3420                                 FROM {role_assignments}
3421                                WHERE contextid IN ($ctxids)
3422                                      AND roleid IN (".implode(',', array_keys($needed['any'])) .")
3423                              ) ra ON ra.userid = u.id";
3424         }
3425     } else {
3426         $unions = array();
3427         $everybody = false;
3428         foreach ($needed as $cap=>$unused) {
3429             if (empty($prohibited[$cap])) {
3430                 if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
3431                     $everybody = true;
3432                     break;
3433                 } else {
3434                     $unions[] = "SELECT userid
3435                                    FROM {role_assignments}
3436                                   WHERE contextid IN ($ctxids)
3437                                         AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
3438                 }
3439             } else {
3440                 if (!empty($prohibited[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
3441                     // nobody can have this cap because it is prevented in default roles
3442                     continue;
3444                 } else if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
3445                     // everybody except the prohibitted - hiding does not matter
3446                     $unions[] = "SELECT id AS userid
3447                                    FROM {user}
3448                                   WHERE id NOT IN (SELECT userid
3449                                                      FROM {role_assignments}
3450                                                     WHERE contextid IN ($ctxids)
3451                                                           AND roleid IN (".implode(',', array_keys($prohibited[$cap])) ."))";
3453                 } else {
3454                     $unions[] = "SELECT userid
3455                                    FROM {role_assignments}
3456                                   WHERE contextid IN ($ctxids) AND roleid IN (".implode(',', array_keys($needed[$cap])) .")
3457                                         AND userid NOT IN (
3458                                             SELECT userid
3459                                               FROM {role_assignments}
3460                                              WHERE contextid IN ($ctxids)
3461                                                     AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . ")
3462                                                         )";
3463                 }
3464             }
3465         }
3466         if (!$everybody) {
3467             if ($unions) {
3468                 $joins[] = "JOIN (SELECT DISTINCT userid FROM ( ".implode(' UNION ', $unions)." ) us) ra ON ra.userid = u.id";
3469             } else {
3470                 // only prohibits found - nobody can be matched
3471                 $wherecond[] = "1 = 2";
3472             }
3473         }
3474     }
3476     // Collect WHERE conditions and needed joins
3477     $where = implode(' AND ', $wherecond);
3478     if ($where !== '') {
3479         $where = 'WHERE ' . $where;
3480     }
3481     $joins = implode("\n", $joins);
3483     // Ok, let's get the users!
3484     $sql = "SELECT $fields
3485               FROM {user} u
3486             $joins
3487             $where
3488           ORDER BY $sort";
3490     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3493 /**
3494  * Re-sort a users array based on a sorting policy
3495  *
3496  * Will re-sort a $users results array (from get_users_by_capability(), usually)
3497  * based on a sorting policy. This is to support the odd practice of
3498  * sorting teachers by 'authority', where authority was "lowest id of the role
3499  * assignment".
3500  *
3501  * Will execute 1 database query. Only suitable for small numbers of users, as it
3502  * uses an u.id IN() clause.
3503  *
3504  * Notes about the sorting criteria.
3505  *
3506  * As a default, we cannot rely on role.sortorder because then
3507  * admins/coursecreators will always win. That is why the sane
3508  * rule "is locality matters most", with sortorder as 2nd
3509  * consideration.
3510  *
3511  * If you want role.sortorder, use the 'sortorder' policy, and
3512  * name explicitly what roles you want to cover. It's probably
3513  * a good idea to see what roles have the capabilities you want
3514  * (array_diff() them against roiles that have 'can-do-anything'
3515  * to weed out admin-ish roles. Or fetch a list of roles from
3516  * variables like $CFG->coursecontact .
3517  *
3518  * @param array $users Users array, keyed on userid
3519  * @param context $context
3520  * @param array $roles ids of the roles to include, optional
3521  * @param string $sortpolicy defaults to locality, more about
3522  * @return array sorted copy of the array
3523  */
3524 function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') {
3525     global $DB;
3527     $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
3528     $contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
3529     if (empty($roles)) {
3530         $roleswhere = '';
3531     } else {
3532         $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
3533     }
3535     $sql = "SELECT ra.userid
3536               FROM {role_assignments} ra
3537               JOIN {role} r
3538                    ON ra.roleid=r.id
3539               JOIN {context} ctx
3540                    ON ra.contextid=ctx.id
3541              WHERE $userswhere
3542                    $contextwhere
3543                    $roleswhere";
3545     // Default 'locality' policy -- read PHPDoc notes
3546     // about sort policies...
3547     $orderby = 'ORDER BY '
3548                     .'ctx.depth DESC, '  /* locality wins */
3549                     .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3550                     .'ra.id';            /* role assignment order tie-breaker */
3551     if ($sortpolicy === 'sortorder') {
3552         $orderby = 'ORDER BY '
3553                         .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3554                         .'ra.id';            /* role assignment order tie-breaker */
3555     }
3557     $sortedids = $DB->get_fieldset_sql($sql . $orderby);
3558     $sortedusers = array();
3559     $seen = array();
3561     foreach ($sortedids as $id) {
3562         // Avoid duplicates
3563         if (isset($seen[$id])) {
3564             continue;
3565         }
3566         $seen[$id] = true;
3568         // assign
3569         $sortedusers[$id] = $users[$id];
3570     }
3571     return $sortedusers;
3574 /**
3575  * Gets all the users assigned this role in this context or higher
3576  *
3577  * Note that moodle is based on capabilities and it is usually better
3578  * to check permissions than to check role ids as the capabilities
3579  * system is more flexible. If you really need, you can to use this
3580  * function but consider has_capability() as a possible substitute.
3581  *
3582  * All $sort fields are added into $fields if not present there yet.
3583  *
3584  * If $roleid is an array or is empty (all roles) you need to set $fields
3585  * (and $sort by extension) params according to it, as the first field
3586  * returned by the database should be unique (ra.id is the best candidate).
3587  *
3588  * @param int $roleid (can also be an array of ints!)
3589  * @param context $context
3590  * @param bool $parent if true, get list of users assigned in higher context too
3591  * @param string $fields fields from user (u.) , role assignment (ra) or role (r.)
3592  * @param string $sort sort from user (u.) , role assignment (ra.) or role (r.).
3593  *      null => use default sort from users_order_by_sql.
3594  * @param bool $all true means all, false means limit to enrolled users
3595  * @param string $group defaults to ''
3596  * @param mixed $limitfrom defaults to ''
3597  * @param mixed $limitnum defaults to ''
3598  * @param string $extrawheretest defaults to ''
3599  * @param array $whereorsortparams any paramter values used by $sort or $extrawheretest.
3600  * @return array
3601  */
3602 function get_role_users($roleid, context $context, $parent = false, $fields = '',
3603         $sort = null, $all = true, $group = '',
3604         $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereorsortparams = array()) {
3605     global $DB;
3607     if (empty($fields)) {
3608         $allnames = get_all_user_name_fields(true, 'u');
3609         $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
3610                   'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
3611                   'u.country, u.picture, u.idnumber, u.department, u.institution, '.
3612                   'u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name AS rolename, r.sortorder, '.
3613                   'r.shortname AS roleshortname, rn.name AS rolecoursealias';
3614     }
3616     // Prevent wrong function uses.
3617     if ((empty($roleid) || is_array($roleid)) && strpos($fields, 'ra.id') !== 0) {
3618         debugging('get_role_users() without specifying one single roleid needs to be called prefixing ' .
3619             'role assignments id (ra.id) as unique field, you can use $fields param for it.');
3621         if (!empty($roleid)) {
3622             // Solving partially the issue when specifying multiple roles.
3623             $users = array();
3624             foreach ($roleid as $id) {
3625                 // Ignoring duplicated keys keeping the first user appearance.
3626                 $users = $users + get_role_users($id, $context, $parent, $fields, $sort, $all, $group,
3627                     $limitfrom, $limitnum, $extrawheretest, $whereorsortparams);
3628             }
3629             return $users;
3630         }
3631     }
3633     $parentcontexts = '';
3634     if ($parent) {
3635         $parentcontexts = substr($context->path, 1); // kill leading slash
3636         $parentcontexts = str_replace('/', ',', $parentcontexts);
3637         if ($parentcontexts !== '') {
3638             $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
3639         }
3640     }
3642     if ($roleid) {
3643         list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'r');
3644         $roleselect = "AND ra.roleid $rids";
3645     } else {
3646         $params = array();
3647         $roleselect = '';
3648     }
3650     if ($coursecontext = $context->get_course_context(false)) {
3651         $params['coursecontext'] = $coursecontext->id;
3652     } else {
3653         $params['coursecontext'] = 0;
3654     }
3656     if ($group) {
3657         $groupjoin   = "JOIN {groups_members} gm ON gm.userid = u.id";
3658         $groupselect = " AND gm.groupid = :groupid ";
3659         $params['groupid'] = $group;
3660     } else {
3661         $groupjoin   = '';
3662         $groupselect = '';
3663     }
3665     $params['contextid'] = $context->id;