/lib/accesslib.php
PHP | 7784 lines | 4316 code | 952 blank | 2516 comment | 829 complexity | 0faea7bbcec8f4e35f89afe381d5329f MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
Large files files are truncated, but you can click here to view the full file
- <?php
- // This file is part of Moodle - http://moodle.org/
- //
- // Moodle is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // Moodle is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
- /**
- * This file contains functions for managing user access
- *
- * <b>Public API vs internals</b>
- *
- * General users probably only care about
- *
- * Context handling
- * - context_course::instance($courseid), context_module::instance($cm->id), context_coursecat::instance($catid)
- * - context::instance_by_id($contextid)
- * - $context->get_parent_contexts();
- * - $context->get_child_contexts();
- *
- * Whether the user can do something...
- * - has_capability()
- * - has_any_capability()
- * - has_all_capabilities()
- * - require_capability()
- * - require_login() (from moodlelib)
- * - is_enrolled()
- * - is_viewing()
- * - is_guest()
- * - is_siteadmin()
- * - isguestuser()
- * - isloggedin()
- *
- * What courses has this user access to?
- * - get_enrolled_users()
- *
- * What users can do X in this context?
- * - get_enrolled_users() - at and bellow course context
- * - get_users_by_capability() - above course context
- *
- * Modify roles
- * - role_assign()
- * - role_unassign()
- * - role_unassign_all()
- *
- * Advanced - for internal use only
- * - load_all_capabilities()
- * - reload_all_capabilities()
- * - has_capability_in_accessdata()
- * - get_user_roles_sitewide_accessdata()
- * - etc.
- *
- * <b>Name conventions</b>
- *
- * "ctx" means context
- * "ra" means role assignment
- * "rdef" means role definition
- *
- * <b>accessdata</b>
- *
- * Access control data is held in the "accessdata" array
- * which - for the logged-in user, will be in $USER->access
- *
- * For other users can be generated and passed around (but may also be cached
- * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser).
- *
- * $accessdata is a multidimensional array, holding
- * role assignments (RAs), role switches and initialization time.
- *
- * Things are keyed on "contextpaths" (the path field of
- * the context table) for fast walking up/down the tree.
- * <code>
- * $accessdata['ra'][$contextpath] = array($roleid=>$roleid)
- * [$contextpath] = array($roleid=>$roleid)
- * [$contextpath] = array($roleid=>$roleid)
- * </code>
- *
- * <b>Stale accessdata</b>
- *
- * For the logged-in user, accessdata is long-lived.
- *
- * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
- * context paths affected by changes. Any check at-or-below
- * a dirty context will trigger a transparent reload of accessdata.
- *
- * Changes at the system level will force the reload for everyone.
- *
- * <b>Default role caps</b>
- * The default role assignment is not in the DB, so we
- * add it manually to accessdata.
- *
- * This means that functions that work directly off the
- * DB need to ensure that the default role caps
- * are dealt with appropriately.
- *
- * @package core_access
- * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
- defined('MOODLE_INTERNAL') || die();
- /** No capability change */
- define('CAP_INHERIT', 0);
- /** Allow permission, overrides CAP_PREVENT defined in parent contexts */
- define('CAP_ALLOW', 1);
- /** Prevent permission, overrides CAP_ALLOW defined in parent contexts */
- define('CAP_PREVENT', -1);
- /** Prohibit permission, overrides everything in current and child contexts */
- define('CAP_PROHIBIT', -1000);
- /** System context level - only one instance in every system */
- define('CONTEXT_SYSTEM', 10);
- /** User context level - one instance for each user describing what others can do to user */
- define('CONTEXT_USER', 30);
- /** Course category context level - one instance for each category */
- define('CONTEXT_COURSECAT', 40);
- /** Course context level - one instances for each course */
- define('CONTEXT_COURSE', 50);
- /** Course module context level - one instance for each course module */
- define('CONTEXT_MODULE', 70);
- /**
- * Block context level - one instance for each block, sticky blocks are tricky
- * because ppl think they should be able to override them at lower contexts.
- * Any other context level instance can be parent of block context.
- */
- define('CONTEXT_BLOCK', 80);
- /** Capability allow management of trusts - NOT IMPLEMENTED YET - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
- define('RISK_MANAGETRUST', 0x0001);
- /** Capability allows changes in system configuration - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
- define('RISK_CONFIG', 0x0002);
- /** Capability allows user to add scripted content - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
- define('RISK_XSS', 0x0004);
- /** Capability allows access to personal user information - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
- define('RISK_PERSONAL', 0x0008);
- /** Capability allows users to add content others may see - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
- define('RISK_SPAM', 0x0010);
- /** capability allows mass delete of data belonging to other users - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
- define('RISK_DATALOSS', 0x0020);
- /** rolename displays - the name as defined in the role definition, localised if name empty */
- define('ROLENAME_ORIGINAL', 0);
- /** rolename displays - the name as defined by a role alias at the course level, falls back to ROLENAME_ORIGINAL if alias not present */
- define('ROLENAME_ALIAS', 1);
- /** rolename displays - Both, like this: Role alias (Original) */
- define('ROLENAME_BOTH', 2);
- /** rolename displays - the name as defined in the role definition and the shortname in brackets */
- define('ROLENAME_ORIGINALANDSHORT', 3);
- /** rolename displays - the name as defined by a role alias, in raw form suitable for editing */
- define('ROLENAME_ALIAS_RAW', 4);
- /** rolename displays - the name is simply short role name */
- define('ROLENAME_SHORT', 5);
- if (!defined('CONTEXT_CACHE_MAX_SIZE')) {
- /** maximum size of context cache - it is possible to tweak this config.php or in any script before inclusion of context.php */
- define('CONTEXT_CACHE_MAX_SIZE', 2500);
- }
- /**
- * Although this looks like a global variable, it isn't really.
- *
- * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
- * It is used to cache various bits of data between function calls for performance reasons.
- * Sadly, a PHP global variable is the only way to implement this, without rewriting everything
- * as methods of a class, instead of functions.
- *
- * @access private
- * @global stdClass $ACCESSLIB_PRIVATE
- * @name $ACCESSLIB_PRIVATE
- */
- global $ACCESSLIB_PRIVATE;
- $ACCESSLIB_PRIVATE = new stdClass();
- $ACCESSLIB_PRIVATE->cacheroledefs = array(); // Holds site-wide role definitions.
- $ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache, loaded from DB once per page
- $ACCESSLIB_PRIVATE->dirtyusers = null; // Dirty users cache, loaded from DB once per $USER->id
- $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
- /**
- * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
- *
- * This method should ONLY BE USED BY UNIT TESTS. It clears all of
- * accesslib's private caches. You need to do this before setting up test data,
- * and also at the end of the tests.
- *
- * @access private
- * @return void
- */
- function accesslib_clear_all_caches_for_unit_testing() {
- global $USER;
- if (!PHPUNIT_TEST) {
- throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
- }
- accesslib_clear_all_caches(true);
- accesslib_reset_role_cache();
- unset($USER->access);
- }
- /**
- * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
- *
- * This reset does not touch global $USER.
- *
- * @access private
- * @param bool $resetcontexts
- * @return void
- */
- function accesslib_clear_all_caches($resetcontexts) {
- global $ACCESSLIB_PRIVATE;
- $ACCESSLIB_PRIVATE->dirtycontexts = null;
- $ACCESSLIB_PRIVATE->dirtyusers = null;
- $ACCESSLIB_PRIVATE->accessdatabyuser = array();
- if ($resetcontexts) {
- context_helper::reset_caches();
- }
- }
- /**
- * Full reset of accesslib's private role cache. ONLY TO BE USED FROM THIS LIBRARY FILE!
- *
- * This reset does not touch global $USER.
- *
- * Note: Only use this when the roles that need a refresh are unknown.
- *
- * @see accesslib_clear_role_cache()
- *
- * @access private
- * @return void
- */
- function accesslib_reset_role_cache() {
- global $ACCESSLIB_PRIVATE;
- $ACCESSLIB_PRIVATE->cacheroledefs = array();
- $cache = cache::make('core', 'roledefs');
- $cache->purge();
- }
- /**
- * Clears accesslib's private cache of a specific role or roles. ONLY BE USED FROM THIS LIBRARY FILE!
- *
- * This reset does not touch global $USER.
- *
- * @access private
- * @param int|array $roles
- * @return void
- */
- function accesslib_clear_role_cache($roles) {
- global $ACCESSLIB_PRIVATE;
- if (!is_array($roles)) {
- $roles = [$roles];
- }
- foreach ($roles as $role) {
- if (isset($ACCESSLIB_PRIVATE->cacheroledefs[$role])) {
- unset($ACCESSLIB_PRIVATE->cacheroledefs[$role]);
- }
- }
- $cache = cache::make('core', 'roledefs');
- $cache->delete_many($roles);
- }
- /**
- * Role is assigned at system context.
- *
- * @access private
- * @param int $roleid
- * @return array
- */
- function get_role_access($roleid) {
- $accessdata = get_empty_accessdata();
- $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
- return $accessdata;
- }
- /**
- * Fetch raw "site wide" role definitions.
- * Even MUC static acceleration cache appears a bit slow for this.
- * Important as can be hit hundreds of times per page.
- *
- * @param array $roleids List of role ids to fetch definitions for.
- * @return array Complete definition for each requested role.
- */
- function get_role_definitions(array $roleids) {
- global $ACCESSLIB_PRIVATE;
- if (empty($roleids)) {
- return array();
- }
- // Grab all keys we have not yet got in our static cache.
- if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
- $cache = cache::make('core', 'roledefs');
- foreach ($cache->get_many($uncached) as $roleid => $cachedroledef) {
- if (is_array($cachedroledef)) {
- $ACCESSLIB_PRIVATE->cacheroledefs[$roleid] = $cachedroledef;
- }
- }
- // Check we have the remaining keys from the MUC.
- if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
- $uncached = get_role_definitions_uncached($uncached);
- $ACCESSLIB_PRIVATE->cacheroledefs += $uncached;
- $cache->set_many($uncached);
- }
- }
- // Return just the roles we need.
- return array_intersect_key($ACCESSLIB_PRIVATE->cacheroledefs, array_flip($roleids));
- }
- /**
- * Query raw "site wide" role definitions.
- *
- * @param array $roleids List of role ids to fetch definitions for.
- * @return array Complete definition for each requested role.
- */
- function get_role_definitions_uncached(array $roleids) {
- global $DB;
- if (empty($roleids)) {
- return array();
- }
- // Create a blank results array: even if a role has no capabilities,
- // we need to ensure it is included in the results to show we have
- // loaded all the capabilities that there are.
- $rdefs = array();
- foreach ($roleids as $roleid) {
- $rdefs[$roleid] = array();
- }
- // Load all the capabilities for these roles in all contexts.
- list($sql, $params) = $DB->get_in_or_equal($roleids);
- $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
- FROM {role_capabilities} rc
- JOIN {context} ctx ON rc.contextid = ctx.id
- JOIN {capabilities} cap ON rc.capability = cap.name
- WHERE rc.roleid $sql";
- $rs = $DB->get_recordset_sql($sql, $params);
- // Store the capabilities into the expected data structure.
- foreach ($rs as $rd) {
- if (!isset($rdefs[$rd->roleid][$rd->path])) {
- $rdefs[$rd->roleid][$rd->path] = array();
- }
- $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
- }
- $rs->close();
- // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
- // we process role definitinons in a way that requires we see parent contexts
- // before child contexts. This sort ensures that works (and is faster than
- // sorting in the SQL query).
- foreach ($rdefs as $roleid => $rdef) {
- ksort($rdefs[$roleid]);
- }
- return $rdefs;
- }
- /**
- * Get the default guest role, this is used for guest account,
- * search engine spiders, etc.
- *
- * @return stdClass role record
- */
- function get_guest_role() {
- global $CFG, $DB;
- if (empty($CFG->guestroleid)) {
- if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
- $guestrole = array_shift($roles); // Pick the first one
- set_config('guestroleid', $guestrole->id);
- return $guestrole;
- } else {
- debugging('Can not find any guest role!');
- return false;
- }
- } else {
- if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
- return $guestrole;
- } else {
- // somebody is messing with guest roles, remove incorrect setting and try to find a new one
- set_config('guestroleid', '');
- return get_guest_role();
- }
- }
- }
- /**
- * Check whether a user has a particular capability in a given context.
- *
- * For example:
- * $context = context_module::instance($cm->id);
- * has_capability('mod/forum:replypost', $context)
- *
- * By default checks the capabilities of the current user, but you can pass a
- * different userid. By default will return true for admin users, but you can override that with the fourth argument.
- *
- * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
- * or capabilities with XSS, config or data loss risks.
- *
- * @category access
- *
- * @param string $capability the name of the capability to check. For example mod/forum:view
- * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
- * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
- * @param boolean $doanything If false, ignores effect of admin role assignment
- * @return boolean true if the user has this capability. Otherwise false.
- */
- function has_capability($capability, context $context, $user = null, $doanything = true) {
- global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
- if (during_initial_install()) {
- if ($SCRIPT === "/$CFG->admin/index.php"
- or $SCRIPT === "/$CFG->admin/cli/install.php"
- or $SCRIPT === "/$CFG->admin/cli/install_database.php"
- or (defined('BEHAT_UTIL') and BEHAT_UTIL)
- or (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL)) {
- // we are in an installer - roles can not work yet
- return true;
- } else {
- return false;
- }
- }
- if (strpos($capability, 'moodle/legacy:') === 0) {
- throw new coding_exception('Legacy capabilities can not be used any more!');
- }
- if (!is_bool($doanything)) {
- throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
- }
- // capability must exist
- if (!$capinfo = get_capability_info($capability)) {
- debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
- return false;
- }
- if (!isset($USER->id)) {
- // should never happen
- $USER->id = 0;
- debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
- }
- // make sure there is a real user specified
- if ($user === null) {
- $userid = $USER->id;
- } else {
- $userid = is_object($user) ? $user->id : $user;
- }
- // make sure forcelogin cuts off not-logged-in users if enabled
- if (!empty($CFG->forcelogin) and $userid == 0) {
- return false;
- }
- // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
- if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
- if (isguestuser($userid) or $userid == 0) {
- return false;
- }
- }
- // Check whether context locking is enabled.
- if (!empty($CFG->contextlocking)) {
- if ($capinfo->captype === 'write' && $context->locked) {
- // Context locking applies to any write capability in a locked context.
- // It does not apply to moodle/site:managecontextlocks - this is to allow context locking to be unlocked.
- if ($capinfo->name !== 'moodle/site:managecontextlocks') {
- // It applies to all users who are not site admins.
- // It also applies to site admins when contextlockappliestoadmin is set.
- if (!is_siteadmin($userid) || !empty($CFG->contextlockappliestoadmin)) {
- return false;
- }
- }
- }
- }
- // somehow make sure the user is not deleted and actually exists
- if ($userid != 0) {
- if ($userid == $USER->id and isset($USER->deleted)) {
- // this prevents one query per page, it is a bit of cheating,
- // but hopefully session is terminated properly once user is deleted
- if ($USER->deleted) {
- return false;
- }
- } else {
- if (!context_user::instance($userid, IGNORE_MISSING)) {
- // no user context == invalid userid
- return false;
- }
- }
- }
- // context path/depth must be valid
- if (empty($context->path) or $context->depth == 0) {
- // this should not happen often, each upgrade tries to rebuild the context paths
- debugging('Context id '.$context->id.' does not have valid path, please use context_helper::build_all_paths()');
- if (is_siteadmin($userid)) {
- return true;
- } else {
- return false;
- }
- }
- if (!empty($USER->loginascontext)) {
- // The current user is logged in as another user and can assume their identity at or below the `loginascontext`
- // defined in the USER session.
- // The user may not assume their identity at any other location.
- if (!$USER->loginascontext->is_parent_of($context, true)) {
- // The context being checked is not the specified context, or one of its children.
- return false;
- }
- }
- // Find out if user is admin - it is not possible to override the doanything in any way
- // and it is not possible to switch to admin role either.
- if ($doanything) {
- if (is_siteadmin($userid)) {
- if ($userid != $USER->id) {
- return true;
- }
- // make sure switchrole is not used in this context
- if (empty($USER->access['rsw'])) {
- return true;
- }
- $parts = explode('/', trim($context->path, '/'));
- $path = '';
- $switched = false;
- foreach ($parts as $part) {
- $path .= '/' . $part;
- if (!empty($USER->access['rsw'][$path])) {
- $switched = true;
- break;
- }
- }
- if (!$switched) {
- return true;
- }
- //ok, admin switched role in this context, let's use normal access control rules
- }
- }
- // Careful check for staleness...
- $context->reload_if_dirty();
- if ($USER->id == $userid) {
- if (!isset($USER->access)) {
- load_all_capabilities();
- }
- $access =& $USER->access;
- } else {
- // make sure user accessdata is really loaded
- get_user_accessdata($userid, true);
- $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
- }
- return has_capability_in_accessdata($capability, $context, $access);
- }
- /**
- * Check if the user has any one of several capabilities from a list.
- *
- * This is just a utility method that calls has_capability in a loop. Try to put
- * the capabilities that most users are likely to have first in the list for best
- * performance.
- *
- * @category access
- * @see has_capability()
- *
- * @param array $capabilities an array of capability names.
- * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
- * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
- * @param boolean $doanything If false, ignore effect of admin role assignment
- * @return boolean true if the user has any of these capabilities. Otherwise false.
- */
- function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
- foreach ($capabilities as $capability) {
- if (has_capability($capability, $context, $user, $doanything)) {
- return true;
- }
- }
- return false;
- }
- /**
- * Check if the user has all the capabilities in a list.
- *
- * This is just a utility method that calls has_capability in a loop. Try to put
- * the capabilities that fewest users are likely to have first in the list for best
- * performance.
- *
- * @category access
- * @see has_capability()
- *
- * @param array $capabilities an array of capability names.
- * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
- * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
- * @param boolean $doanything If false, ignore effect of admin role assignment
- * @return boolean true if the user has all of these capabilities. Otherwise false.
- */
- function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
- foreach ($capabilities as $capability) {
- if (!has_capability($capability, $context, $user, $doanything)) {
- return false;
- }
- }
- return true;
- }
- /**
- * Is course creator going to have capability in a new course?
- *
- * This is intended to be used in enrolment plugins before or during course creation,
- * do not use after the course is fully created.
- *
- * @category access
- *
- * @param string $capability the name of the capability to check.
- * @param context $context course or category context where is course going to be created
- * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
- * @return boolean true if the user will have this capability.
- *
- * @throws coding_exception if different type of context submitted
- */
- function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
- global $CFG;
- if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
- throw new coding_exception('Only course or course category context expected');
- }
- if (has_capability($capability, $context, $user)) {
- // User already has the capability, it could be only removed if CAP_PROHIBIT
- // was involved here, but we ignore that.
- return true;
- }
- if (!has_capability('moodle/course:create', $context, $user)) {
- return false;
- }
- if (!enrol_is_enabled('manual')) {
- return false;
- }
- if (empty($CFG->creatornewroleid)) {
- return false;
- }
- if ($context->contextlevel == CONTEXT_COURSE) {
- if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
- return false;
- }
- } else {
- if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
- return false;
- }
- }
- // Most likely they will be enrolled after the course creation is finished,
- // does the new role have the required capability?
- list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
- return isset($neededroles[$CFG->creatornewroleid]);
- }
- /**
- * Check if the user is an admin at the site level.
- *
- * Please note that use of proper capabilities is always encouraged,
- * this function is supposed to be used from core or for temporary hacks.
- *
- * @category access
- *
- * @param int|stdClass $user_or_id user id or user object
- * @return bool true if user is one of the administrators, false otherwise
- */
- function is_siteadmin($user_or_id = null) {
- global $CFG, $USER;
- if ($user_or_id === null) {
- $user_or_id = $USER;
- }
- if (empty($user_or_id)) {
- return false;
- }
- if (!empty($user_or_id->id)) {
- $userid = $user_or_id->id;
- } else {
- $userid = $user_or_id;
- }
- // Because this script is called many times (150+ for course page) with
- // the same parameters, it is worth doing minor optimisations. This static
- // cache stores the value for a single userid, saving about 2ms from course
- // page load time without using significant memory. As the static cache
- // also includes the value it depends on, this cannot break unit tests.
- static $knownid, $knownresult, $knownsiteadmins;
- if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
- return $knownresult;
- }
- $knownid = $userid;
- $knownsiteadmins = $CFG->siteadmins;
- $siteadmins = explode(',', $CFG->siteadmins);
- $knownresult = in_array($userid, $siteadmins);
- return $knownresult;
- }
- /**
- * Returns true if user has at least one role assign
- * of 'coursecontact' role (is potentially listed in some course descriptions).
- *
- * @param int $userid
- * @return bool
- */
- function has_coursecontact_role($userid) {
- global $DB, $CFG;
- if (empty($CFG->coursecontact)) {
- return false;
- }
- $sql = "SELECT 1
- FROM {role_assignments}
- WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
- return $DB->record_exists_sql($sql, array('userid'=>$userid));
- }
- /**
- * Does the user have a capability to do something?
- *
- * Walk the accessdata array and return true/false.
- * Deals with prohibits, role switching, aggregating
- * capabilities, etc.
- *
- * The main feature of here is being FAST and with no
- * side effects.
- *
- * Notes:
- *
- * Switch Role merges with default role
- * ------------------------------------
- * If you are a teacher in course X, you have at least
- * teacher-in-X + defaultloggedinuser-sitewide. So in the
- * course you'll have techer+defaultloggedinuser.
- * We try to mimic that in switchrole.
- *
- * Permission evaluation
- * ---------------------
- * Originally there was an extremely complicated way
- * to determine the user access that dealt with
- * "locality" or role assignments and role overrides.
- * Now we simply evaluate access for each role separately
- * and then verify if user has at least one role with allow
- * and at the same time no role with prohibit.
- *
- * @access private
- * @param string $capability
- * @param context $context
- * @param array $accessdata
- * @return bool
- */
- function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
- global $CFG;
- // Build $paths as a list of current + all parent "paths" with order bottom-to-top
- $path = $context->path;
- $paths = array($path);
- while ($path = rtrim($path, '0123456789')) {
- $path = rtrim($path, '/');
- if ($path === '') {
- break;
- }
- $paths[] = $path;
- }
- $roles = array();
- $switchedrole = false;
- // Find out if role switched
- if (!empty($accessdata['rsw'])) {
- // From the bottom up...
- foreach ($paths as $path) {
- if (isset($accessdata['rsw'][$path])) {
- // Found a switchrole assignment - check for that role _plus_ the default user role
- $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
- $switchedrole = true;
- break;
- }
- }
- }
- if (!$switchedrole) {
- // get all users roles in this context and above
- foreach ($paths as $path) {
- if (isset($accessdata['ra'][$path])) {
- foreach ($accessdata['ra'][$path] as $roleid) {
- $roles[$roleid] = null;
- }
- }
- }
- }
- // Now find out what access is given to each role, going bottom-->up direction
- $rdefs = get_role_definitions(array_keys($roles));
- $allowed = false;
- foreach ($roles as $roleid => $ignored) {
- foreach ($paths as $path) {
- if (isset($rdefs[$roleid][$path][$capability])) {
- $perm = (int)$rdefs[$roleid][$path][$capability];
- if ($perm === CAP_PROHIBIT) {
- // any CAP_PROHIBIT found means no permission for the user
- return false;
- }
- if (is_null($roles[$roleid])) {
- $roles[$roleid] = $perm;
- }
- }
- }
- // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
- $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
- }
- return $allowed;
- }
- /**
- * A convenience function that tests has_capability, and displays an error if
- * the user does not have that capability.
- *
- * NOTE before Moodle 2.0, this function attempted to make an appropriate
- * require_login call before checking the capability. This is no longer the case.
- * You must call require_login (or one of its variants) if you want to check the
- * user is logged in, before you call this function.
- *
- * @see has_capability()
- *
- * @param string $capability the name of the capability to check. For example mod/forum:view
- * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
- * @param int $userid A user id. By default (null) checks the permissions of the current user.
- * @param bool $doanything If false, ignore effect of admin role assignment
- * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
- * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
- * @return void terminates with an error if the user does not have the given capability.
- */
- function require_capability($capability, context $context, $userid = null, $doanything = true,
- $errormessage = 'nopermissions', $stringfile = '') {
- if (!has_capability($capability, $context, $userid, $doanything)) {
- throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
- }
- }
- /**
- * A convenience function that tests has_capability for a list of capabilities, and displays an error if
- * the user does not have that capability.
- *
- * This is just a utility method that calls has_capability in a loop. Try to put
- * the capabilities that fewest users are likely to have first in the list for best
- * performance.
- *
- * @category access
- * @see has_capability()
- *
- * @param array $capabilities an array of capability names.
- * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
- * @param int $userid A user id. By default (null) checks the permissions of the current user.
- * @param bool $doanything If false, ignore effect of admin role assignment
- * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
- * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
- * @return void terminates with an error if the user does not have the given capability.
- */
- function require_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true,
- $errormessage = 'nopermissions', $stringfile = ''): void {
- foreach ($capabilities as $capability) {
- if (!has_capability($capability, $context, $userid, $doanything)) {
- throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
- }
- }
- }
- /**
- * Return a nested array showing all role assignments for the user.
- * [ra] => [contextpath][roleid] = roleid
- *
- * @access private
- * @param int $userid - the id of the user
- * @return array access info array
- */
- function get_user_roles_sitewide_accessdata($userid) {
- global $CFG, $DB;
- $accessdata = get_empty_accessdata();
- // start with the default role
- if (!empty($CFG->defaultuserroleid)) {
- $syscontext = context_system::instance();
- $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
- }
- // load the "default frontpage role"
- if (!empty($CFG->defaultfrontpageroleid)) {
- $frontpagecontext = context_course::instance(get_site()->id);
- if ($frontpagecontext->path) {
- $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
- }
- }
- // Preload every assigned role.
- $sql = "SELECT ctx.path, ra.roleid, ra.contextid
- FROM {role_assignments} ra
- JOIN {context} ctx ON ctx.id = ra.contextid
- WHERE ra.userid = :userid";
- $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
- foreach ($rs as $ra) {
- // RAs leafs are arrays to support multi-role assignments...
- $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
- }
- $rs->close();
- return $accessdata;
- }
- /**
- * Returns empty accessdata structure.
- *
- * @access private
- * @return array empt accessdata
- */
- function get_empty_accessdata() {
- $accessdata = array(); // named list
- $accessdata['ra'] = array();
- $accessdata['time'] = time();
- $accessdata['rsw'] = array();
- return $accessdata;
- }
- /**
- * Get accessdata for a given user.
- *
- * @access private
- * @param int $userid
- * @param bool $preloadonly true means do not return access array
- * @return array accessdata
- */
- function get_user_accessdata($userid, $preloadonly=false) {
- global $CFG, $ACCESSLIB_PRIVATE, $USER;
- if (isset($USER->access)) {
- $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
- }
- if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
- if (empty($userid)) {
- if (!empty($CFG->notloggedinroleid)) {
- $accessdata = get_role_access($CFG->notloggedinroleid);
- } else {
- // weird
- return get_empty_accessdata();
- }
- } else if (isguestuser($userid)) {
- if ($guestrole = get_guest_role()) {
- $accessdata = get_role_access($guestrole->id);
- } else {
- //weird
- return get_empty_accessdata();
- }
- } else {
- // Includes default role and frontpage role.
- $accessdata = get_user_roles_sitewide_accessdata($userid);
- }
- $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
- }
- if ($preloadonly) {
- return;
- } else {
- return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
- }
- }
- /**
- * A convenience function to completely load all the capabilities
- * for the current user. It is called from has_capability() and functions change permissions.
- *
- * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
- * @see check_enrolment_plugins()
- *
- * @access private
- * @return void
- */
- function load_all_capabilities() {
- global $USER;
- // roles not installed yet - we are in the middle of installation
- if (during_initial_install()) {
- return;
- }
- if (!isset($USER->id)) {
- // this should not happen
- $USER->id = 0;
- }
- unset($USER->access);
- $USER->access = get_user_accessdata($USER->id);
- // Clear to force a refresh
- unset($USER->mycourses);
- // init/reset internal enrol caches - active course enrolments and temp access
- $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
- }
- /**
- * A convenience function to completely reload all the capabilities
- * for the current user when roles have been updated in a relevant
- * context -- but PRESERVING switchroles and loginas.
- * This function resets all accesslib and context caches.
- *
- * That is - completely transparent to the user.
- *
- * Note: reloads $USER->access completely.
- *
- * @access private
- * @return void
- */
- function reload_all_capabilities() {
- global $USER, $DB, $ACCESSLIB_PRIVATE;
- // copy switchroles
- $sw = array();
- if (!empty($USER->access['rsw'])) {
- $sw = $USER->access['rsw'];
- }
- accesslib_clear_all_caches(true);
- unset($USER->access);
- // Prevent dirty flags refetching on this page.
- $ACCESSLIB_PRIVATE->dirtycontexts = array();
- $ACCESSLIB_PRIVATE->dirtyusers = array($USER->id => false);
- load_all_capabilities();
- foreach ($sw as $path => $roleid) {
- if ($record = $DB->get_record('context', array('path'=>$path))) {
- $context = context::instance_by_id($record->id);
- if (has_capability('moodle/role:switchroles', $context)) {
- role_switch($roleid, $context);
- }
- }
- }
- }
- /**
- * Adds a temp role to current USER->access array.
- *
- * Useful for the "temporary guest" access we grant to logged-in users.
- * This is useful for enrol plugins only.
- *
- * @since Moodle 2.2
- * @param context_course $coursecontext
- * @param int $roleid
- * @return void
- */
- function load_temp_course_role(context_course $coursecontext, $roleid) {
- global $USER, $SITE;
- if (empty($roleid)) {
- debugging('invalid role specified in load_temp_course_role()');
- return;
- }
- if ($coursecontext->instanceid == $SITE->id) {
- debugging('Can not use temp roles on the frontpage');
- return;
- }
- if (!isset($USER->access)) {
- load_all_capabilities();
- }
- $coursecontext->reload_if_dirty();
- if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
- return;
- }
- $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
- }
- /**
- * Removes any extra guest roles from current USER->access array.
- * This is useful for enrol plugins only.
- *
- * @since Moodle 2.2
- * @param context_course $coursecontext
- * @return void
- */
- function remove_temp_course_roles(context_course $coursecontext) {
- global $DB, $USER, $SITE;
- if ($coursecontext->instanceid == $SITE->id) {
- debugging('Can not use temp roles on the frontpage');
- return;
- }
- if (empty($USER->access['ra'][$coursecontext->path])) {
- //no roles here, weird
- return;
- }
- $sql = "SELECT DISTINCT ra.roleid AS id
- FROM {role_assignments} ra
- WHERE ra.contextid = :contextid AND ra.userid = :userid";
- $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
- $USER->access['ra'][$coursecontext->path] = array();
- foreach ($ras as $r) {
- $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
- }
- }
- /**
- * Returns array of all role archetypes.
- *
- * @return array
- */
- function get_role_archetypes() {
- return array(
- 'manager' => 'manager',
- 'coursecreator' => 'coursecreator',
- 'editingteacher' => 'editingteacher',
- 'teacher' => 'teacher',
- 'student' => 'student',
- 'guest' => 'guest',
- 'user' => 'user',
- 'frontpage' => 'frontpage'
- );
- }
- /**
- * Assign the defaults found in this capability definition to roles that have
- * the corresponding legacy capabilities assigned to them.
- *
- * @param string $capability
- * @param array $legacyperms an array in the format (example):
- * 'guest' => CAP_PREVENT,
- * 'student' => CAP_ALLOW,
- * 'teacher' => CAP_ALLOW,
- * 'editingteacher' => CAP_ALLOW,
- * 'coursecreator' => CAP_ALLOW,
- * 'manager' => CAP_ALLOW
- * @return boolean success or failure.
- */
- function assign_legacy_capabilities($capability, $legacyperms) {
- $archetypes = get_role_archetypes();
- foreach ($legacyperms as $type => $perm) {
- $systemcontext = context_system::instance();
- if ($type === 'admin') {
- debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
- $type = 'manager';
- }
- if (!array_key_exists($type, $archetypes)) {
- print_error('invalidlegacy', '', '', $type);
- }
- if ($roles = get_archetype_roles($type)) {
- foreach ($roles as $role) {
- // Assign a site level capability.
- if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
- return false;
- }
- }
- }
- }
- return true;
- }
- /**
- * Verify capability risks.
- *
- * @param stdClass $capability a capability - a row from the capabilities table.
- * @return boolean whether this capability is safe - that is, whether people with the
- * safeoverrides capability should be allowed to change it.
- */
- function is_safe_capability($capability) {
- return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
- }
- /**
- * Get the local override (if any) for a given capability in a role in a context
- *
- * @param int $roleid
- * @param int $contextid
- * @param string $capability
- * @return stdClass local capability override
- */
- function get_local_override($roleid, $contextid, $capability) {
- global $DB;
- return $DB->get_record_sql("
- SELECT rc.*
- FROM {role_capabilities} rc
- JOIN {capability} cap ON rc.capability = cap.name
- WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
- 'roleid' => $roleid,
- 'contextid' => $contextid,
- 'capability' => $capability,
- ]);
- }
- /**
- * Returns context instance plus related course and cm instances
- *
- * @param int $contextid
- * @return array of ($context, $course, $cm)
- */
- function get_context_info_array($contextid) {
- global $DB;
- $context = context::instance_by_id($contextid, MUST_EXIST);
- $course = null;
- $cm = null;
- if ($context->contextlevel == CONTEXT_COURSE) {
- $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
- } else if ($context->contextlevel == CONTEXT_MODULE) {
- $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
- $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
- } else if ($context->contextlevel == CONTEXT_BLOCK) {
- $parent = $context->get_parent_context();
- if ($parent->contextlevel == CONTEXT_COURSE) {
- $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
- } else if ($parent->contextlevel == CONTEXT_MODULE) {
- $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
- $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
- }
- }
- return array($context, $course, $cm);
- }
- /**
- * Function that creates a role
- *
- * @param string $name role name
- * @param string $shortname role short name
- * @param string $description role description
- * @param string $archetype
- * @return int id or dml_exception
- */
- function create_role($name, $shortname, $description, $archetype = '') {
- global $DB;
- if (strpos($archetype, 'moodle/legacy:') !== false) {
- throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
- }
- // verify role archetype actually exists
- $archetypes = get_role_archetypes();
- if (empty($archetypes[$archetype])) {
- $archetype = '';
- }
- // Insert the role record.
- $role = new stdClass();
- $role->name = $name;
- $role->shortname = $shortname;
- $role->description = $description;
- $role->archetype = $archetype;
- //find free sortorder number
- $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
- if (empty($role->sortorder)) {
- $role->sortorder = 1;
- }
- $id = $DB->insert_record('role', $role);
- return $id;
- }
- /**
- * Function that deletes a role and cleanups up after it
- *
- * @param int $roleid id of role to delete
- * @return bool always true
- */
- function delete_role($roleid) {
- global $DB;
- // first unssign all users
- role_unassign_all(array('roleid'=>$roleid));
- // cleanup all references to this role, ignore errors
- $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
- $DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
- $DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
- $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
- $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
- $DB->delete_records('role_names', array('roleid'=>$roleid));
- $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
- // Get role record before it's deleted.
- $role = $DB->get_record('role', array('id'=>$roleid));
- // Finally delete the role itself.
- $DB->delete_records('role', array('id'=>$roleid));
- // Trigger event.
- $event = \core\event\role_deleted::create(
- array(
- 'context' => context_system::instance(),
- 'objectid' => $roleid,
- 'other' =>
- array(
- 'shortname' => $role->shortname,
- 'description' => $role->description,
- 'archetype' => $role->archetype
- )
- )
- );
- $event->add_record_snapshot('role', $role);
- $event->trigger();
- // Reset any cache of this role, including MUC.
- accesslib_clear_role_cache($roleid);
- return true;
- }
- /**
- * Function to write context specific overrides, or default capabilities.
- *
- * @param string $capability string name
- * @param int $permission CAP_ constants
- * @param int $roleid role id
- * @param int|context $contextid context id
- * @param bool $overwrite
- * @return bool always true or exception
- */
- function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
- global $USER, $DB;
- if ($contextid instanceof context) {
- $context = $contextid;
- } else {
- $context = context::instance_by_id($contextid);
- }
- // Capability must exist.
- if (!$capinfo = get_capability_info($capability)) {
- throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
- }
- if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
- unassign_capability($capability, $roleid, $context->id);
- return true;
- }
- $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
- if ($existing and !$overwrite) { // We want to keep whatever is there already
- return true;
- }
- $cap = new stdClass();
- $cap->contextid = $context->id;
- $cap->roleid = $roleid;
- $cap->capability = $capability;
- $cap->permission = $permission;
- $cap->timemodified = time();
- $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
- if ($existing) {
- $cap->id = $existing->id;
- $DB->update_record('role_capabilities', $cap);
- } else {
- if ($DB->record_exists('context', array('id'=>$context->id))) {
- $DB->insert_record('role_capabilities', $cap);
- }
- }
- // Trigger capability_assigned event.
- \core\event\capability_assigned::create([
- 'userid' => $cap->modifierid,
- 'context' => $context,
- 'objectid' => $roleid,
- 'other' => [
- 'capability' => $capability,
- 'oldpermission' => $existing->permission ?? CAP_INHERIT,
- 'permission' => $permission
- ]
- ])->trigger();
- // Reset any cache of this role, including MUC.
- accesslib_clear_role_cache($roleid);
- return true;
- }
- /**
- * Unassign a capability from a role.
- *
- * @param string $capability the name of the capability
- * @param int $roleid the role id
- * @param int|context $contextid null means all contexts
- * @return boolean true or exception
- */
- function unassign_capability($capability, $roleid, $contextid = null) {
- glob…
Large files files are truncated, but you can click here to view the full file