Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

7700 lines (6760 sloc) 271.0 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
* - context_course::instance($courseid), context_module::instance($cm->id), context_coursecat::instance($catid)
* - context::instance_by_id($contextid)
* - $context->get_parent_contexts();
* - $context->get_child_contexts();
*
* Whether the user can do something...
* - has_capability()
* - has_any_capability()
* - has_all_capabilities()
* - require_capability()
* - require_login() (from moodlelib)
* - is_enrolled()
* - is_viewing()
* - is_guest()
* - is_siteadmin()
* - isguestuser()
* - isloggedin()
*
* What courses has this user access to?
* - get_enrolled_users()
*
* What users can do X in this context?
* - get_enrolled_users() - at and bellow course context
* - get_users_by_capability() - above course context
*
* Modify roles
* - role_assign()
* - role_unassign()
* - role_unassign_all()
*
* Advanced - for internal use only
* - load_all_capabilities()
* - reload_all_capabilities()
* - has_capability_in_accessdata()
* - get_user_access_sitewide()
* - load_course_context()
* - load_role_access_by_context()
* - etc.
*
* <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=>$roleid)
* [$contextpath] = array($roleid=>$roleid)
* [$contextpath] = array($roleid=>$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 the tree.
*
* First 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($courseid1=>1, $courseid2=>1)
* </code>
*
* <b>Stale accessdata</b>
*
* For the logged-in user, accessdata is long-lived.
*
* On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
* context paths affected by changes. Any check at-or-below
* a dirty context will trigger a transparent reload of accessdata.
*
* Changes at the system level will force the reload for everyone.
*
* <b>Default role caps</b>
* The default role assignment is not in the DB, so we
* add it manually to accessdata.
*
* This means that functions that work directly off the
* DB need to ensure that the default role caps
* are dealt with appropriately.
*
* @package core_access
* @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/** No capability change */
define('CAP_INHERIT', 0);
/** Allow permission, overrides CAP_PREVENT defined in parent contexts */
define('CAP_ALLOW', 1);
/** Prevent permission, overrides CAP_ALLOW defined in parent contexts */
define('CAP_PREVENT', -1);
/** Prohibit permission, overrides everything in current and child contexts */
define('CAP_PROHIBIT', -1000);
/** System context level - only one instance in every system */
define('CONTEXT_SYSTEM', 10);
/** User context level - one instance for each user describing what others can do to user */
define('CONTEXT_USER', 30);
/** Course category context level - one instance for each category */
define('CONTEXT_COURSECAT', 40);
/** Course context level - one instances for each course */
define('CONTEXT_COURSE', 50);
/** Course module context level - one instance for each course module */
define('CONTEXT_MODULE', 70);
/**
* Block context level - one instance for each block, sticky blocks are tricky
* because ppl think they should be able to override them at lower contexts.
* Any other context level instance can be parent of block context.
*/
define('CONTEXT_BLOCK', 80);
/** Capability allow management of trusts - NOT IMPLEMENTED YET - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_MANAGETRUST', 0x0001);
/** Capability allows changes in system configuration - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_CONFIG', 0x0002);
/** Capability allows user to add scripted content - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_XSS', 0x0004);
/** Capability allows access to personal user information - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_PERSONAL', 0x0008);
/** Capability allows users to add content others may see - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_SPAM', 0x0010);
/** capability allows mass delete of data belonging to other users - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
define('RISK_DATALOSS', 0x0020);
/** rolename displays - the name as defined in the role definition, localised if name empty */
define('ROLENAME_ORIGINAL', 0);
/** rolename displays - the name as defined by a role alias at the course level, falls back to ROLENAME_ORIGINAL if alias not present */
define('ROLENAME_ALIAS', 1);
/** rolename displays - Both, like this: Role alias (Original) */
define('ROLENAME_BOTH', 2);
/** rolename displays - the name as defined in the role definition and the shortname in brackets */
define('ROLENAME_ORIGINALANDSHORT', 3);
/** rolename displays - the name as defined by a role alias, in raw form suitable for editing */
define('ROLENAME_ALIAS_RAW', 4);
/** rolename displays - the name is simply short role name */
define('ROLENAME_SHORT', 5);
if (!defined('CONTEXT_CACHE_MAX_SIZE')) {
/** maximum size of context cache - it is possible to tweak this config.php or in any script before inclusion of context.php */
define('CONTEXT_CACHE_MAX_SIZE', 2500);
}
/**
* Although this looks like a global variable, it isn't really.
*
* It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
* It is used to cache various bits of data between function calls for performance reasons.
* Sadly, a PHP global variable is the only way to implement this, without rewriting everything
* as methods of a class, instead of functions.
*
* @access private
* @global stdClass $ACCESSLIB_PRIVATE
* @name $ACCESSLIB_PRIVATE
*/
global $ACCESSLIB_PRIVATE;
$ACCESSLIB_PRIVATE = new stdClass();
$ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache, loaded from DB once per page
$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
$ACCESSLIB_PRIVATE->rolepermissions = array(); // role permissions cache - helps a lot with mem usage
$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.
*
* @access private
* @return void
*/
function accesslib_clear_all_caches_for_unit_testing() {
global $USER;
if (!PHPUNIT_TEST) {
throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
}
accesslib_clear_all_caches(true);
unset($USER->access);
}
/**
* Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
*
* This reset does not touch global $USER.
*
* @access private
* @param bool $resetcontexts
* @return void
*/
function accesslib_clear_all_caches($resetcontexts) {
global $ACCESSLIB_PRIVATE;
$ACCESSLIB_PRIVATE->dirtycontexts = null;
$ACCESSLIB_PRIVATE->accessdatabyuser = array();
$ACCESSLIB_PRIVATE->rolepermissions = array();
$ACCESSLIB_PRIVATE->capabilities = null;
if ($resetcontexts) {
context_helper::reset_caches();
}
}
/**
* Gets the accessdata for role "sitewide" (system down to course)
*
* @access private
* @param int $roleid
* @return array
*/
function get_role_access($roleid) {
global $DB, $ACCESSLIB_PRIVATE;
/* Get it in 1 DB query...
* - relevant role caps at the root and down
* to the course level - but not below
*/
//TODO: MUC - this could be cached in shared memory to speed up first page loading, web crawlers, etc.
$accessdata = get_empty_accessdata();
$accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
// 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
LEFT JOIN {context} cctx
ON (cctx.contextlevel = ".CONTEXT_COURSE." AND ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'").")
WHERE rc.roleid = ? AND cctx.id IS NULL";
$params = array($roleid);
*/
// Note: the commented out query is 100% accurate but slow, so let's cheat instead by hardcoding the blocks mess directly.
$sql = "SELECT COALESCE(ctx.path, bctx.path) AS path, rc.capability, rc.permission
FROM {role_capabilities} rc
LEFT JOIN {context} ctx ON (ctx.id = rc.contextid AND ctx.contextlevel <= ".CONTEXT_COURSE.")
LEFT JOIN ({context} bctx
JOIN {block_instances} bi ON (bi.id = bctx.instanceid)
JOIN {context} pctx ON (pctx.id = bi.parentcontextid AND pctx.contextlevel < ".CONTEXT_COURSE.")
) ON (bctx.id = rc.contextid AND bctx.contextlevel = ".CONTEXT_BLOCK.")
WHERE rc.roleid = :roleid AND (ctx.id IS NOT NULL OR bctx.id IS NOT NULL)";
$params = array('roleid'=>$roleid);
// we need extra caching in CLI scripts and cron
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $rd) {
$k = "{$rd->path}:{$roleid}";
$accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission;
}
$rs->close();
// share the role definitions
foreach ($accessdata['rdef'] as $k=>$unused) {
if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
$ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k];
}
$accessdata['rdef_count']++;
$accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
}
return $accessdata;
}
/**
* Get the default guest role, this is used for guest account,
* search engine spiders, etc.
*
* @return stdClass role record
*/
function get_guest_role() {
global $CFG, $DB;
if (empty($CFG->guestroleid)) {
if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
$guestrole = array_shift($roles); // Pick the first one
set_config('guestroleid', $guestrole->id);
return $guestrole;
} else {
debugging('Can not find any guest role!');
return false;
}
} else {
if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
return $guestrole;
} else {
// somebody is messing with guest roles, remove incorrect setting and try to find a new one
set_config('guestroleid', '');
return get_guest_role();
}
}
}
/**
* Check whether a user has a particular capability in a given context.
*
* For example:
* $context = context_module::instance($cm->id);
* has_capability('mod/forum:replypost', $context)
*
* By default checks the capabilities of the current user, but you can pass a
* different userid. By default will return true for admin users, but you can override that with the fourth argument.
*
* Guest and not-logged-in users can never get any dangerous capability - that is any write capability
* or capabilities with XSS, config or data loss risks.
*
* @category access
*
* @param string $capability the name of the capability to check. For example mod/forum:view
* @param context $context the context to check the capability in. You normally get this with instance method of a context class.
* @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
* @param boolean $doanything If false, ignores effect of admin role assignment
* @return boolean true if the user has this capability. Otherwise false.
*/
function has_capability($capability, context $context, $user = null, $doanything = true) {
global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
if (during_initial_install()) {
if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cli/install.php" or $SCRIPT === "/$CFG->admin/cli/install_database.php") {
// we are in an installer - roles can not work yet
return true;
} else {
return false;
}
}
if (strpos($capability, 'moodle/legacy:') === 0) {
throw new coding_exception('Legacy capabilities can not be used any more!');
}
if (!is_bool($doanything)) {
throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
}
// capability must exist
if (!$capinfo = get_capability_info($capability)) {
debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
return false;
}
if (!isset($USER->id)) {
// should never happen
$USER->id = 0;
debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
}
// make sure there is a real user specified
if ($user === null) {
$userid = $USER->id;
} else {
$userid = is_object($user) ? $user->id : $user;
}
// make sure forcelogin cuts off not-logged-in users if enabled
if (!empty($CFG->forcelogin) and $userid == 0) {
return false;
}
// make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
if (isguestuser($userid) or $userid == 0) {
return false;
}
}
// somehow make sure the user is not deleted and actually exists
if ($userid != 0) {
if ($userid == $USER->id and isset($USER->deleted)) {
// this prevents one query per page, it is a bit of cheating,
// but hopefully session is terminated properly once user is deleted
if ($USER->deleted) {
return false;
}
} else {
if (!context_user::instance($userid, IGNORE_MISSING)) {
// no user context == invalid userid
return false;
}
}
}
// context path/depth must be valid
if (empty($context->path) or $context->depth == 0) {
// this should not happen often, each upgrade tries to rebuild the context paths
debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()');
if (is_siteadmin($userid)) {
return true;
} else {
return false;
}
}
// Find out if user is admin - it is not possible to override the doanything in any way
// and it is not possible to switch to admin role either.
if ($doanything) {
if (is_siteadmin($userid)) {
if ($userid != $USER->id) {
return true;
}
// make sure switchrole is not used in this context
if (empty($USER->access['rsw'])) {
return true;
}
$parts = explode('/', trim($context->path, '/'));
$path = '';
$switched = false;
foreach ($parts as $part) {
$path .= '/' . $part;
if (!empty($USER->access['rsw'][$path])) {
$switched = true;
break;
}
}
if (!$switched) {
return true;
}
//ok, admin switched role in this context, let's use normal access control rules
}
}
// Careful check for staleness...
$context->reload_if_dirty();
if ($USER->id == $userid) {
if (!isset($USER->access)) {
load_all_capabilities();
}
$access =& $USER->access;
} else {
// make sure user accessdata is really loaded
get_user_accessdata($userid, true);
$access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
}
// Load accessdata for below-the-course context if necessary,
// all contexts at and above all courses are already loaded
if ($context->contextlevel != CONTEXT_COURSE and $coursecontext = $context->get_course_context(false)) {
load_course_context($userid, $coursecontext, $access);
}
return has_capability_in_accessdata($capability, $context, $access);
}
/**
* Check if the user has any one of several capabilities from a list.
*
* This is just a utility method that calls has_capability in a loop. Try to put
* the capabilities that most users are likely to have first in the list for best
* performance.
*
* @category access
* @see has_capability()
*
* @param array $capabilities an array of capability names.
* @param context $context the context to check the capability in. You normally get this with instance method of a context class.
* @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
* @param boolean $doanything If false, ignore effect of admin role assignment
* @return boolean true if the user has any of these capabilities. Otherwise false.
*/
function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
foreach ($capabilities as $capability) {
if (has_capability($capability, $context, $user, $doanything)) {
return true;
}
}
return false;
}
/**
* Check if the user has all the capabilities in a list.
*
* This is just a utility method that calls has_capability in a loop. Try to put
* the capabilities that fewest users are likely to have first in the list for best
* performance.
*
* @category access
* @see has_capability()
*
* @param array $capabilities an array of capability names.
* @param context $context the context to check the capability in. You normally get this with instance method of a context class.
* @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
* @param boolean $doanything If false, ignore effect of admin role assignment
* @return boolean true if the user has all of these capabilities. Otherwise false.
*/
function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
foreach ($capabilities as $capability) {
if (!has_capability($capability, $context, $user, $doanything)) {
return false;
}
}
return true;
}
/**
* Check if the user is an admin at the site level.
*
* Please note that use of proper capabilities is always encouraged,
* this function is supposed to be used from core or for temporary hacks.
*
* @category access
*
* @param int|stdClass $user_or_id user id or user object
* @return bool true if user is one of the administrators, false otherwise
*/
function is_siteadmin($user_or_id = null) {
global $CFG, $USER;
if ($user_or_id === null) {
$user_or_id = $USER;
}
if (empty($user_or_id)) {
return false;
}
if (!empty($user_or_id->id)) {
$userid = $user_or_id->id;
} else {
$userid = $user_or_id;
}
$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 int $userid
* @return bool
*/
function has_coursecontact_role($userid) {
global $DB, $CFG;
if (empty($CFG->coursecontact)) {
return false;
}
$sql = "SELECT 1
FROM {role_assignments}
WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
return $DB->record_exists_sql($sql, array('userid'=>$userid));
}
/**
* Does the user have a capability to do something?
*
* Walk the accessdata array and return true/false.
* Deals with prohibits, role switching, aggregating
* capabilities, etc.
*
* The main feature of here is being FAST and with no
* side effects.
*
* Notes:
*
* Switch Role merges with default role
* ------------------------------------
* If you are a teacher in course X, you have at least
* teacher-in-X + defaultloggedinuser-sitewide. So in the
* course you'll have techer+defaultloggedinuser.
* We try to mimic that in switchrole.
*
* Permission evaluation
* ---------------------
* Originally there was an extremely complicated way
* to determine the user access that dealt with
* "locality" or role assignments and role overrides.
* Now we simply evaluate access for each role separately
* and then verify if user has at least one role with allow
* and at the same time no role with prohibit.
*
* @access private
* @param string $capability
* @param context $context
* @param array $accessdata
* @return bool
*/
function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
global $CFG;
// Build $paths as a list of current + all parent "paths" with order bottom-to-top
$path = $context->path;
$paths = array($path);
while($path = rtrim($path, '0123456789')) {
$path = rtrim($path, '/');
if ($path === '') {
break;
}
$paths[] = $path;
}
$roles = array();
$switchedrole = false;
// Find out if role switched
if (!empty($accessdata['rsw'])) {
// From the bottom up...
foreach ($paths as $path) {
if (isset($accessdata['rsw'][$path])) {
// Found a switchrole assignment - check for that role _plus_ the default user role
$roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
$switchedrole = true;
break;
}
}
}
if (!$switchedrole) {
// get all users roles in this context and above
foreach ($paths as $path) {
if (isset($accessdata['ra'][$path])) {
foreach ($accessdata['ra'][$path] as $roleid) {
$roles[$roleid] = null;
}
}
}
}
// Now find out what access is given to each role, going bottom-->up direction
$allowed = false;
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) {
// any CAP_PROHIBIT found means no permission for the user
return false;
}
if (is_null($roles[$roleid])) {
$roles[$roleid] = $perm;
}
}
}
// CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
$allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
}
return $allowed;
}
/**
* A convenience function that tests has_capability, and displays an error if
* the user does not have that capability.
*
* NOTE before Moodle 2.0, this function attempted to make an appropriate
* require_login call before checking the capability. This is no longer the case.
* You must call require_login (or one of its variants) if you want to check the
* user is logged in, before you call this function.
*
* @see has_capability()
*
* @param string $capability the name of the capability to check. For example mod/forum:view
* @param context $context the context to check the capability in. You normally get this with {@link get_context_instance}.
* @param int $userid A user id. By default (null) checks the permissions of the current user.
* @param bool $doanything If false, ignore effect of admin role assignment
* @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
* @param string $stringfile The language file to load the error string from. Defaults to 'error'.
* @return void terminates with an error if the user does not have the given capability.
*/
function require_capability($capability, context $context, $userid = null, $doanything = true,
$errormessage = 'nopermissions', $stringfile = '') {
if (!has_capability($capability, $context, $userid, $doanything)) {
throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
}
}
/**
* 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 can be HUGE.
*
* [ra] => [/path][roleid]=roleid
* [rdef] => [/path:roleid][capability]=permission
*
* @access private
* @param int $userid - the id of the user
* @return array access info array
*/
function get_user_access_sitewide($userid) {
global $CFG, $DB, $ACCESSLIB_PRIVATE;
/* Get in a few cheap DB queries...
* - role assignments
* - relevant role caps
* - above and within this user's RAs
* - below this user's RAs - limited to course level
*/
// raparents collects paths & roles we need to walk up the parenthood to build the minimal rdef
$raparents = array();
$accessdata = get_empty_accessdata();
// start with the default role
if (!empty($CFG->defaultuserroleid)) {
$syscontext = context_system::instance();
$accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
$raparents[$CFG->defaultuserroleid][$syscontext->id] = $syscontext->id;
}
// load the "default frontpage role"
if (!empty($CFG->defaultfrontpageroleid)) {
$frontpagecontext = context_course::instance(get_site()->id);
if ($frontpagecontext->path) {
$accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
$raparents[$CFG->defaultfrontpageroleid][$frontpagecontext->id] = $frontpagecontext->id;
}
}
// preload every assigned role at and above course context
$sql = "SELECT ctx.path, ra.roleid, ra.contextid
FROM {role_assignments} ra
JOIN {context} ctx
ON ctx.id = ra.contextid
LEFT JOIN {block_instances} bi
ON (ctx.contextlevel = ".CONTEXT_BLOCK." AND bi.id = ctx.instanceid)
LEFT JOIN {context} bpctx
ON (bpctx.id = bi.parentcontextid)
WHERE ra.userid = :userid
AND (ctx.contextlevel <= ".CONTEXT_COURSE." OR bpctx.contextlevel < ".CONTEXT_COURSE.")";
$params = array('userid'=>$userid);
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $ra) {
// RAs leafs are arrays to support multi-role assignments...
$accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
$raparents[$ra->roleid][$ra->contextid] = $ra->contextid;
}
$rs->close();
if (empty($raparents)) {
return $accessdata;
}
// now get overrides of interesting roles in all interesting child contexts
// hopefully we will not run out of SQL limits here,
// users would have to have very many roles at/above course context...
$sqls = array();
$params = array();
static $cp = 0;
foreach ($raparents as $roleid=>$ras) {
$cp++;
list($sqlcids, $cids) = $DB->get_in_or_equal($ras, SQL_PARAMS_NAMED, 'c'.$cp.'_');
$params = array_merge($params, $cids);
$params['r'.$cp] = $roleid;
$sqls[] = "(SELECT ctx.path, rc.roleid, rc.capability, rc.permission
FROM {role_capabilities} rc
JOIN {context} ctx
ON (ctx.id = rc.contextid)
JOIN {context} pctx
ON (pctx.id $sqlcids
AND (ctx.id = pctx.id
OR ctx.path LIKE ".$DB->sql_concat('pctx.path',"'/%'")."
OR pctx.path LIKE ".$DB->sql_concat('ctx.path',"'/%'")."))
LEFT JOIN {block_instances} bi
ON (ctx.contextlevel = ".CONTEXT_BLOCK." AND bi.id = ctx.instanceid)
LEFT JOIN {context} bpctx
ON (bpctx.id = bi.parentcontextid)
WHERE rc.roleid = :r{$cp}
AND (ctx.contextlevel <= ".CONTEXT_COURSE." OR bpctx.contextlevel < ".CONTEXT_COURSE.")
)";
}
// fixed capability order is necessary for rdef dedupe
$rs = $DB->get_recordset_sql(implode("\nUNION\n", $sqls). "ORDER BY capability", $params);
foreach ($rs as $rd) {
$k = $rd->path.':'.$rd->roleid;
$accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission;
}
$rs->close();
// share the role definitions
foreach ($accessdata['rdef'] as $k=>$unused) {
if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
$ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k];
}
$accessdata['rdef_count']++;
$accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
}
return $accessdata;
}
/**
* Add to the access ctrl array the data needed by a user for a given course.
*
* This function injects all course related access info into the accessdata array.
*
* @access private
* @param int $userid the id of the user
* @param context_course $coursecontext course context
* @param array $accessdata accessdata array (modified)
* @return void modifies $accessdata parameter
*/
function load_course_context($userid, context_course $coursecontext, &$accessdata) {
global $DB, $CFG, $ACCESSLIB_PRIVATE;
if (empty($coursecontext->path)) {
// weird, this should not happen
return;
}
if (isset($accessdata['loaded'][$coursecontext->instanceid])) {
// already loaded, great!
return;
}
$roles = array();
if (empty($userid)) {
if (!empty($CFG->notloggedinroleid)) {
$roles[$CFG->notloggedinroleid] = $CFG->notloggedinroleid;
}
} else if (isguestuser($userid)) {
if ($guestrole = get_guest_role()) {
$roles[$guestrole->id] = $guestrole->id;
}
} else {
// Interesting role assignments at, above and below the course context
list($parentsaself, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
$params['userid'] = $userid;
$params['children'] = $coursecontext->path."/%";
$sql = "SELECT ra.*, ctx.path
FROM {role_assignments} ra
JOIN {context} ctx ON ra.contextid = ctx.id
WHERE ra.userid = :userid AND (ctx.id $parentsaself OR ctx.path LIKE :children)";
$rs = $DB->get_recordset_sql($sql, $params);
// add missing role definitions
foreach ($rs as $ra) {
$accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
$roles[$ra->roleid] = $ra->roleid;
}
$rs->close();
// add the "default frontpage role" when on the frontpage
if (!empty($CFG->defaultfrontpageroleid)) {
$frontpagecontext = context_course::instance(get_site()->id);
if ($frontpagecontext->id == $coursecontext->id) {
$roles[$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid;
}
}
// do not forget the default role
if (!empty($CFG->defaultuserroleid)) {
$roles[$CFG->defaultuserroleid] = $CFG->defaultuserroleid;
}
}
if (!$roles) {
// weird, default roles must be missing...
$accessdata['loaded'][$coursecontext->instanceid] = 1;
return;
}
// now get overrides of interesting roles in all interesting contexts (this course + children + parents)
$params = array('c'=>$coursecontext->id);
list($parentsaself, $rparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
$params = array_merge($params, $rparams);
list($roleids, $rparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'r_');
$params = array_merge($params, $rparams);
$sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
FROM {role_capabilities} rc
JOIN {context} ctx
ON (ctx.id = rc.contextid)
JOIN {context} cctx
ON (cctx.id = :c
AND (ctx.id $parentsaself OR ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'")."))
WHERE rc.roleid $roleids
ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe
$rs = $DB->get_recordset_sql($sql, $params);
$newrdefs = array();
foreach ($rs as $rd) {
$k = $rd->path.':'.$rd->roleid;
if (isset($accessdata['rdef'][$k])) {
continue;
}
$newrdefs[$k][$rd->capability] = (int)$rd->permission;
}
$rs->close();
// share new role definitions
foreach ($newrdefs as $k=>$unused) {
if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
$ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k];
}
$accessdata['rdef_count']++;
$accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
}
$accessdata['loaded'][$coursecontext->instanceid] = 1;
// we want to deduplicate the USER->access from time to time, this looks like a good place,
// because we have to do it before the end of session
dedupe_user_access();
}
/**
* 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 temporary course roles.
*
* @access private
* @param int $roleid the id of the user
* @param context $context needs path!
* @param array $accessdata accessdata array (is modified)
* @return array
*/
function load_role_access_by_context($roleid, context $context, &$accessdata) {
global $DB, $ACCESSLIB_PRIVATE;
/* Get the relevant rolecaps into rdef
* - relevant role caps
* - at ctx and above
* - below this ctx
*/
if (empty($context->path)) {
// weird, this should not happen
return;
}
list($parentsaself, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
$params['roleid'] = $roleid;
$params['childpath'] = $context->path.'/%';
$sql = "SELECT ctx.path, rc.capability, rc.permission
FROM {role_capabilities} rc
JOIN {context} ctx ON (rc.contextid = ctx.id)
WHERE rc.roleid = :roleid AND (ctx.id $parentsaself OR ctx.path LIKE :childpath)
ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe
$rs = $DB->get_recordset_sql($sql, $params);
$newrdefs = array();
foreach ($rs as $rd) {
$k = $rd->path.':'.$roleid;
if (isset($accessdata['rdef'][$k])) {
continue;
}
$newrdefs[$k][$rd->capability] = (int)$rd->permission;
}
$rs->close();
// share new role definitions
foreach ($newrdefs as $k=>$unused) {
if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
$ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k];
}
$accessdata['rdef_count']++;
$accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
}
}
/**
* Returns empty accessdata structure.
*
* @access private
* @return array empt accessdata
*/
function get_empty_accessdata() {
$accessdata = array(); // named list
$accessdata['ra'] = array();
$accessdata['rdef'] = array();
$accessdata['rdef_count'] = 0; // this bloody hack is necessary because count($array) is slooooowwww in PHP
$accessdata['rdef_lcc'] = 0; // rdef_count during the last compression
$accessdata['loaded'] = array(); // loaded course contexts
$accessdata['time'] = time();
$accessdata['rsw'] = array();
return $accessdata;
}
/**
* Get accessdata for a given user.
*
* @access private
* @param int $userid
* @param bool $preloadonly true means do not return access array
* @return array accessdata
*/
function get_user_accessdata($userid, $preloadonly=false) {
global $CFG, $ACCESSLIB_PRIVATE, $USER;
if (!empty($USER->acces['rdef']) and empty($ACCESSLIB_PRIVATE->rolepermissions)) {
// share rdef from USER session with rolepermissions cache in order to conserve memory
foreach($USER->acces['rdef'] as $k=>$v) {
$ACCESSLIB_PRIVATE->rolepermissions[$k] =& $USER->acces['rdef'][$k];
}
$ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->acces;
}
if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
if (empty($userid)) {
if (!empty($CFG->notloggedinroleid)) {
$accessdata = get_role_access($CFG->notloggedinroleid);
} else {
// weird
return get_empty_accessdata();
}
} else if (isguestuser($userid)) {
if ($guestrole = get_guest_role()) {
$accessdata = get_role_access($guestrole->id);
} else {
//weird
return get_empty_accessdata();
}
} else {
$accessdata = get_user_access_sitewide($userid); // includes default role and frontpage role
}
$ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
}
if ($preloadonly) {
return;
} else {
return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
}
}
/**
* Try to minimise the size of $USER->access by eliminating duplicate override storage,
* this function looks for contexts with the same overrides and shares them.
*
* @access private
* @return void
*/
function dedupe_user_access() {
global $USER;
if (CLI_SCRIPT) {
// no session in CLI --> no compression necessary
return;
}
if (empty($USER->access['rdef_count'])) {
// weird, this should not happen
return;
}
// the rdef is growing only, we never remove stuff from it, the rdef_lcc helps us to detect new stuff in rdef
if ($USER->access['rdef_count'] - $USER->access['rdef_lcc'] > 10) {
// do not compress after each change, wait till there is more stuff to be done
return;
}
$hashmap = array();
foreach ($USER->access['rdef'] as $k=>$def) {
$hash = sha1(serialize($def));
if (isset($hashmap[$hash])) {
$USER->access['rdef'][$k] =& $hashmap[$hash];
} else {
$hashmap[$hash] =& $USER->access['rdef'][$k];
}
}
$USER->access['rdef_lcc'] = $USER->access['rdef_count'];
}
/**
* A convenience function to completely load all the capabilities
* for the current user. It is called from has_capability() and functions change permissions.
*
* Call it only _after_ you've setup $USER and called check_enrolment_plugins();
* @see check_enrolment_plugins()
*
* @access private
* @return void
*/
function load_all_capabilities() {
global $USER;
// roles not installed yet - we are in the middle of installation
if (during_initial_install()) {
return;
}
if (!isset($USER->id)) {
// this should not happen
$USER->id = 0;
}
unset($USER->access);
$USER->access = get_user_accessdata($USER->id);
// deduplicate the overrides to minimize session size
dedupe_user_access();
// Clear to force a refresh
unset($USER->mycourses);
// init/reset internal enrol caches - active course enrolments and temp access
$USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
}
/**
* A convenience function to completely reload all the capabilities
* for the current user when roles have been updated in a relevant
* context -- but PRESERVING switchroles and loginas.
* This function resets all accesslib and context caches.
*
* That is - completely transparent to the user.
*
* Note: reloads $USER->access completely.
*
* @access private
* @return void
*/
function reload_all_capabilities() {
global $USER, $DB, $ACCESSLIB_PRIVATE;
// copy switchroles
$sw = array();
if (!empty($USER->access['rsw'])) {
$sw = $USER->access['rsw'];
}
accesslib_clear_all_caches(true);
unset($USER->access);
$ACCESSLIB_PRIVATE->dirtycontexts = array(); // prevent dirty flags refetching on this page
load_all_capabilities();
foreach ($sw as $path => $roleid) {
if ($record = $DB->get_record('context', array('path'=>$path))) {
$context = context::instance_by_id($record->id);
role_switch($roleid, $context);
}
}
}
/**
* Adds a temp role to current USER->access array.
*
* Useful for the "temporary guest" access we grant to logged-in users.
* This is useful for enrol plugins only.
*
* @since 2.2
* @param context_course $coursecontext
* @param int $roleid
* @return void
*/
function load_temp_course_role(context_course $coursecontext, $roleid) {
global $USER, $SITE;
if (empty($roleid)) {
debugging('invalid role specified in load_temp_course_role()');
return;
}
if ($coursecontext->instanceid == $SITE->id) {
debugging('Can not use temp roles on the frontpage');
return;
}
if (!isset($USER->access)) {
load_all_capabilities();
}
$coursecontext->reload_if_dirty();
if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
return;
}
// load course stuff first
load_course_context($USER->id, $coursecontext, $USER->access);
$USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
load_role_access_by_context($roleid, $coursecontext, $USER->access);
}
/**
* Removes any extra guest roles from current USER->access array.
* This is useful for enrol plugins only.
*
* @since 2.2
* @param context_course $coursecontext
* @return void
*/
function remove_temp_course_roles(context_course $coursecontext) {
global $DB, $USER, $SITE;
if ($coursecontext->instanceid == $SITE->id) {
debugging('Can not use temp roles on the frontpage');
return;
}
if (empty($USER->access['ra'][$coursecontext->path])) {
//no roles here, weird
return;
}
$sql = "SELECT DISTINCT ra.roleid AS id
FROM {role_assignments} ra
WHERE ra.contextid = :contextid AND ra.userid = :userid";
$ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
$USER->access['ra'][$coursecontext->path] = array();
foreach($ras as $r) {
$USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
}
}
/**
* Returns array of all role archetypes.
*
* @return array
*/
function get_role_archetypes() {
return array(
'manager' => 'manager',
'coursecreator' => 'coursecreator',
'editingteacher' => 'editingteacher',
'teacher' => 'teacher',
'student' => 'student',
'guest' => 'guest',
'user' => 'user',
'frontpage' => 'frontpage'
);
}
/**
* Assign the defaults found in this capability definition to roles that have
* the corresponding legacy capabilities assigned to them.
*
* @param string $capability
* @param array $legacyperms an array in the format (example):
* 'guest' => CAP_PREVENT,
* 'student' => CAP_ALLOW,
* 'teacher' => CAP_ALLOW,
* 'editingteacher' => CAP_ALLOW,
* 'coursecreator' => CAP_ALLOW,
* 'manager' => CAP_ALLOW
* @return boolean success or failure.
*/
function assign_legacy_capabilities($capability, $legacyperms) {
$archetypes = get_role_archetypes();
foreach ($legacyperms as $type => $perm) {
$systemcontext = context_system::instance();
if ($type === 'admin') {
debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
$type = 'manager';
}
if (!array_key_exists($type, $archetypes)) {
print_error('invalidlegacy', '', '', $type);
}
if ($roles = get_archetype_roles($type)) {
foreach ($roles as $role) {
// Assign a site level capability.
if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
return false;
}
}
}
}
return true;
}
/**
* Verify capability risks.
*
* @param stdClass $capability a capability - a row from the capabilities table.
* @return boolean whether this capability is safe - that is, whether people with the
* safeoverrides capability should be allowed to change it.
*/
function is_safe_capability($capability) {
return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
}
/**
* Get the local override (if any) for a given capability in a role in a context
*
* @param int $roleid
* @param int $contextid
* @param string $capability
* @return stdClass local capability override
*/
function get_local_override($roleid, $contextid, $capability) {
global $DB;
return $DB->get_record('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 = context::instance_by_id($contextid, MUST_EXIST);
$course = null;
$cm = null;
if ($context->contextlevel == CONTEXT_COURSE) {
$course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
} else if ($context->contextlevel == CONTEXT_MODULE) {
$cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
$course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
} else if ($context->contextlevel == CONTEXT_BLOCK) {
$parent = $context->get_parent_context();
if ($parent->contextlevel == CONTEXT_COURSE) {
$course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
} else if ($parent->contextlevel == CONTEXT_MODULE) {
$cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
$course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
}
}
return array($context, $course, $cm);
}
/**
* Function that creates a role
*
* @param string $name role name
* @param string $shortname role short name
* @param string $description role description
* @param string $archetype
* @return int id or dml_exception
*/
function create_role($name, $shortname, $description, $archetype = '') {
global $DB;
if (strpos($archetype, 'moodle/legacy:') !== false) {
throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
}
// verify role archetype actually exists
$archetypes = get_role_archetypes();
if (empty($archetypes[$archetype])) {
$archetype = '';
}
// Insert the role record.
$role = new stdClass();
$role->name = $name;
$role->shortname = $shortname;
$role->description = $description;
$role->archetype = $archetype;
//find free sortorder number
$role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
if (empty($role->sortorder)) {
$role->sortorder = 1;
}
$id = $DB->insert_record('role', $role);
return $id;
}
/**
* Function that deletes a role and cleanups up after it
*
* @param int $roleid id of role to delete
* @return bool always true
*/
function delete_role($roleid) {
global $DB;
// first unssign all users
role_unassign_all(array('roleid'=>$roleid));
// cleanup all references to this role, ignore errors
$DB->delete_records('role_capabilities', array('roleid'=>$roleid));
$DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
$DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
$DB->delete_records('role_allow_override', array('roleid'=>$roleid));
$DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
$DB->delete_records('role_names', array('roleid'=>$roleid));
$DB->delete_records('role_context_levels', array('roleid'=>$roleid));
// 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.
*
* NOTE: use $context->mark_dirty() after this
*
* @param string $capability string name
* @param int $permission CAP_ constants
* @param int $roleid role id
* @param int|context $contextid context id
* @param bool $overwrite
* @return bool always true or exception
*/
function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
global $USER, $DB;
if ($contextid instanceof context) {
$context = $contextid;
} else {
$context = context::instance_by_id($contextid);
}
if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
unassign_capability($capability, $roleid, $context->id);
return true;
}
$existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
if ($existing and !$overwrite) { // We want to keep whatever is there already
return true;
}
$cap = new stdClass();
$cap->contextid = $context->id;
$cap->roleid = $roleid;
$cap->capability = $capability;
$cap->permission = $permission;
$cap->timemodified = time();
$cap->modifierid = empty($USER->id) ? 0 : $USER->id;
if ($existing) {
$cap->id = $existing->id;
$DB->update_record('role_capabilities', $cap);
} else {
if ($DB->record_exists('context', array('id'=>$context->id))) {
$DB->insert_record('role_capabilities', $cap);
}
}
return true;
}
/**
* Unassign a capability from a role.
*
* NOTE: use $context->mark_dirty() after this
*
* @param string $capability the name of the capability
* @param int $roleid the role id
* @param int|context $contextid null means all contexts
* @return boolean true or exception
*/
function unassign_capability($capability, $roleid, $contextid = null) {
global $DB;
if (!empty($contextid)) {
if ($contextid instanceof context) {
$context = $contextid;
} else {
$context = context::instance_by_id($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'=>$context->id));
} 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 records
*/
function get_roles_with_capability($capability, $permission = null, $context = null) {
global $DB;
if ($context) {
$contexts = $context->get_parent_context_ids(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|context $contextid id of the context
* @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
* @param 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, $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);
}
if ($contextid instanceof context) {
$context = $contextid;
} else {
$context = 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
$context->mark_dirty();
if (!empty($USER->id) && $USER->id == $userid) {
// If the user is the current user, then do full reload of capabilities too.
reload_all_capabilities();
}
events_trigger('role_assigned', $ra);
return $ra->id;
}
/**
* Removes one role assignment
*
* @param int $roleid
* @param int $userid
* @param int|context $contextid
* @param string $component
* @param int $itemid
* @return void
*/
function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
// 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 $includemanual 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 = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
// this is a bit expensive but necessary
$context->mark_dirty();
// If the user is the current user, then do full reload of capabilities too.
if (!empty($USER->id) && $USER->id == $ra->userid) {
reload_all_capabilities();
}
}
events_trigger('role_unassigned', $ra);
}
unset($ras);
// process subcontexts
if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
if ($params['contextid'] instanceof context) {
$context = $params['contextid'];
} else {
$context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
}
if ($context) {
$contexts = $context->get_child_contexts();
$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
$context->mark_dirty();
// If the user is the current user, then do full reload of capabilities too.
if (!empty($USER->id) && $USER->id == $ra->userid) {
reload_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
*
* @category access
*
* @return bool
*/
function isloggedin() {
global $USER;
return (!empty($USER->id));
}
/**
* Determines if a user is logged in as real guest user with username 'guest'.
*
* @category access
*
* @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?
*
* @category access
*
* @param context $context
* @param stdClass|int $user
* @return bool
*/
function is_guest(context $context, $user = null) {
global $USER;
// first find the course context
$coursecontext = $context->get_course_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.
*
* @category access
*
* @param context $context
* @param int|stdClass $user if null $USER is used
* @param string $withcapability extra capability name
* @return bool
*/
function is_viewing(context $context, $user = null, $withcapability = '') {
// first find the course context
$coursecontext = $context->get_course_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.
*
* Since 2.2 the result for active enrolments and current user are cached.
*
* @package core_enrol
* @category access
*
* @param context $context
* @param int|stdClass $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 $context, $user = null, $withcapability = '', $onlyactive = false) {
global $USER, $DB;
// first find the course context
$coursecontext = $context->get_course_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 {
// try cached info first - the enrolled flag is set only when active enrolment present
if ($USER->id == $userid) {
$coursecontext->reload_if_dirty();
if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
if ($withcapability and !has_capability($withcapability, $context, $userid)) {
return false;
}
return true;
}
}
}
if ($onlyactive) {
// look for active enrolments only
$until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
if ($until === false) {
return false;
}
if ($USER->id == $userid) {
if ($until == 0) {
$until = ENROL_MAX_TIMESTAMP;
}
$USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
remove_temp_course_roles($coursecontext);
}
}
} 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.
*
* This function is in no way, shape, or form a substitute for require_login.
* It should only be used in circumstances where it is not possible to call require_login
* such as the navigation.
*
* This function checks many of the methods of access to a course such as the view
* capability, enrollments, and guest access. It also makes use of the cache
* generated by require_login for guest access.
*
* The flags within the $USER object that are used here should NEVER be used outside
* of this function can_access_course and require_login. Doing so WILL break future
* versions.
*
* @param stdClass $course record
* @param stdClass|int|null $user user record or id, current user if null
* @param string $withcapability Check for this capability as well.
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
* @return boolean Returns true if the user is able to access the course
*/
function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
global $DB, $USER;
// this function originally accepted $coursecontext parameter
if ($course instanceof context) {
if ($course instanceof context_course) {
debugging('deprecated context parameter, please use $course record');
$coursecontext = $course;
$course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
} else {
debugging('Invalid context parameter, please use $course record');
return false;
}
} else {
$coursecontext = context_course::instance($course->id);
}
if (!isset($USER->id)) {
// should never happen
$USER->id = 0;
debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
}
// make sure there is a user specified
if ($user === null) {
$userid = $USER->id;
} else {
$userid = is_object($user) ? $user->id : $user;
}
unset($user);
if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
return false;
}
if ($userid == $USER->id) {
if (!empty($USER->access['rsw'][$coursecontext->path])) {
// the fact that somebody switched role means they can access the course no matter to what role they switched
return true;
}
}
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
return false;
}
if (is_viewing($coursecontext, $userid)) {
return true;
}
if ($userid != $USER->id) {
// for performance reasons we do not verify temporary guest access for other users, sorry...
return is_enrolled($coursecontext, $userid, '', $onlyactive);
}
// === from here we deal only with $USER ===
$coursecontext->reload_if_dirty();
if (isset($USER->enrol['enrolled'][$course->id])) {
if ($USER->enrol['enrolled'][$course->id] > time()) {
return true;
}
}
if (isset($USER->enrol['tempguest'][$course->id])) {
if ($USER->enrol['tempguest'][$course->id] > time()) {
return true;
}
}
if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
return true;
}
// if not enrolled try to gain temporary guest access
$instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
$enrols = enrol_get_plugins(true);
foreach($instances as $instance) {
if (!isset($enrols[$instance->enrol])) {
continue;
}
// Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
$until = $enrols[$instance->enrol]->try_guestaccess($instance);
if ($until !== false and $until > time()) {
$USER->enrol['tempguest'][$course->id] = $until;
return true;
}
}
if (isset($USER->enrol['tempguest'][$course->id])) {
unset($USER->enrol['tempguest'][$course->id]);
remove_temp_course_roles($coursecontext);
}
return false;
}
/**
* Returns array with sql code and parameters returning all ids
* of users enrolled into course.
*
* This function is using 'eu[0-9]+_' prefix for table names and parameters.
*
* @package core_enrol
* @category access
*
* @param context $context
* @param string $withcapability
* @param int $groupid 0 means ignore groups, any other value limits the result by group id
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
* @return array list($sql, $params)
*/
function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
global $DB, $CFG;
// use unique prefix just in case somebody makes some SQL magic with the result
static $i = 0;
$i++;
$prefix = 'eu'.$i.'_';
// first find the course context
$coursecontext = $context->get_course_context();
$isfrontpage = ($coursecontext->instanceid == SITEID);
$joins = array();
$wheres = array();
$params = array();
list($contextids, $contextpaths) = get_context_info_list($context);
// get all relevant capability info for all roles
if ($withcapability) {
list($incontexts, $cparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'ctx');
$cparams['cap'] = $withcapability;
$defs = array();
$sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.path
FROM {role_capabilities} rc
JOIN {context} ctx on rc.contextid = ctx.id
WHERE rc.contextid $incontexts AND rc.capability = :cap";
$rcs = $DB->get_records_sql($sql, $cparams);
foreach ($rcs as $rc) {
$defs[$rc->path][$rc->roleid] = $rc->permission;
}
$access = array();
if (!empty($defs)) {
foreach ($contextpaths as $path) {
if (empty($defs[$path])) {
continue;
}
foreach($defs[$path] as $roleid => $perm) {
if ($perm == CAP_PROHIBIT) {
$access[$roleid] = CAP_PROHIBIT;
continue;
}
if (!isset($access[$roleid])) {
$access[$roleid] = (int)$perm;
}
}
}
}
unset($defs);
// make lists of roles that are needed and prohibited
$needed = array(); // one of these is enough
$prohibited = array(); // must not have any of these
foreach ($access as $roleid => $perm) {
if ($perm == CAP_PROHIBIT) {
unset($needed[$roleid]);
$prohibited[$roleid] = true;
} else if ($perm == CAP_ALLOW and empty($prohibited[$roleid])) {
$needed[$roleid] = true;
}
}
$defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
$defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
$nobody = false;
if ($isfrontpage) {
if (!empty($prohibited[$defaultuserroleid]) or !empty($prohibited[$defaultfrontpageroleid])) {
$nobody = true;
} else if (!empty($needed[$defaultuserroleid]) or !empty($needed[$defaultfrontpageroleid])) {
// everybody not having prohibit has the capability
$needed = array();
} else if (empty($needed)) {
$nobody = true;
}
} else {
if (!empty($prohibited[$defaultuserroleid])) {
$nobody = true;
} else if (!empty($needed[$defaultuserroleid])) {
// everybody not having prohibit has the capability
$needed = array();
} else if (empty($needed)) {
$nobody = true;
}
}
if ($nobody) {
// nobody can match so return some SQL that does not return any results
$wheres[] = "1 = 2";
} else {
if ($needed) {
$ctxids = implode(',', $contextids);
$roleids = implode(',', array_keys($needed));
$joins[] = "JOIN {role_assignments} {$prefix}ra3 ON ({$prefix}ra3.userid = {$prefix}u.id AND {$prefix}ra3.roleid IN ($roleids) AND {$prefix}ra3.contextid IN ($ctxids))";
}
if ($prohibited) {
$ctxids = implode(',', $contextids);
$roleids = implode(',', array_keys($prohibited));
$joins[] = "LEFT JOIN {role_assignments} {$prefix}ra4 ON ({$prefix}ra4.userid = {$prefix}u.id AND {$prefix}ra4.roleid IN ($roleids) AND {$prefix}ra4.contextid IN ($ctxids))";
$wheres[] = "{$prefix}ra4.id IS NULL";
}
if ($groupid) {
$joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}u.id AND {$prefix}gm.groupid = :{$prefix}gmid)";
$params["{$prefix}gmid"] = $groupid;
}
}
} else {
if ($groupid) {
$joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}u.id AND {$prefix}gm.groupid = :{$prefix}gmid)";
$params["{$prefix}gmid"] = $groupid;
}
}
$wheres[] = "{$prefix}u.deleted = 0 AND {$prefix}u.id <> :{$prefix}guestid";
$params["{$prefix}guestid"] = $CFG->siteguest;
if ($isfrontpage) {
// all users are "enrolled" on the frontpage
} else {
$joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$prefix}u.id";
$joins[] = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
$params[$prefix.'courseid'] = $coursecontext->instanceid;
if ($onlyactive) {
$wheres[] = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
$wheres[] = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
$now = round(time(), -2); // rounding helps caching in DB
$params = array_merge($params, array($prefix.'enabled'=>ENROL_INSTANCE_ENABLED,
$prefix.'active'=>ENROL_USER_ACTIVE,
$prefix.'now1'=>$now, $prefix.'now2'=>$now));
}
}
$joins = implode("\n", $joins);
$wheres = "WHERE ".implode(" AND ", $wheres);
$sql = "SELECT DISTINCT {$prefix}u.id
FROM {user} {$prefix}u
$joins
$wheres";
return array($sql, $params);
}
/**
* Returns list of users enrolled into course.
*
* @package core_enrol
* @category access
*
* @param context $context
* @param string $withcapability
* @param int $groupid 0 means ignore groups, any other value limits the result by group id
* @param string $userfields requested user record fields
* @param string $orderby
* @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
* @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
* @return array of user records
*/
function get_enrolled_users(context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = null,
$limitfrom = 0, $limitnum = 0, $onlyactive = false) {
global $DB;
list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
$sql = "SELECT $userfields
FROM {user} u
JOIN ($esql) je ON je.id = u.id
WHERE u.deleted = 0";
if ($orderby) {
$sql = "$sql ORDER BY $orderby";
} else {
list($sort, $sortparams) = users_order_by_sql('u');
$sql = "$sql ORDER BY $sort";
$params = array_merge($params, $sortparams);
}
return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
}
/**
* Counts list of users enrolled into course (as per above function)
*
* @package core_enrol
* @category access
*
* @param context $context
* @param string $withcapability
* @param int $groupid 0 means ignore groups, any other value limits the result by group id
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
* @return array of user records
*/
function count_enrolled_users(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
global $DB;
list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
$sql = "SELECT count(u.id)
FROM {user} u
JOIN ($esql) je ON je.id = u.id
WHERE u.deleted = 0";
return $DB->count_records_sql($sql, $params);
}
/**
* Loads the capability definitions for the component (from file).
*
* Loads the capability definitions for the component (from file). If no
* capabilities are defined for the component, we simply return an empty array.
*
* @access private
* @param string $component full plugin name, examples: 'moodle', 'mod_forum'
* @return array array of capabilities
*/
function load_capability_def($component) {
$defpath = get_component_directory($component).'/db/access.php';
$capabilities = array();
if (file_exists($defpath)) {
require($defpath);
if (!empty(${$component.'_capabilities'})) {
// BC capability array name
// since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
$capabilities = ${$component.'_capabilities'};
}
}
return $capabilities;
}
/**
* Gets the capabilities that have been cached in the database for this component.
*
* @access private
* @param string $component - examples: 'moodle', 'mod_forum'
* @return array array of capabilities
*/
function get_cached_capabilities($component = 'moodle') {
global $DB;
return $DB->get_records('capabilities', array('component'=>$component));
}
/**
* Returns default capabilities for given role archetype.
*
* @param string $archetype role archetype
* @return array
*/
function get_default_capabilities($archetype) {
global $DB;
if (!$archetype) {
return array();
}
$alldefs = array();
$defaults = array();
$components = array();
$allcaps = $DB->get_records('capabilities');
foreach ($allcaps as $cap) {
if (!in_array($cap->component, $components)) {
$components[] = $cap->component;
$alldefs = array_merge($alldefs, load_capability_def($cap->component));
}
}
foreach($alldefs as $name=>$def) {
// Use array 'archetypes if available. Only if not specified, use 'legacy'.
if (isset($def['archetypes'])) {
if (isset($def['archetypes'][$archetype])) {
$defaults[$name] = $def['archetypes'][$archetype];
}
// 'legacy' is for backward compatibility with 1.9 access.php
} else {
if (isset($def['legacy'][$archetype])) {
$defaults[$name] = $def['legacy'][$archetype];
}
}
}
return $defaults;
}
/**
* Reset role capabilities to default according to selected role archetype.
* If no archetype selected, removes all capabilities.
*
* @param int $roleid
* @return void
*/
function reset_role_capabilities($roleid) {
global $DB;
$role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
$defaultcaps = get_default_capabilities($role->archetype);
$systemcontext = context_system::instance();
$DB->delete_records('role_capabilities', array('roleid'=>$roleid));
foreach($defaultcaps as $cap=>$permission) {
assign_capability($cap, $permission, $roleid, $systemcontext->id);
}
}
/**
* Updates the capabilities table with the component capability definitions.
* If no parameters are given, the function updates the core moodle
* capabilities.
*
* Note that the absence of the db/access.php capabilities definition file
* will cause any stored capabilities for the component to be removed from
* the database.
*
* @access private
* @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
* @return boolean true if success, exception in case of any problems
*/
function update_capabilities($component = 'moodle') {
global $DB, $OUTPUT;
$storedcaps = array();
$filecaps = load_capability_def($component);
foreach($filecaps as $capname=>$unused) {
if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
}
}
$cachedcaps = get_cached_capabilities($component);
if ($cachedcaps) {
foreach ($cachedcaps as $cachedcap) {
array_push($storedcaps, $cachedcap->name);
// update risk bitmasks and context levels in existing capabilities if needed
if (array_key_exists($cachedcap->name, $filecaps)) {
if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
$filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
}
if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
$updatecap = new stdClass();
$updatecap->id = $cachedcap->id;
$updatecap->captype = $filecaps[$cachedcap->name]['captype'];
$DB->update_record('capabilities', $updatecap);
}
if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
$updatecap = new stdClass();
$updatecap->id = $cachedcap->id;
$updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
$DB->update_record('capabilities', $updatecap);
}
if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
$filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
}
if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
$updatecap = new stdClass();
$updatecap->id = $cachedcap->id;
$updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
$DB->update_record('capabilities', $updatecap);
}
}
}
}
// Are there new capabilities in the file definition?
$newcaps = array();
foreach ($filecaps as $filecap => $def) {
if (!$storedcaps ||
($storedcaps && in_array($filecap, $storedcaps) === false)) {
if (!array_key_exists('riskbitmask', $def)) {
$def['riskbitmask'] = 0; // no risk if not specified
}
$newcaps[$filecap] = $def;
}
}
// Add new capabilities to the stored definition.
$existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
foreach ($newcaps as $capname => $capdef) {
$capability = new stdClass();
$capability->name = $capname;
$capability->captype = $capdef['captype'];
$capability->contextlevel = $capdef['contextlevel'];
$capability->component = $component;
$capability->riskbitmask = $capdef['riskbitmask'];
$DB->insert_record('capabilities', $capability, false);
if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
foreach ($rolecapabilities as $rolecapability){
//assign_capability will update rather than insert if capability exists
if (!assign_capability($capname, $rolecapability->permission,
$rolecapability->roleid, $rolecapability->contextid, true)){
echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
}
}
}
// we ignore archetype key if we have cloned permissions
} else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
assign_legacy_capabilities($capname, $capdef['archetypes']);
// 'legacy' is for backward compatibility with 1.9 access.php
} else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
assign_legacy_capabilities($capname, $capdef['legacy']);
}
}
// Are there any capabilities that have been removed from the file
// definition that we need to delete from the stored capabilities and
// role assignments?
capabilities_cleanup($component, $filecaps);
// reset static caches
accesslib_clear_all_caches(false);
return true;
}
/**
* Deletes cached capabilities that are no longer needed by the component.
* Also unassigns these capabilities from any roles that have them.
*
* @access private
* @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
* @param array $newcapdef array of the new capability definitions that will be
* compared with the cached capabilities
* @return int number of deprecated capabilities that have been removed
*/
function capabilities_cleanup($component, $newcapdef = null) {
global $DB;
$removedcount = 0;
if ($cachedcaps = get_cached_capabilities($component)) {
foreach ($cachedcaps as $cachedcap) {
if (empty($newcapdef) ||
array_key_exists($cachedcap->name, $newcapdef) === false) {
// Remove from capabilities cache.
$DB->delete_records('capabilities', array('name'=>$cachedcap->name));
$removedcount++;
// Delete from roles.
if ($roles = get_roles_with_capability($cachedcap->name)) {
foreach($roles as $role) {
if (!unassign_capability($cachedcap->name, $role->id)) {
print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
}
}
}
} // End if.
}
}
return $removedcount;
}
/**
* Returns an array of all the known types of risk
* The array keys can be used, for example as CSS class names, or in calls to
* print_risk_icon. The values are the corresponding RISK_ constants.
*
* @return array all the known types of risk.
*/
function get_all_risks() {
return array(
'riskmanagetrust' => RISK_MANAGETRUST,
'riskconfig' => RISK_CONFIG,
'riskxss' => RISK_XSS,
'riskpersonal' => RISK_PERSONAL,
'riskspam' => RISK_SPAM,
'riskdataloss' => RISK_DATALOSS,
);
}
/**
* Return a link to moodle docs for a given capability name
*
* @param stdClass $capability a capability - a row from the mdl_capabilities table.
* @return string the human-readable capability name as a link to Moodle Docs.
*/
function get_capability_docs_link($capability) {
$url = get_docs_url('Capabilities/' . $capability->name);
return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
}
/**
* This function pulls out all the resolved capabilities (overrides and
* defaults) of a role used in capability overrides in contexts at a given
* context.
*
* @param int $roleid
* @param context $context
* @param string $cap capability, optional, defaults to ''
* @return array Array of capabilities
*/
function role_context_capabilities($roleid, context $context, $cap = '') {
global $DB;
$contexts = $context->get_parent_context_ids(true);
$contexts = '('.implode(',', $contexts).')';
$params = array($roleid);
if ($cap) {
$search = " AND rc.capability = ? ";
$params[] = $cap;
} else {
$search = '';
}
$sql = "SELECT rc.*
FROM {role_capabilities} rc, {context} c
WHERE rc.contextid in $contexts
AND rc.roleid = ?
AND rc.contextid = c.id $search
ORDER BY c.contextlevel DESC, rc.capability DESC";
$capabilities = array();
if ($records = $DB->get_records_sql($sql, $params)) {
// We are traversing via reverse order.
foreach ($records as $record) {
// If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
$capabilities[$record->capability] = $record->permission;
}
}
}
return $capabilities;
}
/**
* Constructs array with contextids as first parameter and context paths,
* in both cases bottom top including self.
*
* @access private
* @param context $context
* @return array
*/
function get_context_info_list(context $context) {
$contextids = explode('/', ltrim($context->path, '/'));
$contextpaths = array();
$contextids2 = $contextids;
while ($contextids2) {
$contextpaths[] = '/' . implode('/', $contextids2);
array_pop($contextids2);
}
return array($contextids, $contextpaths);
}
/**
* Check if context is the front page context or a context inside it
*
* Returns true if this context is the front page context, or a context inside it,
* otherwise false.
*
* @param context $context a context object.
* @return bool
*/
function is_inside_frontpage(context $context) {
$frontpagecontext = context_course::instance(SITEID);
return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
}
/**
* Returns capability information (cached)
*
* @param string $capabilityname
* @return stdClass or null if capability not found
*/
function get_capability_info($capabilityname) {
global $ACCESSLIB_PRIVATE, $DB; // one request per page only
//TODO: MUC - this could be cached in shared memory, it would eliminate 1 query per page
if (empty($ACCESSLIB_PRIVATE->capabilities)) {
$ACCESSLIB_PRIVATE->capabilities = array();
$caps = $DB->get_records('capabilities', array(), 'id, name, captype, riskbitmask');
foreach ($caps as $cap) {
$capname = $cap->name;
unset($cap->id);
unset($cap->name);
$cap->riskbitmask = (int)$cap->riskbitmask;
$ACCESSLIB_PRIVATE->capabilities[$capname] = $cap;
}
}
return isset($ACCESSLIB_PRIVATE->capabilities[$capabilityname]) ? $ACCESSLIB_PRIVATE->capabilities[$capabilityname] : null;
}
/**
* Returns the human-readable, translated version of the capability.
* Basically a big switch statement.
*
* @param string $capabilityname e.g. mod/choice:readresponses
* @return string
*/
function get_capability_string($capabilityname) {
// Typical capability name is 'plugintype/pluginname:capabilityname'
list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
if ($type === 'moodle') {
$component = 'core_role';
} else if ($type === 'quizreport') {
//ugly hack!!
$component = 'quiz_'.$name;
} else {
$component = $type.'_'.$name;
}
$stringname = $name.':'.$capname;
if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
return get_string($stringname, $component);
}
$dir = get_component_directory($component);
if (!file_exists($dir)) {
// plugin broken or does not exist, do not bother with printing of debug message
return $capabilityname.' ???';
}
// something is wrong in plugin, better print debug
return get_string($stringname, $component);
}
/**
* This gets the mod/block/course/core etc strings.
*
* @param string $component
* @param int $contextlevel
* @return string|bool String is success, false if failed
*/
function get_component_string($component, $contextlevel) {
if ($component === 'moodle' or $component === 'core') {
switch ($contextlevel) {
// TODO: this should probably use context level names instead
case CONTEXT_SYSTEM: return get_string('coresystem');
case CONTEXT_USER: return get_string('users');
case CONTEXT_COURSECAT: return get_string('categories');
case CONTEXT_COURSE: return get_string('course');
case CONTEXT_MODULE: return get_string('activities');
case CONTEXT_BLOCK: return get_string('block');
default: print_error('unknowncontext');
}
}
list($type, $name) = normalize_component($component);
$dir = get_plugin_directory($type, $name);
if (!file_exists($dir)) {
// plugin not installed, bad luck, there is no way to find the name
return $component.' ???';
}
switch ($type) {
// TODO: this is really hacky, anyway it should be probably moved to lib/pluginlib.php
case 'quiz': return get_string($name.':componentname', $component);// insane hack!!!
case 'repository': return get_string('repository', 'repository').': '.get_string('pluginname', $component);
case 'gradeimport': return get_string('gradeimport', 'grades').': '.get_string('pluginname', $component);
case 'gradeexport': return get_string('gradeexport', 'grades').': '.get_string('pluginname', $component);
case 'gradereport': return get_string('gradereport', 'grades').': '.get_string('pluginname', $component);
case 'webservice': return get_string('webservice', 'webservice').': '.get_string('pluginname', $component);
case 'block': return get_string('block').': '.get_string('pluginname', basename($component));
case 'mod':
if (get_string_manager()->string_exists('pluginname', $component)) {
return get_string('activity').': '.get_string('pluginname', $component);
} else {
return get_string('activity').': '.get_string('modulename', $component);
}
default: return get_string('pluginname', $component);
}
}
/**
* Gets the list of roles assigned to this context and up (parents)
* from the list of roles that are visible on user profile page
* and participants page.
*
* @param context $context
* @return array
*/
function get_profile_roles(context $context) {
global $CFG, $DB;
if (empty($CFG->profileroles)) {
return array();
}
list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a');
list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
$params = array_merge($params, $cparams);
if ($coursecontext = $context->get_course_context(false)) {
$params['coursecontext'] = $coursecontext->id;
} else {
$params['coursecontext'] = 0;
}
$sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
FROM {role_assignments} ra, {role} r
LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
WHERE r.id = ra.roleid
AND ra.contextid $contextlist
AND r.id $rallowed
ORDER BY r.sortorder ASC";
return $DB->get_records_sql($sql, $params);
}
/**
* Gets the list of roles assigned to this context and up (parents)
*
* @param context $context
* @return array
*/
function get_roles_used_in_context(context $context) {
global $DB;
list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
if ($coursecontext = $context->get_course_context(false)) {
$params['coursecontext'] = $coursecontext->id;
} else {
$params['coursecontext'] = 0;
}
$sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
FROM {role_assignments} ra, {role} r
LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
WHERE r.id = ra.roleid
AND ra.contextid $contextlist
ORDER BY r.sortorder ASC";
return $DB->get_records_sql($sql, $params);
}
/**
* This function is used to print roles column in user profile page.
* It is using the CFG->profileroles to limit the list to only interesting roles.
* (The permission tab has full details of user role assignments.)
*
* @param int $userid
* @param int $courseid
* @return string
*/
function get_user_roles_in_course($userid, $courseid) {
global $CFG, $DB;
if (empty($CFG->profileroles)) {
return '';
}
if ($courseid == SITEID) {
$context = context_system::instance();
} else {
$context = context_course::instance($courseid);
}
if (empty($CFG->profileroles)) {
return array();
}
list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a');
list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
$params = array_merge($params, $cparams);
if ($coursecontext = $context->get_course_context(false)) {
$params['coursecontext'] = $coursecontext->id;
} else {
$params['coursecontext'] = 0;
}
$sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
FROM {role_assignments} ra, {role} r
LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
WHERE r.id = ra.roleid
AND ra.contextid $contextlist
AND r.id $rallowed
AND ra.userid = :userid
ORDER BY r.sortorder ASC";
$params['userid'] = $userid;
$rolestring = '';
if ($roles = $DB->get_records_sql($sql, $params)) {
$rolenames = role_fix_names($roles, $context, ROLENAME_ALIAS, true); // Substitute aliases
foreach ($rolenames as $roleid => $rolename) {
$rolenames[$roleid] = '<a href="'.$CFG->wwwroot.'/user/index.php?contextid='.$context->id.'&amp;roleid='.$roleid.'">'.$rolename.'</a>';
}
$rolestring = implode(',', $rolenames);
}
return $rolestring;
}
/**
* Checks if a user can assign users to a particular role in this context
*
* @param context $context
* @param int $targetroleid - the id of the role you want to assign users to
* @return boolean
*/
function user_can_assign(context $context, $targetroleid) {
global $DB;
// First check to see if the user is a site administrator.
if (is_siteadmin()) {
return true;
}
// Check if user has override capability.
// If not return false.
if (!has_capability('moodle/role:assign', $context)) {
return false;
}
// pull out all active roles of this user from this context(or above)
if ($userroles = get_user_roles($context)) {
foreach ($userroles as $userrole) {
// if any in the role_allow_override table, then it's ok
if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
return true;
}
}
}
return false;
}
/**
* Returns all site roles in correct sort order.
*
* @param context $context optional context for course role name aliases
* @return array of role records with optional coursealias property
*/
function get_all_roles(context $context = null) {
global $DB;
if (!$context or !$coursecontext = $context->get_course_context(false)) {
$coursecontext = null;
}
if ($coursecontext) {
$sql = "SELECT r.*, rn.name AS coursealias
FROM {role} r
LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
ORDER BY r.sortorder ASC";
return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
} else {
return $DB->get_records('role', array(), 'sortorder ASC');
}
}
/**
* Returns roles of a specified archetype
*
* @param string $archetype
* @return array of full role records
*/
function get_archetype_roles($archetype) {
global $DB;
return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
}
/**
* Gets all the user roles assigned in this context, or higher contexts
* this is mainly used when checking if a user can assign a role, or overriding a role
* i.e. we need to know what this user holds, in order to verify against allow_assign and
* allow_override tables
*
* @param context $context
* @param int $userid
* @param bool $checkparentcontexts defaults to true
* @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
* @return array
*/
function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
global $USER, $DB;
if (empty($userid)) {
if (empty($USER->id)) {
return array();
}
$userid = $USER->id;
}
if ($checkparentcontexts) {
$contextids = $context->get_parent_context_ids();
} else {
$contextids = array();
}
$contextids[] = $context->id;
list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
array_unshift($params, $userid);
$sql = "SELECT ra.*, r.name, r.shortname
FROM {role_assignments} ra, {role} r, {context} c
WHERE ra.userid = ?
AND ra.roleid = r.id
AND ra.contextid = c.id
AND ra.contextid $contextids
ORDER BY $order";
return $DB->get_records_sql($sql ,$params);
}
/**
* Like get_user_roles, but adds in the authenticated user role, and the front
* page roles, if applicable.
*
* @param context $context the context.
* @param int $userid optional. Defaults to $USER->id
* @return array of objects with fields ->userid, ->contextid and ->roleid.
*/
function get_user_roles_with_special(context $context, $userid = 0) {
global $CFG, $USER;
if (empty($userid)) {
if (empty($USER->id)) {
return array();
}
$userid = $USER->id;
}
$ras = get_user_roles($context, $userid);
// Add front-page role if relevant.
$defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
$isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
is_inside_frontpage($context);
if ($defaultfrontpageroleid && $isfrontpage) {
$frontpagecontext = context_course::instance(SITEID);
$ra = new stdClass();
$ra->userid = $userid;
$ra->contextid = $frontpagecontext->id;
$ra->roleid = $defaultfrontpageroleid;
$ras[] = $ra;
}