/lib/accesslib.php
PHP | 6291 lines | 3637 code | 755 blank | 1899 comment | 749 complexity | 8ce3d4a97f29057435ac570bd95d5440 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, BSD-3-Clause
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
- * - get_context_instance()
- * - get_context_instance_by_id()
- * - get_parent_contexts()
- * - get_child_contexts()
- *
- * Whether the user can do something...
- * - has_capability()
- * - has_any_capability()
- * - has_all_capabilities()
- * - require_capability()
- * - require_login() (from moodlelib)
- *
- * What courses has this user access to?
- * - get_user_courses_bycap()
- *
- * What users can do X in this context?
- * - get_users_by_capability()
- *
- * Enrol/unenrol
- * - enrol_into_course()
- * - role_assign()/role_unassign()
- *
- *
- * Advanced use
- * - load_all_capabilities()
- * - reload_all_capabilities()
- * - has_capability_in_accessdata()
- * - is_siteadmin()
- * - get_user_access_sitewide()
- * - load_subcontext()
- * - get_role_access_bycontext()
- *
- * <b>Name conventions</b>
- *
- * "ctx" means context
- *
- * <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-capabilities-perm sets
- * (role defs) and a list of courses we have loaded
- * data for.
- *
- * 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)
- * [$contextpath]= array($roleid)
- * [$contextpath]= array($roleid)
- * </code>
- *
- * Role definitions are stored like this
- * (no cap merge is done - so it's compact)
- *
- * <code>
- * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
- * [mod/forum:editallpost] = -1
- * [mod/forum:startdiscussion] = -1000
- * </code>
- *
- * See how has_capability_in_accessdata() walks up/down the tree.
- *
- * Normally - specially for the logged-in user, we only load
- * rdef and ra down to the course level, but not below. This
- * keeps accessdata small and compact. Below-the-course ra/rdef
- * are loaded as needed. We keep track of which courses we
- * have loaded ra/rdef in
- * <code>
- * $accessdata[loaded] = array($contextpath, $contextpath)
- * </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
- * @subpackage role
- * @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();
- /** permission definitions */
- define('CAP_INHERIT', 0);
- /** permission definitions */
- define('CAP_ALLOW', 1);
- /** permission definitions */
- define('CAP_PREVENT', -1);
- /** permission definitions */
- define('CAP_PROHIBIT', -1000);
- /** context definitions */
- define('CONTEXT_SYSTEM', 10);
- /** context definitions */
- define('CONTEXT_USER', 30);
- /** context definitions */
- define('CONTEXT_COURSECAT', 40);
- /** context definitions */
- define('CONTEXT_COURSE', 50);
- /** context definitions */
- define('CONTEXT_MODULE', 70);
- /** context definitions */
- define('CONTEXT_BLOCK', 80);
- /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
- define('RISK_MANAGETRUST', 0x0001);
- /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
- define('RISK_CONFIG', 0x0002);
- /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
- define('RISK_XSS', 0x0004);
- /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
- define('RISK_PERSONAL', 0x0008);
- /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
- define('RISK_SPAM', 0x0010);
- /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
- define('RISK_DATALOSS', 0x0020);
- /** rolename displays - the name as defined in the role definition */
- define('ROLENAME_ORIGINAL', 0);
- /** rolename displays - the name as defined by a role alias */
- 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);
- /**
- * Internal class provides a cache of context information. The cache is
- * restricted in size.
- *
- * This cache should NOT be used outside accesslib.php!
- *
- * @private
- * @author Sam Marshall
- */
- class context_cache {
- private $contextsbyid;
- private $contexts;
- private $count;
- /**
- * @var int Maximum number of contexts that will be cached.
- */
- const MAX_SIZE = 2500;
- /**
- * @var int Once contexts reach maximum number, this many will be removed from cache.
- */
- const REDUCE_SIZE = 1000;
- /**
- * Initialises (empty)
- */
- public function __construct() {
- $this->reset();
- }
- /**
- * Resets the cache to remove all data.
- */
- public function reset() {
- $this->contexts = array();
- $this->contextsbyid = array();
- $this->count = 0;
- }
- /**
- * Adds a context to the cache. If the cache is full, discards a batch of
- * older entries.
- * @param stdClass $context New context to add
- */
- public function add(stdClass $context) {
- if ($this->count >= self::MAX_SIZE) {
- for ($i=0; $i<self::REDUCE_SIZE; $i++) {
- if ($first = reset($this->contextsbyid)) {
- unset($this->contextsbyid[$first->id]);
- unset($this->contexts[$first->contextlevel][$first->instanceid]);
- }
- }
- $this->count -= self::REDUCE_SIZE;
- if ($this->count < 0) {
- // most probably caused by the drift, the reset() above
- // might have returned false because there might not be any more elements
- $this->count = 0;
- }
- }
- $this->contexts[$context->contextlevel][$context->instanceid] = $context;
- $this->contextsbyid[$context->id] = $context;
- // Note the count may get out of synch slightly if you cache a context
- // that is already cached, but it doesn't really matter much and I
- // didn't think it was worth the performance hit.
- $this->count++;
- }
- /**
- * Removes a context from the cache.
- * @param stdClass $context Context object to remove (must include fields
- * ->id, ->contextlevel, ->instanceid at least)
- */
- public function remove(stdClass $context) {
- unset($this->contexts[$context->contextlevel][$context->instanceid]);
- unset($this->contextsbyid[$context->id]);
- // Again the count may get a bit out of synch if you remove things
- // that don't exist
- $this->count--;
- if ($this->count < 0) {
- $this->count = 0;
- }
- }
- /**
- * Gets a context from the cache.
- * @param int $contextlevel Context level
- * @param int $instance Instance ID
- * @return stdClass|bool Context or false if not in cache
- */
- public function get($contextlevel, $instance) {
- if (isset($this->contexts[$contextlevel][$instance])) {
- return $this->contexts[$contextlevel][$instance];
- }
- return false;
- }
- /**
- * Gets a context from the cache based on its id.
- * @param int $id Context ID
- * @return stdClass|bool Context or false if not in cache
- */
- public function get_by_id($id) {
- if (isset($this->contextsbyid[$id])) {
- return $this->contextsbyid[$id];
- }
- return false;
- }
- /**
- * @return int Count of contexts in cache (approximately)
- */
- public function get_approx_count() {
- return $this->count;
- }
- }
- /**
- * 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.
- *
- * @global stdClass $ACCESSLIB_PRIVATE
- * @name $ACCESSLIB_PRIVATE
- */
- global $ACCESSLIB_PRIVATE;
- $ACCESSLIB_PRIVATE = new stdClass();
- $ACCESSLIB_PRIVATE->contexcache = new context_cache();
- $ACCESSLIB_PRIVATE->systemcontext = null; // Used in get_system_context
- $ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache
- $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the $accessdata structure for users other than $USER
- $ACCESSLIB_PRIVATE->roledefinitions = array(); // role definitions cache - helps a lot with mem usage in cron
- $ACCESSLIB_PRIVATE->croncache = array(); // Used in get_role_access
- $ACCESSLIB_PRIVATE->preloadedcourses = array(); // Used in preload_course_contexts.
- $ACCESSLIB_PRIVATE->capabilities = null; // detailed information about the capabilities
- /**
- * 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.
- */
- function accesslib_clear_all_caches_for_unit_testing() {
- global $UNITTEST, $USER, $ACCESSLIB_PRIVATE;
- if (empty($UNITTEST->running)) {
- throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
- }
- $ACCESSLIB_PRIVATE->contexcache = new context_cache();
- $ACCESSLIB_PRIVATE->systemcontext = null;
- $ACCESSLIB_PRIVATE->dirtycontexts = null;
- $ACCESSLIB_PRIVATE->accessdatabyuser = array();
- $ACCESSLIB_PRIVATE->roledefinitions = array();
- $ACCESSLIB_PRIVATE->croncache = array();
- $ACCESSLIB_PRIVATE->preloadedcourses = array();
- $ACCESSLIB_PRIVATE->capabilities = null;
- unset($USER->access);
- }
- /**
- * This is really slow!!! do not use above course context level
- *
- * @param int $roleid
- * @param object $context
- * @return array
- */
- function get_role_context_caps($roleid, $context) {
- global $DB;
- //this is really slow!!!! - do not use above course context level!
- $result = array();
- $result[$context->id] = array();
- // first emulate the parent context capabilities merging into context
- $searchcontexts = array_reverse(get_parent_contexts($context));
- array_push($searchcontexts, $context->id);
- foreach ($searchcontexts as $cid) {
- if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
- foreach ($capabilities as $cap) {
- if (!array_key_exists($cap->capability, $result[$context->id])) {
- $result[$context->id][$cap->capability] = 0;
- }
- $result[$context->id][$cap->capability] += $cap->permission;
- }
- }
- }
- // now go through the contexts bellow given context
- $searchcontexts = array_keys(get_child_contexts($context));
- foreach ($searchcontexts as $cid) {
- if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
- foreach ($capabilities as $cap) {
- if (!array_key_exists($cap->contextid, $result)) {
- $result[$cap->contextid] = array();
- }
- $result[$cap->contextid][$cap->capability] = $cap->permission;
- }
- }
- }
- return $result;
- }
- /**
- * Gets the accessdata for role "sitewide" (system down to course)
- *
- * @param int $roleid
- * @param array $accessdata defaults to null
- * @return array
- */
- function get_role_access($roleid, $accessdata = null) {
- global $CFG, $DB;
- /* Get it in 1 cheap DB query...
- * - relevant role caps at the root and down
- * to the course level - but not below
- */
- if (is_null($accessdata)) {
- $accessdata = array(); // named list
- $accessdata['ra'] = array();
- $accessdata['rdef'] = array();
- $accessdata['loaded'] = array();
- }
- //
- // Overrides for the role IN ANY CONTEXTS
- // down to COURSE - not below -
- //
- $sql = "SELECT ctx.path,
- rc.capability, rc.permission
- FROM {context} ctx
- JOIN {role_capabilities} rc
- ON rc.contextid=ctx.id
- WHERE rc.roleid = ?
- AND ctx.contextlevel <= ".CONTEXT_COURSE."
- ORDER BY ctx.depth, ctx.path";
- $params = array($roleid);
- // we need extra caching in CLI scripts and cron
- if (CLI_SCRIPT) {
- global $ACCESSLIB_PRIVATE;
- if (!isset($ACCESSLIB_PRIVATE->croncache[$roleid])) {
- $ACCESSLIB_PRIVATE->croncache[$roleid] = array();
- $rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $rd) {
- $ACCESSLIB_PRIVATE->croncache[$roleid][] = $rd;
- }
- $rs->close();
- }
- foreach ($ACCESSLIB_PRIVATE->croncache[$roleid] as $rd) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- } else {
- $rs = $DB->get_recordset_sql($sql, $params);
- if ($rs->valid()) {
- foreach ($rs as $rd) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- }
- $rs->close();
- }
- return $accessdata;
- }
- /**
- * Gets the accessdata for role "sitewide" (system down to course)
- *
- * @param int $roleid
- * @param array $accessdata defaults to null
- * @return array
- */
- function get_default_frontpage_role_access($roleid, $accessdata = null) {
- global $CFG, $DB;
- $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
- $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
- //
- // Overrides for the role in any contexts related to the course
- //
- $sql = "SELECT ctx.path,
- rc.capability, rc.permission
- FROM {context} ctx
- JOIN {role_capabilities} rc
- ON rc.contextid=ctx.id
- WHERE rc.roleid = ?
- AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?)
- AND ctx.contextlevel <= ".CONTEXT_COURSE."
- ORDER BY ctx.depth, ctx.path";
- $params = array($roleid, "$base/%");
- $rs = $DB->get_recordset_sql($sql, $params);
- if ($rs->valid()) {
- foreach ($rs as $rd) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- }
- $rs->close();
- return $accessdata;
- }
- /**
- * Get the default guest role
- *
- * @return stdClass role
- */
- 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 = get_context_instance(CONTEXT_MODULE, $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.
- *
- * @param string $capability the name of the capability to check. For example mod/forum:view
- * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
- * @param integer|object $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, $user = null, $doanything = true) {
- global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE;
- if (during_initial_install()) {
- if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") {
- // 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!');
- }
- // the original $CONTEXT here was hiding serious errors
- // for security reasons do not reuse previous context
- if (empty($context)) {
- debugging('Incorrect context specified');
- return false;
- }
- if (!is_bool($doanything)) {
- throw new coding_exception('Capability parameter "doanything" is wierd ("'.$doanything.'"). This has to be fixed in code.');
- }
- // make sure there is a real user specified
- if ($user === null) {
- $userid = !empty($USER->id) ? $USER->id : 0;
- } else {
- $userid = !empty($user->id) ? $user->id : $user;
- }
- // capability must exist
- if (!$capinfo = get_capability_info($capability)) {
- debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
- 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 ((int)$capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
- if (isguestuser($userid) or $userid == 0) {
- return false;
- }
- }
- if (is_null($context->path) or $context->depth == 0) {
- //this should not happen
- $contexts = array(SYSCONTEXTID, $context->id);
- $context->path = '/'.SYSCONTEXTID.'/'.$context->id;
- debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);
- } else {
- $contexts = explode('/', $context->path);
- array_shift($contexts);
- }
- if (CLI_SCRIPT && !isset($USER->access)) {
- // In cron, some modules setup a 'fake' $USER,
- // ensure we load the appropriate accessdata.
- if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
- $ACCESSLIB_PRIVATE->dirtycontexts = null; //load fresh dirty contexts
- } else {
- load_user_accessdata($userid);
- $ACCESSLIB_PRIVATE->dirtycontexts = array();
- }
- $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
- } else if (isset($USER->id) && ($USER->id == $userid) && !isset($USER->access)) {
- // caps not loaded yet - better to load them to keep BC with 1.8
- // not-logged-in user or $USER object set up manually first time here
- load_all_capabilities();
- $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now
- $ACCESSLIB_PRIVATE->roledefinitions = array();
- }
- // Load dirty contexts list if needed
- if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
- if (isset($USER->access['time'])) {
- $ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']);
- }
- else {
- $ACCESSLIB_PRIVATE->dirtycontexts = array();
- }
- }
- // Careful check for staleness...
- if (count($ACCESSLIB_PRIVATE->dirtycontexts) !== 0 and is_contextpath_dirty($contexts, $ACCESSLIB_PRIVATE->dirtycontexts)) {
- // reload all capabilities - preserving loginas, roleswitches, etc
- // and then cleanup any marks of dirtyness... at least from our short
- // term memory! :-)
- $ACCESSLIB_PRIVATE->accessdatabyuser = array();
- $ACCESSLIB_PRIVATE->roledefinitions = array();
- if (CLI_SCRIPT) {
- load_user_accessdata($userid);
- $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
- $ACCESSLIB_PRIVATE->dirtycontexts = array();
- } else {
- reload_all_capabilities();
- }
- }
- // 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
- }
- }
- // divulge how many times we are called
- //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
- if (isset($USER->id) && ($USER->id == $userid)) { // we must accept strings and integers in $userid
- //
- // For the logged in user, we have $USER->access
- // which will have all RAs and caps preloaded for
- // course and above contexts.
- //
- // Contexts below courses && contexts that do not
- // hang from courses are loaded into $USER->access
- // on demand, and listed in $USER->access[loaded]
- //
- if ($context->contextlevel <= CONTEXT_COURSE) {
- // Course and above are always preloaded
- return has_capability_in_accessdata($capability, $context, $USER->access);
- }
- // Load accessdata for below-the-course contexts
- if (!path_inaccessdata($context->path,$USER->access)) {
- // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
- // $bt = debug_backtrace();
- // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
- load_subcontext($USER->id, $context, $USER->access);
- }
- return has_capability_in_accessdata($capability, $context, $USER->access);
- }
- if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
- load_user_accessdata($userid);
- }
- if ($context->contextlevel <= CONTEXT_COURSE) {
- // Course and above are always preloaded
- return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
- }
- // Load accessdata for below-the-course contexts as needed
- if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
- // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
- // $bt = debug_backtrace();
- // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
- load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
- }
- return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
- }
- /**
- * 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.
- *
- * There are probably tricks that could be done to improve the performance here, for example,
- * check the capabilities that are already cached first.
- *
- * @see has_capability()
- * @param array $capabilities an array of capability names.
- * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
- * @param integer $userid A user id. 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($capabilities, $context, $userid = null, $doanything = true) {
- if (!is_array($capabilities)) {
- debugging('Incorrect $capabilities parameter in has_any_capabilities() call - must be an array');
- return false;
- }
- foreach ($capabilities as $capability) {
- if (has_capability($capability, $context, $userid, $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.
- *
- * There are probably tricks that could be done to improve the performance here, for example,
- * check the capabilities that are already cached first.
- *
- * @see has_capability()
- * @param array $capabilities an array of capability names.
- * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
- * @param integer $userid A user id. 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($capabilities, $context, $userid = null, $doanything = true) {
- if (!is_array($capabilities)) {
- debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
- return false;
- }
- foreach ($capabilities as $capability) {
- if (!has_capability($capability, $context, $userid, $doanything)) {
- return false;
- }
- }
- return true;
- }
- /**
- * 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.
- *
- * @param int|object $user_or_id user id or user object
- * @returns 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)) {
- // we support
- $userid = $user_or_id->id;
- } else {
- $userid = $user_or_id;
- }
- $siteadmins = explode(',', $CFG->siteadmins);
- return in_array($userid, $siteadmins);
- }
- /**
- * Returns true if user has at least one role assign
- * of 'coursecontact' role (is potentially listed in some course descriptions).
- *
- * @param $userid
- * @return stdClass
- */
- 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));
- }
- /**
- * @param string $path
- * @return string
- */
- function get_course_from_path($path) {
- // assume that nothing is more than 1 course deep
- if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
- return $matches[1];
- }
- return false;
- }
- /**
- * @param string $path
- * @param array $accessdata
- * @return bool
- */
- function path_inaccessdata($path, $accessdata) {
- if (empty($accessdata['loaded'])) {
- return false;
- }
- // assume that contexts hang from sys or from a course
- // this will only work well with stuff that hangs from a course
- if (in_array($path, $accessdata['loaded'], true)) {
- // error_log("found it!");
- return true;
- }
- $base = '/' . SYSCONTEXTID;
- while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
- $path = $matches[1];
- if ($path === $base) {
- return false;
- }
- if (in_array($path, $accessdata['loaded'], true)) {
- return true;
- }
- }
- return false;
- }
- /**
- * Does the user have a capability to do something?
- *
- * Walk the accessdata array and return true/false.
- * Deals with prohibits, roleswitching, aggregating
- * capabilities, etc.
- *
- * The main feature of here is being FAST and with no
- * side effects.
- *
- * Notes:
- *
- * Switch Roles exits early
- * ------------------------
- * cap checks within a switchrole need to exit early
- * in our bottom up processing so they don't "see" that
- * there are real RAs that can do all sorts of things.
- *
- * 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.
- *
- * @param string $capability
- * @param object $context
- * @param array $accessdata
- * @return bool
- */
- function has_capability_in_accessdata($capability, $context, array $accessdata) {
- global $CFG;
- if (empty($context->id)) {
- throw new coding_exception('Invalid context specified');
- }
- // Build $paths as a list of current + all parent "paths" with order bottom-to-top
- $contextids = explode('/', trim($context->path, '/'));
- $paths = array($context->path);
- while ($contextids) {
- array_pop($contextids);
- $paths[] = '/' . implode('/', $contextids);
- }
- unset($contextids);
- $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
- foreach ($roles as $roleid => $ignored) {
- foreach ($paths as $path) {
- if (isset($accessdata['rdef']["{$path}:$roleid"][$capability])) {
- $perm = (int)$accessdata['rdef']["{$path}:$roleid"][$capability];
- if ($perm === CAP_PROHIBIT or is_null($roles[$roleid])) {
- $roles[$roleid] = $perm;
- }
- }
- }
- }
- // any CAP_PROHIBIT found means no permission for the user
- if (array_search(CAP_PROHIBIT, $roles) !== false) {
- return false;
- }
- // at least one CAP_ALLOW means the user has a permission
- return (array_search(CAP_ALLOW, $roles) !== false);
- }
- /**
- * @param object $context
- * @param array $accessdata
- * @return array
- */
- function aggregate_roles_from_accessdata($context, $accessdata) {
- $path = $context->path;
- // build $contexts as a list of "paths" of the current
- // contexts and parents with the order top-to-bottom
- $contexts = array($path);
- while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
- $path = $matches[1];
- array_unshift($contexts, $path);
- }
- $cc = count($contexts);
- $roles = array();
- // From the bottom up...
- for ($n=$cc-1; $n>=0; $n--) {
- $ctxp = $contexts[$n];
- if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
- // Found assignments on this leaf
- $addroles = $accessdata['ra'][$ctxp];
- $roles = array_merge($roles, $addroles);
- }
- }
- return array_unique($roles);
- }
- /**
- * 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 object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
- * @param integer $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 $errorstring 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, $userid = null, $doanything = true,
- $errormessage = 'nopermissions', $stringfile = '') {
- if (!has_capability($capability, $context, $userid, $doanything)) {
- throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
- }
- }
- /**
- * Get an array of courses where cap requested is available
- * and user is enrolled, this can be relatively slow.
- *
- * @param string $capability - name of the capability
- * @param array $accessdata_ignored
- * @param bool $doanything_ignored
- * @param string $sort - sorting fields - prefix each fieldname with "c."
- * @param array $fields - additional fields you are interested in...
- * @param int $limit_ignored
- * @return array $courses - ordered array of course objects - see notes above
- */
- function get_user_courses_bycap($userid, $cap, $accessdata_ignored, $doanything_ignored, $sort = 'c.sortorder ASC', $fields = null, $limit_ignored = 0) {
- //TODO: this should be most probably deprecated
- $courses = enrol_get_users_courses($userid, true, $fields, $sort);
- foreach ($courses as $id=>$course) {
- $context = get_context_instance(CONTEXT_COURSE, $id);
- if (!has_capability($cap, $context, $userid)) {
- unset($courses[$id]);
- }
- }
- return $courses;
- }
- /**
- * Return a nested array showing role assignments
- * all relevant role capabilities for the user at
- * site/course_category/course levels
- *
- * We do _not_ delve deeper than courses because the number of
- * overrides at the module/block levels is HUGE.
- *
- * [ra] => [/path/][]=roleid
- * [rdef] => [/path/:roleid][capability]=permission
- * [loaded] => array('/path', '/path')
- *
- * @param int $userid - the id of the user
- * @return array
- */
- function get_user_access_sitewide($userid) {
- global $CFG, $DB;
- /* Get in 3 cheap DB queries...
- * - role assignments
- * - relevant role caps
- * - above and within this user's RAs
- * - below this user's RAs - limited to course level
- */
- $accessdata = array(); // named list
- $accessdata['ra'] = array();
- $accessdata['rdef'] = array();
- $accessdata['loaded'] = array();
- //
- // Role assignments
- //
- $sql = "SELECT ctx.path, ra.roleid
- FROM {role_assignments} ra
- JOIN {context} ctx ON ctx.id=ra.contextid
- WHERE ra.userid = ? AND ctx.contextlevel <= ".CONTEXT_COURSE;
- $params = array($userid);
- $rs = $DB->get_recordset_sql($sql, $params);
- //
- // raparents collects paths & roles we need to walk up
- // the parenthood to build the rdef
- //
- $raparents = array();
- if ($rs) {
- foreach ($rs as $ra) {
- // RAs leafs are arrays to support multi
- // role assignments...
- if (!isset($accessdata['ra'][$ra->path])) {
- $accessdata['ra'][$ra->path] = array();
- }
- $accessdata['ra'][$ra->path][$ra->roleid] = $ra->roleid;
- // Concatenate as string the whole path (all related context)
- // for this role. This is damn faster than using array_merge()
- // Will unique them later
- if (isset($raparents[$ra->roleid])) {
- $raparents[$ra->roleid] .= $ra->path;
- } else {
- $raparents[$ra->roleid] = $ra->path;
- }
- }
- unset($ra);
- $rs->close();
- }
- // Walk up the tree to grab all the roledefs
- // of interest to our user...
- //
- // NOTE: we use a series of IN clauses here - which
- // might explode on huge sites with very convoluted nesting of
- // categories... - extremely unlikely that the number of categories
- // and roletypes is so large that we hit the limits of IN()
- $clauses = '';
- $cparams = array();
- foreach ($raparents as $roleid=>$strcontexts) {
- $contexts = implode(',', array_unique(explode('/', trim($strcontexts, '/'))));
- if ($contexts ==! '') {
- if ($clauses) {
- $clauses .= ' OR ';
- }
- $clauses .= "(roleid=? AND contextid IN ($contexts))";
- $cparams[] = $roleid;
- }
- }
- if ($clauses !== '') {
- $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
- FROM {role_capabilities} rc
- JOIN {context} ctx ON rc.contextid=ctx.id
- WHERE $clauses";
- unset($clauses);
- $rs = $DB->get_recordset_sql($sql, $cparams);
- if ($rs) {
- foreach ($rs as $rd) {
- $k = "{$rd->path}:{$rd->roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- $rs->close();
- }
- }
- //
- // Overrides for the role assignments IN SUBCONTEXTS
- // (though we still do _not_ go below the course level.
- //
- // NOTE that the JOIN w sctx is with 3-way triangulation to
- // catch overrides to the applicable role in any subcontext, based
- // on the path field of the parent.
- //
- $sql = "SELECT sctx.path, ra.roleid,
- ctx.path AS parentpath,
- rco.capability, rco.permission
- FROM {role_assignments} ra
- JOIN {context} ctx
- ON ra.contextid=ctx.id
- JOIN {context} sctx
- ON (sctx.path LIKE " . $DB->sql_concat('ctx.path',"'/%'"). " )
- JOIN {role_capabilities} rco
- ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
- WHERE ra.userid = ?
- AND ctx.contextlevel <= ".CONTEXT_COURSECAT."
- AND sctx.contextlevel <= ".CONTEXT_COURSE."
- ORDER BY sctx.depth, sctx.path, ra.roleid";
- $params = array($userid);
- $rs = $DB->get_recordset_sql($sql, $params);
- if ($rs) {
- foreach ($rs as $rd) {
- $k = "{$rd->path}:{$rd->roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- $rs->close();
- }
- return $accessdata;
- }
- /**
- * Add to the access ctrl array the data needed by a user for a given context
- *
- * @param integer $userid the id of the user
- * @param object $context needs path!
- * @param array $accessdata accessdata array
- * @return void
- */
- function load_subcontext($userid, $context, &$accessdata) {
- global $CFG, $DB;
- /* Get the additional RAs and relevant rolecaps
- * - role assignments - with role_caps
- * - relevant role caps
- * - above this user's RAs
- * - below this user's RAs - limited to course level
- */
- $base = "/" . SYSCONTEXTID;
- //
- // Replace $context with the target context we will
- // load. Normally, this will be a course context, but
- // may be a different top-level context.
- //
- // We have 3 cases
- //
- // - Course
- // - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM
- // - BLOCK/MODULE/GROUP hanging from a course
- //
- // For course contexts, we _already_ have the RAs
- // but the cost of re-fetching is minimal so we don't care.
- //
- if ($context->contextlevel !== CONTEXT_COURSE
- && $context->path !== "$base/{$context->id}") {
- // Case BLOCK/MODULE/GROUP hanging from a course
- // Assumption: the course _must_ be our parent
- // If we ever see stuff nested further this needs to
- // change to do 1 query over the exploded path to
- // find out which one is the course
- $courses = explode('/',get_course_from_path($context->path));
- $targetid = array_pop($courses);
- $context = get_context_instance_by_id($targetid);
- }
- //
- // Role assignments in the context and below
- //
- $sql = "SELECT ctx.path, ra.roleid
- FROM {role_assignments} ra
- JOIN {context} ctx
- ON ra.contextid=ctx.id
- WHERE ra.userid = ?
- AND (ctx.path = ? OR ctx.path LIKE ?)
- ORDER BY ctx.depth, ctx.path, ra.roleid";
- $params = array($userid, $context->path, $context->path."/%");
- $rs = $DB->get_recordset_sql($sql, $params);
- //
- // Read in the RAs, preventing duplicates
- //
- if ($rs) {
- $localroles = array();
- $lastseen = '';
- foreach ($rs as $ra) {
- if (!isset($accessdata['ra'][$ra->path])) {
- $accessdata['ra'][$ra->path] = array();
- }
- // only add if is not a repeat caused
- // by capability join...
- // (this check is cheaper than in_array())
- if ($lastseen !== $ra->path.':'.$ra->roleid) {
- $lastseen = $ra->path.':'.$ra->roleid;
- $accessdata['ra'][$ra->path][$ra->roleid] = $ra->roleid;
- array_push($localroles, $ra->roleid);
- }
- }
- $rs->close();
- }
- //
- // Walk up and down the tree to grab all the roledefs
- // of interest to our user...
- //
- // NOTES
- // - we use IN() but the number of roles is very limited.
- //
- $courseroles = aggregate_roles_from_accessdata($context, $accessdata);
- // Do we have any interesting "local" roles?
- $localroles = array_diff($localroles,$courseroles); // only "new" local roles
- $wherelocalroles='';
- if (count($localroles)) {
- // Role defs for local roles in 'higher' contexts...
- $contexts = substr($context->path, 1); // kill leading slash
- $contexts = str_replace('/', ',', $contexts);
- $localroleids = implode(',',$localroles);
- $wherelocalroles="OR (rc.roleid IN ({$localroleids})
- AND ctx.id IN ($contexts))" ;
- }
- // We will want overrides for all of them
- $whereroles = '';
- if ($roleids = implode(',',array_merge($courseroles,$localroles))) {
- $whereroles = "rc.roleid IN ($roleids) AND";
- }
- $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
- FROM {role_capabilities} rc
- JOIN {context} ctx
- ON rc.contextid=ctx.id
- WHERE ($whereroles
- (ctx.id=? OR ctx.path LIKE ?))
- $wherelocalroles
- ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
- $params = array($context->id, $context->path."/%");
- $newrdefs = array();
- $rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $rd) {
- $k = "{$rd->path}:{$rd->roleid}";
- if (!array_key_exists($k, $newrdefs)) {
- $newrdefs[$k] = array();
- }
- $newrdefs[$k][$rd->capability] = $rd->permission;
- }
- $rs->close();
- compact_rdefs($newrdefs);
- foreach ($newrdefs as $key=>$value) {
- $accessdata['rdef'][$key] =& $newrdefs[$key];
- }
- // error_log("loaded {$context->path}");
- $accessdata['loaded'][] = $context->path;
- }
- /**
- * Add to the access ctrl array the data needed by a role for a given context.
- *
- * The data is added in the rdef key.
- *
- * This role-centric function is useful for role_switching
- * and to get an overview of what a role gets under a
- * given context and below...
- *
- * @param integer $roleid the id of the user
- * @param object $context needs path!
- * @param array $accessdata accessdata array null by default
- * @return array
- */
- function get_role_access_bycontext($roleid, $context, $accessdata = null) {
- global $CFG, $DB;
- /* Get the relevant rolecaps into rdef
- * - relevant role caps
- * - at ctx and above
- * - below this ctx
- */
- if (is_null($accessdata)) {
- $accessdata = array(); // named list
- $accessdata['ra'] = array();
- $accessdata['rdef'] = array();
- $accessdata['loaded'] = array();
- }
- $contexts = substr($context->path, 1); // kill leading slash
- $contexts = str_replace('/', ',', $contexts);
- //
- // Walk up and down the tree to grab all the roledefs
- // of interest to our role...
- //
- // NOTE: we use an IN clauses here - which
- // might explode on huge sites with very convoluted nesting of
- // categories... - extremely unlikely that the number of nested
- // categories is so large that we hit the limits of IN()
- //
- $sql = "SELECT ctx.path, rc.capability, rc.permission
- FROM {role_capabilities} rc
- JOIN {context} ctx
- ON rc.contextid=ctx.id
- WHERE rc.roleid=? AND
- ( ctx.id IN ($contexts) OR
- ctx.path LIKE ? )
- ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
- $params = array($roleid, $context->path."/%");
- $rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $rd) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- $rs->close();
- return $accessdata;
- }
- /**
- * Load accessdata for a user into the $ACCESSLIB_PRIVATE->accessdatabyuser global
- *
- * Used by has_capability() - but feel free
- * to call it if you are about to run a BIG
- * cron run across a bazillion users.
- *
- * @param int $userid
- * @return array returns ACCESSLIB_PRIVATE->accessdatabyuser[userid]
- */
- function load_user_accessdata($userid) {
- global $CFG, $ACCESSLIB_PRIVATE;
- $base = '/'.SYSCONTEXTID;
- $accessdata = get_user_access_sitewide($userid);
- $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
- //
- // provide "default role" & set 'dr'
- //
- if (!empty($CFG->defaultuserroleid)) {
- $accessdata = ge…
Large files files are truncated, but you can click here to view the full file