/lib/accesslib.php
PHP | 5809 lines | 3392 code | 733 blank | 1684 comment | 752 complexity | 80a2cdbb08e7015d6d1d38c21de86bd4 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.0, LGPL-2.1
Large files files are truncated, but you can click here to view the full file
- <?php // $Id: accesslib.php,v 1.421.2.111 2011/08/03 16:28:19 moodlerobot Exp $
-
- ///////////////////////////////////////////////////////////////////////////
- // //
- // NOTICE OF COPYRIGHT //
- // //
- // Moodle - Modular Object-Oriented Dynamic Learning Environment //
- // http://moodle.org //
- // //
- // Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
- // //
- // This program 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 2 of the License, or //
- // (at your option) any later version. //
- // //
- // This program 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: //
- // //
- // http://www.gnu.org/copyleft/gpl.html //
- // //
- ///////////////////////////////////////////////////////////////////////////
-
- /**
- * Public API vs internals
- * -----------------------
- *
- * 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()
- * - $ACCESS global
- * - has_capability_in_accessdata()
- * - is_siteadmin()
- * - get_user_access_sitewide()
- * - load_subcontext()
- * - get_role_access_bycontext()
- *
- * Name conventions
- * ----------------
- *
- * - "ctx" means context
- *
- * accessdata
- * ----------
- *
- * 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 see
- * the $ACCESS global).
- *
- * $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.
- *
- * $accessdata[ra][$contextpath]= array($roleid)
- * [$contextpath]= array($roleid)
- * [$contextpath]= array($roleid)
- *
- * Role definitions are stored like this
- * (no cap merge is done - so it's compact)
- *
- * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
- * [mod/forum:editallpost] = -1
- * [mod/forum:startdiscussion] = -1000
- *
- * 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
- *
- * $accessdata[loaded] = array($contextpath, $contextpath)
- *
- * Stale accessdata
- * ----------------
- *
- * For the logged-in user, accessdata is long-lived.
- *
- * On each pageload we load $DIRTYPATHS 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 sytem level will force the reload for everyone.
- *
- * Default role caps
- * -----------------
- * 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.
- *
- */
-
- require_once $CFG->dirroot.'/lib/blocklib.php';
-
- // permission definitions
- define('CAP_INHERIT', 0);
- define('CAP_ALLOW', 1);
- define('CAP_PREVENT', -1);
- define('CAP_PROHIBIT', -1000);
-
- // context definitions
- define('CONTEXT_SYSTEM', 10);
- define('CONTEXT_USER', 30);
- define('CONTEXT_COURSECAT', 40);
- define('CONTEXT_COURSE', 50);
- define('CONTEXT_MODULE', 70);
- define('CONTEXT_BLOCK', 80);
-
- // capability risks - see http://docs.moodle.org/dev/Hardening_new_Roles_system
- define('RISK_MANAGETRUST', 0x0001);
- define('RISK_CONFIG', 0x0002);
- define('RISK_XSS', 0x0004);
- define('RISK_PERSONAL', 0x0008);
- define('RISK_SPAM', 0x0010);
- define('RISK_DATALOSS', 0x0020);
-
- // rolename displays
- define('ROLENAME_ORIGINAL', 0);// the name as defined in the role definition
- define('ROLENAME_ALIAS', 1); // the name as defined by a role alias
- define('ROLENAME_BOTH', 2); // Both, like this: Role alias (Original)
-
- require_once($CFG->dirroot.'/group/lib.php');
-
- if (!defined('MAX_CONTEXT_CACHE_SIZE')) {
- define('MAX_CONTEXT_CACHE_SIZE', 5000);
- }
-
- $context_cache = array(); // Cache of all used context objects for performance (by level and instance)
- $context_cache_id = array(); // Index to above cache by id
-
- $DIRTYCONTEXTS = null; // dirty contexts cache
- $ACCESS = array(); // cache of caps for cron user switching and has_capability for other users (==not $USER)
- $RDEFS = array(); // role definitions cache - helps a lot with mem usage in cron
-
- /**
- * Adds a context to the cache.
- * @param object $context Context object to be cached
- */
- function cache_context($context) {
- global $context_cache, $context_cache_id;
-
- // If there are too many items in the cache already, remove items until
- // there is space
- while (count($context_cache_id) >= MAX_CONTEXT_CACHE_SIZE) {
- $first = reset($context_cache_id);
- unset($context_cache_id[$first->id]);
- unset($context_cache[$first->contextlevel][$first->instanceid]);
- }
-
- // Add this context to the cache
- $context_cache_id[$context->id] = $context;
- $context_cache[$context->contextlevel][$context->instanceid] = $context;
- }
-
- function get_role_context_caps($roleid, $context) {
- //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 = get_records_select('role_capabilities', "roleid = $roleid AND 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 = get_records_select('role_capabilities', "roleid = $roleid AND 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)
- *
- * @return array
- */
- function get_role_access($roleid, $accessdata=NULL) {
-
- global $CFG;
-
- /* 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 {$CFG->prefix}context ctx
- JOIN {$CFG->prefix}role_capabilities rc
- ON rc.contextid=ctx.id
- WHERE rc.roleid = {$roleid}
- AND ctx.contextlevel <= ".CONTEXT_COURSE."
- ORDER BY ctx.depth, ctx.path";
-
- // we need extra caching in cron only
- if (defined('FULLME') and FULLME === 'cron') {
- static $cron_cache = array();
-
- if (!isset($cron_cache[$roleid])) {
- $cron_cache[$roleid] = array();
- if ($rs = get_recordset_sql($sql)) {
- while ($rd = rs_fetch_next_record($rs)) {
- $cron_cache[$roleid][] = $rd;
- }
- rs_close($rs);
- }
- }
-
- foreach ($cron_cache[$roleid] as $rd) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
-
- } else {
- if ($rs = get_recordset_sql($sql)) {
- while ($rd = rs_fetch_next_record($rs)) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- rs_close($rs);
- }
- }
-
- return $accessdata;
- }
-
- /**
- * Gets the accessdata for role "sitewide"
- * (system down to course)
- *
- * @return array
- */
- function get_default_frontpage_role_access($roleid, $accessdata=NULL) {
-
- global $CFG;
-
- $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 {$CFG->prefix}context ctx
- JOIN {$CFG->prefix}role_capabilities rc
- ON rc.contextid=ctx.id
- WHERE rc.roleid = {$roleid}
- AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE '$base/%')
- AND ctx.contextlevel <= ".CONTEXT_COURSE."
- ORDER BY ctx.depth, ctx.path";
-
- if ($rs = get_recordset_sql($sql)) {
- while ($rd = rs_fetch_next_record($rs)) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- rs_close($rs);
- }
-
- return $accessdata;
- }
-
-
- /**
- * Get the default guest role
- * @return object role
- */
- function get_guest_role() {
- global $CFG;
-
- if (empty($CFG->guestroleid)) {
- if ($roles = get_roles_with_capability('moodle/legacy:guest', CAP_ALLOW)) {
- $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 = get_record('role','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();
- }
- }
- }
-
- /**
- * This function returns whether the current user has the capability of performing a function
- * For example, we can do has_capability('mod/forum:replypost',$context) in forum
- * @param string $capability - name of the capability (or debugcache or clearcache)
- * @param object $context - a context object (record from context table)
- * @param integer $userid - a userid number, empty if current $USER
- * @param bool $doanything - if false, ignore do anything
- * @return bool
- */
- function has_capability($capability, $context, $userid=NULL, $doanything=true) {
- global $USER, $ACCESS, $CFG, $DIRTYCONTEXTS;
-
- // 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;
- }
-
- /// Some sanity checks
- if (debugging('',DEBUG_DEVELOPER)) {
- if (!is_valid_capability($capability)) {
- debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
- }
- if (!is_bool($doanything)) {
- debugging('Capability parameter "doanything" is wierd ("'.$doanything.'"). This should be fixed in code.');
- }
- }
-
- if (empty($userid)) { // we must accept null, 0, '0', '' etc. in $userid
- $userid = $USER->id;
- }
-
- 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 (defined('FULLME') && FULLME === 'cron' && !isset($USER->access)) {
- // In cron, some modules setup a 'fake' $USER,
- // ensure we load the appropriate accessdata.
- if (isset($ACCESS[$userid])) {
- $DIRTYCONTEXTS = NULL; //load fresh dirty contexts
- } else {
- load_user_accessdata($userid);
- $DIRTYCONTEXTS = array();
- }
- $USER->access = $ACCESS[$userid];
-
- } else if ($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();
- $ACCESS = array(); // reset the cache for other users too, the dirty contexts are empty now
- $RDEFS = array();
- }
-
- // Load dirty contexts list if needed
- if (!isset($DIRTYCONTEXTS)) {
- if (isset($USER->access['time'])) {
- $DIRTYCONTEXTS = get_dirty_contexts($USER->access['time']);
- }
- else {
- $DIRTYCONTEXTS = array();
- }
- }
-
- // Careful check for staleness...
- if (count($DIRTYCONTEXTS) !== 0 and is_contextpath_dirty($contexts, $DIRTYCONTEXTS)) {
- // reload all capabilities - preserving loginas, roleswitches, etc
- // and then cleanup any marks of dirtyness... at least from our short
- // term memory! :-)
- $ACCESS = array();
- $RDEFS = array();
-
- if (defined('FULLME') && FULLME === 'cron') {
- load_user_accessdata($userid);
- $USER->access = $ACCESS[$userid];
- $DIRTYCONTEXTS = array();
-
- } else {
- reload_all_capabilities();
- }
- }
-
- // divulge how many times we are called
- //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
-
- if ($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, $doanything);
- }
- // 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, $doanything);
- }
-
- if (!isset($ACCESS[$userid])) {
- load_user_accessdata($userid);
- }
- if ($context->contextlevel <= CONTEXT_COURSE) {
- // Course and above are always preloaded
- return has_capability_in_accessdata($capability, $context, $ACCESS[$userid], $doanything);
- }
- // Load accessdata for below-the-course contexts as needed
- if (!path_inaccessdata($context->path, $ACCESS[$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, $ACCESS[$userid]);
- }
- return has_capability_in_accessdata($capability, $context, $ACCESS[$userid], $doanything);
- }
-
- /**
- * This function returns whether the current user has any of the capabilities in the
- * $capabilities array. This is a simple wrapper around has_capability for convinience.
- *
- * There are probably tricks that could be done to improve the performance here, for example,
- * check the capabilities that are already cached first.
- *
- * @param array $capabilities - an array of capability names.
- * @param object $context - a context object (record from context table)
- * @param integer $userid - a userid number, empty if current $USER
- * @param bool $doanything - if false, ignore do anything
- * @return bool
- */
- function has_any_capability($capabilities, $context, $userid=NULL, $doanything=true) {
- foreach ($capabilities as $capability) {
- if (has_capability($capability, $context, $userid, $doanything)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * This function returns whether the current user has all of the capabilities in the
- * $capabilities array. This is a simple wrapper around has_capability for convinience.
- *
- * There are probably tricks that could be done to improve the performance here, for example,
- * check the capabilities that are already cached first.
- *
- * @param array $capabilities - an array of capability names.
- * @param object $context - a context object (record from context table)
- * @param integer $userid - a userid number, empty if current $USER
- * @param bool $doanything - if false, ignore do anything
- * @return bool
- */
- 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;
- }
-
- /**
- * Uses 1 DB query to answer whether a user is an admin at the sitelevel.
- * It depends on DB schema >=1.7 but does not depend on the new datastructures
- * in v1.9 (context.path, or $USER->access)
- *
- * Will return true if the userid has any of
- * - moodle/site:config
- * - moodle/legacy:admin
- * - moodle/site:doanything
- *
- * @param int $userid
- * @returns bool $isadmin
- */
- function is_siteadmin($userid) {
- global $CFG;
-
- $sql = "SELECT SUM(rc.permission)
- FROM " . $CFG->prefix . "role_capabilities rc
- JOIN " . $CFG->prefix . "context ctx
- ON ctx.id=rc.contextid
- JOIN " . $CFG->prefix . "role_assignments ra
- ON ra.roleid=rc.roleid AND ra.contextid=ctx.id
- WHERE ctx.contextlevel=10
- AND ra.userid={$userid}
- AND rc.capability IN ('moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything')
- GROUP BY rc.capability
- HAVING SUM(rc.permission) > 0";
-
- $isadmin = record_exists_sql($sql);
- return $isadmin;
- }
-
- 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;
- }
-
- function path_inaccessdata($path, $accessdata) {
-
- // 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;
- }
-
- /**
- * 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.
- *
- * Local-most role definition and role-assignment wins
- * ---------------------------------------------------
- * So if the local context has said 'allow', it wins
- * over a high-level context that says 'deny'.
- * This is applied when walking rdefs, and RAs.
- * Only at the same context the values are SUM()med.
- *
- * The exception is CAP_PROHIBIT.
- *
- * "Guest default role" exception
- * ------------------------------
- *
- * See MDL-7513 and $ignoreguest below for details.
- *
- * The rule is that
- *
- * IF we are being asked about moodle/legacy:guest
- * OR moodle/course:view
- * FOR a real, logged-in user
- * AND we reached the top of the path in ra and rdef
- * AND that role has moodle/legacy:guest === 1...
- * THEN we act as if we hadn't seen it.
- *
- * Note that this function must be kept in synch with has_capability_in_accessdata.
- *
- * To Do:
- *
- * - Document how it works
- * - Rewrite in ASM :-)
- *
- */
- function has_capability_in_accessdata($capability, $context, $accessdata, $doanything) {
-
- global $CFG;
-
- $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);
- }
-
- $ignoreguest = false;
- if (isset($accessdata['dr'])
- && ($capability == 'moodle/course:view'
- || $capability == 'moodle/legacy:guest')) {
- // At the base, ignore rdefs where moodle/legacy:guest
- // is set
- $ignoreguest = $accessdata['dr'];
- }
-
- // Coerce it to an int
- $CAP_PROHIBIT = (int)CAP_PROHIBIT;
-
- $cc = count($contexts);
-
- $can = 0;
- $capdepth = 0;
-
- //
- // role-switches loop
- //
- if (isset($accessdata['rsw'])) {
- // check for isset() is fast
- // empty() is slow...
- if (empty($accessdata['rsw'])) {
- unset($accessdata['rsw']); // keep things fast and unambiguous
- break;
- }
- // From the bottom up...
- for ($n=$cc-1;$n>=0;$n--) {
- $ctxp = $contexts[$n];
- if (isset($accessdata['rsw'][$ctxp])) {
- // Found a switchrole assignment
- // check for that role _plus_ the default user role
- $ras = array($accessdata['rsw'][$ctxp],$CFG->defaultuserroleid);
- for ($rn=0;$rn<2;$rn++) {
- $roleid = (int)$ras[$rn];
- // Walk the path for capabilities
- // from the bottom up...
- for ($m=$cc-1;$m>=0;$m--) {
- $capctxp = $contexts[$m];
- if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
- $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
-
- // The most local permission (first to set) wins
- // the only exception is CAP_PROHIBIT
- if ($can === 0) {
- $can = $perm;
- } elseif ($perm === $CAP_PROHIBIT) {
- $can = $perm;
- break;
- }
- }
- }
- }
- // As we are dealing with a switchrole,
- // we return _here_, do _not_ walk up
- // the hierarchy any further
- if ($can < 1) {
- if ($doanything) {
- // didn't find it as an explicit cap,
- // but maybe the user can doanything in this context...
- return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
- } else {
- return false;
- }
- } else {
- return true;
- }
-
- }
- }
- }
-
- //
- // Main loop for normal RAs
- // From the bottom up...
- //
- for ($n=$cc-1;$n>=0;$n--) {
- $ctxp = $contexts[$n];
- if (isset($accessdata['ra'][$ctxp])) {
- // Found role assignments on this leaf
- $ras = $accessdata['ra'][$ctxp];
-
- $rc = count($ras);
- $ctxcan = 0;
- $ctxcapdepth = 0;
- for ($rn=0;$rn<$rc;$rn++) {
- $roleid = (int)$ras[$rn];
- $rolecan = 0;
- $rolecapdepth = 0;
- // Walk the path for capabilities
- // from the bottom up...
- for ($m=$cc-1;$m>=0;$m--) {
- $capctxp = $contexts[$m];
- // ignore some guest caps
- // at base ra and rdef
- if ($ignoreguest == $roleid
- && $n === 0
- && $m === 0
- && isset($accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'])
- && $accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'] > 0) {
- continue;
- }
- if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
- $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
- // The most local permission (first to set) wins
- // the only exception is CAP_PROHIBIT
- if ($rolecan === 0) {
- $rolecan = $perm;
- $rolecapdepth = $m;
- } elseif ($perm === $CAP_PROHIBIT) {
- $rolecan = $perm;
- $rolecapdepth = $m;
- break;
- }
- }
- }
- // Rules for RAs at the same context...
- // - prohibits always wins
- // - permissions at the same ctxlevel & capdepth are added together
- // - deeper capdepth wins
- if ($ctxcan === $CAP_PROHIBIT || $rolecan === $CAP_PROHIBIT) {
- $ctxcan = $CAP_PROHIBIT;
- $ctxcapdepth = 0;
- } elseif ($ctxcapdepth === $rolecapdepth) {
- $ctxcan += $rolecan;
- } elseif ($ctxcapdepth < $rolecapdepth) {
- $ctxcan = $rolecan;
- $ctxcapdepth = $rolecapdepth;
- } else { // ctxcaptdepth is deeper
- // rolecap ignored
- }
- }
- // The most local RAs with a defined
- // permission ($ctxcan) win, except
- // for CAP_PROHIBIT
- // NOTE: If we want the deepest RDEF to
- // win regardless of the depth of the RA,
- // change the elseif below to read
- // ($can === 0 || $capdepth < $ctxcapdepth) {
- if ($ctxcan === $CAP_PROHIBIT) {
- $can = $ctxcan;
- break;
- } elseif ($can === 0) { // see note above
- $can = $ctxcan;
- $capdepth = $ctxcapdepth;
- }
- }
- }
-
- if ($can < 1) {
- if ($doanything) {
- // didn't find it as an explicit cap,
- // but maybe the user can doanything in this context...
- return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
- } else {
- return false;
- }
- } else {
- return true;
- }
-
- }
-
- 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);
- }
-
- /**
- * This is an easy to use function, combining has_capability() with require_course_login().
- * And will call those where needed.
- *
- * It checks for a capability assertion being true. If it isn't
- * then the page is terminated neatly with a standard error message.
- *
- * If the user is not logged in, or is using 'guest' access or other special "users,
- * it provides a logon prompt.
- *
- * @param string $capability - name of the capability
- * @param object $context - a context object (record from context table)
- * @param integer $userid - a userid number
- * @param bool $doanything - if false, ignore do anything
- * @param string $errorstring - an errorstring
- * @param string $stringfile - which stringfile to get it from
- */
- function require_capability($capability, $context, $userid=NULL, $doanything=true,
- $errormessage='nopermissions', $stringfile='') {
-
- global $USER, $CFG;
-
- /* Empty $userid means current user, if the current user is not logged in,
- * then make sure they are (if needed).
- * Originally there was a check for loaded permissions - it is not needed here.
- * Context is now required parameter, the cached $CONTEXT was only hiding errors.
- */
- $errorlink = '';
-
- if (empty($userid)) {
- if ($context->contextlevel == CONTEXT_COURSE) {
- require_login($context->instanceid);
-
- } else if ($context->contextlevel == CONTEXT_MODULE) {
- if (!$cm = get_record('course_modules', 'id', $context->instanceid)) {
- error('Incorrect module');
- }
- if (!$course = get_record('course', 'id', $cm->course)) {
- error('Incorrect course.');
- }
- require_course_login($course, true, $cm);
- $errorlink = $CFG->wwwroot.'/course/view.php?id='.$cm->course;
-
- } else if ($context->contextlevel == CONTEXT_SYSTEM) {
- if (!empty($CFG->forcelogin)) {
- require_login();
- }
-
- } else {
- require_login();
- }
- }
-
- /// OK, if they still don't have the capability then print a nice error message
-
- if (!has_capability($capability, $context, $userid, $doanything)) {
- $capabilityname = get_capability_string($capability);
- print_error($errormessage, $stringfile, $errorlink, $capabilityname);
- }
- }
-
- /**
- * Get an array of courses (with magic extra bits)
- * where the accessdata and in DB enrolments show
- * that the cap requested is available.
- *
- * The main use is for get_my_courses().
- *
- * Notes
- *
- * - $fields is an array of fieldnames to ADD
- * so name the fields you really need, which will
- * be added and uniq'd
- *
- * - the course records have $c->context which is a fully
- * valid context object. Saves you a query per course!
- *
- * - the course records have $c->categorypath to make
- * category lookups cheap
- *
- * - current implementation is split in -
- *
- * - if the user has the cap systemwide, stupidly
- * grab *every* course for a capcheck. This eats
- * a TON of bandwidth, specially on large sites
- * with separate DBs...
- *
- * - otherwise, fetch "likely" courses with a wide net
- * that should get us _cheaply_ at least the courses we need, and some
- * we won't - we get courses that...
- * - are in a category where user has the cap
- * - or where use has a role-assignment (any kind)
- * - or where the course has an override on for this cap
- *
- * - walk the courses recordset checking the caps oneach one
- * the checks are all in memory and quite fast
- * (though we could implement a specialised variant of the
- * has_capability_in_accessdata() code to speed it up)
- *
- * @param string $capability - name of the capability
- * @param array $accessdata - accessdata session array
- * @param bool $doanything - if false, ignore do anything
- * @param string $sort - sorting fields - prefix each fieldname with "c."
- * @param array $fields - additional fields you are interested in...
- * @param int $limit - set if you want to limit the number of courses
- * @return array $courses - ordered array of course objects - see notes above
- *
- */
- function get_user_courses_bycap($userid, $cap, $accessdata, $doanything, $sort='c.sortorder ASC', $fields=NULL, $limit=0) {
-
- global $CFG;
-
- // Slim base fields, let callers ask for what they need...
- $basefields = array('id', 'sortorder', 'shortname', 'idnumber');
-
- if (!is_null($fields)) {
- $fields = array_merge($basefields, $fields);
- $fields = array_unique($fields);
- } else {
- $fields = $basefields;
- }
- // If any of the fields is '*', leave it alone, discarding the rest
- // to avoid ambiguous columns under some silly DBs. See MDL-18746 :-D
- if (in_array('*', $fields)) {
- $fields = array('*');
- }
- $coursefields = 'c.' .implode(',c.', $fields);
-
- $sort = trim($sort);
- if ($sort !== '') {
- $sort = "ORDER BY $sort";
- }
-
- $sysctx = get_context_instance(CONTEXT_SYSTEM);
- if (has_capability_in_accessdata($cap, $sysctx, $accessdata, $doanything)) {
- //
- // Apparently the user has the cap sitewide, so walk *every* course
- // (the cap checks are moderately fast, but this moves massive bandwidth w the db)
- // Yuck.
- //
- $sql = "SELECT $coursefields,
- ctx.id AS ctxid, ctx.path AS ctxpath,
- ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
- cc.path AS categorypath
- FROM {$CFG->prefix}course c
- JOIN {$CFG->prefix}course_categories cc
- ON c.category=cc.id
- JOIN {$CFG->prefix}context ctx
- ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
- $sort ";
- $rs = get_recordset_sql($sql);
- } else {
- //
- // narrow down where we have the caps to a few contexts
- // this will be a combination of
- // - courses where user has an explicit enrolment
- // - courses that have an override (any status) on that capability
- // - categories where user has the rights (granted status) on that capability
- //
- $sql = "SELECT ctx.*
- FROM {$CFG->prefix}context ctx
- WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
- ORDER BY ctx.depth";
- $rs = get_recordset_sql($sql);
- $catpaths = array();
- while ($catctx = rs_fetch_next_record($rs)) {
- if ($catctx->path != ''
- && has_capability_in_accessdata($cap, $catctx, $accessdata, $doanything)) {
- $catpaths[] = $catctx->path;
- }
- }
- rs_close($rs);
- $catclause = '';
- if (count($catpaths)) {
- $cc = count($catpaths);
- for ($n=0;$n<$cc;$n++) {
- $catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'";
- }
- $catclause = 'WHERE (' . implode(' OR ', $catpaths) .')';
- }
- unset($catpaths);
-
- $capany = '';
- if ($doanything) {
- $capany = " OR rc.capability='moodle/site:doanything'";
- }
-
- /// UNION 3 queries:
- /// - user role assignments in courses
- /// - user capability (override - any status) in courses
- /// - user right (granted status) in categories (optionally executed)
- /// Enclosing the 3-UNION into an inline_view to avoid column names conflict and making the ORDER BY cross-db
- /// and to allow selection of TEXT columns in the query (MSSQL and Oracle limitation). MDL-16209
- $sql = "
- SELECT $coursefields, ctxid, ctxpath, ctxdepth, ctxlevel, categorypath
- FROM (
- SELECT c.id,
- ctx.id AS ctxid, ctx.path AS ctxpath,
- ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
- cc.path AS categorypath
- FROM {$CFG->prefix}course c
- JOIN {$CFG->prefix}course_categories cc
- ON c.category=cc.id
- JOIN {$CFG->prefix}context ctx
- ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
- JOIN {$CFG->prefix}role_assignments ra
- ON (ra.contextid=ctx.id AND ra.userid=$userid)
- UNION
- SELECT c.id,
- ctx.id AS ctxid, ctx.path AS ctxpath,
- ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
- cc.path AS categorypath
- FROM {$CFG->prefix}course c
- JOIN {$CFG->prefix}course_categories cc
- ON c.category=cc.id
- JOIN {$CFG->prefix}context ctx
- ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
- JOIN {$CFG->prefix}role_capabilities rc
- ON (rc.contextid=ctx.id AND (rc.capability='$cap' $capany)) ";
-
- if (!empty($catclause)) { /// If we have found the right in categories, add child courses here too
- $sql .= "
- UNION
- SELECT c.id,
- ctx.id AS ctxid, ctx.path AS ctxpath,
- ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
- cc.path AS categorypath
- FROM {$CFG->prefix}course c
- JOIN {$CFG->prefix}course_categories cc
- ON c.category=cc.id
- JOIN {$CFG->prefix}context ctx
- ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
- $catclause";
- }
-
- /// Close the inline_view and join with courses table to get requested $coursefields
- $sql .= "
- ) inline_view
- INNER JOIN {$CFG->prefix}course c
- ON inline_view.id = c.id";
-
- /// To keep cross-db we need to strip any prefix in the ORDER BY clause for queries using UNION
- $sql .= "
- " . preg_replace('/[a-z]+\./i', '', $sort); /// Add ORDER BY clause
-
- $rs = get_recordset_sql($sql);
- }
-
- /// Confirm rights (granted capability) for each course returned
- $courses = array();
- $cc = 0; // keep count
- while ($c = rs_fetch_next_record($rs)) {
- // build the context obj
- $c = make_context_subobj($c);
-
- if (has_capability_in_accessdata($cap, $c->context, $accessdata, $doanything)) {
- if ($limit > 0 && $cc >= $limit) {
- break;
- }
-
- $courses[] = $c;
- $cc++;
- }
- }
- rs_close($rs);
- return $courses;
- }
-
-
- /**
- * It will return a nested array showing role assignments
- * all relevant role capabilities for the user at
- * site/metacourse/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/] = array(roleid, roleid)
- * [rdef] => [/path/:roleid][capability]=permission
- * [loaded] => array('/path', '/path')
- *
- * @param $userid integer - the id of the user
- *
- */
- function get_user_access_sitewide($userid) {
-
- global $CFG;
-
- // this flag has not been set!
- // (not clean install, or upgraded successfully to 1.7 and up)
- if (empty($CFG->rolesactive)) {
- return false;
- }
-
- /* Get in 3 cheap DB queries...
- * - role assignments - with role_caps
- * - relevant role caps
- * - above 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();
-
- $sitectx = get_system_context();
- $base = '/'.$sitectx->id;
-
- //
- // Role assignments - and any rolecaps directly linked
- // because it's cheap to read rolecaps here over many
- // RAs
- //
- $sql = "SELECT ctx.path, ra.roleid, rc.capability, rc.permission
- FROM {$CFG->prefix}role_assignments ra
- JOIN {$CFG->prefix}context ctx
- ON ra.contextid=ctx.id
- LEFT OUTER JOIN {$CFG->prefix}role_capabilities rc
- ON (rc.roleid=ra.roleid AND rc.contextid=ra.contextid)
- WHERE ra.userid = $userid AND ctx.contextlevel <= ".CONTEXT_COURSE."
- ORDER BY ctx.depth, ctx.path, ra.roleid";
- $rs = get_recordset_sql($sql);
- //
- // raparents collects paths & roles we need to walk up
- // the parenthood to build the rdef
- //
- // the array will bulk up a bit with dups
- // which we'll later clear up
- //
- $raparents = array();
- $lastseen = '';
- if ($rs) {
- while ($ra = rs_fetch_next_record($rs)) {
- // RAs leafs are arrays to support multi
- // role assignments...
- 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;
- array_push($accessdata['ra'][$ra->path], $ra->roleid);
- $parentids = explode('/', $ra->path);
- array_shift($parentids); // drop empty leading "context"
- array_pop($parentids); // drop _this_ context
-
- if (isset($raparents[$ra->roleid])) {
- $raparents[$ra->roleid] = array_merge($raparents[$ra->roleid],
- $parentids);
- } else {
- $raparents[$ra->roleid] = $parentids;
- }
- }
- // Always add the roleded
- if (!empty($ra->capability)) {
- $k = "{$ra->path}:{$ra->roleid}";
- $accessdata['rdef'][$k][$ra->capability] = $ra->permission;
- }
- }
- unset($ra);
- rs_close($rs);
- }
-
- // 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 = array();
- foreach ($raparents as $roleid=>$contexts) {
- $contexts = implode(',', array_unique($contexts));
- if ($contexts ==! '') {
- $clauses[] = "(roleid=$roleid AND contextid IN ($contexts))";
- }
- }
- $clauses = implode(" OR ", $clauses);
- if ($clauses !== '') {
- $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
- FROM {$CFG->prefix}role_capabilities rc
- JOIN {$CFG->prefix}context ctx
- ON rc.contextid=ctx.id
- WHERE $clauses
- ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
-
- $rs = get_recordset_sql($sql);
- unset($clauses);
-
- if ($rs) {
- while ($rd = rs_fetch_next_record($rs)) {
- $k = "{$rd->path}:{$rd->roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- rs_close($rs);
- }
- }
-
- //
- // 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 {$CFG->prefix}role_assignments ra
- JOIN {$CFG->prefix}context ctx
- ON ra.contextid=ctx.id
- JOIN {$CFG->prefix}context sctx
- ON (sctx.path LIKE " . sql_concat('ctx.path',"'/%'"). " )
- JOIN {$CFG->prefix}role_capabilities rco
- ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
- WHERE ra.userid = $userid
- AND ctx.contextlevel <= ".CONTEXT_COURSECAT."
- AND sctx.contextlevel <= ".CONTEXT_COURSE."
- ORDER BY sctx.depth, sctx.path, ra.roleid";
- $rs = get_recordset_sql($sql);
- if ($rs) {
- while ($rd = rs_fetch_next_record($rs)) {
- $k = "{$rd->path}:{$rd->roleid}";
- $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
- }
- unset($rd);
- rs_close($rs);
- }
- return $accessdata;
- }
-
- /**
- * It add to the access ctrl array the data
- * needed by a user for a given context
- *
- * @param $userid integer - the id of the user
- * @param $context context obj - needs path!
- * @param $accessdata array accessdata array
- */
- function load_subcontext($userid, $context, &$accessdata) {
-
- global $CFG;
-
-
-
- /* 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
- …
Large files files are truncated, but you can click here to view the full file