3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * This file contains functions for managing user access
21 * <b>Public API vs internals</b>
23 * General users probably only care about
26 * - get_context_instance()
27 * - get_context_instance_by_id()
28 * - get_parent_contexts()
29 * - get_child_contexts()
31 * Whether the user can do something...
33 * - has_any_capability()
34 * - has_all_capabilities()
35 * - require_capability()
36 * - require_login() (from moodlelib)
38 * What courses has this user access to?
39 * - get_user_courses_bycap()
41 * What users can do X in this context?
42 * - get_users_by_capability()
45 * - enrol_into_course()
46 * - role_assign()/role_unassign()
50 * - load_all_capabilities()
51 * - reload_all_capabilities()
52 * - has_capability_in_accessdata()
54 * - get_user_access_sitewide()
56 * - get_role_access_bycontext()
58 * <b>Name conventions</b>
64 * Access control data is held in the "accessdata" array
65 * which - for the logged-in user, will be in $USER->access
67 * For other users can be generated and passed around (but may also be cached
68 * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser.
70 * $accessdata is a multidimensional array, holding
71 * role assignments (RAs), role-capabilities-perm sets
72 * (role defs) and a list of courses we have loaded
75 * Things are keyed on "contextpaths" (the path field of
76 * the context table) for fast walking up/down the tree.
78 * $accessdata[ra][$contextpath]= array($roleid)
79 * [$contextpath]= array($roleid)
80 * [$contextpath]= array($roleid)
83 * Role definitions are stored like this
84 * (no cap merge is done - so it's compact)
87 * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
88 * [mod/forum:editallpost] = -1
89 * [mod/forum:startdiscussion] = -1000
92 * See how has_capability_in_accessdata() walks up/down the tree.
94 * Normally - specially for the logged-in user, we only load
95 * rdef and ra down to the course level, but not below. This
96 * keeps accessdata small and compact. Below-the-course ra/rdef
97 * are loaded as needed. We keep track of which courses we
98 * have loaded ra/rdef in
100 * $accessdata[loaded] = array($contextpath, $contextpath)
103 * <b>Stale accessdata</b>
105 * For the logged-in user, accessdata is long-lived.
107 * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
108 * context paths affected by changes. Any check at-or-below
109 * a dirty context will trigger a transparent reload of accessdata.
111 * Changes at the sytem level will force the reload for everyone.
113 * <b>Default role caps</b>
114 * The default role assignment is not in the DB, so we
115 * add it manually to accessdata.
117 * This means that functions that work directly off the
118 * DB need to ensure that the default role caps
119 * are dealt with appropriately.
121 * @package moodlecore
122 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
123 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
126 /** permission definitions */
127 define('CAP_INHERIT', 0);
128 /** permission definitions */
129 define('CAP_ALLOW', 1);
130 /** permission definitions */
131 define('CAP_PREVENT', -1);
132 /** permission definitions */
133 define('CAP_PROHIBIT', -1000);
135 /** context definitions */
136 define('CONTEXT_SYSTEM', 10);
137 /** context definitions */
138 define('CONTEXT_USER', 30);
139 /** context definitions */
140 define('CONTEXT_COURSECAT', 40);
141 /** context definitions */
142 define('CONTEXT_COURSE', 50);
143 /** context definitions */
144 define('CONTEXT_MODULE', 70);
145 /** context definitions */
146 define('CONTEXT_BLOCK', 80);
148 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
149 define('RISK_MANAGETRUST', 0x0001);
150 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
151 define('RISK_CONFIG', 0x0002);
152 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
153 define('RISK_XSS', 0x0004);
154 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
155 define('RISK_PERSONAL', 0x0008);
156 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
157 define('RISK_SPAM', 0x0010);
158 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
159 define('RISK_DATALOSS', 0x0020);
161 /** rolename displays - the name as defined in the role definition */
162 define('ROLENAME_ORIGINAL', 0);
163 /** rolename displays - the name as defined by a role alias */
164 define('ROLENAME_ALIAS', 1);
165 /** rolename displays - Both, like this: Role alias (Original)*/
166 define('ROLENAME_BOTH', 2);
167 /** rolename displays - the name as defined in the role definition and the shortname in brackets*/
168 define('ROLENAME_ORIGINALANDSHORT', 3);
169 /** rolename displays - the name as defined by a role alias, in raw form suitable for editing*/
170 define('ROLENAME_ALIAS_RAW', 4);
172 /** size limit for context cache */
173 if (!defined('MAX_CONTEXT_CACHE_SIZE')) {
174 define('MAX_CONTEXT_CACHE_SIZE', 5000);
178 * Although this looks like a global variable, it isn't really.
180 * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
181 * It is used to cache various bits of data between function calls for performance reasons.
182 * Sadly, a PHP global variale is the only way to impleemnt this, withough rewriting everything
183 * as methods of a class, instead of functions.
185 * @global stdClass $ACCESSLIB_PRIVATE
186 * @name $ACCESSLIB_PRIVATE
188 $ACCESSLIB_PRIVATE = new stdClass;
189 $ACCESSLIB_PRIVATE->contexts = array(); // Cache of context objects by level and instance
190 $ACCESSLIB_PRIVATE->contextsbyid = array(); // Cache of context objects by id
191 $ACCESSLIB_PRIVATE->systemcontext = NULL; // Used in get_system_context
192 $ACCESSLIB_PRIVATE->dirtycontexts = NULL; // Dirty contexts cache
193 $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the $accessdata structure for users other than $USER
194 $ACCESSLIB_PRIVATE->roledefinitions = array(); // role definitions cache - helps a lot with mem usage in cron
195 $ACCESSLIB_PRIVATE->croncache = array(); // Used in get_role_access
196 $ACCESSLIB_PRIVATE->preloadedcourses = array(); // Used in preload_course_contexts.
197 $ACCESSLIB_PRIVATE->capabilities = NULL; // detailed information about the capabilities
200 * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
202 * This method should ONLY BE USED BY UNIT TESTS. It clears all of
203 * accesslib's private caches. You need to do this before setting up test data,
204 * and also at the end fo the tests.
209 function accesslib_clear_all_caches_for_unit_testing() {
210 global $UNITTEST, $USER, $ACCESSLIB_PRIVATE;
211 if (empty($UNITTEST->running)) {
212 throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
214 $ACCESSLIB_PRIVATE->contexts = array();
215 $ACCESSLIB_PRIVATE->contextsbyid = array();
216 $ACCESSLIB_PRIVATE->systemcontext = NULL;
217 $ACCESSLIB_PRIVATE->dirtycontexts = NULL;
218 $ACCESSLIB_PRIVATE->accessdatabyuser = array();
219 $ACCESSLIB_PRIVATE->roledefinitions = array();
220 $ACCESSLIB_PRIVATE->croncache = array();
221 $ACCESSLIB_PRIVATE->preloadedcourses = array();
222 $ACCESSLIB_PRIVATE->capabilities = NULL;
224 unset($USER->access);
228 * Private function. Add a context object to accesslib's caches.
230 * @param object $context
232 function cache_context($context) {
233 global $ACCESSLIB_PRIVATE;
235 // If there are too many items in the cache already, remove items until
237 while (count($ACCESSLIB_PRIVATE->contextsbyid) >= MAX_CONTEXT_CACHE_SIZE) {
238 $first = reset($ACCESSLIB_PRIVATE->contextsbyid);
239 unset($ACCESSLIB_PRIVATE->contextsbyid[$first->id]);
240 unset($ACCESSLIB_PRIVATE->contexts[$first->contextlevel][$first->instanceid]);
243 $ACCESSLIB_PRIVATE->contexts[$context->contextlevel][$context->instanceid] = $context;
244 $ACCESSLIB_PRIVATE->contextsbyid[$context->id] = $context;
248 * This is really slow!!! do not use above course context level
252 * @param object $context
255 function get_role_context_caps($roleid, $context) {
258 //this is really slow!!!! - do not use above course context level!
260 $result[$context->id] = array();
262 // first emulate the parent context capabilities merging into context
263 $searchcontexts = array_reverse(get_parent_contexts($context));
264 array_push($searchcontexts, $context->id);
265 foreach ($searchcontexts as $cid) {
266 if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
267 foreach ($capabilities as $cap) {
268 if (!array_key_exists($cap->capability, $result[$context->id])) {
269 $result[$context->id][$cap->capability] = 0;
271 $result[$context->id][$cap->capability] += $cap->permission;
276 // now go through the contexts bellow given context
277 $searchcontexts = array_keys(get_child_contexts($context));
278 foreach ($searchcontexts as $cid) {
279 if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
280 foreach ($capabilities as $cap) {
281 if (!array_key_exists($cap->contextid, $result)) {
282 $result[$cap->contextid] = array();
284 $result[$cap->contextid][$cap->capability] = $cap->permission;
293 * Gets the accessdata for role "sitewide" (system down to course)
298 * @param array $accessdata defaults to NULL
301 function get_role_access($roleid, $accessdata=NULL) {
305 /* Get it in 1 cheap DB query...
306 * - relevant role caps at the root and down
307 * to the course level - but not below
309 if (is_null($accessdata)) {
310 $accessdata = array(); // named list
311 $accessdata['ra'] = array();
312 $accessdata['rdef'] = array();
313 $accessdata['loaded'] = array();
317 // Overrides for the role IN ANY CONTEXTS
318 // down to COURSE - not below -
320 $sql = "SELECT ctx.path,
321 rc.capability, rc.permission
323 JOIN {role_capabilities} rc
324 ON rc.contextid=ctx.id
326 AND ctx.contextlevel <= ".CONTEXT_COURSE."
327 ORDER BY ctx.depth, ctx.path";
328 $params = array($roleid);
330 // we need extra caching in CLI scripts and cron
332 global $ACCESSLIB_PRIVATE;
334 if (!isset($ACCESSLIB_PRIVATE->croncache[$roleid])) {
335 $ACCESSLIB_PRIVATE->croncache[$roleid] = array();
336 if ($rs = $DB->get_recordset_sql($sql, $params)) {
337 foreach ($rs as $rd) {
338 $ACCESSLIB_PRIVATE->croncache[$roleid][] = $rd;
344 foreach ($ACCESSLIB_PRIVATE->croncache[$roleid] as $rd) {
345 $k = "{$rd->path}:{$roleid}";
346 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
350 if ($rs = $DB->get_recordset_sql($sql, $params)) {
351 foreach ($rs as $rd) {
352 $k = "{$rd->path}:{$roleid}";
353 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
364 * Gets the accessdata for role "sitewide" (system down to course)
369 * @param array $accessdata defaults to NULL
372 function get_default_frontpage_role_access($roleid, $accessdata=NULL) {
376 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
377 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
380 // Overrides for the role in any contexts related to the course
382 $sql = "SELECT ctx.path,
383 rc.capability, rc.permission
385 JOIN {role_capabilities} rc
386 ON rc.contextid=ctx.id
388 AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?)
389 AND ctx.contextlevel <= ".CONTEXT_COURSE."
390 ORDER BY ctx.depth, ctx.path";
391 $params = array($roleid, "$base/%");
393 if ($rs = $DB->get_recordset_sql($sql, $params)) {
394 foreach ($rs as $rd) {
395 $k = "{$rd->path}:{$roleid}";
396 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
407 * Get the default guest role
411 * @return object role
413 function get_guest_role() {
416 if (empty($CFG->guestroleid)) {
417 if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
418 $guestrole = array_shift($roles); // Pick the first one
419 set_config('guestroleid', $guestrole->id);
422 debugging('Can not find any guest role!');
426 if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
429 //somebody is messing with guest roles, remove incorrect setting and try to find a new one
430 set_config('guestroleid', '');
431 return get_guest_role();
437 * Check whether a user has a paritcular capability in a given context.
440 * $context = get_context_instance(CONTEXT_MODULE, $cm->id);
441 * has_capability('mod/forum:replypost',$context)
443 * By default checks the capabilties of the current user, but you can pass a
444 * different userid. By default will return true for admin users, but you can override that with the fourth argument.
446 * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
447 * or capabilities with XSS, config or data loss risks.
449 * @param string $capability the name of the capability to check. For example mod/forum:view
450 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
451 * @param integer|object $user A user id or object. By default (NULL) checks the permissions of the current user.
452 * @param boolean $doanything If false, ignores effect of admin role assignment
453 * @return boolean true if the user has this capability. Otherwise false.
455 function has_capability($capability, $context, $user = NULL, $doanything=true) {
456 global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE;
458 if (during_initial_install()) {
459 if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") {
460 // we are in an installer - roles can not work yet
467 if (strpos($capability, 'moodle/legacy:') === 0) {
468 throw new coding_exception('Legacy capabilities can not be used any more!');
471 // the original $CONTEXT here was hiding serious errors
472 // for security reasons do not reuse previous context
473 if (empty($context)) {
474 debugging('Incorrect context specified');
477 if (!is_bool($doanything)) {
478 throw new coding_exception('Capability parameter "doanything" is wierd ("'.$doanything.'"). This has to be fixed in code.');
481 // make sure there is a real user specified
482 if ($user === NULL) {
483 $userid = !empty($USER->id) ? $USER->id : 0;
485 $userid = !empty($user->id) ? $user->id : $user;
488 // capability must exist
489 if (!$capinfo = get_capability_info($capability)) {
490 debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
493 // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
494 if (($capinfo->captype === 'write') or ((int)$capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
495 if (isguestuser($userid) or $userid == 0) {
500 if (is_null($context->path) or $context->depth == 0) {
501 //this should not happen
502 $contexts = array(SYSCONTEXTID, $context->id);
503 $context->path = '/'.SYSCONTEXTID.'/'.$context->id;
504 debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);
507 $contexts = explode('/', $context->path);
508 array_shift($contexts);
511 if (CLI_SCRIPT && !isset($USER->access)) {
512 // In cron, some modules setup a 'fake' $USER,
513 // ensure we load the appropriate accessdata.
514 if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
515 $ACCESSLIB_PRIVATE->dirtycontexts = NULL; //load fresh dirty contexts
517 load_user_accessdata($userid);
518 $ACCESSLIB_PRIVATE->dirtycontexts = array();
520 $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
522 } else if (isset($USER->id) && ($USER->id == $userid) && !isset($USER->access)) {
523 // caps not loaded yet - better to load them to keep BC with 1.8
524 // not-logged-in user or $USER object set up manually first time here
525 load_all_capabilities();
526 $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now
527 $ACCESSLIB_PRIVATE->roledefinitions = array();
530 // Load dirty contexts list if needed
531 if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
532 if (isset($USER->access['time'])) {
533 $ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']);
536 $ACCESSLIB_PRIVATE->dirtycontexts = array();
540 // Careful check for staleness...
541 if (count($ACCESSLIB_PRIVATE->dirtycontexts) !== 0 and is_contextpath_dirty($contexts, $ACCESSLIB_PRIVATE->dirtycontexts)) {
542 // reload all capabilities - preserving loginas, roleswitches, etc
543 // and then cleanup any marks of dirtyness... at least from our short
545 $ACCESSLIB_PRIVATE->accessdatabyuser = array();
546 $ACCESSLIB_PRIVATE->roledefinitions = array();
549 load_user_accessdata($userid);
550 $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
551 $ACCESSLIB_PRIVATE->dirtycontexts = array();
554 reload_all_capabilities();
558 // Find out if user is admin - it is not possible to override the doanything in any way
559 // and it is not possible to switch to admin role either.
561 if (is_siteadmin($userid)) {
566 // divulge how many times we are called
567 //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
569 if (isset($USER->id) && ($USER->id == $userid)) { // we must accept strings and integers in $userid
571 // For the logged in user, we have $USER->access
572 // which will have all RAs and caps preloaded for
573 // course and above contexts.
575 // Contexts below courses && contexts that do not
576 // hang from courses are loaded into $USER->access
577 // on demand, and listed in $USER->access[loaded]
579 if ($context->contextlevel <= CONTEXT_COURSE) {
580 // Course and above are always preloaded
581 return has_capability_in_accessdata($capability, $context, $USER->access);
583 // Load accessdata for below-the-course contexts
584 if (!path_inaccessdata($context->path,$USER->access)) {
585 // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
586 // $bt = debug_backtrace();
587 // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
588 load_subcontext($USER->id, $context, $USER->access);
590 return has_capability_in_accessdata($capability, $context, $USER->access);
593 if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
594 load_user_accessdata($userid);
597 if ($context->contextlevel <= CONTEXT_COURSE) {
598 // Course and above are always preloaded
599 return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
601 // Load accessdata for below-the-course contexts as needed
602 if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
603 // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
604 // $bt = debug_backtrace();
605 // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
606 load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
608 return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
612 * Check if the user has any one of several capabilities from a list.
614 * This is just a utility method that calls has_capability in a loop. Try to put
615 * the capabilities that most users are likely to have first in the list for best
618 * There are probably tricks that could be done to improve the performance here, for example,
619 * check the capabilities that are already cached first.
621 * @see has_capability()
622 * @param array $capabilities an array of capability names.
623 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
624 * @param integer $userid A user id. By default (NULL) checks the permissions of the current user.
625 * @param boolean $doanything If false, ignore effect of admin role assignment
626 * @return boolean true if the user has any of these capabilities. Otherwise false.
628 function has_any_capability($capabilities, $context, $userid=NULL, $doanything=true) {
629 if (!is_array($capabilities)) {
630 debugging('Incorrect $capabilities parameter in has_any_capabilities() call - must be an array');
633 foreach ($capabilities as $capability) {
634 if (has_capability($capability, $context, $userid, $doanything)) {
642 * Check if the user has all the capabilities in a list.
644 * This is just a utility method that calls has_capability in a loop. Try to put
645 * the capabilities that fewest users are likely to have first in the list for best
648 * There are probably tricks that could be done to improve the performance here, for example,
649 * check the capabilities that are already cached first.
651 * @see has_capability()
652 * @param array $capabilities an array of capability names.
653 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
654 * @param integer $userid A user id. By default (NULL) checks the permissions of the current user.
655 * @param boolean $doanything If false, ignore effect of admin role assignment
656 * @return boolean true if the user has all of these capabilities. Otherwise false.
658 function has_all_capabilities($capabilities, $context, $userid=NULL, $doanything=true) {
659 if (!is_array($capabilities)) {
660 debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
663 foreach ($capabilities as $capability) {
664 if (!has_capability($capability, $context, $userid, $doanything)) {
672 * Check if the user is an admin at the site level.
674 * Please note that use of proper capabilities is always encouraged,
675 * this function is supposed to be used from core or for temporary hacks.
677 * @param int|object $user_or_id user id or user object
678 * @returns bool true if user is one of the administrators, false otherwise
680 function is_siteadmin($user_or_id = NULL) {
683 if ($user_or_id === NULL) {
687 if (empty($user_or_id)) {
690 if (!empty($user_or_id->id)) {
692 $userid = $user_or_id->id;
694 $userid = $user_or_id;
697 $siteadmins = explode(',', $CFG->siteadmins);
698 return in_array($userid, $siteadmins);
702 * Returns true if user has at least one role assign
703 * of 'coursemanager' role (is potentially listed in some course descriptions).
705 * @return unknown_type
707 function has_coursemanager_role($userid) {
710 if (empty($CFG->coursemanager)) {
714 FROM {role_assignments}
715 WHERE userid = :userid AND roleid IN ($CFG->coursemanager)";
716 return $DB->record_exists($sql, array('userid'=>$userid));
720 * @param string $path
723 function get_course_from_path($path) {
724 // assume that nothing is more than 1 course deep
725 if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
732 * @param string $path
733 * @param array $accessdata
736 function path_inaccessdata($path, $accessdata) {
737 if (empty($accessdata['loaded'])) {
741 // assume that contexts hang from sys or from a course
742 // this will only work well with stuff that hangs from a course
743 if (in_array($path, $accessdata['loaded'], true)) {
744 // error_log("found it!");
747 $base = '/' . SYSCONTEXTID;
748 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
750 if ($path === $base) {
753 if (in_array($path, $accessdata['loaded'], true)) {
761 * Does the user have a capability to do something?
763 * Walk the accessdata array and return true/false.
764 * Deals with prohibits, roleswitching, aggregating
767 * The main feature of here is being FAST and with no
772 * Switch Roles exits early
773 * ------------------------
774 * cap checks within a switchrole need to exit early
775 * in our bottom up processing so they don't "see" that
776 * there are real RAs that can do all sorts of things.
778 * Switch Role merges with default role
779 * ------------------------------------
780 * If you are a teacher in course X, you have at least
781 * teacher-in-X + defaultloggedinuser-sitewide. So in the
782 * course you'll have techer+defaultloggedinuser.
783 * We try to mimic that in switchrole.
785 * Permission evaluation
786 * ---------------------
787 * Originaly there was an extremely complicated way
788 * to determine the user access that dealt with
789 * "locality" or role assignemnts and role overrides.
790 * Now we simply evaluate access for each roel separately
791 * and then verify if user has at least one role with allow
792 * and at the same time no role with prohibit.
794 * @param string $capability
795 * @param object $context
796 * @param array $accessdata
799 function has_capability_in_accessdata($capability, $context, array $accessdata) {
802 if (empty($context->id)) {
803 throw new coding_exception('Invalid context specified');
806 // Build $paths as a list of current + all parent "paths" with order bottom-to-top
807 $contextids = explode('/', trim($context->path, '/'));
808 $paths = array($context->path);
809 while ($contextids) {
810 array_pop($contextids);
811 $paths[] = '/' . implode('/', $contextids);
816 $switchedrole = false;
818 // Find out if role switched
819 if (!empty($accessdata['rsw'])) {
820 // From the bottom up...
821 foreach ($paths as $path) {
822 if (isset($accessdata['rsw'][$path])) {
823 // Found a switchrole assignment - check for that role _plus_ the default user role
824 $roles = array($accessdata['rsw'][$path]=>NULL, $CFG->defaultuserroleid=>NULL);
825 $switchedrole = true;
831 if (!$switchedrole) {
832 // get all users roles in this context and above
833 foreach ($paths as $path) {
834 if (isset($accessdata['ra'][$path])) {
835 foreach ($accessdata['ra'][$path] as $roleid) {
836 $roles[$roleid] = NULL;
842 // Now find out what access is given to each role, going bottom-->up direction
843 foreach ($roles as $roleid => $ignored) {
844 foreach ($paths as $path) {
845 if (isset($accessdata['rdef']["{$path}:$roleid"][$capability])) {
846 $perm = (int)$accessdata['rdef']["{$path}:$roleid"][$capability];
847 if ($perm === CAP_PROHIBIT or is_null($roles[$roleid])) {
848 $roles[$roleid] = $perm;
853 // any CAP_PROHIBIT found means no permission for the user
854 if (array_search(CAP_PROHIBIT, $roles) !== false) {
858 // at least one CAP_ALLOW means the user has a permission
859 return (array_search(CAP_ALLOW, $roles) !== false);
863 * @param object $context
864 * @param array $accessdata
867 function aggregate_roles_from_accessdata($context, $accessdata) {
869 $path = $context->path;
871 // build $contexts as a list of "paths" of the current
872 // contexts and parents with the order top-to-bottom
873 $contexts = array($path);
874 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
876 array_unshift($contexts, $path);
879 $cc = count($contexts);
882 // From the bottom up...
883 for ($n=$cc-1;$n>=0;$n--) {
884 $ctxp = $contexts[$n];
885 if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
886 // Found assignments on this leaf
887 $addroles = $accessdata['ra'][$ctxp];
888 $roles = array_merge($roles, $addroles);
892 return array_unique($roles);
896 * A convenience function that tests has_capability, and displays an error if
897 * the user does not have that capability.
899 * NOTE before Moodle 2.0, this function attempted to make an appropriate
900 * require_login call before checking the capability. This is no longer the case.
901 * You must call require_login (or one of its variants) if you want to check the
902 * user is logged in, before you call this function.
904 * @see has_capability()
906 * @param string $capability the name of the capability to check. For example mod/forum:view
907 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
908 * @param integer $userid A user id. By default (NULL) checks the permissions of the current user.
909 * @param bool $doanything If false, ignore effect of admin role assignment
910 * @param string $errorstring The error string to to user. Defaults to 'nopermissions'.
911 * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
912 * @return void terminates with an error if the user does not have the given capability.
914 function require_capability($capability, $context, $userid = NULL, $doanything = true,
915 $errormessage = 'nopermissions', $stringfile = '') {
916 if (!has_capability($capability, $context, $userid, $doanything)) {
917 throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
922 * Get an array of courses where cap requested is available
924 * Get an array of courses (with magic extra bits)
925 * where the accessdata and in DB enrolments show
926 * that the cap requested is available.
928 * The main use is for get_my_courses().
932 * - $fields is an array of fieldnames to ADD
933 * so name the fields you really need, which will
934 * be added and uniq'd
936 * - the course records have $c->context which is a fully
937 * valid context object. Saves you a query per course!
939 * - the course records have $c->categorypath to make
940 * category lookups cheap
942 * - current implementation is split in -
944 * - if the user has the cap systemwide, stupidly
945 * grab *every* course for a capcheck. This eats
946 * a TON of bandwidth, specially on large sites
947 * with separate DBs...
949 * - otherwise, fetch "likely" courses with a wide net
950 * that should get us _cheaply_ at least the courses we need, and some
951 * we won't - we get courses that...
952 * - are in a category where user has the cap
953 * - or where use has a role-assignment (any kind)
954 * - or where the course has an override on for this cap
956 * - walk the courses recordset checking the caps oneach one
957 * the checks are all in memory and quite fast
958 * (though we could implement a specialised variant of the
959 * has_capability_in_accessdata() code to speed it up)
963 * @param string $capability - name of the capability
964 * @param array $accessdata - accessdata session array
965 * @param bool $doanything_ignored - admin roles are completely ignored here
966 * @param string $sort - sorting fields - prefix each fieldname with "c."
967 * @param array $fields - additional fields you are interested in...
968 * @param int $limit - set if you want to limit the number of courses
969 * @return array $courses - ordered array of course objects - see notes above
971 function get_user_courses_bycap($userid, $cap, $accessdata, $doanything_ignored, $sort='c.sortorder ASC', $fields=NULL, $limit=0) {
975 // Slim base fields, let callers ask for what they need...
976 $basefields = array('id', 'sortorder', 'shortname', 'idnumber');
978 if (!is_null($fields)) {
979 $fields = array_merge($basefields, $fields);
980 $fields = array_unique($fields);
982 $fields = $basefields;
984 // If any of the fields is '*', leave it alone, discarding the rest
985 // to avoid ambiguous columns under some silly DBs. See MDL-18746 :-D
986 if (in_array('*', $fields)) {
987 $fields = array('*');
989 $coursefields = 'c.' .implode(',c.', $fields);
993 $sort = "ORDER BY $sort";
996 $sysctx = get_context_instance(CONTEXT_SYSTEM);
997 if (has_capability_in_accessdata($cap, $sysctx, $accessdata)) {
999 // Apparently the user has the cap sitewide, so walk *every* course
1000 // (the cap checks are moderately fast, but this moves massive bandwidth w the db)
1003 $sql = "SELECT $coursefields,
1004 ctx.id AS ctxid, ctx.path AS ctxpath,
1005 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1006 cc.path AS categorypath
1008 JOIN {course_categories} cc
1011 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1013 $rs = $DB->get_recordset_sql($sql);
1016 // narrow down where we have the caps to a few contexts
1017 // this will be a combination of
1018 // - courses where user has an explicit enrolment
1019 // - courses that have an override (any status) on that capability
1020 // - categories where user has the rights (granted status) on that capability
1022 $sql = "SELECT ctx.*
1024 WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
1025 ORDER BY ctx.depth";
1026 $rs = $DB->get_recordset_sql($sql);
1027 $catpaths = array();
1028 foreach ($rs as $catctx) {
1029 if ($catctx->path != ''
1030 && has_capability_in_accessdata($cap, $catctx, $accessdata)) {
1031 $catpaths[] = $catctx->path;
1037 if (count($catpaths)) {
1038 $cc = count($catpaths);
1039 for ($n=0;$n<$cc;$n++) {
1040 $catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'";
1042 $catclause = 'WHERE (' . implode(' OR ', $catpaths) .')';
1046 /// UNION 3 queries:
1047 /// - user role assignments in courses
1048 /// - user capability (override - any status) in courses
1049 /// - user right (granted status) in categories (optionally executed)
1050 /// Enclosing the 3-UNION into an inline_view to avoid column names conflict and making the ORDER BY cross-db
1051 /// and to allow selection of TEXT columns in the query (MSSQL and Oracle limitation). MDL-16209
1053 SELECT $coursefields, ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance, categorypath
1056 ctx.id AS ctxid, ctx.path AS ctxpath,
1057 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel, ctx.instanceid AS ctxinstance,
1058 cc.path AS categorypath
1060 JOIN {course_categories} cc
1063 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1064 JOIN {role_assignments} ra
1065 ON (ra.contextid=ctx.id AND ra.userid=:userid)
1068 ctx.id AS ctxid, ctx.path AS ctxpath,
1069 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel, ctx.instanceid AS ctxinstance,
1070 cc.path AS categorypath
1072 JOIN {course_categories} cc
1075 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1076 JOIN {role_capabilities} rc
1077 ON (rc.contextid=ctx.id AND (rc.capability=:cap)) ";
1079 if (!empty($catclause)) { /// If we have found the right in categories, add child courses here too
1083 ctx.id AS ctxid, ctx.path AS ctxpath,
1084 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel, ctx.instanceid AS ctxinstance,
1085 cc.path AS categorypath
1087 JOIN {course_categories} cc
1090 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1094 /// Close the inline_view and join with courses table to get requested $coursefields
1097 INNER JOIN {course} c
1098 ON inline_view.id = c.id";
1100 /// To keep cross-db we need to strip any prefix in the ORDER BY clause for queries using UNION
1102 " . preg_replace('/[a-z]+\./i', '', $sort); /// Add ORDER BY clause
1104 $params['userid'] = $userid;
1105 $params['cap'] = $cap;
1106 $rs = $DB->get_recordset_sql($sql, $params);
1109 /// Confirm rights (granted capability) for each course returned
1111 $cc = 0; // keep count
1113 foreach ($rs as $c) {
1114 // build the context obj
1115 context_instance_preload($c);
1116 $context = get_context_instance(CONTEXT_COURSE, $c->id);
1118 if (has_capability_in_accessdata($cap, $context, $accessdata)) {
1119 if ($limit > 0 && $cc >= $limit) {
1135 * Return a nested array showing role assignments
1136 * all relevant role capabilities for the user at
1137 * site/metacourse/course_category/course levels
1139 * We do _not_ delve deeper than courses because the number of
1140 * overrides at the module/block levels is HUGE.
1142 * [ra] => [/path/][]=roleid
1143 * [rdef] => [/path/:roleid][capability]=permission
1144 * [loaded] => array('/path', '/path')
1148 * @param $userid integer - the id of the user
1150 function get_user_access_sitewide($userid) {
1154 /* Get in 3 cheap DB queries...
1155 * - role assignments
1156 * - relevant role caps
1157 * - above and within this user's RAs
1158 * - below this user's RAs - limited to course level
1161 $accessdata = array(); // named list
1162 $accessdata['ra'] = array();
1163 $accessdata['rdef'] = array();
1164 $accessdata['loaded'] = array();
1169 $sql = "SELECT ctx.path, ra.roleid
1170 FROM {role_assignments} ra
1171 JOIN {context} ctx ON ctx.id=ra.contextid
1172 WHERE ra.userid = ? AND ctx.contextlevel <= ".CONTEXT_COURSE;
1173 $params = array($userid);
1174 $rs = $DB->get_recordset_sql($sql, $params);
1177 // raparents collects paths & roles we need to walk up
1178 // the parenthood to build the rdef
1180 $raparents = array();
1182 foreach ($rs as $ra) {
1183 // RAs leafs are arrays to support multi
1184 // role assignments...
1185 if (!isset($accessdata['ra'][$ra->path])) {
1186 $accessdata['ra'][$ra->path] = array();
1188 array_push($accessdata['ra'][$ra->path], $ra->roleid);
1190 // Concatenate as string the whole path (all related context)
1191 // for this role. This is damn faster than using array_merge()
1192 // Will unique them later
1193 if (isset($raparents[$ra->roleid])) {
1194 $raparents[$ra->roleid] .= $ra->path;
1196 $raparents[$ra->roleid] = $ra->path;
1203 // Walk up the tree to grab all the roledefs
1204 // of interest to our user...
1206 // NOTE: we use a series of IN clauses here - which
1207 // might explode on huge sites with very convoluted nesting of
1208 // categories... - extremely unlikely that the number of categories
1209 // and roletypes is so large that we hit the limits of IN()
1212 foreach ($raparents as $roleid=>$strcontexts) {
1213 $contexts = implode(',', array_unique(explode('/', trim($strcontexts, '/'))));
1214 if ($contexts ==! '') {
1218 $clauses .= "(roleid=? AND contextid IN ($contexts))";
1219 $cparams[] = $roleid;
1223 if ($clauses !== '') {
1224 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
1225 FROM {role_capabilities} rc
1227 ON rc.contextid=ctx.id
1231 $rs = $DB->get_recordset_sql($sql, $cparams);
1234 foreach ($rs as $rd) {
1235 $k = "{$rd->path}:{$rd->roleid}";
1236 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1244 // Overrides for the role assignments IN SUBCONTEXTS
1245 // (though we still do _not_ go below the course level.
1247 // NOTE that the JOIN w sctx is with 3-way triangulation to
1248 // catch overrides to the applicable role in any subcontext, based
1249 // on the path field of the parent.
1251 $sql = "SELECT sctx.path, ra.roleid,
1252 ctx.path AS parentpath,
1253 rco.capability, rco.permission
1254 FROM {role_assignments} ra
1256 ON ra.contextid=ctx.id
1258 ON (sctx.path LIKE " . $DB->sql_concat('ctx.path',"'/%'"). " )
1259 JOIN {role_capabilities} rco
1260 ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
1262 AND ctx.contextlevel <= ".CONTEXT_COURSECAT."
1263 AND sctx.contextlevel <= ".CONTEXT_COURSE."
1264 ORDER BY sctx.depth, sctx.path, ra.roleid";
1265 $params = array($userid);
1266 $rs = $DB->get_recordset_sql($sql, $params);
1268 foreach ($rs as $rd) {
1269 $k = "{$rd->path}:{$rd->roleid}";
1270 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1279 * Add to the access ctrl array the data needed by a user for a given context
1283 * @param integer $userid the id of the user
1284 * @param object $context needs path!
1285 * @param array $accessdata accessdata array
1287 function load_subcontext($userid, $context, &$accessdata) {
1291 /* Get the additional RAs and relevant rolecaps
1292 * - role assignments - with role_caps
1293 * - relevant role caps
1294 * - above this user's RAs
1295 * - below this user's RAs - limited to course level
1298 $base = "/" . SYSCONTEXTID;
1301 // Replace $context with the target context we will
1302 // load. Normally, this will be a course context, but
1303 // may be a different top-level context.
1308 // - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM
1309 // - BLOCK/MODULE/GROUP hanging from a course
1311 // For course contexts, we _already_ have the RAs
1312 // but the cost of re-fetching is minimal so we don't care.
1314 if ($context->contextlevel !== CONTEXT_COURSE
1315 && $context->path !== "$base/{$context->id}") {
1316 // Case BLOCK/MODULE/GROUP hanging from a course
1317 // Assumption: the course _must_ be our parent
1318 // If we ever see stuff nested further this needs to
1319 // change to do 1 query over the exploded path to
1320 // find out which one is the course
1321 $courses = explode('/',get_course_from_path($context->path));
1322 $targetid = array_pop($courses);
1323 $context = get_context_instance_by_id($targetid);
1328 // Role assignments in the context and below
1330 $sql = "SELECT ctx.path, ra.roleid
1331 FROM {role_assignments} ra
1333 ON ra.contextid=ctx.id
1335 AND (ctx.path = ? OR ctx.path LIKE ?)
1336 ORDER BY ctx.depth, ctx.path, ra.roleid";
1337 $params = array($userid, $context->path, $context->path."/%");
1338 $rs = $DB->get_recordset_sql($sql, $params);
1341 // Read in the RAs, preventing duplicates
1344 $localroles = array();
1346 foreach ($rs as $ra) {
1347 if (!isset($accessdata['ra'][$ra->path])) {
1348 $accessdata['ra'][$ra->path] = array();
1350 // only add if is not a repeat caused
1351 // by capability join...
1352 // (this check is cheaper than in_array())
1353 if ($lastseen !== $ra->path.':'.$ra->roleid) {
1354 $lastseen = $ra->path.':'.$ra->roleid;
1355 array_push($accessdata['ra'][$ra->path], $ra->roleid);
1356 array_push($localroles, $ra->roleid);
1363 // Walk up and down the tree to grab all the roledefs
1364 // of interest to our user...
1367 // - we use IN() but the number of roles is very limited.
1369 $courseroles = aggregate_roles_from_accessdata($context, $accessdata);
1371 // Do we have any interesting "local" roles?
1372 $localroles = array_diff($localroles,$courseroles); // only "new" local roles
1373 $wherelocalroles='';
1374 if (count($localroles)) {
1375 // Role defs for local roles in 'higher' contexts...
1376 $contexts = substr($context->path, 1); // kill leading slash
1377 $contexts = str_replace('/', ',', $contexts);
1378 $localroleids = implode(',',$localroles);
1379 $wherelocalroles="OR (rc.roleid IN ({$localroleids})
1380 AND ctx.id IN ($contexts))" ;
1383 // We will want overrides for all of them
1385 if ($roleids = implode(',',array_merge($courseroles,$localroles))) {
1386 $whereroles = "rc.roleid IN ($roleids) AND";
1388 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
1389 FROM {role_capabilities} rc
1391 ON rc.contextid=ctx.id
1393 (ctx.id=? OR ctx.path LIKE ?))
1395 ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1396 $params = array($context->id, $context->path."/%");
1398 $newrdefs = array();
1399 if ($rs = $DB->get_recordset_sql($sql, $params)) {
1400 foreach ($rs as $rd) {
1401 $k = "{$rd->path}:{$rd->roleid}";
1402 if (!array_key_exists($k, $newrdefs)) {
1403 $newrdefs[$k] = array();
1405 $newrdefs[$k][$rd->capability] = $rd->permission;
1409 debugging('Bad SQL encountered!');
1412 compact_rdefs($newrdefs);
1413 foreach ($newrdefs as $key=>$value) {
1414 $accessdata['rdef'][$key] =& $newrdefs[$key];
1417 // error_log("loaded {$context->path}");
1418 $accessdata['loaded'][] = $context->path;
1422 * Add to the access ctrl array the data needed by a role for a given context.
1424 * The data is added in the rdef key.
1426 * This role-centric function is useful for role_switching
1427 * and to get an overview of what a role gets under a
1428 * given context and below...
1432 * @param integer $roleid the id of the user
1433 * @param object $context needs path!
1434 * @param array $accessdata accessdata array NULL by default
1437 function get_role_access_bycontext($roleid, $context, $accessdata=NULL) {
1441 /* Get the relevant rolecaps into rdef
1442 * - relevant role caps
1443 * - at ctx and above
1447 if (is_null($accessdata)) {
1448 $accessdata = array(); // named list
1449 $accessdata['ra'] = array();
1450 $accessdata['rdef'] = array();
1451 $accessdata['loaded'] = array();
1454 $contexts = substr($context->path, 1); // kill leading slash
1455 $contexts = str_replace('/', ',', $contexts);
1458 // Walk up and down the tree to grab all the roledefs
1459 // of interest to our role...
1461 // NOTE: we use an IN clauses here - which
1462 // might explode on huge sites with very convoluted nesting of
1463 // categories... - extremely unlikely that the number of nested
1464 // categories is so large that we hit the limits of IN()
1466 $sql = "SELECT ctx.path, rc.capability, rc.permission
1467 FROM {role_capabilities} rc
1469 ON rc.contextid=ctx.id
1470 WHERE rc.roleid=? AND
1471 ( ctx.id IN ($contexts) OR
1473 ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1474 $params = array($roleid, $context->path."/%");
1476 if ($rs = $DB->get_recordset_sql($sql, $params)) {
1477 foreach ($rs as $rd) {
1478 $k = "{$rd->path}:{$roleid}";
1479 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1488 * Load accessdata for a user into the $ACCESSLIB_PRIVATE->accessdatabyuser global
1490 * Used by has_capability() - but feel free
1491 * to call it if you are about to run a BIG
1492 * cron run across a bazillion users.
1496 * @param int $userid
1497 * @return array returns ACCESSLIB_PRIVATE->accessdatabyuser[userid]
1499 function load_user_accessdata($userid) {
1500 global $CFG, $ACCESSLIB_PRIVATE;
1502 $base = '/'.SYSCONTEXTID;
1504 $accessdata = get_user_access_sitewide($userid);
1505 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
1507 // provide "default role" & set 'dr'
1509 if (!empty($CFG->defaultuserroleid)) {
1510 $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1511 if (!isset($accessdata['ra'][$base])) {
1512 $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1514 array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1516 $accessdata['dr'] = $CFG->defaultuserroleid;
1520 // provide "default frontpage role"
1522 if (!empty($CFG->defaultfrontpageroleid)) {
1523 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
1524 $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
1525 if (!isset($accessdata['ra'][$base])) {
1526 $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
1528 array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
1531 // for dirty timestamps in cron
1532 $accessdata['time'] = time();
1534 $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
1535 compact_rdefs($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['rdef']);
1537 return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
1541 * Use shared copy of role definistions stored in ACCESSLIB_PRIVATE->roledefinitions;
1544 * @param array $rdefs array of role definitions in contexts
1546 function compact_rdefs(&$rdefs) {
1547 global $ACCESSLIB_PRIVATE;
1550 * This is a basic sharing only, we could also
1551 * use md5 sums of values. The main purpose is to
1552 * reduce mem in cron jobs - many users in $ACCESSLIB_PRIVATE->accessdatabyuser array.
1555 foreach ($rdefs as $key => $value) {
1556 if (!array_key_exists($key, $ACCESSLIB_PRIVATE->roledefinitions)) {
1557 $ACCESSLIB_PRIVATE->roledefinitions[$key] = $rdefs[$key];
1559 $rdefs[$key] =& $ACCESSLIB_PRIVATE->roledefinitions[$key];
1564 * A convenience function to completely load all the capabilities
1565 * for the current user. This is what gets called from complete_user_login()
1566 * for example. Call it only _after_ you've setup $USER and called
1567 * check_enrolment_plugins();
1568 * @see check_enrolment_plugins()
1574 function load_all_capabilities() {
1575 global $USER, $CFG, $ACCESSLIB_PRIVATE;
1577 // roles not installed yet - we are in the middle of installation
1578 if (during_initial_install()) {
1582 $base = '/'.SYSCONTEXTID;
1584 if (isguestuser()) {
1585 $guest = get_guest_role();
1588 $USER->access = get_role_access($guest->id);
1589 // Put the ghost enrolment in place...
1590 $USER->access['ra'][$base] = array($guest->id);
1593 } else if (isloggedin()) {
1595 $accessdata = get_user_access_sitewide($USER->id);
1598 // provide "default role" & set 'dr'
1600 if (!empty($CFG->defaultuserroleid)) {
1601 $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1602 if (!isset($accessdata['ra'][$base])) {
1603 $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1605 array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1607 $accessdata['dr'] = $CFG->defaultuserroleid;
1610 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
1613 // provide "default frontpage role"
1615 if (!empty($CFG->defaultfrontpageroleid)) {
1616 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
1617 $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
1618 if (!isset($accessdata['ra'][$base])) {
1619 $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
1621 array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
1624 $USER->access = $accessdata;
1626 } else if (!empty($CFG->notloggedinroleid)) {
1627 $USER->access = get_role_access($CFG->notloggedinroleid);
1628 $USER->access['ra'][$base] = array($CFG->notloggedinroleid);
1631 // Timestamp to read dirty context timestamps later
1632 $USER->access['time'] = time();
1633 $ACCESSLIB_PRIVATE->dirtycontexts = array();
1635 // Clear to force a refresh
1636 unset($USER->mycourses);
1640 * A convenience function to completely reload all the capabilities
1641 * for the current user when roles have been updated in a relevant
1642 * context -- but PRESERVING switchroles and loginas.
1644 * That is - completely transparent to the user.
1646 * Note: rewrites $USER->access completely.
1651 function reload_all_capabilities() {
1654 // error_log("reloading");
1657 if (isset($USER->access['rsw'])) {
1658 $sw = $USER->access['rsw'];
1659 // error_log(print_r($sw,1));
1662 unset($USER->access);
1663 unset($USER->mycourses);
1665 load_all_capabilities();
1667 foreach ($sw as $path => $roleid) {
1668 $context = $DB->get_record('context', array('path'=>$path));
1669 role_switch($roleid, $context);
1675 * Adds a temp role to an accessdata array.
1677 * Useful for the "temporary guest" access
1678 * we grant to logged-in users.
1680 * Note - assumes a course context!
1684 * @param object $content
1685 * @param int $roleid
1686 * @param array $accessdata
1687 * @return array Returns access data
1689 function load_temp_role($context, $roleid, $accessdata) {
1694 // Load rdefs for the role in -
1696 // - all the parents
1697 // - and below - IOWs overrides...
1700 // turn the path into a list of context ids
1701 $contexts = substr($context->path, 1); // kill leading slash
1702 $contexts = str_replace('/', ',', $contexts);
1704 $sql = "SELECT ctx.path, rc.capability, rc.permission
1706 JOIN {role_capabilities} rc
1707 ON rc.contextid=ctx.id
1708 WHERE (ctx.id IN ($contexts)
1711 ORDER BY ctx.depth, ctx.path";
1712 $params = array($context->path."/%", $roleid);
1713 if ($rs = $DB->get_recordset_sql($sql, $params)) {
1714 foreach ($rs as $rd) {
1715 $k = "{$rd->path}:{$roleid}";
1716 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1722 // Say we loaded everything for the course context
1723 // - which we just did - if the user gets a proper
1724 // RA in this session, this data will need to be reloaded,
1725 // but that is handled by the complete accessdata reload
1727 array_push($accessdata['loaded'], $context->path);
1732 if (isset($accessdata['ra'][$context->path])) {
1733 array_push($accessdata['ra'][$context->path], $roleid);
1735 $accessdata['ra'][$context->path] = array($roleid);
1743 * Check all the login enrolment information for the given user object
1744 * by querying the enrolment plugins
1747 * @param object $user
1750 function check_enrolment_plugins(&$user) {
1753 if (empty($user->id) or isguestuser($user)) {
1754 // shortcut - there is no enrolment work for guests and not-logged-in users
1758 static $inprogress = array(); // To prevent this function being called more than once in an invocation
1760 if (!empty($inprogress[$user->id])) {
1764 $inprogress[$user->id] = true; // Set the flag
1766 require_once($CFG->dirroot .'/enrol/enrol.class.php');
1768 if (!($plugins = explode(',', $CFG->enrol_plugins_enabled))) {
1769 $plugins = array($CFG->enrol);
1772 foreach ($plugins as $plugin) {
1773 $enrol = enrolment_factory::factory($plugin);
1774 if (method_exists($enrol, 'setup_enrolments')) { /// Plugin supports Roles (Moodle 1.7 and later)
1775 $enrol->setup_enrolments($user);
1776 } else { /// Run legacy enrolment methods
1777 if (method_exists($enrol, 'get_student_courses')) {
1778 $enrol->get_student_courses($user);
1780 if (method_exists($enrol, 'get_teacher_courses')) {
1781 $enrol->get_teacher_courses($user);
1784 /// deal with $user->students and $user->teachers stuff
1785 unset($user->student);
1786 unset($user->teacher);
1791 unset($inprogress[$user->id]); // Unset the flag
1795 * Returns array of all role archetypes.
1799 function get_role_archetypes() {
1801 'manager' => 'manager',
1802 'coursecreator' => 'coursecreator',
1803 'editingteacher' => 'editingteacher',
1804 'teacher' => 'teacher',
1805 'student' => 'student',
1808 'frontpage' => 'frontpage'
1813 * Assign the defaults found in this capabality definition to roles that have
1814 * the corresponding legacy capabilities assigned to them.
1816 * @param string $capability
1817 * @param array $legacyperms an array in the format (example):
1818 * 'guest' => CAP_PREVENT,
1819 * 'student' => CAP_ALLOW,
1820 * 'teacher' => CAP_ALLOW,
1821 * 'editingteacher' => CAP_ALLOW,
1822 * 'coursecreator' => CAP_ALLOW,
1823 * 'manager' => CAP_ALLOW
1824 * @return boolean success or failure.
1826 function assign_legacy_capabilities($capability, $legacyperms) {
1828 $archetypes = get_role_archetypes();
1830 foreach ($legacyperms as $type => $perm) {
1832 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
1833 if ($type === 'admin') {
1834 debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1838 if (!array_key_exists($type, $archetypes)) {
1839 print_error('invalidlegacy', '', '', $type);
1842 if ($roles = get_archetype_roles($type)) {
1843 foreach ($roles as $role) {
1844 // Assign a site level capability.
1845 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1855 * @param object $capability a capbility - a row from the capabilitites table.
1856 * @return boolean whether this capability is safe - that is, wether people with the
1857 * safeoverrides capability should be allowed to change it.
1859 function is_safe_capability($capability) {
1860 return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1863 /**********************************
1864 * Context Manipulation functions *
1865 **********************************/
1868 * Create a new context record for use by all roles-related stuff
1870 * Create a new context record for use by all roles-related stuff
1871 * assumes that the caller has done the homework.
1875 * @param int $contextlevel
1876 * @param int $instanceid
1877 * @param int $strictness
1878 * @return object newly created context
1880 function create_context($contextlevel, $instanceid, $strictness=IGNORE_MISSING) {
1884 if ($contextlevel == CONTEXT_SYSTEM) {
1885 return create_system_context();
1888 $context = new object();
1889 $context->contextlevel = $contextlevel;
1890 $context->instanceid = $instanceid;
1892 // Define $context->path based on the parent
1893 // context. In other words... Who is your daddy?
1894 $basepath = '/' . SYSCONTEXTID;
1898 $error_message = NULL;
1900 switch ($contextlevel) {
1901 case CONTEXT_COURSECAT:
1902 $sql = "SELECT ctx.path, ctx.depth
1904 JOIN {course_categories} cc
1905 ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
1907 $params = array($instanceid);
1908 if ($p = $DB->get_record_sql($sql, $params)) {
1909 $basepath = $p->path;
1910 $basedepth = $p->depth;
1911 } else if ($category = $DB->get_record('course_categories', array('id'=>$instanceid), '*', $strictness)) {
1912 if (empty($category->parent)) {
1913 // ok - this is a top category
1914 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) {
1915 $basepath = $parent->path;
1916 $basedepth = $parent->depth;
1918 // wrong parent category - no big deal, this can be fixed later
1923 // incorrect category id
1924 $error_message = "incorrect course category id ($instanceid)";
1929 case CONTEXT_COURSE:
1930 $sql = "SELECT ctx.path, ctx.depth
1933 ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
1934 WHERE c.id=? AND c.id !=" . SITEID;
1935 $params = array($instanceid);
1936 if ($p = $DB->get_record_sql($sql, $params)) {
1937 $basepath = $p->path;
1938 $basedepth = $p->depth;
1939 } else if ($course = $DB->get_record('course', array('id'=>$instanceid), '*', $strictness)) {
1940 if ($course->id == SITEID) {
1941 //ok - no parent category
1942 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) {
1943 $basepath = $parent->path;
1944 $basedepth = $parent->depth;
1946 // wrong parent category of course - no big deal, this can be fixed later
1950 } else if ($instanceid == SITEID) {
1951 // no errors for missing site course during installation
1954 // incorrect course id
1955 $error_message = "incorrect course id ($instanceid)";
1960 case CONTEXT_MODULE:
1961 $sql = "SELECT ctx.path, ctx.depth
1963 JOIN {course_modules} cm
1964 ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1966 $params = array($instanceid);
1967 if ($p = $DB->get_record_sql($sql, $params)) {
1968 $basepath = $p->path;
1969 $basedepth = $p->depth;
1970 } else if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid), '*', $strictness)) {
1971 if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course, $strictness)) {
1972 $basepath = $parent->path;
1973 $basedepth = $parent->depth;
1975 // course does not exist - modules can not exist without a course
1976 $error_message = "course does not exist ($cm->course) - modules can not exist without a course";
1980 // cm does not exist
1981 $error_message = "cm with id $instanceid does not exist";
1987 $sql = "SELECT ctx.path, ctx.depth
1989 JOIN {block_instances} bi ON (bi.parentcontextid=ctx.id)
1991 $params = array($instanceid, CONTEXT_COURSE);
1992 if ($p = $DB->get_record_sql($sql, $params, '*', $strictness)) {
1993 $basepath = $p->path;
1994 $basedepth = $p->depth;
1996 // block does not exist
1997 $error_message = 'block or parent context does not exist';
2002 // default to basepath
2006 // if grandparents unknown, maybe rebuild_context_path() will solve it later
2007 if ($basedepth != 0) {
2008 $context->depth = $basedepth+1;
2012 debugging('Error: could not insert new context level "'.
2013 s($contextlevel).'", instance "'.
2014 s($instanceid).'". ' . $error_message);
2019 $id = $DB->insert_record('context', $context);
2020 // can't set the full path till we know the id!
2021 if ($basedepth != 0 and !empty($basepath)) {
2022 $DB->set_field('context', 'path', $basepath.'/'. $id, array('id'=>$id));
2024 return get_context_instance_by_id($id);
2028 * Returns system context or NULL if can not be created yet.
2030 * @todo can not use get_record() because we do not know if query failed :-(
2031 * switch to get_record() later
2035 * @param bool $cache use caching
2036 * @return mixed system context or NULL
2038 function get_system_context($cache=true) {
2039 global $DB, $ACCESSLIB_PRIVATE;
2040 if ($cache and defined('SYSCONTEXTID')) {
2041 if (is_null($ACCESSLIB_PRIVATE->systemcontext)) {
2042 $ACCESSLIB_PRIVATE->systemcontext = new object();
2043 $ACCESSLIB_PRIVATE->systemcontext->id = SYSCONTEXTID;
2044 $ACCESSLIB_PRIVATE->systemcontext->contextlevel = CONTEXT_SYSTEM;
2045 $ACCESSLIB_PRIVATE->systemcontext->instanceid = 0;
2046 $ACCESSLIB_PRIVATE->systemcontext->path = '/'.SYSCONTEXTID;
2047 $ACCESSLIB_PRIVATE->systemcontext->depth = 1;
2049 return $ACCESSLIB_PRIVATE->systemcontext;
2052 $context = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM));
2053 } catch (dml_exception $e) {
2054 //table does not exist yet, sorry
2059 $context = new object();
2060 $context->contextlevel = CONTEXT_SYSTEM;
2061 $context->instanceid = 0;
2062 $context->depth = 1;
2063 $context->path = NULL; //not known before insert
2066 $context->id = $DB->insert_record('context', $context);
2067 } catch (dml_exception $e) {
2068 // can not create context yet, sorry
2073 if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) {
2074 $context->instanceid = 0;
2075 $context->path = '/'.$context->id;
2076 $context->depth = 1;
2077 $DB->update_record('context', $context);
2080 if (!defined('SYSCONTEXTID')) {
2081 define('SYSCONTEXTID', $context->id);
2084 $ACCESSLIB_PRIVATE->systemcontext = $context;
2085 return $ACCESSLIB_PRIVATE->systemcontext;
2089 * Remove a context record and any dependent entries,
2090 * removes context from static context cache too
2095 * @param int $instanceid
2096 * @return bool properly deleted
2098 function delete_context($contextlevel, $instanceid) {
2099 global $DB, $ACCESSLIB_PRIVATE, $CFG;
2101 // do not use get_context_instance(), because the related object might not exist,
2102 // or the context does not exist yet and it would be created now
2103 if ($context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
2104 $result = $DB->delete_records('role_assignments', array('contextid'=>$context->id)) &&
2105 $DB->delete_records('role_capabilities', array('contextid'=>$context->id)) &&
2106 $DB->delete_records('context', array('id'=>$context->id)) &&
2107 $DB->delete_records('role_names', array('contextid'=>$context->id));
2109 // do not mark dirty contexts if parents unknown
2110 if (!is_null($context->path) and $context->depth > 0) {
2111 mark_context_dirty($context->path);
2114 // purge static context cache if entry present
2115 unset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instanceid]);
2116 unset($ACCESSLIB_PRIVATE->contextsbyid[$context->id]);
2118 blocks_delete_all_for_context($context->id);
2119 filter_delete_all_for_context($context->id);
2129 * Precreates all contexts including all parents
2132 * @param int $contextlevel empty means all
2133 * @param bool $buildpaths update paths and depths
2136 function create_contexts($contextlevel=NULL, $buildpaths=true) {
2139 //make sure system context exists
2140 $syscontext = get_system_context(false);
2142 if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT
2143 or $contextlevel == CONTEXT_COURSE
2144 or $contextlevel == CONTEXT_MODULE
2145 or $contextlevel == CONTEXT_BLOCK) {
2146 $sql = "INSERT INTO {context} (contextlevel, instanceid)
2147 SELECT ".CONTEXT_COURSECAT.", cc.id
2148 FROM {course}_categories cc
2149 WHERE NOT EXISTS (SELECT 'x'
2151 WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
2156 if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE
2157 or $contextlevel == CONTEXT_MODULE
2158 or $contextlevel == CONTEXT_BLOCK) {
2159 $sql = "INSERT INTO {context} (contextlevel, instanceid)
2160 SELECT ".CONTEXT_COURSE.", c.id
2162 WHERE NOT EXISTS (SELECT 'x'
2164 WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
2169 if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE
2170 or $contextlevel == CONTEXT_BLOCK) {
2171 $sql = "INSERT INTO {context} (contextlevel, instanceid)
2172 SELECT ".CONTEXT_MODULE.", cm.id
2173 FROM {course}_modules cm
2174 WHERE NOT EXISTS (SELECT 'x'
2176 WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
2180 if (empty($contextlevel) or $contextlevel == CONTEXT_USER
2181 or $contextlevel == CONTEXT_BLOCK) {
2182 $sql = "INSERT INTO {context} (contextlevel, instanceid)
2183 SELECT ".CONTEXT_USER.", u.id
2186 AND NOT EXISTS (SELECT 'x'
2188 WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
2193 if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) {
2194 $sql = "INSERT INTO {context} (contextlevel, instanceid)
2195 SELECT ".CONTEXT_BLOCK.", bi.id
2196 FROM {block_instances} bi
2197 WHERE NOT EXISTS (SELECT 'x'
2199 WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
2204 build_context_path(false);
2209 * Remove stale context records
2214 function cleanup_contexts() {
2217 $sql = " SELECT c.contextlevel,
2218 c.instanceid AS instanceid
2220 LEFT OUTER JOIN {course}_categories t
2221 ON c.instanceid = t.id
2222 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
2224 SELECT c.contextlevel,
2227 LEFT OUTER JOIN {course} t
2228 ON c.instanceid = t.id
2229 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
2231 SELECT c.contextlevel,
2234 LEFT OUTER JOIN {course}_modules t
2235 ON c.instanceid = t.id
2236 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
2238 SELECT c.contextlevel,
2241 LEFT OUTER JOIN {user} t
2242 ON c.instanceid = t.id
2243 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
2245 SELECT c.contextlevel,
2248 LEFT OUTER JOIN {block_instances} t
2249 ON c.instanceid = t.id
2250 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
2253 // transactions used only for performance reasons here
2254 $transaction = $DB->start_delegated_transaction();
2256 if ($rs = $DB->get_recordset_sql($sql)) {
2257 foreach ($rs as $ctx) {
2258 delete_context($ctx->contextlevel, $ctx->instanceid);
2263 $transaction->allow_commit();
2268 * Preloads all contexts relating to a course: course, modules. Block contexts
2269 * are no longer loaded here. The contexts for all the blocks on the current
2270 * page are now efficiently loaded by {@link block_manager::load_blocks()}.
2272 * @param int $courseid Course ID
2275 function preload_course_contexts($courseid) {
2276 global $DB, $ACCESSLIB_PRIVATE;
2278 // Users can call this multiple times without doing any harm
2279 global $ACCESSLIB_PRIVATE;
2280 if (array_key_exists($courseid, $ACCESSLIB_PRIVATE->preloadedcourses)) {
2284 $params = array($courseid, $courseid, $courseid);
2285 $sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2286 FROM {course_modules} cm
2287 JOIN {context} x ON x.instanceid=cm.id
2288 WHERE cm.course=? AND x.contextlevel=".CONTEXT_MODULE."
2292 SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2294 WHERE x.instanceid=? AND x.contextlevel=".CONTEXT_COURSE."";
2296 $rs = $DB->get_recordset_sql($sql, $params);
2297 foreach($rs as $context) {
2298 cache_context($context);
2301 $ACCESSLIB_PRIVATE->preloadedcourses[$courseid] = true;
2305 * Get the context instance as an object. This function will create the
2306 * context instance if it does not exist yet.
2308 * @todo Remove code branch from previous fix MDL-9016 which is no longer needed
2310 * @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE.
2311 * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id,
2312 * for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0
2313 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
2314 * MUST_EXIST means throw exception if no record or multiple records found
2315 * @return object The context object.
2317 function get_context_instance($contextlevel, $instance=0, $strictness=IGNORE_MISSING) {
2319 global $DB, $ACCESSLIB_PRIVATE;
2320 static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_BLOCK);
2322 if ($contextlevel === 'clearcache') {
2323 // TODO: Remove for v2.0
2324 // No longer needed, but we'll catch it to avoid erroring out on custom code.
2325 // This used to be a fix for MDL-9016
2326 // "Restoring into existing course, deleting first
2327 // deletes context and doesn't recreate it"
2331 /// System context has special cache
2332 if ($contextlevel == CONTEXT_SYSTEM) {
2333 return get_system_context();
2336 /// check allowed context levels
2337 if (!in_array($contextlevel, $allowed_contexts)) {
2338 // fatal error, code must be fixed - probably typo or switched parameters
2339 print_error('invalidcourselevel');
2342 if (!is_array($instance)) {
2344 if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) { // Already cached
2345 return $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
2348 /// Get it from the database, or create it
2349 if (!$context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instance))) {
2350 $context = create_context($contextlevel, $instance, $strictness);
2353 /// Only add to cache if context isn't empty.
2354 if (!empty($context)) {
2355 cache_context($context);
2362 /// ok, somebody wants to load several contexts to save some db queries ;-)
2363 $instances = $instance;
2366 foreach ($instances as $key=>$instance) {
2367 /// Check the cache first
2368 if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) { // Already cached
2369 $result[$instance] = $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
2370 unset($instances[$key]);
2376 list($instanceids, $params) = $DB->get_in_or_equal($instances, SQL_PARAMS_QM);
2377 array_unshift($params, $contextlevel);
2378 $sql = "SELECT instanceid, id, contextlevel, path, depth
2380 WHERE contextlevel=? AND instanceid $instanceids";
2382 if (!$contexts = $DB->get_records_sql($sql, $params)) {
2383 $contexts = array();
2386 foreach ($instances as $instance) {
2387 if (isset($contexts[$instance])) {
2388 $context = $contexts[$instance];
2390 $context = create_context($contextlevel, $instance);
2393 if (!empty($context)) {
2394 cache_context($context);
2397 $result[$instance] = $context;
2406 * Get a context instance as an object, from a given context id.
2408 * @param mixed $id a context id or array of ids.
2409 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
2410 * MUST_EXIST means throw exception if no record or multiple records found
2411 * @return mixed object, array of the context object, or false.
2413 function get_context_instance_by_id($id, $strictness=IGNORE_MISSING) {
2414 global $DB, $ACCESSLIB_PRIVATE;
2416 if ($id == SYSCONTEXTID) {
2417 return get_system_context();
2420 if (isset($ACCESSLIB_PRIVATE->contextsbyid[$id])) { // Already cached
2421 return $ACCESSLIB_PRIVATE->contextsbyid[$id];
2424 if ($context = $DB->get_record('context', array('id'=>$id), '*', $strictness)) {
2425 cache_context($context);
2434 * Get the local override (if any) for a given capability in a role in a context
2437 * @param int $roleid
2438 * @param int $contextid
2439 * @param string $capability
2441 function get_local_override($roleid, $contextid, $capability) {
2443 return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
2447 * Returns context instance plus related course and cm instances
2448 * @param int $contextid
2449 * @return array of ($context, $course, $cm)
2451 function get_context_info_array($contextid) {
2454 $context = get_context_instance_by_id($contextid, MUST_EXIST);
2458 if ($context->contextlevel == CONTEXT_COURSE) {
2459 $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
2461 } else if ($context->contextlevel == CONTEXT_MODULE) {
2462 $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
2463 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
2465 } else if ($context->contextlevel == CONTEXT_BLOCK) {
2466 $parentcontexts = get_parent_contexts($context, false);
2467 $parent = reset($parentcontexts);
2468 $parent = get_context_instance_by_id($parent);
2470 if ($parent->contextlevel == CONTEXT_COURSE) {
2471 $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
2472 } else if ($parent->contextlevel == CONTEXT_MODULE) {
2473 $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
2474 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
2478 return array($context, $course, $cm);
2482 //////////////////////////////////////
2483 // DB TABLE RELATED FUNCTIONS //
2484 //////////////////////////////////////
2487 * function that creates a role
2490 * @param string $name role name
2491 * @param string $shortname role short name
2492 * @param string $description role description
2493 * @param string $archetype
2494 * @return mixed id or dml_exception
2496 function create_role($name, $shortname, $description, $archetype='') {
2499 if (strpos($archetype, 'moodle/legacy:') !== false) {
2500 throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
2503 // verify role archetype actually exists
2504 $archetypes = get_role_archetypes();
2505 if (empty($archetypes[$archetype])) {
2509 // Get the system context.
2510 $context = get_context_instance(CONTEXT_SYSTEM);
2512 // Insert the role record.
2513 $role = new object();
2514 $role->name = $name;
2515 $role->shortname = $shortname;
2516 $role->description = $description;
2517 $role->archetype = $archetype;
2519 //find free sortorder number
2520 $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
2521 if (empty($role->sortorder)) {
2522 $role->sortorder = 1;
2524 $id = $DB->insert_record('role', $role);
2530 * Function that deletes a role and cleanups up after it
2532 * @param int $roleid id of role to delete
2533 * @return bool lways true
2535 function delete_role($roleid) {
2538 // first unssign all users
2539 role_unassign($roleid);
2541 // cleanup all references to this role, ignore errors
2542 $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
2543 $DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
2544 $DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
2545 $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
2546 $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
2547 $DB->delete_records('role_names', array('roleid'=>$roleid));
2548 $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
2550 // finally delete the role itself
2551 // get this before the name is gone for logging
2552 $rolename = $DB->get_field('role', 'name', array('id'=>$roleid));
2554 $DB->delete_records('role', array('id'=>$roleid));
2556 add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '');
2562 * Function to write context specific overrides, or default capabilities.
2566 * @param string module string name
2567 * @param string capability string name
2568 * @param int contextid context id
2569 * @param int roleid role id
2570 * @param int permission int 1,-1 or -1000 should not be writing if permission is 0
2573 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite=false) {
2576 if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
2577 unassign_capability($capability, $roleid, $contextid);
2581 $existing = $DB->get_record('role_capabilities', array('contextid'=>$contextid, 'roleid'=>$roleid, 'capability'=>$capability));
2583 if ($existing and !$overwrite) { // We want to keep whatever is there already
2588 $cap->contextid = $contextid;
2589 $cap->roleid = $roleid;
2590 $cap->capability = $capability;
2591 $cap->permission = $permission;
2592 $cap->timemodified = time();
2593 $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
2596 $cap->id = $existing->id;
2597 $DB->update_record('role_capabilities', $cap);
2599 $c = $DB->get_record('context', array('id'=>$contextid));
2600 $DB->insert_record('role_capabilities', $cap);
2606 * Unassign a capability from a role.
2609 * @param int $roleid the role id
2610 * @param string $capability the name of the capability
2611 * @return boolean success or failure
2613 function unassign_capability($capability, $roleid, $contextid=NULL) {
2616 if (!empty($contextid)) {
2617 // delete from context rel, if this is the last override in this context
2618 $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$contextid));
2620 $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
2627 * Get the roles that have a given capability assigned to it
2628 * Get the roles that have a given capability assigned to it. This function
2629 * does not resolve the actual permission of the capability. It just checks
2630 * for assignment only.
2634 * @param string $capability - capability name (string)
2635 * @param string $permission - optional, the permission defined for this capability
2636 * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to NULL
2637 * @param object $contect
2638 * @return mixed array or role objects
2640 function get_roles_with_capability($capability, $permission=NULL, $context=NULL) {
2646 if ($contexts = get_parent_contexts($context)) {
2647 $listofcontexts = '('.implode(',', $contexts).')';
2649 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
2650 $listofcontexts = '('.$sitecontext->id.')'; // must be site
2652 $contextstr = "AND (rc.contextid = ? OR rc.contextid IN $listofcontexts)";
2653 $params[] = $context->id;
2658 $selectroles = "SELECT r.*
2660 {role_capabilities} rc
2661 WHERE rc.capability = ?
2662 AND rc.roleid = r.id $contextstr";
2664 array_unshift($params, $capability);
2666 if (isset($permission)) {
2667 $selectroles .= " AND rc.permission = ?";
2668 $params[] = $permission;
2670 return $DB->get_records_sql($selectroles, $params);
2675 * This function makes a role-assignment (a role for a user or group in a particular context)
2677 * @param int $roleid the role of the id
2678 * @param int $userid userid
2679 * @param int $groupid group id
2680 * @param int $contextid id of the context
2681 * @param int $timestart time this assignment becomes effective defaults to 0
2682 * @param int $timeend time this assignemnt ceases to be effective defaults to 0
2683 * @param int $hidden_ignored - use roels with moodle/course:view capability or enrolemnt instead
2684 * @param string $enrol defaults to 'manual'
2685 * @param string $timemodified defaults to ''
2686 * @return int new id of the assigment
2688 function role_assign($roleid, $userid, $groupid, $contextid, $timestart=0, $timeend=0, $hidden_ignored=0, $enrol='manual',$timemodified='') {
2689 global $USER, $CFG, $DB;
2691 /// Do some data validation
2693 if (empty($roleid)) {
2694 debugging('Role ID not provided');
2698 if (empty($userid) && empty($groupid)) {
2699 debugging('Either userid or groupid must be provided');
2703 if ($userid && !$DB->record_exists('user', array('id'=>$userid))) {
2704 debugging('User ID '.intval($userid).' does not exist!');
2708 if ($groupid && !groups_group_exists($groupid)) {
2709 debugging('Group ID '.intval($groupid).' does not exist!');
2713 if (!$context = get_context_instance_by_id($contextid)) {
2714 debugging('Context ID '.intval($contextid).' does not exist!');
2718 if (($timestart and $timeend) and ($timestart > $timeend)) {
2719 debugging('The end time can not be earlier than the start time');
2723 if (!$timemodified) {
2724 $timemodified = time();
2727 /// Check for existing entry
2729 $ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid));
2731 $ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'groupid'=>$groupid));
2734 if (empty($ra)) { // Create a new entry
2736 $ra->roleid = $roleid;
2737 $ra->contextid = $context->id;
2738 $ra->userid = $userid;
2739 $ra->enrol = $enrol;
2740 /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
2741 /// by repeating queries with the same exact parameters in a 100 secs time window
2742 $ra->timestart = round($timestart, -2);
2743 $ra->timeend = $timeend;
2744 $ra->timemodified = $timemodified;
2745 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
2747 $ra->id = $DB->insert_record('role_assignments', $ra);
2749 } else { // We already have one, just update it
2751 $ra->enrol = $enrol;
2752 /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
2753 /// by repeating queries with the same exact parameters in a 100 secs time window
2754 $ra->timestart = round($timestart, -2);
2755 $ra->timeend = $timeend;
2756 $ra->timemodified = $timemodified;
2757 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
2759 $DB->update_record('role_assignments', $ra);
2762 /// mark context as dirty - modules might use has_capability() in xxx_role_assing()
2763 /// again expensive, but needed
2764 mark_context_dirty($context->path);
2766 if (!empty($USER->id) && $USER->id == $userid) {
2767 /// If the user is the current user, then do full reload of capabilities too.
2768 load_all_capabilities();
2771 /// Ask all the modules if anything needs to be done for this user
2772 $mods = get_plugin_list('mod');
2773 foreach ($mods as $mod => $moddir) {
2774 include_once($moddir.'/lib.php');
2775 $functionname = $mod.'_role_assign';
2776 if (function_exists($functionname)) {
2777 $functionname($userid, $context, $roleid);
2781 /// now handle metacourse role assignments if in course context
2782 if ($context->contextlevel == CONTEXT_COURSE) {
2783 if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
2784 foreach ($parents as $parent) {
2785 sync_metacourse($parent->parent_course);
2790 events_trigger('role_assigned', $ra);
2797 * Deletes one or more role assignments. You must specify at least one parameter.
2802 * @param int $roleid defaults to 0
2803 * @param int $userid defaults to 0
2804 * @param int $groupid defaults to 0
2805 * @param int $contextid defaults to 0
2806 * @param mixed $enrol unassign only if enrolment type matches, NULL means anything. Defaults to NULL
2807 * @return boolean success or failure
2809 function role_unassign($roleid=0, $userid=0, $groupid=0, $contextid=0, $enrol=NULL) {
2811 global $USER, $CFG, $DB;
2812 require_once($CFG->dirroot.'/group/lib.php');
2814 $args = array('roleid', 'userid', 'groupid', 'contextid');
2818 foreach ($args as $arg) {
2820 $select[] = "$arg = ?";
2824 if (!empty($enrol)) {
2825 $select[] = "enrol=?";
2830 if ($ras = $DB->get_records_select('role_assignments', implode(' AND ', $select), $params)) {
2831 $mods = get_plugin_list('mod');
2832 foreach($ras as $ra) {
2833 /// infinite loop protection when deleting recursively
2834 if (!$ra = $DB->get_record('role_assignments', array('id'=>$ra->id))) {
2837 $DB->delete_records('role_assignments', array('id'=>$ra->id));
2839 if (!$context = get_context_instance_by_id($ra->contextid)) {
2840 // strange error, not much to do
2844 /* mark contexts as dirty here, because we need the refreshed
2845 * caps bellow to delete group membership and user_lastaccess!
2846 * and yes, this is very expensive for bulk operations :-(
2848 mark_context_dirty($context->path);
2850 /// If the user is the current user, then do full reload of capabilities too.
2851 if (!empty($USER->id) && $USER->id == $ra->userid) {
2852 load_all_capabilities();
2855 /// Ask all the modules if anything needs to be done for this user
2856 foreach ($mods as $mod=>$moddir) {
2857 include_once($moddir.'/lib.php');
2858 $functionname = $mod.'_role_unassign';
2859 if (function_exists($functionname)) {
2860 $functionname($ra->userid, $context); // watch out, $context might be NULL if something goes wrong
2864 /// now handle metacourse role unassigment and removing from goups if in course context
2865 if ($context->contextlevel == CONTEXT_COURSE) {
2867 // cleanup leftover course groups/subscriptions etc when user has
2868 // no capability to view course
2869 // this may be slow, but this is the proper way of doing it
2870 if (!has_capability('moodle/course:participate', $context, $ra->userid)) {
2871 // remove from groups
2872 groups_delete_group_members($context->instanceid, $ra->userid);
2874 // delete lastaccess records
2875 $DB->delete_records('user_lastaccess', array('userid'=>$ra->userid, 'courseid'=>$context->instanceid));
2878 //unassign roles in metacourses if needed
2879 if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
2880 foreach ($parents as $parent) {
2881 sync_metacourse($parent->parent_course);
2886 events_trigger('role_unassigned', $ra);
2895 * Enrol someone without using the default role in a course
2897 * A convenience function to take care of the common case where you
2898 * just want to enrol someone using the default role into a course
2900 * @param object $course
2901 * @param object $user
2902 * @param string $enrol the plugin used to do this enrolment
2905 function enrol_into_course($course, $user, $enrol) {
2907 $timestart = time();
2908 // remove time part from the timestamp and keep only the date part
2909 $timestart = make_timestamp(date('Y', $timestart), date('m', $timestart), date('d', $timestart), 0, 0, 0);
2910 if ($course->enrolperiod) {
2911 $timeend = $timestart + $course->enrolperiod;
2916 if ($role = get_default_course_role($course)) {
2918 $context = get_context_instance(CONTEXT_COURSE, $course->id);
2920 if (!role_assign($role->id, $user->id, 0, $context->id, $timestart, $timeend, 0, $enrol)) {
2924 // force accessdata refresh for users visiting this context...
2925 mark_context_dirty($context->path);
2927 email_welcome_message_to_user($course, $user);
2929 add_to_log($course->id, 'course', 'enrol',
2930 'view.php?id='.$course->id, $course->id);
2939 * Determines if a user is currently logged in
2943 function isloggedin() {
2946 return (!empty($USER->id));
2950 * Determines if a user is logged in as real guest user with username 'guest'.
2952 * @param int $user mixed user object or id, $USER if not specified
2953 * @return bool true if user is the real guest user, false if not logged in or other user
2955 function isguestuser($user = NULL) {
2956 global $USER, $DB, $CFG;
2958 // make sure we have the user id cached in config table, because we are going to use it a lot
2959 if (empty($CFG->siteguest)) {
2960 if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
2961 // guest does not exist yet, weird
2964 set_config('siteguest', $guestid);
2966 if ($user === NULL) {
2970 if ($user === NULL) {
2971 // happens when setting the $USER
2974 } else if (is_numeric($user)) {
2975 return ($CFG->siteguest == $user);
2977 } else if (is_object($user)) {
2978 if (empty($user->id)) {
2979 return false; // not logged in means is not be guest
2981 return ($CFG->siteguest == $user->id);
2985 throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
2990 * Does user have a (temporary or real) guest access to course?
2992 * @param object $context
2993 * @param object|int $user
2996 function is_guest($context, $user = NULL) {
2997 // first find the course context
2998 $coursecontext = get_course_context($context);
3000 // make sure there is a real user specified
3001 if ($user === NULL) {
3002 $userid = !empty($USER->id) ? $USER->id : 0;
3004 $userid = !empty($user->id) ? $user->id : $user;
3007 if (isguestuser($userid)) {
3008 // can not inspect or be enrolled
3012 if (has_capability('moodle/course:view', $coursecontext, $user)) {
3013 // viewing users appear out of nowhere, they are neither guests nor participants
3017 if (has_capability('moodle/course:participate', $coursecontext, $userid, false)) {
3026 * Returns true if user has course:inspect capability in course,
3027 * this is intended for admins, managers (aka small admins), inspectors, etc.
3029 * @param object $context
3030 * @param int|object $user, if NULL $USER is used
3031 * @param string $withcapability extra capability name
3034 function is_viewing($context, $user = NULL, $withcapability = '') {
3037 // first find the course context
3038 $coursecontext = get_course_context($context);
3040 if (isguestuser($user)) {
3045 if (!has_capability('moodle/course:view', $coursecontext, $user)) {
3046 // admins are allowed to inspect courses
3050 if ($withcapability and !has_capability($withcapability, $context, $user)) {
3051 // site admins always have the capability, but the enrolment above blocks
3059 * Returns true if user is enrolled (is participating) in course
3060 * this is intended for students and teachers.
3062 * @param object $context
3063 * @param int|object $user, if NULL $USER is used, oherwise user object or id expected
3064 * @param string $withcapability extra capability name
3067 function is_enrolled($context, $user = NULL, $withcapability = '') {
3070 // first find the course context
3071 $coursecontext = get_course_context($context);
3073 // make sure there is a real user specified
3074 if ($user === NULL) {
3075 $userid = !empty($USER->id) ? $USER->id : 0;
3077 $userid = !empty($user->id) ? $user->id : $user;
3080 if (empty($userid)) {
3083 } else if (isguestuser($userid)) {
3084 // guest account can not be enrolled anywhere
3088 if ($coursecontext->instanceid != SITEID and !has_capability('moodle/course:participate', $coursecontext, $userid, false)) {
3089 // admins are not enrolled, everybody is "enrolled" in the frontpage course
3093 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
3101 * Returns array with sql code and parameters returning all ids
3102 * of users enrolled into course.
3103 * @param object $context
3104 * @param string $withcapability
3105 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3106 * @param string $prefix used for alias of user table, parameter names and in aliases of other used tables
3107 * @return array list($sql, $params)
3109 function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $prefix = 'eu') {
3112 if ($context->contextlevel < CONTEXT_COURSE) {
3113 throw new coding_exception('get_enrolled_sql() expects course context and bellow!');
3116 // first find the course context
3117 if ($context->contextlevel == CONTEXT_COURSE) {
3118 $coursecontext = $context;
3120 } else if ($context->contextlevel == CONTEXT_MODULE) {
3121 $coursecontext = get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST));
3123 } else if ($context->contextlevel == CONTEXT_BLOCK) {
3124 $parentcontext = get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST));
3125 if ($parentcontext->contextlevel == CONTEXT_COURSE) {
3126 $coursecontext = $parentcontext;
3127 } else if ($parentcontext->contextlevel == CONTEXT_MODULE) {
3128 $coursecontext = get_context_instance_by_id(get_parent_contextid($parentcontext, MUST_EXIST));
3130 throw new coding_exception('Invalid context supplied to get_enrolled_sql()!');
3134 throw new coding_exception('Invalid context supplied to get_enrolled_sql()!');
3137 list($contextids, $contextpaths) = get_context_info_list($context);
3138 list($coursecontextids, $coursecontextpaths) = get_context_info_list($coursecontext);
3140 // get all relevant capability info for all roles
3141 if ($withcapability) {
3142 list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con00');
3143 $incaps = "IN (:participate, :withcap)";
3144 $params['participate'] = 'moodle/course:participate';
3145 $params['withcap'] = $withcapability;
3147 list($incontexts, $params) = $DB->get_in_or_equal($coursecontextids, SQL_PARAMS_NAMED, 'con00');
3148 $incaps = "= :participate";
3149 $params['participate'] = 'moodle/course:participate';
3152 $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3153 FROM {role_capabilities} rc
3154 JOIN {context} ctx on rc.contextid = ctx.id
3155 WHERE rc.contextid $incontexts AND rc.capability $incaps";
3156 $rcs = $DB->get_records_sql($sql, $params);
3157 foreach ($rcs as $rc) {
3158 $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3161 $courseaccess = array();
3162 if (!empty($defs['moodle/course:participate'])) {
3163 foreach ($coursecontextpaths as $path) {
3164 if (empty($defs['moodle/course:participate'][$path])) {
3168 foreach($defs['moodle/course:participate'][$path] as $roleid => $perm) {
3169 if ($perm == CAP_PROHIBIT) {
3170 $courseaccess[$roleid] = CAP_PROHIBIT;
3173 if (!isset($courseaccess[$roleid])) {
3174 $courseaccess[$roleid] = (int)$perm;
3181 if (!empty($defs[$withcapability])) {
3182 foreach ($contextpaths as $path) {
3183 if (empty($defs[$withcapability][$path])) {
3186 foreach($defs[$withcapability][$path] as $roleid => $perm) {
3187 if ($perm == CAP_PROHIBIT) {
3188 $access[$roleid] = CAP_PROHIBIT;
3191 if (!isset($access[$roleid])) {
3192 $access[$roleid] = (int)$perm;
3200 // make lists of roles that are needed and prohibited
3201 $courseneeded = array(); // one of these is enough
3202 $courseprohibited = array(); // must not have any of these
3203 foreach ($courseaccess as $roleid => $perm) {
3204 if ($perm == CAP_PROHIBIT) {
3205 unset($courseneeded[$roleid]);
3206 $courseprohibited[$roleid] = true;
3207 } else if ($perm == CAP_ALLOW and empty($courseprohibited[$roleid])) {
3208 $courseneeded[$roleid] = true;
3211 $needed = array(); // one of these is enough
3212 $prohibited = array(); // must not have any of these
3213 if ($withcapability) {
3214 foreach ($access as $roleid => $perm) {
3215 if ($perm == CAP_PROHIBIT) {
3216 unset($needed[$roleid]);
3217 $prohibited[$roleid] = true;
3218 } else if ($perm == CAP_ALLOW and empty($prohibited[$roleid])) {
3219 $needed[$roleid] = true;
3224 $isfrontpage = ($coursecontext->instanceid == SITEID);
3226 $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : NULL;
3227 $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : NULL;
3232 // on the frontpage all users are kind of enrolled, we have to respect only the prohibits
3233 $courseneeded = array();
3235 if (empty($courseneeded)) {
3240 if ($withcapability and !$nobody) {
3242 if (!empty($prohibited[$defaultuserroleid]) or !empty($prohibited[$defaultfrontpageroleid])) {
3244 } else if (!empty($neded[$defaultuserroleid]) or !empty($neded[$defaultfrontpageroleid])) {
3245 // everybody not having prohibit has the capability
3247 } else if (empty($needed)) {
3251 if (!empty($prohibited[$defaultuserroleid])) {
3253 } else if (!empty($neded[$defaultuserroleid])) {
3254 // everybody not having prohibit has the capability
3256 } else if (empty($needed)) {
3263 // nobody can match so return some SQL that does not return any results
3264 return array("SELECT {$prefix}.id FROM {user} {$prefix} WHERE 1=2", array());
3269 $wheres = array("{$prefix}.deleted = 0 AND {$prefix}.username <> 'guest'");
3271 if ($courseneeded) {
3272 $ctxids = implode(',', $coursecontextids);
3273 $roleids = implode(',', array_keys($courseneeded));
3274 $joins[] = "JOIN {role_assignments} {$prefix}_ra1 ON ({$prefix}_ra1.userid = {$prefix}.id AND {$prefix}_ra1.roleid IN ($roleids) AND {$prefix}_ra1.contextid IN ($ctxids))";
3277 if ($courseprohibited) {
3278 $ctxids = implode(',', $coursecontextids);
3279 $roleids = implode(',', array_keys($courseprohibited));
3280 $joins[] = "LEFT JOIN {role_assignments} {$prefix}_ra2 ON ({$prefix}_ra2.userid = {$prefix}.id AND {$prefix}_ra2.roleid IN ($roleids) AND {$prefix}_ra2.contextid IN ($ctxids))";
3281 $wheres[] = "{$prefix}_ra2 IS NULL";
3285 $ctxids = implode(',', $contextids);
3286 $roleids = implode(',', array_keys($needed));
3287 $joins[] = "JOIN {role_assignments} {$prefix}_ra3 ON ({$prefix}_ra3.userid = {$prefix}.id AND {$prefix}_ra3.roleid IN ($roleids) AND {$prefix}_ra3.contextid IN ($ctxids))";
3291 $ctxids = implode(',', $contextids);
3292 $roleids = implode(',', array_keys($prohibited));
3293 $joins[] = "LEFT JOIN {role_assignments} {$prefix}_ra4 ON ({$prefix}_ra4.userid = {$prefix}.id AND {$prefix}_ra4.roleid IN ($roleids) AND {$prefix}_ra4.contextid IN ($ctxids))";
3294 $wheres[] = "{$prefix}_ra4 IS NULL";
3298 $joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}.id AND {$prefix}.roleid = :{$prefix}gmid)";
3299 $params["{$prefix}gmid"] = $groupid;
3302 $joins = implode("\n", $joins);
3303 $wheres = "WHERE ".implode(" AND ", $wheres);
3305 $sql = "SELECT DISTINCT {$prefix}.id
3306 FROM {user} {$prefix}
3310 return array($sql, $params);
3314 * Returns list of users enrolled into course.
3315 * @param object $context
3316 * @param string $withcapability
3317 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3318 * @param string $userfields requested user record fields
3319 * @param string $orderby
3320 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
3321 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
3322 * @return array of user records
3324 function get_enrolled_users($context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = '', $limitfrom = 0, $limitnum = 0) {
3327 list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid);
3328 $sql = "SELECT $userfields
3330 JOIN ($esql) je ON je.id = u.id
3331 WHERE u.deleted = 0";
3334 $sql = "$sql ORDER BY $orderby";
3336 $sql = "$sql ORDER BY u.lastname ASC, u.firstname ASC";
3339 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3343 * Counts list of users enrolled into course (as per above function)
3344 * @param object $context
3345 * @param string $withcapability
3346 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3347 * @return array of user records
3349 function count_enrolled_users($context, $withcapability = '', $groupid = 0) {
3352 list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid);
3353 $sql = "SELECT count(u.id)
3355 JOIN ($esql) je ON je.id = u.id
3356 WHERE u.deleted = 0";
3358 return $DB->count_records_sql($sql, $params);
3363 * Loads the capability definitions for the component (from file).
3365 * Loads the capability definitions for the component (from file). If no
3366 * capabilities are defined for the component, we simply return an empty array.
3369 * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
3370 * @return array array of capabilities
3372 function load_capability_def($component) {
3373 $defpath = get_component_directory($component).'/db/access.php';
3375 $capabilities = array();
3376 if (file_exists($defpath)) {
3378 if (!empty(${$component.'_capabilities'})) {
3379 // BC capability array name
3380 // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
3381 debugging('componentname_capabilities array is deprecated, please use capabilities array only in access.php files');
3382 $capabilities = ${$component.'_capabilities'};
3386 return $capabilities;
3391 * Gets the capabilities that have been cached in the database for this component.
3392 * @param string $component - examples: 'moodle', 'mod_forum'
3393 * @return array array of capabilities
3395 function get_cached_capabilities($component='moodle') {
3397 return $DB->get_records('capabilities', array('component'=>$component));
3401 * Returns default capabilities for given role archetype.
3402 * @param string $archetype role archetype
3405 function get_default_capabilities($archetype) {
3413 $defaults = array();
3414 $components = array();
3415 $allcaps = $DB->get_records('capabilities');
3417 foreach ($allcaps as $cap) {
3418 if (!in_array($cap->component, $components)) {
3419 $components[] = $cap->component;
3420 $alldefs = array_merge($alldefs, load_capability_def($cap->component));
3423 foreach($alldefs as $name=>$def) {
3424 if (isset($def['legacy'][$archetype])) {
3425 $defaults[$name] = $def['legacy'][$archetype];
3433 * Reset role capabilitites to default according to selected role archetype.
3434 * If no archetype selected, removes all capabilities.
3435 * @param int @roleid
3437 function reset_role_capabilities($roleid) {
3440 $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
3441 $defaultcaps = get_default_capabilities($role->archetype);
3443 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
3445 $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
3447 foreach($defaultcaps as $cap=>$permission) {
3448 assign_capability($cap, $permission, $roleid, $sitecontext->id);
3453 * Updates the capabilities table with the component capability definitions.
3454 * If no parameters are given, the function updates the core moodle
3457 * Note that the absence of the db/access.php capabilities definition file
3458 * will cause any stored capabilities for the component to be removed from
3462 * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
3463 * @return boolean true if success, exception in case of any problems
3465 function update_capabilities($component='moodle') {
3466 global $DB, $OUTPUT;
3468 $storedcaps = array();
3470 $filecaps = load_capability_def($component);
3471 $cachedcaps = get_cached_capabilities($component);
3473 foreach ($cachedcaps as $cachedcap) {
3474 array_push($storedcaps, $cachedcap->name);
3475 // update risk bitmasks and context levels in existing capabilities if needed
3476 if (array_key_exists($cachedcap->name, $filecaps)) {
3477 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
3478 $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
3480 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
3481 $updatecap = new object();
3482 $updatecap->id = $cachedcap->id;
3483 $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
3484 $DB->update_record('capabilities', $updatecap);
3486 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
3487 $updatecap = new object();
3488 $updatecap->id = $cachedcap->id;
3489 $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
3490 $DB->update_record('capabilities', $updatecap);
3493 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
3494 $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
3496 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
3497 $updatecap = new object();
3498 $updatecap->id = $cachedcap->id;
3499 $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
3500 $DB->update_record('capabilities', $updatecap);
3506 // Are there new capabilities in the file definition?
3509 foreach ($filecaps as $filecap => $def) {
3511 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
3512 if (!array_key_exists('riskbitmask', $def)) {
3513 $def['riskbitmask'] = 0; // no risk if not specified
3515 $newcaps[$filecap] = $def;
3518 // Add new capabilities to the stored definition.
3519 foreach ($newcaps as $capname => $capdef) {
3520 $capability = new object();
3521 $capability->name = $capname;
3522 $capability->captype = $capdef['captype'];
3523 $capability->contextlevel = $capdef['contextlevel'];
3524 $capability->component = $component;
3525 $capability->riskbitmask = $capdef['riskbitmask'];
3527 $DB->insert_record('capabilities', $capability, false);
3529 if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $storedcaps)){
3530 if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
3531 foreach ($rolecapabilities as $rolecapability){
3532 //assign_capability will update rather than insert if capability exists
3533 if (!assign_capability($capname, $rolecapability->permission,
3534 $rolecapability->roleid, $rolecapability->contextid, true)){
3535 echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
3539 // we ignore legacy key if we have cloned permissions
3540 } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
3541 assign_legacy_capabilities($capname, $capdef['legacy']);
3544 // Are there any capabilities that have been removed from the file
3545 // definition that we need to delete from the stored capabilities and
3546 // role assignments?
3547 capabilities_cleanup($component, $filecaps);
3549 // reset static caches
3550 $ACCESSLIB_PRIVATE->capabilities = NULL;
3557 * Deletes cached capabilities that are no longer needed by the component.
3558 * Also unassigns these capabilities from any roles that have them.
3561 * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
3562 * @param array $newcapdef array of the new capability definitions that will be
3563 * compared with the cached capabilities
3564 * @return int number of deprecated capabilities that have been removed
3566 function capabilities_cleanup($component, $newcapdef=NULL) {
3571 if ($cachedcaps = get_cached_capabilities($component)) {
3572 foreach ($cachedcaps as $cachedcap) {
3573 if (empty($newcapdef) ||
3574 array_key_exists($cachedcap->name, $newcapdef) === false) {
3576 // Remove from capabilities cache.
3577 $DB->delete_records('capabilities', array('name'=>$cachedcap->name));
3579 // Delete from roles.
3580 if ($roles = get_roles_with_capability($cachedcap->name)) {
3581 foreach($roles as $role) {
3582 if (!unassign_capability($cachedcap->name, $role->id)) {
3583 print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
3590 return $removedcount;
3600 * @param integer $contextlevel $context->context level. One of the CONTEXT_... constants.
3601 * @return string the name for this type of context.
3603 function get_contextlevel_name($contextlevel) {
3604 static $strcontextlevels = NULL;
3605 if (is_null($strcontextlevels)) {
3606 $strcontextlevels = array(
3607 CONTEXT_SYSTEM => get_string('coresystem'),
3608 CONTEXT_USER => get_string('user'),
3609 CONTEXT_COURSECAT => get_string('category'),
3610 CONTEXT_COURSE => get_string('course'),
3611 CONTEXT_MODULE => get_string('activitymodule'),
3612 CONTEXT_BLOCK => get_string('block')
3615 return $strcontextlevels[$contextlevel];
3619 * Prints human readable context identifier.
3622 * @param object $context the context.
3623 * @param boolean $withprefix whether to prefix the name of the context with the
3624 * type of context, e.g. User, Course, Forum, etc.
3625 * @param boolean $short whether to user the short name of the thing. Only applies
3626 * to course contexts
3627 * @return string the human readable context name.
3629 function print_context_name($context, $withprefix = true, $short = false) {
3633 switch ($context->contextlevel) {
3635 case CONTEXT_SYSTEM:
3636 $name = get_string('coresystem');
3640 if ($user = $DB->get_record('user', array('id'=>$context->instanceid))) {