MDL-39846 rename 'extra' event property to 'other'
[moodle.git] / lib / accesslib.php
blob3be25d9fe7e3221cb38b666393b00ccec7cbd815
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * This file contains functions for managing user access
20 * <b>Public API vs internals</b>
22 * General users probably only care about
24 * Context handling
25 * - context_course::instance($courseid), context_module::instance($cm->id), context_coursecat::instance($catid)
26 * - context::instance_by_id($contextid)
27 * - $context->get_parent_contexts();
28 * - $context->get_child_contexts();
30 * Whether the user can do something...
31 * - has_capability()
32 * - has_any_capability()
33 * - has_all_capabilities()
34 * - require_capability()
35 * - require_login() (from moodlelib)
36 * - is_enrolled()
37 * - is_viewing()
38 * - is_guest()
39 * - is_siteadmin()
40 * - isguestuser()
41 * - isloggedin()
43 * What courses has this user access to?
44 * - get_enrolled_users()
46 * What users can do X in this context?
47 * - get_enrolled_users() - at and bellow course context
48 * - get_users_by_capability() - above course context
50 * Modify roles
51 * - role_assign()
52 * - role_unassign()
53 * - role_unassign_all()
55 * Advanced - for internal use only
56 * - load_all_capabilities()
57 * - reload_all_capabilities()
58 * - has_capability_in_accessdata()
59 * - get_user_access_sitewide()
60 * - load_course_context()
61 * - load_role_access_by_context()
62 * - etc.
64 * <b>Name conventions</b>
66 * "ctx" means context
68 * <b>accessdata</b>
70 * Access control data is held in the "accessdata" array
71 * which - for the logged-in user, will be in $USER->access
73 * For other users can be generated and passed around (but may also be cached
74 * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser).
76 * $accessdata is a multidimensional array, holding
77 * role assignments (RAs), role-capabilities-perm sets
78 * (role defs) and a list of courses we have loaded
79 * data for.
81 * Things are keyed on "contextpaths" (the path field of
82 * the context table) for fast walking up/down the tree.
83 * <code>
84 * $accessdata['ra'][$contextpath] = array($roleid=>$roleid)
85 * [$contextpath] = array($roleid=>$roleid)
86 * [$contextpath] = array($roleid=>$roleid)
87 * </code>
89 * Role definitions are stored like this
90 * (no cap merge is done - so it's compact)
92 * <code>
93 * $accessdata['rdef']["$contextpath:$roleid"]['mod/forum:viewpost'] = 1
94 * ['mod/forum:editallpost'] = -1
95 * ['mod/forum:startdiscussion'] = -1000
96 * </code>
98 * See how has_capability_in_accessdata() walks up the tree.
100 * First we only load rdef and ra down to the course level, but not below.
101 * This keeps accessdata small and compact. Below-the-course ra/rdef
102 * are loaded as needed. We keep track of which courses we have loaded ra/rdef in
103 * <code>
104 * $accessdata['loaded'] = array($courseid1=>1, $courseid2=>1)
105 * </code>
107 * <b>Stale accessdata</b>
109 * For the logged-in user, accessdata is long-lived.
111 * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
112 * context paths affected by changes. Any check at-or-below
113 * a dirty context will trigger a transparent reload of accessdata.
115 * Changes at the system level will force the reload for everyone.
117 * <b>Default role caps</b>
118 * The default role assignment is not in the DB, so we
119 * add it manually to accessdata.
121 * This means that functions that work directly off the
122 * DB need to ensure that the default role caps
123 * are dealt with appropriately.
125 * @package core_access
126 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
127 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
130 defined('MOODLE_INTERNAL') || die();
132 /** No capability change */
133 define('CAP_INHERIT', 0);
134 /** Allow permission, overrides CAP_PREVENT defined in parent contexts */
135 define('CAP_ALLOW', 1);
136 /** Prevent permission, overrides CAP_ALLOW defined in parent contexts */
137 define('CAP_PREVENT', -1);
138 /** Prohibit permission, overrides everything in current and child contexts */
139 define('CAP_PROHIBIT', -1000);
141 /** System context level - only one instance in every system */
142 define('CONTEXT_SYSTEM', 10);
143 /** User context level - one instance for each user describing what others can do to user */
144 define('CONTEXT_USER', 30);
145 /** Course category context level - one instance for each category */
146 define('CONTEXT_COURSECAT', 40);
147 /** Course context level - one instances for each course */
148 define('CONTEXT_COURSE', 50);
149 /** Course module context level - one instance for each course module */
150 define('CONTEXT_MODULE', 70);
152 * Block context level - one instance for each block, sticky blocks are tricky
153 * because ppl think they should be able to override them at lower contexts.
154 * Any other context level instance can be parent of block context.
156 define('CONTEXT_BLOCK', 80);
158 /** Capability allow management of trusts - NOT IMPLEMENTED YET - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
159 define('RISK_MANAGETRUST', 0x0001);
160 /** Capability allows changes in system configuration - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
161 define('RISK_CONFIG', 0x0002);
162 /** Capability allows user to add scripted content - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
163 define('RISK_XSS', 0x0004);
164 /** Capability allows access to personal user information - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
165 define('RISK_PERSONAL', 0x0008);
166 /** Capability allows users to add content others may see - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
167 define('RISK_SPAM', 0x0010);
168 /** capability allows mass delete of data belonging to other users - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
169 define('RISK_DATALOSS', 0x0020);
171 /** rolename displays - the name as defined in the role definition, localised if name empty */
172 define('ROLENAME_ORIGINAL', 0);
173 /** rolename displays - the name as defined by a role alias at the course level, falls back to ROLENAME_ORIGINAL if alias not present */
174 define('ROLENAME_ALIAS', 1);
175 /** rolename displays - Both, like this: Role alias (Original) */
176 define('ROLENAME_BOTH', 2);
177 /** rolename displays - the name as defined in the role definition and the shortname in brackets */
178 define('ROLENAME_ORIGINALANDSHORT', 3);
179 /** rolename displays - the name as defined by a role alias, in raw form suitable for editing */
180 define('ROLENAME_ALIAS_RAW', 4);
181 /** rolename displays - the name is simply short role name */
182 define('ROLENAME_SHORT', 5);
184 if (!defined('CONTEXT_CACHE_MAX_SIZE')) {
185 /** maximum size of context cache - it is possible to tweak this config.php or in any script before inclusion of context.php */
186 define('CONTEXT_CACHE_MAX_SIZE', 2500);
190 * Although this looks like a global variable, it isn't really.
192 * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
193 * It is used to cache various bits of data between function calls for performance reasons.
194 * Sadly, a PHP global variable is the only way to implement this, without rewriting everything
195 * as methods of a class, instead of functions.
197 * @access private
198 * @global stdClass $ACCESSLIB_PRIVATE
199 * @name $ACCESSLIB_PRIVATE
201 global $ACCESSLIB_PRIVATE;
202 $ACCESSLIB_PRIVATE = new stdClass();
203 $ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache, loaded from DB once per page
204 $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
205 $ACCESSLIB_PRIVATE->rolepermissions = array(); // role permissions cache - helps a lot with mem usage
206 $ACCESSLIB_PRIVATE->capabilities = null; // detailed information about the capabilities
209 * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
211 * This method should ONLY BE USED BY UNIT TESTS. It clears all of
212 * accesslib's private caches. You need to do this before setting up test data,
213 * and also at the end of the tests.
215 * @access private
216 * @return void
218 function accesslib_clear_all_caches_for_unit_testing() {
219 global $USER;
220 if (!PHPUNIT_TEST) {
221 throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
224 accesslib_clear_all_caches(true);
226 unset($USER->access);
230 * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
232 * This reset does not touch global $USER.
234 * @access private
235 * @param bool $resetcontexts
236 * @return void
238 function accesslib_clear_all_caches($resetcontexts) {
239 global $ACCESSLIB_PRIVATE;
241 $ACCESSLIB_PRIVATE->dirtycontexts = null;
242 $ACCESSLIB_PRIVATE->accessdatabyuser = array();
243 $ACCESSLIB_PRIVATE->rolepermissions = array();
244 $ACCESSLIB_PRIVATE->capabilities = null;
246 if ($resetcontexts) {
247 context_helper::reset_caches();
252 * Gets the accessdata for role "sitewide" (system down to course)
254 * @access private
255 * @param int $roleid
256 * @return array
258 function get_role_access($roleid) {
259 global $DB, $ACCESSLIB_PRIVATE;
261 /* Get it in 1 DB query...
262 * - relevant role caps at the root and down
263 * to the course level - but not below
266 //TODO: MUC - this could be cached in shared memory to speed up first page loading, web crawlers, etc.
268 $accessdata = get_empty_accessdata();
270 $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
272 // Overrides for the role IN ANY CONTEXTS down to COURSE - not below -.
275 $sql = "SELECT ctx.path,
276 rc.capability, rc.permission
277 FROM {context} ctx
278 JOIN {role_capabilities} rc ON rc.contextid = ctx.id
279 LEFT JOIN {context} cctx
280 ON (cctx.contextlevel = ".CONTEXT_COURSE." AND ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'").")
281 WHERE rc.roleid = ? AND cctx.id IS NULL";
282 $params = array($roleid);
285 // Note: the commented out query is 100% accurate but slow, so let's cheat instead by hardcoding the blocks mess directly.
287 $sql = "SELECT COALESCE(ctx.path, bctx.path) AS path, rc.capability, rc.permission
288 FROM {role_capabilities} rc
289 LEFT JOIN {context} ctx ON (ctx.id = rc.contextid AND ctx.contextlevel <= ".CONTEXT_COURSE.")
290 LEFT JOIN ({context} bctx
291 JOIN {block_instances} bi ON (bi.id = bctx.instanceid)
292 JOIN {context} pctx ON (pctx.id = bi.parentcontextid AND pctx.contextlevel < ".CONTEXT_COURSE.")
293 ) ON (bctx.id = rc.contextid AND bctx.contextlevel = ".CONTEXT_BLOCK.")
294 WHERE rc.roleid = :roleid AND (ctx.id IS NOT NULL OR bctx.id IS NOT NULL)";
295 $params = array('roleid'=>$roleid);
297 // we need extra caching in CLI scripts and cron
298 $rs = $DB->get_recordset_sql($sql, $params);
299 foreach ($rs as $rd) {
300 $k = "{$rd->path}:{$roleid}";
301 $accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission;
303 $rs->close();
305 // share the role definitions
306 foreach ($accessdata['rdef'] as $k=>$unused) {
307 if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
308 $ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k];
310 $accessdata['rdef_count']++;
311 $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
314 return $accessdata;
318 * Get the default guest role, this is used for guest account,
319 * search engine spiders, etc.
321 * @return stdClass role record
323 function get_guest_role() {
324 global $CFG, $DB;
326 if (empty($CFG->guestroleid)) {
327 if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
328 $guestrole = array_shift($roles); // Pick the first one
329 set_config('guestroleid', $guestrole->id);
330 return $guestrole;
331 } else {
332 debugging('Can not find any guest role!');
333 return false;
335 } else {
336 if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
337 return $guestrole;
338 } else {
339 // somebody is messing with guest roles, remove incorrect setting and try to find a new one
340 set_config('guestroleid', '');
341 return get_guest_role();
347 * Check whether a user has a particular capability in a given context.
349 * For example:
350 * $context = context_module::instance($cm->id);
351 * has_capability('mod/forum:replypost', $context)
353 * By default checks the capabilities of the current user, but you can pass a
354 * different userid. By default will return true for admin users, but you can override that with the fourth argument.
356 * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
357 * or capabilities with XSS, config or data loss risks.
359 * @category access
361 * @param string $capability the name of the capability to check. For example mod/forum:view
362 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
363 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
364 * @param boolean $doanything If false, ignores effect of admin role assignment
365 * @return boolean true if the user has this capability. Otherwise false.
367 function has_capability($capability, context $context, $user = null, $doanything = true) {
368 global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
370 if (during_initial_install()) {
371 if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cli/install.php" or $SCRIPT === "/$CFG->admin/cli/install_database.php") {
372 // we are in an installer - roles can not work yet
373 return true;
374 } else {
375 return false;
379 if (strpos($capability, 'moodle/legacy:') === 0) {
380 throw new coding_exception('Legacy capabilities can not be used any more!');
383 if (!is_bool($doanything)) {
384 throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
387 // capability must exist
388 if (!$capinfo = get_capability_info($capability)) {
389 debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
390 return false;
393 if (!isset($USER->id)) {
394 // should never happen
395 $USER->id = 0;
396 debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
399 // make sure there is a real user specified
400 if ($user === null) {
401 $userid = $USER->id;
402 } else {
403 $userid = is_object($user) ? $user->id : $user;
406 // make sure forcelogin cuts off not-logged-in users if enabled
407 if (!empty($CFG->forcelogin) and $userid == 0) {
408 return false;
411 // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
412 if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
413 if (isguestuser($userid) or $userid == 0) {
414 return false;
418 // somehow make sure the user is not deleted and actually exists
419 if ($userid != 0) {
420 if ($userid == $USER->id and isset($USER->deleted)) {
421 // this prevents one query per page, it is a bit of cheating,
422 // but hopefully session is terminated properly once user is deleted
423 if ($USER->deleted) {
424 return false;
426 } else {
427 if (!context_user::instance($userid, IGNORE_MISSING)) {
428 // no user context == invalid userid
429 return false;
434 // context path/depth must be valid
435 if (empty($context->path) or $context->depth == 0) {
436 // this should not happen often, each upgrade tries to rebuild the context paths
437 debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()');
438 if (is_siteadmin($userid)) {
439 return true;
440 } else {
441 return false;
445 // Find out if user is admin - it is not possible to override the doanything in any way
446 // and it is not possible to switch to admin role either.
447 if ($doanything) {
448 if (is_siteadmin($userid)) {
449 if ($userid != $USER->id) {
450 return true;
452 // make sure switchrole is not used in this context
453 if (empty($USER->access['rsw'])) {
454 return true;
456 $parts = explode('/', trim($context->path, '/'));
457 $path = '';
458 $switched = false;
459 foreach ($parts as $part) {
460 $path .= '/' . $part;
461 if (!empty($USER->access['rsw'][$path])) {
462 $switched = true;
463 break;
466 if (!$switched) {
467 return true;
469 //ok, admin switched role in this context, let's use normal access control rules
473 // Careful check for staleness...
474 $context->reload_if_dirty();
476 if ($USER->id == $userid) {
477 if (!isset($USER->access)) {
478 load_all_capabilities();
480 $access =& $USER->access;
482 } else {
483 // make sure user accessdata is really loaded
484 get_user_accessdata($userid, true);
485 $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
489 // Load accessdata for below-the-course context if necessary,
490 // all contexts at and above all courses are already loaded
491 if ($context->contextlevel != CONTEXT_COURSE and $coursecontext = $context->get_course_context(false)) {
492 load_course_context($userid, $coursecontext, $access);
495 return has_capability_in_accessdata($capability, $context, $access);
499 * Check if the user has any one of several capabilities from a list.
501 * This is just a utility method that calls has_capability in a loop. Try to put
502 * the capabilities that most users are likely to have first in the list for best
503 * performance.
505 * @category access
506 * @see has_capability()
508 * @param array $capabilities an array of capability names.
509 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
510 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
511 * @param boolean $doanything If false, ignore effect of admin role assignment
512 * @return boolean true if the user has any of these capabilities. Otherwise false.
514 function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
515 foreach ($capabilities as $capability) {
516 if (has_capability($capability, $context, $user, $doanything)) {
517 return true;
520 return false;
524 * Check if the user has all the capabilities in a list.
526 * This is just a utility method that calls has_capability in a loop. Try to put
527 * the capabilities that fewest users are likely to have first in the list for best
528 * performance.
530 * @category access
531 * @see has_capability()
533 * @param array $capabilities an array of capability names.
534 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
535 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
536 * @param boolean $doanything If false, ignore effect of admin role assignment
537 * @return boolean true if the user has all of these capabilities. Otherwise false.
539 function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
540 foreach ($capabilities as $capability) {
541 if (!has_capability($capability, $context, $user, $doanything)) {
542 return false;
545 return true;
549 * Check if the user is an admin at the site level.
551 * Please note that use of proper capabilities is always encouraged,
552 * this function is supposed to be used from core or for temporary hacks.
554 * @category access
556 * @param int|stdClass $user_or_id user id or user object
557 * @return bool true if user is one of the administrators, false otherwise
559 function is_siteadmin($user_or_id = null) {
560 global $CFG, $USER;
562 if ($user_or_id === null) {
563 $user_or_id = $USER;
566 if (empty($user_or_id)) {
567 return false;
569 if (!empty($user_or_id->id)) {
570 $userid = $user_or_id->id;
571 } else {
572 $userid = $user_or_id;
575 // Because this script is called many times (150+ for course page) with
576 // the same parameters, it is worth doing minor optimisations. This static
577 // cache stores the value for a single userid, saving about 2ms from course
578 // page load time without using significant memory. As the static cache
579 // also includes the value it depends on, this cannot break unit tests.
580 static $knownid, $knownresult, $knownsiteadmins;
581 if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
582 return $knownresult;
584 $knownid = $userid;
585 $knownsiteadmins = $CFG->siteadmins;
587 $siteadmins = explode(',', $CFG->siteadmins);
588 $knownresult = in_array($userid, $siteadmins);
589 return $knownresult;
593 * Returns true if user has at least one role assign
594 * of 'coursecontact' role (is potentially listed in some course descriptions).
596 * @param int $userid
597 * @return bool
599 function has_coursecontact_role($userid) {
600 global $DB, $CFG;
602 if (empty($CFG->coursecontact)) {
603 return false;
605 $sql = "SELECT 1
606 FROM {role_assignments}
607 WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
608 return $DB->record_exists_sql($sql, array('userid'=>$userid));
612 * Does the user have a capability to do something?
614 * Walk the accessdata array and return true/false.
615 * Deals with prohibits, role switching, aggregating
616 * capabilities, etc.
618 * The main feature of here is being FAST and with no
619 * side effects.
621 * Notes:
623 * Switch Role merges with default role
624 * ------------------------------------
625 * If you are a teacher in course X, you have at least
626 * teacher-in-X + defaultloggedinuser-sitewide. So in the
627 * course you'll have techer+defaultloggedinuser.
628 * We try to mimic that in switchrole.
630 * Permission evaluation
631 * ---------------------
632 * Originally there was an extremely complicated way
633 * to determine the user access that dealt with
634 * "locality" or role assignments and role overrides.
635 * Now we simply evaluate access for each role separately
636 * and then verify if user has at least one role with allow
637 * and at the same time no role with prohibit.
639 * @access private
640 * @param string $capability
641 * @param context $context
642 * @param array $accessdata
643 * @return bool
645 function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
646 global $CFG;
648 // Build $paths as a list of current + all parent "paths" with order bottom-to-top
649 $path = $context->path;
650 $paths = array($path);
651 while($path = rtrim($path, '0123456789')) {
652 $path = rtrim($path, '/');
653 if ($path === '') {
654 break;
656 $paths[] = $path;
659 $roles = array();
660 $switchedrole = false;
662 // Find out if role switched
663 if (!empty($accessdata['rsw'])) {
664 // From the bottom up...
665 foreach ($paths as $path) {
666 if (isset($accessdata['rsw'][$path])) {
667 // Found a switchrole assignment - check for that role _plus_ the default user role
668 $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
669 $switchedrole = true;
670 break;
675 if (!$switchedrole) {
676 // get all users roles in this context and above
677 foreach ($paths as $path) {
678 if (isset($accessdata['ra'][$path])) {
679 foreach ($accessdata['ra'][$path] as $roleid) {
680 $roles[$roleid] = null;
686 // Now find out what access is given to each role, going bottom-->up direction
687 $allowed = false;
688 foreach ($roles as $roleid => $ignored) {
689 foreach ($paths as $path) {
690 if (isset($accessdata['rdef']["{$path}:$roleid"][$capability])) {
691 $perm = (int)$accessdata['rdef']["{$path}:$roleid"][$capability];
692 if ($perm === CAP_PROHIBIT) {
693 // any CAP_PROHIBIT found means no permission for the user
694 return false;
696 if (is_null($roles[$roleid])) {
697 $roles[$roleid] = $perm;
701 // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
702 $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
705 return $allowed;
709 * A convenience function that tests has_capability, and displays an error if
710 * the user does not have that capability.
712 * NOTE before Moodle 2.0, this function attempted to make an appropriate
713 * require_login call before checking the capability. This is no longer the case.
714 * You must call require_login (or one of its variants) if you want to check the
715 * user is logged in, before you call this function.
717 * @see has_capability()
719 * @param string $capability the name of the capability to check. For example mod/forum:view
720 * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
721 * @param int $userid A user id. By default (null) checks the permissions of the current user.
722 * @param bool $doanything If false, ignore effect of admin role assignment
723 * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
724 * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
725 * @return void terminates with an error if the user does not have the given capability.
727 function require_capability($capability, context $context, $userid = null, $doanything = true,
728 $errormessage = 'nopermissions', $stringfile = '') {
729 if (!has_capability($capability, $context, $userid, $doanything)) {
730 throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
735 * Return a nested array showing role assignments
736 * all relevant role capabilities for the user at
737 * site/course_category/course levels
739 * We do _not_ delve deeper than courses because the number of
740 * overrides at the module/block levels can be HUGE.
742 * [ra] => [/path][roleid]=roleid
743 * [rdef] => [/path:roleid][capability]=permission
745 * @access private
746 * @param int $userid - the id of the user
747 * @return array access info array
749 function get_user_access_sitewide($userid) {
750 global $CFG, $DB, $ACCESSLIB_PRIVATE;
752 /* Get in a few cheap DB queries...
753 * - role assignments
754 * - relevant role caps
755 * - above and within this user's RAs
756 * - below this user's RAs - limited to course level
759 // raparents collects paths & roles we need to walk up the parenthood to build the minimal rdef
760 $raparents = array();
761 $accessdata = get_empty_accessdata();
763 // start with the default role
764 if (!empty($CFG->defaultuserroleid)) {
765 $syscontext = context_system::instance();
766 $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
767 $raparents[$CFG->defaultuserroleid][$syscontext->id] = $syscontext->id;
770 // load the "default frontpage role"
771 if (!empty($CFG->defaultfrontpageroleid)) {
772 $frontpagecontext = context_course::instance(get_site()->id);
773 if ($frontpagecontext->path) {
774 $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
775 $raparents[$CFG->defaultfrontpageroleid][$frontpagecontext->id] = $frontpagecontext->id;
779 // preload every assigned role at and above course context
780 $sql = "SELECT ctx.path, ra.roleid, ra.contextid
781 FROM {role_assignments} ra
782 JOIN {context} ctx
783 ON ctx.id = ra.contextid
784 LEFT JOIN {block_instances} bi
785 ON (ctx.contextlevel = ".CONTEXT_BLOCK." AND bi.id = ctx.instanceid)
786 LEFT JOIN {context} bpctx
787 ON (bpctx.id = bi.parentcontextid)
788 WHERE ra.userid = :userid
789 AND (ctx.contextlevel <= ".CONTEXT_COURSE." OR bpctx.contextlevel < ".CONTEXT_COURSE.")";
790 $params = array('userid'=>$userid);
791 $rs = $DB->get_recordset_sql($sql, $params);
792 foreach ($rs as $ra) {
793 // RAs leafs are arrays to support multi-role assignments...
794 $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
795 $raparents[$ra->roleid][$ra->contextid] = $ra->contextid;
797 $rs->close();
799 if (empty($raparents)) {
800 return $accessdata;
803 // now get overrides of interesting roles in all interesting child contexts
804 // hopefully we will not run out of SQL limits here,
805 // users would have to have very many roles at/above course context...
806 $sqls = array();
807 $params = array();
809 static $cp = 0;
810 foreach ($raparents as $roleid=>$ras) {
811 $cp++;
812 list($sqlcids, $cids) = $DB->get_in_or_equal($ras, SQL_PARAMS_NAMED, 'c'.$cp.'_');
813 $params = array_merge($params, $cids);
814 $params['r'.$cp] = $roleid;
815 $sqls[] = "(SELECT ctx.path, rc.roleid, rc.capability, rc.permission
816 FROM {role_capabilities} rc
817 JOIN {context} ctx
818 ON (ctx.id = rc.contextid)
819 JOIN {context} pctx
820 ON (pctx.id $sqlcids
821 AND (ctx.id = pctx.id
822 OR ctx.path LIKE ".$DB->sql_concat('pctx.path',"'/%'")."
823 OR pctx.path LIKE ".$DB->sql_concat('ctx.path',"'/%'")."))
824 LEFT JOIN {block_instances} bi
825 ON (ctx.contextlevel = ".CONTEXT_BLOCK." AND bi.id = ctx.instanceid)
826 LEFT JOIN {context} bpctx
827 ON (bpctx.id = bi.parentcontextid)
828 WHERE rc.roleid = :r{$cp}
829 AND (ctx.contextlevel <= ".CONTEXT_COURSE." OR bpctx.contextlevel < ".CONTEXT_COURSE.")
833 // fixed capability order is necessary for rdef dedupe
834 $rs = $DB->get_recordset_sql(implode("\nUNION\n", $sqls). "ORDER BY capability", $params);
836 foreach ($rs as $rd) {
837 $k = $rd->path.':'.$rd->roleid;
838 $accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission;
840 $rs->close();
842 // share the role definitions
843 foreach ($accessdata['rdef'] as $k=>$unused) {
844 if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
845 $ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k];
847 $accessdata['rdef_count']++;
848 $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
851 return $accessdata;
855 * Add to the access ctrl array the data needed by a user for a given course.
857 * This function injects all course related access info into the accessdata array.
859 * @access private
860 * @param int $userid the id of the user
861 * @param context_course $coursecontext course context
862 * @param array $accessdata accessdata array (modified)
863 * @return void modifies $accessdata parameter
865 function load_course_context($userid, context_course $coursecontext, &$accessdata) {
866 global $DB, $CFG, $ACCESSLIB_PRIVATE;
868 if (empty($coursecontext->path)) {
869 // weird, this should not happen
870 return;
873 if (isset($accessdata['loaded'][$coursecontext->instanceid])) {
874 // already loaded, great!
875 return;
878 $roles = array();
880 if (empty($userid)) {
881 if (!empty($CFG->notloggedinroleid)) {
882 $roles[$CFG->notloggedinroleid] = $CFG->notloggedinroleid;
885 } else if (isguestuser($userid)) {
886 if ($guestrole = get_guest_role()) {
887 $roles[$guestrole->id] = $guestrole->id;
890 } else {
891 // Interesting role assignments at, above and below the course context
892 list($parentsaself, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
893 $params['userid'] = $userid;
894 $params['children'] = $coursecontext->path."/%";
895 $sql = "SELECT ra.*, ctx.path
896 FROM {role_assignments} ra
897 JOIN {context} ctx ON ra.contextid = ctx.id
898 WHERE ra.userid = :userid AND (ctx.id $parentsaself OR ctx.path LIKE :children)";
899 $rs = $DB->get_recordset_sql($sql, $params);
901 // add missing role definitions
902 foreach ($rs as $ra) {
903 $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
904 $roles[$ra->roleid] = $ra->roleid;
906 $rs->close();
908 // add the "default frontpage role" when on the frontpage
909 if (!empty($CFG->defaultfrontpageroleid)) {
910 $frontpagecontext = context_course::instance(get_site()->id);
911 if ($frontpagecontext->id == $coursecontext->id) {
912 $roles[$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid;
916 // do not forget the default role
917 if (!empty($CFG->defaultuserroleid)) {
918 $roles[$CFG->defaultuserroleid] = $CFG->defaultuserroleid;
922 if (!$roles) {
923 // weird, default roles must be missing...
924 $accessdata['loaded'][$coursecontext->instanceid] = 1;
925 return;
928 // now get overrides of interesting roles in all interesting contexts (this course + children + parents)
929 $params = array('c'=>$coursecontext->id);
930 list($parentsaself, $rparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
931 $params = array_merge($params, $rparams);
932 list($roleids, $rparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'r_');
933 $params = array_merge($params, $rparams);
935 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
936 FROM {role_capabilities} rc
937 JOIN {context} ctx
938 ON (ctx.id = rc.contextid)
939 JOIN {context} cctx
940 ON (cctx.id = :c
941 AND (ctx.id $parentsaself OR ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'")."))
942 WHERE rc.roleid $roleids
943 ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe
944 $rs = $DB->get_recordset_sql($sql, $params);
946 $newrdefs = array();
947 foreach ($rs as $rd) {
948 $k = $rd->path.':'.$rd->roleid;
949 if (isset($accessdata['rdef'][$k])) {
950 continue;
952 $newrdefs[$k][$rd->capability] = (int)$rd->permission;
954 $rs->close();
956 // share new role definitions
957 foreach ($newrdefs as $k=>$unused) {
958 if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
959 $ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k];
961 $accessdata['rdef_count']++;
962 $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
965 $accessdata['loaded'][$coursecontext->instanceid] = 1;
967 // we want to deduplicate the USER->access from time to time, this looks like a good place,
968 // because we have to do it before the end of session
969 dedupe_user_access();
973 * Add to the access ctrl array the data needed by a role for a given context.
975 * The data is added in the rdef key.
976 * This role-centric function is useful for role_switching
977 * and temporary course roles.
979 * @access private
980 * @param int $roleid the id of the user
981 * @param context $context needs path!
982 * @param array $accessdata accessdata array (is modified)
983 * @return array
985 function load_role_access_by_context($roleid, context $context, &$accessdata) {
986 global $DB, $ACCESSLIB_PRIVATE;
988 /* Get the relevant rolecaps into rdef
989 * - relevant role caps
990 * - at ctx and above
991 * - below this ctx
994 if (empty($context->path)) {
995 // weird, this should not happen
996 return;
999 list($parentsaself, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
1000 $params['roleid'] = $roleid;
1001 $params['childpath'] = $context->path.'/%';
1003 $sql = "SELECT ctx.path, rc.capability, rc.permission
1004 FROM {role_capabilities} rc
1005 JOIN {context} ctx ON (rc.contextid = ctx.id)
1006 WHERE rc.roleid = :roleid AND (ctx.id $parentsaself OR ctx.path LIKE :childpath)
1007 ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe
1008 $rs = $DB->get_recordset_sql($sql, $params);
1010 $newrdefs = array();
1011 foreach ($rs as $rd) {
1012 $k = $rd->path.':'.$roleid;
1013 if (isset($accessdata['rdef'][$k])) {
1014 continue;
1016 $newrdefs[$k][$rd->capability] = (int)$rd->permission;
1018 $rs->close();
1020 // share new role definitions
1021 foreach ($newrdefs as $k=>$unused) {
1022 if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
1023 $ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k];
1025 $accessdata['rdef_count']++;
1026 $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
1031 * Returns empty accessdata structure.
1033 * @access private
1034 * @return array empt accessdata
1036 function get_empty_accessdata() {
1037 $accessdata = array(); // named list
1038 $accessdata['ra'] = array();
1039 $accessdata['rdef'] = array();
1040 $accessdata['rdef_count'] = 0; // this bloody hack is necessary because count($array) is slooooowwww in PHP
1041 $accessdata['rdef_lcc'] = 0; // rdef_count during the last compression
1042 $accessdata['loaded'] = array(); // loaded course contexts
1043 $accessdata['time'] = time();
1044 $accessdata['rsw'] = array();
1046 return $accessdata;
1050 * Get accessdata for a given user.
1052 * @access private
1053 * @param int $userid
1054 * @param bool $preloadonly true means do not return access array
1055 * @return array accessdata
1057 function get_user_accessdata($userid, $preloadonly=false) {
1058 global $CFG, $ACCESSLIB_PRIVATE, $USER;
1060 if (!empty($USER->acces['rdef']) and empty($ACCESSLIB_PRIVATE->rolepermissions)) {
1061 // share rdef from USER session with rolepermissions cache in order to conserve memory
1062 foreach($USER->acces['rdef'] as $k=>$v) {
1063 $ACCESSLIB_PRIVATE->rolepermissions[$k] =& $USER->acces['rdef'][$k];
1065 $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->acces;
1068 if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
1069 if (empty($userid)) {
1070 if (!empty($CFG->notloggedinroleid)) {
1071 $accessdata = get_role_access($CFG->notloggedinroleid);
1072 } else {
1073 // weird
1074 return get_empty_accessdata();
1077 } else if (isguestuser($userid)) {
1078 if ($guestrole = get_guest_role()) {
1079 $accessdata = get_role_access($guestrole->id);
1080 } else {
1081 //weird
1082 return get_empty_accessdata();
1085 } else {
1086 $accessdata = get_user_access_sitewide($userid); // includes default role and frontpage role
1089 $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
1092 if ($preloadonly) {
1093 return;
1094 } else {
1095 return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
1100 * Try to minimise the size of $USER->access by eliminating duplicate override storage,
1101 * this function looks for contexts with the same overrides and shares them.
1103 * @access private
1104 * @return void
1106 function dedupe_user_access() {
1107 global $USER;
1109 if (CLI_SCRIPT) {
1110 // no session in CLI --> no compression necessary
1111 return;
1114 if (empty($USER->access['rdef_count'])) {
1115 // weird, this should not happen
1116 return;
1119 // the rdef is growing only, we never remove stuff from it, the rdef_lcc helps us to detect new stuff in rdef
1120 if ($USER->access['rdef_count'] - $USER->access['rdef_lcc'] > 10) {
1121 // do not compress after each change, wait till there is more stuff to be done
1122 return;
1125 $hashmap = array();
1126 foreach ($USER->access['rdef'] as $k=>$def) {
1127 $hash = sha1(serialize($def));
1128 if (isset($hashmap[$hash])) {
1129 $USER->access['rdef'][$k] =& $hashmap[$hash];
1130 } else {
1131 $hashmap[$hash] =& $USER->access['rdef'][$k];
1135 $USER->access['rdef_lcc'] = $USER->access['rdef_count'];
1139 * A convenience function to completely load all the capabilities
1140 * for the current user. It is called from has_capability() and functions change permissions.
1142 * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
1143 * @see check_enrolment_plugins()
1145 * @access private
1146 * @return void
1148 function load_all_capabilities() {
1149 global $USER;
1151 // roles not installed yet - we are in the middle of installation
1152 if (during_initial_install()) {
1153 return;
1156 if (!isset($USER->id)) {
1157 // this should not happen
1158 $USER->id = 0;
1161 unset($USER->access);
1162 $USER->access = get_user_accessdata($USER->id);
1164 // deduplicate the overrides to minimize session size
1165 dedupe_user_access();
1167 // Clear to force a refresh
1168 unset($USER->mycourses);
1170 // init/reset internal enrol caches - active course enrolments and temp access
1171 $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
1175 * A convenience function to completely reload all the capabilities
1176 * for the current user when roles have been updated in a relevant
1177 * context -- but PRESERVING switchroles and loginas.
1178 * This function resets all accesslib and context caches.
1180 * That is - completely transparent to the user.
1182 * Note: reloads $USER->access completely.
1184 * @access private
1185 * @return void
1187 function reload_all_capabilities() {
1188 global $USER, $DB, $ACCESSLIB_PRIVATE;
1190 // copy switchroles
1191 $sw = array();
1192 if (!empty($USER->access['rsw'])) {
1193 $sw = $USER->access['rsw'];
1196 accesslib_clear_all_caches(true);
1197 unset($USER->access);
1198 $ACCESSLIB_PRIVATE->dirtycontexts = array(); // prevent dirty flags refetching on this page
1200 load_all_capabilities();
1202 foreach ($sw as $path => $roleid) {
1203 if ($record = $DB->get_record('context', array('path'=>$path))) {
1204 $context = context::instance_by_id($record->id);
1205 role_switch($roleid, $context);
1211 * Adds a temp role to current USER->access array.
1213 * Useful for the "temporary guest" access we grant to logged-in users.
1214 * This is useful for enrol plugins only.
1216 * @since 2.2
1217 * @param context_course $coursecontext
1218 * @param int $roleid
1219 * @return void
1221 function load_temp_course_role(context_course $coursecontext, $roleid) {
1222 global $USER, $SITE;
1224 if (empty($roleid)) {
1225 debugging('invalid role specified in load_temp_course_role()');
1226 return;
1229 if ($coursecontext->instanceid == $SITE->id) {
1230 debugging('Can not use temp roles on the frontpage');
1231 return;
1234 if (!isset($USER->access)) {
1235 load_all_capabilities();
1238 $coursecontext->reload_if_dirty();
1240 if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
1241 return;
1244 // load course stuff first
1245 load_course_context($USER->id, $coursecontext, $USER->access);
1247 $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
1249 load_role_access_by_context($roleid, $coursecontext, $USER->access);
1253 * Removes any extra guest roles from current USER->access array.
1254 * This is useful for enrol plugins only.
1256 * @since 2.2
1257 * @param context_course $coursecontext
1258 * @return void
1260 function remove_temp_course_roles(context_course $coursecontext) {
1261 global $DB, $USER, $SITE;
1263 if ($coursecontext->instanceid == $SITE->id) {
1264 debugging('Can not use temp roles on the frontpage');
1265 return;
1268 if (empty($USER->access['ra'][$coursecontext->path])) {
1269 //no roles here, weird
1270 return;
1273 $sql = "SELECT DISTINCT ra.roleid AS id
1274 FROM {role_assignments} ra
1275 WHERE ra.contextid = :contextid AND ra.userid = :userid";
1276 $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
1278 $USER->access['ra'][$coursecontext->path] = array();
1279 foreach($ras as $r) {
1280 $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
1285 * Returns array of all role archetypes.
1287 * @return array
1289 function get_role_archetypes() {
1290 return array(
1291 'manager' => 'manager',
1292 'coursecreator' => 'coursecreator',
1293 'editingteacher' => 'editingteacher',
1294 'teacher' => 'teacher',
1295 'student' => 'student',
1296 'guest' => 'guest',
1297 'user' => 'user',
1298 'frontpage' => 'frontpage'
1303 * Assign the defaults found in this capability definition to roles that have
1304 * the corresponding legacy capabilities assigned to them.
1306 * @param string $capability
1307 * @param array $legacyperms an array in the format (example):
1308 * 'guest' => CAP_PREVENT,
1309 * 'student' => CAP_ALLOW,
1310 * 'teacher' => CAP_ALLOW,
1311 * 'editingteacher' => CAP_ALLOW,
1312 * 'coursecreator' => CAP_ALLOW,
1313 * 'manager' => CAP_ALLOW
1314 * @return boolean success or failure.
1316 function assign_legacy_capabilities($capability, $legacyperms) {
1318 $archetypes = get_role_archetypes();
1320 foreach ($legacyperms as $type => $perm) {
1322 $systemcontext = context_system::instance();
1323 if ($type === 'admin') {
1324 debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1325 $type = 'manager';
1328 if (!array_key_exists($type, $archetypes)) {
1329 print_error('invalidlegacy', '', '', $type);
1332 if ($roles = get_archetype_roles($type)) {
1333 foreach ($roles as $role) {
1334 // Assign a site level capability.
1335 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1336 return false;
1341 return true;
1345 * Verify capability risks.
1347 * @param stdClass $capability a capability - a row from the capabilities table.
1348 * @return boolean whether this capability is safe - that is, whether people with the
1349 * safeoverrides capability should be allowed to change it.
1351 function is_safe_capability($capability) {
1352 return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1356 * Get the local override (if any) for a given capability in a role in a context
1358 * @param int $roleid
1359 * @param int $contextid
1360 * @param string $capability
1361 * @return stdClass local capability override
1363 function get_local_override($roleid, $contextid, $capability) {
1364 global $DB;
1365 return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
1369 * Returns context instance plus related course and cm instances
1371 * @param int $contextid
1372 * @return array of ($context, $course, $cm)
1374 function get_context_info_array($contextid) {
1375 global $DB;
1377 $context = context::instance_by_id($contextid, MUST_EXIST);
1378 $course = null;
1379 $cm = null;
1381 if ($context->contextlevel == CONTEXT_COURSE) {
1382 $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
1384 } else if ($context->contextlevel == CONTEXT_MODULE) {
1385 $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
1386 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1388 } else if ($context->contextlevel == CONTEXT_BLOCK) {
1389 $parent = $context->get_parent_context();
1391 if ($parent->contextlevel == CONTEXT_COURSE) {
1392 $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
1393 } else if ($parent->contextlevel == CONTEXT_MODULE) {
1394 $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
1395 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1399 return array($context, $course, $cm);
1403 * Function that creates a role
1405 * @param string $name role name
1406 * @param string $shortname role short name
1407 * @param string $description role description
1408 * @param string $archetype
1409 * @return int id or dml_exception
1411 function create_role($name, $shortname, $description, $archetype = '') {
1412 global $DB;
1414 if (strpos($archetype, 'moodle/legacy:') !== false) {
1415 throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
1418 // verify role archetype actually exists
1419 $archetypes = get_role_archetypes();
1420 if (empty($archetypes[$archetype])) {
1421 $archetype = '';
1424 // Insert the role record.
1425 $role = new stdClass();
1426 $role->name = $name;
1427 $role->shortname = $shortname;
1428 $role->description = $description;
1429 $role->archetype = $archetype;
1431 //find free sortorder number
1432 $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
1433 if (empty($role->sortorder)) {
1434 $role->sortorder = 1;
1436 $id = $DB->insert_record('role', $role);
1438 return $id;
1442 * Function that deletes a role and cleanups up after it
1444 * @param int $roleid id of role to delete
1445 * @return bool always true
1447 function delete_role($roleid) {
1448 global $DB;
1450 // first unssign all users
1451 role_unassign_all(array('roleid'=>$roleid));
1453 // cleanup all references to this role, ignore errors
1454 $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
1455 $DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
1456 $DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
1457 $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
1458 $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
1459 $DB->delete_records('role_names', array('roleid'=>$roleid));
1460 $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
1462 // finally delete the role itself
1463 // get this before the name is gone for logging
1464 $rolename = $DB->get_field('role', 'name', array('id'=>$roleid));
1466 $DB->delete_records('role', array('id'=>$roleid));
1468 add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '');
1470 return true;
1474 * Function to write context specific overrides, or default capabilities.
1476 * NOTE: use $context->mark_dirty() after this
1478 * @param string $capability string name
1479 * @param int $permission CAP_ constants
1480 * @param int $roleid role id
1481 * @param int|context $contextid context id
1482 * @param bool $overwrite
1483 * @return bool always true or exception
1485 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
1486 global $USER, $DB;
1488 if ($contextid instanceof context) {
1489 $context = $contextid;
1490 } else {
1491 $context = context::instance_by_id($contextid);
1494 if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
1495 unassign_capability($capability, $roleid, $context->id);
1496 return true;
1499 $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
1501 if ($existing and !$overwrite) { // We want to keep whatever is there already
1502 return true;
1505 $cap = new stdClass();
1506 $cap->contextid = $context->id;
1507 $cap->roleid = $roleid;
1508 $cap->capability = $capability;
1509 $cap->permission = $permission;
1510 $cap->timemodified = time();
1511 $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
1513 if ($existing) {
1514 $cap->id = $existing->id;
1515 $DB->update_record('role_capabilities', $cap);
1516 } else {
1517 if ($DB->record_exists('context', array('id'=>$context->id))) {
1518 $DB->insert_record('role_capabilities', $cap);
1521 return true;
1525 * Unassign a capability from a role.
1527 * NOTE: use $context->mark_dirty() after this
1529 * @param string $capability the name of the capability
1530 * @param int $roleid the role id
1531 * @param int|context $contextid null means all contexts
1532 * @return boolean true or exception
1534 function unassign_capability($capability, $roleid, $contextid = null) {
1535 global $DB;
1537 if (!empty($contextid)) {
1538 if ($contextid instanceof context) {
1539 $context = $contextid;
1540 } else {
1541 $context = context::instance_by_id($contextid);
1543 // delete from context rel, if this is the last override in this context
1544 $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
1545 } else {
1546 $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
1548 return true;
1552 * Get the roles that have a given capability assigned to it
1554 * This function does not resolve the actual permission of the capability.
1555 * It just checks for permissions and overrides.
1556 * Use get_roles_with_cap_in_context() if resolution is required.
1558 * @param string $capability capability name (string)
1559 * @param string $permission optional, the permission defined for this capability
1560 * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
1561 * @param stdClass $context null means any
1562 * @return array of role records
1564 function get_roles_with_capability($capability, $permission = null, $context = null) {
1565 global $DB;
1567 if ($context) {
1568 $contexts = $context->get_parent_context_ids(true);
1569 list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
1570 $contextsql = "AND rc.contextid $insql";
1571 } else {
1572 $params = array();
1573 $contextsql = '';
1576 if ($permission) {
1577 $permissionsql = " AND rc.permission = :permission";
1578 $params['permission'] = $permission;
1579 } else {
1580 $permissionsql = '';
1583 $sql = "SELECT r.*
1584 FROM {role} r
1585 WHERE r.id IN (SELECT rc.roleid
1586 FROM {role_capabilities} rc
1587 WHERE rc.capability = :capname
1588 $contextsql
1589 $permissionsql)";
1590 $params['capname'] = $capability;
1593 return $DB->get_records_sql($sql, $params);
1597 * This function makes a role-assignment (a role for a user in a particular context)
1599 * @param int $roleid the role of the id
1600 * @param int $userid userid
1601 * @param int|context $contextid id of the context
1602 * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
1603 * @param int $itemid id of enrolment/auth plugin
1604 * @param string $timemodified defaults to current time
1605 * @return int new/existing id of the assignment
1607 function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
1608 global $USER, $DB;
1610 // first of all detect if somebody is using old style parameters
1611 if ($contextid === 0 or is_numeric($component)) {
1612 throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
1615 // now validate all parameters
1616 if (empty($roleid)) {
1617 throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
1620 if (empty($userid)) {
1621 throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
1624 if ($itemid) {
1625 if (strpos($component, '_') === false) {
1626 throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
1628 } else {
1629 $itemid = 0;
1630 if ($component !== '' and strpos($component, '_') === false) {
1631 throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1635 if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
1636 throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
1639 if ($contextid instanceof context) {
1640 $context = $contextid;
1641 } else {
1642 $context = context::instance_by_id($contextid, MUST_EXIST);
1645 if (!$timemodified) {
1646 $timemodified = time();
1649 // Check for existing entry
1650 $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
1652 if ($ras) {
1653 // role already assigned - this should not happen
1654 if (count($ras) > 1) {
1655 // very weird - remove all duplicates!
1656 $ra = array_shift($ras);
1657 foreach ($ras as $r) {
1658 $DB->delete_records('role_assignments', array('id'=>$r->id));
1660 } else {
1661 $ra = reset($ras);
1664 // actually there is no need to update, reset anything or trigger any event, so just return
1665 return $ra->id;
1668 // Create a new entry
1669 $ra = new stdClass();
1670 $ra->roleid = $roleid;
1671 $ra->contextid = $context->id;
1672 $ra->userid = $userid;
1673 $ra->component = $component;
1674 $ra->itemid = $itemid;
1675 $ra->timemodified = $timemodified;
1676 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
1678 $ra->id = $DB->insert_record('role_assignments', $ra);
1680 // mark context as dirty - again expensive, but needed
1681 $context->mark_dirty();
1683 if (!empty($USER->id) && $USER->id == $userid) {
1684 // If the user is the current user, then do full reload of capabilities too.
1685 reload_all_capabilities();
1688 $event = \core\event\role_assigned::create(
1689 array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1690 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1691 $event->add_cached_record('role_assignments', $ra);
1692 $event->trigger();
1694 return $ra->id;
1698 * Removes one role assignment
1700 * @param int $roleid
1701 * @param int $userid
1702 * @param int $contextid
1703 * @param string $component
1704 * @param int $itemid
1705 * @return void
1707 function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
1708 // first make sure the params make sense
1709 if ($roleid == 0 or $userid == 0 or $contextid == 0) {
1710 throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
1713 if ($itemid) {
1714 if (strpos($component, '_') === false) {
1715 throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
1717 } else {
1718 $itemid = 0;
1719 if ($component !== '' and strpos($component, '_') === false) {
1720 throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1724 role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
1728 * Removes multiple role assignments, parameters may contain:
1729 * 'roleid', 'userid', 'contextid', 'component', 'enrolid'.
1731 * @param array $params role assignment parameters
1732 * @param bool $subcontexts unassign in subcontexts too
1733 * @param bool $includemanual include manual role assignments too
1734 * @return void
1736 function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
1737 global $USER, $CFG, $DB;
1739 if (!$params) {
1740 throw new coding_exception('Missing parameters in role_unsassign_all() call');
1743 $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
1744 foreach ($params as $key=>$value) {
1745 if (!in_array($key, $allowed)) {
1746 throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
1750 if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
1751 throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
1754 if ($includemanual) {
1755 if (!isset($params['component']) or $params['component'] === '') {
1756 throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
1760 if ($subcontexts) {
1761 if (empty($params['contextid'])) {
1762 throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
1766 $ras = $DB->get_records('role_assignments', $params);
1767 foreach($ras as $ra) {
1768 $DB->delete_records('role_assignments', array('id'=>$ra->id));
1769 if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
1770 // this is a bit expensive but necessary
1771 $context->mark_dirty();
1772 // If the user is the current user, then do full reload of capabilities too.
1773 if (!empty($USER->id) && $USER->id == $ra->userid) {
1774 reload_all_capabilities();
1776 $event = \core\event\role_unassigned::create(
1777 array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1778 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1779 $event->add_cached_record('role_assignments', $ra);
1780 $event->trigger();
1783 unset($ras);
1785 // process subcontexts
1786 if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
1787 if ($params['contextid'] instanceof context) {
1788 $context = $params['contextid'];
1789 } else {
1790 $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
1793 if ($context) {
1794 $contexts = $context->get_child_contexts();
1795 $mparams = $params;
1796 foreach($contexts as $context) {
1797 $mparams['contextid'] = $context->id;
1798 $ras = $DB->get_records('role_assignments', $mparams);
1799 foreach($ras as $ra) {
1800 $DB->delete_records('role_assignments', array('id'=>$ra->id));
1801 // this is a bit expensive but necessary
1802 $context->mark_dirty();
1803 // If the user is the current user, then do full reload of capabilities too.
1804 if (!empty($USER->id) && $USER->id == $ra->userid) {
1805 reload_all_capabilities();
1807 $event = \core\event\role_unassigned::create(
1808 array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1809 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1810 $event->add_cached_record('role_assignments', $ra);
1811 $event->trigger();
1817 // do this once more for all manual role assignments
1818 if ($includemanual) {
1819 $params['component'] = '';
1820 role_unassign_all($params, $subcontexts, false);
1825 * Determines if a user is currently logged in
1827 * @category access
1829 * @return bool
1831 function isloggedin() {
1832 global $USER;
1834 return (!empty($USER->id));
1838 * Determines if a user is logged in as real guest user with username 'guest'.
1840 * @category access
1842 * @param int|object $user mixed user object or id, $USER if not specified
1843 * @return bool true if user is the real guest user, false if not logged in or other user
1845 function isguestuser($user = null) {
1846 global $USER, $DB, $CFG;
1848 // make sure we have the user id cached in config table, because we are going to use it a lot
1849 if (empty($CFG->siteguest)) {
1850 if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
1851 // guest does not exist yet, weird
1852 return false;
1854 set_config('siteguest', $guestid);
1856 if ($user === null) {
1857 $user = $USER;
1860 if ($user === null) {
1861 // happens when setting the $USER
1862 return false;
1864 } else if (is_numeric($user)) {
1865 return ($CFG->siteguest == $user);
1867 } else if (is_object($user)) {
1868 if (empty($user->id)) {
1869 return false; // not logged in means is not be guest
1870 } else {
1871 return ($CFG->siteguest == $user->id);
1874 } else {
1875 throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
1880 * Does user have a (temporary or real) guest access to course?
1882 * @category access
1884 * @param context $context
1885 * @param stdClass|int $user
1886 * @return bool
1888 function is_guest(context $context, $user = null) {
1889 global $USER;
1891 // first find the course context
1892 $coursecontext = $context->get_course_context();
1894 // make sure there is a real user specified
1895 if ($user === null) {
1896 $userid = isset($USER->id) ? $USER->id : 0;
1897 } else {
1898 $userid = is_object($user) ? $user->id : $user;
1901 if (isguestuser($userid)) {
1902 // can not inspect or be enrolled
1903 return true;
1906 if (has_capability('moodle/course:view', $coursecontext, $user)) {
1907 // viewing users appear out of nowhere, they are neither guests nor participants
1908 return false;
1911 // consider only real active enrolments here
1912 if (is_enrolled($coursecontext, $user, '', true)) {
1913 return false;
1916 return true;
1920 * Returns true if the user has moodle/course:view capability in the course,
1921 * this is intended for admins, managers (aka small admins), inspectors, etc.
1923 * @category access
1925 * @param context $context
1926 * @param int|stdClass $user if null $USER is used
1927 * @param string $withcapability extra capability name
1928 * @return bool
1930 function is_viewing(context $context, $user = null, $withcapability = '') {
1931 // first find the course context
1932 $coursecontext = $context->get_course_context();
1934 if (isguestuser($user)) {
1935 // can not inspect
1936 return false;
1939 if (!has_capability('moodle/course:view', $coursecontext, $user)) {
1940 // admins are allowed to inspect courses
1941 return false;
1944 if ($withcapability and !has_capability($withcapability, $context, $user)) {
1945 // site admins always have the capability, but the enrolment above blocks
1946 return false;
1949 return true;
1953 * Returns true if user is enrolled (is participating) in course
1954 * this is intended for students and teachers.
1956 * Since 2.2 the result for active enrolments and current user are cached.
1958 * @package core_enrol
1959 * @category access
1961 * @param context $context
1962 * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1963 * @param string $withcapability extra capability name
1964 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1965 * @return bool
1967 function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1968 global $USER, $DB;
1970 // first find the course context
1971 $coursecontext = $context->get_course_context();
1973 // make sure there is a real user specified
1974 if ($user === null) {
1975 $userid = isset($USER->id) ? $USER->id : 0;
1976 } else {
1977 $userid = is_object($user) ? $user->id : $user;
1980 if (empty($userid)) {
1981 // not-logged-in!
1982 return false;
1983 } else if (isguestuser($userid)) {
1984 // guest account can not be enrolled anywhere
1985 return false;
1988 if ($coursecontext->instanceid == SITEID) {
1989 // everybody participates on frontpage
1990 } else {
1991 // try cached info first - the enrolled flag is set only when active enrolment present
1992 if ($USER->id == $userid) {
1993 $coursecontext->reload_if_dirty();
1994 if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1995 if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1996 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1997 return false;
1999 return true;
2004 if ($onlyactive) {
2005 // look for active enrolments only
2006 $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
2008 if ($until === false) {
2009 return false;
2012 if ($USER->id == $userid) {
2013 if ($until == 0) {
2014 $until = ENROL_MAX_TIMESTAMP;
2016 $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
2017 if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
2018 unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
2019 remove_temp_course_roles($coursecontext);
2023 } else {
2024 // any enrolment is good for us here, even outdated, disabled or inactive
2025 $sql = "SELECT 'x'
2026 FROM {user_enrolments} ue
2027 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
2028 JOIN {user} u ON u.id = ue.userid
2029 WHERE ue.userid = :userid AND u.deleted = 0";
2030 $params = array('userid'=>$userid, 'courseid'=>$coursecontext->instanceid);
2031 if (!$DB->record_exists_sql($sql, $params)) {
2032 return false;
2037 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
2038 return false;
2041 return true;
2045 * Returns true if the user is able to access the course.
2047 * This function is in no way, shape, or form a substitute for require_login.
2048 * It should only be used in circumstances where it is not possible to call require_login
2049 * such as the navigation.
2051 * This function checks many of the methods of access to a course such as the view
2052 * capability, enrollments, and guest access. It also makes use of the cache
2053 * generated by require_login for guest access.
2055 * The flags within the $USER object that are used here should NEVER be used outside
2056 * of this function can_access_course and require_login. Doing so WILL break future
2057 * versions.
2059 * @param stdClass $course record
2060 * @param stdClass|int|null $user user record or id, current user if null
2061 * @param string $withcapability Check for this capability as well.
2062 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
2063 * @return boolean Returns true if the user is able to access the course
2065 function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
2066 global $DB, $USER;
2068 // this function originally accepted $coursecontext parameter
2069 if ($course instanceof context) {
2070 if ($course instanceof context_course) {
2071 debugging('deprecated context parameter, please use $course record');
2072 $coursecontext = $course;
2073 $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
2074 } else {
2075 debugging('Invalid context parameter, please use $course record');
2076 return false;
2078 } else {
2079 $coursecontext = context_course::instance($course->id);
2082 if (!isset($USER->id)) {
2083 // should never happen
2084 $USER->id = 0;
2085 debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
2088 // make sure there is a user specified
2089 if ($user === null) {
2090 $userid = $USER->id;
2091 } else {
2092 $userid = is_object($user) ? $user->id : $user;
2094 unset($user);
2096 if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
2097 return false;
2100 if ($userid == $USER->id) {
2101 if (!empty($USER->access['rsw'][$coursecontext->path])) {
2102 // the fact that somebody switched role means they can access the course no matter to what role they switched
2103 return true;
2107 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
2108 return false;
2111 if (is_viewing($coursecontext, $userid)) {
2112 return true;
2115 if ($userid != $USER->id) {
2116 // for performance reasons we do not verify temporary guest access for other users, sorry...
2117 return is_enrolled($coursecontext, $userid, '', $onlyactive);
2120 // === from here we deal only with $USER ===
2122 $coursecontext->reload_if_dirty();
2124 if (isset($USER->enrol['enrolled'][$course->id])) {
2125 if ($USER->enrol['enrolled'][$course->id] > time()) {
2126 return true;
2129 if (isset($USER->enrol['tempguest'][$course->id])) {
2130 if ($USER->enrol['tempguest'][$course->id] > time()) {
2131 return true;
2135 if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
2136 return true;
2139 // if not enrolled try to gain temporary guest access
2140 $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
2141 $enrols = enrol_get_plugins(true);
2142 foreach($instances as $instance) {
2143 if (!isset($enrols[$instance->enrol])) {
2144 continue;
2146 // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
2147 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2148 if ($until !== false and $until > time()) {
2149 $USER->enrol['tempguest'][$course->id] = $until;
2150 return true;
2153 if (isset($USER->enrol['tempguest'][$course->id])) {
2154 unset($USER->enrol['tempguest'][$course->id]);
2155 remove_temp_course_roles($coursecontext);
2158 return false;
2162 * Returns array with sql code and parameters returning all ids
2163 * of users enrolled into course.
2165 * This function is using 'eu[0-9]+_' prefix for table names and parameters.
2167 * @package core_enrol
2168 * @category access
2170 * @param context $context
2171 * @param string $withcapability
2172 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
2173 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
2174 * @return array list($sql, $params)
2176 function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
2177 global $DB, $CFG;
2179 // use unique prefix just in case somebody makes some SQL magic with the result
2180 static $i = 0;
2181 $i++;
2182 $prefix = 'eu'.$i.'_';
2184 // first find the course context
2185 $coursecontext = $context->get_course_context();
2187 $isfrontpage = ($coursecontext->instanceid == SITEID);
2189 $joins = array();
2190 $wheres = array();
2191 $params = array();
2193 list($contextids, $contextpaths) = get_context_info_list($context);
2195 // get all relevant capability info for all roles
2196 if ($withcapability) {
2197 list($incontexts, $cparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'ctx');
2198 $cparams['cap'] = $withcapability;
2200 $defs = array();
2201 $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.path
2202 FROM {role_capabilities} rc
2203 JOIN {context} ctx on rc.contextid = ctx.id
2204 WHERE rc.contextid $incontexts AND rc.capability = :cap";
2205 $rcs = $DB->get_records_sql($sql, $cparams);
2206 foreach ($rcs as $rc) {
2207 $defs[$rc->path][$rc->roleid] = $rc->permission;
2210 $access = array();
2211 if (!empty($defs)) {
2212 foreach ($contextpaths as $path) {
2213 if (empty($defs[$path])) {
2214 continue;
2216 foreach($defs[$path] as $roleid => $perm) {
2217 if ($perm == CAP_PROHIBIT) {
2218 $access[$roleid] = CAP_PROHIBIT;
2219 continue;
2221 if (!isset($access[$roleid])) {
2222 $access[$roleid] = (int)$perm;
2228 unset($defs);
2230 // make lists of roles that are needed and prohibited
2231 $needed = array(); // one of these is enough
2232 $prohibited = array(); // must not have any of these
2233 foreach ($access as $roleid => $perm) {
2234 if ($perm == CAP_PROHIBIT) {
2235 unset($needed[$roleid]);
2236 $prohibited[$roleid] = true;
2237 } else if ($perm == CAP_ALLOW and empty($prohibited[$roleid])) {
2238 $needed[$roleid] = true;
2242 $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
2243 $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
2245 $nobody = false;
2247 if ($isfrontpage) {
2248 if (!empty($prohibited[$defaultuserroleid]) or !empty($prohibited[$defaultfrontpageroleid])) {
2249 $nobody = true;
2250 } else if (!empty($needed[$defaultuserroleid]) or !empty($needed[$defaultfrontpageroleid])) {
2251 // everybody not having prohibit has the capability
2252 $needed = array();
2253 } else if (empty($needed)) {
2254 $nobody = true;
2256 } else {
2257 if (!empty($prohibited[$defaultuserroleid])) {
2258 $nobody = true;
2259 } else if (!empty($needed[$defaultuserroleid])) {
2260 // everybody not having prohibit has the capability
2261 $needed = array();
2262 } else if (empty($needed)) {
2263 $nobody = true;
2267 if ($nobody) {
2268 // nobody can match so return some SQL that does not return any results
2269 $wheres[] = "1 = 2";
2271 } else {
2273 if ($needed) {
2274 $ctxids = implode(',', $contextids);
2275 $roleids = implode(',', array_keys($needed));
2276 $joins[] = "JOIN {role_assignments} {$prefix}ra3 ON ({$prefix}ra3.userid = {$prefix}u.id AND {$prefix}ra3.roleid IN ($roleids) AND {$prefix}ra3.contextid IN ($ctxids))";
2279 if ($prohibited) {
2280 $ctxids = implode(',', $contextids);
2281 $roleids = implode(',', array_keys($prohibited));
2282 $joins[] = "LEFT JOIN {role_assignments} {$prefix}ra4 ON ({$prefix}ra4.userid = {$prefix}u.id AND {$prefix}ra4.roleid IN ($roleids) AND {$prefix}ra4.contextid IN ($ctxids))";
2283 $wheres[] = "{$prefix}ra4.id IS NULL";
2286 if ($groupid) {
2287 $joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}u.id AND {$prefix}gm.groupid = :{$prefix}gmid)";
2288 $params["{$prefix}gmid"] = $groupid;
2292 } else {
2293 if ($groupid) {
2294 $joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}u.id AND {$prefix}gm.groupid = :{$prefix}gmid)";
2295 $params["{$prefix}gmid"] = $groupid;
2299 $wheres[] = "{$prefix}u.deleted = 0 AND {$prefix}u.id <> :{$prefix}guestid";
2300 $params["{$prefix}guestid"] = $CFG->siteguest;
2302 if ($isfrontpage) {
2303 // all users are "enrolled" on the frontpage
2304 } else {
2305 $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$prefix}u.id";
2306 $joins[] = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
2307 $params[$prefix.'courseid'] = $coursecontext->instanceid;
2309 if ($onlyactive) {
2310 $wheres[] = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
2311 $wheres[] = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
2312 $now = round(time(), -2); // rounding helps caching in DB
2313 $params = array_merge($params, array($prefix.'enabled'=>ENROL_INSTANCE_ENABLED,
2314 $prefix.'active'=>ENROL_USER_ACTIVE,
2315 $prefix.'now1'=>$now, $prefix.'now2'=>$now));
2319 $joins = implode("\n", $joins);
2320 $wheres = "WHERE ".implode(" AND ", $wheres);
2322 $sql = "SELECT DISTINCT {$prefix}u.id
2323 FROM {user} {$prefix}u
2324 $joins
2325 $wheres";
2327 return array($sql, $params);
2331 * Returns list of users enrolled into course.
2333 * @package core_enrol
2334 * @category access
2336 * @param context $context
2337 * @param string $withcapability
2338 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
2339 * @param string $userfields requested user record fields
2340 * @param string $orderby
2341 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
2342 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
2343 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
2344 * @return array of user records
2346 function get_enrolled_users(context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = null,
2347 $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
2348 global $DB;
2350 list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
2351 $sql = "SELECT $userfields
2352 FROM {user} u
2353 JOIN ($esql) je ON je.id = u.id
2354 WHERE u.deleted = 0";
2356 if ($orderby) {
2357 $sql = "$sql ORDER BY $orderby";
2358 } else {
2359 list($sort, $sortparams) = users_order_by_sql('u');
2360 $sql = "$sql ORDER BY $sort";
2361 $params = array_merge($params, $sortparams);
2364 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2368 * Counts list of users enrolled into course (as per above function)
2370 * @package core_enrol
2371 * @category access
2373 * @param context $context
2374 * @param string $withcapability
2375 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
2376 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
2377 * @return array of user records
2379 function count_enrolled_users(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
2380 global $DB;
2382 list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
2383 $sql = "SELECT count(u.id)
2384 FROM {user} u
2385 JOIN ($esql) je ON je.id = u.id
2386 WHERE u.deleted = 0";
2388 return $DB->count_records_sql($sql, $params);
2392 * Loads the capability definitions for the component (from file).
2394 * Loads the capability definitions for the component (from file). If no
2395 * capabilities are defined for the component, we simply return an empty array.
2397 * @access private
2398 * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
2399 * @return array array of capabilities
2401 function load_capability_def($component) {
2402 $defpath = core_component::get_component_directory($component).'/db/access.php';
2404 $capabilities = array();
2405 if (file_exists($defpath)) {
2406 require($defpath);
2407 if (!empty(${$component.'_capabilities'})) {
2408 // BC capability array name
2409 // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
2410 debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
2411 $capabilities = ${$component.'_capabilities'};
2415 return $capabilities;
2419 * Gets the capabilities that have been cached in the database for this component.
2421 * @access private
2422 * @param string $component - examples: 'moodle', 'mod_forum'
2423 * @return array array of capabilities
2425 function get_cached_capabilities($component = 'moodle') {
2426 global $DB;
2427 return $DB->get_records('capabilities', array('component'=>$component));
2431 * Returns default capabilities for given role archetype.
2433 * @param string $archetype role archetype
2434 * @return array
2436 function get_default_capabilities($archetype) {
2437 global $DB;
2439 if (!$archetype) {
2440 return array();
2443 $alldefs = array();
2444 $defaults = array();
2445 $components = array();
2446 $allcaps = $DB->get_records('capabilities');
2448 foreach ($allcaps as $cap) {
2449 if (!in_array($cap->component, $components)) {
2450 $components[] = $cap->component;
2451 $alldefs = array_merge($alldefs, load_capability_def($cap->component));
2454 foreach($alldefs as $name=>$def) {
2455 // Use array 'archetypes if available. Only if not specified, use 'legacy'.
2456 if (isset($def['archetypes'])) {
2457 if (isset($def['archetypes'][$archetype])) {
2458 $defaults[$name] = $def['archetypes'][$archetype];
2460 // 'legacy' is for backward compatibility with 1.9 access.php
2461 } else {
2462 if (isset($def['legacy'][$archetype])) {
2463 $defaults[$name] = $def['legacy'][$archetype];
2468 return $defaults;
2472 * Return default roles that can be assigned, overridden or switched
2473 * by give role archetype.
2475 * @param string $type assign|override|switch
2476 * @param string $archetype
2477 * @return array of role ids
2479 function get_default_role_archetype_allows($type, $archetype) {
2480 global $DB;
2482 if (empty($archetype)) {
2483 return array();
2486 $roles = $DB->get_records('role');
2487 $archetypemap = array();
2488 foreach ($roles as $role) {
2489 if ($role->archetype) {
2490 $archetypemap[$role->archetype][$role->id] = $role->id;
2494 $defaults = array(
2495 'assign' => array(
2496 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
2497 'coursecreator' => array(),
2498 'editingteacher' => array('teacher', 'student'),
2499 'teacher' => array(),
2500 'student' => array(),
2501 'guest' => array(),
2502 'user' => array(),
2503 'frontpage' => array(),
2505 'override' => array(
2506 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2507 'coursecreator' => array(),
2508 'editingteacher' => array('teacher', 'student', 'guest'),
2509 'teacher' => array(),
2510 'student' => array(),
2511 'guest' => array(),
2512 'user' => array(),
2513 'frontpage' => array(),
2515 'switch' => array(
2516 'manager' => array('editingteacher', 'teacher', 'student', 'guest'),
2517 'coursecreator' => array(),
2518 'editingteacher' => array('teacher', 'student', 'guest'),
2519 'teacher' => array('student', 'guest'),
2520 'student' => array(),
2521 'guest' => array(),
2522 'user' => array(),
2523 'frontpage' => array(),
2527 if (!isset($defaults[$type][$archetype])) {
2528 debugging("Unknown type '$type'' or archetype '$archetype''");
2529 return array();
2532 $return = array();
2533 foreach ($defaults[$type][$archetype] as $at) {
2534 if (isset($archetypemap[$at])) {
2535 foreach ($archetypemap[$at] as $roleid) {
2536 $return[$roleid] = $roleid;
2541 return $return;
2545 * Reset role capabilities to default according to selected role archetype.
2546 * If no archetype selected, removes all capabilities.
2548 * @param int $roleid
2549 * @return void
2551 function reset_role_capabilities($roleid) {
2552 global $DB;
2554 $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
2555 $defaultcaps = get_default_capabilities($role->archetype);
2557 $systemcontext = context_system::instance();
2559 $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
2561 foreach($defaultcaps as $cap=>$permission) {
2562 assign_capability($cap, $permission, $roleid, $systemcontext->id);
2567 * Updates the capabilities table with the component capability definitions.
2568 * If no parameters are given, the function updates the core moodle
2569 * capabilities.
2571 * Note that the absence of the db/access.php capabilities definition file
2572 * will cause any stored capabilities for the component to be removed from
2573 * the database.
2575 * @access private
2576 * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
2577 * @return boolean true if success, exception in case of any problems
2579 function update_capabilities($component = 'moodle') {
2580 global $DB, $OUTPUT;
2582 $storedcaps = array();
2584 $filecaps = load_capability_def($component);
2585 foreach($filecaps as $capname=>$unused) {
2586 if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
2587 debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
2591 $cachedcaps = get_cached_capabilities($component);
2592 if ($cachedcaps) {
2593 foreach ($cachedcaps as $cachedcap) {
2594 array_push($storedcaps, $cachedcap->name);
2595 // update risk bitmasks and context levels in existing capabilities if needed
2596 if (array_key_exists($cachedcap->name, $filecaps)) {
2597 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
2598 $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
2600 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
2601 $updatecap = new stdClass();
2602 $updatecap->id = $cachedcap->id;
2603 $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
2604 $DB->update_record('capabilities', $updatecap);
2606 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
2607 $updatecap = new stdClass();
2608 $updatecap->id = $cachedcap->id;
2609 $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
2610 $DB->update_record('capabilities', $updatecap);
2613 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
2614 $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
2616 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
2617 $updatecap = new stdClass();
2618 $updatecap->id = $cachedcap->id;
2619 $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
2620 $DB->update_record('capabilities', $updatecap);
2626 // Are there new capabilities in the file definition?
2627 $newcaps = array();
2629 foreach ($filecaps as $filecap => $def) {
2630 if (!$storedcaps ||
2631 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
2632 if (!array_key_exists('riskbitmask', $def)) {
2633 $def['riskbitmask'] = 0; // no risk if not specified
2635 $newcaps[$filecap] = $def;
2638 // Add new capabilities to the stored definition.
2639 $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
2640 foreach ($newcaps as $capname => $capdef) {
2641 $capability = new stdClass();
2642 $capability->name = $capname;
2643 $capability->captype = $capdef['captype'];
2644 $capability->contextlevel = $capdef['contextlevel'];
2645 $capability->component = $component;
2646 $capability->riskbitmask = $capdef['riskbitmask'];
2648 $DB->insert_record('capabilities', $capability, false);
2650 if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
2651 if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
2652 foreach ($rolecapabilities as $rolecapability){
2653 //assign_capability will update rather than insert if capability exists
2654 if (!assign_capability($capname, $rolecapability->permission,
2655 $rolecapability->roleid, $rolecapability->contextid, true)){
2656 echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
2660 // we ignore archetype key if we have cloned permissions
2661 } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
2662 assign_legacy_capabilities($capname, $capdef['archetypes']);
2663 // 'legacy' is for backward compatibility with 1.9 access.php
2664 } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
2665 assign_legacy_capabilities($capname, $capdef['legacy']);
2668 // Are there any capabilities that have been removed from the file
2669 // definition that we need to delete from the stored capabilities and
2670 // role assignments?
2671 capabilities_cleanup($component, $filecaps);
2673 // reset static caches
2674 accesslib_clear_all_caches(false);
2676 return true;
2680 * Deletes cached capabilities that are no longer needed by the component.
2681 * Also unassigns these capabilities from any roles that have them.
2682 * NOTE: this function is called from lib/db/upgrade.php
2684 * @access private
2685 * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
2686 * @param array $newcapdef array of the new capability definitions that will be
2687 * compared with the cached capabilities
2688 * @return int number of deprecated capabilities that have been removed
2690 function capabilities_cleanup($component, $newcapdef = null) {
2691 global $DB;
2693 $removedcount = 0;
2695 if ($cachedcaps = get_cached_capabilities($component)) {
2696 foreach ($cachedcaps as $cachedcap) {
2697 if (empty($newcapdef) ||
2698 array_key_exists($cachedcap->name, $newcapdef) === false) {
2700 // Remove from capabilities cache.
2701 $DB->delete_records('capabilities', array('name'=>$cachedcap->name));
2702 $removedcount++;
2703 // Delete from roles.
2704 if ($roles = get_roles_with_capability($cachedcap->name)) {
2705 foreach($roles as $role) {
2706 if (!unassign_capability($cachedcap->name, $role->id)) {
2707 print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
2711 } // End if.
2714 return $removedcount;
2718 * Returns an array of all the known types of risk
2719 * The array keys can be used, for example as CSS class names, or in calls to
2720 * print_risk_icon. The values are the corresponding RISK_ constants.
2722 * @return array all the known types of risk.
2724 function get_all_risks() {
2725 return array(
2726 'riskmanagetrust' => RISK_MANAGETRUST,
2727 'riskconfig' => RISK_CONFIG,
2728 'riskxss' => RISK_XSS,
2729 'riskpersonal' => RISK_PERSONAL,
2730 'riskspam' => RISK_SPAM,
2731 'riskdataloss' => RISK_DATALOSS,
2736 * Return a link to moodle docs for a given capability name
2738 * @param stdClass $capability a capability - a row from the mdl_capabilities table.
2739 * @return string the human-readable capability name as a link to Moodle Docs.
2741 function get_capability_docs_link($capability) {
2742 $url = get_docs_url('Capabilities/' . $capability->name);
2743 return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
2747 * This function pulls out all the resolved capabilities (overrides and
2748 * defaults) of a role used in capability overrides in contexts at a given
2749 * context.
2751 * @param int $roleid
2752 * @param context $context
2753 * @param string $cap capability, optional, defaults to ''
2754 * @return array Array of capabilities
2756 function role_context_capabilities($roleid, context $context, $cap = '') {
2757 global $DB;
2759 $contexts = $context->get_parent_context_ids(true);
2760 $contexts = '('.implode(',', $contexts).')';
2762 $params = array($roleid);
2764 if ($cap) {
2765 $search = " AND rc.capability = ? ";
2766 $params[] = $cap;
2767 } else {
2768 $search = '';
2771 $sql = "SELECT rc.*
2772 FROM {role_capabilities} rc, {context} c
2773 WHERE rc.contextid in $contexts
2774 AND rc.roleid = ?
2775 AND rc.contextid = c.id $search
2776 ORDER BY c.contextlevel DESC, rc.capability DESC";
2778 $capabilities = array();
2780 if ($records = $DB->get_records_sql($sql, $params)) {
2781 // We are traversing via reverse order.
2782 foreach ($records as $record) {
2783 // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
2784 if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
2785 $capabilities[$record->capability] = $record->permission;
2789 return $capabilities;
2793 * Constructs array with contextids as first parameter and context paths,
2794 * in both cases bottom top including self.
2796 * @access private
2797 * @param context $context
2798 * @return array
2800 function get_context_info_list(context $context) {
2801 $contextids = explode('/', ltrim($context->path, '/'));
2802 $contextpaths = array();
2803 $contextids2 = $contextids;
2804 while ($contextids2) {
2805 $contextpaths[] = '/' . implode('/', $contextids2);
2806 array_pop($contextids2);
2808 return array($contextids, $contextpaths);
2812 * Check if context is the front page context or a context inside it
2814 * Returns true if this context is the front page context, or a context inside it,
2815 * otherwise false.
2817 * @param context $context a context object.
2818 * @return bool
2820 function is_inside_frontpage(context $context) {
2821 $frontpagecontext = context_course::instance(SITEID);
2822 return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
2826 * Returns capability information (cached)
2828 * @param string $capabilityname
2829 * @return stdClass or null if capability not found
2831 function get_capability_info($capabilityname) {
2832 global $ACCESSLIB_PRIVATE, $DB; // one request per page only
2834 //TODO: MUC - this could be cached in shared memory, it would eliminate 1 query per page
2836 if (empty($ACCESSLIB_PRIVATE->capabilities)) {
2837 $ACCESSLIB_PRIVATE->capabilities = array();
2838 $caps = $DB->get_records('capabilities', array(), 'id, name, captype, riskbitmask');
2839 foreach ($caps as $cap) {
2840 $capname = $cap->name;
2841 unset($cap->id);
2842 unset($cap->name);
2843 $cap->riskbitmask = (int)$cap->riskbitmask;
2844 $ACCESSLIB_PRIVATE->capabilities[$capname] = $cap;
2848 return isset($ACCESSLIB_PRIVATE->capabilities[$capabilityname]) ? $ACCESSLIB_PRIVATE->capabilities[$capabilityname] : null;
2852 * Returns the human-readable, translated version of the capability.
2853 * Basically a big switch statement.
2855 * @param string $capabilityname e.g. mod/choice:readresponses
2856 * @return string
2858 function get_capability_string($capabilityname) {
2860 // Typical capability name is 'plugintype/pluginname:capabilityname'
2861 list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
2863 if ($type === 'moodle') {
2864 $component = 'core_role';
2865 } else if ($type === 'quizreport') {
2866 //ugly hack!!
2867 $component = 'quiz_'.$name;
2868 } else {
2869 $component = $type.'_'.$name;
2872 $stringname = $name.':'.$capname;
2874 if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
2875 return get_string($stringname, $component);
2878 $dir = core_component::get_component_directory($component);
2879 if (!file_exists($dir)) {
2880 // plugin broken or does not exist, do not bother with printing of debug message
2881 return $capabilityname.' ???';
2884 // something is wrong in plugin, better print debug
2885 return get_string($stringname, $component);
2889 * This gets the mod/block/course/core etc strings.
2891 * @param string $component
2892 * @param int $contextlevel
2893 * @return string|bool String is success, false if failed
2895 function get_component_string($component, $contextlevel) {
2897 if ($component === 'moodle' or $component === 'core') {
2898 switch ($contextlevel) {
2899 // TODO: this should probably use context level names instead
2900 case CONTEXT_SYSTEM: return get_string('coresystem');
2901 case CONTEXT_USER: return get_string('users');
2902 case CONTEXT_COURSECAT: return get_string('categories');
2903 case CONTEXT_COURSE: return get_string('course');
2904 case CONTEXT_MODULE: return get_string('activities');
2905 case CONTEXT_BLOCK: return get_string('block');
2906 default: print_error('unknowncontext');
2910 list($type, $name) = core_component::normalize_component($component);
2911 $dir = core_component::get_plugin_directory($type, $name);
2912 if (!file_exists($dir)) {
2913 // plugin not installed, bad luck, there is no way to find the name
2914 return $component.' ???';
2917 switch ($type) {
2918 // TODO: this is really hacky, anyway it should be probably moved to lib/pluginlib.php
2919 case 'quiz': return get_string($name.':componentname', $component);// insane hack!!!
2920 case 'repository': return get_string('repository', 'repository').': '.get_string('pluginname', $component);
2921 case 'gradeimport': return get_string('gradeimport', 'grades').': '.get_string('pluginname', $component);
2922 case 'gradeexport': return get_string('gradeexport', 'grades').': '.get_string('pluginname', $component);
2923 case 'gradereport': return get_string('gradereport', 'grades').': '.get_string('pluginname', $component);
2924 case 'webservice': return get_string('webservice', 'webservice').': '.get_string('pluginname', $component);
2925 case 'block': return get_string('block').': '.get_string('pluginname', basename($component));
2926 case 'mod':
2927 if (get_string_manager()->string_exists('pluginname', $component)) {
2928 return get_string('activity').': '.get_string('pluginname', $component);
2929 } else {
2930 return get_string('activity').': '.get_string('modulename', $component);
2932 default: return get_string('pluginname', $component);
2937 * Gets the list of roles assigned to this context and up (parents)
2938 * from the list of roles that are visible on user profile page
2939 * and participants page.
2941 * @param context $context
2942 * @return array
2944 function get_profile_roles(context $context) {
2945 global $CFG, $DB;
2947 if (empty($CFG->profileroles)) {
2948 return array();
2951 list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a');
2952 list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2953 $params = array_merge($params, $cparams);
2955 if ($coursecontext = $context->get_course_context(false)) {
2956 $params['coursecontext'] = $coursecontext->id;
2957 } else {
2958 $params['coursecontext'] = 0;
2961 $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2962 FROM {role_assignments} ra, {role} r
2963 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2964 WHERE r.id = ra.roleid
2965 AND ra.contextid $contextlist
2966 AND r.id $rallowed
2967 ORDER BY r.sortorder ASC";
2969 return $DB->get_records_sql($sql, $params);
2973 * Gets the list of roles assigned to this context and up (parents)
2975 * @param context $context
2976 * @return array
2978 function get_roles_used_in_context(context $context) {
2979 global $DB;
2981 list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
2983 if ($coursecontext = $context->get_course_context(false)) {
2984 $params['coursecontext'] = $coursecontext->id;
2985 } else {
2986 $params['coursecontext'] = 0;
2989 $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2990 FROM {role_assignments} ra, {role} r
2991 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2992 WHERE r.id = ra.roleid
2993 AND ra.contextid $contextlist
2994 ORDER BY r.sortorder ASC";
2996 return $DB->get_records_sql($sql, $params);
3000 * This function is used to print roles column in user profile page.
3001 * It is using the CFG->profileroles to limit the list to only interesting roles.
3002 * (The permission tab has full details of user role assignments.)
3004 * @param int $userid
3005 * @param int $courseid
3006 * @return string
3008 function get_user_roles_in_course($userid, $courseid) {
3009 global $CFG, $DB;
3011 if (empty($CFG->profileroles)) {
3012 return '';
3015 if ($courseid == SITEID) {
3016 $context = context_system::instance();
3017 } else {
3018 $context = context_course::instance($courseid);
3021 if (empty($CFG->profileroles)) {
3022 return array();
3025 list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a');
3026 list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
3027 $params = array_merge($params, $cparams);
3029 if ($coursecontext = $context->get_course_context(false)) {
3030 $params['coursecontext'] = $coursecontext->id;
3031 } else {
3032 $params['coursecontext'] = 0;
3035 $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
3036 FROM {role_assignments} ra, {role} r
3037 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3038 WHERE r.id = ra.roleid
3039 AND ra.contextid $contextlist
3040 AND r.id $rallowed
3041 AND ra.userid = :userid
3042 ORDER BY r.sortorder ASC";
3043 $params['userid'] = $userid;
3045 $rolestring = '';
3047 if ($roles = $DB->get_records_sql($sql, $params)) {
3048 $rolenames = role_fix_names($roles, $context, ROLENAME_ALIAS, true); // Substitute aliases
3050 foreach ($rolenames as $roleid => $rolename) {
3051 $rolenames[$roleid] = '<a href="'.$CFG->wwwroot.'/user/index.php?contextid='.$context->id.'&amp;roleid='.$roleid.'">'.$rolename.'</a>';
3053 $rolestring = implode(',', $rolenames);
3056 return $rolestring;
3060 * Checks if a user can assign users to a particular role in this context
3062 * @param context $context
3063 * @param int $targetroleid - the id of the role you want to assign users to
3064 * @return boolean
3066 function user_can_assign(context $context, $targetroleid) {
3067 global $DB;
3069 // First check to see if the user is a site administrator.
3070 if (is_siteadmin()) {
3071 return true;
3074 // Check if user has override capability.
3075 // If not return false.
3076 if (!has_capability('moodle/role:assign', $context)) {
3077 return false;
3079 // pull out all active roles of this user from this context(or above)
3080 if ($userroles = get_user_roles($context)) {
3081 foreach ($userroles as $userrole) {
3082 // if any in the role_allow_override table, then it's ok
3083 if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
3084 return true;
3089 return false;
3093 * Returns all site roles in correct sort order.
3095 * Note: this method does not localise role names or descriptions,
3096 * use role_get_names() if you need role names.
3098 * @param context $context optional context for course role name aliases
3099 * @return array of role records with optional coursealias property
3101 function get_all_roles(context $context = null) {
3102 global $DB;
3104 if (!$context or !$coursecontext = $context->get_course_context(false)) {
3105 $coursecontext = null;
3108 if ($coursecontext) {
3109 $sql = "SELECT r.*, rn.name AS coursealias
3110 FROM {role} r
3111 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3112 ORDER BY r.sortorder ASC";
3113 return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
3115 } else {
3116 return $DB->get_records('role', array(), 'sortorder ASC');
3121 * Returns roles of a specified archetype
3123 * @param string $archetype
3124 * @return array of full role records
3126 function get_archetype_roles($archetype) {
3127 global $DB;
3128 return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
3132 * Gets all the user roles assigned in this context, or higher contexts
3133 * this is mainly used when checking if a user can assign a role, or overriding a role
3134 * i.e. we need to know what this user holds, in order to verify against allow_assign and
3135 * allow_override tables
3137 * @param context $context
3138 * @param int $userid
3139 * @param bool $checkparentcontexts defaults to true
3140 * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
3141 * @return array
3143 function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
3144 global $USER, $DB;
3146 if (empty($userid)) {
3147 if (empty($USER->id)) {
3148 return array();
3150 $userid = $USER->id;
3153 if ($checkparentcontexts) {
3154 $contextids = $context->get_parent_context_ids();
3155 } else {
3156 $contextids = array();
3158 $contextids[] = $context->id;
3160 list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
3162 array_unshift($params, $userid);
3164 $sql = "SELECT ra.*, r.name, r.shortname
3165 FROM {role_assignments} ra, {role} r, {context} c
3166 WHERE ra.userid = ?
3167 AND ra.roleid = r.id
3168 AND ra.contextid = c.id
3169 AND ra.contextid $contextids
3170 ORDER BY $order";
3172 return $DB->get_records_sql($sql ,$params);
3176 * Like get_user_roles, but adds in the authenticated user role, and the front
3177 * page roles, if applicable.
3179 * @param context $context the context.
3180 * @param int $userid optional. Defaults to $USER->id
3181 * @return array of objects with fields ->userid, ->contextid and ->roleid.
3183 function get_user_roles_with_special(context $context, $userid = 0) {
3184 global $CFG, $USER;
3186 if (empty($userid)) {
3187 if (empty($USER->id)) {
3188 return array();
3190 $userid = $USER->id;
3193 $ras = get_user_roles($context, $userid);
3195 // Add front-page role if relevant.
3196 $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3197 $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
3198 is_inside_frontpage($context);
3199 if ($defaultfrontpageroleid && $isfrontpage) {
3200 $frontpagecontext = context_course::instance(SITEID);
3201 $ra = new stdClass();
3202 $ra->userid = $userid;
3203 $ra->contextid = $frontpagecontext->id;
3204 $ra->roleid = $defaultfrontpageroleid;
3205 $ras[] = $ra;
3208 // Add authenticated user role if relevant.
3209 $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3210 if ($defaultuserroleid && !isguestuser($userid)) {
3211 $systemcontext = context_system::instance();
3212 $ra = new stdClass();
3213 $ra->userid = $userid;
3214 $ra->contextid = $systemcontext->id;
3215 $ra->roleid = $defaultuserroleid;
3216 $ras[] = $ra;
3219 return $ras;
3223 * Creates a record in the role_allow_override table
3225 * @param int $sroleid source roleid
3226 * @param int $troleid target roleid
3227 * @return void
3229 function allow_override($sroleid, $troleid) {
3230 global $DB;
3232 $record = new stdClass();
3233 $record->roleid = $sroleid;
3234 $record->allowoverride = $troleid;
3235 $DB->insert_record('role_allow_override', $record);
3239 * Creates a record in the role_allow_assign table
3241 * @param int $fromroleid source roleid
3242 * @param int $targetroleid target roleid
3243 * @return void
3245 function allow_assign($fromroleid, $targetroleid) {
3246 global $DB;
3248 $record = new stdClass();
3249 $record->roleid = $fromroleid;
3250 $record->allowassign = $targetroleid;
3251 $DB->insert_record('role_allow_assign', $record);
3255 * Creates a record in the role_allow_switch table
3257 * @param int $fromroleid source roleid
3258 * @param int $targetroleid target roleid
3259 * @return void
3261 function allow_switch($fromroleid, $targetroleid) {
3262 global $DB;
3264 $record = new stdClass();
3265 $record->roleid = $fromroleid;
3266 $record->allowswitch = $targetroleid;
3267 $DB->insert_record('role_allow_switch', $record);
3271 * Gets a list of roles that this user can assign in this context
3273 * @param context $context the context.
3274 * @param int $rolenamedisplay the type of role name to display. One of the
3275 * ROLENAME_X constants. Default ROLENAME_ALIAS.
3276 * @param bool $withusercounts if true, count the number of users with each role.
3277 * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
3278 * @return array if $withusercounts is false, then an array $roleid => $rolename.
3279 * if $withusercounts is true, returns a list of three arrays,
3280 * $rolenames, $rolecounts, and $nameswithcounts.
3282 function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
3283 global $USER, $DB;
3285 // make sure there is a real user specified
3286 if ($user === null) {
3287 $userid = isset($USER->id) ? $USER->id : 0;
3288 } else {
3289 $userid = is_object($user) ? $user->id : $user;
3292 if (!has_capability('moodle/role:assign', $context, $userid)) {
3293 if ($withusercounts) {
3294 return array(array(), array(), array());
3295 } else {
3296 return array();
3300 $params = array();
3301 $extrafields = '';
3303 if ($withusercounts) {
3304 $extrafields = ', (SELECT count(u.id)
3305 FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
3306 WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
3307 ) AS usercount';
3308 $params['conid'] = $context->id;
3311 if (is_siteadmin($userid)) {
3312 // show all roles allowed in this context to admins
3313 $assignrestriction = "";
3314 } else {
3315 $parents = $context->get_parent_context_ids(true);
3316 $contexts = implode(',' , $parents);
3317 $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
3318 FROM {role_allow_assign} raa
3319 JOIN {role_assignments} ra ON ra.roleid = raa.roleid
3320 WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3321 ) ar ON ar.id = r.id";
3322 $params['userid'] = $userid;
3324 $params['contextlevel'] = $context->contextlevel;
3326 if ($coursecontext = $context->get_course_context(false)) {
3327 $params['coursecontext'] = $coursecontext->id;
3328 } else {
3329 $params['coursecontext'] = 0; // no course aliases
3330 $coursecontext = null;
3332 $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
3333 FROM {role} r
3334 $assignrestriction
3335 JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
3336 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3337 ORDER BY r.sortorder ASC";
3338 $roles = $DB->get_records_sql($sql, $params);
3340 $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
3342 if (!$withusercounts) {
3343 return $rolenames;
3346 $rolecounts = array();
3347 $nameswithcounts = array();
3348 foreach ($roles as $role) {
3349 $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
3350 $rolecounts[$role->id] = $roles[$role->id]->usercount;
3352 return array($rolenames, $rolecounts, $nameswithcounts);
3356 * Gets a list of roles that this user can switch to in a context
3358 * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
3359 * This function just process the contents of the role_allow_switch table. You also need to
3360 * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
3362 * @param context $context a context.
3363 * @return array an array $roleid => $rolename.
3365 function get_switchable_roles(context $context) {
3366 global $USER, $DB;
3368 $params = array();
3369 $extrajoins = '';
3370 $extrawhere = '';
3371 if (!is_siteadmin()) {
3372 // Admins are allowed to switch to any role with.
3373 // Others are subject to the additional constraint that the switch-to role must be allowed by
3374 // 'role_allow_switch' for some role they have assigned in this context or any parent.
3375 $parents = $context->get_parent_context_ids(true);
3376 $contexts = implode(',' , $parents);
3378 $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
3379 JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3380 $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
3381 $params['userid'] = $USER->id;
3384 if ($coursecontext = $context->get_course_context(false)) {
3385 $params['coursecontext'] = $coursecontext->id;
3386 } else {
3387 $params['coursecontext'] = 0; // no course aliases
3388 $coursecontext = null;
3391 $query = "
3392 SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3393 FROM (SELECT DISTINCT rc.roleid
3394 FROM {role_capabilities} rc
3395 $extrajoins
3396 $extrawhere) idlist
3397 JOIN {role} r ON r.id = idlist.roleid
3398 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3399 ORDER BY r.sortorder";
3400 $roles = $DB->get_records_sql($query, $params);
3402 return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
3406 * Gets a list of roles that this user can override in this context.
3408 * @param context $context the context.
3409 * @param int $rolenamedisplay the type of role name to display. One of the
3410 * ROLENAME_X constants. Default ROLENAME_ALIAS.
3411 * @param bool $withcounts if true, count the number of overrides that are set for each role.
3412 * @return array if $withcounts is false, then an array $roleid => $rolename.
3413 * if $withusercounts is true, returns a list of three arrays,
3414 * $rolenames, $rolecounts, and $nameswithcounts.
3416 function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
3417 global $USER, $DB;
3419 if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
3420 if ($withcounts) {
3421 return array(array(), array(), array());
3422 } else {
3423 return array();
3427 $parents = $context->get_parent_context_ids(true);
3428 $contexts = implode(',' , $parents);
3430 $params = array();
3431 $extrafields = '';
3433 $params['userid'] = $USER->id;
3434 if ($withcounts) {
3435 $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
3436 WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
3437 $params['conid'] = $context->id;
3440 if ($coursecontext = $context->get_course_context(false)) {
3441 $params['coursecontext'] = $coursecontext->id;
3442 } else {
3443 $params['coursecontext'] = 0; // no course aliases
3444 $coursecontext = null;
3447 if (is_siteadmin()) {
3448 // show all roles to admins
3449 $roles = $DB->get_records_sql("
3450 SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3451 FROM {role} ro
3452 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3453 ORDER BY ro.sortorder ASC", $params);
3455 } else {
3456 $roles = $DB->get_records_sql("
3457 SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3458 FROM {role} ro
3459 JOIN (SELECT DISTINCT r.id
3460 FROM {role} r
3461 JOIN {role_allow_override} rao ON r.id = rao.allowoverride
3462 JOIN {role_assignments} ra ON rao.roleid = ra.roleid
3463 WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3464 ) inline_view ON ro.id = inline_view.id
3465 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3466 ORDER BY ro.sortorder ASC", $params);
3469 $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
3471 if (!$withcounts) {
3472 return $rolenames;
3475 $rolecounts = array();
3476 $nameswithcounts = array();
3477 foreach ($roles as $role) {
3478 $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
3479 $rolecounts[$role->id] = $roles[$role->id]->overridecount;
3481 return array($rolenames, $rolecounts, $nameswithcounts);
3485 * Create a role menu suitable for default role selection in enrol plugins.
3487 * @package core_enrol
3489 * @param context $context
3490 * @param int $addroleid current or default role - always added to list
3491 * @return array roleid=>localised role name
3493 function get_default_enrol_roles(context $context, $addroleid = null) {
3494 global $DB;
3496 $params = array('contextlevel'=>CONTEXT_COURSE);
3498 if ($coursecontext = $context->get_course_context(false)) {
3499 $params['coursecontext'] = $coursecontext->id;
3500 } else {
3501 $params['coursecontext'] = 0; // no course names
3502 $coursecontext = null;
3505 if ($addroleid) {
3506 $addrole = "OR r.id = :addroleid";
3507 $params['addroleid'] = $addroleid;
3508 } else {
3509 $addrole = "";
3512 $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3513 FROM {role} r
3514 LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
3515 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3516 WHERE rcl.id IS NOT NULL $addrole
3517 ORDER BY sortorder DESC";
3519 $roles = $DB->get_records_sql($sql, $params);
3521 return role_fix_names($roles, $context, ROLENAME_BOTH, true);
3525 * Return context levels where this role is assignable.
3527 * @param integer $roleid the id of a role.
3528 * @return array list of the context levels at which this role may be assigned.
3530 function get_role_contextlevels($roleid) {
3531 global $DB;
3532 return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
3533 'contextlevel', 'id,contextlevel');
3537 * Return roles suitable for assignment at the specified context level.
3539 * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
3541 * @param integer $contextlevel a contextlevel.
3542 * @return array list of role ids that are assignable at this context level.
3544 function get_roles_for_contextlevels($contextlevel) {
3545 global $DB;
3546 return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
3547 '', 'id,roleid');
3551 * Returns default context levels where roles can be assigned.
3553 * @param string $rolearchetype one of the role archetypes - that is, one of the keys
3554 * from the array returned by get_role_archetypes();
3555 * @return array list of the context levels at which this type of role may be assigned by default.
3557 function get_default_contextlevels($rolearchetype) {
3558 static $defaults = array(
3559 'manager' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
3560 'coursecreator' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
3561 'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3562 'teacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3563 'student' => array(CONTEXT_COURSE, CONTEXT_MODULE),
3564 'guest' => array(),
3565 'user' => array(),
3566 'frontpage' => array());
3568 if (isset($defaults[$rolearchetype])) {
3569 return $defaults[$rolearchetype];
3570 } else {
3571 return array();
3576 * Set the context levels at which a particular role can be assigned.
3577 * Throws exceptions in case of error.
3579 * @param integer $roleid the id of a role.
3580 * @param array $contextlevels the context levels at which this role should be assignable,
3581 * duplicate levels are removed.
3582 * @return void
3584 function set_role_contextlevels($roleid, array $contextlevels) {
3585 global $DB;
3586 $DB->delete_records('role_context_levels', array('roleid' => $roleid));
3587 $rcl = new stdClass();
3588 $rcl->roleid = $roleid;
3589 $contextlevels = array_unique($contextlevels);
3590 foreach ($contextlevels as $level) {
3591 $rcl->contextlevel = $level;
3592 $DB->insert_record('role_context_levels', $rcl, false, true);
3597 * Who has this capability in this context?
3599 * This can be a very expensive call - use sparingly and keep
3600 * the results if you are going to need them again soon.
3602 * Note if $fields is empty this function attempts to get u.*
3603 * which can get rather large - and has a serious perf impact
3604 * on some DBs.
3606 * @param context $context
3607 * @param string|array $capability - capability name(s)
3608 * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
3609 * @param string $sort - the sort order. Default is lastaccess time.
3610 * @param mixed $limitfrom - number of records to skip (offset)
3611 * @param mixed $limitnum - number of records to fetch
3612 * @param string|array $groups - single group or array of groups - only return
3613 * users who are in one of these group(s).
3614 * @param string|array $exceptions - list of users to exclude, comma separated or array
3615 * @param bool $doanything_ignored not used any more, admin accounts are never returned
3616 * @param bool $view_ignored - use get_enrolled_sql() instead
3617 * @param bool $useviewallgroups if $groups is set the return users who
3618 * have capability both $capability and moodle/site:accessallgroups
3619 * in this context, as well as users who have $capability and who are
3620 * in $groups.
3621 * @return array of user records
3623 function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
3624 $groups = '', $exceptions = '', $doanything_ignored = null, $view_ignored = null, $useviewallgroups = false) {
3625 global $CFG, $DB;
3627 $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3628 $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3630 $ctxids = trim($context->path, '/');
3631 $ctxids = str_replace('/', ',', $ctxids);
3633 // Context is the frontpage
3634 $iscoursepage = false; // coursepage other than fp
3635 $isfrontpage = false;
3636 if ($context->contextlevel == CONTEXT_COURSE) {
3637 if ($context->instanceid == SITEID) {
3638 $isfrontpage = true;
3639 } else {
3640 $iscoursepage = true;
3643 $isfrontpage = ($isfrontpage || is_inside_frontpage($context));
3645 $caps = (array)$capability;
3647 // construct list of context paths bottom-->top
3648 list($contextids, $paths) = get_context_info_list($context);
3650 // we need to find out all roles that have these capabilities either in definition or in overrides
3651 $defs = array();
3652 list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
3653 list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, 'cap');
3654 $params = array_merge($params, $params2);
3655 $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3656 FROM {role_capabilities} rc
3657 JOIN {context} ctx on rc.contextid = ctx.id
3658 WHERE rc.contextid $incontexts AND rc.capability $incaps";
3660 $rcs = $DB->get_records_sql($sql, $params);
3661 foreach ($rcs as $rc) {
3662 $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3665 // go through the permissions bottom-->top direction to evaluate the current permission,
3666 // first one wins (prohibit is an exception that always wins)
3667 $access = array();
3668 foreach ($caps as $cap) {
3669 foreach ($paths as $path) {
3670 if (empty($defs[$cap][$path])) {
3671 continue;
3673 foreach($defs[$cap][$path] as $roleid => $perm) {
3674 if ($perm == CAP_PROHIBIT) {
3675 $access[$cap][$roleid] = CAP_PROHIBIT;
3676 continue;
3678 if (!isset($access[$cap][$roleid])) {
3679 $access[$cap][$roleid] = (int)$perm;
3685 // make lists of roles that are needed and prohibited in this context
3686 $needed = array(); // one of these is enough
3687 $prohibited = array(); // must not have any of these
3688 foreach ($caps as $cap) {
3689 if (empty($access[$cap])) {
3690 continue;
3692 foreach ($access[$cap] as $roleid => $perm) {
3693 if ($perm == CAP_PROHIBIT) {
3694 unset($needed[$cap][$roleid]);
3695 $prohibited[$cap][$roleid] = true;
3696 } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
3697 $needed[$cap][$roleid] = true;
3700 if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
3701 // easy, nobody has the permission
3702 unset($needed[$cap]);
3703 unset($prohibited[$cap]);
3704 } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
3705 // everybody is disqualified on the frontpage
3706 unset($needed[$cap]);
3707 unset($prohibited[$cap]);
3709 if (empty($prohibited[$cap])) {
3710 unset($prohibited[$cap]);
3714 if (empty($needed)) {
3715 // there can not be anybody if no roles match this request
3716 return array();
3719 if (empty($prohibited)) {
3720 // we can compact the needed roles
3721 $n = array();
3722 foreach ($needed as $cap) {
3723 foreach ($cap as $roleid=>$unused) {
3724 $n[$roleid] = true;
3727 $needed = array('any'=>$n);
3728 unset($n);
3731 // ***** Set up default fields ******
3732 if (empty($fields)) {
3733 if ($iscoursepage) {
3734 $fields = 'u.*, ul.timeaccess AS lastaccess';
3735 } else {
3736 $fields = 'u.*';
3738 } else {
3739 if (debugging('', DEBUG_DEVELOPER) && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
3740 debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
3744 // Set up default sort
3745 if (empty($sort)) { // default to course lastaccess or just lastaccess
3746 if ($iscoursepage) {
3747 $sort = 'ul.timeaccess';
3748 } else {
3749 $sort = 'u.lastaccess';
3753 // Prepare query clauses
3754 $wherecond = array();
3755 $params = array();
3756 $joins = array();
3758 // User lastaccess JOIN
3759 if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
3760 // user_lastaccess is not required MDL-13810
3761 } else {
3762 if ($iscoursepage) {
3763 $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
3764 } else {
3765 throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
3769 // We never return deleted users or guest account.
3770 $wherecond[] = "u.deleted = 0 AND u.id <> :guestid";
3771 $params['guestid'] = $CFG->siteguest;
3773 // Groups
3774 if ($groups) {
3775 $groups = (array)$groups;
3776 list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
3777 $grouptest = "u.id IN (SELECT userid FROM {groups_members} gm WHERE gm.groupid $grouptest)";
3778 $params = array_merge($params, $grpparams);
3780 if ($useviewallgroups) {
3781 $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
3782 if (!empty($viewallgroupsusers)) {
3783 $wherecond[] = "($grouptest OR u.id IN (" . implode(',', array_keys($viewallgroupsusers)) . '))';
3784 } else {
3785 $wherecond[] = "($grouptest)";
3787 } else {
3788 $wherecond[] = "($grouptest)";
3792 // User exceptions
3793 if (!empty($exceptions)) {
3794 $exceptions = (array)$exceptions;
3795 list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc', false);
3796 $params = array_merge($params, $exparams);
3797 $wherecond[] = "u.id $exsql";
3800 // now add the needed and prohibited roles conditions as joins
3801 if (!empty($needed['any'])) {
3802 // simple case - there are no prohibits involved
3803 if (!empty($needed['any'][$defaultuserroleid]) or ($isfrontpage and !empty($needed['any'][$defaultfrontpageroleid]))) {
3804 // everybody
3805 } else {
3806 $joins[] = "JOIN (SELECT DISTINCT userid
3807 FROM {role_assignments}
3808 WHERE contextid IN ($ctxids)
3809 AND roleid IN (".implode(',', array_keys($needed['any'])) .")
3810 ) ra ON ra.userid = u.id";
3812 } else {
3813 $unions = array();
3814 $everybody = false;
3815 foreach ($needed as $cap=>$unused) {
3816 if (empty($prohibited[$cap])) {
3817 if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
3818 $everybody = true;
3819 break;
3820 } else {
3821 $unions[] = "SELECT userid
3822 FROM {role_assignments}
3823 WHERE contextid IN ($ctxids)
3824 AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
3826 } else {
3827 if (!empty($prohibited[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
3828 // nobody can have this cap because it is prevented in default roles
3829 continue;
3831 } else if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
3832 // everybody except the prohibitted - hiding does not matter
3833 $unions[] = "SELECT id AS userid
3834 FROM {user}
3835 WHERE id NOT IN (SELECT userid
3836 FROM {role_assignments}
3837 WHERE contextid IN ($ctxids)
3838 AND roleid IN (".implode(',', array_keys($prohibited[$cap])) ."))";
3840 } else {
3841 $unions[] = "SELECT userid
3842 FROM {role_assignments}
3843 WHERE contextid IN ($ctxids)
3844 AND roleid IN (".implode(',', array_keys($needed[$cap])) .")
3845 AND roleid NOT IN (".implode(',', array_keys($prohibited[$cap])) .")";
3849 if (!$everybody) {
3850 if ($unions) {
3851 $joins[] = "JOIN (SELECT DISTINCT userid FROM ( ".implode(' UNION ', $unions)." ) us) ra ON ra.userid = u.id";
3852 } else {
3853 // only prohibits found - nobody can be matched
3854 $wherecond[] = "1 = 2";
3859 // Collect WHERE conditions and needed joins
3860 $where = implode(' AND ', $wherecond);
3861 if ($where !== '') {
3862 $where = 'WHERE ' . $where;
3864 $joins = implode("\n", $joins);
3866 // Ok, let's get the users!
3867 $sql = "SELECT $fields
3868 FROM {user} u
3869 $joins
3870 $where
3871 ORDER BY $sort";
3873 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3877 * Re-sort a users array based on a sorting policy
3879 * Will re-sort a $users results array (from get_users_by_capability(), usually)
3880 * based on a sorting policy. This is to support the odd practice of
3881 * sorting teachers by 'authority', where authority was "lowest id of the role
3882 * assignment".
3884 * Will execute 1 database query. Only suitable for small numbers of users, as it
3885 * uses an u.id IN() clause.
3887 * Notes about the sorting criteria.
3889 * As a default, we cannot rely on role.sortorder because then
3890 * admins/coursecreators will always win. That is why the sane
3891 * rule "is locality matters most", with sortorder as 2nd
3892 * consideration.
3894 * If you want role.sortorder, use the 'sortorder' policy, and
3895 * name explicitly what roles you want to cover. It's probably
3896 * a good idea to see what roles have the capabilities you want
3897 * (array_diff() them against roiles that have 'can-do-anything'
3898 * to weed out admin-ish roles. Or fetch a list of roles from
3899 * variables like $CFG->coursecontact .
3901 * @param array $users Users array, keyed on userid
3902 * @param context $context
3903 * @param array $roles ids of the roles to include, optional
3904 * @param string $sortpolicy defaults to locality, more about
3905 * @return array sorted copy of the array
3907 function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') {
3908 global $DB;
3910 $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
3911 $contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
3912 if (empty($roles)) {
3913 $roleswhere = '';
3914 } else {
3915 $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
3918 $sql = "SELECT ra.userid
3919 FROM {role_assignments} ra
3920 JOIN {role} r
3921 ON ra.roleid=r.id
3922 JOIN {context} ctx
3923 ON ra.contextid=ctx.id
3924 WHERE $userswhere
3925 $contextwhere
3926 $roleswhere";
3928 // Default 'locality' policy -- read PHPDoc notes
3929 // about sort policies...
3930 $orderby = 'ORDER BY '
3931 .'ctx.depth DESC, ' /* locality wins */
3932 .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3933 .'ra.id'; /* role assignment order tie-breaker */
3934 if ($sortpolicy === 'sortorder') {
3935 $orderby = 'ORDER BY '
3936 .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3937 .'ra.id'; /* role assignment order tie-breaker */
3940 $sortedids = $DB->get_fieldset_sql($sql . $orderby);
3941 $sortedusers = array();
3942 $seen = array();
3944 foreach ($sortedids as $id) {
3945 // Avoid duplicates
3946 if (isset($seen[$id])) {
3947 continue;
3949 $seen[$id] = true;
3951 // assign
3952 $sortedusers[$id] = $users[$id];
3954 return $sortedusers;
3958 * Gets all the users assigned this role in this context or higher
3960 * @param int $roleid (can also be an array of ints!)
3961 * @param context $context
3962 * @param bool $parent if true, get list of users assigned in higher context too
3963 * @param string $fields fields from user (u.) , role assignment (ra) or role (r.)
3964 * @param string $sort sort from user (u.) , role assignment (ra.) or role (r.).
3965 * null => use default sort from users_order_by_sql.
3966 * @param bool $all true means all, false means limit to enrolled users
3967 * @param string $group defaults to ''
3968 * @param mixed $limitfrom defaults to ''
3969 * @param mixed $limitnum defaults to ''
3970 * @param string $extrawheretest defaults to ''
3971 * @param array $whereorsortparams any paramter values used by $sort or $extrawheretest.
3972 * @return array
3974 function get_role_users($roleid, context $context, $parent = false, $fields = '',
3975 $sort = null, $all = true, $group = '',
3976 $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereorsortparams = array()) {
3977 global $DB;
3979 if (empty($fields)) {
3980 $allnames = get_all_user_name_fields(true, 'u');
3981 $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
3982 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
3983 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
3984 'u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name AS rolename, r.sortorder, '.
3985 'r.shortname AS roleshortname, rn.name AS rolecoursealias';
3988 $parentcontexts = '';
3989 if ($parent) {
3990 $parentcontexts = substr($context->path, 1); // kill leading slash
3991 $parentcontexts = str_replace('/', ',', $parentcontexts);
3992 if ($parentcontexts !== '') {
3993 $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
3997 if ($roleid) {
3998 list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'r');
3999 $roleselect = "AND ra.roleid $rids";
4000 } else {
4001 $params = array();
4002 $roleselect = '';
4005 if ($coursecontext = $context->get_course_context(false)) {
4006 $params['coursecontext'] = $coursecontext->id;
4007 } else {
4008 $params['coursecontext'] = 0;
4011 if ($group) {
4012 $groupjoin = "JOIN {groups_members} gm ON gm.userid = u.id";
4013 $groupselect = " AND gm.groupid = :groupid ";
4014 $params['groupid'] = $group;
4015 } else {
4016 $groupjoin = '';
4017 $groupselect = '';
4020 $params['contextid'] = $context->id;
4022 if ($extrawheretest) {
4023 $extrawheretest = ' AND ' . $extrawheretest;
4026 if ($whereorsortparams) {
4027 $params = array_merge($params, $whereorsortparams);
4030 if (!$sort) {
4031 list($sort, $sortparams) = users_order_by_sql('u');
4032 $params = array_merge($params, $sortparams);
4035 if ($all === null) {
4036 // Previously null was used to indicate that parameter was not used.
4037 $all = true;
4039 if (!$all and $coursecontext) {
4040 // Do not use get_enrolled_sql() here for performance reasons.
4041 $ejoin = "JOIN {user_enrolments} ue ON ue.userid = u.id
4042 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :ecourseid)";
4043 $params['ecourseid'] = $coursecontext->instanceid;
4044 } else {
4045 $ejoin = "";
4048 $sql = "SELECT DISTINCT $fields, ra.roleid
4049 FROM {role_assignments} ra
4050 JOIN {user} u ON u.id = ra.userid
4051 JOIN {role} r ON ra.roleid = r.id
4052 $ejoin
4053 LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
4054 $groupjoin
4055 WHERE (ra.contextid = :contextid $parentcontexts)
4056 $roleselect
4057 $groupselect
4058 $extrawheretest
4059 ORDER BY $sort"; // join now so that we can just use fullname() later
4061 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
4065 * Counts all the users assigned this role in this context or higher
4067 * @param int|array $roleid either int or an array of ints
4068 * @param context $context
4069 * @param bool $parent if true, get list of users assigned in higher context too
4070 * @return int Returns the result count
4072 function count_role_users($roleid, context $context, $parent = false) {
4073 global $DB;
4075 if ($parent) {
4076 if ($contexts = $context->get_parent_context_ids()) {
4077 $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
4078 } else {
4079 $parentcontexts = '';
4081 } else {
4082 $parentcontexts = '';
4085 if ($roleid) {
4086 list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
4087 $roleselect = "AND r.roleid $rids";
4088 } else {
4089 $params = array();
4090 $roleselect = '';
4093 array_unshift($params, $context->id);
4095 $sql = "SELECT COUNT(u.id)
4096 FROM {role_assignments} r
4097 JOIN {user} u ON u.id = r.userid
4098 WHERE (r.contextid = ? $parentcontexts)
4099 $roleselect
4100 AND u.deleted = 0";
4102 return $DB->count_records_sql($sql, $params);
4106 * This function gets the list of courses that this user has a particular capability in.
4107 * It is still not very efficient.
4109 * @param string $capability Capability in question
4110 * @param int $userid User ID or null for current user
4111 * @param bool $doanything True if 'doanything' is permitted (default)
4112 * @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
4113 * otherwise use a comma-separated list of the fields you require, not including id
4114 * @param string $orderby If set, use a comma-separated list of fields from course
4115 * table with sql modifiers (DESC) if needed
4116 * @return array Array of courses, may have zero entries. Or false if query failed.
4118 function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', $orderby = '') {
4119 global $DB;
4121 // Convert fields list and ordering
4122 $fieldlist = '';
4123 if ($fieldsexceptid) {
4124 $fields = explode(',', $fieldsexceptid);
4125 foreach($fields as $field) {
4126 $fieldlist .= ',c.'.$field;
4129 if ($orderby) {
4130 $fields = explode(',', $orderby);
4131 $orderby = '';
4132 foreach($fields as $field) {
4133 if ($orderby) {
4134 $orderby .= ',';
4136 $orderby .= 'c.'.$field;
4138 $orderby = 'ORDER BY '.$orderby;
4141 // Obtain a list of everything relevant about all courses including context.
4142 // Note the result can be used directly as a context (we are going to), the course
4143 // fields are just appended.
4145 $contextpreload = context_helper::get_preload_record_columns_sql('x');
4147 $courses = array();
4148 $rs = $DB->get_recordset_sql("SELECT c.id $fieldlist, $contextpreload
4149 FROM {course} c
4150 JOIN {context} x ON (c.id=x.instanceid AND x.contextlevel=".CONTEXT_COURSE.")
4151 $orderby");
4152 // Check capability for each course in turn
4153 foreach ($rs as $course) {
4154 context_helper::preload_from_record($course);
4155 $context = context_course::instance($course->id);
4156 if (has_capability($capability, $context, $userid, $doanything)) {
4157 // We've got the capability. Make the record look like a course record
4158 // and store it
4159 $courses[] = $course;
4162 $rs->close();
4163 return empty($courses) ? false : $courses;
4167 * This function finds the roles assigned directly to this context only
4168 * i.e. no roles in parent contexts
4170 * @param context $context
4171 * @return array
4173 function get_roles_on_exact_context(context $context) {
4174 global $DB;
4176 return $DB->get_records_sql("SELECT r.*
4177 FROM {role_assignments} ra, {role} r
4178 WHERE ra.roleid = r.id AND ra.contextid = ?",
4179 array($context->id));
4183 * Switches the current user to another role for the current session and only
4184 * in the given context.
4186 * The caller *must* check
4187 * - that this op is allowed
4188 * - that the requested role can be switched to in this context (use get_switchable_roles)
4189 * - that the requested role is NOT $CFG->defaultuserroleid
4191 * To "unswitch" pass 0 as the roleid.
4193 * This function *will* modify $USER->access - beware
4195 * @param integer $roleid the role to switch to.
4196 * @param context $context the context in which to perform the switch.
4197 * @return bool success or failure.
4199 function role_switch($roleid, context $context) {
4200 global $USER;
4203 // Plan of action
4205 // - Add the ghost RA to $USER->access
4206 // as $USER->access['rsw'][$path] = $roleid
4208 // - Make sure $USER->access['rdef'] has the roledefs
4209 // it needs to honour the switcherole
4211 // Roledefs will get loaded "deep" here - down to the last child
4212 // context. Note that
4214 // - When visiting subcontexts, our selective accessdata loading
4215 // will still work fine - though those ra/rdefs will be ignored
4216 // appropriately while the switch is in place
4218 // - If a switcherole happens at a category with tons of courses
4219 // (that have many overrides for switched-to role), the session
4220 // will get... quite large. Sometimes you just can't win.
4222 // To un-switch just unset($USER->access['rsw'][$path])
4224 // Note: it is not possible to switch to roles that do not have course:view
4226 if (!isset($USER->access)) {
4227 load_all_capabilities();
4231 // Add the switch RA
4232 if ($roleid == 0) {
4233 unset($USER->access['rsw'][$context->path]);
4234 return true;
4237 $USER->access['rsw'][$context->path] = $roleid;
4239 // Load roledefs
4240 load_role_access_by_context($roleid, $context, $USER->access);
4242 return true;
4246 * Checks if the user has switched roles within the given course.
4248 * Note: You can only switch roles within the course, hence it takes a course id
4249 * rather than a context. On that note Petr volunteered to implement this across
4250 * all other contexts, all requests for this should be forwarded to him ;)
4252 * @param int $courseid The id of the course to check
4253 * @return bool True if the user has switched roles within the course.
4255 function is_role_switched($courseid) {
4256 global $USER;
4257 $context = context_course::instance($courseid, MUST_EXIST);
4258 return (!empty($USER->access['rsw'][$context->path]));
4262 * Get any role that has an override on exact context
4264 * @param context $context The context to check
4265 * @return array An array of roles
4267 function get_roles_with_override_on_context(context $context) {
4268 global $DB;
4270 return $DB->get_records_sql("SELECT r.*
4271 FROM {role_capabilities} rc, {role} r
4272 WHERE rc.roleid = r.id AND rc.contextid = ?",
4273 array($context->id));
4277 * Get all capabilities for this role on this context (overrides)
4279 * @param stdClass $role
4280 * @param context $context
4281 * @return array
4283 function get_capabilities_from_role_on_context($role, context $context) {
4284 global $DB;
4286 return $DB->get_records_sql("SELECT *
4287 FROM {role_capabilities}
4288 WHERE contextid = ? AND roleid = ?",
4289 array($context->id, $role->id));
4293 * Find out which roles has assignment on this context
4295 * @param context $context
4296 * @return array
4299 function get_roles_with_assignment_on_context(context $context) {
4300 global $DB;
4302 return $DB->get_records_sql("SELECT r.*
4303 FROM {role_assignments} ra, {role} r
4304 WHERE ra.roleid = r.id AND ra.contextid = ?",
4305 array($context->id));
4309 * Find all user assignment of users for this role, on this context
4311 * @param stdClass $role
4312 * @param context $context
4313 * @return array
4315 function get_users_from_role_on_context($role, context $context) {
4316 global $DB;
4318 return $DB->get_records_sql("SELECT *
4319 FROM {role_assignments}
4320 WHERE contextid = ? AND roleid = ?",
4321 array($context->id, $role->id));
4325 * Simple function returning a boolean true if user has roles
4326 * in context or parent contexts, otherwise false.
4328 * @param int $userid
4329 * @param int $roleid
4330 * @param int $contextid empty means any context
4331 * @return bool
4333 function user_has_role_assignment($userid, $roleid, $contextid = 0) {
4334 global $DB;
4336 if ($contextid) {
4337 if (!$context = context::instance_by_id($contextid, IGNORE_MISSING)) {
4338 return false;
4340 $parents = $context->get_parent_context_ids(true);
4341 list($contexts, $params) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED, 'r');
4342 $params['userid'] = $userid;
4343 $params['roleid'] = $roleid;
4345 $sql = "SELECT COUNT(ra.id)
4346 FROM {role_assignments} ra
4347 WHERE ra.userid = :userid AND ra.roleid = :roleid AND ra.contextid $contexts";
4349 $count = $DB->get_field_sql($sql, $params);
4350 return ($count > 0);
4352 } else {
4353 return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid));
4358 * Get localised role name or alias if exists and format the text.
4360 * @param stdClass $role role object
4361 * - optional 'coursealias' property should be included for performance reasons if course context used
4362 * - description property is not required here
4363 * @param context|bool $context empty means system context
4364 * @param int $rolenamedisplay type of role name
4365 * @return string localised role name or course role name alias
4367 function role_get_name(stdClass $role, $context = null, $rolenamedisplay = ROLENAME_ALIAS) {
4368 global $DB;
4370 if ($rolenamedisplay == ROLENAME_SHORT) {
4371 return $role->shortname;
4374 if (!$context or !$coursecontext = $context->get_course_context(false)) {
4375 $coursecontext = null;
4378 if ($coursecontext and !property_exists($role, 'coursealias') and ($rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH or $rolenamedisplay == ROLENAME_ALIAS_RAW)) {
4379 $role = clone($role); // Do not modify parameters.
4380 if ($r = $DB->get_record('role_names', array('roleid'=>$role->id, 'contextid'=>$coursecontext->id))) {
4381 $role->coursealias = $r->name;
4382 } else {
4383 $role->coursealias = null;
4387 if ($rolenamedisplay == ROLENAME_ALIAS_RAW) {
4388 if ($coursecontext) {
4389 return $role->coursealias;
4390 } else {
4391 return null;
4395 if (trim($role->name) !== '') {
4396 // For filtering always use context where was the thing defined - system for roles here.
4397 $original = format_string($role->name, true, array('context'=>context_system::instance()));
4399 } else {
4400 // Empty role->name means we want to see localised role name based on shortname,
4401 // only default roles are supposed to be localised.
4402 switch ($role->shortname) {
4403 case 'manager': $original = get_string('manager', 'role'); break;
4404 case 'coursecreator': $original = get_string('coursecreators'); break;
4405 case 'editingteacher': $original = get_string('defaultcourseteacher'); break;
4406 case 'teacher': $original = get_string('noneditingteacher'); break;
4407 case 'student': $original = get_string('defaultcoursestudent'); break;
4408 case 'guest': $original = get_string('guest'); break;
4409 case 'user': $original = get_string('authenticateduser'); break;
4410 case 'frontpage': $original = get_string('frontpageuser', 'role'); break;
4411 // We should not get here, the role UI should require the name for custom roles!
4412 default: $original = $role->shortname; break;
4416 if ($rolenamedisplay == ROLENAME_ORIGINAL) {
4417 return $original;
4420 if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
4421 return "$original ($role->shortname)";
4424 if ($rolenamedisplay == ROLENAME_ALIAS) {
4425 if ($coursecontext and trim($role->coursealias) !== '') {
4426 return format_string($role->coursealias, true, array('context'=>$coursecontext));
4427 } else {
4428 return $original;
4432 if ($rolenamedisplay == ROLENAME_BOTH) {
4433 if ($coursecontext and trim($role->coursealias) !== '') {
4434 return format_string($role->coursealias, true, array('context'=>$coursecontext)) . " ($original)";
4435 } else {
4436 return $original;
4440 throw new coding_exception('Invalid $rolenamedisplay parameter specified in role_get_name()');
4444 * Returns localised role description if available.
4445 * If the name is empty it tries to find the default role name using
4446 * hardcoded list of default role names or other methods in the future.
4448 * @param stdClass $role
4449 * @return string localised role name
4451 function role_get_description(stdClass $role) {
4452 if (!html_is_blank($role->description)) {
4453 return format_text($role->description, FORMAT_HTML, array('context'=>context_system::instance()));
4456 switch ($role->shortname) {
4457 case 'manager': return get_string('managerdescription', 'role');
4458 case 'coursecreator': return get_string('coursecreatorsdescription');
4459 case 'editingteacher': return get_string('defaultcourseteacherdescription');
4460 case 'teacher': return get_string('noneditingteacherdescription');
4461 case 'student': return get_string('defaultcoursestudentdescription');
4462 case 'guest': return get_string('guestdescription');
4463 case 'user': return get_string('authenticateduserdescription');
4464 case 'frontpage': return get_string('frontpageuserdescription', 'role');
4465 default: return '';
4470 * Get all the localised role names for a context.
4472 * In new installs default roles have empty names, this function
4473 * add localised role names using current language pack.
4475 * @param context $context the context, null means system context
4476 * @param array of role objects with a ->localname field containing the context-specific role name.
4477 * @param int $rolenamedisplay
4478 * @param bool $returnmenu true means id=>localname, false means id=>rolerecord
4479 * @return array Array of context-specific role names, or role objects with a ->localname field added.
4481 function role_get_names(context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
4482 return role_fix_names(get_all_roles($context), $context, $rolenamedisplay, $returnmenu);
4486 * Prepare list of roles for display, apply aliases and localise default role names.
4488 * @param array $roleoptions array roleid => roleobject (with optional coursealias), strings are accepted for backwards compatibility only
4489 * @param context $context the context, null means system context
4490 * @param int $rolenamedisplay
4491 * @param bool $returnmenu null means keep the same format as $roleoptions, true means id=>localname, false means id=>rolerecord
4492 * @return array Array of context-specific role names, or role objects with a ->localname field added.
4494 function role_fix_names($roleoptions, context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
4495 global $DB;
4497 if (empty($roleoptions)) {
4498 return array();
4501 if (!$context or !$coursecontext = $context->get_course_context(false)) {
4502 $coursecontext = null;
4505 // We usually need all role columns...
4506 $first = reset($roleoptions);
4507 if ($returnmenu === null) {
4508 $returnmenu = !is_object($first);
4511 if (!is_object($first) or !property_exists($first, 'shortname')) {
4512 $allroles = get_all_roles($context);
4513 foreach ($roleoptions as $rid => $unused) {
4514 $roleoptions[$rid] = $allroles[$rid];
4518 // Inject coursealias if necessary.
4519 if ($coursecontext and ($rolenamedisplay == ROLENAME_ALIAS_RAW or $rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH)) {
4520 $first = reset($roleoptions);
4521 if (!property_exists($first, 'coursealias')) {
4522 $aliasnames = $DB->get_records('role_names', array('contextid'=>$coursecontext->id));
4523 foreach ($aliasnames as $alias) {
4524 if (isset($roleoptions[$alias->roleid])) {
4525 $roleoptions[$alias->roleid]->coursealias = $alias->name;
4531 // Add localname property.
4532 foreach ($roleoptions as $rid => $role) {
4533 $roleoptions[$rid]->localname = role_get_name($role, $coursecontext, $rolenamedisplay);
4536 if (!$returnmenu) {
4537 return $roleoptions;
4540 $menu = array();
4541 foreach ($roleoptions as $rid => $role) {
4542 $menu[$rid] = $role->localname;
4545 return $menu;
4549 * Aids in detecting if a new line is required when reading a new capability
4551 * This function helps admin/roles/manage.php etc to detect if a new line should be printed
4552 * when we read in a new capability.
4553 * Most of the time, if the 2 components are different we should print a new line, (e.g. course system->rss client)
4554 * but when we are in grade, all reports/import/export capabilities should be together
4556 * @param string $cap component string a
4557 * @param string $comp component string b
4558 * @param int $contextlevel
4559 * @return bool whether 2 component are in different "sections"
4561 function component_level_changed($cap, $comp, $contextlevel) {
4563 if (strstr($cap->component, '/') && strstr($comp, '/')) {
4564 $compsa = explode('/', $cap->component);
4565 $compsb = explode('/', $comp);
4567 // list of system reports
4568 if (($compsa[0] == 'report') && ($compsb[0] == 'report')) {
4569 return false;
4572 // we are in gradebook, still
4573 if (($compsa[0] == 'gradeexport' || $compsa[0] == 'gradeimport' || $compsa[0] == 'gradereport') &&
4574 ($compsb[0] == 'gradeexport' || $compsb[0] == 'gradeimport' || $compsb[0] == 'gradereport')) {
4575 return false;
4578 if (($compsa[0] == 'coursereport') && ($compsb[0] == 'coursereport')) {
4579 return false;
4583 return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
4587 * Fix the roles.sortorder field in the database, so it contains sequential integers,
4588 * and return an array of roleids in order.
4590 * @param array $allroles array of roles, as returned by get_all_roles();
4591 * @return array $role->sortorder =-> $role->id with the keys in ascending order.
4593 function fix_role_sortorder($allroles) {
4594 global $DB;
4596 $rolesort = array();
4597 $i = 0;
4598 foreach ($allroles as $role) {
4599 $rolesort[$i] = $role->id;
4600 if ($role->sortorder != $i) {
4601 $r = new stdClass();
4602 $r->id = $role->id;
4603 $r->sortorder = $i;
4604 $DB->update_record('role', $r);
4605 $allroles[$role->id]->sortorder = $i;
4607 $i++;
4609 return $rolesort;
4613 * Switch the sort order of two roles (used in admin/roles/manage.php).
4615 * @param stdClass $first The first role. Actually, only ->sortorder is used.
4616 * @param stdClass $second The second role. Actually, only ->sortorder is used.
4617 * @return boolean success or failure
4619 function switch_roles($first, $second) {
4620 global $DB;
4621 $temp = $DB->get_field('role', 'MAX(sortorder) + 1', array());
4622 $result = $DB->set_field('role', 'sortorder', $temp, array('sortorder' => $first->sortorder));
4623 $result = $result && $DB->set_field('role', 'sortorder', $first->sortorder, array('sortorder' => $second->sortorder));
4624 $result = $result && $DB->set_field('role', 'sortorder', $second->sortorder, array('sortorder' => $temp));
4625 return $result;
4629 * Duplicates all the base definitions of a role
4631 * @param stdClass $sourcerole role to copy from
4632 * @param int $targetrole id of role to copy to
4634 function role_cap_duplicate($sourcerole, $targetrole) {
4635 global $DB;
4637 $systemcontext = context_system::instance();
4638 $caps = $DB->get_records_sql("SELECT *
4639 FROM {role_capabilities}
4640 WHERE roleid = ? AND contextid = ?",
4641 array($sourcerole->id, $systemcontext->id));
4642 // adding capabilities
4643 foreach ($caps as $cap) {
4644 unset($cap->id);
4645 $cap->roleid = $targetrole;
4646 $DB->insert_record('role_capabilities', $cap);
4651 * Returns two lists, this can be used to find out if user has capability.
4652 * Having any needed role and no forbidden role in this context means
4653 * user has this capability in this context.
4654 * Use get_role_names_with_cap_in_context() if you need role names to display in the UI
4656 * @param stdClass $context
4657 * @param string $capability
4658 * @return array($neededroles, $forbiddenroles)
4660 function get_roles_with_cap_in_context($context, $capability) {
4661 global $DB;
4663 $ctxids = trim($context->path, '/'); // kill leading slash
4664 $ctxids = str_replace('/', ',', $ctxids);
4666 $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
4667 FROM {role_capabilities} rc
4668 JOIN {context} ctx ON ctx.id = rc.contextid
4669 WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
4670 ORDER BY rc.roleid ASC, ctx.depth DESC";
4671 $params = array('cap'=>$capability);
4673 if (!$capdefs = $DB->get_records_sql($sql, $params)) {
4674 // no cap definitions --> no capability
4675 return array(array(), array());
4678 $forbidden = array();
4679 $needed = array();
4680 foreach($capdefs as $def) {
4681 if (isset($forbidden[$def->roleid])) {
4682 continue;
4684 if ($def->permission == CAP_PROHIBIT) {
4685 $forbidden[$def->roleid] = $def->roleid;
4686 unset($needed[$def->roleid]);
4687 continue;
4689 if (!isset($needed[$def->roleid])) {
4690 if ($def->permission == CAP_ALLOW) {
4691 $needed[$def->roleid] = true;
4692 } else if ($def->permission == CAP_PREVENT) {
4693 $needed[$def->roleid] = false;
4697 unset($capdefs);
4699 // remove all those roles not allowing
4700 foreach($needed as $key=>$value) {
4701 if (!$value) {
4702 unset($needed[$key]);
4703 } else {
4704 $needed[$key] = $key;
4708 return array($needed, $forbidden);
4712 * Returns an array of role IDs that have ALL of the the supplied capabilities
4713 * Uses get_roles_with_cap_in_context(). Returns $allowed minus $forbidden
4715 * @param stdClass $context
4716 * @param array $capabilities An array of capabilities
4717 * @return array of roles with all of the required capabilities
4719 function get_roles_with_caps_in_context($context, $capabilities) {
4720 $neededarr = array();
4721 $forbiddenarr = array();
4722 foreach($capabilities as $caprequired) {
4723 list($neededarr[], $forbiddenarr[]) = get_roles_with_cap_in_context($context, $caprequired);
4726 $rolesthatcanrate = array();
4727 if (!empty($neededarr)) {
4728 foreach ($neededarr as $needed) {
4729 if (empty($rolesthatcanrate)) {
4730 $rolesthatcanrate = $needed;
4731 } else {
4732 //only want roles that have all caps
4733 $rolesthatcanrate = array_intersect_key($rolesthatcanrate,$needed);
4737 if (!empty($forbiddenarr) && !empty($rolesthatcanrate)) {
4738 foreach ($forbiddenarr as $forbidden) {
4739 //remove any roles that are forbidden any of the caps
4740 $rolesthatcanrate = array_diff($rolesthatcanrate, $forbidden);
4743 return $rolesthatcanrate;
4747 * Returns an array of role names that have ALL of the the supplied capabilities
4748 * Uses get_roles_with_caps_in_context(). Returns $allowed minus $forbidden
4750 * @param stdClass $context
4751 * @param array $capabilities An array of capabilities
4752 * @return array of roles with all of the required capabilities
4754 function get_role_names_with_caps_in_context($context, $capabilities) {
4755 global $DB;
4757 $rolesthatcanrate = get_roles_with_caps_in_context($context, $capabilities);
4758 $allroles = $DB->get_records('role', null, 'sortorder DESC');
4760 $roles = array();
4761 foreach ($rolesthatcanrate as $r) {
4762 $roles[$r] = $allroles[$r];
4765 return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
4769 * This function verifies the prohibit comes from this context
4770 * and there are no more prohibits in parent contexts.
4772 * @param int $roleid
4773 * @param context $context
4774 * @param string $capability name
4775 * @return bool
4777 function prohibit_is_removable($roleid, context $context, $capability) {
4778 global $DB;
4780 $ctxids = trim($context->path, '/'); // kill leading slash
4781 $ctxids = str_replace('/', ',', $ctxids);
4783 $params = array('roleid'=>$roleid, 'cap'=>$capability, 'prohibit'=>CAP_PROHIBIT);
4785 $sql = "SELECT ctx.id
4786 FROM {role_capabilities} rc
4787 JOIN {context} ctx ON ctx.id = rc.contextid
4788 WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
4789 ORDER BY ctx.depth DESC";
4791 if (!$prohibits = $DB->get_records_sql($sql, $params)) {
4792 // no prohibits == nothing to remove
4793 return true;
4796 if (count($prohibits) > 1) {
4797 // more prohibits can not be removed
4798 return false;
4801 return !empty($prohibits[$context->id]);
4805 * More user friendly role permission changing,
4806 * it should produce as few overrides as possible.
4808 * @param int $roleid
4809 * @param stdClass $context
4810 * @param string $capname capability name
4811 * @param int $permission
4812 * @return void
4814 function role_change_permission($roleid, $context, $capname, $permission) {
4815 global $DB;
4817 if ($permission == CAP_INHERIT) {
4818 unassign_capability($capname, $roleid, $context->id);
4819 $context->mark_dirty();
4820 return;
4823 $ctxids = trim($context->path, '/'); // kill leading slash
4824 $ctxids = str_replace('/', ',', $ctxids);
4826 $params = array('roleid'=>$roleid, 'cap'=>$capname);
4828 $sql = "SELECT ctx.id, rc.permission, ctx.depth
4829 FROM {role_capabilities} rc
4830 JOIN {context} ctx ON ctx.id = rc.contextid
4831 WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
4832 ORDER BY ctx.depth DESC";
4834 if ($existing = $DB->get_records_sql($sql, $params)) {
4835 foreach($existing as $e) {
4836 if ($e->permission == CAP_PROHIBIT) {
4837 // prohibit can not be overridden, no point in changing anything
4838 return;
4841 $lowest = array_shift($existing);
4842 if ($lowest->permission == $permission) {
4843 // permission already set in this context or parent - nothing to do
4844 return;
4846 if ($existing) {
4847 $parent = array_shift($existing);
4848 if ($parent->permission == $permission) {
4849 // permission already set in parent context or parent - just unset in this context
4850 // we do this because we want as few overrides as possible for performance reasons
4851 unassign_capability($capname, $roleid, $context->id);
4852 $context->mark_dirty();
4853 return;
4857 } else {
4858 if ($permission == CAP_PREVENT) {
4859 // nothing means role does not have permission
4860 return;
4864 // assign the needed capability
4865 assign_capability($capname, $permission, $roleid, $context->id, true);
4867 // force cap reloading
4868 $context->mark_dirty();
4873 * Basic moodle context abstraction class.
4875 * Google confirms that no other important framework is using "context" class,
4876 * we could use something else like mcontext or moodle_context, but we need to type
4877 * this very often which would be annoying and it would take too much space...
4879 * This class is derived from stdClass for backwards compatibility with
4880 * odl $context record that was returned from DML $DB->get_record()
4882 * @package core_access
4883 * @category access
4884 * @copyright Petr Skoda {@link http://skodak.org}
4885 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4886 * @since 2.2
4888 * @property-read int $id context id
4889 * @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc.
4890 * @property-read int $instanceid id of related instance in each context
4891 * @property-read string $path path to context, starts with system context
4892 * @property-read int $depth
4894 abstract class context extends stdClass implements IteratorAggregate {
4897 * The context id
4898 * Can be accessed publicly through $context->id
4899 * @var int
4901 protected $_id;
4904 * The context level
4905 * Can be accessed publicly through $context->contextlevel
4906 * @var int One of CONTEXT_* e.g. CONTEXT_COURSE, CONTEXT_MODULE
4908 protected $_contextlevel;
4911 * Id of the item this context is related to e.g. COURSE_CONTEXT => course.id
4912 * Can be accessed publicly through $context->instanceid
4913 * @var int
4915 protected $_instanceid;
4918 * The path to the context always starting from the system context
4919 * Can be accessed publicly through $context->path
4920 * @var string
4922 protected $_path;
4925 * The depth of the context in relation to parent contexts
4926 * Can be accessed publicly through $context->depth
4927 * @var int
4929 protected $_depth;
4932 * @var array Context caching info
4934 private static $cache_contextsbyid = array();
4937 * @var array Context caching info
4939 private static $cache_contexts = array();
4942 * Context count
4943 * Why do we do count contexts? Because count($array) is horribly slow for large arrays
4944 * @var int
4946 protected static $cache_count = 0;
4949 * @var array Context caching info
4951 protected static $cache_preloaded = array();
4954 * @var context_system The system context once initialised
4956 protected static $systemcontext = null;
4959 * Resets the cache to remove all data.
4960 * @static
4962 protected static function reset_caches() {
4963 self::$cache_contextsbyid = array();
4964 self::$cache_contexts = array();
4965 self::$cache_count = 0;
4966 self::$cache_preloaded = array();
4968 self::$systemcontext = null;
4972 * Adds a context to the cache. If the cache is full, discards a batch of
4973 * older entries.
4975 * @static
4976 * @param context $context New context to add
4977 * @return void
4979 protected static function cache_add(context $context) {
4980 if (isset(self::$cache_contextsbyid[$context->id])) {
4981 // already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow
4982 return;
4985 if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) {
4986 $i = 0;
4987 foreach(self::$cache_contextsbyid as $ctx) {
4988 $i++;
4989 if ($i <= 100) {
4990 // we want to keep the first contexts to be loaded on this page, hopefully they will be needed again later
4991 continue;
4993 if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
4994 // we remove oldest third of the contexts to make room for more contexts
4995 break;
4997 unset(self::$cache_contextsbyid[$ctx->id]);
4998 unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
4999 self::$cache_count--;
5003 self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
5004 self::$cache_contextsbyid[$context->id] = $context;
5005 self::$cache_count++;
5009 * Removes a context from the cache.
5011 * @static
5012 * @param context $context Context object to remove
5013 * @return void
5015 protected static function cache_remove(context $context) {
5016 if (!isset(self::$cache_contextsbyid[$context->id])) {
5017 // not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow
5018 return;
5020 unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
5021 unset(self::$cache_contextsbyid[$context->id]);
5023 self::$cache_count--;
5025 if (self::$cache_count < 0) {
5026 self::$cache_count = 0;
5031 * Gets a context from the cache.
5033 * @static
5034 * @param int $contextlevel Context level
5035 * @param int $instance Instance ID
5036 * @return context|bool Context or false if not in cache
5038 protected static function cache_get($contextlevel, $instance) {
5039 if (isset(self::$cache_contexts[$contextlevel][$instance])) {
5040 return self::$cache_contexts[$contextlevel][$instance];
5042 return false;
5046 * Gets a context from the cache based on its id.
5048 * @static
5049 * @param int $id Context ID
5050 * @return context|bool Context or false if not in cache
5052 protected static function cache_get_by_id($id) {
5053 if (isset(self::$cache_contextsbyid[$id])) {
5054 return self::$cache_contextsbyid[$id];
5056 return false;
5060 * Preloads context information from db record and strips the cached info.
5062 * @static
5063 * @param stdClass $rec
5064 * @return void (modifies $rec)
5066 protected static function preload_from_record(stdClass $rec) {
5067 if (empty($rec->ctxid) or empty($rec->ctxlevel) or empty($rec->ctxinstance) or empty($rec->ctxpath) or empty($rec->ctxdepth)) {
5068 // $rec does not have enough data, passed here repeatedly or context does not exist yet
5069 return;
5072 // note: in PHP5 the objects are passed by reference, no need to return $rec
5073 $record = new stdClass();
5074 $record->id = $rec->ctxid; unset($rec->ctxid);
5075 $record->contextlevel = $rec->ctxlevel; unset($rec->ctxlevel);
5076 $record->instanceid = $rec->ctxinstance; unset($rec->ctxinstance);
5077 $record->path = $rec->ctxpath; unset($rec->ctxpath);
5078 $record->depth = $rec->ctxdepth; unset($rec->ctxdepth);
5080 return context::create_instance_from_record($record);
5084 // ====== magic methods =======
5087 * Magic setter method, we do not want anybody to modify properties from the outside
5088 * @param string $name
5089 * @param mixed $value
5091 public function __set($name, $value) {
5092 debugging('Can not change context instance properties!');
5096 * Magic method getter, redirects to read only values.
5097 * @param string $name
5098 * @return mixed
5100 public function __get($name) {
5101 switch ($name) {
5102 case 'id': return $this->_id;
5103 case 'contextlevel': return $this->_contextlevel;
5104 case 'instanceid': return $this->_instanceid;
5105 case 'path': return $this->_path;
5106 case 'depth': return $this->_depth;
5108 default:
5109 debugging('Invalid context property accessed! '.$name);
5110 return null;
5115 * Full support for isset on our magic read only properties.
5116 * @param string $name
5117 * @return bool
5119 public function __isset($name) {
5120 switch ($name) {
5121 case 'id': return isset($this->_id);
5122 case 'contextlevel': return isset($this->_contextlevel);
5123 case 'instanceid': return isset($this->_instanceid);
5124 case 'path': return isset($this->_path);
5125 case 'depth': return isset($this->_depth);
5127 default: return false;
5133 * ALl properties are read only, sorry.
5134 * @param string $name
5136 public function __unset($name) {
5137 debugging('Can not unset context instance properties!');
5140 // ====== implementing method from interface IteratorAggregate ======
5143 * Create an iterator because magic vars can't be seen by 'foreach'.
5145 * Now we can convert context object to array using convert_to_array(),
5146 * and feed it properly to json_encode().
5148 public function getIterator() {
5149 $ret = array(
5150 'id' => $this->id,
5151 'contextlevel' => $this->contextlevel,
5152 'instanceid' => $this->instanceid,
5153 'path' => $this->path,
5154 'depth' => $this->depth
5156 return new ArrayIterator($ret);
5159 // ====== general context methods ======
5162 * Constructor is protected so that devs are forced to
5163 * use context_xxx::instance() or context::instance_by_id().
5165 * @param stdClass $record
5167 protected function __construct(stdClass $record) {
5168 $this->_id = $record->id;
5169 $this->_contextlevel = (int)$record->contextlevel;
5170 $this->_instanceid = $record->instanceid;
5171 $this->_path = $record->path;
5172 $this->_depth = $record->depth;
5176 * This function is also used to work around 'protected' keyword problems in context_helper.
5177 * @static
5178 * @param stdClass $record
5179 * @return context instance
5181 protected static function create_instance_from_record(stdClass $record) {
5182 $classname = context_helper::get_class_for_level($record->contextlevel);
5184 if ($context = context::cache_get_by_id($record->id)) {
5185 return $context;
5188 $context = new $classname($record);
5189 context::cache_add($context);
5191 return $context;
5195 * Copy prepared new contexts from temp table to context table,
5196 * we do this in db specific way for perf reasons only.
5197 * @static
5199 protected static function merge_context_temp_table() {
5200 global $DB;
5202 /* MDL-11347:
5203 * - mysql does not allow to use FROM in UPDATE statements
5204 * - using two tables after UPDATE works in mysql, but might give unexpected
5205 * results in pg 8 (depends on configuration)
5206 * - using table alias in UPDATE does not work in pg < 8.2
5208 * Different code for each database - mostly for performance reasons
5211 $dbfamily = $DB->get_dbfamily();
5212 if ($dbfamily == 'mysql') {
5213 $updatesql = "UPDATE {context} ct, {context_temp} temp
5214 SET ct.path = temp.path,
5215 ct.depth = temp.depth
5216 WHERE ct.id = temp.id";
5217 } else if ($dbfamily == 'oracle') {
5218 $updatesql = "UPDATE {context} ct
5219 SET (ct.path, ct.depth) =
5220 (SELECT temp.path, temp.depth
5221 FROM {context_temp} temp
5222 WHERE temp.id=ct.id)
5223 WHERE EXISTS (SELECT 'x'
5224 FROM {context_temp} temp
5225 WHERE temp.id = ct.id)";
5226 } else if ($dbfamily == 'postgres' or $dbfamily == 'mssql') {
5227 $updatesql = "UPDATE {context}
5228 SET path = temp.path,
5229 depth = temp.depth
5230 FROM {context_temp} temp
5231 WHERE temp.id={context}.id";
5232 } else {
5233 // sqlite and others
5234 $updatesql = "UPDATE {context}
5235 SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
5236 depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id)
5237 WHERE id IN (SELECT id FROM {context_temp})";
5240 $DB->execute($updatesql);
5244 * Get a context instance as an object, from a given context id.
5246 * @static
5247 * @param int $id context id
5248 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
5249 * MUST_EXIST means throw exception if no record found
5250 * @return context|bool the context object or false if not found
5252 public static function instance_by_id($id, $strictness = MUST_EXIST) {
5253 global $DB;
5255 if (get_called_class() !== 'context' and get_called_class() !== 'context_helper') {
5256 // some devs might confuse context->id and instanceid, better prevent these mistakes completely
5257 throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
5260 if ($id == SYSCONTEXTID) {
5261 return context_system::instance(0, $strictness);
5264 if (is_array($id) or is_object($id) or empty($id)) {
5265 throw new coding_exception('Invalid context id specified context::instance_by_id()');
5268 if ($context = context::cache_get_by_id($id)) {
5269 return $context;
5272 if ($record = $DB->get_record('context', array('id'=>$id), '*', $strictness)) {
5273 return context::create_instance_from_record($record);
5276 return false;
5280 * Update context info after moving context in the tree structure.
5282 * @param context $newparent
5283 * @return void
5285 public function update_moved(context $newparent) {
5286 global $DB;
5288 $frompath = $this->_path;
5289 $newpath = $newparent->path . '/' . $this->_id;
5291 $trans = $DB->start_delegated_transaction();
5293 $this->mark_dirty();
5295 $setdepth = '';
5296 if (($newparent->depth +1) != $this->_depth) {
5297 $diff = $newparent->depth - $this->_depth + 1;
5298 $setdepth = ", depth = depth + $diff";
5300 $sql = "UPDATE {context}
5301 SET path = ?
5302 $setdepth
5303 WHERE id = ?";
5304 $params = array($newpath, $this->_id);
5305 $DB->execute($sql, $params);
5307 $this->_path = $newpath;
5308 $this->_depth = $newparent->depth + 1;
5310 $sql = "UPDATE {context}
5311 SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath)+1))."
5312 $setdepth
5313 WHERE path LIKE ?";
5314 $params = array($newpath, "{$frompath}/%");
5315 $DB->execute($sql, $params);
5317 $this->mark_dirty();
5319 context::reset_caches();
5321 $trans->allow_commit();
5325 * Remove all context path info and optionally rebuild it.
5327 * @param bool $rebuild
5328 * @return void
5330 public function reset_paths($rebuild = true) {
5331 global $DB;
5333 if ($this->_path) {
5334 $this->mark_dirty();
5336 $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
5337 $DB->set_field_select('context', 'path', NULL, "path LIKE '%/$this->_id/%'");
5338 if ($this->_contextlevel != CONTEXT_SYSTEM) {
5339 $DB->set_field('context', 'depth', 0, array('id'=>$this->_id));
5340 $DB->set_field('context', 'path', NULL, array('id'=>$this->_id));
5341 $this->_depth = 0;
5342 $this->_path = null;
5345 if ($rebuild) {
5346 context_helper::build_all_paths(false);
5349 context::reset_caches();
5353 * Delete all data linked to content, do not delete the context record itself
5355 public function delete_content() {
5356 global $CFG, $DB;
5358 blocks_delete_all_for_context($this->_id);
5359 filter_delete_all_for_context($this->_id);
5361 require_once($CFG->dirroot . '/comment/lib.php');
5362 comment::delete_comments(array('contextid'=>$this->_id));
5364 require_once($CFG->dirroot.'/rating/lib.php');
5365 $delopt = new stdclass();
5366 $delopt->contextid = $this->_id;
5367 $rm = new rating_manager();
5368 $rm->delete_ratings($delopt);
5370 // delete all files attached to this context
5371 $fs = get_file_storage();
5372 $fs->delete_area_files($this->_id);
5374 // Delete all repository instances attached to this context.
5375 require_once($CFG->dirroot . '/repository/lib.php');
5376 repository::delete_all_for_context($this->_id);
5378 // delete all advanced grading data attached to this context
5379 require_once($CFG->dirroot.'/grade/grading/lib.php');
5380 grading_manager::delete_all_for_context($this->_id);
5382 // now delete stuff from role related tables, role_unassign_all
5383 // and unenrol should be called earlier to do proper cleanup
5384 $DB->delete_records('role_assignments', array('contextid'=>$this->_id));
5385 $DB->delete_records('role_capabilities', array('contextid'=>$this->_id));
5386 $DB->delete_records('role_names', array('contextid'=>$this->_id));
5390 * Delete the context content and the context record itself
5392 public function delete() {
5393 global $DB;
5395 // double check the context still exists
5396 if (!$DB->record_exists('context', array('id'=>$this->_id))) {
5397 context::cache_remove($this);
5398 return;
5401 $this->delete_content();
5402 $DB->delete_records('context', array('id'=>$this->_id));
5403 // purge static context cache if entry present
5404 context::cache_remove($this);
5406 // do not mark dirty contexts if parents unknown
5407 if (!is_null($this->_path) and $this->_depth > 0) {
5408 $this->mark_dirty();
5412 // ====== context level related methods ======
5415 * Utility method for context creation
5417 * @static
5418 * @param int $contextlevel
5419 * @param int $instanceid
5420 * @param string $parentpath
5421 * @return stdClass context record
5423 protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
5424 global $DB;
5426 $record = new stdClass();
5427 $record->contextlevel = $contextlevel;
5428 $record->instanceid = $instanceid;
5429 $record->depth = 0;
5430 $record->path = null; //not known before insert
5432 $record->id = $DB->insert_record('context', $record);
5434 // now add path if known - it can be added later
5435 if (!is_null($parentpath)) {
5436 $record->path = $parentpath.'/'.$record->id;
5437 $record->depth = substr_count($record->path, '/');
5438 $DB->update_record('context', $record);
5441 return $record;
5445 * Returns human readable context identifier.
5447 * @param boolean $withprefix whether to prefix the name of the context with the
5448 * type of context, e.g. User, Course, Forum, etc.
5449 * @param boolean $short whether to use the short name of the thing. Only applies
5450 * to course contexts
5451 * @return string the human readable context name.
5453 public function get_context_name($withprefix = true, $short = false) {
5454 // must be implemented in all context levels
5455 throw new coding_exception('can not get name of abstract context');
5459 * Returns the most relevant URL for this context.
5461 * @return moodle_url
5463 public abstract function get_url();
5466 * Returns array of relevant context capability records.
5468 * @return array
5470 public abstract function get_capabilities();
5473 * Recursive function which, given a context, find all its children context ids.
5475 * For course category contexts it will return immediate children and all subcategory contexts.
5476 * It will NOT recurse into courses or subcategories categories.
5477 * If you want to do that, call it on the returned courses/categories.
5479 * When called for a course context, it will return the modules and blocks
5480 * displayed in the course page and blocks displayed on the module pages.
5482 * If called on a user/course/module context it _will_ populate the cache with the appropriate
5483 * contexts ;-)
5485 * @return array Array of child records
5487 public function get_child_contexts() {
5488 global $DB;
5490 $sql = "SELECT ctx.*
5491 FROM {context} ctx
5492 WHERE ctx.path LIKE ?";
5493 $params = array($this->_path.'/%');
5494 $records = $DB->get_records_sql($sql, $params);
5496 $result = array();
5497 foreach ($records as $record) {
5498 $result[$record->id] = context::create_instance_from_record($record);
5501 return $result;
5505 * Returns parent contexts of this context in reversed order, i.e. parent first,
5506 * then grand parent, etc.
5508 * @param bool $includeself tre means include self too
5509 * @return array of context instances
5511 public function get_parent_contexts($includeself = false) {
5512 if (!$contextids = $this->get_parent_context_ids($includeself)) {
5513 return array();
5516 $result = array();
5517 foreach ($contextids as $contextid) {
5518 $parent = context::instance_by_id($contextid, MUST_EXIST);
5519 $result[$parent->id] = $parent;
5522 return $result;
5526 * Returns parent contexts of this context in reversed order, i.e. parent first,
5527 * then grand parent, etc.
5529 * @param bool $includeself tre means include self too
5530 * @return array of context ids
5532 public function get_parent_context_ids($includeself = false) {
5533 if (empty($this->_path)) {
5534 return array();
5537 $parentcontexts = trim($this->_path, '/'); // kill leading slash
5538 $parentcontexts = explode('/', $parentcontexts);
5539 if (!$includeself) {
5540 array_pop($parentcontexts); // and remove its own id
5543 return array_reverse($parentcontexts);
5547 * Returns parent context
5549 * @return context
5551 public function get_parent_context() {
5552 if (empty($this->_path) or $this->_id == SYSCONTEXTID) {
5553 return false;
5556 $parentcontexts = trim($this->_path, '/'); // kill leading slash
5557 $parentcontexts = explode('/', $parentcontexts);
5558 array_pop($parentcontexts); // self
5559 $contextid = array_pop($parentcontexts); // immediate parent
5561 return context::instance_by_id($contextid, MUST_EXIST);
5565 * Is this context part of any course? If yes return course context.
5567 * @param bool $strict true means throw exception if not found, false means return false if not found
5568 * @return course_context context of the enclosing course, null if not found or exception
5570 public function get_course_context($strict = true) {
5571 if ($strict) {
5572 throw new coding_exception('Context does not belong to any course.');
5573 } else {
5574 return false;
5579 * Returns sql necessary for purging of stale context instances.
5581 * @static
5582 * @return string cleanup SQL
5584 protected static function get_cleanup_sql() {
5585 throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
5589 * Rebuild context paths and depths at context level.
5591 * @static
5592 * @param bool $force
5593 * @return void
5595 protected static function build_paths($force) {
5596 throw new coding_exception('build_paths() method must be implemented in all context levels');
5600 * Create missing context instances at given level
5602 * @static
5603 * @return void
5605 protected static function create_level_instances() {
5606 throw new coding_exception('create_level_instances() method must be implemented in all context levels');
5610 * Reset all cached permissions and definitions if the necessary.
5611 * @return void
5613 public function reload_if_dirty() {
5614 global $ACCESSLIB_PRIVATE, $USER;
5616 // Load dirty contexts list if needed
5617 if (CLI_SCRIPT) {
5618 if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
5619 // we do not load dirty flags in CLI and cron
5620 $ACCESSLIB_PRIVATE->dirtycontexts = array();
5622 } else {
5623 if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
5624 if (!isset($USER->access['time'])) {
5625 // nothing was loaded yet, we do not need to check dirty contexts now
5626 return;
5628 // no idea why -2 is there, server cluster time difference maybe... (skodak)
5629 $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time']-2);
5633 foreach ($ACCESSLIB_PRIVATE->dirtycontexts as $path=>$unused) {
5634 if ($path === $this->_path or strpos($this->_path, $path.'/') === 0) {
5635 // reload all capabilities of USER and others - preserving loginas, roleswitches, etc
5636 // and then cleanup any marks of dirtyness... at least from our short term memory! :-)
5637 reload_all_capabilities();
5638 break;
5644 * Mark a context as dirty (with timestamp) so as to force reloading of the context.
5646 public function mark_dirty() {
5647 global $CFG, $USER, $ACCESSLIB_PRIVATE;
5649 if (during_initial_install()) {
5650 return;
5653 // only if it is a non-empty string
5654 if (is_string($this->_path) && $this->_path !== '') {
5655 set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time()+$CFG->sessiontimeout);
5656 if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
5657 $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
5658 } else {
5659 if (CLI_SCRIPT) {
5660 $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
5661 } else {
5662 if (isset($USER->access['time'])) {
5663 $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time']-2);
5664 } else {
5665 $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
5667 // flags not loaded yet, it will be done later in $context->reload_if_dirty()
5676 * Context maintenance and helper methods.
5678 * This is "extends context" is a bloody hack that tires to work around the deficiencies
5679 * in the "protected" keyword in PHP, this helps us to hide all the internals of context
5680 * level implementation from the rest of code, the code completion returns what developers need.
5682 * Thank you Tim Hunt for helping me with this nasty trick.
5684 * @package core_access
5685 * @category access
5686 * @copyright Petr Skoda {@link http://skodak.org}
5687 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5688 * @since 2.2
5690 class context_helper extends context {
5693 * @var array An array mapping context levels to classes
5695 private static $alllevels = array(
5696 CONTEXT_SYSTEM => 'context_system',
5697 CONTEXT_USER => 'context_user',
5698 CONTEXT_COURSECAT => 'context_coursecat',
5699 CONTEXT_COURSE => 'context_course',
5700 CONTEXT_MODULE => 'context_module',
5701 CONTEXT_BLOCK => 'context_block',
5705 * Instance does not make sense here, only static use
5707 protected function __construct() {
5711 * Returns a class name of the context level class
5713 * @static
5714 * @param int $contextlevel (CONTEXT_SYSTEM, etc.)
5715 * @return string class name of the context class
5717 public static function get_class_for_level($contextlevel) {
5718 if (isset(self::$alllevels[$contextlevel])) {
5719 return self::$alllevels[$contextlevel];
5720 } else {
5721 throw new coding_exception('Invalid context level specified');
5726 * Returns a list of all context levels
5728 * @static
5729 * @return array int=>string (level=>level class name)
5731 public static function get_all_levels() {
5732 return self::$alllevels;
5736 * Remove stale contexts that belonged to deleted instances.
5737 * Ideally all code should cleanup contexts properly, unfortunately accidents happen...
5739 * @static
5740 * @return void
5742 public static function cleanup_instances() {
5743 global $DB;
5744 $sqls = array();
5745 foreach (self::$alllevels as $level=>$classname) {
5746 $sqls[] = $classname::get_cleanup_sql();
5749 $sql = implode(" UNION ", $sqls);
5751 // it is probably better to use transactions, it might be faster too
5752 $transaction = $DB->start_delegated_transaction();
5754 $rs = $DB->get_recordset_sql($sql);
5755 foreach ($rs as $record) {
5756 $context = context::create_instance_from_record($record);
5757 $context->delete();
5759 $rs->close();
5761 $transaction->allow_commit();
5765 * Create all context instances at the given level and above.
5767 * @static
5768 * @param int $contextlevel null means all levels
5769 * @param bool $buildpaths
5770 * @return void
5772 public static function create_instances($contextlevel = null, $buildpaths = true) {
5773 foreach (self::$alllevels as $level=>$classname) {
5774 if ($contextlevel and $level > $contextlevel) {
5775 // skip potential sub-contexts
5776 continue;
5778 $classname::create_level_instances();
5779 if ($buildpaths) {
5780 $classname::build_paths(false);
5786 * Rebuild paths and depths in all context levels.
5788 * @static
5789 * @param bool $force false means add missing only
5790 * @return void
5792 public static function build_all_paths($force = false) {
5793 foreach (self::$alllevels as $classname) {
5794 $classname::build_paths($force);
5797 // reset static course cache - it might have incorrect cached data
5798 accesslib_clear_all_caches(true);
5802 * Resets the cache to remove all data.
5803 * @static
5805 public static function reset_caches() {
5806 context::reset_caches();
5810 * Returns all fields necessary for context preloading from user $rec.
5812 * This helps with performance when dealing with hundreds of contexts.
5814 * @static
5815 * @param string $tablealias context table alias in the query
5816 * @return array (table.column=>alias, ...)
5818 public static function get_preload_record_columns($tablealias) {
5819 return array("$tablealias.id"=>"ctxid", "$tablealias.path"=>"ctxpath", "$tablealias.depth"=>"ctxdepth", "$tablealias.contextlevel"=>"ctxlevel", "$tablealias.instanceid"=>"ctxinstance");
5823 * Returns all fields necessary for context preloading from user $rec.
5825 * This helps with performance when dealing with hundreds of contexts.
5827 * @static
5828 * @param string $tablealias context table alias in the query
5829 * @return string
5831 public static function get_preload_record_columns_sql($tablealias) {
5832 return "$tablealias.id AS ctxid, $tablealias.path AS ctxpath, $tablealias.depth AS ctxdepth, $tablealias.contextlevel AS ctxlevel, $tablealias.instanceid AS ctxinstance";
5836 * Preloads context information from db record and strips the cached info.
5838 * The db request has to contain all columns from context_helper::get_preload_record_columns().
5840 * @static
5841 * @param stdClass $rec
5842 * @return void (modifies $rec)
5844 public static function preload_from_record(stdClass $rec) {
5845 context::preload_from_record($rec);
5849 * Preload all contexts instances from course.
5851 * To be used if you expect multiple queries for course activities...
5853 * @static
5854 * @param int $courseid
5856 public static function preload_course($courseid) {
5857 // Users can call this multiple times without doing any harm
5858 if (isset(context::$cache_preloaded[$courseid])) {
5859 return;
5861 $coursecontext = context_course::instance($courseid);
5862 $coursecontext->get_child_contexts();
5864 context::$cache_preloaded[$courseid] = true;
5868 * Delete context instance
5870 * @static
5871 * @param int $contextlevel
5872 * @param int $instanceid
5873 * @return void
5875 public static function delete_instance($contextlevel, $instanceid) {
5876 global $DB;
5878 // double check the context still exists
5879 if ($record = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
5880 $context = context::create_instance_from_record($record);
5881 $context->delete();
5882 } else {
5883 // we should try to purge the cache anyway
5888 * Returns the name of specified context level
5890 * @static
5891 * @param int $contextlevel
5892 * @return string name of the context level
5894 public static function get_level_name($contextlevel) {
5895 $classname = context_helper::get_class_for_level($contextlevel);
5896 return $classname::get_level_name();
5900 * not used
5902 public function get_url() {
5906 * not used
5908 public function get_capabilities() {
5914 * System context class
5916 * @package core_access
5917 * @category access
5918 * @copyright Petr Skoda {@link http://skodak.org}
5919 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5920 * @since 2.2
5922 class context_system extends context {
5924 * Please use context_system::instance() if you need the instance of context.
5926 * @param stdClass $record
5928 protected function __construct(stdClass $record) {
5929 parent::__construct($record);
5930 if ($record->contextlevel != CONTEXT_SYSTEM) {
5931 throw new coding_exception('Invalid $record->contextlevel in context_system constructor.');
5936 * Returns human readable context level name.
5938 * @static
5939 * @return string the human readable context level name.
5941 public static function get_level_name() {
5942 return get_string('coresystem');
5946 * Returns human readable context identifier.
5948 * @param boolean $withprefix does not apply to system context
5949 * @param boolean $short does not apply to system context
5950 * @return string the human readable context name.
5952 public function get_context_name($withprefix = true, $short = false) {
5953 return self::get_level_name();
5957 * Returns the most relevant URL for this context.
5959 * @return moodle_url
5961 public function get_url() {
5962 return new moodle_url('/');
5966 * Returns array of relevant context capability records.
5968 * @return array
5970 public function get_capabilities() {
5971 global $DB;
5973 $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
5975 $params = array();
5976 $sql = "SELECT *
5977 FROM {capabilities}";
5979 return $DB->get_records_sql($sql.' '.$sort, $params);
5983 * Create missing context instances at system context
5984 * @static
5986 protected static function create_level_instances() {
5987 // nothing to do here, the system context is created automatically in installer
5988 self::instance(0);
5992 * Returns system context instance.
5994 * @static
5995 * @param int $instanceid
5996 * @param int $strictness
5997 * @param bool $cache
5998 * @return context_system context instance
6000 public static function instance($instanceid = 0, $strictness = MUST_EXIST, $cache = true) {
6001 global $DB;
6003 if ($instanceid != 0) {
6004 debugging('context_system::instance(): invalid $id parameter detected, should be 0');
6007 if (defined('SYSCONTEXTID') and $cache) { // dangerous: define this in config.php to eliminate 1 query/page
6008 if (!isset(context::$systemcontext)) {
6009 $record = new stdClass();
6010 $record->id = SYSCONTEXTID;
6011 $record->contextlevel = CONTEXT_SYSTEM;
6012 $record->instanceid = 0;
6013 $record->path = '/'.SYSCONTEXTID;
6014 $record->depth = 1;
6015 context::$systemcontext = new context_system($record);
6017 return context::$systemcontext;
6021 try {
6022 // We ignore the strictness completely because system context must exist except during install.
6023 $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST);
6024 } catch (dml_exception $e) {
6025 //table or record does not exist
6026 if (!during_initial_install()) {
6027 // do not mess with system context after install, it simply must exist
6028 throw $e;
6030 $record = null;
6033 if (!$record) {
6034 $record = new stdClass();
6035 $record->contextlevel = CONTEXT_SYSTEM;
6036 $record->instanceid = 0;
6037 $record->depth = 1;
6038 $record->path = null; //not known before insert
6040 try {
6041 if ($DB->count_records('context')) {
6042 // contexts already exist, this is very weird, system must be first!!!
6043 return null;
6045 if (defined('SYSCONTEXTID')) {
6046 // this would happen only in unittest on sites that went through weird 1.7 upgrade
6047 $record->id = SYSCONTEXTID;
6048 $DB->import_record('context', $record);
6049 $DB->get_manager()->reset_sequence('context');
6050 } else {
6051 $record->id = $DB->insert_record('context', $record);
6053 } catch (dml_exception $e) {
6054 // can not create context - table does not exist yet, sorry
6055 return null;
6059 if ($record->instanceid != 0) {
6060 // this is very weird, somebody must be messing with context table
6061 debugging('Invalid system context detected');
6064 if ($record->depth != 1 or $record->path != '/'.$record->id) {
6065 // fix path if necessary, initial install or path reset
6066 $record->depth = 1;
6067 $record->path = '/'.$record->id;
6068 $DB->update_record('context', $record);
6071 if (!defined('SYSCONTEXTID')) {
6072 define('SYSCONTEXTID', $record->id);
6075 context::$systemcontext = new context_system($record);
6076 return context::$systemcontext;
6080 * Returns all site contexts except the system context, DO NOT call on production servers!!
6082 * Contexts are not cached.
6084 * @return array
6086 public function get_child_contexts() {
6087 global $DB;
6089 debugging('Fetching of system context child courses is strongly discouraged on production servers (it may eat all available memory)!');
6091 // Just get all the contexts except for CONTEXT_SYSTEM level
6092 // and hope we don't OOM in the process - don't cache
6093 $sql = "SELECT c.*
6094 FROM {context} c
6095 WHERE contextlevel > ".CONTEXT_SYSTEM;
6096 $records = $DB->get_records_sql($sql);
6098 $result = array();
6099 foreach ($records as $record) {
6100 $result[$record->id] = context::create_instance_from_record($record);
6103 return $result;
6107 * Returns sql necessary for purging of stale context instances.
6109 * @static
6110 * @return string cleanup SQL
6112 protected static function get_cleanup_sql() {
6113 $sql = "
6114 SELECT c.*
6115 FROM {context} c
6116 WHERE 1=2
6119 return $sql;
6123 * Rebuild context paths and depths at system context level.
6125 * @static
6126 * @param bool $force
6128 protected static function build_paths($force) {
6129 global $DB;
6131 /* note: ignore $force here, we always do full test of system context */
6133 // exactly one record must exist
6134 $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST);
6136 if ($record->instanceid != 0) {
6137 debugging('Invalid system context detected');
6140 if (defined('SYSCONTEXTID') and $record->id != SYSCONTEXTID) {
6141 debugging('Invalid SYSCONTEXTID detected');
6144 if ($record->depth != 1 or $record->path != '/'.$record->id) {
6145 // fix path if necessary, initial install or path reset
6146 $record->depth = 1;
6147 $record->path = '/'.$record->id;
6148 $DB->update_record('context', $record);
6155 * User context class
6157 * @package core_access
6158 * @category access
6159 * @copyright Petr Skoda {@link http://skodak.org}
6160 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6161 * @since 2.2
6163 class context_user extends context {
6165 * Please use context_user::instance($userid) if you need the instance of context.
6166 * Alternatively if you know only the context id use context::instance_by_id($contextid)
6168 * @param stdClass $record
6170 protected function __construct(stdClass $record) {
6171 parent::__construct($record);
6172 if ($record->contextlevel != CONTEXT_USER) {
6173 throw new coding_exception('Invalid $record->contextlevel in context_user constructor.');
6178 * Returns human readable context level name.
6180 * @static
6181 * @return string the human readable context level name.
6183 public static function get_level_name() {
6184 return get_string('user');
6188 * Returns human readable context identifier.
6190 * @param boolean $withprefix whether to prefix the name of the context with User
6191 * @param boolean $short does not apply to user context
6192 * @return string the human readable context name.
6194 public function get_context_name($withprefix = true, $short = false) {
6195 global $DB;
6197 $name = '';
6198 if ($user = $DB->get_record('user', array('id'=>$this->_instanceid, 'deleted'=>0))) {
6199 if ($withprefix){
6200 $name = get_string('user').': ';
6202 $name .= fullname($user);
6204 return $name;
6208 * Returns the most relevant URL for this context.
6210 * @return moodle_url
6212 public function get_url() {
6213 global $COURSE;
6215 if ($COURSE->id == SITEID) {
6216 $url = new moodle_url('/user/profile.php', array('id'=>$this->_instanceid));
6217 } else {
6218 $url = new moodle_url('/user/view.php', array('id'=>$this->_instanceid, 'courseid'=>$COURSE->id));
6220 return $url;
6224 * Returns array of relevant context capability records.
6226 * @return array
6228 public function get_capabilities() {
6229 global $DB;
6231 $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
6233 $extracaps = array('moodle/grade:viewall');
6234 list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
6235 $sql = "SELECT *
6236 FROM {capabilities}
6237 WHERE contextlevel = ".CONTEXT_USER."
6238 OR name $extra";
6240 return $records = $DB->get_records_sql($sql.' '.$sort, $params);
6244 * Returns user context instance.
6246 * @static
6247 * @param int $instanceid
6248 * @param int $strictness
6249 * @return context_user context instance
6251 public static function instance($instanceid, $strictness = MUST_EXIST) {
6252 global $DB;
6254 if ($context = context::cache_get(CONTEXT_USER, $instanceid)) {
6255 return $context;
6258 if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_USER, 'instanceid'=>$instanceid))) {
6259 if ($user = $DB->get_record('user', array('id'=>$instanceid, 'deleted'=>0), 'id', $strictness)) {
6260 $record = context::insert_context_record(CONTEXT_USER, $user->id, '/'.SYSCONTEXTID, 0);
6264 if ($record) {
6265 $context = new context_user($record);
6266 context::cache_add($context);
6267 return $context;
6270 return false;
6274 * Create missing context instances at user context level
6275 * @static
6277 protected static function create_level_instances() {
6278 global $DB;
6280 $sql = "INSERT INTO {context} (contextlevel, instanceid)
6281 SELECT ".CONTEXT_USER.", u.id
6282 FROM {user} u
6283 WHERE u.deleted = 0
6284 AND NOT EXISTS (SELECT 'x'
6285 FROM {context} cx
6286 WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
6287 $DB->execute($sql);
6291 * Returns sql necessary for purging of stale context instances.
6293 * @static
6294 * @return string cleanup SQL
6296 protected static function get_cleanup_sql() {
6297 $sql = "
6298 SELECT c.*
6299 FROM {context} c
6300 LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0)
6301 WHERE u.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
6304 return $sql;
6308 * Rebuild context paths and depths at user context level.
6310 * @static
6311 * @param bool $force
6313 protected static function build_paths($force) {
6314 global $DB;
6316 // First update normal users.
6317 $path = $DB->sql_concat('?', 'id');
6318 $pathstart = '/' . SYSCONTEXTID . '/';
6319 $params = array($pathstart);
6321 if ($force) {
6322 $where = "depth <> 2 OR path IS NULL OR path <> ({$path})";
6323 $params[] = $pathstart;
6324 } else {
6325 $where = "depth = 0 OR path IS NULL";
6328 $sql = "UPDATE {context}
6329 SET depth = 2,
6330 path = {$path}
6331 WHERE contextlevel = " . CONTEXT_USER . "
6332 AND ($where)";
6333 $DB->execute($sql, $params);
6339 * Course category context class
6341 * @package core_access
6342 * @category access
6343 * @copyright Petr Skoda {@link http://skodak.org}
6344 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6345 * @since 2.2
6347 class context_coursecat extends context {
6349 * Please use context_coursecat::instance($coursecatid) if you need the instance of context.
6350 * Alternatively if you know only the context id use context::instance_by_id($contextid)
6352 * @param stdClass $record
6354 protected function __construct(stdClass $record) {
6355 parent::__construct($record);
6356 if ($record->contextlevel != CONTEXT_COURSECAT) {
6357 throw new coding_exception('Invalid $record->contextlevel in context_coursecat constructor.');
6362 * Returns human readable context level name.
6364 * @static
6365 * @return string the human readable context level name.
6367 public static function get_level_name() {
6368 return get_string('category');
6372 * Returns human readable context identifier.
6374 * @param boolean $withprefix whether to prefix the name of the context with Category
6375 * @param boolean $short does not apply to course categories
6376 * @return string the human readable context name.
6378 public function get_context_name($withprefix = true, $short = false) {
6379 global $DB;
6381 $name = '';
6382 if ($category = $DB->get_record('course_categories', array('id'=>$this->_instanceid))) {
6383 if ($withprefix){
6384 $name = get_string('category').': ';
6386 $name .= format_string($category->name, true, array('context' => $this));
6388 return $name;
6392 * Returns the most relevant URL for this context.
6394 * @return moodle_url
6396 public function get_url() {
6397 return new moodle_url('/course/index.php', array('categoryid' => $this->_instanceid));
6401 * Returns array of relevant context capability records.
6403 * @return array
6405 public function get_capabilities() {
6406 global $DB;
6408 $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
6410 $params = array();
6411 $sql = "SELECT *
6412 FROM {capabilities}
6413 WHERE contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
6415 return $DB->get_records_sql($sql.' '.$sort, $params);
6419 * Returns course category context instance.
6421 * @static
6422 * @param int $instanceid
6423 * @param int $strictness
6424 * @return context_coursecat context instance
6426 public static function instance($instanceid, $strictness = MUST_EXIST) {
6427 global $DB;
6429 if ($context = context::cache_get(CONTEXT_COURSECAT, $instanceid)) {
6430 return $context;
6433 if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSECAT, 'instanceid'=>$instanceid))) {
6434 if ($category = $DB->get_record('course_categories', array('id'=>$instanceid), 'id,parent', $strictness)) {
6435 if ($category->parent) {
6436 $parentcontext = context_coursecat::instance($category->parent);
6437 $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, $parentcontext->path);
6438 } else {
6439 $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, '/'.SYSCONTEXTID, 0);
6444 if ($record) {
6445 $context = new context_coursecat($record);
6446 context::cache_add($context);
6447 return $context;
6450 return false;
6454 * Returns immediate child contexts of category and all subcategories,
6455 * children of subcategories and courses are not returned.
6457 * @return array
6459 public function get_child_contexts() {
6460 global $DB;
6462 $sql = "SELECT ctx.*
6463 FROM {context} ctx
6464 WHERE ctx.path LIKE ? AND (ctx.depth = ? OR ctx.contextlevel = ?)";
6465 $params = array($this->_path.'/%', $this->depth+1, CONTEXT_COURSECAT);
6466 $records = $DB->get_records_sql($sql, $params);
6468 $result = array();
6469 foreach ($records as $record) {
6470 $result[$record->id] = context::create_instance_from_record($record);
6473 return $result;
6477 * Create missing context instances at course category context level
6478 * @static
6480 protected static function create_level_instances() {
6481 global $DB;
6483 $sql = "INSERT INTO {context} (contextlevel, instanceid)
6484 SELECT ".CONTEXT_COURSECAT.", cc.id
6485 FROM {course_categories} cc
6486 WHERE NOT EXISTS (SELECT 'x'
6487 FROM {context} cx
6488 WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
6489 $DB->execute($sql);
6493 * Returns sql necessary for purging of stale context instances.
6495 * @static
6496 * @return string cleanup SQL
6498 protected static function get_cleanup_sql() {
6499 $sql = "
6500 SELECT c.*
6501 FROM {context} c
6502 LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id
6503 WHERE cc.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
6506 return $sql;
6510 * Rebuild context paths and depths at course category context level.
6512 * @static
6513 * @param bool $force
6515 protected static function build_paths($force) {
6516 global $DB;
6518 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSECAT." AND (depth = 0 OR path IS NULL)")) {
6519 if ($force) {
6520 $ctxemptyclause = $emptyclause = '';
6521 } else {
6522 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
6523 $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
6526 $base = '/'.SYSCONTEXTID;
6528 // Normal top level categories
6529 $sql = "UPDATE {context}
6530 SET depth=2,
6531 path=".$DB->sql_concat("'$base/'", 'id')."
6532 WHERE contextlevel=".CONTEXT_COURSECAT."
6533 AND EXISTS (SELECT 'x'
6534 FROM {course_categories} cc
6535 WHERE cc.id = {context}.instanceid AND cc.depth=1)
6536 $emptyclause";
6537 $DB->execute($sql);
6539 // Deeper categories - one query per depthlevel
6540 $maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}");
6541 for ($n=2; $n<=$maxdepth; $n++) {
6542 $sql = "INSERT INTO {context_temp} (id, path, depth)
6543 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
6544 FROM {context} ctx
6545 JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT." AND cc.depth = $n)
6546 JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".CONTEXT_COURSECAT.")
6547 WHERE pctx.path IS NOT NULL AND pctx.depth > 0
6548 $ctxemptyclause";
6549 $trans = $DB->start_delegated_transaction();
6550 $DB->delete_records('context_temp');
6551 $DB->execute($sql);
6552 context::merge_context_temp_table();
6553 $DB->delete_records('context_temp');
6554 $trans->allow_commit();
6563 * Course context class
6565 * @package core_access
6566 * @category access
6567 * @copyright Petr Skoda {@link http://skodak.org}
6568 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6569 * @since 2.2
6571 class context_course extends context {
6573 * Please use context_course::instance($courseid) if you need the instance of context.
6574 * Alternatively if you know only the context id use context::instance_by_id($contextid)
6576 * @param stdClass $record
6578 protected function __construct(stdClass $record) {
6579 parent::__construct($record);
6580 if ($record->contextlevel != CONTEXT_COURSE) {
6581 throw new coding_exception('Invalid $record->contextlevel in context_course constructor.');
6586 * Returns human readable context level name.
6588 * @static
6589 * @return string the human readable context level name.
6591 public static function get_level_name() {
6592 return get_string('course');
6596 * Returns human readable context identifier.
6598 * @param boolean $withprefix whether to prefix the name of the context with Course
6599 * @param boolean $short whether to use the short name of the thing.
6600 * @return string the human readable context name.
6602 public function get_context_name($withprefix = true, $short = false) {
6603 global $DB;
6605 $name = '';
6606 if ($this->_instanceid == SITEID) {
6607 $name = get_string('frontpage', 'admin');
6608 } else {
6609 if ($course = $DB->get_record('course', array('id'=>$this->_instanceid))) {
6610 if ($withprefix){
6611 $name = get_string('course').': ';
6613 if ($short){
6614 $name .= format_string($course->shortname, true, array('context' => $this));
6615 } else {
6616 $name .= format_string(get_course_display_name_for_list($course));
6620 return $name;
6624 * Returns the most relevant URL for this context.
6626 * @return moodle_url
6628 public function get_url() {
6629 if ($this->_instanceid != SITEID) {
6630 return new moodle_url('/course/view.php', array('id'=>$this->_instanceid));
6633 return new moodle_url('/');
6637 * Returns array of relevant context capability records.
6639 * @return array
6641 public function get_capabilities() {
6642 global $DB;
6644 $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
6646 $params = array();
6647 $sql = "SELECT *
6648 FROM {capabilities}
6649 WHERE contextlevel IN (".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
6651 return $DB->get_records_sql($sql.' '.$sort, $params);
6655 * Is this context part of any course? If yes return course context.
6657 * @param bool $strict true means throw exception if not found, false means return false if not found
6658 * @return course_context context of the enclosing course, null if not found or exception
6660 public function get_course_context($strict = true) {
6661 return $this;
6665 * Returns course context instance.
6667 * @static
6668 * @param int $instanceid
6669 * @param int $strictness
6670 * @return context_course context instance
6672 public static function instance($instanceid, $strictness = MUST_EXIST) {
6673 global $DB;
6675 if ($context = context::cache_get(CONTEXT_COURSE, $instanceid)) {
6676 return $context;
6679 if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>$instanceid))) {
6680 if ($course = $DB->get_record('course', array('id'=>$instanceid), 'id,category', $strictness)) {
6681 if ($course->category) {
6682 $parentcontext = context_coursecat::instance($course->category);
6683 $record = context::insert_context_record(CONTEXT_COURSE, $course->id, $parentcontext->path);
6684 } else {
6685 $record = context::insert_context_record(CONTEXT_COURSE, $course->id, '/'.SYSCONTEXTID, 0);
6690 if ($record) {
6691 $context = new context_course($record);
6692 context::cache_add($context);
6693 return $context;
6696 return false;
6700 * Create missing context instances at course context level
6701 * @static
6703 protected static function create_level_instances() {
6704 global $DB;
6706 $sql = "INSERT INTO {context} (contextlevel, instanceid)
6707 SELECT ".CONTEXT_COURSE.", c.id
6708 FROM {course} c
6709 WHERE NOT EXISTS (SELECT 'x'
6710 FROM {context} cx
6711 WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
6712 $DB->execute($sql);
6716 * Returns sql necessary for purging of stale context instances.
6718 * @static
6719 * @return string cleanup SQL
6721 protected static function get_cleanup_sql() {
6722 $sql = "
6723 SELECT c.*
6724 FROM {context} c
6725 LEFT OUTER JOIN {course} co ON c.instanceid = co.id
6726 WHERE co.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
6729 return $sql;
6733 * Rebuild context paths and depths at course context level.
6735 * @static
6736 * @param bool $force
6738 protected static function build_paths($force) {
6739 global $DB;
6741 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSE." AND (depth = 0 OR path IS NULL)")) {
6742 if ($force) {
6743 $ctxemptyclause = $emptyclause = '';
6744 } else {
6745 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
6746 $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
6749 $base = '/'.SYSCONTEXTID;
6751 // Standard frontpage
6752 $sql = "UPDATE {context}
6753 SET depth = 2,
6754 path = ".$DB->sql_concat("'$base/'", 'id')."
6755 WHERE contextlevel = ".CONTEXT_COURSE."
6756 AND EXISTS (SELECT 'x'
6757 FROM {course} c
6758 WHERE c.id = {context}.instanceid AND c.category = 0)
6759 $emptyclause";
6760 $DB->execute($sql);
6762 // standard courses
6763 $sql = "INSERT INTO {context_temp} (id, path, depth)
6764 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
6765 FROM {context} ctx
6766 JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE." AND c.category <> 0)
6767 JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".CONTEXT_COURSECAT.")
6768 WHERE pctx.path IS NOT NULL AND pctx.depth > 0
6769 $ctxemptyclause";
6770 $trans = $DB->start_delegated_transaction();
6771 $DB->delete_records('context_temp');
6772 $DB->execute($sql);
6773 context::merge_context_temp_table();
6774 $DB->delete_records('context_temp');
6775 $trans->allow_commit();
6782 * Course module context class
6784 * @package core_access
6785 * @category access
6786 * @copyright Petr Skoda {@link http://skodak.org}
6787 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6788 * @since 2.2
6790 class context_module extends context {
6792 * Please use context_module::instance($cmid) if you need the instance of context.
6793 * Alternatively if you know only the context id use context::instance_by_id($contextid)
6795 * @param stdClass $record
6797 protected function __construct(stdClass $record) {
6798 parent::__construct($record);
6799 if ($record->contextlevel != CONTEXT_MODULE) {
6800 throw new coding_exception('Invalid $record->contextlevel in context_module constructor.');
6805 * Returns human readable context level name.
6807 * @static
6808 * @return string the human readable context level name.
6810 public static function get_level_name() {
6811 return get_string('activitymodule');
6815 * Returns human readable context identifier.
6817 * @param boolean $withprefix whether to prefix the name of the context with the
6818 * module name, e.g. Forum, Glossary, etc.
6819 * @param boolean $short does not apply to module context
6820 * @return string the human readable context name.
6822 public function get_context_name($withprefix = true, $short = false) {
6823 global $DB;
6825 $name = '';
6826 if ($cm = $DB->get_record_sql("SELECT cm.*, md.name AS modname
6827 FROM {course_modules} cm
6828 JOIN {modules} md ON md.id = cm.module
6829 WHERE cm.id = ?", array($this->_instanceid))) {
6830 if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) {
6831 if ($withprefix){
6832 $name = get_string('modulename', $cm->modname).': ';
6834 $name .= format_string($mod->name, true, array('context' => $this));
6837 return $name;
6841 * Returns the most relevant URL for this context.
6843 * @return moodle_url
6845 public function get_url() {
6846 global $DB;
6848 if ($modname = $DB->get_field_sql("SELECT md.name AS modname
6849 FROM {course_modules} cm
6850 JOIN {modules} md ON md.id = cm.module
6851 WHERE cm.id = ?", array($this->_instanceid))) {
6852 return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$this->_instanceid));
6855 return new moodle_url('/');
6859 * Returns array of relevant context capability records.
6861 * @return array
6863 public function get_capabilities() {
6864 global $DB, $CFG;
6866 $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
6868 $cm = $DB->get_record('course_modules', array('id'=>$this->_instanceid));
6869 $module = $DB->get_record('modules', array('id'=>$cm->module));
6871 $subcaps = array();
6872 $subpluginsfile = "$CFG->dirroot/mod/$module->name/db/subplugins.php";
6873 if (file_exists($subpluginsfile)) {
6874 $subplugins = array(); // should be redefined in the file
6875 include($subpluginsfile);
6876 if (!empty($subplugins)) {
6877 foreach (array_keys($subplugins) as $subplugintype) {
6878 foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) {
6879 $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
6885 $modfile = "$CFG->dirroot/mod/$module->name/lib.php";
6886 $extracaps = array();
6887 if (file_exists($modfile)) {
6888 include_once($modfile);
6889 $modfunction = $module->name.'_get_extra_capabilities';
6890 if (function_exists($modfunction)) {
6891 $extracaps = $modfunction();
6895 $extracaps = array_merge($subcaps, $extracaps);
6896 $extra = '';
6897 list($extra, $params) = $DB->get_in_or_equal(
6898 $extracaps, SQL_PARAMS_NAMED, 'cap0', true, '');
6899 if (!empty($extra)) {
6900 $extra = "OR name $extra";
6902 $sql = "SELECT *
6903 FROM {capabilities}
6904 WHERE (contextlevel = ".CONTEXT_MODULE."
6905 AND (component = :component OR component = 'moodle'))
6906 $extra";
6907 $params['component'] = "mod_$module->name";
6909 return $DB->get_records_sql($sql.' '.$sort, $params);
6913 * Is this context part of any course? If yes return course context.
6915 * @param bool $strict true means throw exception if not found, false means return false if not found
6916 * @return context_course context of the enclosing course, null if not found or exception
6918 public function get_course_context($strict = true) {
6919 return $this->get_parent_context();
6923 * Returns module context instance.
6925 * @static
6926 * @param int $instanceid
6927 * @param int $strictness
6928 * @return context_module context instance
6930 public static function instance($instanceid, $strictness = MUST_EXIST) {
6931 global $DB;
6933 if ($context = context::cache_get(CONTEXT_MODULE, $instanceid)) {
6934 return $context;
6937 if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_MODULE, 'instanceid'=>$instanceid))) {
6938 if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid), 'id,course', $strictness)) {
6939 $parentcontext = context_course::instance($cm->course);
6940 $record = context::insert_context_record(CONTEXT_MODULE, $cm->id, $parentcontext->path);
6944 if ($record) {
6945 $context = new context_module($record);
6946 context::cache_add($context);
6947 return $context;
6950 return false;
6954 * Create missing context instances at module context level
6955 * @static
6957 protected static function create_level_instances() {
6958 global $DB;
6960 $sql = "INSERT INTO {context} (contextlevel, instanceid)
6961 SELECT ".CONTEXT_MODULE.", cm.id
6962 FROM {course_modules} cm
6963 WHERE NOT EXISTS (SELECT 'x'
6964 FROM {context} cx
6965 WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
6966 $DB->execute($sql);
6970 * Returns sql necessary for purging of stale context instances.
6972 * @static
6973 * @return string cleanup SQL
6975 protected static function get_cleanup_sql() {
6976 $sql = "
6977 SELECT c.*
6978 FROM {context} c
6979 LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id
6980 WHERE cm.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
6983 return $sql;
6987 * Rebuild context paths and depths at module context level.
6989 * @static
6990 * @param bool $force
6992 protected static function build_paths($force) {
6993 global $DB;
6995 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_MODULE." AND (depth = 0 OR path IS NULL)")) {
6996 if ($force) {
6997 $ctxemptyclause = '';
6998 } else {
6999 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
7002 $sql = "INSERT INTO {context_temp} (id, path, depth)
7003 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
7004 FROM {context} ctx
7005 JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_MODULE.")
7006 JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = ".CONTEXT_COURSE.")
7007 WHERE pctx.path IS NOT NULL AND pctx.depth > 0
7008 $ctxemptyclause";
7009 $trans = $DB->start_delegated_transaction();
7010 $DB->delete_records('context_temp');
7011 $DB->execute($sql);
7012 context::merge_context_temp_table();
7013 $DB->delete_records('context_temp');
7014 $trans->allow_commit();
7021 * Block context class
7023 * @package core_access
7024 * @category access
7025 * @copyright Petr Skoda {@link http://skodak.org}
7026 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7027 * @since 2.2
7029 class context_block extends context {
7031 * Please use context_block::instance($blockinstanceid) if you need the instance of context.
7032 * Alternatively if you know only the context id use context::instance_by_id($contextid)
7034 * @param stdClass $record
7036 protected function __construct(stdClass $record) {
7037 parent::__construct($record);
7038 if ($record->contextlevel != CONTEXT_BLOCK) {
7039 throw new coding_exception('Invalid $record->contextlevel in context_block constructor');
7044 * Returns human readable context level name.
7046 * @static
7047 * @return string the human readable context level name.
7049 public static function get_level_name() {
7050 return get_string('block');
7054 * Returns human readable context identifier.
7056 * @param boolean $withprefix whether to prefix the name of the context with Block
7057 * @param boolean $short does not apply to block context
7058 * @return string the human readable context name.
7060 public function get_context_name($withprefix = true, $short = false) {
7061 global $DB, $CFG;
7063 $name = '';
7064 if ($blockinstance = $DB->get_record('block_instances', array('id'=>$this->_instanceid))) {
7065 global $CFG;
7066 require_once("$CFG->dirroot/blocks/moodleblock.class.php");
7067 require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php");
7068 $blockname = "block_$blockinstance->blockname";
7069 if ($blockobject = new $blockname()) {
7070 if ($withprefix){
7071 $name = get_string('block').': ';
7073 $name .= $blockobject->title;
7077 return $name;
7081 * Returns the most relevant URL for this context.
7083 * @return moodle_url
7085 public function get_url() {
7086 $parentcontexts = $this->get_parent_context();
7087 return $parentcontexts->get_url();
7091 * Returns array of relevant context capability records.
7093 * @return array
7095 public function get_capabilities() {
7096 global $DB;
7098 $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
7100 $params = array();
7101 $bi = $DB->get_record('block_instances', array('id' => $this->_instanceid));
7103 $extra = '';
7104 $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities');
7105 if ($extracaps) {
7106 list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
7107 $extra = "OR name $extra";
7110 $sql = "SELECT *
7111 FROM {capabilities}
7112 WHERE (contextlevel = ".CONTEXT_BLOCK."
7113 AND component = :component)
7114 $extra";
7115 $params['component'] = 'block_' . $bi->blockname;
7117 return $DB->get_records_sql($sql.' '.$sort, $params);
7121 * Is this context part of any course? If yes return course context.
7123 * @param bool $strict true means throw exception if not found, false means return false if not found
7124 * @return course_context context of the enclosing course, null if not found or exception
7126 public function get_course_context($strict = true) {
7127 $parentcontext = $this->get_parent_context();
7128 return $parentcontext->get_course_context($strict);
7132 * Returns block context instance.
7134 * @static
7135 * @param int $instanceid
7136 * @param int $strictness
7137 * @return context_block context instance
7139 public static function instance($instanceid, $strictness = MUST_EXIST) {
7140 global $DB;
7142 if ($context = context::cache_get(CONTEXT_BLOCK, $instanceid)) {
7143 return $context;
7146 if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_BLOCK, 'instanceid'=>$instanceid))) {
7147 if ($bi = $DB->get_record('block_instances', array('id'=>$instanceid), 'id,parentcontextid', $strictness)) {
7148 $parentcontext = context::instance_by_id($bi->parentcontextid);
7149 $record = context::insert_context_record(CONTEXT_BLOCK, $bi->id, $parentcontext->path);
7153 if ($record) {
7154 $context = new context_block($record);
7155 context::cache_add($context);
7156 return $context;
7159 return false;
7163 * Block do not have child contexts...
7164 * @return array
7166 public function get_child_contexts() {
7167 return array();
7171 * Create missing context instances at block context level
7172 * @static
7174 protected static function create_level_instances() {
7175 global $DB;
7177 $sql = "INSERT INTO {context} (contextlevel, instanceid)
7178 SELECT ".CONTEXT_BLOCK.", bi.id
7179 FROM {block_instances} bi
7180 WHERE NOT EXISTS (SELECT 'x'
7181 FROM {context} cx
7182 WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
7183 $DB->execute($sql);
7187 * Returns sql necessary for purging of stale context instances.
7189 * @static
7190 * @return string cleanup SQL
7192 protected static function get_cleanup_sql() {
7193 $sql = "
7194 SELECT c.*
7195 FROM {context} c
7196 LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id
7197 WHERE bi.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
7200 return $sql;
7204 * Rebuild context paths and depths at block context level.
7206 * @static
7207 * @param bool $force
7209 protected static function build_paths($force) {
7210 global $DB;
7212 if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_BLOCK." AND (depth = 0 OR path IS NULL)")) {
7213 if ($force) {
7214 $ctxemptyclause = '';
7215 } else {
7216 $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
7219 // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent
7220 $sql = "INSERT INTO {context_temp} (id, path, depth)
7221 SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
7222 FROM {context} ctx
7223 JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.")
7224 JOIN {context} pctx ON (pctx.id = bi.parentcontextid)
7225 WHERE (pctx.path IS NOT NULL AND pctx.depth > 0)
7226 $ctxemptyclause";
7227 $trans = $DB->start_delegated_transaction();
7228 $DB->delete_records('context_temp');
7229 $DB->execute($sql);
7230 context::merge_context_temp_table();
7231 $DB->delete_records('context_temp');
7232 $trans->allow_commit();
7238 // ============== DEPRECATED FUNCTIONS ==========================================
7239 // Old context related functions were deprecated in 2.0, it is recommended
7240 // to use context classes in new code. Old function can be used when
7241 // creating patches that are supposed to be backported to older stable branches.
7242 // These deprecated functions will not be removed in near future,
7243 // before removing devs will be warned with a debugging message first,
7244 // then we will add error message and only after that we can remove the functions
7245 // completely.
7248 * Runs get_records select on context table and returns the result
7249 * Does get_records_select on the context table, and returns the results ordered
7250 * by contextlevel, and then the natural sort order within each level.
7251 * for the purpose of $select, you need to know that the context table has been
7252 * aliased to ctx, so for example, you can call get_sorted_contexts('ctx.depth = 3');
7254 * @param string $select the contents of the WHERE clause. Remember to do ctx.fieldname.
7255 * @param array $params any parameters required by $select.
7256 * @return array the requested context records.
7258 function get_sorted_contexts($select, $params = array()) {
7260 //TODO: we should probably rewrite all the code that is using this thing, the trouble is we MUST NOT modify the context instances...
7262 global $DB;
7263 if ($select) {
7264 $select = 'WHERE ' . $select;
7266 return $DB->get_records_sql("
7267 SELECT ctx.*
7268 FROM {context} ctx
7269 LEFT JOIN {user} u ON ctx.contextlevel = " . CONTEXT_USER . " AND u.id = ctx.instanceid
7270 LEFT JOIN {course_categories} cat ON ctx.contextlevel = " . CONTEXT_COURSECAT . " AND cat.id = ctx.instanceid
7271 LEFT JOIN {course} c ON ctx.contextlevel = " . CONTEXT_COURSE . " AND c.id = ctx.instanceid
7272 LEFT JOIN {course_modules} cm ON ctx.contextlevel = " . CONTEXT_MODULE . " AND cm.id = ctx.instanceid
7273 LEFT JOIN {block_instances} bi ON ctx.contextlevel = " . CONTEXT_BLOCK . " AND bi.id = ctx.instanceid
7274 $select
7275 ORDER BY ctx.contextlevel, bi.defaultregion, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.defaultweight), u.lastname, u.firstname, cm.id
7276 ", $params);
7280 * Gets a string for sql calls, searching for stuff in this context or above
7282 * NOTE: use $DB->get_in_or_equal($context->get_parent_context_ids()...
7284 * @deprecated since 2.2, $context->use get_parent_context_ids() instead
7285 * @param context $context
7286 * @return string
7288 function get_related_contexts_string(context $context) {
7290 if ($parents = $context->get_parent_context_ids()) {
7291 return (' IN ('.$context->id.','.implode(',', $parents).')');
7292 } else {
7293 return (' ='.$context->id);
7298 * Given context and array of users, returns array of users whose enrolment status is suspended,
7299 * or enrolment has expired or has not started. Also removes those users from the given array
7301 * @param context $context context in which suspended users should be extracted.
7302 * @param array $users list of users.
7303 * @param array $ignoreusers array of user ids to ignore, e.g. guest
7304 * @return array list of suspended users.
7306 function extract_suspended_users($context, &$users, $ignoreusers=array()) {
7307 global $DB;
7309 // Get active enrolled users.
7310 list($sql, $params) = get_enrolled_sql($context, null, null, true);
7311 $activeusers = $DB->get_records_sql($sql, $params);
7313 // Move suspended users to a separate array & remove from the initial one.
7314 $susers = array();
7315 if (sizeof($activeusers)) {
7316 foreach ($users as $userid => $user) {
7317 if (!array_key_exists($userid, $activeusers) && !in_array($userid, $ignoreusers)) {
7318 $susers[$userid] = $user;
7319 unset($users[$userid]);
7323 return $susers;
7327 * Given context and array of users, returns array of user ids whose enrolment status is suspended,
7328 * or enrolment has expired or not started.
7330 * @param context $context context in which user enrolment is checked.
7331 * @return array list of suspended user id's.
7333 function get_suspended_userids($context){
7334 global $DB;
7336 // Get all enrolled users.
7337 list($sql, $params) = get_enrolled_sql($context);
7338 $users = $DB->get_records_sql($sql, $params);
7340 // Get active enrolled users.
7341 list($sql, $params) = get_enrolled_sql($context, null, null, true);
7342 $activeusers = $DB->get_records_sql($sql, $params);
7344 $susers = array();
7345 if (sizeof($activeusers) != sizeof($users)) {
7346 foreach ($users as $userid => $user) {
7347 if (!array_key_exists($userid, $activeusers)) {
7348 $susers[$userid] = $userid;
7352 return $susers;