Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

6296 lines (5540 sloc) 225.2 kB
<?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/dev/Hardening_new_Roles_system} */
define('RISK_MANAGETRUST', 0x0001);
/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_CONFIG', 0x0002);
/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_XSS', 0x0004);
/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_PERSONAL', 0x0008);
/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_SPAM', 0x0010);
/** capability risks - 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 */
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 = isset($USER->id) ? $USER->id : 0;
} else {
$userid = is_object($user) ? $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 = get_role_access($CFG->defaultuserroleid, $accessdata);
if (!isset($accessdata['ra'][$base])) {
$accessdata['ra'][$base] = array();
}
$accessdata['ra'][$base][$CFG->defaultuserroleid] = $CFG->defaultuserroleid;
$accessdata['dr'] = $CFG->defaultuserroleid;
}
//
// provide "default frontpage role"
//
if (!empty($CFG->defaultfrontpageroleid)) {
$base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
$accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
if (!isset($accessdata['ra'][$base])) {
$accessdata['ra'][$base] = array();
}
$accessdata['ra'][$base][$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid;
}
// for dirty timestamps in cron
$accessdata['time'] = time();
$ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
compact_rdefs($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['rdef']);
return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
}
/**
* Use shared copy of role definitions stored in ACCESSLIB_PRIVATE->roledefinitions;
*
* @param array $rdefs array of role definitions in contexts
*/
function compact_rdefs(&$rdefs) {
global $ACCESSLIB_PRIVATE;
/*
* This is a basic sharing only, we could also
* use md5 sums of values. The main purpose is to
* reduce mem in cron jobs - many users in $ACCESSLIB_PRIVATE->accessdatabyuser array.
*/
foreach ($rdefs as $key => $value) {
if (!array_key_exists($key, $ACCESSLIB_PRIVATE->roledefinitions)) {
$ACCESSLIB_PRIVATE->roledefinitions[$key] = $rdefs[$key];
}
$rdefs[$key] =& $ACCESSLIB_PRIVATE->roledefinitions[$key];
}
}
/**
* A convenience function to completely load all the capabilities
* for the current user. This is what gets called from complete_user_login()
* for example. Call it only _after_ you've setup $USER and called
* check_enrolment_plugins();
* @see check_enrolment_plugins()
*
* @return void
*/
function load_all_capabilities() {
global $CFG, $ACCESSLIB_PRIVATE;
//NOTE: we can not use $USER here because it may no be linked to $_SESSION['USER'] yet!
// roles not installed yet - we are in the middle of installation
if (during_initial_install()) {
return;
}
$base = '/'.SYSCONTEXTID;
if (isguestuser($_SESSION['USER'])) {
$guest = get_guest_role();
// Load the rdefs
$_SESSION['USER']->access = get_role_access($guest->id);
// Put the ghost enrolment in place...
$_SESSION['USER']->access['ra'][$base] = array($guest->id => $guest->id);
} else if (!empty($_SESSION['USER']->id)) { // can not use isloggedin() yet
$accessdata = get_user_access_sitewide($_SESSION['USER']->id);
//
// provide "default role" & set 'dr'
//
if (!empty($CFG->defaultuserroleid)) {
$accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
if (!isset($accessdata['ra'][$base])) {
$accessdata['ra'][$base] = array();
}
$accessdata['ra'][$base][$CFG->defaultuserroleid] = $CFG->defaultuserroleid;
$accessdata['dr'] = $CFG->defaultuserroleid;
}
$frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
//
// provide "default frontpage role"
//
if (!empty($CFG->defaultfrontpageroleid)) {
$base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
$accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
if (!isset($accessdata['ra'][$base])) {
$accessdata['ra'][$base] = array();
}
$accessdata['ra'][$base][$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid;
}
$_SESSION['USER']->access = $accessdata;
} else if (!empty($CFG->notloggedinroleid)) {
$_SESSION['USER']->access = get_role_access($CFG->notloggedinroleid);
$_SESSION['USER']->access['ra'][$base] = array($CFG->notloggedinroleid => $CFG->notloggedinroleid);
}
// Timestamp to read dirty context timestamps later
$_SESSION['USER']->access['time'] = time();
$ACCESSLIB_PRIVATE->dirtycontexts = array();
// Clear to force a refresh
unset($_SESSION['USER']->mycourses);
}
/**
* 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.
*
* That is - completely transparent to the user.
*
* Note: rewrites $USER->access completely.
*
* @return void
*/
function reload_all_capabilities() {
global $USER, $DB;
// error_log("reloading");
// copy switchroles
$sw = array();
if (isset($USER->access['rsw'])) {
$sw = $USER->access['rsw'];
// error_log(print_r($sw,1));
}
unset($USER->access);
unset($USER->mycourses);
load_all_capabilities();
foreach ($sw as $path => $roleid) {
$context = $DB->get_record('context', array('path'=>$path));
role_switch($roleid, $context);
}
}
/**
* Adds a temp role to an accessdata array.
*
* Useful for the "temporary guest" access
* we grant to logged-in users.
*
* Note - assumes a course context!
*
* @param object $content
* @param int $roleid
* @param array $accessdata
* @return array Returns access data
*/
function load_temp_role($context, $roleid, array $accessdata) {
global $CFG, $DB;
//
// Load rdefs for the role in -
// - this context
// - all the parents
// - and below - IOWs overrides...
//
// turn the path into a list of context ids
$contexts = substr($context->path, 1); // kill leading slash
$contexts = str_replace('/', ',', $contexts);
$sql = "SELECT ctx.path, rc.capability, rc.permission
FROM {context} ctx
JOIN {role_capabilities} rc
ON rc.contextid=ctx.id
WHERE (ctx.id IN ($contexts)
OR ctx.path LIKE ?)
AND rc.roleid = ?
ORDER BY ctx.depth, ctx.path";
$params = array($context->path."/%", $roleid);
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $rd) {
$k = "{$rd->path}:{$roleid}";
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
}
$rs->close();
//
// Say we loaded everything for the course context
// - which we just did - if the user gets a proper
// RA in this session, this data will need to be reloaded,
// but that is handled by the complete accessdata reload
//
array_push($accessdata['loaded'], $context->path);
//
// Add the ghost RA
//
if (!isset($accessdata['ra'][$context->path])) {
$accessdata['ra'][$context->path] = array();
}
$accessdata['ra'][$context->path][$roleid] = $roleid;
return $accessdata;
}
/**
* Removes any extra guest roles from accessdata
* @param object $context
* @param array $accessdata
* @return array access data
*/
function remove_temp_roles($context, array $accessdata) {
global $DB, $USER;
$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'=>$context->id, 'userid'=>$USER->id));
if ($ras) {
$accessdata['ra'][$context->path] = array_combine(array_keys($ras), array_keys($ras));
} else {
$accessdata['ra'][$context->path] = array();
}
return $accessdata;
}
/**
* 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 = get_context_instance(CONTEXT_SYSTEM);
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;
}
/**
* @param object $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);
}
/**********************************
* Context Manipulation functions *
**********************************/
/**
* Context creation - internal implementation.
*
* Create a new context record for use by all roles-related stuff
* assumes that the caller has done the homework.
*
* DO NOT CALL THIS DIRECTLY, instead use {@link get_context_instance}!
*
* @param int $contextlevel
* @param int $instanceid
* @param int $strictness
* @return object newly created context
*/
function create_context($contextlevel, $instanceid, $strictness = IGNORE_MISSING) {
global $CFG, $DB;
if ($contextlevel == CONTEXT_SYSTEM) {
return get_system_context();
}
$context = new stdClass();
$context->contextlevel = $contextlevel;
$context->instanceid = $instanceid;
// Define $context->path based on the parent
// context. In other words... Who is your daddy?
$basepath = '/' . SYSCONTEXTID;
$basedepth = 1;
$result = true;
$error_message = null;
switch ($contextlevel) {
case CONTEXT_COURSECAT:
$sql = "SELECT ctx.path, ctx.depth
FROM {context} ctx
JOIN {course_categories} cc
ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
WHERE cc.id=?";
$params = array($instanceid);
if ($p = $DB->get_record_sql($sql, $params)) {
$basepath = $p->path;
$basedepth = $p->depth;
} else if ($category = $DB->get_record('course_categories', array('id'=>$instanceid), '*', $strictness)) {
if (empty($category->parent)) {
// ok - this is a top category
} else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) {
$basepath = $parent->path;
$basedepth = $parent->depth;
} else {
// wrong parent category - no big deal, this can be fixed later
$basepath = null;
$basedepth = 0;
}
} else {
// incorrect category id
$error_message = "incorrect course category id ($instanceid)";
$result = false;
}
break;
case CONTEXT_COURSE:
$sql = "SELECT ctx.path, ctx.depth
FROM {context} ctx
JOIN {course} c
ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
WHERE c.id=? AND c.id !=" . SITEID;
$params = array($instanceid);
if ($p = $DB->get_record_sql($sql, $params)) {
$basepath = $p->path;
$basedepth = $p->depth;
} else if ($course = $DB->get_record('course', array('id'=>$instanceid), '*', $strictness)) {
if ($course->id == SITEID) {
//ok - no parent category
} else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) {
$basepath = $parent->path;
$basedepth = $parent->depth;
} else {
// wrong parent category of course - no big deal, this can be fixed later
$basepath = null;
$basedepth = 0;
}
} else if ($instanceid == SITEID) {
// no errors for missing site course during installation
return false;
} else {
// incorrect course id
$error_message = "incorrect course id ($instanceid)";
$result = false;
}
break;
case CONTEXT_MODULE:
$sql = "SELECT ctx.path, ctx.depth
FROM {context} ctx
JOIN {course_modules} cm
ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
WHERE cm.id=?";
$params = array($instanceid);
if ($p = $DB->get_record_sql($sql, $params)) {
$basepath = $p->path;
$basedepth = $p->depth;
} else if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid), '*', $strictness)) {
if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course, $strictness)) {
$basepath = $parent->path;
$basedepth = $parent->depth;
} else {
// course does not exist - modules can not exist without a course
$error_message = "course does not exist ($cm->course) - modules can not exist without a course";
$result = false;
}
} else {
// cm does not exist
$error_message = "cm with id $instanceid does not exist";
$result = false;
}
break;
case CONTEXT_BLOCK:
$sql = "SELECT ctx.path, ctx.depth
FROM {context} ctx
JOIN {block_instances} bi ON (bi.parentcontextid=ctx.id)
WHERE bi.id = ?";
$params = array($instanceid, CONTEXT_COURSE);
if ($p = $DB->get_record_sql($sql, $params, '*', $strictness)) {
$basepath = $p->path;
$basedepth = $p->depth;
} else {
// block does not exist
$error_message = 'block or parent context does not exist';
$result = false;
}
break;
case CONTEXT_USER:
// default to basepath
break;
}
// if grandparents unknown, maybe rebuild_context_path() will solve it later
if ($basedepth != 0) {
$context->depth = $basedepth+1;
}
if (!$result) {
debugging('Error: could not insert new context level "'.
s($contextlevel).'", instance "'.
s($instanceid).'". ' . $error_message);
return false;
}
$id = $DB->insert_record('context', $context);
// can't set the full path till we know the id!
if ($basedepth != 0 and !empty($basepath)) {
$DB->set_field('context', 'path', $basepath.'/'. $id, array('id'=>$id));
}
return get_context_instance_by_id($id);
}
/**
* Returns system context or null if can not be created yet.
*
* @param bool $cache use caching
* @return mixed system context or null
*/
function get_system_context($cache = true) {
global $DB, $ACCESSLIB_PRIVATE;
if ($cache and defined('SYSCONTEXTID')) {
if (is_null($ACCESSLIB_PRIVATE->systemcontext)) {
$ACCESSLIB_PRIVATE->systemcontext = new stdClass();
$ACCESSLIB_PRIVATE->systemcontext->id = SYSCONTEXTID;
$ACCESSLIB_PRIVATE->systemcontext->contextlevel = CONTEXT_SYSTEM;
$ACCESSLIB_PRIVATE->systemcontext->instanceid = 0;
$ACCESSLIB_PRIVATE->systemcontext->path = '/'.SYSCONTEXTID;
$ACCESSLIB_PRIVATE->systemcontext->depth = 1;
}
return $ACCESSLIB_PRIVATE->systemcontext;
}
try {
$context = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM));
} catch (dml_exception $e) {
//table does not exist yet, sorry
return null;
}
if (!$context) {
$context = new stdClass();
$context->contextlevel = CONTEXT_SYSTEM;
$context->instanceid = 0;
$context->depth = 1;
$context->path = null; //not known before insert
try {
$context->id = $DB->insert_record('context', $context);
} catch (dml_exception $e) {
// can not create context yet, sorry
return null;
}
}
if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) {
$context->instanceid = 0;
$context->path = '/'.$context->id;
$context->depth = 1;
$DB->update_record('context', $context);
}
if (!defined('SYSCONTEXTID')) {
define('SYSCONTEXTID', $context->id);
}
$ACCESSLIB_PRIVATE->systemcontext = $context;
return $ACCESSLIB_PRIVATE->systemcontext;
}
/**
* Remove a context record and any dependent entries,
* removes context from static context cache too
*
* @param int $level
* @param int $instanceid
* @param bool $deleterecord false means keep record for now
* @return bool returns true or throws an exception
*/
function delete_context($contextlevel, $instanceid, $deleterecord = true) {
global $DB, $ACCESSLIB_PRIVATE, $CFG;
// do not use get_context_instance(), because the related object might not exist,
// or the context does not exist yet and it would be created now
if ($context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
// delete these first because they might fetch the context and try to recreate it!
blocks_delete_all_for_context($context->id);
filter_delete_all_for_context($context->id);
require_once($CFG->dirroot . '/comment/lib.php');
comment::delete_comments(array('contextid'=>$context->id));
require_once($CFG->dirroot.'/rating/lib.php');
$delopt = new stdclass();
$delopt->contextid = $context->id;
$rm = new rating_manager();
$rm->delete_ratings($delopt);
// delete all files attached to this context
$fs = get_file_storage();
$fs->delete_area_files($context->id);
// now delete stuff from role related tables, role_unassign_all
// and unenrol should be called earlier to do proper cleanup
$DB->delete_records('role_assignments', array('contextid'=>$context->id));
$DB->delete_records('role_capabilities', array('contextid'=>$context->id));
$DB->delete_records('role_names', array('contextid'=>$context->id));
// and finally it is time to delete the context record if requested
if ($deleterecord) {
$DB->delete_records('context', array('id'=>$context->id));
// purge static context cache if entry present
$ACCESSLIB_PRIVATE->contexcache->remove($context);
}
// do not mark dirty contexts if parents unknown
if (!is_null($context->path) and $context->depth > 0) {
mark_context_dirty($context->path);
}
}
return true;
}
/**
* Precreates all contexts including all parents
*
* @param int $contextlevel empty means all
* @param bool $buildpaths update paths and depths
* @return void
*/
function create_contexts($contextlevel = null, $buildpaths = true) {
global $DB;
//make sure system context exists
$syscontext = get_system_context(false);
if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT
or $contextlevel == CONTEXT_COURSE
or $contextlevel == CONTEXT_MODULE
or $contextlevel == CONTEXT_BLOCK) {
$sql = "INSERT INTO {context} (contextlevel, instanceid)
SELECT ".CONTEXT_COURSECAT.", cc.id
FROM {course}_categories cc
WHERE NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
$DB->execute($sql);
}
if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE
or $contextlevel == CONTEXT_MODULE
or $contextlevel == CONTEXT_BLOCK) {
$sql = "INSERT INTO {context} (contextlevel, instanceid)
SELECT ".CONTEXT_COURSE.", c.id
FROM {course} c
WHERE NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
$DB->execute($sql);
}
if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE
or $contextlevel == CONTEXT_BLOCK) {
$sql = "INSERT INTO {context} (contextlevel, instanceid)
SELECT ".CONTEXT_MODULE.", cm.id
FROM {course}_modules cm
WHERE NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
$DB->execute($sql);
}
if (empty($contextlevel) or $contextlevel == CONTEXT_USER
or $contextlevel == CONTEXT_BLOCK) {
$sql = "INSERT INTO {context} (contextlevel, instanceid)
SELECT ".CONTEXT_USER.", u.id
FROM {user} u
WHERE u.deleted=0
AND NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
$DB->execute($sql);
}
if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) {
$sql = "INSERT INTO {context} (contextlevel, instanceid)
SELECT ".CONTEXT_BLOCK.", bi.id
FROM {block_instances} bi
WHERE NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
$DB->execute($sql);
}
if ($buildpaths) {
build_context_path(false);
}
}
/**
* Remove stale context records
*
* @return bool
*/
function cleanup_contexts() {
global $DB;
$sql = " SELECT c.contextlevel,
c.instanceid AS instanceid
FROM {context} c
LEFT OUTER JOIN {course}_categories t
ON c.instanceid = t.id
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
UNION
SELECT c.contextlevel,
c.instanceid
FROM {context} c
LEFT OUTER JOIN {course} t
ON c.instanceid = t.id
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
UNION
SELECT c.contextlevel,
c.instanceid
FROM {context} c
LEFT OUTER JOIN {course}_modules t
ON c.instanceid = t.id
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
UNION
SELECT c.contextlevel,
c.instanceid
FROM {context} c
LEFT OUTER JOIN {user} t
ON c.instanceid = t.id
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
UNION
SELECT c.contextlevel,
c.instanceid
FROM {context} c
LEFT OUTER JOIN {block_instances} t
ON c.instanceid = t.id
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
";
// transactions used only for performance reasons here
$transaction = $DB->start_delegated_transaction();
$rs = $DB->get_recordset_sql($sql);
foreach ($rs as $ctx) {
delete_context($ctx->contextlevel, $ctx->instanceid);
}
$rs->close();
$transaction->allow_commit();
return true;
}
/**
* Preloads all contexts relating to a course: course, modules. Block contexts
* are no longer loaded here. The contexts for all the blocks on the current
* page are now efficiently loaded by {@link block_manager::load_blocks()}.
*
* @param int $courseid Course ID
* @return void
*/
function preload_course_contexts($courseid) {
global $DB, $ACCESSLIB_PRIVATE;
// Users can call this multiple times without doing any harm
global $ACCESSLIB_PRIVATE;
if (array_key_exists($courseid, $ACCESSLIB_PRIVATE->preloadedcourses)) {
return;
}
$params = array($courseid, $courseid, $courseid);
$sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
FROM {course_modules} cm
JOIN {context} x ON x.instanceid=cm.id
WHERE cm.course=? AND x.contextlevel=".CONTEXT_MODULE."
UNION ALL
SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
FROM {context} x
WHERE x.instanceid=? AND x.contextlevel=".CONTEXT_COURSE."";
$rs = $DB->get_recordset_sql($sql, $params);
foreach($rs as $context) {
$ACCESSLIB_PRIVATE->contexcache->add($context);
}
$rs->close();
$ACCESSLIB_PRIVATE->preloadedcourses[$courseid] = true;
}
/**
* Get the context instance as an object. This function will create the
* context instance if it does not exist yet.
*
* @todo Remove code branch from previous fix MDL-9016 which is no longer needed
*
* @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE.
* @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id,
* for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0
* @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
* MUST_EXIST means throw exception if no record or multiple records found
* @return object The context object.
*/
function get_context_instance($contextlevel, $instance = 0, $strictness = IGNORE_MISSING) {
global $DB, $ACCESSLIB_PRIVATE;
static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_BLOCK);
/// System context has special cache
if ($contextlevel == CONTEXT_SYSTEM) {
return get_system_context();
}
/// check allowed context levels
if (!in_array($contextlevel, $allowed_contexts)) {
// fatal error, code must be fixed - probably typo or switched parameters
print_error('invalidcourselevel');
}
// Various operations rely on context cache
$cache = $ACCESSLIB_PRIVATE->contexcache;
if (!is_array($instance)) {
/// Check the cache
$context = $cache->get($contextlevel, $instance);
if ($context) {
return $context;
}
/// Get it from the database, or create it
if (!$context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instance))) {
$context = create_context($contextlevel, $instance, $strictness);
}
/// Only add to cache if context isn't empty.
if (!empty($context)) {
$cache->add($context);
}
return $context;
}
/// ok, somebody wants to load several contexts to save some db queries ;-)
$instances = $instance;
$result = array();
foreach ($instances as $key=>$instance) {
/// Check the cache first
if ($context = $cache->get($contextlevel, $instance)) { // Already cached
$result[$instance] = $context;
unset($instances[$key]);
continue;
}
}
if ($instances) {
list($instanceids, $params) = $DB->get_in_or_equal($instances, SQL_PARAMS_QM);
array_unshift($params, $contextlevel);
$sql = "SELECT instanceid, id, contextlevel, path, depth
FROM {context}
WHERE contextlevel=? AND instanceid $instanceids";
if (!$contexts = $DB->get_records_sql($sql, $params)) {
$contexts = array();
}
foreach ($instances as $instance) {
if (isset($contexts[$instance])) {
$context = $contexts[$instance];
} else {
$context = create_context($contextlevel, $instance);
}
if (!empty($context)) {
$cache->add($context);
}
$result[$instance] = $context;
}
}
return $result;
}
/**
* Get a context instance as an object, from a given context id.
*
* @param int $id context id
* @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
* MUST_EXIST means throw exception if no record or multiple records found
* @return stdClass|bool the context object or false if not found.
*/
function get_context_instance_by_id($id, $strictness = IGNORE_MISSING) {
global $DB, $ACCESSLIB_PRIVATE;
if ($id == SYSCONTEXTID) {
return get_system_context();
}
$cache = $ACCESSLIB_PRIVATE->contexcache;
if ($context = $cache->get_by_id($id)) {
return $context;
}
if ($context = $DB->get_record('context', array('id'=>$id), '*', $strictness)) {
$cache->add($context);
return $context;
}
return false;
}
/**
* 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
*/
function get_local_override($roleid, $contextid, $capability) {
global $DB;
return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
}
/**
* 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 = get_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) {
$parentcontexts = get_parent_contexts($context, false);
$parent = reset($parentcontexts);
$parent = get_context_instance_by_id($parent);
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);
}
/**
* Returns current course id or null if outside of course based on context parameter.
* @param object $context
* @return int|bool related course id or false
*/
function get_courseid_from_context($context) {
if (empty($context->contextlevel)) {
debugging('Invalid context object specified in get_courseid_from_context() call');
return false;
}
if ($context->contextlevel == CONTEXT_COURSE) {
return $context->instanceid;
}
if ($context->contextlevel < CONTEXT_COURSE) {
return false;
}
if ($context->contextlevel == CONTEXT_MODULE) {
$parentcontexts = get_parent_contexts($context, false);
$parent = reset($parentcontexts);
$parent = get_context_instance_by_id($parent);
return $parent->instanceid;
}
if ($context->contextlevel == CONTEXT_BLOCK) {
$parentcontexts = get_parent_contexts($context, false);
$parent = reset($parentcontexts);
return get_courseid_from_context(get_context_instance_by_id($parent));
}
return false;
}
//////////////////////////////////////
// DB TABLE RELATED FUNCTIONS //
//////////////////////////////////////
/**
* 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 = '';
}
// Get the system context.
$context = get_context_instance(CONTEXT_SYSTEM);
// 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 $CFG, $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));
// finally delete the role itself
// get this before the name is gone for logging
$rolename = $DB->get_field('role', 'name', array('id'=>$roleid));
$DB->delete_records('role', array('id'=>$roleid));
add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '');
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 $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 (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
unassign_capability($capability, $roleid, $contextid);
return true;
}
$existing = $DB->get_record('role_capabilities', array('contextid'=>$contextid, 'roleid'=>$roleid, 'capability'=>$capability));
if ($existing and !$overwrite) { // We want to keep whatever is there already
return true;
}
$cap = new stdClass();
$cap->contextid = $contextid;
$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 {
$c = $DB->get_record('context', array('id'=>$contextid));
$DB->insert_record('role_capabilities', $cap);
}
return true;
}
/**
* Unassign a capability from a role.
*
* @param string $capability the name of the capability
* @param int $roleid the role id
* @param int $contextid null means all contexts
* @return boolean success or failure
*/
function unassign_capability($capability, $roleid, $contextid = null) {
global $DB;
if (!empty($contextid)) {
// delete from context rel, if this is the last override in this context
$DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$contextid));
} else {
$DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
}
return true;
}
/**
* Get the roles that have a given capability assigned to it
*
* This function does not resolve the actual permission of the capability.
* It just checks for permissions and overrides.
* Use get_roles_with_cap_in_context() if resolution is required.
*
* @param string $capability - capability name (string)
* @param string $permission - optional, the permission defined for this capability
* either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
* @param stdClass $context, null means any
* @return array of role objects
*/
function get_roles_with_capability($capability, $permission = null, $context = null) {
global $DB;
if ($context) {
$contexts = get_parent_contexts($context, true);
list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
$contextsql = "AND rc.contextid $insql";
} else {
$params = array();
$contextsql = '';
}
if ($permission) {
$permissionsql = " AND rc.permission = :permission";
$params['permission'] = $permission;
} else {
$permissionsql = '';
}
$sql = "SELECT r.*
FROM {role} r
WHERE r.id IN (SELECT rc.roleid
FROM {role_capabilities} rc
WHERE rc.capability = :capname
$contextsql
$permissionsql)";
$params['capname'] = $capability;
return $DB->get_records_sql($sql, $params);
}
/**
* This function makes a role-assignment (a role for a user in a particular context)
*
* @param int $roleid the role of the id
* @param int $userid userid
* @param int $contextid id of the context
* @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
* @prama int $itemid id of enrolment/auth plugin
* @param string $timemodified defaults to current time
* @return int new/existing id of the assignment
*/
function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
global $USER, $CFG, $DB;
// first of all detect if somebody is using old style parameters
if ($contextid === 0 or is_numeric($component)) {
throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
}
// now validate all parameters
if (empty($roleid)) {
throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
}
if (empty($userid)) {
throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
}
if ($itemid) {
if (strpos($component, '_') === false) {
throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
}
} else {
$itemid = 0;
if ($component !== '' and strpos($component, '_') === false) {
throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
}
}
if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
}
$context = get_context_instance_by_id($contextid, MUST_EXIST);
if (!$timemodified) {
$timemodified = time();
}
/// Check for existing entry
$ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
if ($ras) {
// role already assigned - this should not happen
if (count($ras) > 1) {
//very weird - remove all duplicates!
$ra = array_shift($ras);
foreach ($ras as $r) {
$DB->delete_records('role_assignments', array('id'=>$r->id));
}
} else {
$ra = reset($ras);
}
// actually there is no need to update, reset anything or trigger any event, so just return
return $ra->id;
}
// Create a new entry
$ra = new stdClass();
$ra->roleid = $roleid;
$ra->contextid = $context->id;
$ra->userid = $userid;
$ra->component = $component;
$ra->itemid = $itemid;
$ra->timemodified = $timemodified;
$ra->modifierid = empty($USER->id) ? 0 : $USER->id;
$ra->id = $DB->insert_record('role_assignments', $ra);
// mark context as dirty - again expensive, but needed
mark_context_dirty($context->path);
if (!empty($USER->id) && $USER->id == $userid) {
// If the user is the current user, then do full reload of capabilities too.
load_all_capabilities();
}
events_trigger('role_assigned', $ra);
return $ra->id;
}
/**
* Removes one role assignment
*
* @param int $roleid
* @param int $userid
* @param int $contextid
* @param string $component
* @param int $itemid
* @return void
*/
function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
global $USER, $CFG, $DB;
// first make sure the params make sense
if ($roleid == 0 or $userid == 0 or $contextid == 0) {
throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
}
if ($itemid) {
if (strpos($component, '_') === false) {
throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
}
} else {
$itemid = 0;
if ($component !== '' and strpos($component, '_') === false) {
throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
}
}
role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
}
/**
* Removes multiple role assignments, parameters may contain:
* 'roleid', 'userid', 'contextid', 'component', 'enrolid'.
*
* @param array $params role assignment parameters
* @param bool $subcontexts unassign in subcontexts too
* @param bool $includmanual include manual role assignments too
* @return void
*/
function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
global $USER, $CFG, $DB;
if (!$params) {
throw new coding_exception('Missing parameters in role_unsassign_all() call');
}
$allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
foreach ($params as $key=>$value) {
if (!in_array($key, $allowed)) {
throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
}
}
if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
}
if ($includemanual) {
if (!isset($params['component']) or $params['component'] === '') {
throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
}
}
if ($subcontexts) {
if (empty($params['contextid'])) {
throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
}
}
$ras = $DB->get_records('role_assignments', $params);
foreach($ras as $ra) {
$DB->delete_records('role_assignments', array('id'=>$ra->id));
if ($context = get_context_instance_by_id($ra->contextid)) {
// this is a bit expensive but necessary
mark_context_dirty($context->path);
/// If the user is the current user, then do full reload of capabilities too.
if (!empty($USER->id) && $USER->id == $ra->userid) {
load_all_capabilities();
}
}
events_trigger('role_unassigned', $ra);
}
unset($ras);
// process subcontexts
if ($subcontexts and $context = get_context_instance_by_id($params['contextid'])) {
$contexts = get_child_contexts($context);
$mparams = $params;
foreach($contexts as $context) {
$mparams['contextid'] = $context->id;
$ras = $DB->get_records('role_assignments', $mparams);
foreach($ras as $ra) {
$DB->delete_records('role_assignments', array('id'=>$ra->id));
// this is a bit expensive but necessary
mark_context_dirty($context->path);
/// If the user is the current user, then do full reload of capabilities too.
if (!empty($USER->id) && $USER->id == $ra->userid) {
load_all_capabilities();
}
events_trigger('role_unassigned', $ra);
}
}
}
// do this once more for all manual role assignments
if ($includemanual) {
$params['component'] = '';
role_unassign_all($params, $subcontexts, false);
}
}
/**
* Determines if a user is currently logged in
*
* @return bool
*/
function isloggedin() {
global $USER;
return (!empty($USER->id));
}
/**
* Determines if a user is logged in as real guest user with username 'guest'.
*
* @param int|object $user mixed user object or id, $USER if not specified
* @return bool true if user is the real guest user, false if not logged in or other user
*/
function isguestuser($user = null) {
global $USER, $DB, $CFG;
// make sure we have the user id cached in config table, because we are going to use it a lot
if (empty($CFG->siteguest)) {
if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
// guest does not exist yet, weird
return false;
}
set_config('siteguest', $guestid);
}
if ($user === null) {
$user = $USER;
}
if ($user === null) {
// happens when setting the $USER
return false;
} else if (is_numeric($user)) {
return ($CFG->siteguest == $user);
} else if (is_object($user)) {
if (empty($user->id)) {
return false; // not logged in means is not be guest
} else {
return ($CFG->siteguest == $user->id);
}
} else {
throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
}
}
/**
* Does user have a (temporary or real) guest access to course?
*
* @param stdClass $context
* @param stdClass|int $user
* @return bool
*/
function is_guest($context, $user = null) {
global $USER;
// first find the course context
$coursecontext = get_course_context($context);
// make sure there is a real user specified
if ($user === null) {
$userid = isset($USER->id) ? $USER->id : 0;
} else {
$userid = is_object($user) ? $user->id : $user;
}
if (isguestuser($userid)) {
// can not inspect or be enrolled
return true;
}
if (has_capability('moodle/course:view', $coursecontext, $user)) {
// viewing users appear out of nowhere, they are neither guests nor participants
return false;
}
// consider only real active enrolments here
if (is_enrolled($coursecontext, $user, '', true)) {
return false;
}
return true;
}
/**
* Returns true if the user has moodle/course:view capability in the course,
* this is intended for admins, managers (aka small admins), inspectors, etc.
*
* @param stdClass $context
* @param int|object $user, if null $USER is used
* @param string $withcapability extra capability name
* @return bool
*/
function is_viewing($context, $user = null, $withcapability = '') {
// first find the course context
$coursecontext = get_course_context($context);
if (isguestuser($user)) {
// can not inspect
return false;
}
if (!has_capability('moodle/course:view', $coursecontext, $user)) {
// admins are allowed to inspect courses
return false;
}
if ($withcapability and !has_capability($withcapability, $context, $user)) {
// site admins always have the capability, but the enrolment above blocks
return false;
}
return true;
}
/**
* Returns true if user is enrolled (is participating) in course
* this is intended for students and teachers.
*
* @param object $context
* @param int|object $user, if null $USER is used, otherwise user object or id expected
* @param string $withcapability extra capability name
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
* @return bool
*/
function is_enrolled($context, $user = null, $withcapability = '', $onlyactive = false) {
global $USER, $DB;
// first find the course context
$coursecontext = get_course_context($context);
// make sure there is a real user specified
if ($user === null) {
$userid = isset($USER->id) ? $USER->id : 0;
} else {
$userid = is_object($user) ? $user->id : $user;
}
if (empty($userid)) {
// not-logged-in!
return false;
} else if (isguestuser($userid)) {
// guest account can not be enrolled anywhere
return false;
}
if ($coursecontext->instanceid == SITEID) {
// everybody participates on frontpage
} else {
if ($onlyactive) {
$sql = "SELECT ue.*
FROM {user_enrolments} ue
JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
JOIN {user} u ON u.id = ue.userid
WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
$params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$coursecontext->instanceid);
// this result should be very small, better not do the complex time checks in sql for now ;-)
$enrolments = $DB->get_records_sql($sql, $params);
$now = time();
// make sure the enrol period is ok
$result = false;
foreach ($enrolments as $e) {
if ($e->timestart > $now) {
continue;
}
if ($e->timeend and $e->timeend < $now) {
continue;
}
$result = true;
break;
}
if (!$result) {
return false;
}
} else {
// any enrolment is good for us here, even outdated, disabled or inactive
$sql = "SELECT 'x'
FROM {user_enrolments} ue
JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
JOIN {user} u ON u.id = ue.userid
WHERE ue.userid = :userid AND u.deleted = 0";
$params = array('userid'=>$userid, 'courseid'=>$coursecontext->instanceid);
if (!$DB->record_exists_sql($sql, $params)) {
return false;
}
}
}
if ($withcapability and !has_capability($withcapability, $context, $userid)) {
return false;
}
return true;
}
/**
* Returns true if the user is able to access the course.