mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 14:27:22 +01:00
d867e696aa
Instead we have a new $ACCESSLIB_PRIVATE for all caching. Regrettably, the only way to make this work in PHP (other than rewriting everything to be methods of a class rather than functions) is to make this a global variable. However $ACCESSLIB_PRIVATE should not be thought of a global, it is a private implementation detail of accesslib.php. (And there is a comment saying that.) There is a new function accesslib_clear_all_caches_for_unit_testing(). In a unit test, you need to call this at the start of your test method, before you set up any test data, and again at the end, after you have discarded all your test data. This new $ACCESSLIB_PRIVATE subsumes the old $ACCESS, $RDEFS and $DITRYCONTEXTS globals. Also, I took the opportunity to refactor the (inconsistently) duplicated code for adding a context to the caches code into a cache_context function.
6046 lines
212 KiB
PHP
Executable File
6046 lines
212 KiB
PHP
Executable File
<?php // $Id$
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// NOTICE OF COPYRIGHT //
|
|
// //
|
|
// Moodle - Modular Object-Oriented Dynamic Learning Environment //
|
|
// http://moodle.org //
|
|
// //
|
|
// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
|
|
// //
|
|
// This program is free software; you can redistribute it and/or modify //
|
|
// it under the terms of the GNU General Public License as published by //
|
|
// the Free Software Foundation; either version 2 of the License, or //
|
|
// (at your option) any later version. //
|
|
// //
|
|
// This program is distributed in the hope that it will be useful, //
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
|
// GNU General Public License for more details: //
|
|
// //
|
|
// http://www.gnu.org/copyleft/gpl.html //
|
|
// //
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Public API vs internals
|
|
* -----------------------
|
|
*
|
|
* General users probably only care about
|
|
*
|
|
* Context handling
|
|
* - get_context_instance()
|
|
* - get_context_instance_by_id()
|
|
* - get_parent_contexts()
|
|
* - get_child_contexts()
|
|
*
|
|
* Whether the user can do something...
|
|
* - has_capability()
|
|
* - has_any_capability()
|
|
* - has_all_capabilities()
|
|
* - require_capability()
|
|
* - require_login() (from moodlelib)
|
|
*
|
|
* What courses has this user access to?
|
|
* - get_user_courses_bycap()
|
|
*
|
|
* What users can do X in this context?
|
|
* - get_users_by_capability()
|
|
*
|
|
* Enrol/unenrol
|
|
* - enrol_into_course()
|
|
* - role_assign()/role_unassign()
|
|
*
|
|
*
|
|
* Advanced use
|
|
* - load_all_capabilities()
|
|
* - reload_all_capabilities()
|
|
* - has_capability_in_accessdata()
|
|
* - is_siteadmin()
|
|
* - get_user_access_sitewide()
|
|
* - load_subcontext()
|
|
* - get_role_access_bycontext()
|
|
*
|
|
* Name conventions
|
|
* ----------------
|
|
*
|
|
* - "ctx" means context
|
|
*
|
|
* accessdata
|
|
* ----------
|
|
*
|
|
* Access control data is held in the "accessdata" array
|
|
* which - for the logged-in user, will be in $USER->access
|
|
*
|
|
* For other users can be generated and passed around (but 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.
|
|
*
|
|
* $accessdata[ra][$contextpath]= array($roleid)
|
|
* [$contextpath]= array($roleid)
|
|
* [$contextpath]= array($roleid)
|
|
*
|
|
* Role definitions are stored like this
|
|
* (no cap merge is done - so it's compact)
|
|
*
|
|
* $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
|
|
* [mod/forum:editallpost] = -1
|
|
* [mod/forum:startdiscussion] = -1000
|
|
*
|
|
* See how has_capability_in_accessdata() walks up/down the tree.
|
|
*
|
|
* Normally - specially for the logged-in user, we only load
|
|
* rdef and ra down to the course level, but not below. This
|
|
* keeps accessdata small and compact. Below-the-course ra/rdef
|
|
* are loaded as needed. We keep track of which courses we
|
|
* have loaded ra/rdef in
|
|
*
|
|
* $accessdata[loaded] = array($contextpath, $contextpath)
|
|
*
|
|
* Stale accessdata
|
|
* ----------------
|
|
*
|
|
* For the logged-in user, accessdata is long-lived.
|
|
*
|
|
* On each pageload we load $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 sytem level will force the reload for everyone.
|
|
*
|
|
* Default role caps
|
|
* -----------------
|
|
* The default role assignment is not in the DB, so we
|
|
* add it manually to accessdata.
|
|
*
|
|
* This means that functions that work directly off the
|
|
* DB need to ensure that the default role caps
|
|
* are dealt with appropriately.
|
|
*
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
|
* @package roles
|
|
*/
|
|
|
|
require_once $CFG->dirroot.'/lib/blocklib.php';
|
|
|
|
// permission definitions
|
|
define('CAP_INHERIT', 0);
|
|
define('CAP_ALLOW', 1);
|
|
define('CAP_PREVENT', -1);
|
|
define('CAP_PROHIBIT', -1000);
|
|
|
|
// context definitions
|
|
define('CONTEXT_SYSTEM', 10);
|
|
define('CONTEXT_USER', 30);
|
|
define('CONTEXT_COURSECAT', 40);
|
|
define('CONTEXT_COURSE', 50);
|
|
define('CONTEXT_GROUP', 60);
|
|
define('CONTEXT_MODULE', 70);
|
|
define('CONTEXT_BLOCK', 80);
|
|
|
|
// capability risks - see http://docs.moodle.org/en/Development:Hardening_new_Roles_system
|
|
define('RISK_MANAGETRUST', 0x0001);
|
|
define('RISK_CONFIG', 0x0002);
|
|
define('RISK_XSS', 0x0004);
|
|
define('RISK_PERSONAL', 0x0008);
|
|
define('RISK_SPAM', 0x0010);
|
|
define('RISK_DATALOSS', 0x0020);
|
|
|
|
// rolename displays
|
|
define('ROLENAME_ORIGINAL', 0);// the name as defined in the role definition
|
|
define('ROLENAME_ALIAS', 1); // the name as defined by a role alias
|
|
define('ROLENAME_BOTH', 2); // Both, like this: Role alias (Original)
|
|
define('ROLENAME_ORIGINALANDSHORT', 3); // the name as defined in the role definition and the shortname in brackets
|
|
define('ROLENAME_ALIAS_RAW', 4); // the name as defined by a role alias, in raw form suitable for editing
|
|
|
|
// 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 variale is the only way to impleemnt this, withough rewriting everything
|
|
// as methods of a class, instead of functions.
|
|
$ACCESSLIB_PRIVATE = new stdClass;
|
|
$ACCESSLIB_PRIVATE->contexts = array(); // Cache of context objects by level and instance
|
|
$ACCESSLIB_PRIVATE->contextsbyid = array(); // Cache of context objects by id
|
|
$ACCESSLIB_PRIVATE->systemcontext = null; // Used in get_system_context
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache
|
|
$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the $accessdata structure for users other than $USER
|
|
$ACCESSLIB_PRIVATE->roledefinitions = array(); // role definitions cache - helps a lot with mem usage in cron
|
|
$ACCESSLIB_PRIVATE->croncache = array(); // Used in get_role_access
|
|
$ACCESSLIB_PRIVATE->preloadedcourses = array(); // Used in preload_course_contexts.
|
|
$ACCESSLIB_PRIVATE->capabilitynames = null; // Used in is_valid_capability (only in developer debug mode)
|
|
|
|
/**
|
|
* 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 fo the tests.
|
|
*/
|
|
function accesslib_clear_all_caches_for_unit_testing() {
|
|
global $UNITTEST, $USER, $ACCESSLIB_PRIVATE;
|
|
if (empty($UNITTEST->running)) {
|
|
throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
|
|
}
|
|
$ACCESSLIB_PRIVATE->contexts = array();
|
|
$ACCESSLIB_PRIVATE->contextsbyid = array();
|
|
$ACCESSLIB_PRIVATE->systemcontext = null;
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = null;
|
|
$ACCESSLIB_PRIVATE->accessdatabyuser = array();
|
|
$ACCESSLIB_PRIVATE->roledefinitions = array();
|
|
$ACCESSLIB_PRIVATE->croncache = array();
|
|
$ACCESSLIB_PRIVATE->preloadedcourses = array();
|
|
$ACCESSLIB_PRIVATE->capabilitynames = null;
|
|
|
|
unset($USER->access);
|
|
}
|
|
|
|
/**
|
|
* Private function. Add a context object to accesslib's caches.
|
|
*/
|
|
function cache_context($context) {
|
|
global $ACCESSLIB_PRIVATE;
|
|
$ACCESSLIB_PRIVATE->contexts[$context->contextlevel][$context->instanceid] = $context;
|
|
$ACCESSLIB_PRIVATE->contextsbyid[$context->id] = $context;
|
|
}
|
|
|
|
function get_role_context_caps($roleid, $context) {
|
|
global $DB;
|
|
|
|
//this is really slow!!!! - do not use above course context level!
|
|
$result = array();
|
|
$result[$context->id] = array();
|
|
|
|
// first emulate the parent context capabilities merging into context
|
|
$searchcontexts = array_reverse(get_parent_contexts($context));
|
|
array_push($searchcontexts, $context->id);
|
|
foreach ($searchcontexts as $cid) {
|
|
if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
|
|
foreach ($capabilities as $cap) {
|
|
if (!array_key_exists($cap->capability, $result[$context->id])) {
|
|
$result[$context->id][$cap->capability] = 0;
|
|
}
|
|
$result[$context->id][$cap->capability] += $cap->permission;
|
|
}
|
|
}
|
|
}
|
|
|
|
// now go through the contexts bellow given context
|
|
$searchcontexts = array_keys(get_child_contexts($context));
|
|
foreach ($searchcontexts as $cid) {
|
|
if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
|
|
foreach ($capabilities as $cap) {
|
|
if (!array_key_exists($cap->contextid, $result)) {
|
|
$result[$cap->contextid] = array();
|
|
}
|
|
$result[$cap->contextid][$cap->capability] = $cap->permission;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Gets the accessdata for role "sitewide"
|
|
* (system down to course)
|
|
*
|
|
* @return array
|
|
*/
|
|
function get_role_access($roleid, $accessdata=NULL) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
/* Get it in 1 cheap DB query...
|
|
* - relevant role caps at the root and down
|
|
* to the course level - but not below
|
|
*/
|
|
if (is_null($accessdata)) {
|
|
$accessdata = array(); // named list
|
|
$accessdata['ra'] = array();
|
|
$accessdata['rdef'] = array();
|
|
$accessdata['loaded'] = array();
|
|
}
|
|
|
|
//
|
|
// Overrides for the role IN ANY CONTEXTS
|
|
// down to COURSE - not below -
|
|
//
|
|
$sql = "SELECT ctx.path,
|
|
rc.capability, rc.permission
|
|
FROM {context} ctx
|
|
JOIN {role_capabilities} rc
|
|
ON rc.contextid=ctx.id
|
|
WHERE rc.roleid = ?
|
|
AND ctx.contextlevel <= ".CONTEXT_COURSE."
|
|
ORDER BY ctx.depth, ctx.path";
|
|
$params = array($roleid);
|
|
|
|
// we need extra caching in CLI scripts and cron
|
|
if (CLI_SCRIPT) {
|
|
global $ACCESSLIB_PRIVATE;
|
|
|
|
if (!isset($ACCESSLIB_PRIVATE->croncache[$roleid])) {
|
|
$ACCESSLIB_PRIVATE->croncache[$roleid] = array();
|
|
if ($rs = $DB->get_recordset_sql($sql, $params)) {
|
|
foreach ($rs as $rd) {
|
|
$ACCESSLIB_PRIVATE->croncache[$roleid][] = $rd;
|
|
}
|
|
$rs->close();
|
|
}
|
|
}
|
|
|
|
foreach ($ACCESSLIB_PRIVATE->croncache[$roleid] as $rd) {
|
|
$k = "{$rd->path}:{$roleid}";
|
|
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
|
|
}
|
|
|
|
} else {
|
|
if ($rs = $DB->get_recordset_sql($sql, $params)) {
|
|
foreach ($rs as $rd) {
|
|
$k = "{$rd->path}:{$roleid}";
|
|
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
|
|
}
|
|
unset($rd);
|
|
$rs->close();
|
|
}
|
|
}
|
|
|
|
return $accessdata;
|
|
}
|
|
|
|
/**
|
|
* Gets the accessdata for role "sitewide"
|
|
* (system down to course)
|
|
*
|
|
* @return array
|
|
*/
|
|
function get_default_frontpage_role_access($roleid, $accessdata=NULL) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
$frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
|
|
$base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
|
|
|
|
//
|
|
// Overrides for the role in any contexts related to the course
|
|
//
|
|
$sql = "SELECT ctx.path,
|
|
rc.capability, rc.permission
|
|
FROM {context} ctx
|
|
JOIN {role_capabilities} rc
|
|
ON rc.contextid=ctx.id
|
|
WHERE rc.roleid = ?
|
|
AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?)
|
|
AND ctx.contextlevel <= ".CONTEXT_COURSE."
|
|
ORDER BY ctx.depth, ctx.path";
|
|
$params = array($roleid, "$base/%");
|
|
|
|
if ($rs = $DB->get_recordset_sql($sql, $params)) {
|
|
foreach ($rs as $rd) {
|
|
$k = "{$rd->path}:{$roleid}";
|
|
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
|
|
}
|
|
unset($rd);
|
|
$rs->close();
|
|
}
|
|
|
|
return $accessdata;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the default guest role
|
|
* @return object role
|
|
*/
|
|
function get_guest_role() {
|
|
global $CFG, $DB;
|
|
|
|
if (empty($CFG->guestroleid)) {
|
|
if ($roles = get_roles_with_capability('moodle/legacy:guest', CAP_ALLOW)) {
|
|
$guestrole = array_shift($roles); // Pick the first one
|
|
set_config('guestroleid', $guestrole->id);
|
|
return $guestrole;
|
|
} else {
|
|
debugging('Can not find any guest role!');
|
|
return false;
|
|
}
|
|
} else {
|
|
if ($guestrole = $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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function returns whether the current user has the capability of performing a function
|
|
* For example, we can do has_capability('mod/forum:replypost',$context) in forum
|
|
* @param string $capability - name of the capability (or debugcache or clearcache)
|
|
* @param object $context - a context object (record from context table)
|
|
* @param integer $userid - a userid number, empty if current $USER
|
|
* @param bool $doanything - if false, ignore do anything
|
|
* @return bool
|
|
*/
|
|
function has_capability($capability, $context, $userid=NULL, $doanything=true) {
|
|
global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE;
|
|
|
|
if (empty($CFG->rolesactive)) {
|
|
if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") {
|
|
// we are in an installer - roles can not work yet
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// the original $CONTEXT here was hiding serious errors
|
|
// for security reasons do not reuse previous context
|
|
if (empty($context)) {
|
|
debugging('Incorrect context specified');
|
|
return false;
|
|
}
|
|
|
|
/// Some sanity checks
|
|
if (debugging('',DEBUG_DEVELOPER)) {
|
|
if (!is_valid_capability($capability)) {
|
|
debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
|
|
}
|
|
if (!is_bool($doanything)) {
|
|
debugging('Capability parameter "doanything" is wierd ("'.$doanything.'"). This should be fixed in code.');
|
|
}
|
|
}
|
|
|
|
if (empty($userid)) { // we must accept null, 0, '0', '' etc. in $userid
|
|
$userid = $USER->id;
|
|
}
|
|
|
|
if (is_null($context->path) or $context->depth == 0) {
|
|
//this should not happen
|
|
$contexts = array(SYSCONTEXTID, $context->id);
|
|
$context->path = '/'.SYSCONTEXTID.'/'.$context->id;
|
|
debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);
|
|
|
|
} else {
|
|
$contexts = explode('/', $context->path);
|
|
array_shift($contexts);
|
|
}
|
|
|
|
if (CLI_SCRIPT && !isset($USER->access)) {
|
|
// In cron, some modules setup a 'fake' $USER,
|
|
// ensure we load the appropriate accessdata.
|
|
if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = NULL; //load fresh dirty contexts
|
|
} else {
|
|
load_user_accessdata($userid);
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = array();
|
|
}
|
|
$USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
|
|
|
|
} else if ($USER->id == $userid && !isset($USER->access)) {
|
|
// caps not loaded yet - better to load them to keep BC with 1.8
|
|
// not-logged-in user or $USER object set up manually first time here
|
|
load_all_capabilities();
|
|
$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now
|
|
$ACCESSLIB_PRIVATE->roledefinitions = array();
|
|
}
|
|
|
|
// Load dirty contexts list if needed
|
|
if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
|
|
if (isset($USER->access['time'])) {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']);
|
|
}
|
|
else {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = array();
|
|
}
|
|
}
|
|
|
|
// Careful check for staleness...
|
|
if (count($ACCESSLIB_PRIVATE->dirtycontexts) !== 0 and is_contextpath_dirty($contexts, $ACCESSLIB_PRIVATE->dirtycontexts)) {
|
|
// reload all capabilities - preserving loginas, roleswitches, etc
|
|
// and then cleanup any marks of dirtyness... at least from our short
|
|
// term memory! :-)
|
|
$ACCESSLIB_PRIVATE->accessdatabyuser = array();
|
|
$ACCESSLIB_PRIVATE->roledefinitions = array();
|
|
|
|
if (CLI_SCRIPT) {
|
|
load_user_accessdata($userid);
|
|
$USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = array();
|
|
|
|
} else {
|
|
reload_all_capabilities();
|
|
}
|
|
}
|
|
|
|
// divulge how many times we are called
|
|
//// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
|
|
|
|
if ($USER->id == $userid) { // we must accept strings and integers in $userid
|
|
//
|
|
// For the logged in user, we have $USER->access
|
|
// which will have all RAs and caps preloaded for
|
|
// course and above contexts.
|
|
//
|
|
// Contexts below courses && contexts that do not
|
|
// hang from courses are loaded into $USER->access
|
|
// on demand, and listed in $USER->access[loaded]
|
|
//
|
|
if ($context->contextlevel <= CONTEXT_COURSE) {
|
|
// Course and above are always preloaded
|
|
return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
|
|
}
|
|
// Load accessdata for below-the-course contexts
|
|
if (!path_inaccessdata($context->path,$USER->access)) {
|
|
// error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
|
|
// $bt = debug_backtrace();
|
|
// error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
|
|
load_subcontext($USER->id, $context, $USER->access);
|
|
}
|
|
return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
|
|
}
|
|
|
|
if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
|
|
load_user_accessdata($userid);
|
|
}
|
|
if ($context->contextlevel <= CONTEXT_COURSE) {
|
|
// Course and above are always preloaded
|
|
return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid], $doanything);
|
|
}
|
|
// Load accessdata for below-the-course contexts as needed
|
|
if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
|
|
// error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
|
|
// $bt = debug_backtrace();
|
|
// error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
|
|
load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
|
|
}
|
|
return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid], $doanything);
|
|
}
|
|
|
|
/**
|
|
* This function returns whether the current user has any of the capabilities in the
|
|
* $capabilities array. This is a simple wrapper around has_capability for convinience.
|
|
*
|
|
* There are probably tricks that could be done to improve the performance here, for example,
|
|
* check the capabilities that are already cached first.
|
|
*
|
|
* @param array $capabilities - an array of capability names.
|
|
* @param object $context - a context object (record from context table)
|
|
* @param integer $userid - a userid number, empty if current $USER
|
|
* @param bool $doanything - if false, ignore do anything
|
|
* @return bool
|
|
*/
|
|
function has_any_capability($capabilities, $context, $userid=NULL, $doanything=true) {
|
|
foreach ($capabilities as $capability) {
|
|
if (has_capability($capability, $context, $userid, $doanything)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This function returns whether the current user has all of the capabilities in the
|
|
* $capabilities array. This is a simple wrapper around has_capability for convinience.
|
|
*
|
|
* There are probably tricks that could be done to improve the performance here, for example,
|
|
* check the capabilities that are already cached first.
|
|
*
|
|
* @param array $capabilities - an array of capability names.
|
|
* @param object $context - a context object (record from context table)
|
|
* @param integer $userid - a userid number, empty if current $USER
|
|
* @param bool $doanything - if false, ignore do anything
|
|
* @return bool
|
|
*/
|
|
function has_all_capabilities($capabilities, $context, $userid=NULL, $doanything=true) {
|
|
if (!is_array($capabilities)) {
|
|
debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
|
|
return false;
|
|
}
|
|
foreach ($capabilities as $capability) {
|
|
if (!has_capability($capability, $context, $userid, $doanything)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Uses 1 DB query to answer whether a user is an admin at the sitelevel.
|
|
* It depends on DB schema >=1.7 but does not depend on the new datastructures
|
|
* in v1.9 (context.path, or $USER->access)
|
|
*
|
|
* Will return true if the userid has any of
|
|
* - moodle/site:config
|
|
* - moodle/legacy:admin
|
|
* - moodle/site:doanything
|
|
*
|
|
* @param int $userid
|
|
* @returns bool true is user can administer server settings
|
|
*/
|
|
function is_siteadmin($userid) {
|
|
global $CFG, $DB;
|
|
|
|
$sql = "SELECT SUM(rc.permission)
|
|
FROM {role_capabilities} rc
|
|
JOIN {context} ctx
|
|
ON ctx.id=rc.contextid
|
|
JOIN {role_assignments} ra
|
|
ON ra.roleid=rc.roleid AND ra.contextid=ctx.id
|
|
WHERE ctx.contextlevel=10
|
|
AND ra.userid=?
|
|
AND rc.capability IN (?, ?, ?)
|
|
GROUP BY rc.capability
|
|
HAVING SUM(rc.permission) > 0";
|
|
$params = array($userid, 'moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');
|
|
|
|
return $DB->record_exists_sql($sql, $params);
|
|
}
|
|
|
|
/**
|
|
* @param integer $roleid a role id.
|
|
* @return boolean, whether this role is an admin role.
|
|
*/
|
|
function is_admin_role($roleid) {
|
|
global $DB;
|
|
|
|
$sql = "SELECT 1
|
|
FROM {role_capabilities} rc
|
|
JOIN {context} ctx ON ctx.id = rc.contextid
|
|
WHERE ctx.contextlevel = 10
|
|
AND rc.roleid = ?
|
|
AND rc.capability IN (?, ?, ?)
|
|
GROUP BY rc.capability
|
|
HAVING SUM(rc.permission) > 0";
|
|
$params = array($roleid, 'moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');
|
|
|
|
return $DB->record_exists_sql($sql, $params);
|
|
}
|
|
|
|
/**
|
|
* @return all the roles for which is_admin_role($role->id) is true.
|
|
*/
|
|
function get_admin_roles() {
|
|
global $DB;
|
|
|
|
$sql = "SELECT *
|
|
FROM {role} r
|
|
WHERE EXISTS (
|
|
SELECT 1
|
|
FROM {role_capabilities} rc
|
|
JOIN {context} ctx ON ctx.id = rc.contextid
|
|
WHERE ctx.contextlevel = 10
|
|
AND rc.roleid = r.id
|
|
AND rc.capability IN (?, ?, ?)
|
|
GROUP BY rc.capability
|
|
HAVING SUM(rc.permission) > 0
|
|
)
|
|
ORDER BY r.sortorder";
|
|
$params = array('moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');
|
|
|
|
return $DB->get_records_sql($sql, $params);
|
|
}
|
|
|
|
function get_course_from_path ($path) {
|
|
// assume that nothing is more than 1 course deep
|
|
if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function path_inaccessdata($path, $accessdata) {
|
|
|
|
// assume that contexts hang from sys or from a course
|
|
// this will only work well with stuff that hangs from a course
|
|
if (in_array($path, $accessdata['loaded'], true)) {
|
|
// error_log("found it!");
|
|
return true;
|
|
}
|
|
$base = '/' . SYSCONTEXTID;
|
|
while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
|
|
$path = $matches[1];
|
|
if ($path === $base) {
|
|
return false;
|
|
}
|
|
if (in_array($path, $accessdata['loaded'], true)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Walk the accessdata array and return true/false.
|
|
* Deals with prohibits, roleswitching, aggregating
|
|
* capabilities, etc.
|
|
*
|
|
* The main feature of here is being FAST and with no
|
|
* side effects.
|
|
*
|
|
* Notes:
|
|
*
|
|
* Switch Roles exits early
|
|
* -----------------------
|
|
* cap checks within a switchrole need to exit early
|
|
* in our bottom up processing so they don't "see" that
|
|
* there are real RAs that can do all sorts of things.
|
|
*
|
|
* Switch Role merges with default role
|
|
* ------------------------------------
|
|
* If you are a teacher in course X, you have at least
|
|
* teacher-in-X + defaultloggedinuser-sitewide. So in the
|
|
* course you'll have techer+defaultloggedinuser.
|
|
* We try to mimic that in switchrole.
|
|
*
|
|
* Local-most role definition and role-assignment wins
|
|
* ---------------------------------------------------
|
|
* So if the local context has said 'allow', it wins
|
|
* over a high-level context that says 'deny'.
|
|
* This is applied when walking rdefs, and RAs.
|
|
* Only at the same context the values are SUM()med.
|
|
*
|
|
* The exception is CAP_PROHIBIT.
|
|
*
|
|
* "Guest default role" exception
|
|
* ------------------------------
|
|
*
|
|
* See MDL-7513 and $ignoreguest below for details.
|
|
*
|
|
* The rule is that
|
|
*
|
|
* IF we are being asked about moodle/legacy:guest
|
|
* OR moodle/course:view
|
|
* FOR a real, logged-in user
|
|
* AND we reached the top of the path in ra and rdef
|
|
* AND that role has moodle/legacy:guest === 1...
|
|
* THEN we act as if we hadn't seen it.
|
|
*
|
|
* Note that this function must be kept in synch with has_capability_in_accessdata.
|
|
*
|
|
* To Do:
|
|
*
|
|
* - Document how it works
|
|
* - Rewrite in ASM :-)
|
|
*
|
|
*/
|
|
function has_capability_in_accessdata($capability, $context, $accessdata, $doanything) {
|
|
|
|
global $CFG;
|
|
|
|
$path = $context->path;
|
|
|
|
// build $contexts as a list of "paths" of the current
|
|
// contexts and parents with the order top-to-bottom
|
|
$contexts = array($path);
|
|
while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
|
|
$path = $matches[1];
|
|
array_unshift($contexts, $path);
|
|
}
|
|
|
|
$ignoreguest = false;
|
|
if (isset($accessdata['dr'])
|
|
&& ($capability == 'moodle/course:view'
|
|
|| $capability == 'moodle/legacy:guest')) {
|
|
// At the base, ignore rdefs where moodle/legacy:guest
|
|
// is set
|
|
$ignoreguest = $accessdata['dr'];
|
|
}
|
|
|
|
// Coerce it to an int
|
|
$CAP_PROHIBIT = (int)CAP_PROHIBIT;
|
|
|
|
$cc = count($contexts);
|
|
|
|
$can = 0;
|
|
$capdepth = 0;
|
|
|
|
//
|
|
// role-switches loop
|
|
//
|
|
if (isset($accessdata['rsw'])) {
|
|
// check for isset() is fast
|
|
// empty() is slow...
|
|
if (empty($accessdata['rsw'])) {
|
|
unset($accessdata['rsw']); // keep things fast and unambiguous
|
|
break;
|
|
}
|
|
// From the bottom up...
|
|
for ($n=$cc-1;$n>=0;$n--) {
|
|
$ctxp = $contexts[$n];
|
|
if (isset($accessdata['rsw'][$ctxp])) {
|
|
// Found a switchrole assignment
|
|
// check for that role _plus_ the default user role
|
|
$ras = array($accessdata['rsw'][$ctxp],$CFG->defaultuserroleid);
|
|
for ($rn=0;$rn<2;$rn++) {
|
|
$roleid = (int)$ras[$rn];
|
|
// Walk the path for capabilities
|
|
// from the bottom up...
|
|
for ($m=$cc-1;$m>=0;$m--) {
|
|
$capctxp = $contexts[$m];
|
|
if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
|
|
$perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
|
|
|
|
// The most local permission (first to set) wins
|
|
// the only exception is CAP_PROHIBIT
|
|
if ($can === 0) {
|
|
$can = $perm;
|
|
} elseif ($perm === $CAP_PROHIBIT) {
|
|
$can = $perm;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// As we are dealing with a switchrole,
|
|
// we return _here_, do _not_ walk up
|
|
// the hierarchy any further
|
|
if ($can < 1) {
|
|
if ($doanything) {
|
|
// didn't find it as an explicit cap,
|
|
// but maybe the user can doanything in this context...
|
|
return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Main loop for normal RAs
|
|
// From the bottom up...
|
|
//
|
|
for ($n=$cc-1;$n>=0;$n--) {
|
|
$ctxp = $contexts[$n];
|
|
if (isset($accessdata['ra'][$ctxp])) {
|
|
// Found role assignments on this leaf
|
|
$ras = $accessdata['ra'][$ctxp];
|
|
|
|
$rc = count($ras);
|
|
$ctxcan = 0;
|
|
$ctxcapdepth = 0;
|
|
for ($rn=0;$rn<$rc;$rn++) {
|
|
$roleid = (int)$ras[$rn];
|
|
$rolecan = 0;
|
|
$rolecapdepth = 0;
|
|
// Walk the path for capabilities
|
|
// from the bottom up...
|
|
for ($m=$cc-1;$m>=0;$m--) {
|
|
$capctxp = $contexts[$m];
|
|
// ignore some guest caps
|
|
// at base ra and rdef
|
|
if ($ignoreguest == $roleid
|
|
&& $n === 0
|
|
&& $m === 0
|
|
&& isset($accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'])
|
|
&& $accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'] > 0) {
|
|
continue;
|
|
}
|
|
if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
|
|
$perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
|
|
// The most local permission (first to set) wins
|
|
// the only exception is CAP_PROHIBIT
|
|
if ($rolecan === 0) {
|
|
$rolecan = $perm;
|
|
$rolecapdepth = $m;
|
|
} elseif ($perm === $CAP_PROHIBIT) {
|
|
$rolecan = $perm;
|
|
$rolecapdepth = $m;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Rules for RAs at the same context...
|
|
// - prohibits always wins
|
|
// - permissions at the same ctxlevel & capdepth are added together
|
|
// - deeper capdepth wins
|
|
if ($ctxcan === $CAP_PROHIBIT || $rolecan === $CAP_PROHIBIT) {
|
|
$ctxcan = $CAP_PROHIBIT;
|
|
$ctxcapdepth = 0;
|
|
} elseif ($ctxcapdepth === $rolecapdepth) {
|
|
$ctxcan += $rolecan;
|
|
} elseif ($ctxcapdepth < $rolecapdepth) {
|
|
$ctxcan = $rolecan;
|
|
$ctxcapdepth = $rolecapdepth;
|
|
} else { // ctxcaptdepth is deeper
|
|
// rolecap ignored
|
|
}
|
|
}
|
|
// The most local RAs with a defined
|
|
// permission ($ctxcan) win, except
|
|
// for CAP_PROHIBIT
|
|
// NOTE: If we want the deepest RDEF to
|
|
// win regardless of the depth of the RA,
|
|
// change the elseif below to read
|
|
// ($can === 0 || $capdepth < $ctxcapdepth) {
|
|
if ($ctxcan === $CAP_PROHIBIT) {
|
|
$can = $ctxcan;
|
|
break;
|
|
} elseif ($can === 0) { // see note above
|
|
$can = $ctxcan;
|
|
$capdepth = $ctxcapdepth;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($can < 1) {
|
|
if ($doanything) {
|
|
// didn't find it as an explicit cap,
|
|
// but maybe the user can doanything in this context...
|
|
return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
function aggregate_roles_from_accessdata($context, $accessdata) {
|
|
|
|
$path = $context->path;
|
|
|
|
// build $contexts as a list of "paths" of the current
|
|
// contexts and parents with the order top-to-bottom
|
|
$contexts = array($path);
|
|
while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
|
|
$path = $matches[1];
|
|
array_unshift($contexts, $path);
|
|
}
|
|
|
|
$cc = count($contexts);
|
|
|
|
$roles = array();
|
|
// From the bottom up...
|
|
for ($n=$cc-1;$n>=0;$n--) {
|
|
$ctxp = $contexts[$n];
|
|
if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
|
|
// Found assignments on this leaf
|
|
$addroles = $accessdata['ra'][$ctxp];
|
|
$roles = array_merge($roles, $addroles);
|
|
}
|
|
}
|
|
|
|
return array_unique($roles);
|
|
}
|
|
|
|
/**
|
|
* This is an easy to use function, combining has_capability() with require_course_login().
|
|
* And will call those where needed.
|
|
*
|
|
* It checks for a capability assertion being true. If it isn't
|
|
* then the page is terminated neatly with a standard error message.
|
|
*
|
|
* If the user is not logged in, or is using 'guest' access or other special "users,
|
|
* it provides a logon prompt.
|
|
*
|
|
* @param string $capability - name of the capability
|
|
* @param object $context - a context object (record from context table)
|
|
* @param integer $userid - a userid number
|
|
* @param bool $doanything - if false, ignore do anything
|
|
* @param string $errorstring - an errorstring
|
|
* @param string $stringfile - which stringfile to get it from
|
|
*/
|
|
function require_capability($capability, $context, $userid=NULL, $doanything=true,
|
|
$errormessage='nopermissions', $stringfile='') {
|
|
|
|
global $USER, $CFG, $DB;
|
|
|
|
/* Empty $userid means current user, if the current user is not logged in,
|
|
* then make sure they are (if needed).
|
|
* Originally there was a check for loaded permissions - it is not needed here.
|
|
* Context is now required parameter, the cached $CONTEXT was only hiding errors.
|
|
*/
|
|
$errorlink = '';
|
|
|
|
if (empty($userid)) {
|
|
if ($context->contextlevel == CONTEXT_COURSE) {
|
|
require_login($context->instanceid);
|
|
|
|
} else if ($context->contextlevel == CONTEXT_MODULE) {
|
|
if (!$cm = $DB->get_record('course_modules', array('id'=>$context->instanceid))) {
|
|
print_error('invalidmodule');
|
|
}
|
|
if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
|
|
print_error('invalidcourseid');
|
|
}
|
|
require_course_login($course, true, $cm);
|
|
$errorlink = $CFG->wwwroot.'/course/view.php?id='.$cm->course;
|
|
|
|
} else if ($context->contextlevel == CONTEXT_SYSTEM) {
|
|
if (!empty($CFG->forcelogin)) {
|
|
require_login();
|
|
}
|
|
|
|
} else {
|
|
require_login();
|
|
}
|
|
}
|
|
|
|
/// OK, if they still don't have the capability then print a nice error message
|
|
|
|
if (!has_capability($capability, $context, $userid, $doanything)) {
|
|
$capabilityname = get_capability_string($capability);
|
|
print_error('nopermissions', '', $errorlink, $capabilityname);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get an array of courses (with magic extra bits)
|
|
* where the accessdata and in DB enrolments show
|
|
* that the cap requested is available.
|
|
*
|
|
* The main use is for get_my_courses().
|
|
*
|
|
* Notes
|
|
*
|
|
* - $fields is an array of fieldnames to ADD
|
|
* so name the fields you really need, which will
|
|
* be added and uniq'd
|
|
*
|
|
* - the course records have $c->context which is a fully
|
|
* valid context object. Saves you a query per course!
|
|
*
|
|
* - the course records have $c->categorypath to make
|
|
* category lookups cheap
|
|
*
|
|
* - current implementation is split in -
|
|
*
|
|
* - if the user has the cap systemwide, stupidly
|
|
* grab *every* course for a capcheck. This eats
|
|
* a TON of bandwidth, specially on large sites
|
|
* with separate DBs...
|
|
*
|
|
* - otherwise, fetch "likely" courses with a wide net
|
|
* that should get us _cheaply_ at least the courses we need, and some
|
|
* we won't - we get courses that...
|
|
* - are in a category where user has the cap
|
|
* - or where use has a role-assignment (any kind)
|
|
* - or where the course has an override on for this cap
|
|
*
|
|
* - walk the courses recordset checking the caps oneach one
|
|
* the checks are all in memory and quite fast
|
|
* (though we could implement a specialised variant of the
|
|
* has_capability_in_accessdata() code to speed it up)
|
|
*
|
|
* @param string $capability - name of the capability
|
|
* @param array $accessdata - accessdata session array
|
|
* @param bool $doanything - if false, ignore do anything
|
|
* @param string $sort - sorting fields - prefix each fieldname with "c."
|
|
* @param array $fields - additional fields you are interested in...
|
|
* @param int $limit - set if you want to limit the number of courses
|
|
* @return array $courses - ordered array of course objects - see notes above
|
|
*
|
|
*/
|
|
function get_user_courses_bycap($userid, $cap, $accessdata, $doanything, $sort='c.sortorder ASC', $fields=NULL, $limit=0) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
// Slim base fields, let callers ask for what they need...
|
|
$basefields = array('id', 'sortorder', 'shortname', 'idnumber');
|
|
|
|
if (!is_null($fields)) {
|
|
$fields = array_merge($basefields, $fields);
|
|
$fields = array_unique($fields);
|
|
} else {
|
|
$fields = $basefields;
|
|
}
|
|
$coursefields = 'c.' .implode(',c.', $fields);
|
|
|
|
$sort = trim($sort);
|
|
if ($sort !== '') {
|
|
$sort = "ORDER BY $sort";
|
|
}
|
|
|
|
$sysctx = get_context_instance(CONTEXT_SYSTEM);
|
|
if (has_capability_in_accessdata($cap, $sysctx, $accessdata, $doanything)) {
|
|
//
|
|
// Apparently the user has the cap sitewide, so walk *every* course
|
|
// (the cap checks are moderately fast, but this moves massive bandwidth w the db)
|
|
// Yuck.
|
|
//
|
|
$sql = "SELECT $coursefields,
|
|
ctx.id AS ctxid, ctx.path AS ctxpath,
|
|
ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
|
|
cc.path AS categorypath
|
|
FROM {course} c
|
|
JOIN {course_categories} cc
|
|
ON c.category=cc.id
|
|
JOIN {context} ctx
|
|
ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
|
|
$sort ";
|
|
$rs = $DB->get_recordset_sql($sql);
|
|
} else {
|
|
//
|
|
// narrow down where we have the caps to a few contexts
|
|
// this will be a combination of
|
|
// - courses where user has an explicit enrolment
|
|
// - courses that have an override (any status) on that capability
|
|
// - categories where user has the rights (granted status) on that capability
|
|
//
|
|
$sql = "SELECT ctx.*
|
|
FROM {context} ctx
|
|
WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
|
|
ORDER BY ctx.depth";
|
|
$rs = $DB->get_recordset_sql($sql);
|
|
$catpaths = array();
|
|
foreach ($rs as $catctx) {
|
|
if ($catctx->path != ''
|
|
&& has_capability_in_accessdata($cap, $catctx, $accessdata, $doanything)) {
|
|
$catpaths[] = $catctx->path;
|
|
}
|
|
}
|
|
$rs->close();
|
|
$catclause = '';
|
|
$params = array();
|
|
if (count($catpaths)) {
|
|
$cc = count($catpaths);
|
|
for ($n=0;$n<$cc;$n++) {
|
|
$catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'";
|
|
}
|
|
$catclause = 'WHERE (' . implode(' OR ', $catpaths) .')';
|
|
}
|
|
unset($catpaths);
|
|
|
|
$capany = '';
|
|
if ($doanything) {
|
|
$capany = " OR rc.capability=:doany";
|
|
$params['doany'] = 'moodle/site:doanything';
|
|
}
|
|
|
|
/// UNION 3 queries:
|
|
/// - user role assignments in courses
|
|
/// - user capability (override - any status) in courses
|
|
/// - user right (granted status) in categories (optionally executed)
|
|
/// Enclosing the 3-UNION into an inline_view to avoid column names conflict and making the ORDER BY cross-db
|
|
/// and to allow selection of TEXT columns in the query (MSSQL and Oracle limitation). MDL-16209
|
|
$sql = "
|
|
SELECT $coursefields, ctxid, ctxpath, ctxdepth, ctxlevel, categorypath
|
|
FROM (
|
|
SELECT c.id,
|
|
ctx.id AS ctxid, ctx.path AS ctxpath,
|
|
ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
|
|
cc.path AS categorypath
|
|
FROM {$CFG->prefix}course c
|
|
JOIN {$CFG->prefix}course_categories cc
|
|
ON c.category=cc.id
|
|
JOIN {$CFG->prefix}context ctx
|
|
ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
|
|
JOIN {$CFG->prefix}role_assignments ra
|
|
ON (ra.contextid=ctx.id AND ra.userid=:userid)
|
|
UNION
|
|
SELECT c.id,
|
|
ctx.id AS ctxid, ctx.path AS ctxpath,
|
|
ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
|
|
cc.path AS categorypath
|
|
FROM {$CFG->prefix}course c
|
|
JOIN {$CFG->prefix}course_categories cc
|
|
ON c.category=cc.id
|
|
JOIN {$CFG->prefix}context ctx
|
|
ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
|
|
JOIN {$CFG->prefix}role_capabilities rc
|
|
ON (rc.contextid=ctx.id AND (rc.capability=:cap $capany)) ";
|
|
|
|
if (!empty($catclause)) { /// If we have found the right in categories, add child courses here too
|
|
$sql .= "
|
|
UNION
|
|
SELECT c.id,
|
|
ctx.id AS ctxid, ctx.path AS ctxpath,
|
|
ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
|
|
cc.path AS categorypath
|
|
FROM {$CFG->prefix}course c
|
|
JOIN {$CFG->prefix}course_categories cc
|
|
ON c.category=cc.id
|
|
JOIN {$CFG->prefix}context ctx
|
|
ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
|
|
$catclause";
|
|
}
|
|
|
|
/// Close the inline_view and join with courses table to get requested $coursefields
|
|
$sql .= "
|
|
) inline_view
|
|
INNER JOIN {$CFG->prefix}course c
|
|
ON inline_view.id = c.id";
|
|
|
|
/// To keep cross-db we need to strip any prefix in the ORDER BY clause for queries using UNION
|
|
$sql .= "
|
|
" . preg_replace('/[a-z]+\./i', '', $sort); /// Add ORDER BY clause
|
|
|
|
$params['userid'] = $userid;
|
|
$params['cap'] = $cap;
|
|
$rs = $DB->get_recordset_sql($sql, $params);
|
|
}
|
|
|
|
/// Confirm rights (granted capability) for each course returned
|
|
$courses = array();
|
|
$cc = 0; // keep count
|
|
if ($rs) {
|
|
foreach ($rs as $c) {
|
|
// build the context obj
|
|
$c = make_context_subobj($c);
|
|
|
|
if (has_capability_in_accessdata($cap, $c->context, $accessdata, $doanything)) {
|
|
$courses[] = $c;
|
|
if ($limit > 0 && $cc++ > $limit) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$rs->close();
|
|
}
|
|
|
|
return $courses;
|
|
}
|
|
|
|
|
|
/**
|
|
* It will return a nested array showing role assignments
|
|
* all relevant role capabilities for the user at
|
|
* site/metacourse/course_category/course levels
|
|
*
|
|
* We do _not_ delve deeper than courses because the number of
|
|
* overrides at the module/block levels is HUGE.
|
|
*
|
|
* [ra] => [/path/] = array(roleid, roleid)
|
|
* [rdef] => [/path/:roleid][capability]=permission
|
|
* [loaded] => array('/path', '/path')
|
|
*
|
|
* @param $userid integer - the id of the user
|
|
*
|
|
*/
|
|
function get_user_access_sitewide($userid) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
/* Get in 3 cheap DB queries...
|
|
* - role assignments - with role_caps
|
|
* - relevant role caps
|
|
* - above this user's RAs
|
|
* - below this user's RAs - limited to course level
|
|
*/
|
|
|
|
$accessdata = array(); // named list
|
|
$accessdata['ra'] = array();
|
|
$accessdata['rdef'] = array();
|
|
$accessdata['loaded'] = array();
|
|
|
|
$sitectx = get_system_context();
|
|
$base = '/'.$sitectx->id;
|
|
|
|
//
|
|
// Role assignments - and any rolecaps directly linked
|
|
// because it's cheap to read rolecaps here over many
|
|
// RAs
|
|
//
|
|
$sql = "SELECT ctx.path, ra.roleid, rc.capability, rc.permission
|
|
FROM {role_assignments} ra
|
|
JOIN {context} ctx
|
|
ON ra.contextid=ctx.id
|
|
LEFT OUTER JOIN {role_capabilities} rc
|
|
ON (rc.roleid=ra.roleid AND rc.contextid=ra.contextid)
|
|
WHERE ra.userid = ? AND ctx.contextlevel <= ".CONTEXT_COURSE."
|
|
ORDER BY ctx.depth, ctx.path, ra.roleid";
|
|
$params = array($userid);
|
|
$rs = $DB->get_recordset_sql($sql, $params);
|
|
//
|
|
// raparents collects paths & roles we need to walk up
|
|
// the parenthood to build the rdef
|
|
//
|
|
// the array will bulk up a bit with dups
|
|
// which we'll later clear up
|
|
//
|
|
$raparents = array();
|
|
$lastseen = '';
|
|
if ($rs) {
|
|
foreach ($rs as $ra) {
|
|
// RAs leafs are arrays to support multi
|
|
// role assignments...
|
|
if (!isset($accessdata['ra'][$ra->path])) {
|
|
$accessdata['ra'][$ra->path] = array();
|
|
}
|
|
// only add if is not a repeat caused
|
|
// by capability join...
|
|
// (this check is cheaper than in_array())
|
|
if ($lastseen !== $ra->path.':'.$ra->roleid) {
|
|
$lastseen = $ra->path.':'.$ra->roleid;
|
|
array_push($accessdata['ra'][$ra->path], $ra->roleid);
|
|
$parentids = explode('/', $ra->path);
|
|
array_shift($parentids); // drop empty leading "context"
|
|
array_pop($parentids); // drop _this_ context
|
|
|
|
if (isset($raparents[$ra->roleid])) {
|
|
$raparents[$ra->roleid] = array_merge($raparents[$ra->roleid],
|
|
$parentids);
|
|
} else {
|
|
$raparents[$ra->roleid] = $parentids;
|
|
}
|
|
}
|
|
// Always add the roleded
|
|
if (!empty($ra->capability)) {
|
|
$k = "{$ra->path}:{$ra->roleid}";
|
|
$accessdata['rdef'][$k][$ra->capability] = $ra->permission;
|
|
}
|
|
}
|
|
unset($ra);
|
|
$rs->close();
|
|
}
|
|
|
|
// Walk up the tree to grab all the roledefs
|
|
// of interest to our user...
|
|
// NOTE: we use a series of IN clauses here - which
|
|
// might explode on huge sites with very convoluted nesting of
|
|
// categories... - extremely unlikely that the number of categories
|
|
// and roletypes is so large that we hit the limits of IN()
|
|
$clauses = array();
|
|
$cparams = array();
|
|
foreach ($raparents as $roleid=>$contexts) {
|
|
$contexts = implode(',', array_unique($contexts));
|
|
if ($contexts ==! '') {
|
|
$clauses[] = "(roleid=? AND contextid IN ($contexts))";
|
|
$cparams[] = $roleid;
|
|
}
|
|
}
|
|
$clauses = implode(" OR ", $clauses);
|
|
if ($clauses !== '') {
|
|
$sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
|
|
FROM {role_capabilities} rc
|
|
JOIN {context} ctx
|
|
ON rc.contextid=ctx.id
|
|
WHERE $clauses
|
|
ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
|
|
$rs = $DB->get_recordset_sql($sql, $cparams);
|
|
unset($clauses);
|
|
|
|
if ($rs) {
|
|
foreach ($rs as $rd) {
|
|
$k = "{$rd->path}:{$rd->roleid}";
|
|
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
|
|
}
|
|
unset($rd);
|
|
$rs->close();
|
|
}
|
|
}
|
|
|
|
//
|
|
// Overrides for the role assignments IN SUBCONTEXTS
|
|
// (though we still do _not_ go below the course level.
|
|
//
|
|
// NOTE that the JOIN w sctx is with 3-way triangulation to
|
|
// catch overrides to the applicable role in any subcontext, based
|
|
// on the path field of the parent.
|
|
//
|
|
$sql = "SELECT sctx.path, ra.roleid,
|
|
ctx.path AS parentpath,
|
|
rco.capability, rco.permission
|
|
FROM {role_assignments} ra
|
|
JOIN {context} ctx
|
|
ON ra.contextid=ctx.id
|
|
JOIN {context} sctx
|
|
ON (sctx.path LIKE " . $DB->sql_concat('ctx.path',"'/%'"). " )
|
|
JOIN {role_capabilities} rco
|
|
ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
|
|
WHERE ra.userid = ?
|
|
AND ctx.contextlevel <= ".CONTEXT_COURSECAT."
|
|
AND sctx.contextlevel <= ".CONTEXT_COURSE."
|
|
ORDER BY sctx.depth, sctx.path, ra.roleid";
|
|
$params = array($userid);
|
|
$rs = $DB->get_recordset_sql($sql, $params);
|
|
if ($rs) {
|
|
foreach ($rs as $rd) {
|
|
$k = "{$rd->path}:{$rd->roleid}";
|
|
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
|
|
}
|
|
unset($rd);
|
|
$rs->close();
|
|
}
|
|
return $accessdata;
|
|
}
|
|
|
|
/**
|
|
* It add to the access ctrl array the data
|
|
* needed by a user for a given context
|
|
*
|
|
* @param $userid integer - the id of the user
|
|
* @param $context context obj - needs path!
|
|
* @param $accessdata array accessdata array
|
|
*/
|
|
function load_subcontext($userid, $context, &$accessdata) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
/* Get the additional RAs and relevant rolecaps
|
|
* - role assignments - with role_caps
|
|
* - relevant role caps
|
|
* - above this user's RAs
|
|
* - below this user's RAs - limited to course level
|
|
*/
|
|
|
|
$base = "/" . SYSCONTEXTID;
|
|
|
|
//
|
|
// Replace $context with the target context we will
|
|
// load. Normally, this will be a course context, but
|
|
// may be a different top-level context.
|
|
//
|
|
// We have 3 cases
|
|
//
|
|
// - Course
|
|
// - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM
|
|
// - BLOCK/MODULE/GROUP hanging from a course
|
|
//
|
|
// For course contexts, we _already_ have the RAs
|
|
// but the cost of re-fetching is minimal so we don't care.
|
|
//
|
|
if ($context->contextlevel !== CONTEXT_COURSE
|
|
&& $context->path !== "$base/{$context->id}") {
|
|
// Case BLOCK/MODULE/GROUP hanging from a course
|
|
// Assumption: the course _must_ be our parent
|
|
// If we ever see stuff nested further this needs to
|
|
// change to do 1 query over the exploded path to
|
|
// find out which one is the course
|
|
$courses = explode('/',get_course_from_path($context->path));
|
|
$targetid = array_pop($courses);
|
|
$context = get_context_instance_by_id($targetid);
|
|
|
|
}
|
|
|
|
//
|
|
// Role assignments in the context and below
|
|
//
|
|
$sql = "SELECT ctx.path, ra.roleid
|
|
FROM {role_assignments} ra
|
|
JOIN {context} ctx
|
|
ON ra.contextid=ctx.id
|
|
WHERE ra.userid = ?
|
|
AND (ctx.path = ? OR ctx.path LIKE ?)
|
|
ORDER BY ctx.depth, ctx.path, ra.roleid";
|
|
$params = array($userid, $context->path, $context->path."/%");
|
|
$rs = $DB->get_recordset_sql($sql, $params);
|
|
|
|
//
|
|
// Read in the RAs, preventing duplicates
|
|
//
|
|
if ($rs) {
|
|
$localroles = array();
|
|
$lastseen = '';
|
|
foreach ($rs as $ra) {
|
|
if (!isset($accessdata['ra'][$ra->path])) {
|
|
$accessdata['ra'][$ra->path] = array();
|
|
}
|
|
// only add if is not a repeat caused
|
|
// by capability join...
|
|
// (this check is cheaper than in_array())
|
|
if ($lastseen !== $ra->path.':'.$ra->roleid) {
|
|
$lastseen = $ra->path.':'.$ra->roleid;
|
|
array_push($accessdata['ra'][$ra->path], $ra->roleid);
|
|
array_push($localroles, $ra->roleid);
|
|
}
|
|
}
|
|
$rs->close();
|
|
}
|
|
|
|
//
|
|
// Walk up and down the tree to grab all the roledefs
|
|
// of interest to our user...
|
|
//
|
|
// NOTES
|
|
// - we use IN() but the number of roles is very limited.
|
|
//
|
|
$courseroles = aggregate_roles_from_accessdata($context, $accessdata);
|
|
|
|
// Do we have any interesting "local" roles?
|
|
$localroles = array_diff($localroles,$courseroles); // only "new" local roles
|
|
$wherelocalroles='';
|
|
if (count($localroles)) {
|
|
// Role defs for local roles in 'higher' contexts...
|
|
$contexts = substr($context->path, 1); // kill leading slash
|
|
$contexts = str_replace('/', ',', $contexts);
|
|
$localroleids = implode(',',$localroles);
|
|
$wherelocalroles="OR (rc.roleid IN ({$localroleids})
|
|
AND ctx.id IN ($contexts))" ;
|
|
}
|
|
|
|
// We will want overrides for all of them
|
|
$whereroles = '';
|
|
if ($roleids = implode(',',array_merge($courseroles,$localroles))) {
|
|
$whereroles = "rc.roleid IN ($roleids) AND";
|
|
}
|
|
$sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
|
|
FROM {role_capabilities} rc
|
|
JOIN {context} ctx
|
|
ON rc.contextid=ctx.id
|
|
WHERE ($whereroles
|
|
(ctx.id=? OR ctx.path LIKE ?))
|
|
$wherelocalroles
|
|
ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
|
|
$params = array($context->id, $context->path."/%");
|
|
|
|
$newrdefs = array();
|
|
if ($rs = $DB->get_recordset_sql($sql, $params)) {
|
|
foreach ($rs as $rd) {
|
|
$k = "{$rd->path}:{$rd->roleid}";
|
|
if (!array_key_exists($k, $newrdefs)) {
|
|
$newrdefs[$k] = array();
|
|
}
|
|
$newrdefs[$k][$rd->capability] = $rd->permission;
|
|
}
|
|
$rs->close();
|
|
} else {
|
|
debugging('Bad SQL encountered!');
|
|
}
|
|
|
|
compact_rdefs($newrdefs);
|
|
foreach ($newrdefs as $key=>$value) {
|
|
$accessdata['rdef'][$key] =& $newrdefs[$key];
|
|
}
|
|
|
|
// error_log("loaded {$context->path}");
|
|
$accessdata['loaded'][] = $context->path;
|
|
}
|
|
|
|
/**
|
|
* It add to the access ctrl array the data
|
|
* needed by a role for a given context.
|
|
*
|
|
* The data is added in the rdef key.
|
|
*
|
|
* This role-centric function is useful for role_switching
|
|
* and to get an overview of what a role gets under a
|
|
* given context and below...
|
|
*
|
|
* @param $roleid integer - the id of the user
|
|
* @param $context context obj - needs path!
|
|
* @param $accessdata accessdata array
|
|
*
|
|
*/
|
|
function get_role_access_bycontext($roleid, $context, $accessdata=NULL) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
/* Get the relevant rolecaps into rdef
|
|
* - relevant role caps
|
|
* - at ctx and above
|
|
* - below this ctx
|
|
*/
|
|
|
|
if (is_null($accessdata)) {
|
|
$accessdata = array(); // named list
|
|
$accessdata['ra'] = array();
|
|
$accessdata['rdef'] = array();
|
|
$accessdata['loaded'] = array();
|
|
}
|
|
|
|
$contexts = substr($context->path, 1); // kill leading slash
|
|
$contexts = str_replace('/', ',', $contexts);
|
|
|
|
//
|
|
// Walk up and down the tree to grab all the roledefs
|
|
// of interest to our role...
|
|
//
|
|
// NOTE: we use an IN clauses here - which
|
|
// might explode on huge sites with very convoluted nesting of
|
|
// categories... - extremely unlikely that the number of nested
|
|
// categories is so large that we hit the limits of IN()
|
|
//
|
|
$sql = "SELECT ctx.path, rc.capability, rc.permission
|
|
FROM {role_capabilities} rc
|
|
JOIN {context} ctx
|
|
ON rc.contextid=ctx.id
|
|
WHERE rc.roleid=? AND
|
|
( ctx.id IN ($contexts) OR
|
|
ctx.path LIKE ? )
|
|
ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
|
|
$params = array($roleid, $context->path."/%");
|
|
|
|
if ($rs = $DB->get_recordset_sql($sql, $params)) {
|
|
foreach ($rs as $rd) {
|
|
$k = "{$rd->path}:{$roleid}";
|
|
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
|
|
}
|
|
$rs->close();
|
|
}
|
|
|
|
return $accessdata;
|
|
}
|
|
|
|
/**
|
|
* Load accessdata for a user
|
|
* into the $ACCESSLIB_PRIVATE->accessdatabyuser global
|
|
*
|
|
* Used by has_capability() - but feel free
|
|
* to call it if you are about to run a BIG
|
|
* cron run across a bazillion users.
|
|
*
|
|
*/
|
|
function load_user_accessdata($userid) {
|
|
global $CFG, $ACCESSLIB_PRIVATE;
|
|
|
|
$base = '/'.SYSCONTEXTID;
|
|
|
|
$accessdata = get_user_access_sitewide($userid);
|
|
$frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
|
|
//
|
|
// provide "default role" & set 'dr'
|
|
//
|
|
if (!empty($CFG->defaultuserroleid)) {
|
|
$accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
|
|
if (!isset($accessdata['ra'][$base])) {
|
|
$accessdata['ra'][$base] = array($CFG->defaultuserroleid);
|
|
} else {
|
|
array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
|
|
}
|
|
$accessdata['dr'] = $CFG->defaultuserroleid;
|
|
}
|
|
|
|
//
|
|
// provide "default frontpage role"
|
|
//
|
|
if (!empty($CFG->defaultfrontpageroleid)) {
|
|
$base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
|
|
$accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
|
|
if (!isset($accessdata['ra'][$base])) {
|
|
$accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
|
|
} else {
|
|
array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
|
|
}
|
|
}
|
|
// for dirty timestamps in cron
|
|
$accessdata['time'] = time();
|
|
|
|
$ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
|
|
compact_rdefs($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['rdef']);
|
|
|
|
return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
|
|
}
|
|
|
|
/**
|
|
* Use shared copy of role definistions stored in $ACCESSLIB_PRIVATE->roledefinitions;
|
|
* @param array $rdefs array of role definitions in contexts
|
|
*/
|
|
function compact_rdefs(&$rdefs) {
|
|
global $ACCESSLIB_PRIVATE;
|
|
|
|
/*
|
|
* This is a basic sharing only, we could also
|
|
* use md5 sums of values. The main purpose is to
|
|
* reduce mem in cron jobs - many users in $ACCESSLIB_PRIVATE->accessdatabyuser array.
|
|
*/
|
|
|
|
foreach ($rdefs as $key => $value) {
|
|
if (!array_key_exists($key, $ACCESSLIB_PRIVATE->roledefinitions)) {
|
|
$ACCESSLIB_PRIVATE->roledefinitions[$key] = $rdefs[$key];
|
|
}
|
|
$rdefs[$key] =& $ACCESSLIB_PRIVATE->roledefinitions[$key];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A convenience function to completely load all the capabilities
|
|
* for the current user. This is what gets called from complete_user_login()
|
|
* for example. Call it only _after_ you've setup $USER and called
|
|
* check_enrolment_plugins();
|
|
*
|
|
*/
|
|
function load_all_capabilities() {
|
|
global $USER, $CFG, $ACCESSLIB_PRIVATE;
|
|
|
|
// roles not installed yet - we are in the middle of installation
|
|
if (empty($CFG->rolesactive)) {
|
|
return;
|
|
}
|
|
|
|
$base = '/'.SYSCONTEXTID;
|
|
|
|
if (isguestuser()) {
|
|
$guest = get_guest_role();
|
|
|
|
// Load the rdefs
|
|
$USER->access = get_role_access($guest->id);
|
|
// Put the ghost enrolment in place...
|
|
$USER->access['ra'][$base] = array($guest->id);
|
|
|
|
|
|
} else if (isloggedin()) {
|
|
|
|
$accessdata = get_user_access_sitewide($USER->id);
|
|
|
|
//
|
|
// provide "default role" & set 'dr'
|
|
//
|
|
if (!empty($CFG->defaultuserroleid)) {
|
|
$accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
|
|
if (!isset($accessdata['ra'][$base])) {
|
|
$accessdata['ra'][$base] = array($CFG->defaultuserroleid);
|
|
} else {
|
|
array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
|
|
}
|
|
$accessdata['dr'] = $CFG->defaultuserroleid;
|
|
}
|
|
|
|
$frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
|
|
|
|
//
|
|
// provide "default frontpage role"
|
|
//
|
|
if (!empty($CFG->defaultfrontpageroleid)) {
|
|
$base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
|
|
$accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
|
|
if (!isset($accessdata['ra'][$base])) {
|
|
$accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
|
|
} else {
|
|
array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
|
|
}
|
|
}
|
|
$USER->access = $accessdata;
|
|
|
|
} else if (!empty($CFG->notloggedinroleid)) {
|
|
$USER->access = get_role_access($CFG->notloggedinroleid);
|
|
$USER->access['ra'][$base] = array($CFG->notloggedinroleid);
|
|
}
|
|
|
|
// Timestamp to read dirty context timestamps later
|
|
$USER->access['time'] = time();
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = array();
|
|
|
|
// Clear to force a refresh
|
|
unset($USER->mycourses);
|
|
}
|
|
|
|
/**
|
|
* A convenience function to completely reload all the capabilities
|
|
* for the current user when roles have been updated in a relevant
|
|
* context -- but PRESERVING switchroles and loginas.
|
|
*
|
|
* That is - completely transparent to the user.
|
|
*
|
|
* Note: rewrites $USER->access completely.
|
|
*
|
|
*/
|
|
function reload_all_capabilities() {
|
|
global $USER, $DB;
|
|
|
|
// error_log("reloading");
|
|
// copy switchroles
|
|
$sw = array();
|
|
if (isset($USER->access['rsw'])) {
|
|
$sw = $USER->access['rsw'];
|
|
// error_log(print_r($sw,1));
|
|
}
|
|
|
|
unset($USER->access);
|
|
unset($USER->mycourses);
|
|
|
|
load_all_capabilities();
|
|
|
|
foreach ($sw as $path => $roleid) {
|
|
$context = $DB->get_record('context', array('path'=>$path));
|
|
role_switch($roleid, $context);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Adds a temp role to an accessdata array.
|
|
*
|
|
* Useful for the "temporary guest" access
|
|
* we grant to logged-in users.
|
|
*
|
|
* Note - assumes a course context!
|
|
*
|
|
*/
|
|
function load_temp_role($context, $roleid, $accessdata) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
//
|
|
// Load rdefs for the role in -
|
|
// - this context
|
|
// - all the parents
|
|
// - and below - IOWs overrides...
|
|
//
|
|
|
|
// turn the path into a list of context ids
|
|
$contexts = substr($context->path, 1); // kill leading slash
|
|
$contexts = str_replace('/', ',', $contexts);
|
|
|
|
$sql = "SELECT ctx.path, rc.capability, rc.permission
|
|
FROM {context} ctx
|
|
JOIN {role_capabilities} rc
|
|
ON rc.contextid=ctx.id
|
|
WHERE (ctx.id IN ($contexts)
|
|
OR ctx.path LIKE ?)
|
|
AND rc.roleid = ?
|
|
ORDER BY ctx.depth, ctx.path";
|
|
$params = array($context->path."/%", $roleid);
|
|
if ($rs = $DB->get_recordset_sql($sql, $params)) {
|
|
foreach ($rs as $rd) {
|
|
$k = "{$rd->path}:{$roleid}";
|
|
$accessdata['rdef'][$k][$rd->capability] = $rd->permission;
|
|
}
|
|
$rs->close();
|
|
}
|
|
|
|
//
|
|
// Say we loaded everything for the course context
|
|
// - which we just did - if the user gets a proper
|
|
// RA in this session, this data will need to be reloaded,
|
|
// but that is handled by the complete accessdata reload
|
|
//
|
|
array_push($accessdata['loaded'], $context->path);
|
|
|
|
//
|
|
// Add the ghost RA
|
|
//
|
|
if (isset($accessdata['ra'][$context->path])) {
|
|
array_push($accessdata['ra'][$context->path], $roleid);
|
|
} else {
|
|
$accessdata['ra'][$context->path] = array($roleid);
|
|
}
|
|
|
|
return $accessdata;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check all the login enrolment information for the given user object
|
|
* by querying the enrolment plugins
|
|
* @return void
|
|
*/
|
|
function check_enrolment_plugins(&$user) {
|
|
global $CFG;
|
|
|
|
if (empty($user->id) or isguestuser($user)) {
|
|
// shortcut - there is no enrolment work for guests and not-logged-in users
|
|
return;
|
|
}
|
|
|
|
static $inprogress = array(); // To prevent this function being called more than once in an invocation
|
|
|
|
if (!empty($inprogress[$user->id])) {
|
|
return;
|
|
}
|
|
|
|
$inprogress[$user->id] = true; // Set the flag
|
|
|
|
require_once($CFG->dirroot .'/enrol/enrol.class.php');
|
|
|
|
if (!($plugins = explode(',', $CFG->enrol_plugins_enabled))) {
|
|
$plugins = array($CFG->enrol);
|
|
}
|
|
|
|
foreach ($plugins as $plugin) {
|
|
$enrol = enrolment_factory::factory($plugin);
|
|
if (method_exists($enrol, 'setup_enrolments')) { /// Plugin supports Roles (Moodle 1.7 and later)
|
|
$enrol->setup_enrolments($user);
|
|
} else { /// Run legacy enrolment methods
|
|
if (method_exists($enrol, 'get_student_courses')) {
|
|
$enrol->get_student_courses($user);
|
|
}
|
|
if (method_exists($enrol, 'get_teacher_courses')) {
|
|
$enrol->get_teacher_courses($user);
|
|
}
|
|
|
|
/// deal with $user->students and $user->teachers stuff
|
|
unset($user->student);
|
|
unset($user->teacher);
|
|
}
|
|
unset($enrol);
|
|
}
|
|
|
|
unset($inprogress[$user->id]); // Unset the flag
|
|
}
|
|
|
|
/**
|
|
* Returns array of all legacy roles.
|
|
*/
|
|
function get_legacy_roles() {
|
|
return array(
|
|
'admin' => 'moodle/legacy:admin',
|
|
'coursecreator' => 'moodle/legacy:coursecreator',
|
|
'editingteacher' => 'moodle/legacy:editingteacher',
|
|
'teacher' => 'moodle/legacy:teacher',
|
|
'student' => 'moodle/legacy:student',
|
|
'guest' => 'moodle/legacy:guest',
|
|
'user' => 'moodle/legacy:user'
|
|
);
|
|
}
|
|
|
|
function get_legacy_type($roleid) {
|
|
$sitecontext = get_context_instance(CONTEXT_SYSTEM);
|
|
$legacyroles = get_legacy_roles();
|
|
|
|
$result = '';
|
|
foreach($legacyroles as $ltype=>$lcap) {
|
|
$localoverride = get_local_override($roleid, $sitecontext->id, $lcap);
|
|
if (!empty($localoverride->permission) and $localoverride->permission == CAP_ALLOW) {
|
|
//choose first selected legacy capability - reset the rest
|
|
if (empty($result)) {
|
|
$result = $ltype;
|
|
} else {
|
|
unassign_capability($lcap, $roleid);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Assign the defaults found in this capabality definition to roles that have
|
|
* the corresponding legacy capabilities assigned to them.
|
|
* @param $legacyperms - an array in the format (example):
|
|
* 'guest' => CAP_PREVENT,
|
|
* 'student' => CAP_ALLOW,
|
|
* 'teacher' => CAP_ALLOW,
|
|
* 'editingteacher' => CAP_ALLOW,
|
|
* 'coursecreator' => CAP_ALLOW,
|
|
* 'admin' => CAP_ALLOW
|
|
* @return boolean - success or failure.
|
|
*/
|
|
function assign_legacy_capabilities($capability, $legacyperms) {
|
|
|
|
$legacyroles = get_legacy_roles();
|
|
|
|
foreach ($legacyperms as $type => $perm) {
|
|
|
|
$systemcontext = get_context_instance(CONTEXT_SYSTEM);
|
|
|
|
if (!array_key_exists($type, $legacyroles)) {
|
|
print_error('invalidlegacy', '', '', $type);
|
|
}
|
|
|
|
if ($roles = get_roles_with_capability($legacyroles[$type], CAP_ALLOW)) {
|
|
foreach ($roles as $role) {
|
|
// Assign a site level capability.
|
|
if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks to see if a capability is one of the special capabilities
|
|
* (either a legacy capability, or moodle/site:doanything).
|
|
* @param string $capabilityname the capability name, e.g. mod/forum:view.
|
|
* @return boolean whether this is one of the special capabilities.
|
|
*/
|
|
function is_legacy($capabilityname) {
|
|
if ($capabilityname == 'moodle/site:doanything' || strpos($capabilityname, 'moodle/legacy') === 0) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param object $capability a capbility - a row from the capabilitites table.
|
|
* @return boolean whether this capability is safe - that is, wether people with the
|
|
* safeoverrides capability should be allowed to change it.
|
|
*/
|
|
function is_safe_capability($capability) {
|
|
return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
|
|
}
|
|
|
|
/**********************************
|
|
* Context Manipulation functions *
|
|
**********************************/
|
|
|
|
/**
|
|
* Create a new context record for use by all roles-related stuff
|
|
* assumes that the caller has done the homework.
|
|
*
|
|
* @param $level
|
|
* @param $instanceid
|
|
*
|
|
* @return object newly created context
|
|
*/
|
|
function create_context($contextlevel, $instanceid) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
if ($contextlevel == CONTEXT_SYSTEM) {
|
|
return create_system_context();
|
|
}
|
|
|
|
$context = new object();
|
|
$context->contextlevel = $contextlevel;
|
|
$context->instanceid = $instanceid;
|
|
|
|
// Define $context->path based on the parent
|
|
// context. In other words... Who is your daddy?
|
|
$basepath = '/' . SYSCONTEXTID;
|
|
$basedepth = 1;
|
|
|
|
$result = true;
|
|
$error_message = null;
|
|
|
|
switch ($contextlevel) {
|
|
case CONTEXT_COURSECAT:
|
|
$sql = "SELECT ctx.path, ctx.depth
|
|
FROM {context} ctx
|
|
JOIN {course_categories} cc
|
|
ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
|
|
WHERE cc.id=?";
|
|
$params = array($instanceid);
|
|
if ($p = $DB->get_record_sql($sql, $params)) {
|
|
$basepath = $p->path;
|
|
$basedepth = $p->depth;
|
|
} else if ($category = $DB->get_record('course_categories', array('id'=>$instanceid))) {
|
|
if (empty($category->parent)) {
|
|
// ok - this is a top category
|
|
} else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) {
|
|
$basepath = $parent->path;
|
|
$basedepth = $parent->depth;
|
|
} else {
|
|
// wrong parent category - no big deal, this can be fixed later
|
|
$basepath = null;
|
|
$basedepth = 0;
|
|
}
|
|
} else {
|
|
// incorrect category id
|
|
$error_message = "incorrect course category id ($instanceid)";
|
|
$result = false;
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_COURSE:
|
|
$sql = "SELECT ctx.path, ctx.depth
|
|
FROM {context} ctx
|
|
JOIN {course} c
|
|
ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
|
|
WHERE c.id=? AND c.id !=" . SITEID;
|
|
$params = array($instanceid);
|
|
if ($p = $DB->get_record_sql($sql, $params)) {
|
|
$basepath = $p->path;
|
|
$basedepth = $p->depth;
|
|
} else if ($course = $DB->get_record('course', array('id'=>$instanceid))) {
|
|
if ($course->id == SITEID) {
|
|
//ok - no parent category
|
|
} else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) {
|
|
$basepath = $parent->path;
|
|
$basedepth = $parent->depth;
|
|
} else {
|
|
// wrong parent category of course - no big deal, this can be fixed later
|
|
$basepath = null;
|
|
$basedepth = 0;
|
|
}
|
|
} else if ($instanceid == SITEID) {
|
|
// no errors for missing site course during installation
|
|
return false;
|
|
} else {
|
|
// incorrect course id
|
|
$error_message = "incorrect course id ($instanceid)";
|
|
$result = false;
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_MODULE:
|
|
$sql = "SELECT ctx.path, ctx.depth
|
|
FROM {context} ctx
|
|
JOIN {course_modules} cm
|
|
ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
|
|
WHERE cm.id=?";
|
|
$params = array($instanceid);
|
|
if ($p = $DB->get_record_sql($sql, $params)) {
|
|
$basepath = $p->path;
|
|
$basedepth = $p->depth;
|
|
} else if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid))) {
|
|
if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course)) {
|
|
$basepath = $parent->path;
|
|
$basedepth = $parent->depth;
|
|
} else {
|
|
// course does not exist - modules can not exist without a course
|
|
$error_message = "course does not exist ($cm->course) - modules can not exist without a course";
|
|
$result = false;
|
|
}
|
|
} else {
|
|
// cm does not exist
|
|
$error_message = "cm with id $instanceid does not exist";
|
|
$result = false;
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_BLOCK:
|
|
// Only non-pinned & course-page based
|
|
$sql = "SELECT ctx.path, ctx.depth
|
|
FROM {context} ctx
|
|
JOIN {block_instance} bi
|
|
ON (bi.pageid=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
|
|
WHERE bi.id=? AND bi.pagetype='course-view'";
|
|
$params = array($instanceid);
|
|
if ($p = $DB->get_record_sql($sql, $params)) {
|
|
$basepath = $p->path;
|
|
$basedepth = $p->depth;
|
|
} else if ($bi = $DB->get_record('block_instance', array('id'=>$instanceid))) {
|
|
if ($bi->pagetype != 'course-view') {
|
|
// ok - not a course block
|
|
} else if ($parent = get_context_instance(CONTEXT_COURSE, $bi->pageid)) {
|
|
$basepath = $parent->path;
|
|
$basedepth = $parent->depth;
|
|
} else {
|
|
// parent course does not exist - course blocks can not exist without a course
|
|
$error_message = 'parent course does not exist - course blocks can not exist without a course';
|
|
$result = false;
|
|
}
|
|
} else {
|
|
// block does not exist
|
|
$error_message = 'block does not exist';
|
|
$result = false;
|
|
}
|
|
break;
|
|
case CONTEXT_USER:
|
|
// default to basepath
|
|
break;
|
|
}
|
|
|
|
// if grandparents unknown, maybe rebuild_context_path() will solve it later
|
|
if ($basedepth != 0) {
|
|
$context->depth = $basedepth+1;
|
|
}
|
|
|
|
if ($result and $id = $DB->insert_record('context', $context)) {
|
|
// can't set the full path till we know the id!
|
|
if ($basedepth != 0 and !empty($basepath)) {
|
|
$DB->set_field('context', 'path', $basepath.'/'. $id, array('id'=>$id));
|
|
}
|
|
return get_context_instance_by_id($id);
|
|
|
|
} else {
|
|
debugging('Error: could not insert new context level "'.
|
|
s($contextlevel).'", instance "'.
|
|
s($instanceid).'". ' . $error_message);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns system context or null if can not be created yet.
|
|
* @param bool $cache use caching
|
|
* @return mixed system context or null
|
|
*/
|
|
function get_system_context($cache=true) {
|
|
global $DB, $ACCESSLIB_PRIVATE;
|
|
if ($cache and defined('SYSCONTEXTID')) {
|
|
if (is_null($ACCESSLIB_PRIVATE->systemcontext)) {
|
|
$ACCESSLIB_PRIVATE->systemcontext = new object();
|
|
$ACCESSLIB_PRIVATE->systemcontext->id = SYSCONTEXTID;
|
|
$ACCESSLIB_PRIVATE->systemcontext->contextlevel = CONTEXT_SYSTEM;
|
|
$ACCESSLIB_PRIVATE->systemcontext->instanceid = 0;
|
|
$ACCESSLIB_PRIVATE->systemcontext->path = '/'.SYSCONTEXTID;
|
|
$ACCESSLIB_PRIVATE->systemcontext->depth = 1;
|
|
}
|
|
return $ACCESSLIB_PRIVATE->systemcontext;
|
|
}
|
|
try {
|
|
// TODO: can not use get_record() because we do not know if query failed :-(
|
|
// switch to get_record() later
|
|
$contextarr = $DB->get_records('context', array('contextlevel'=>CONTEXT_SYSTEM));
|
|
if ($contextarr === false) {
|
|
return null;
|
|
}
|
|
$context = $contextarr ? reset($contextarr) : null;
|
|
} catch (dml_exception $e) {
|
|
//table does not exist yet, sorry
|
|
return null;
|
|
}
|
|
|
|
if (!$context) {
|
|
$context = new object();
|
|
$context->contextlevel = CONTEXT_SYSTEM;
|
|
$context->instanceid = 0;
|
|
$context->depth = 1;
|
|
$context->path = NULL; //not known before insert
|
|
|
|
try {
|
|
if (!$context->id = $DB->insert_record('context', $context)) {
|
|
// can not create context yet, sorry
|
|
return null;
|
|
}
|
|
} catch (dml_exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) {
|
|
$context->instanceid = 0;
|
|
$context->path = '/'.$context->id;
|
|
$context->depth = 1;
|
|
$DB->update_record('context', $context);
|
|
}
|
|
|
|
if (!defined('SYSCONTEXTID')) {
|
|
define('SYSCONTEXTID', $context->id);
|
|
}
|
|
|
|
$ACCESSLIB_PRIVATE->systemcontext = $context;
|
|
return $ACCESSLIB_PRIVATE->systemcontext;
|
|
}
|
|
|
|
/**
|
|
* Remove a context record and any dependent entries,
|
|
* removes context from static context cache too
|
|
* @param $level
|
|
* @param $instanceid
|
|
*
|
|
* @return bool properly deleted
|
|
*/
|
|
function delete_context($contextlevel, $instanceid) {
|
|
global $DB, $ACCESSLIB_PRIVATE;
|
|
|
|
// do not use get_context_instance(), because the related object might not exist,
|
|
// or the context does not exist yet and it would be created now
|
|
if ($context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
|
|
$result = $DB->delete_records('role_assignments', array('contextid'=>$context->id)) &&
|
|
$DB->delete_records('role_capabilities', array('contextid'=>$context->id)) &&
|
|
$DB->delete_records('role_names', array('contextid'=>$context->id)) &&
|
|
$DB->delete_records('context', array('id'=>$context->id));
|
|
|
|
// do not mark dirty contexts if parents unknown
|
|
if (!is_null($context->path) and $context->depth > 0) {
|
|
mark_context_dirty($context->path);
|
|
}
|
|
|
|
// purge static context cache if entry present
|
|
unset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instanceid]);
|
|
unset($ACCESSLIB_PRIVATE->contextsbyid[$context->id]);
|
|
|
|
return $result;
|
|
} else {
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Precreates all contexts including all parents
|
|
* @param int $contextlevel, empty means all
|
|
* @param bool $buildpaths update paths and depths
|
|
* @return void
|
|
*/
|
|
function create_contexts($contextlevel=null, $buildpaths=true) {
|
|
global $DB;
|
|
|
|
//make sure system context exists
|
|
$syscontext = get_system_context(false);
|
|
|
|
if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT
|
|
or $contextlevel == CONTEXT_COURSE
|
|
or $contextlevel == CONTEXT_MODULE
|
|
or $contextlevel == CONTEXT_BLOCK) {
|
|
$sql = "INSERT INTO {context} (contextlevel, instanceid)
|
|
SELECT ".CONTEXT_COURSECAT.", cc.id
|
|
FROM {course}_categories cc
|
|
WHERE NOT EXISTS (SELECT 'x'
|
|
FROM {context} cx
|
|
WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
|
|
$DB->execute($sql);
|
|
|
|
}
|
|
|
|
if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE
|
|
or $contextlevel == CONTEXT_MODULE
|
|
or $contextlevel == CONTEXT_BLOCK) {
|
|
$sql = "INSERT INTO {context} (contextlevel, instanceid)
|
|
SELECT ".CONTEXT_COURSE.", c.id
|
|
FROM {course} c
|
|
WHERE NOT EXISTS (SELECT 'x'
|
|
FROM {context} cx
|
|
WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
|
|
$DB->execute($sql);
|
|
|
|
}
|
|
|
|
if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE) {
|
|
$sql = "INSERT INTO {context} (contextlevel, instanceid)
|
|
SELECT ".CONTEXT_MODULE.", cm.id
|
|
FROM {course}_modules cm
|
|
WHERE NOT EXISTS (SELECT 'x'
|
|
FROM {context} cx
|
|
WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
|
|
$DB->execute($sql);
|
|
}
|
|
|
|
if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) {
|
|
$sql = "INSERT INTO {context} (contextlevel, instanceid)
|
|
SELECT ".CONTEXT_BLOCK.", bi.id
|
|
FROM {block_instance} bi
|
|
WHERE NOT EXISTS (SELECT 'x'
|
|
FROM {context} cx
|
|
WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
|
|
$DB->execute($sql);
|
|
}
|
|
|
|
if (empty($contextlevel) or $contextlevel == CONTEXT_USER) {
|
|
$sql = "INSERT INTO {context} (contextlevel, instanceid)
|
|
SELECT ".CONTEXT_USER.", u.id
|
|
FROM {user} u
|
|
WHERE u.deleted=0
|
|
AND NOT EXISTS (SELECT 'x'
|
|
FROM {context} cx
|
|
WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
|
|
$DB->execute($sql);
|
|
|
|
}
|
|
|
|
if ($buildpaths) {
|
|
build_context_path(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove stale context records
|
|
*
|
|
* @return bool
|
|
*/
|
|
function cleanup_contexts() {
|
|
global $DB;
|
|
|
|
$sql = " SELECT c.contextlevel,
|
|
c.instanceid AS instanceid
|
|
FROM {context} c
|
|
LEFT OUTER JOIN {course}_categories t
|
|
ON c.instanceid = t.id
|
|
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
|
|
UNION
|
|
SELECT c.contextlevel,
|
|
c.instanceid
|
|
FROM {context} c
|
|
LEFT OUTER JOIN {course} t
|
|
ON c.instanceid = t.id
|
|
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
|
|
UNION
|
|
SELECT c.contextlevel,
|
|
c.instanceid
|
|
FROM {context} c
|
|
LEFT OUTER JOIN {course}_modules t
|
|
ON c.instanceid = t.id
|
|
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
|
|
UNION
|
|
SELECT c.contextlevel,
|
|
c.instanceid
|
|
FROM {context} c
|
|
LEFT OUTER JOIN {user} t
|
|
ON c.instanceid = t.id
|
|
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
|
|
UNION
|
|
SELECT c.contextlevel,
|
|
c.instanceid
|
|
FROM {context} c
|
|
LEFT OUTER JOIN {block_instance} t
|
|
ON c.instanceid = t.id
|
|
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
|
|
UNION
|
|
SELECT c.contextlevel,
|
|
c.instanceid
|
|
FROM {context} c
|
|
LEFT OUTER JOIN {groups} t
|
|
ON c.instanceid = t.id
|
|
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_GROUP."
|
|
";
|
|
if ($rs = $DB->get_recordset_sql($sql)) {
|
|
$DB->begin_sql();
|
|
$ok = true;
|
|
foreach ($rs as $ctx) {
|
|
if (!delete_context($ctx->contextlevel, $ctx->instanceid)) {
|
|
$ok = false;
|
|
break;
|
|
}
|
|
}
|
|
$rs->close();
|
|
if ($ok) {
|
|
$DB->commit_sql();
|
|
return true;
|
|
} else {
|
|
$DB->rollback_sql();
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Preloads all contexts relating to a course: course, modules, and blocks.
|
|
*
|
|
* @param int $courseid Course ID
|
|
* @return void
|
|
*/
|
|
function preload_course_contexts($courseid) {
|
|
global $DB, $ACCESSLIB_PRIVATE;
|
|
|
|
// Users can call this multiple times without doing any harm
|
|
global $ACCESSLIB_PRIVATE;
|
|
if (array_key_exists($courseid, $ACCESSLIB_PRIVATE->preloadedcourses)) {
|
|
return;
|
|
}
|
|
|
|
$params = array($courseid, $courseid, $courseid);
|
|
$sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
|
|
FROM {course_modules} cm
|
|
JOIN {context} x ON x.instanceid=cm.id
|
|
WHERE cm.course=? AND x.contextlevel=".CONTEXT_MODULE."
|
|
|
|
UNION ALL
|
|
|
|
SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
|
|
FROM {block_instance} bi
|
|
JOIN {context} x ON x.instanceid=bi.id
|
|
WHERE bi.pageid=? AND bi.pagetype='course-view'
|
|
AND x.contextlevel=".CONTEXT_BLOCK."
|
|
|
|
UNION ALL
|
|
|
|
SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
|
|
FROM {context} x
|
|
WHERE x.instanceid=? AND x.contextlevel=".CONTEXT_COURSE."";
|
|
|
|
$rs = $DB->get_recordset_sql($sql, $params);
|
|
foreach($rs as $context) {
|
|
cache_context($context);
|
|
}
|
|
$rs->close();
|
|
$ACCESSLIB_PRIVATE->preloadedcourses[$courseid] = true;
|
|
}
|
|
|
|
/**
|
|
* Get the context instance as an object. This function will create the
|
|
* context instance if it does not exist yet.
|
|
* @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE.
|
|
* @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id,
|
|
* for $level = CONTEXT_MODULE, this would be $cm->id. And so on.
|
|
* @return object The context object.
|
|
*/
|
|
function get_context_instance($contextlevel, $instance=0) {
|
|
|
|
global $DB, $ACCESSLIB_PRIVATE;
|
|
static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_GROUP, CONTEXT_MODULE, CONTEXT_BLOCK);
|
|
|
|
if ($contextlevel === 'clearcache') {
|
|
// TODO: Remove for v2.0
|
|
// No longer needed, but we'll catch it to avoid erroring out on custom code.
|
|
// This used to be a fix for MDL-9016
|
|
// "Restoring into existing course, deleting first
|
|
// deletes context and doesn't recreate it"
|
|
return false;
|
|
}
|
|
|
|
/// System context has special cache
|
|
if ($contextlevel == CONTEXT_SYSTEM) {
|
|
return get_system_context();
|
|
}
|
|
|
|
/// check allowed context levels
|
|
if (!in_array($contextlevel, $allowed_contexts)) {
|
|
// fatal error, code must be fixed - probably typo or switched parameters
|
|
print_error('invalidcourselevel');
|
|
}
|
|
|
|
if (!is_array($instance)) {
|
|
/// Check the cache
|
|
if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) { // Already cached
|
|
return $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
|
|
}
|
|
|
|
/// Get it from the database, or create it
|
|
if (!$context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instance))) {
|
|
$context = create_context($contextlevel, $instance);
|
|
}
|
|
|
|
/// Only add to cache if context isn't empty.
|
|
if (!empty($context)) {
|
|
cache_context($context);
|
|
}
|
|
|
|
return $context;
|
|
}
|
|
|
|
|
|
/// ok, somebody wants to load several contexts to save some db queries ;-)
|
|
$instances = $instance;
|
|
$result = array();
|
|
|
|
foreach ($instances as $key=>$instance) {
|
|
/// Check the cache first
|
|
if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) { // Already cached
|
|
$result[$instance] = $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
|
|
unset($instances[$key]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ($instances) {
|
|
list($instanceids, $params) = $DB->get_in_or_equal($instances, SQL_PARAMS_QM);
|
|
array_unshift($params, $contextlevel);
|
|
$sql = "SELECT instanceid, id, contextlevel, path, depth
|
|
FROM {context}
|
|
WHERE contextlevel=? AND instanceid $instanceids";
|
|
|
|
if (!$contexts = $DB->get_records_sql($sql, $params)) {
|
|
$contexts = array();
|
|
}
|
|
|
|
foreach ($instances as $instance) {
|
|
if (isset($contexts[$instance])) {
|
|
$context = $contexts[$instance];
|
|
} else {
|
|
$context = create_context($contextlevel, $instance);
|
|
}
|
|
|
|
if (!empty($context)) {
|
|
cache_context($context);
|
|
}
|
|
|
|
$result[$instance] = $context;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a context instance as an object, from a given context id.
|
|
* @param mixed $id a context id or array of ids.
|
|
* @return mixed object or array of the context object.
|
|
*/
|
|
function get_context_instance_by_id($id) {
|
|
global $DB, $ACCESSLIB_PRIVATE;
|
|
|
|
if ($id == SYSCONTEXTID) {
|
|
return get_system_context();
|
|
}
|
|
|
|
if (isset($ACCESSLIB_PRIVATE->contextsbyid[$id])) { // Already cached
|
|
return $ACCESSLIB_PRIVATE->contextsbyid[$id];
|
|
}
|
|
|
|
if ($context = $DB->get_record('context', array('id'=>$id))) {
|
|
cache_context($context);
|
|
return $context;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the local override (if any) for a given capability in a role in a context
|
|
* @param $roleid
|
|
* @param $contextid
|
|
* @param $capability
|
|
*/
|
|
function get_local_override($roleid, $contextid, $capability) {
|
|
global $DB;
|
|
return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
|
|
}
|
|
|
|
|
|
|
|
/************************************
|
|
* DB TABLE RELATED FUNCTIONS *
|
|
************************************/
|
|
|
|
/**
|
|
* function that creates a role
|
|
* @param name - role name
|
|
* @param shortname - role short name
|
|
* @param description - role description
|
|
* @param legacy - optional legacy capability
|
|
* @return id or dml_exception
|
|
*/
|
|
function create_role($name, $shortname, $description, $legacy='') {
|
|
global $DB;
|
|
|
|
// Get the system context.
|
|
$context = get_context_instance(CONTEXT_SYSTEM);
|
|
|
|
// Insert the role record.
|
|
$role = new object();
|
|
$role->name = $name;
|
|
$role->shortname = $shortname;
|
|
$role->description = $description;
|
|
|
|
//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);
|
|
|
|
if ($legacy) {
|
|
assign_capability($legacy, CAP_ALLOW, $id, $context->id);
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* function that deletes a role and cleanups up after it
|
|
* @param roleid - id of role to delete
|
|
* @return success
|
|
*/
|
|
function delete_role($roleid) {
|
|
global $CFG, $DB;
|
|
$success = true;
|
|
|
|
// mdl 10149, check if this is the last active admin role
|
|
// if we make the admin role not deletable then this part can go
|
|
|
|
$systemcontext = get_context_instance(CONTEXT_SYSTEM);
|
|
|
|
if ($role = $DB->get_record('role', array('id'=>$roleid))) {
|
|
if ($DB->record_exists('role_capabilities', array('contextid'=>$systemcontext->id, 'roleid'=>$roleid, 'capability'=>'moodle/site:doanything'))) {
|
|
// deleting an admin role
|
|
$status = false;
|
|
if ($adminroles = get_roles_with_capability('moodle/site:doanything', CAP_ALLOW, $systemcontext)) {
|
|
foreach ($adminroles as $adminrole) {
|
|
if ($adminrole->id != $roleid) {
|
|
// some other admin role
|
|
if ($DB->record_exists('role_assignments', array('roleid'=>$adminrole->id, 'contextid'=>$systemcontext->id))) {
|
|
// found another admin role with at least 1 user assigned
|
|
$status = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($status !== true) {
|
|
print_error('cannotdeleterolenoadmin', 'access');
|
|
}
|
|
}
|
|
}
|
|
|
|
// first unssign all users
|
|
if (!role_unassign($roleid)) {
|
|
debugging("Error while unassigning all users from role with ID $roleid!");
|
|
$success = false;
|
|
}
|
|
|
|
// cleanup all references to this role, ignore errors
|
|
if ($success) {
|
|
$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));
|
|
|
|
if ($success and !$DB->delete_records('role', array('id'=>$roleid))) {
|
|
debugging("Could not delete role record with ID $roleid!");
|
|
$success = false;
|
|
}
|
|
|
|
if ($success) {
|
|
add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '');
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Function to write context specific overrides, or default capabilities.
|
|
* @param module - string name
|
|
* @param capability - string name
|
|
* @param contextid - context id
|
|
* @param roleid - role id
|
|
* @param permission - int 1,-1 or -1000
|
|
* should not be writing if permission is 0
|
|
*/
|
|
function assign_capability($capability, $permission, $roleid, $contextid, $overwrite=false) {
|
|
|
|
global $USER, $DB;
|
|
|
|
if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
|
|
unassign_capability($capability, $roleid, $contextid);
|
|
return true;
|
|
}
|
|
|
|
$existing = $DB->get_record('role_capabilities', array('contextid'=>$contextid, 'roleid'=>$roleid, 'capability'=>$capability));
|
|
|
|
if ($existing and !$overwrite) { // We want to keep whatever is there already
|
|
return true;
|
|
}
|
|
|
|
$cap = new object;
|
|
$cap->contextid = $contextid;
|
|
$cap->roleid = $roleid;
|
|
$cap->capability = $capability;
|
|
$cap->permission = $permission;
|
|
$cap->timemodified = time();
|
|
$cap->modifierid = empty($USER->id) ? 0 : $USER->id;
|
|
|
|
if ($existing) {
|
|
$cap->id = $existing->id;
|
|
return $DB->update_record('role_capabilities', $cap);
|
|
} else {
|
|
$c = $DB->get_record('context', array('id'=>$contextid));
|
|
return $DB->insert_record('role_capabilities', $cap);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unassign a capability from a role.
|
|
* @param $roleid - the role id
|
|
* @param $capability - the name of the capability
|
|
* @return boolean - success or failure
|
|
*/
|
|
function unassign_capability($capability, $roleid, $contextid=NULL) {
|
|
global $DB;
|
|
|
|
if (isset($contextid)) {
|
|
// delete from context rel, if this is the last override in this context
|
|
$status = $DB->delete_records('role_capabilities', array('capability'=>$capability,
|
|
'roleid'=>$roleid, 'contextid'=>$contextid));
|
|
} else {
|
|
$status = $DB->delete_records('role_capabilities', array('capability'=>$capability,
|
|
'roleid'=>$roleid));
|
|
}
|
|
return $status;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 assignment only.
|
|
* @param $capability - capability name (string)
|
|
* @param $permission - optional, the permission defined for this capability
|
|
* either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT
|
|
* @return array or role objects
|
|
*/
|
|
function get_roles_with_capability($capability, $permission=NULL, $context='') {
|
|
|
|
global $CFG, $DB;
|
|
|
|
$params = array();
|
|
|
|
if ($context) {
|
|
if ($contexts = get_parent_contexts($context)) {
|
|
$listofcontexts = '('.implode(',', $contexts).')';
|
|
} else {
|
|
$sitecontext = get_context_instance(CONTEXT_SYSTEM);
|
|
$listofcontexts = '('.$sitecontext->id.')'; // must be site
|
|
}
|
|
$contextstr = "AND (rc.contextid = ? OR rc.contextid IN $listofcontexts)";
|
|
$params[] = $context->id;
|
|
} else {
|
|
$contextstr = '';
|
|
}
|
|
|
|
$selectroles = "SELECT r.*
|
|
FROM {role} r,
|
|
{role_capabilities} rc
|
|
WHERE rc.capability = ?
|
|
AND rc.roleid = r.id $contextstr";
|
|
|
|
array_unshift($params, $capability);
|
|
|
|
if (isset($permission)) {
|
|
$selectroles .= " AND rc.permission = ?";
|
|
$params[] = $permission;
|
|
}
|
|
return $DB->get_records_sql($selectroles, $params);
|
|
}
|
|
|
|
|
|
/**
|
|
* This function makes a role-assignment (a role for a user or group in a particular context)
|
|
* @param $roleid - the role of the id
|
|
* @param $userid - userid
|
|
* @param $groupid - group id
|
|
* @param $contextid - id of the context
|
|
* @param $timestart - time this assignment becomes effective
|
|
* @param $timeend - time this assignemnt ceases to be effective
|
|
* @uses $USER
|
|
* @return id - new id of the assigment
|
|
*/
|
|
function role_assign($roleid, $userid, $groupid, $contextid, $timestart=0, $timeend=0, $hidden=0, $enrol='manual',$timemodified='') {
|
|
global $USER, $CFG, $DB;
|
|
|
|
/// Do some data validation
|
|
|
|
if (empty($roleid)) {
|
|
debugging('Role ID not provided');
|
|
return false;
|
|
}
|
|
|
|
if (empty($userid) && empty($groupid)) {
|
|
debugging('Either userid or groupid must be provided');
|
|
return false;
|
|
}
|
|
|
|
if ($userid && !$DB->record_exists('user', array('id'=>$userid))) {
|
|
debugging('User ID '.intval($userid).' does not exist!');
|
|
return false;
|
|
}
|
|
|
|
if ($groupid && !groups_group_exists($groupid)) {
|
|
debugging('Group ID '.intval($groupid).' does not exist!');
|
|
return false;
|
|
}
|
|
|
|
if (!$context = get_context_instance_by_id($contextid)) {
|
|
debugging('Context ID '.intval($contextid).' does not exist!');
|
|
return false;
|
|
}
|
|
|
|
if (($timestart and $timeend) and ($timestart > $timeend)) {
|
|
debugging('The end time can not be earlier than the start time');
|
|
return false;
|
|
}
|
|
|
|
if (!$timemodified) {
|
|
$timemodified = time();
|
|
}
|
|
|
|
/// Check for existing entry
|
|
if ($userid) {
|
|
$ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid));
|
|
} else {
|
|
$ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'groupid'=>$groupid));
|
|
}
|
|
|
|
if (empty($ra)) { // Create a new entry
|
|
$ra = new object();
|
|
$ra->roleid = $roleid;
|
|
$ra->contextid = $context->id;
|
|
$ra->userid = $userid;
|
|
$ra->hidden = $hidden;
|
|
$ra->enrol = $enrol;
|
|
/// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
|
|
/// by repeating queries with the same exact parameters in a 100 secs time window
|
|
$ra->timestart = round($timestart, -2);
|
|
$ra->timeend = $timeend;
|
|
$ra->timemodified = $timemodified;
|
|
$ra->modifierid = empty($USER->id) ? 0 : $USER->id;
|
|
|
|
if (!$ra->id = $DB->insert_record('role_assignments', $ra)) {
|
|
return false;
|
|
}
|
|
|
|
} else { // We already have one, just update it
|
|
$ra->id = $ra->id;
|
|
$ra->hidden = $hidden;
|
|
$ra->enrol = $enrol;
|
|
/// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
|
|
/// by repeating queries with the same exact parameters in a 100 secs time window
|
|
$ra->timestart = round($timestart, -2);
|
|
$ra->timeend = $timeend;
|
|
$ra->timemodified = $timemodified;
|
|
$ra->modifierid = empty($USER->id) ? 0 : $USER->id;
|
|
|
|
if (!$DB->update_record('role_assignments', $ra)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// mark context as dirty - modules might use has_capability() in xxx_role_assing()
|
|
/// again expensive, but needed
|
|
mark_context_dirty($context->path);
|
|
|
|
if (!empty($USER->id) && $USER->id == $userid) {
|
|
/// If the user is the current user, then do full reload of capabilities too.
|
|
load_all_capabilities();
|
|
}
|
|
|
|
/// Ask all the modules if anything needs to be done for this user
|
|
if ($mods = get_list_of_plugins('mod')) {
|
|
foreach ($mods as $mod) {
|
|
include_once($CFG->dirroot.'/mod/'.$mod.'/lib.php');
|
|
$functionname = $mod.'_role_assign';
|
|
if (function_exists($functionname)) {
|
|
$functionname($userid, $context, $roleid);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// now handle metacourse role assignments if in course context
|
|
if ($context->contextlevel == CONTEXT_COURSE) {
|
|
if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
|
|
foreach ($parents as $parent) {
|
|
sync_metacourse($parent->parent_course);
|
|
}
|
|
}
|
|
}
|
|
|
|
events_trigger('role_assigned', $ra);
|
|
|
|
return $ra->id;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes one or more role assignments. You must specify at least one parameter.
|
|
* @param $roleid
|
|
* @param $userid
|
|
* @param $groupid
|
|
* @param $contextid
|
|
* @param $enrol unassign only if enrolment type matches, NULL means anything
|
|
* @return boolean - success or failure
|
|
*/
|
|
function role_unassign($roleid=0, $userid=0, $groupid=0, $contextid=0, $enrol=NULL) {
|
|
|
|
global $USER, $CFG, $DB;
|
|
require_once($CFG->dirroot.'/group/lib.php');
|
|
|
|
$success = true;
|
|
|
|
$args = array('roleid', 'userid', 'groupid', 'contextid');
|
|
$select = array();
|
|
$params = array();
|
|
|
|
foreach ($args as $arg) {
|
|
if ($$arg) {
|
|
$select[] = "$arg = ?";
|
|
$params[] = $$arg;
|
|
}
|
|
}
|
|
if (!empty($enrol)) {
|
|
$select[] = "enrol=?";
|
|
$params[] = $enrol;
|
|
}
|
|
|
|
if ($select) {
|
|
if ($ras = $DB->get_records_select('role_assignments', implode(' AND ', $select), $params)) {
|
|
$mods = get_list_of_plugins('mod');
|
|
foreach($ras as $ra) {
|
|
$fireevent = false;
|
|
/// infinite loop protection when deleting recursively
|
|
if (!$ra = $DB->get_record('role_assignments', array('id'=>$ra->id))) {
|
|
continue;
|
|
}
|
|
if ($DB->delete_records('role_assignments', array('id'=>$ra->id))) {
|
|
$fireevent = true;
|
|
} else {
|
|
$success = false;
|
|
}
|
|
|
|
if (!$context = get_context_instance_by_id($ra->contextid)) {
|
|
// strange error, not much to do
|
|
continue;
|
|
}
|
|
|
|
/* mark contexts as dirty here, because we need the refreshed
|
|
* caps bellow to delete group membership and user_lastaccess!
|
|
* and yes, this is very expensive for bulk operations :-(
|
|
*/
|
|
mark_context_dirty($context->path);
|
|
|
|
/// If the user is the current user, then do full reload of capabilities too.
|
|
if (!empty($USER->id) && $USER->id == $ra->userid) {
|
|
load_all_capabilities();
|
|
}
|
|
|
|
/// Ask all the modules if anything needs to be done for this user
|
|
foreach ($mods as $mod) {
|
|
include_once($CFG->dirroot.'/mod/'.$mod.'/lib.php');
|
|
$functionname = $mod.'_role_unassign';
|
|
if (function_exists($functionname)) {
|
|
$functionname($ra->userid, $context); // watch out, $context might be NULL if something goes wrong
|
|
}
|
|
}
|
|
|
|
/// now handle metacourse role unassigment and removing from goups if in course context
|
|
if ($context->contextlevel == CONTEXT_COURSE) {
|
|
|
|
// cleanup leftover course groups/subscriptions etc when user has
|
|
// no capability to view course
|
|
// this may be slow, but this is the proper way of doing it
|
|
if (!has_capability('moodle/course:view', $context, $ra->userid)) {
|
|
// remove from groups
|
|
groups_delete_group_members($context->instanceid, $ra->userid);
|
|
|
|
// delete lastaccess records
|
|
$DB->delete_records('user_lastaccess', array('userid'=>$ra->userid, 'courseid'=>$context->instanceid));
|
|
}
|
|
|
|
//unassign roles in metacourses if needed
|
|
if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
|
|
foreach ($parents as $parent) {
|
|
sync_metacourse($parent->parent_course);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($fireevent) {
|
|
events_trigger('role_unassigned', $ra);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* A convenience function to take care of the common case where you
|
|
* just want to enrol someone using the default role into a course
|
|
*
|
|
* @param object $course
|
|
* @param object $user
|
|
* @param string $enrol - the plugin used to do this enrolment
|
|
*/
|
|
function enrol_into_course($course, $user, $enrol) {
|
|
|
|
$timestart = time();
|
|
// remove time part from the timestamp and keep only the date part
|
|
$timestart = make_timestamp(date('Y', $timestart), date('m', $timestart), date('d', $timestart), 0, 0, 0);
|
|
if ($course->enrolperiod) {
|
|
$timeend = $timestart + $course->enrolperiod;
|
|
} else {
|
|
$timeend = 0;
|
|
}
|
|
|
|
if ($role = get_default_course_role($course)) {
|
|
|
|
$context = get_context_instance(CONTEXT_COURSE, $course->id);
|
|
|
|
if (!role_assign($role->id, $user->id, 0, $context->id, $timestart, $timeend, 0, $enrol)) {
|
|
return false;
|
|
}
|
|
|
|
// force accessdata refresh for users visiting this context...
|
|
mark_context_dirty($context->path);
|
|
|
|
email_welcome_message_to_user($course, $user);
|
|
|
|
add_to_log($course->id, 'course', 'enrol',
|
|
'view.php?id='.$course->id, $course->id);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Loads the capability definitions for the component (from file). If no
|
|
* capabilities are defined for the component, we simply return an empty array.
|
|
* @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
|
|
* @return array of capabilities
|
|
*/
|
|
function load_capability_def($component) {
|
|
global $CFG;
|
|
|
|
if ($component == 'moodle') {
|
|
$defpath = $CFG->libdir.'/db/access.php';
|
|
$varprefix = 'moodle';
|
|
} else {
|
|
$compparts = explode('/', $component);
|
|
|
|
if ($compparts[0] == 'report') {
|
|
$defpath = $CFG->dirroot.'/'.$CFG->admin.'/report/'.$compparts[1].'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else if ($compparts[0] == 'block') {
|
|
// Blocks are an exception. Blocks directory is 'blocks', and not
|
|
// 'block'. So we need to jump through hoops.
|
|
$defpath = $CFG->dirroot.'/'.$compparts[0].
|
|
's/'.$compparts[1].'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else if ($compparts[0] == 'format') {
|
|
// Similar to the above, course formats are 'format' while they
|
|
// are stored in 'course/format'.
|
|
$defpath = $CFG->dirroot.'/course/'.$component.'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else if ($compparts[0] == 'gradeimport') {
|
|
$defpath = $CFG->dirroot.'/grade/import/'.$compparts[1].'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else if ($compparts[0] == 'gradeexport') {
|
|
$defpath = $CFG->dirroot.'/grade/export/'.$compparts[1].'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else if ($compparts[0] == 'gradereport') {
|
|
$defpath = $CFG->dirroot.'/grade/report/'.$compparts[1].'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else if ($compparts[0] == 'quizreport') {
|
|
$defpath = $CFG->dirroot.'/mod/quiz/report/'.$compparts[1].'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else if ($compparts[0] == 'coursereport') {
|
|
$defpath = $CFG->dirroot.'/course/report/'.$compparts[1].'/db/access.php';
|
|
$varprefix = $compparts[0].'_'.$compparts[1];
|
|
|
|
} else {
|
|
$defpath = $CFG->dirroot.'/'.$component.'/db/access.php';
|
|
$varprefix = str_replace('/', '_', $component);
|
|
}
|
|
}
|
|
$capabilities = array();
|
|
|
|
if (file_exists($defpath)) {
|
|
require($defpath);
|
|
$capabilities = ${$varprefix.'_capabilities'};
|
|
}
|
|
return $capabilities;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the capabilities that have been cached in the database for this
|
|
* component.
|
|
* @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
|
|
* @return array of capabilities
|
|
*/
|
|
function get_cached_capabilities($component='moodle') {
|
|
global $DB;
|
|
|
|
if ($component == 'moodle') {
|
|
$storedcaps = $DB->get_records_select('capabilities', "name LIKE ?", array('moodle/%:%'));
|
|
|
|
} else if ($component == 'local') {
|
|
$storedcaps = $DB->get_records_select('capabilities', "name LIKE ?", array('moodle/local:%'));
|
|
|
|
} else {
|
|
$storedcaps = $DB->get_records_select('capabilities', "name LIKE ?", array("$component:%"));
|
|
}
|
|
|
|
return $storedcaps;
|
|
}
|
|
|
|
/**
|
|
* Returns default capabilities for given legacy role type.
|
|
*
|
|
* @param string legacy role name
|
|
* @return array
|
|
*/
|
|
function get_default_capabilities($legacyrole) {
|
|
global $DB;
|
|
$allcaps = $DB->get_records('capabilities');
|
|
$alldefs = array();
|
|
$defaults = array();
|
|
$components = array();
|
|
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) {
|
|
if (isset($def['legacy'][$legacyrole])) {
|
|
$defaults[$name] = $def['legacy'][$legacyrole];
|
|
}
|
|
}
|
|
|
|
//some exceptions
|
|
$defaults['moodle/legacy:'.$legacyrole] = CAP_ALLOW;
|
|
if ($legacyrole == 'admin') {
|
|
$defaults['moodle/site:doanything'] = CAP_ALLOW;
|
|
}
|
|
return $defaults;
|
|
}
|
|
|
|
/**
|
|
* Reset role capabilitites to default according to selected legacy capability.
|
|
* If several legacy caps selected, use the first from get_default_capabilities.
|
|
* If no legacy selected, removes all capabilities.
|
|
*
|
|
* @param int @roleid
|
|
*/
|
|
function reset_role_capabilities($roleid) {
|
|
global $DB;
|
|
|
|
$sitecontext = get_context_instance(CONTEXT_SYSTEM);
|
|
$legacyroles = get_legacy_roles();
|
|
|
|
$defaultcaps = array();
|
|
foreach($legacyroles as $ltype=>$lcap) {
|
|
$localoverride = get_local_override($roleid, $sitecontext->id, $lcap);
|
|
if (!empty($localoverride->permission) and $localoverride->permission == CAP_ALLOW) {
|
|
//choose first selected legacy capability
|
|
$defaultcaps = get_default_capabilities($ltype);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$DB->delete_records('role_capabilities', array('roleid'=>$roleid));
|
|
if (!empty($defaultcaps)) {
|
|
foreach($defaultcaps as $cap=>$permission) {
|
|
assign_capability($cap, $permission, $roleid, $sitecontext->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.
|
|
*
|
|
* @param $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;
|
|
|
|
$storedcaps = array();
|
|
|
|
$filecaps = load_capability_def($component);
|
|
$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->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
|
|
$updatecap = new object();
|
|
$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 object();
|
|
$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.
|
|
foreach ($newcaps as $capname => $capdef) {
|
|
$capability = new object;
|
|
$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'], $storedcaps)){
|
|
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)){
|
|
notify('Could not clone capabilities for '.$capname);
|
|
}
|
|
}
|
|
}
|
|
// Do we need to assign the new capabilities to roles that have the
|
|
// legacy capabilities moodle/legacy:* as well?
|
|
// we ignore legacy key if we have cloned permissions
|
|
} else if (isset($capdef['legacy']) && is_array($capdef['legacy']) &&
|
|
!assign_legacy_capabilities($capname, $capdef['legacy'])) {
|
|
notify('Could not assign legacy capabilities for '.$capname);
|
|
}
|
|
}
|
|
// 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
|
|
is_valid_capability('reset', false);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes cached capabilities that are no longer needed by the component.
|
|
* Also unassigns these capabilities from any roles that have them.
|
|
* @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
|
|
* @param $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.
|
|
if (!$DB->delete_records('capabilities', array('name'=>$cachedcap->name))) {
|
|
print_error('cannotdeletecap', '', '', $cachedcap->name);
|
|
} else {
|
|
$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;
|
|
}
|
|
|
|
|
|
|
|
/****************
|
|
* UI FUNCTIONS *
|
|
****************/
|
|
|
|
/**
|
|
* @param integer $contextlevel $context->context level. One of the CONTEXT_... constants.
|
|
* @return string the name for this type of context.
|
|
*/
|
|
function get_contextlevel_name($contextlevel) {
|
|
static $strcontextlevels = null;
|
|
if (is_null($strcontextlevels)) {
|
|
$strcontextlevels = array(
|
|
CONTEXT_SYSTEM => get_string('coresystem'),
|
|
CONTEXT_USER => get_string('user'),
|
|
CONTEXT_COURSECAT => get_string('category'),
|
|
CONTEXT_COURSE => get_string('course'),
|
|
CONTEXT_MODULE => get_string('activitymodule'),
|
|
CONTEXT_BLOCK => get_string('block')
|
|
);
|
|
}
|
|
return $strcontextlevels[$contextlevel];
|
|
}
|
|
|
|
/**
|
|
* Prints human readable context identifier.
|
|
*
|
|
* @param object $context the context.
|
|
* @param boolean $withprefix whether to prefix the name of the context with the
|
|
* type of context, e.g. User, Course, Forum, etc.
|
|
* @param boolean $short whether to user the short name of the thing. Only applies
|
|
* to course contexts
|
|
* @return string the human readable context name.
|
|
*/
|
|
function print_context_name($context, $withprefix = true, $short = false) {
|
|
global $DB;
|
|
|
|
$name = '';
|
|
switch ($context->contextlevel) {
|
|
|
|
case CONTEXT_SYSTEM: // by now it's a definite an inherit
|
|
$name = get_string('coresystem');
|
|
break;
|
|
|
|
case CONTEXT_USER:
|
|
if ($user = $DB->get_record('user', array('id'=>$context->instanceid))) {
|
|
if ($withprefix){
|
|
$name = get_string('user').': ';
|
|
}
|
|
$name .= fullname($user);
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_COURSECAT: // Coursecat -> coursecat or site
|
|
if ($category = $DB->get_record('course_categories', array('id'=>$context->instanceid))) {
|
|
if ($withprefix){
|
|
$name = get_string('category').': ';
|
|
}
|
|
$name .=format_string($category->name);
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_COURSE: // 1 to 1 to course cat
|
|
if ($context->instanceid == SITEID) {
|
|
$name = get_string('frontpage', 'admin');
|
|
} else {
|
|
if ($course = $DB->get_record('course', array('id'=>$context->instanceid))) {
|
|
if ($withprefix){
|
|
$name = get_string('course').': ';
|
|
}
|
|
if ($short){
|
|
$name .= format_string($course->shortname);
|
|
} else {
|
|
$name .= format_string($course->fullname);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_GROUP: // 1 to 1 to course
|
|
if ($name = groups_get_group_name($context->instanceid)) {
|
|
if ($withprefix){
|
|
$name = get_string('group').': '. $name;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_MODULE: // 1 to 1 to course
|
|
if ($cm = $DB->get_record_sql('SELECT cm.*, md.name AS modname FROM {course_modules} cm ' .
|
|
'JOIN {modules} md ON md.id = cm.module WHERE cm.id = ?', array($context->instanceid))) {
|
|
if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) {
|
|
if ($withprefix){
|
|
$name = get_string('modulename', $cm->modname).': ';
|
|
}
|
|
$name .= $mod->name;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_BLOCK: // not necessarily 1 to 1 to course
|
|
if ($blockinstance = $DB->get_record('block_instance', array('id'=>$context->instanceid))) {
|
|
if ($block = $DB->get_record('block', array('id'=>$blockinstance->blockid))) {
|
|
global $CFG;
|
|
require_once("$CFG->dirroot/blocks/moodleblock.class.php");
|
|
require_once("$CFG->dirroot/blocks/$block->name/block_$block->name.php");
|
|
$blockname = "block_$block->name";
|
|
if ($blockobject = new $blockname()) {
|
|
if ($withprefix){
|
|
$name = get_string('block').': ';
|
|
}
|
|
$name .= $blockobject->title;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
print_error('unknowncontext');
|
|
return false;
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* Get a URL for a context, if there is a natural one. For example, for
|
|
* CONTEXT_COURSE, this is the course page. For CONTEXT_USER it is the
|
|
* user profile page.
|
|
*
|
|
* First three parameters as for
|
|
* @param object $context the context.
|
|
* @return string a suitable URL, or blank.
|
|
*/
|
|
function get_context_url($context) {
|
|
global $CFG, $COURSE, $DB;
|
|
|
|
$url = '';
|
|
switch ($context->contextlevel) {
|
|
case CONTEXT_USER:
|
|
$url = $CFG->wwwroot . '/user/view.php?id=' . $context->instanceid;
|
|
if ($COURSE->id != SITEID) {
|
|
$url .= '&courseid=' . $COURSE->id;
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_COURSECAT: // Coursecat -> coursecat or site
|
|
$url = $CFG->wwwroot . '/course/category.php?id=' . $context->instanceid;
|
|
break;
|
|
|
|
case CONTEXT_COURSE: // 1 to 1 to course cat
|
|
if ($context->instanceid == SITEID) {
|
|
$url = $CFG->wwwroot . '/';
|
|
} else {
|
|
$url = $CFG->wwwroot . '/course/view.php?id=' . $context->instanceid;
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_MODULE: // 1 to 1 to course
|
|
if ($modname = $DB->get_field_sql('SELECT md.name AS modname FROM {course_modules} cm ' .
|
|
'JOIN {modules} md ON md.id = cm.module WHERE cm.id = ?', array($context->instanceid))) {
|
|
$url = $CFG->wwwroot . '/mod/' . $modname . '/view.php?id=' . $context->instanceid;
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_SYSTEM:
|
|
case CONTEXT_GROUP:
|
|
case CONTEXT_BLOCK:
|
|
default:
|
|
$url = '';
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* @return array 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.
|
|
*/
|
|
function get_all_risks() {
|
|
return array(
|
|
'riskmanagetrust' => RISK_MANAGETRUST,
|
|
'riskconfig' => RISK_CONFIG,
|
|
'riskxss' => RISK_XSS,
|
|
'riskpersonal' => RISK_PERSONAL,
|
|
'riskspam' => RISK_SPAM,
|
|
'riskdataloss' => RISK_DATALOSS,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param object $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) {
|
|
global $CFG;
|
|
$url = get_docs_url('Capabilities/' . $capability->name);
|
|
return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
|
|
}
|
|
|
|
/**
|
|
* Extracts the relevant capabilities given a contextid.
|
|
* All case based, example an instance of forum context.
|
|
* Will fetch all forum related capabilities, while course contexts
|
|
* Will fetch all capabilities
|
|
* @param object context
|
|
* @return array();
|
|
*
|
|
* capabilities
|
|
* `name` varchar(150) NOT NULL,
|
|
* `captype` varchar(50) NOT NULL,
|
|
* `contextlevel` int(10) NOT NULL,
|
|
* `component` varchar(100) NOT NULL,
|
|
*/
|
|
function fetch_context_capabilities($context) {
|
|
global $DB, $CFG;
|
|
|
|
$sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
|
|
|
|
$params = array();
|
|
|
|
switch ($context->contextlevel) {
|
|
|
|
case CONTEXT_SYSTEM: // all
|
|
$SQL = "SELECT *
|
|
FROM {capabilities}";
|
|
break;
|
|
|
|
case CONTEXT_USER:
|
|
$extracaps = array('moodle/grade:viewall');
|
|
list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap0');
|
|
$SQL = "SELECT *
|
|
FROM {capabilities}
|
|
WHERE contextlevel = ".CONTEXT_USER."
|
|
OR name $extra";
|
|
break;
|
|
|
|
case CONTEXT_COURSECAT: // course category context and bellow
|
|
$SQL = "SELECT *
|
|
FROM {capabilities}
|
|
WHERE contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
|
|
break;
|
|
|
|
case CONTEXT_COURSE: // course context and bellow
|
|
$SQL = "SELECT *
|
|
FROM {capabilities}
|
|
WHERE contextlevel IN (".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
|
|
break;
|
|
|
|
case CONTEXT_MODULE: // mod caps
|
|
$cm = $DB->get_record('course_modules', array('id'=>$context->instanceid));
|
|
$module = $DB->get_record('modules', array('id'=>$cm->module));
|
|
|
|
$extra = "";
|
|
$modfile = "$CFG->dirroot/mod/$module->name/lib.php";
|
|
if (file_exists($modfile)) {
|
|
include_once($modfile);
|
|
$modfunction = $module->name.'_get_extra_capabilities';
|
|
if (function_exists($modfunction)) {
|
|
if ($extracaps = $modfunction()) {
|
|
list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap0');
|
|
$extra = "OR name $extra";
|
|
}
|
|
}
|
|
}
|
|
|
|
$SQL = "SELECT *
|
|
FROM {capabilities}
|
|
WHERE (contextlevel = ".CONTEXT_MODULE."
|
|
AND component = :component)
|
|
$extra";
|
|
$params['component'] = "mod/$module->name";
|
|
break;
|
|
|
|
case CONTEXT_BLOCK: // block caps
|
|
$cb = $DB->get_record('block_instance', array('id'=>$context->instanceid));
|
|
$block = $DB->get_record('block', array('id'=>$cb->blockid));
|
|
|
|
$extra = "";
|
|
if ($blockinstance = block_instance($block->name)) {
|
|
if ($extracaps = $blockinstance->get_extra_capabilities()) {
|
|
list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap0');
|
|
$extra = "OR name $extra";
|
|
}
|
|
}
|
|
|
|
$SQL = "SELECT *
|
|
FROM {capabilities}
|
|
WHERE (contextlevel = ".CONTEXT_BLOCK."
|
|
AND component = :component)
|
|
$extra";
|
|
$params['component'] = "block/$block->name";
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (!$records = $DB->get_records_sql($SQL.' '.$sort, $params)) {
|
|
$records = array();
|
|
}
|
|
|
|
return $records;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 obj $context
|
|
* @param int $roleid
|
|
* @param bool self - if set to true, resolve till this level, else stop at immediate parent level
|
|
* @return array
|
|
*/
|
|
function role_context_capabilities($roleid, $context, $cap='') {
|
|
global $DB;
|
|
|
|
$contexts = get_parent_contexts($context);
|
|
$contexts[] = $context->id;
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Recursive function which, given a context, find all parent context ids,
|
|
* and return the array in reverse order, i.e. parent first, then grand
|
|
* parent, etc.
|
|
*
|
|
* @param object $context
|
|
* @return array()
|
|
*/
|
|
function get_parent_contexts($context, $includeself = false) {
|
|
|
|
if ($context->path == '') {
|
|
return array();
|
|
}
|
|
|
|
$parentcontexts = substr($context->path, 1); // kill leading slash
|
|
$parentcontexts = explode('/', $parentcontexts);
|
|
if (!$includeself) {
|
|
array_pop($parentcontexts); // and remove its own id
|
|
}
|
|
|
|
return array_reverse($parentcontexts);
|
|
}
|
|
|
|
/**
|
|
* Return the id of the parent of this context, or false if there is no parent (only happens if this
|
|
* is the site context.)
|
|
*
|
|
* @param object $context
|
|
* @return integer the id of the parent context.
|
|
*/
|
|
function get_parent_contextid($context) {
|
|
$parentcontexts = get_parent_contexts($context);
|
|
if (count($parentcontexts) == 0) {
|
|
return false;
|
|
}
|
|
return array_shift($parentcontexts);
|
|
}
|
|
|
|
/**
|
|
* @param object $context a context object.
|
|
* @return true if this context is the front page context, or a context inside it,
|
|
* otherwise false.
|
|
*/
|
|
function is_inside_frontpage($context) {
|
|
$frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
|
|
return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
|
|
}
|
|
|
|
/**
|
|
* Does get_records_select on the context table, and returns the results ordered
|
|
* by contextlevel, and then the natural sort order within each level.
|
|
* for the purpose of $select, you need to know that the context table has been
|
|
* aliased to ctx, so for example, you can call get_sorted_contexts('ctx.depth = 3');
|
|
*
|
|
* @param string $select the contents of the WHERE clause. Remember to do ctx.fieldname.
|
|
* @param array $params any parameters required by $select.
|
|
* @return array the requested context records.
|
|
*/
|
|
function get_sorted_contexts($select, $params = array()) {
|
|
global $DB;
|
|
if ($select) {
|
|
$select = 'WHERE ' . $select;
|
|
}
|
|
return $DB->get_records_sql("
|
|
SELECT ctx.*
|
|
FROM {context} ctx
|
|
LEFT JOIN {user} u ON ctx.contextlevel = 30 AND u.id = ctx.instanceid
|
|
LEFT JOIN {course_categories} cat ON ctx.contextlevel = 40 AND cat.id = ctx.instanceid
|
|
LEFT JOIN {course} c ON ctx.contextlevel = 50 AND c.id = ctx.instanceid
|
|
LEFT JOIN {course_modules} cm ON ctx.contextlevel = 70 AND cm.id = ctx.instanceid
|
|
LEFT JOIN {block_instance} bi ON ctx.contextlevel = 80 AND bi.id = ctx.instanceid
|
|
$select
|
|
ORDER BY ctx.contextlevel, bi.position, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.weight), u.lastname, u.firstname, cm.id
|
|
", $params);
|
|
}
|
|
|
|
/**
|
|
* Recursive function which, given a context, find all its children context ids.
|
|
*
|
|
* When called for a course context, it will return the modules and blocks
|
|
* displayed in the course page.
|
|
*
|
|
* For course category contexts it will return categories and courses. It will
|
|
* NOT recurse into courses - if you want to do that, call it on the returned
|
|
* courses.
|
|
*
|
|
* If called on a course context it _will_ populate the cache with the appropriate
|
|
* contexts ;-)
|
|
*
|
|
* @param object $context.
|
|
* @return array of child records
|
|
*/
|
|
function get_child_contexts($context) {
|
|
|
|
global $DB, $ACCESSLIB_PRIVATE;
|
|
|
|
// We *MUST* populate the context_cache as the callers
|
|
// will probably ask for the full record anyway soon after
|
|
// soon after calling us ;-)
|
|
|
|
switch ($context->contextlevel) {
|
|
|
|
case CONTEXT_BLOCK:
|
|
// No children.
|
|
return array();
|
|
break;
|
|
|
|
case CONTEXT_MODULE:
|
|
// No children.
|
|
return array();
|
|
break;
|
|
|
|
case CONTEXT_GROUP:
|
|
// No children.
|
|
return array();
|
|
break;
|
|
|
|
case CONTEXT_COURSE:
|
|
// Find
|
|
// - module instances - easy
|
|
// - groups
|
|
// - blocks assigned to the course-view page explicitly - easy
|
|
// - blocks pinned (note! we get all of them here, regardless of vis)
|
|
$sql = " SELECT ctx.*
|
|
FROM {context} ctx
|
|
WHERE ctx.path LIKE ?
|
|
AND ctx.contextlevel IN (".CONTEXT_MODULE.",".CONTEXT_BLOCK.")
|
|
UNION
|
|
SELECT ctx.*
|
|
FROM {context} ctx
|
|
JOIN {groups} g ON (ctx.instanceid=g.id AND ctx.contextlevel=".CONTEXT_GROUP.")
|
|
WHERE g.courseid=?
|
|
UNION
|
|
SELECT ctx.*
|
|
FROM {context} ctx
|
|
JOIN {block_pinned} b ON (ctx.instanceid=b.blockid AND ctx.contextlevel=".CONTEXT_BLOCK.")
|
|
WHERE b.pagetype='course-view'";
|
|
$params = array("{$context->path}/%", $context->instanceid);
|
|
$records = $DB->get_recordset_sql($sql, $params);
|
|
foreach ($records as $rec) {
|
|
cache_context($rec);
|
|
}
|
|
return $records;
|
|
break;
|
|
|
|
case CONTEXT_COURSECAT:
|
|
// Find
|
|
// - categories
|
|
// - courses
|
|
$sql = " SELECT ctx.*
|
|
FROM {context} ctx
|
|
WHERE ctx.path LIKE ?
|
|
AND ctx.contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.")";
|
|
$params = array("{$context->path}/%");
|
|
$records = $DB->get_recordset_sql($sql, $params);
|
|
foreach ($records as $rec) {
|
|
cache_context($rec);
|
|
}
|
|
return $records;
|
|
break;
|
|
|
|
case CONTEXT_USER:
|
|
// No children.
|
|
return array();
|
|
break;
|
|
|
|
case CONTEXT_SYSTEM:
|
|
// Just get all the contexts except for CONTEXT_SYSTEM level
|
|
// and hope we don't OOM in the process - don't cache
|
|
$sql = "SELECT c.*
|
|
FROM {context} c
|
|
WHERE contextlevel != ".CONTEXT_SYSTEM;
|
|
|
|
return $DB->get_records_sql($sql);
|
|
break;
|
|
|
|
default:
|
|
print_error('unknowcontext', '', '', $context->contextlevel);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets a string for sql calls, searching for stuff in this context or above
|
|
* @param object $context
|
|
* @return string
|
|
*/
|
|
function get_related_contexts_string($context) {
|
|
if ($parents = get_parent_contexts($context)) {
|
|
return (' IN ('.$context->id.','.implode(',', $parents).')');
|
|
} else {
|
|
return (' ='.$context->id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies if given capability installed.
|
|
*
|
|
* @param string $capabilityname
|
|
* @param bool $cached
|
|
* @return book true if capability exists
|
|
*/
|
|
function is_valid_capability($capabilityname, $cached = true) {
|
|
global $ACCESSLIB_PRIVATE; // one request per page only
|
|
|
|
if (is_null($ACCESSLIB_PRIVATE->capabilitynames) or !$cached) {
|
|
global $DB;
|
|
$ACCESSLIB_PRIVATE->capabilitynames = $DB->get_records_menu('capabilities', null, '', 'name, 1');
|
|
}
|
|
|
|
return array_key_exists($capabilityname, $ACCESSLIB_PRIVATE->capabilitynames);
|
|
}
|
|
|
|
/**
|
|
* Returns the human-readable, translated version of the capability.
|
|
* Basically a big switch statement.
|
|
* @param $capabilityname - e.g. mod/choice:readresponses
|
|
*/
|
|
function get_capability_string($capabilityname) {
|
|
|
|
// Typical capabilityname is mod/choice:readresponses
|
|
|
|
$names = split('/', $capabilityname);
|
|
$stringname = $names[1]; // choice:readresponses
|
|
$components = split(':', $stringname);
|
|
$componentname = $components[0]; // choice
|
|
|
|
switch ($names[0]) {
|
|
case 'report':
|
|
$string = get_string($stringname, 'report_'.$componentname);
|
|
break;
|
|
|
|
case 'mod':
|
|
$string = get_string($stringname, $componentname);
|
|
break;
|
|
|
|
case 'block':
|
|
$string = get_string($stringname, 'block_'.$componentname);
|
|
break;
|
|
|
|
case 'moodle':
|
|
if ($componentname == 'local') {
|
|
$string = get_string($stringname, 'local');
|
|
} else {
|
|
$string = get_string($stringname, 'role');
|
|
}
|
|
break;
|
|
|
|
case 'enrol':
|
|
$string = get_string($stringname, 'enrol_'.$componentname);
|
|
break;
|
|
|
|
case 'format':
|
|
$string = get_string($stringname, 'format_'.$componentname);
|
|
break;
|
|
|
|
case 'gradeexport':
|
|
$string = get_string($stringname, 'gradeexport_'.$componentname);
|
|
break;
|
|
|
|
case 'gradeimport':
|
|
$string = get_string($stringname, 'gradeimport_'.$componentname);
|
|
break;
|
|
|
|
case 'gradereport':
|
|
$string = get_string($stringname, 'gradereport_'.$componentname);
|
|
break;
|
|
|
|
case 'coursereport':
|
|
$string = get_string($stringname, 'coursereport_'.$componentname);
|
|
break;
|
|
|
|
case 'quizreport':
|
|
$string = get_string($stringname, 'quiz_'.$componentname);
|
|
break;
|
|
|
|
default:
|
|
$string = get_string($stringname);
|
|
break;
|
|
|
|
}
|
|
return $string;
|
|
}
|
|
|
|
|
|
/**
|
|
* This gets the mod/block/course/core etc strings.
|
|
* @param $component
|
|
* @param $contextlevel
|
|
*/
|
|
function get_component_string($component, $contextlevel) {
|
|
|
|
switch ($contextlevel) {
|
|
|
|
case CONTEXT_SYSTEM:
|
|
if (preg_match('|^enrol/|', $component)) {
|
|
$langname = str_replace('/', '_', $component);
|
|
$string = get_string('enrolname', $langname);
|
|
} else if (preg_match('|^block/|', $component)) {
|
|
$langname = str_replace('/', '_', $component);
|
|
$string = get_string('blockname', $langname);
|
|
} else if (preg_match('|^local|', $component)) {
|
|
$langname = str_replace('/', '_', $component);
|
|
$string = get_string('local');
|
|
} else if (preg_match('|^report/|', $component)) {
|
|
$string = get_string('reports');
|
|
} else {
|
|
$string = get_string('coresystem');
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_USER:
|
|
$string = get_string('users');
|
|
break;
|
|
|
|
case CONTEXT_COURSECAT:
|
|
$string = get_string('categories');
|
|
break;
|
|
|
|
case CONTEXT_COURSE:
|
|
if (preg_match('|^gradeimport/|', $component)
|
|
|| preg_match('|^gradeexport/|', $component)
|
|
|| preg_match('|^gradereport/|', $component)) {
|
|
$string = get_string('gradebook', 'admin');
|
|
} else if (preg_match('|^coursereport/|', $component)) {
|
|
$string = get_string('coursereports');
|
|
} else {
|
|
$string = get_string('course');
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_GROUP:
|
|
$string = get_string('group');
|
|
break;
|
|
|
|
case CONTEXT_MODULE:
|
|
if (preg_match('|^quizreport/([a-z_]*)|', $component, $matches)){
|
|
$langname = 'quiz_'.$matches[1];
|
|
$string = get_string($matches[1].':componentname', $langname);
|
|
} else {
|
|
$string = get_string('modulename', basename($component));
|
|
}
|
|
break;
|
|
|
|
case CONTEXT_BLOCK:
|
|
if( $component == 'moodle' ){
|
|
$string = get_string('block');
|
|
}else{
|
|
$string = get_string('blockname', 'block_'.basename($component));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
print_error('unknowncontext');
|
|
return false;
|
|
|
|
}
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* Gets the list of roles assigned to this context and up (parents)
|
|
* @param object $context
|
|
* @param view - set to true when roles are pulled for display only
|
|
* this is so that we can filter roles with no visible
|
|
* assignment, for example, you might want to "hide" all
|
|
* course creators when browsing the course participants
|
|
* list.
|
|
* @return array
|
|
*/
|
|
function get_roles_used_in_context($context, $view = false) {
|
|
global $DB;
|
|
|
|
// filter for roles with all hidden assignments
|
|
// no need to return when only pulling roles for reviewing
|
|
// e.g. participants page.
|
|
$hiddensql = ($view && !has_capability('moodle/role:viewhiddenassigns', $context))? ' AND ra.hidden = 0 ':'';
|
|
$contextlist = get_related_contexts_string($context);
|
|
|
|
$sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder
|
|
FROM {role_assignments} ra, {role} r
|
|
WHERE r.id = ra.roleid
|
|
AND ra.contextid $contextlist
|
|
$hiddensql
|
|
ORDER BY r.sortorder ASC";
|
|
|
|
return $DB->get_records_sql($sql);
|
|
}
|
|
|
|
/**
|
|
* This function is used to print roles column in user profile page.
|
|
* @param int userid
|
|
* @param object context
|
|
* @return string
|
|
*/
|
|
function get_user_roles_in_context($userid, $context, $view=true){
|
|
global $CFG, $DB,$USER;
|
|
|
|
$rolestring = '';
|
|
$sql = "SELECT *
|
|
FROM {role_assignments} ra, {role} r
|
|
WHERE ra.userid = ? and ra.contextid = ? and ra.roleid = r.id";
|
|
$params = array($userid, $context->id);
|
|
$rolenames = array();
|
|
if ($roles = $DB->get_records_sql($sql, $params)) {
|
|
foreach ($roles as $userrole) {
|
|
// MDL-12544, if we are in view mode and current user has no capability to view hidden assignment, skip it
|
|
if ($userrole->hidden && $view && !has_capability('moodle/role:viewhiddenassigns', $context)) {
|
|
continue;
|
|
}
|
|
$rolenames[$userrole->roleid] = $userrole->name;
|
|
}
|
|
|
|
$rolenames = role_fix_names($rolenames, $context); // Substitute aliases
|
|
|
|
foreach ($rolenames as $roleid => $rolename) {
|
|
$rolenames[$roleid] = '<a href="'.$CFG->wwwroot.'/user/index.php?contextid='.$context->id.'&roleid='.$roleid.'">'.$rolename.'</a>';
|
|
}
|
|
$rolestring = implode(',', $rolenames);
|
|
}
|
|
return $rolestring;
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if a user can override capabilities of a particular role in this context
|
|
* @param object $context
|
|
* @param int targetroleid - the id of the role you want to override
|
|
* @return boolean
|
|
*/
|
|
function user_can_override($context, $targetroleid) {
|
|
|
|
// TODO: not needed anymore, remove in 2.0
|
|
|
|
global $DB;
|
|
// first check if user has override capability
|
|
// if not return false;
|
|
if (!has_capability('moodle/role:override', $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_override', array('roleid'=>$userrole->roleid, 'allowoverride'=>$targetroleid))) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
/**
|
|
* Checks if a user can assign users to a particular role in this context
|
|
* @param object $context
|
|
* @param int targetroleid - the id of the role you want to assign users to
|
|
* @return boolean
|
|
*/
|
|
function user_can_assign($context, $targetroleid) {
|
|
global $DB;
|
|
|
|
// first 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.
|
|
* @return array
|
|
*/
|
|
function get_all_roles() {
|
|
global $DB;
|
|
return $DB->get_records('role', null, '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 object $context
|
|
* @param int $userid
|
|
* @param view - set to true when roles are pulled for display only
|
|
* this is so that we can filter roles with no visible
|
|
* assignment, for example, you might want to "hide" all
|
|
* course creators when browsing the course participants
|
|
* list.
|
|
* @return array
|
|
*/
|
|
function get_user_roles($context, $userid=0, $checkparentcontexts=true, $order='c.contextlevel DESC, r.sortorder ASC', $view=false) {
|
|
global $USER, $DB;
|
|
|
|
if (empty($userid)) {
|
|
if (empty($USER->id)) {
|
|
return array();
|
|
}
|
|
$userid = $USER->id;
|
|
}
|
|
// set up hidden sql
|
|
$hiddensql = ($view && !has_capability('moodle/role:viewhiddenassigns', $context)) ? "AND ra.hidden = 0" : "";
|
|
|
|
if ($checkparentcontexts) {
|
|
$contextids = get_parent_contexts($context);
|
|
} 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
|
|
$hiddensql
|
|
ORDER BY $order";
|
|
|
|
return $DB->get_records_sql($sql ,$params);
|
|
}
|
|
|
|
/**
|
|
* Creates a record in the allow_override table
|
|
* @param int sroleid - source roleid
|
|
* @param int troleid - target roleid
|
|
* @return int - id or false
|
|
*/
|
|
function allow_override($sroleid, $troleid) {
|
|
global $DB;
|
|
|
|
$record = new object();
|
|
$record->roleid = $sroleid;
|
|
$record->allowoverride = $troleid;
|
|
return $DB->insert_record('role_allow_override', $record);
|
|
}
|
|
|
|
/**
|
|
* Creates a record in the allow_assign table
|
|
* @param int sroleid - source roleid
|
|
* @param int troleid - target roleid
|
|
* @return int - id or false
|
|
*/
|
|
function allow_assign($fromroleid, $targetroleid) {
|
|
global $DB;
|
|
|
|
$record = new object;
|
|
$record->roleid = $fromroleid;
|
|
$record->allowassign = $targetroleid;
|
|
return $DB->insert_record('role_allow_assign', $record);
|
|
}
|
|
|
|
/**
|
|
* Gets a list of roles that this user can assign in this context
|
|
* @param object $context the context.
|
|
* @param int $rolenamedisplay the type of role name to display. One of the
|
|
* ROLENAME_X constants. Default ROLENAME_ALIAS.
|
|
* @param $withusercounts if true, count the number of users with each role.
|
|
* @return array if $withusercounts is false, then an array $roleid => $rolename.
|
|
* if $withusercounts is true, returns a list of three arrays,
|
|
* $rolenames, $rolecounts, and $nameswithcounts.
|
|
*/
|
|
function get_assignable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false) {
|
|
global $USER, $DB;
|
|
|
|
if (!has_capability('moodle/role:assign', $context)) {
|
|
if ($withusercounts) {
|
|
return array(array(), array(), array());
|
|
} else {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
$parents = get_parent_contexts($context);
|
|
$parents[] = $context->id;
|
|
$contexts = implode(',' , $parents);
|
|
|
|
$params = array();
|
|
$extrafields = '';
|
|
if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
|
|
$extrafields .= ', ro.shortname';
|
|
}
|
|
|
|
if ($withusercounts) {
|
|
$extrafields = ', (SELECT count(u.id)
|
|
FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
|
|
WHERE cra.roleid = ro.id AND cra.contextid = :conid AND u.deleted = 0
|
|
) AS usercount';
|
|
$params['conid'] = $context->id;
|
|
}
|
|
|
|
$raafrom = ", {role_allow_assign} raa";
|
|
$raawhere = "AND raa.roleid = ra.roleid AND r.id = raa.allowassign";
|
|
if (has_capability('moodle/site:doanything', get_context_instance(CONTEXT_SYSTEM))) {
|
|
// show all roles allowed in this context to admins
|
|
$raafrom = "";
|
|
$raawhere = "";
|
|
}
|
|
|
|
$params['userid'] = $USER->id;
|
|
$params['contextlevel'] = $context->contextlevel;
|
|
$roles = $DB->get_records_sql("
|
|
SELECT ro.id, ro.name$extrafields
|
|
FROM {role} ro
|
|
JOIN (SELECT DISTINCT r.id
|
|
FROM {role} r,
|
|
{role_assignments} ra $raafrom
|
|
WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
|
|
$raawhere
|
|
) inline_view ON ro.id = inline_view.id
|
|
JOIN {role_context_levels} rcl ON ro.id = rcl.roleid
|
|
WHERE rcl.contextlevel = :contextlevel
|
|
ORDER BY ro.sortorder ASC", $params);
|
|
|
|
$rolenames = array();
|
|
foreach ($roles as $role) {
|
|
$rolenames[$role->id] = $role->name;
|
|
if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
|
|
$rolenames[$role->id] .= ' (' . $role->shortname . ')';
|
|
}
|
|
}
|
|
if ($rolenamedisplay != ROLENAME_ORIGINALANDSHORT) {
|
|
$rolenames = role_fix_names($rolenames, $context, $rolenamedisplay);
|
|
}
|
|
|
|
if (!$withusercounts) {
|
|
return $rolenames;
|
|
}
|
|
|
|
$rolecounts = array();
|
|
$nameswithcounts = array();
|
|
foreach ($roles as $role) {
|
|
$nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
|
|
$rolecounts[$role->id] = $roles[$role->id]->usercount;
|
|
}
|
|
return array($rolenames, $rolecounts, $nameswithcounts);
|
|
}
|
|
|
|
/**
|
|
* Gets a list of roles that this user can assign in this context, for the switchrole menu
|
|
*
|
|
* @param object $context the context.
|
|
* @return an array $roleid => $rolename.
|
|
*/
|
|
function get_assignable_roles_for_switchrole($context) {
|
|
global $USER, $DB;
|
|
|
|
if (!has_capability('moodle/role:assign', $context)) {
|
|
return array();
|
|
}
|
|
|
|
$parents = get_parent_contexts($context);
|
|
$parents[] = $context->id;
|
|
$contexts = implode(',' , $parents);
|
|
|
|
$raafrom = "{role_allow_assign} raa,";
|
|
$raawhere = "AND raa.roleid = ra.roleid AND r.id = raa.allowassign";
|
|
if (has_capability('moodle/site:doanything', get_context_instance(CONTEXT_SYSTEM))) {
|
|
// show all roles allowed in this context to admins
|
|
$raafrom = "";
|
|
$raawhere = "";
|
|
}
|
|
|
|
if (!$roles = $DB->get_records_sql("
|
|
SELECT ro.*
|
|
FROM {role} ro
|
|
JOIN (SELECT DISTINCT r.id
|
|
FROM {role} r,
|
|
{role_assignments} ra,
|
|
$raafrom
|
|
{role_capabilities} rc
|
|
WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
|
|
$raawhere
|
|
AND r.id = rc.roleid AND rc.capability = :viewcap AND rc.capability <> :anythingcap
|
|
) inline_view ON ro.id = inline_view.id
|
|
ORDER BY ro.sortorder ASC",
|
|
array('userid'=>$USER->id, 'viewcap'=>'moodle/course:view',
|
|
'anythingcap'=>'moodle/site:doanything'))) {
|
|
return array();
|
|
}
|
|
|
|
$rolenames = array();
|
|
foreach ($roles as $role) {
|
|
$rolenames[$role->id] = $role->name;
|
|
}
|
|
return role_fix_names($rolenames, $context, ROLENAME_ALIAS);
|
|
}
|
|
|
|
/**
|
|
* Gets a list of roles that this user can override in this context.
|
|
*
|
|
* @param object $context the context.
|
|
* @param int $rolenamedisplay the type of role name to display. One of the
|
|
* ROLENAME_X constants. Default ROLENAME_ALIAS.
|
|
* @param $withcounts if true, count the number of overrides that are set for each role.
|
|
* @return array if $withcounts is false, then an array $roleid => $rolename.
|
|
* if $withusercounts is true, returns a list of three arrays,
|
|
* $rolenames, $rolecounts, and $nameswithcounts.
|
|
*/
|
|
function get_overridable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
|
|
global $USER, $DB;
|
|
|
|
if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
|
|
if ($withcounts) {
|
|
return array(array(), array(), array());
|
|
} else {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
$parents = get_parent_contexts($context);
|
|
$parents[] = $context->id;
|
|
$contexts = implode(',' , $parents);
|
|
|
|
$params = array();
|
|
$extrafields = '';
|
|
if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
|
|
$extrafields .= ', ro.shortname';
|
|
}
|
|
|
|
$params['userid'] = $USER->id;
|
|
if ($withcounts) {
|
|
$extrafields = ', (SELECT count(rc.id) FROM {role_capabilities} rc
|
|
WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
|
|
$params['conid'] = $context->id;
|
|
}
|
|
|
|
if (has_capability('moodle/site:doanything', get_context_instance(CONTEXT_SYSTEM))) {
|
|
// show all roles to admins
|
|
$roles = $DB->get_records_sql("
|
|
SELECT ro.id, ro.name$extrafields
|
|
FROM {role} ro
|
|
ORDER BY ro.sortorder ASC", $params);
|
|
|
|
} else {
|
|
$roles = $DB->get_records_sql("
|
|
SELECT ro.id, ro.name$extrafields
|
|
FROM {role} ro
|
|
JOIN (SELECT DISTINCT r.id
|
|
FROM {role} r
|
|
JOIN {role_allow_override} rao ON r.id = rao.allowoverride
|
|
JOIN {role_assignments} ra ON rao.roleid = ra.roleid
|
|
WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
|
|
) inline_view ON ro.id = inline_view.id
|
|
ORDER BY ro.sortorder ASC", $params);
|
|
}
|
|
|
|
$rolenames = array();
|
|
foreach ($roles as $role) {
|
|
$rolenames[$role->id] = $role->name;
|
|
if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
|
|
$rolenames[$role->id] .= ' (' . $role->shortname . ')';
|
|
}
|
|
}
|
|
if ($rolenamedisplay != ROLENAME_ORIGINALANDSHORT) {
|
|
$rolenames = role_fix_names($rolenames, $context, $rolenamedisplay);
|
|
}
|
|
|
|
if (!$withcounts) {
|
|
return $rolenames;
|
|
}
|
|
|
|
$rolecounts = array();
|
|
$nameswithcounts = array();
|
|
foreach ($roles as $role) {
|
|
$nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
|
|
$rolecounts[$role->id] = $roles[$role->id]->overridecount;
|
|
}
|
|
return array($rolenames, $rolecounts, $nameswithcounts);
|
|
}
|
|
|
|
/**
|
|
* @param integer $roleid the id of a role.
|
|
* @return array list of the context levels at which this role may be assigned.
|
|
*/
|
|
function get_role_contextlevels($roleid) {
|
|
global $DB;
|
|
return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
|
|
'contextlevel', 'id,contextlevel');
|
|
}
|
|
|
|
/**
|
|
* @param integer $contextlevel a contextlevel.
|
|
* @return array list of role ids that are assignable at this context level.
|
|
*/
|
|
function get_roles_for_contextlevels($contextlevel) {
|
|
global $DB;
|
|
return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
|
|
'', 'id,roleid');
|
|
}
|
|
|
|
/**
|
|
* @param string $roleid one of the legacy role types - that is, one of the keys
|
|
* from the array returned by get_legacy_roles();
|
|
* @return array list of the context levels at which this type of role may be assigned by default.
|
|
*/
|
|
function get_default_contextlevels($roletype) {
|
|
static $defaults = array(
|
|
'admin' => array(CONTEXT_SYSTEM),
|
|
'coursecreator' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
|
|
'editingteacher' => array(CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE),
|
|
'teacher' => array(CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE),
|
|
'student' => array(CONTEXT_COURSE, CONTEXT_MODULE),
|
|
'guest' => array(),
|
|
'user' => array()
|
|
);
|
|
if (isset($defaults[$roletype])) {
|
|
return $defaults[$roletype];
|
|
} else {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the context levels at which a particular role can be assigned.
|
|
* Throws exceptions in case of error.
|
|
*
|
|
* @param integer $roleid the id of a role.
|
|
* @param array $contextlevels the context levels at which this role should be assignable.
|
|
*/
|
|
function set_role_contextlevels($roleid, array $contextlevels) {
|
|
global $DB;
|
|
$DB->delete_records('role_context_levels', array('roleid' => $roleid));
|
|
$rcl = new stdClass;
|
|
$rcl->roleid = $roleid;
|
|
foreach ($contextlevels as $level) {
|
|
$rcl->contextlevel = $level;
|
|
$DB->insert_record('role_context_levels', $rcl, false, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a role object that is the default role for new enrolments
|
|
* in a given course
|
|
*
|
|
* @param object $course
|
|
* @return object $role
|
|
*/
|
|
function get_default_course_role($course) {
|
|
global $DB, $CFG;
|
|
|
|
/// First let's take the default role the course may have
|
|
if (!empty($course->defaultrole)) {
|
|
if ($role = $DB->get_record('role', array('id'=>$course->defaultrole))) {
|
|
return $role;
|
|
}
|
|
}
|
|
|
|
/// Otherwise the site setting should tell us
|
|
if ($CFG->defaultcourseroleid) {
|
|
if ($role = $DB->get_record('role', array('id'=>$CFG->defaultcourseroleid))) {
|
|
return $role;
|
|
}
|
|
}
|
|
|
|
/// It's unlikely we'll get here, but just in case, try and find a student role
|
|
if ($studentroles = get_roles_with_capability('moodle/legacy:student', CAP_ALLOW)) {
|
|
return array_shift($studentroles); /// Take the first one
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Who has this capability in this context?
|
|
*
|
|
* This can be a very expensive call - use sparingly and keep
|
|
* the results if you are going to need them again soon.
|
|
*
|
|
* Note if $fields is empty this function attempts to get u.*
|
|
* which can get rather large - and has a serious perf impact
|
|
* on some DBs.
|
|
*
|
|
* @param $context - object
|
|
* @param $capability - string capability, or an array of capabilities, in which
|
|
* case users having any of those capabilities will be returned.
|
|
* For performance reasons, you are advised to put the capability
|
|
* that the user is most likely to have first.
|
|
* @param $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
|
|
* @param $sort - the sort order. Default is lastaccess time.
|
|
* @param $limitfrom - number of records to skip (offset)
|
|
* @param $limitnum - number of records to fetch
|
|
* @param $groups - single group or array of groups - only return
|
|
* users who are in one of these group(s).
|
|
* @param $exceptions - list of users to exclude, comma separated or array
|
|
* @param view - set to true when roles are pulled for display only
|
|
* this is so that we can filter roles with no visible
|
|
* assignment, for example, you might want to "hide" all
|
|
* course creators when browsing the course participants
|
|
* list.
|
|
* @param boolean $useviewallgroups if $groups is set the return users who
|
|
* have capability both $capability and moodle/site:accessallgroups
|
|
* in this context, as well as users who have $capability and who are
|
|
* in $groups.
|
|
*/
|
|
function get_users_by_capability($context, $capability, $fields='', $sort='',
|
|
$limitfrom='', $limitnum='', $groups='', $exceptions='', $doanything=true,
|
|
$view=false, $useviewallgroups=false) {
|
|
global $CFG, $DB;
|
|
|
|
$ctxids = substr($context->path, 1); // kill leading slash
|
|
$ctxids = str_replace('/', ',', $ctxids);
|
|
|
|
// Context is the frontpage
|
|
$isfrontpage = false;
|
|
$iscoursepage = false; // coursepage other than fp
|
|
if ($context->contextlevel == CONTEXT_COURSE) {
|
|
if ($context->instanceid == SITEID) {
|
|
$isfrontpage = true;
|
|
} else {
|
|
$iscoursepage = true;
|
|
}
|
|
}
|
|
|
|
// What roles/rolecaps are interesting?
|
|
if (is_array($capability)) {
|
|
$caps = $capability;
|
|
} else {
|
|
$caps = array($capability);
|
|
}
|
|
if ($doanything === true) {
|
|
$caps[] = 'moodle/site:doanything';
|
|
$doanything_join='';
|
|
$doanything_cond='';
|
|
|
|
} else {
|
|
// This is an outer join against
|
|
// admin-ish roleids. Any row that succeeds
|
|
// in JOINing here ends up removed from
|
|
// the resultset. This means we remove
|
|
// rolecaps from roles that also have
|
|
// 'doanything' capabilities.
|
|
$doanything_join="LEFT OUTER JOIN (
|
|
SELECT DISTINCT rc.roleid
|
|
FROM {role_capabilities} rc
|
|
WHERE rc.capability=:capany
|
|
AND rc.permission=".CAP_ALLOW."
|
|
AND rc.contextid IN ($ctxids)
|
|
) dar
|
|
ON rc.roleid=dar.roleid";
|
|
$doanything_cond="AND dar.roleid IS NULL";
|
|
}
|
|
|
|
// fetch all capability records - we'll walk several
|
|
// times over them, and should be a small set
|
|
|
|
$negperm = false; // has any negative (<0) permission?
|
|
$roleids = array();
|
|
|
|
list($capstest, $params) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, 'cap0');
|
|
$params['capany'] = 'moodle/site:doanything';
|
|
|
|
$sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability,
|
|
ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel
|
|
FROM {role_capabilities} rc
|
|
JOIN {context} ctx on rc.contextid = ctx.id
|
|
$doanything_join
|
|
WHERE rc.capability $capstest AND ctx.id IN ($ctxids)
|
|
$doanything_cond
|
|
ORDER BY rc.roleid ASC, ctx.depth ASC";
|
|
|
|
if ($capdefs = $DB->get_records_sql($sql, $params)) {
|
|
foreach ($capdefs AS $rcid=>$rc) {
|
|
$roleids[] = (int)$rc->roleid;
|
|
if ($rc->permission < 0) {
|
|
$negperm = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$roleids = array_unique($roleids);
|
|
|
|
if (count($roleids)===0) { // noone here!
|
|
return array();
|
|
}
|
|
|
|
// is the default role interesting? does it have
|
|
// a relevant rolecap? (we use this a lot later)
|
|
if (isset($CFG->defaultuserroleid) and in_array((int)$CFG->defaultuserroleid, $roleids, true)) {
|
|
$defaultroleinteresting = true;
|
|
} else {
|
|
$defaultroleinteresting = false;
|
|
}
|
|
|
|
//
|
|
// Prepare query clauses
|
|
//
|
|
$wherecond = array();
|
|
|
|
// Non-deleted users. We never return deleted users.
|
|
$wherecond['nondeleted'] = 'u.deleted = 0';
|
|
|
|
/// Groups
|
|
if ($groups) {
|
|
if (is_array($groups)) {
|
|
$grouptest = 'gm.groupid IN (' . implode(',', $groups) . ')';
|
|
} else {
|
|
$grouptest = 'gm.groupid = ' . (int)$groups;
|
|
}
|
|
$grouptest = 'ra.userid IN (SELECT userid FROM ' .
|
|
'{groups_members} gm WHERE ' . $grouptest . ')';
|
|
|
|
if ($useviewallgroups) {
|
|
$viewallgroupsusers = get_users_by_capability($context,
|
|
'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
|
|
$wherecond['groups'] = '('. $grouptest . ' OR ra.userid IN (' .
|
|
implode(',', array_keys($viewallgroupsusers)) . '))';
|
|
} else {
|
|
$wherecond['groups'] = '(' . $grouptest .')';
|
|
}
|
|
}
|
|
|
|
/// User exceptions
|
|
if (!empty($exceptions)) {
|
|
if (is_array($exceptions)) {
|
|
$exceptions = implode(',', $exceptions);
|
|
}
|
|
$wherecond['userexceptions'] = ' u.id NOT IN ('.$exceptions.')';
|
|
}
|
|
|
|
/// Set up hidden role-assignments sql
|
|
if ($view && !has_capability('moodle/role:viewhiddenassigns', $context)) {
|
|
$condhiddenra = 'AND ra.hidden = 0 ';
|
|
$sscondhiddenra = 'AND ssra.hidden = 0 ';
|
|
} else {
|
|
$condhiddenra = '';
|
|
$sscondhiddenra = '';
|
|
}
|
|
|
|
// Collect WHERE conditions
|
|
$where = implode(' AND ', array_values($wherecond));
|
|
if ($where != '') {
|
|
$where = 'WHERE ' . $where;
|
|
}
|
|
|
|
/// Set up default fields
|
|
if (empty($fields)) {
|
|
if ($iscoursepage) {
|
|
$fields = 'u.*, ul.timeaccess as lastaccess';
|
|
} else {
|
|
$fields = 'u.*';
|
|
}
|
|
} else {
|
|
if (debugging('', DEBUG_DEVELOPER) && strpos($fields, 'u.*') === false &&
|
|
strpos($fields, 'u.id') === false) {
|
|
debugging('u.id must be included in the list of fields passed to get_users_by_capability.', DEBUG_DEVELOPER);
|
|
}
|
|
}
|
|
|
|
/// Set up default sort
|
|
if (empty($sort)) { // default to course lastaccess or just lastaccess
|
|
if ($iscoursepage) {
|
|
$sort = 'ul.timeaccess';
|
|
} else {
|
|
$sort = 'u.lastaccess';
|
|
}
|
|
}
|
|
$sortby = $sort ? " ORDER BY $sort " : '';
|
|
|
|
// User lastaccess JOIN
|
|
if ((strpos($sort, 'ul.timeaccess') === FALSE) and (strpos($fields, 'ul.timeaccess') === FALSE)) { // user_lastaccess is not required MDL-13810
|
|
$uljoin = '';
|
|
} else {
|
|
$uljoin = "LEFT OUTER JOIN {user_lastaccess} ul
|
|
ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
|
|
}
|
|
|
|
//
|
|
// Simple cases - No negative permissions means we can take shortcuts
|
|
//
|
|
if (!$negperm) {
|
|
|
|
// at the frontpage, and all site users have it - easy!
|
|
if ($isfrontpage && !empty($CFG->defaultfrontpageroleid)
|
|
&& in_array((int)$CFG->defaultfrontpageroleid, $roleids, true)) {
|
|
|
|
return $DB->get_records_sql("SELECT $fields
|
|
FROM {user} u
|
|
WHERE u.deleted = 0
|
|
ORDER BY $sort",
|
|
$limitfrom, $limitnum);
|
|
}
|
|
|
|
// all site users have it, anyway
|
|
// TODO: NOT ALWAYS! Check this case because this gets run for cases like this:
|
|
// 1) Default role has the permission for a module thing like mod/choice:choose
|
|
// 2) We are checking for an activity module context in a course
|
|
// 3) Thus all users are returned even though course:view is also required
|
|
if ($defaultroleinteresting) {
|
|
$sql = "SELECT $fields
|
|
FROM {user} u
|
|
$uljoin
|
|
$where
|
|
ORDER BY $sort";
|
|
return $DB->get_records_sql($sql, null, $limitfrom, $limitnum);
|
|
}
|
|
|
|
/// Simple SQL assuming no negative rolecaps.
|
|
/// We use a subselect to grab the role assignments
|
|
/// ensuring only one row per user -- even if they
|
|
/// have many "relevant" role assignments.
|
|
$select = " SELECT $fields";
|
|
$from = " FROM {user} u
|
|
JOIN (SELECT DISTINCT ssra.userid
|
|
FROM {role_assignments} ssra
|
|
WHERE ssra.contextid IN ($ctxids)
|
|
AND ssra.roleid IN (".implode(',',$roleids) .")
|
|
$sscondhiddenra
|
|
) ra ON ra.userid = u.id
|
|
$uljoin ";
|
|
return $DB->get_records_sql($select.$from.$where.$sortby, null, $limitfrom, $limitnum);
|
|
}
|
|
|
|
//
|
|
// If there are any negative rolecaps, we need to
|
|
// work through a subselect that will bring several rows
|
|
// per user (one per RA).
|
|
// Since we cannot do the job in pure SQL (not without SQL stored
|
|
// procedures anyway), we end up tied to processing the data in PHP
|
|
// all the way down to pagination.
|
|
//
|
|
// In some cases, this will mean bringing across a ton of data --
|
|
// when paginating, we have to walk the permisisons of all the rows
|
|
// in the _previous_ pages to get the pagination correct in the case
|
|
// of users that end up not having the permission - this removed.
|
|
//
|
|
|
|
// Prepare the role permissions datastructure for fast lookups
|
|
$roleperms = array(); // each role cap and depth
|
|
foreach ($capdefs AS $rcid=>$rc) {
|
|
|
|
$rid = (int)$rc->roleid;
|
|
$perm = (int)$rc->permission;
|
|
$rcdepth = (int)$rc->ctxdepth;
|
|
if (!isset($roleperms[$rc->capability][$rid])) {
|
|
$roleperms[$rc->capability][$rid] = (object)array('perm' => $perm,
|
|
'rcdepth' => $rcdepth);
|
|
} else {
|
|
if ($roleperms[$rc->capability][$rid]->perm == CAP_PROHIBIT) {
|
|
continue;
|
|
}
|
|
// override - as we are going
|
|
// from general to local perms
|
|
// (as per the ORDER BY...depth ASC above)
|
|
// and local perms win...
|
|
$roleperms[$rc->capability][$rid] = (object)array('perm' => $perm,
|
|
'rcdepth' => $rcdepth);
|
|
}
|
|
|
|
}
|
|
|
|
if ($context->contextlevel == CONTEXT_SYSTEM
|
|
|| $isfrontpage
|
|
|| $defaultroleinteresting) {
|
|
|
|
// Handle system / sitecourse / defaultrole-with-perhaps-neg-overrides
|
|
// with a SELECT FROM user LEFT OUTER JOIN against ra -
|
|
// This is expensive on the SQL and PHP sides -
|
|
// moves a ton of data across the wire.
|
|
$ss = "SELECT u.id as userid, ra.roleid,
|
|
ctx.depth
|
|
FROM {user} u
|
|
LEFT OUTER JOIN {role_assignments} ra
|
|
ON (ra.userid = u.id
|
|
AND ra.contextid IN ($ctxids)
|
|
AND ra.roleid IN (".implode(',',$roleids) .")
|
|
$condhiddenra)
|
|
LEFT OUTER JOIN {context} ctx
|
|
ON ra.contextid=ctx.id
|
|
WHERE u.deleted=0";
|
|
} else {
|
|
// "Normal complex case" - the rolecaps we are after will
|
|
// be defined in a role assignment somewhere.
|
|
$ss = "SELECT ra.userid as userid, ra.roleid,
|
|
ctx.depth
|
|
FROM {role_assignments} ra
|
|
JOIN {context} ctx
|
|
ON ra.contextid=ctx.id
|
|
WHERE ra.contextid IN ($ctxids)
|
|
$condhiddenra
|
|
AND ra.roleid IN (".implode(',',$roleids) .")";
|
|
}
|
|
|
|
$select = "SELECT $fields ,ra.roleid, ra.depth ";
|
|
$from = "FROM ($ss) ra
|
|
JOIN {user} u
|
|
ON ra.userid=u.id
|
|
$uljoin ";
|
|
|
|
// Each user's entries MUST come clustered together
|
|
// and RAs ordered in depth DESC - the role/cap resolution
|
|
// code depends on this.
|
|
$sort .= ' , ra.userid ASC, ra.depth DESC';
|
|
$sortby .= ' , ra.userid ASC, ra.depth DESC ';
|
|
|
|
if (!$rs = $DB->get_recordset_sql($select.$from.$where.$sortby)) {
|
|
return array();
|
|
}
|
|
|
|
//
|
|
// Process the user accounts+RAs, folding repeats together...
|
|
//
|
|
// The processing for this recordset is tricky - to fold
|
|
// the role/perms of users with multiple role-assignments
|
|
// correctly while still processing one-row-at-a-time
|
|
// we need to add a few additional 'private' fields to
|
|
// the results array - so we can treat the rows as a
|
|
// state machine to track the cap/perms and at what RA-depth
|
|
// and RC-depth they were defined.
|
|
//
|
|
// So what we do here is:
|
|
// - loop over rows, checking pagination limits
|
|
// - when we find a new user, if we are in the page add it to the
|
|
// $results, and start building $ras array with its role-assignments
|
|
// - when we are dealing with the next user, or are at the end of the userlist
|
|
// (last rec or last in page), trigger the check-permission idiom
|
|
// - the check permission idiom will
|
|
// - add the default enrolment if needed
|
|
// - call has_any_capability_from_rarc(), which based on RAs and RCs will return a bool
|
|
// (should be fairly tight code ;-) )
|
|
// - if the user has permission, all is good, just $c++ (counter)
|
|
// - ...else, decrease the counter - so pagination is kept straight,
|
|
// and (if we are in the page) remove from the results
|
|
//
|
|
$results = array();
|
|
|
|
// pagination controls
|
|
$c = 0;
|
|
$limitfrom = (int)$limitfrom;
|
|
$limitnum = (int)$limitnum;
|
|
|
|
//
|
|
// Track our last user id so we know when we are dealing
|
|
// with a new user...
|
|
//
|
|
$lastuserid = 0;
|
|
//
|
|
// In this loop, we
|
|
// $ras: role assignments, multidimensional array
|
|
// treat as a stack - going from local to general
|
|
// $ras = (( roleid=> x, $depth=>y) , ( roleid=> x, $depth=>y))
|
|
//
|
|
foreach($rs as $user) {
|
|
|
|
//error_log(" Record: " . print_r($user,1));
|
|
|
|
//
|
|
// Pagination controls
|
|
// Note that we might end up removing a user
|
|
// that ends up _not_ having the rights,
|
|
// therefore rolling back $c
|
|
//
|
|
if ($lastuserid != $user->id) {
|
|
|
|
// Did the last user end up with a positive permission?
|
|
if ($lastuserid !=0) {
|
|
if ($defaultroleinteresting) {
|
|
// add the role at the end of $ras
|
|
$ras[] = array( 'roleid' => $CFG->defaultuserroleid,
|
|
'depth' => 1 );
|
|
}
|
|
if (has_any_capability_from_rarc($ras, $roleperms, $caps)) {
|
|
$c++;
|
|
} else {
|
|
// remove the user from the result set,
|
|
// only if we are 'in the page'
|
|
if ($limitfrom === 0 || $c >= $limitfrom) {
|
|
unset($results[$lastuserid]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Did we hit pagination limit?
|
|
if ($limitnum !==0 && $c >= ($limitfrom+$limitnum)) { // we are done!
|
|
break;
|
|
}
|
|
|
|
// New user setup, and $ras reset
|
|
$lastuserid = $user->id;
|
|
$ras = array();
|
|
if (!empty($user->roleid)) {
|
|
$ras[] = array( 'roleid' => (int)$user->roleid,
|
|
'depth' => (int)$user->depth );
|
|
}
|
|
|
|
// if we are 'in the page', also add the rec
|
|
// to the results...
|
|
if ($limitfrom === 0 || $c >= $limitfrom) {
|
|
$results[$user->id] = $user; // trivial
|
|
}
|
|
} else {
|
|
// Additional RA for $lastuserid
|
|
$ras[] = array( 'roleid'=>(int)$user->roleid,
|
|
'depth'=>(int)$user->depth );
|
|
}
|
|
|
|
} // end while(fetch)
|
|
$rs->close();
|
|
|
|
// Prune last entry if necessary
|
|
if ($lastuserid !=0) {
|
|
if ($defaultroleinteresting) {
|
|
// add the role at the end of $ras
|
|
$ras[] = array( 'roleid' => $CFG->defaultuserroleid,
|
|
'depth' => 1 );
|
|
}
|
|
if (!has_any_capability_from_rarc($ras, $roleperms, $caps)) {
|
|
// remove the user from the result set,
|
|
// only if we are 'in the page'
|
|
if ($limitfrom === 0 || $c >= $limitfrom) {
|
|
if (isset($results[$lastuserid])) {
|
|
unset($results[$lastuserid]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Fast (fast!) utility function to resolve if any of a list of capabilities is
|
|
* granted, based on Role Assignments and Role Capabilities.
|
|
*
|
|
* Used (at least) by get_users_by_capability().
|
|
*
|
|
* If PHP had fast built-in memoize functions, we could
|
|
* add a $contextid parameter and memoize the return values.
|
|
*
|
|
* Note that this function must be kept in synch with has_capability_in_accessdata.
|
|
*
|
|
* @param array $ras - role assignments
|
|
* @param array $roleperms - role permissions
|
|
* @param string $capabilities - array of capability names
|
|
* @return boolean
|
|
*/
|
|
function has_any_capability_from_rarc($ras, $roleperms, $caps) {
|
|
// Mini-state machine, using $hascap
|
|
// $hascap[ 'moodle/foo:bar' ]->perm = CAP_SOMETHING (numeric constant)
|
|
// $hascap[ 'moodle/foo:bar' ]->radepth = depth of the role assignment that set it
|
|
// $hascap[ 'moodle/foo:bar' ]->rcdepth = depth of the rolecap that set it
|
|
// -- when resolving conflicts, we need to look into radepth first, if unresolved
|
|
|
|
$hascap = array();
|
|
|
|
//
|
|
// Compute which permission/roleassignment/rolecap
|
|
// wins for each capability we are walking
|
|
//
|
|
foreach ($ras as $ra) {
|
|
foreach ($caps as $cap) {
|
|
if (!isset($roleperms[$cap][$ra['roleid']])) {
|
|
// nothing set for this cap - skip
|
|
continue;
|
|
}
|
|
// We explicitly clone here as we
|
|
// add more properties to it
|
|
// that must stay separate from the
|
|
// original roleperm data structure
|
|
$rp = clone($roleperms[$cap][$ra['roleid']]);
|
|
$rp->radepth = $ra['depth'];
|
|
|
|
// Trivial case, we are the first to set
|
|
if (!isset($hascap[$cap])) {
|
|
$hascap[$cap] = $rp;
|
|
}
|
|
|
|
//
|
|
// Resolve who prevails, in order of precendence
|
|
// - Prohibits always wins
|
|
// - Locality of RA
|
|
// - Locality of RC
|
|
//
|
|
//// Prohibits...
|
|
if ($rp->perm === CAP_PROHIBIT) {
|
|
$hascap[$cap] = $rp;
|
|
continue;
|
|
}
|
|
if ($hascap[$cap]->perm === CAP_PROHIBIT) {
|
|
continue;
|
|
}
|
|
|
|
// Locality of RA - the look is ordered by depth DESC
|
|
// so from local to general -
|
|
// Higher RA loses to local RA... unless perm===0
|
|
/// Thanks to the order of the records, $rp->radepth <= $hascap[$cap]->radepth
|
|
if ($rp->radepth > $hascap[$cap]->radepth) {
|
|
error_log('Should not happen @ ' . __FUNCTION__.':'.__LINE__);
|
|
}
|
|
if ($rp->radepth < $hascap[$cap]->radepth) {
|
|
if ($hascap[$cap]->perm!==0) {
|
|
// Wider RA loses to local RAs...
|
|
continue;
|
|
} else {
|
|
// "Higher RA resolves conflict" case,
|
|
// local RAs had cancelled eachother
|
|
$hascap[$cap] = $rp;
|
|
continue;
|
|
}
|
|
}
|
|
// Same ralevel - locality of RC wins
|
|
if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
|
|
$hascap[$cap] = $rp;
|
|
continue;
|
|
}
|
|
if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
|
|
continue;
|
|
}
|
|
// We match depth - add them
|
|
$hascap[$cap]->perm += $rp->perm;
|
|
}
|
|
}
|
|
foreach ($caps as $capability) {
|
|
if (isset($hascap[$capability]) && $hascap[$capability]->perm > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Will re-sort a $users results array (from get_users_by_capability(), usually)
|
|
* based on a sorting policy. This is to support the odd practice of
|
|
* sorting teachers by 'authority', where authority was "lowest id of the role
|
|
* assignment".
|
|
*
|
|
* Will execute 1 database query. Only suitable for small numbers of users, as it
|
|
* uses an u.id IN() clause.
|
|
*
|
|
* Notes about the sorting criteria.
|
|
*
|
|
* As a default, we cannot rely on role.sortorder because then
|
|
* admins/coursecreators will always win. That is why the sane
|
|
* rule "is locality matters most", with sortorder as 2nd
|
|
* consideration.
|
|
*
|
|
* If you want role.sortorder, use the 'sortorder' policy, and
|
|
* name explicitly what roles you want to cover. It's probably
|
|
* a good idea to see what roles have the capabilities you want
|
|
* (array_diff() them against roiles that have 'can-do-anything'
|
|
* to weed out admin-ish roles. Or fetch a list of roles from
|
|
* variables like $CFG->coursemanagers .
|
|
*
|
|
* @param array users Users' array, keyed on userid
|
|
* @param object context
|
|
* @param array roles - ids of the roles to include, optional
|
|
* @param string policy - defaults to locality, more about
|
|
* @return array - sorted copy of the array
|
|
*/
|
|
function sort_by_roleassignment_authority($users, $context, $roles=array(), $sortpolicy='locality') {
|
|
global $DB;
|
|
|
|
$userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
|
|
$contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
|
|
if (empty($roles)) {
|
|
$roleswhere = '';
|
|
} else {
|
|
$roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
|
|
}
|
|
|
|
$sql = "SELECT ra.userid
|
|
FROM {role_assignments} ra
|
|
JOIN {role} r
|
|
ON ra.roleid=r.id
|
|
JOIN {context} ctx
|
|
ON ra.contextid=ctx.id
|
|
WHERE $userswhere
|
|
$contextwhere
|
|
$roleswhere";
|
|
|
|
// Default 'locality' policy -- read PHPDoc notes
|
|
// about sort policies...
|
|
$orderby = 'ORDER BY '
|
|
.'ctx.depth DESC, ' /* locality wins */
|
|
.'r.sortorder ASC, ' /* rolesorting 2nd criteria */
|
|
.'ra.id'; /* role assignment order tie-breaker */
|
|
if ($sortpolicy === 'sortorder') {
|
|
$orderby = 'ORDER BY '
|
|
.'r.sortorder ASC, ' /* rolesorting 2nd criteria */
|
|
.'ra.id'; /* role assignment order tie-breaker */
|
|
}
|
|
|
|
$sortedids = $DB->get_fieldset_sql($sql . $orderby);
|
|
$sortedusers = array();
|
|
$seen = array();
|
|
|
|
foreach ($sortedids as $id) {
|
|
// Avoid duplicates
|
|
if (isset($seen[$id])) {
|
|
continue;
|
|
}
|
|
$seen[$id] = true;
|
|
|
|
// assign
|
|
$sortedusers[$id] = $users[$id];
|
|
}
|
|
return $sortedusers;
|
|
}
|
|
|
|
/**
|
|
* gets all the users assigned this role in this context or higher
|
|
* @param int roleid (can also be an array of ints!)
|
|
* @param int contextid
|
|
* @param bool parent if true, get list of users assigned in higher context too
|
|
* @param string fields - fields from user (u.) , role assignment (ra) or role (r.)
|
|
* @param string sort - sort from user (u.) , role assignment (ra) or role (r.)
|
|
* @param bool gethidden - whether to fetch hidden enrolments too
|
|
* @return array()
|
|
*/
|
|
function get_role_users($roleid, $context, $parent=false, $fields='',
|
|
$sort='u.lastname, u.firstname', $gethidden=true, $group='',
|
|
$limitfrom='', $limitnum='', $extrawheretest='', $whereparams=array()) {
|
|
global $DB;
|
|
|
|
if (empty($fields)) {
|
|
$fields = 'u.id, u.confirmed, u.username, u.firstname, u.lastname, '.
|
|
'u.maildisplay, u.mailformat, u.maildigest, u.email, u.city, '.
|
|
'u.country, u.picture, u.idnumber, u.department, u.institution, '.
|
|
'u.emailstop, u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name as rolename';
|
|
}
|
|
|
|
// whether this assignment is hidden
|
|
$hiddensql = $gethidden ? '': ' AND ra.hidden = 0 ';
|
|
|
|
$parentcontexts = '';
|
|
if ($parent) {
|
|
$parentcontexts = substr($context->path, 1); // kill leading slash
|
|
$parentcontexts = str_replace('/', ',', $parentcontexts);
|
|
if ($parentcontexts !== '') {
|
|
$parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
|
|
}
|
|
}
|
|
|
|
if ($roleid) {
|
|
list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
|
|
$roleselect = "AND ra.roleid $rids";
|
|
} else {
|
|
$params = array();
|
|
$roleselect = '';
|
|
}
|
|
|
|
if ($group) {
|
|
$groupjoin = "JOIN {groups_members} gm ON gm.userid = u.id";
|
|
$groupselect = " AND gm.groupid = ? ";
|
|
$params[] = $group;
|
|
} else {
|
|
$groupjoin = '';
|
|
$groupselect = '';
|
|
}
|
|
|
|
array_unshift($params, $context->id);
|
|
|
|
if ($extrawheretest) {
|
|
$extrawheretest = ' AND ' . $extrawheretest;
|
|
$params = array_merge($params, $whereparams);
|
|
}
|
|
|
|
$sql = "SELECT $fields, ra.roleid
|
|
FROM {role_assignments} ra
|
|
JOIN {user} u ON u.id = ra.userid
|
|
JOIN {role} r ON ra.roleid = r.id
|
|
$groupjoin
|
|
WHERE (ra.contextid = ? $parentcontexts)
|
|
$roleselect
|
|
$groupselect
|
|
$hiddensql
|
|
$extrawheretest
|
|
ORDER BY $sort"; // join now so that we can just use fullname() later
|
|
|
|
return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
|
|
}
|
|
|
|
/**
|
|
* Counts all the users assigned this role in this context or higher
|
|
* @param int roleid (can also be an array of ints!)
|
|
* @param int contextid
|
|
* @param bool parent if true, get list of users assigned in higher context too
|
|
* @return count
|
|
*/
|
|
function count_role_users($roleid, $context, $parent=false) {
|
|
global $DB;
|
|
|
|
if ($parent) {
|
|
if ($contexts = get_parent_contexts($context)) {
|
|
$parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
|
|
} else {
|
|
$parentcontexts = '';
|
|
}
|
|
} else {
|
|
$parentcontexts = '';
|
|
}
|
|
|
|
if ($roleid) {
|
|
list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
|
|
$roleselect = "AND r.roleid $rids";
|
|
} else {
|
|
$params = array();
|
|
$roleselect = '';
|
|
}
|
|
|
|
array_unshift($params, $context->id);
|
|
|
|
$sql = "SELECT count(u.id)
|
|
FROM {role_assignments} r
|
|
JOIN {user} u ON u.id = r.userid
|
|
WHERE (r.contextid = ? $parentcontexts)
|
|
$roleselect
|
|
AND u.deleted = 0";
|
|
|
|
return $DB->count_records_sql($sql, $params);
|
|
}
|
|
|
|
/**
|
|
* This function gets the list of courses that this user has a particular capability in.
|
|
* It is still not very efficient.
|
|
* @param string $capability Capability in question
|
|
* @param int $userid User ID or null for current user
|
|
* @param bool $doanything True if 'doanything' is permitted (default)
|
|
* @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
|
|
* otherwise use a comma-separated list of the fields you require, not including id
|
|
* @param string $orderby If set, use a comma-separated list of fields from course
|
|
* table with sql modifiers (DESC) if needed
|
|
* @return array Array of courses, may have zero entries. Or false if query failed.
|
|
*/
|
|
function get_user_capability_course($capability, $userid=NULL, $doanything=true, $fieldsexceptid='', $orderby='') {
|
|
global $DB;
|
|
|
|
// Convert fields list and ordering
|
|
$fieldlist = '';
|
|
if ($fieldsexceptid) {
|
|
$fields = explode(',', $fieldsexceptid);
|
|
foreach($fields as $field) {
|
|
$fieldlist .= ',c.'.$field;
|
|
}
|
|
}
|
|
if ($orderby) {
|
|
$fields = explode(',', $orderby);
|
|
$orderby = '';
|
|
foreach($fields as $field) {
|
|
if($orderby) {
|
|
$orderby .= ',';
|
|
}
|
|
$orderby .= 'c.'.$field;
|
|
}
|
|
$orderby = 'ORDER BY '.$orderby;
|
|
}
|
|
|
|
// Obtain a list of everything relevant about all courses including context.
|
|
// Note the result can be used directly as a context (we are going to), the course
|
|
// fields are just appended.
|
|
|
|
if (!$rs = $DB->get_recordset_sql("SELECT x.*, c.id AS courseid $fieldlist
|
|
FROM {course} c
|
|
INNER JOIN {context} x
|
|
ON (c.id=x.instanceid AND x.contextlevel=".CONTEXT_COURSE.")
|
|
$orderby")) {
|
|
return false;
|
|
}
|
|
|
|
// Check capability for each course in turn
|
|
$courses = array();
|
|
foreach ($rs as $coursecontext) {
|
|
if(has_capability($capability, $coursecontext, $userid, $doanything)) {
|
|
// We've got the capability. Make the record look like a course record
|
|
// and store it
|
|
$coursecontext->id = $coursecontext->courseid;
|
|
unset($coursecontext->courseid);
|
|
unset($coursecontext->contextlevel);
|
|
unset($coursecontext->instanceid);
|
|
$courses[] = $coursecontext;
|
|
}
|
|
}
|
|
$rs->close();
|
|
return $courses;
|
|
}
|
|
|
|
/** This function finds the roles assigned directly to this context only
|
|
* i.e. no parents role
|
|
* @param object $context
|
|
* @return array
|
|
*/
|
|
function get_roles_on_exact_context($context) {
|
|
global $DB;
|
|
|
|
return $DB->get_records_sql("SELECT r.*
|
|
FROM {role_assignments} ra, {role} r
|
|
WHERE ra.roleid = r.id AND ra.contextid = ?",
|
|
array($context->id));
|
|
|
|
}
|
|
|
|
/**
|
|
* Switches the current user to another role for the current session and only
|
|
* in the given context.
|
|
*
|
|
* The caller *must* check
|
|
* - that this op is allowed
|
|
* - that the requested role can be switched to in this context (use get_switchable_roles)
|
|
* - that the requested role is NOT $CFG->defaultuserroleid
|
|
*
|
|
* To "unswitch" pass 0 as the roleid.
|
|
*
|
|
* This function *will* modify $USER->access - beware
|
|
*
|
|
* @param integer $roleid the role to switch to.
|
|
* @param object $context the context in which to perform the switch.
|
|
* @return bool success or failure.
|
|
*/
|
|
function role_switch($roleid, $context) {
|
|
global $USER;
|
|
|
|
//
|
|
// Plan of action
|
|
//
|
|
// - Add the ghost RA to $USER->access
|
|
// as $USER->access['rsw'][$path] = $roleid
|
|
//
|
|
// - Make sure $USER->access['rdef'] has the roledefs
|
|
// it needs to honour the switcheroo
|
|
//
|
|
// Roledefs will get loaded "deep" here - down to the last child
|
|
// context. Note that
|
|
//
|
|
// - When visiting subcontexts, our selective accessdata loading
|
|
// will still work fine - though those ra/rdefs will be ignored
|
|
// appropriately while the switch is in place
|
|
//
|
|
// - If a switcheroo happens at a category with tons of courses
|
|
// (that have many overrides for switched-to role), the session
|
|
// will get... quite large. Sometimes you just can't win.
|
|
//
|
|
// To un-switch just unset($USER->access['rsw'][$path])
|
|
//
|
|
// Note: it is not possible to switch to roles that do not have course:view
|
|
|
|
// Add the switch RA
|
|
if (!isset($USER->access['rsw'])) {
|
|
$USER->access['rsw'] = array();
|
|
}
|
|
|
|
if ($roleid == 0) {
|
|
unset($USER->access['rsw'][$context->path]);
|
|
if (empty($USER->access['rsw'])) {
|
|
unset($USER->access['rsw']);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
$USER->access['rsw'][$context->path]=$roleid;
|
|
|
|
// Load roledefs
|
|
$USER->access = get_role_access_bycontext($roleid, $context,
|
|
$USER->access);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get any role that has an override on exact context
|
|
*/
|
|
function get_roles_with_override_on_context($context) {
|
|
global $DB;
|
|
|
|
return $DB->get_records_sql("SELECT r.*
|
|
FROM {role_capabilities} rc, {role} r
|
|
WHERE rc.roleid = r.id AND rc.contextid = ?",
|
|
array($context->id));
|
|
}
|
|
|
|
/**
|
|
* Get all capabilities for this role on this context (overrids)
|
|
*/
|
|
function get_capabilities_from_role_on_context($role, $context) {
|
|
global $DB;
|
|
|
|
return $DB->get_records_sql("SELECT *
|
|
FROM {role_capabilities}
|
|
WHERE contextid = ? AND roleid = ?",
|
|
array($context->id, $role->id));
|
|
}
|
|
|
|
/**
|
|
* Find out which roles has assignment on this context
|
|
*/
|
|
function get_roles_with_assignment_on_context($context) {
|
|
global $DB;
|
|
|
|
return $DB->get_records_sql("SELECT r.*
|
|
FROM {role_assignments} ra, {role} r
|
|
WHERE ra.roleid = r.id AND ra.contextid = ?",
|
|
array($context->id));
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Find all user assignemnt of users for this role, on this context
|
|
*/
|
|
function get_users_from_role_on_context($role, $context) {
|
|
global $DB;
|
|
|
|
return $DB->get_records_sql("SELECT *
|
|
FROM {role_assignments}
|
|
WHERE contextid = ? AND roleid = ?",
|
|
array($context->id, $role->id));
|
|
}
|
|
|
|
/**
|
|
* Simple function returning a boolean true if roles exist, otherwise false
|
|
*/
|
|
function user_has_role_assignment($userid, $roleid, $contextid=0) {
|
|
global $DB;
|
|
|
|
if ($contextid) {
|
|
return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid, 'contextid'=>$contextid));
|
|
} else {
|
|
return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get role name or alias if exists and format the text.
|
|
* @param object $role role object
|
|
* @param object $coursecontext
|
|
* @return $string name of role in course context
|
|
*/
|
|
function role_get_name($role, $coursecontext) {
|
|
global $DB;
|
|
|
|
if ($r = $DB->get_record('role_names', array('roleid'=>$role->id, 'contextid'=>$coursecontext->id))) {
|
|
return strip_tags(format_string($r->name));
|
|
} else {
|
|
return strip_tags(format_string($role->name));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare list of roles for display, apply aliases and format text
|
|
* @param array $roleoptions array roleid => rolename or roleid => roleobject
|
|
* @param object $context a context
|
|
* @return array of context-specific role names, or role objexts with a ->localname field added.
|
|
*/
|
|
function role_fix_names($roleoptions, $context, $rolenamedisplay=ROLENAME_ALIAS) {
|
|
global $DB;
|
|
|
|
// Make sure we are working with an array roleid => name. Normally we
|
|
// want to use the unlocalised name if the localised one is not present.
|
|
$newnames = array();
|
|
foreach ($roleoptions as $rid => $roleorname) {
|
|
if ($rolenamedisplay != ROLENAME_ALIAS_RAW) {
|
|
if (is_object($roleorname)) {
|
|
$newnames[$rid] = $roleorname->name;
|
|
} else {
|
|
$newnames[$rid] = $roleorname;
|
|
}
|
|
} else {
|
|
$newnames[$rid] = '';
|
|
}
|
|
}
|
|
|
|
// If necessary, get the localised names.
|
|
if ($rolenamedisplay != ROLENAME_ORIGINAL && !empty($context->id)) {
|
|
// Make sure we have a course context.
|
|
if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_BLOCK) { // find the parent course context
|
|
if ($parentcontextid = array_shift(get_parent_contexts($context))) {
|
|
$context = get_context_instance_by_id($parentcontextid);
|
|
}
|
|
}
|
|
|
|
// The get the relevant renames, and use them.
|
|
$aliasnames = $DB->get_records('role_names', array('contextid'=>$context->id));
|
|
foreach ($aliasnames as $alias) {
|
|
if (isset($newnames[$alias->roleid])) {
|
|
if ($rolenamedisplay == ROLENAME_ALIAS || $rolenamedisplay == ROLENAME_ALIAS_RAW) {
|
|
$newnames[$alias->roleid] = $alias->name;
|
|
} else if ($rolenamedisplay == ROLENAME_BOTH) {
|
|
$newnames[$alias->roleid] = $alias->name . ' (' . $roleoptions[$alias->roleid] . ')';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, apply format_string and put the result in the right place.
|
|
foreach ($roleoptions as $rid => $roleorname) {
|
|
if ($rolenamedisplay != ROLENAME_ALIAS_RAW) {
|
|
$newnames[$rid] = strip_tags(format_string($newnames[$rid]));
|
|
}
|
|
if (is_object($roleorname)) {
|
|
$roleoptions[$rid]->localname = $newnames[$rid];
|
|
} else {
|
|
$roleoptions[$rid] = $newnames[$rid];
|
|
}
|
|
}
|
|
return $roleoptions;
|
|
}
|
|
|
|
/**
|
|
* This function helps admin/roles/manage.php etc to detect if a new line should be printed
|
|
* when we read in a new capability
|
|
* most of the time, if the 2 components are different we should print a new line, (e.g. course system->rss client)
|
|
* but when we are in grade, all reports/import/export capabilites should be together
|
|
* @param string a - component string a
|
|
* @param string b - component string b
|
|
* @return bool - whether 2 component are in different "sections"
|
|
*/
|
|
function component_level_changed($cap, $comp, $contextlevel) {
|
|
|
|
if ($cap->component == 'enrol/authorize' && $comp =='enrol/authorize') {
|
|
return false;
|
|
}
|
|
|
|
if (strstr($cap->component, '/') && strstr($comp, '/')) {
|
|
$compsa = explode('/', $cap->component);
|
|
$compsb = explode('/', $comp);
|
|
|
|
// list of system reports
|
|
if (($compsa[0] == 'report') && ($compsb[0] == 'report')) {
|
|
return false;
|
|
}
|
|
|
|
// we are in gradebook, still
|
|
if (($compsa[0] == 'gradeexport' || $compsa[0] == 'gradeimport' || $compsa[0] == 'gradereport') &&
|
|
($compsb[0] == 'gradeexport' || $compsb[0] == 'gradeimport' || $compsb[0] == 'gradereport')) {
|
|
return false;
|
|
}
|
|
|
|
if (($compsa[0] == 'coursereport') && ($compsb[0] == 'coursereport')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
|
|
}
|
|
|
|
/**
|
|
* Rebuild all related context depth and path caches
|
|
* @param array $fixcontexts array of contexts
|
|
*/
|
|
function rebuild_contexts(array $fixcontexts) {
|
|
global $DB;
|
|
|
|
foreach ($fixcontexts as $context) {
|
|
if ($context->path) {
|
|
mark_context_dirty($context->path);
|
|
}
|
|
$DB->set_field_select('context', 'depth', 0, "path LIKE '%/$context->id/%'");
|
|
$DB->set_field('context', 'depth', 0, array('id'=>$context->id));
|
|
}
|
|
build_context_path(false);
|
|
}
|
|
|
|
/**
|
|
* Populate context.path and context.depth where missing.
|
|
* @param bool $force force a complete rebuild of the path and depth fields.
|
|
* @return void
|
|
*/
|
|
function build_context_path($force=false) {
|
|
global $CFG, $DB;
|
|
|
|
// System context
|
|
$sitectx = get_system_context(!$force);
|
|
$base = '/'.$sitectx->id;
|
|
|
|
// Sitecourse
|
|
$sitecoursectx = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>SITEID));
|
|
if ($force || $sitecoursectx->path !== "$base/{$sitecoursectx->id}") {
|
|
$DB->set_field('context', 'path', "$base/{$sitecoursectx->id}", array('id'=>$sitecoursectx->id));
|
|
$DB->set_field('context', 'depth', 2, array('id'=>$sitecoursectx->id));
|
|
$sitecoursectx = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>SITEID));
|
|
}
|
|
|
|
$ctxemptyclause = " AND (ctx.path IS NULL
|
|
OR ctx.depth=0) ";
|
|
$emptyclause = " AND ({context}.path IS NULL
|
|
OR {context}.depth=0) ";
|
|
if ($force) {
|
|
$ctxemptyclause = $emptyclause = '';
|
|
}
|
|
|
|
/* MDL-11347:
|
|
* - mysql does not allow to use FROM in UPDATE statements
|
|
* - using two tables after UPDATE works in mysql, but might give unexpected
|
|
* results in pg 8 (depends on configuration)
|
|
* - using table alias in UPDATE does not work in pg < 8.2
|
|
*
|
|
* Different code for each database - mostly for performance reasons
|
|
*/
|
|
$dbfamily = $DB->get_dbfamily();
|
|
if ($dbfamily == 'mysql') {
|
|
$updatesql = "UPDATE {context} ct, {context_temp} temp
|
|
SET ct.path = temp.path,
|
|
ct.depth = temp.depth
|
|
WHERE ct.id = temp.id";
|
|
} else if ($dbfamily == 'oracle') {
|
|
$updatesql = "UPDATE {context} ct
|
|
SET (ct.path, ct.depth) =
|
|
(SELECT temp.path, temp.depth
|
|
FROM {context_temp} temp
|
|
WHERE temp.id=ct.id)
|
|
WHERE EXISTS (SELECT 'x'
|
|
FROM {context_temp} temp
|
|
WHERE temp.id = ct.id)";
|
|
} else if ($dbfamily == 'postgres' or $dbfamily == 'mssql') {
|
|
$updatesql = "UPDATE {context}
|
|
SET path = temp.path,
|
|
depth = temp.depth
|
|
FROM {context_temp} temp
|
|
WHERE temp.id={context}.id";
|
|
} else {
|
|
// sqlite and others
|
|
$updatesql = "UPDATE {context}
|
|
SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
|
|
depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id)
|
|
WHERE id IN (SELECT id FROM mdl_context_temp)";
|
|
}
|
|
|
|
// Top level categories
|
|
$sql = "UPDATE {context}
|
|
SET depth=2, path=" . $DB->sql_concat("'$base/'", 'id') . "
|
|
WHERE contextlevel=".CONTEXT_COURSECAT."
|
|
AND EXISTS (SELECT 'x'
|
|
FROM {course_categories} cc
|
|
WHERE cc.id = {context}.instanceid
|
|
AND cc.depth=1)
|
|
$emptyclause";
|
|
|
|
$DB->execute($sql);
|
|
$DB->delete_records('context_temp');
|
|
|
|
// Deeper categories - one query per depthlevel
|
|
$maxdepth = $DB->get_field_sql("SELECT MAX(depth)
|
|
FROM {course_categories}");
|
|
for ($n=2; $n<=$maxdepth; $n++) {
|
|
$sql = "INSERT INTO {context_temp} (id, path, depth)
|
|
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", $n+1
|
|
FROM {context} ctx
|
|
JOIN {course_categories} c ON ctx.instanceid=c.id
|
|
JOIN {context} pctx ON c.parent=pctx.instanceid
|
|
WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
|
|
AND pctx.contextlevel=".CONTEXT_COURSECAT."
|
|
AND c.depth=$n
|
|
AND NOT EXISTS (SELECT 'x'
|
|
FROM {context_temp} temp
|
|
WHERE temp.id = ctx.id)
|
|
$ctxemptyclause";
|
|
$DB->execute($sql);
|
|
|
|
// this is needed after every loop
|
|
// MDL-11532
|
|
$DB->execute($updatesql);
|
|
$DB->delete_records('context_temp');
|
|
}
|
|
|
|
// Courses -- except sitecourse
|
|
$sql = "INSERT INTO {context_temp} (id, path, depth)
|
|
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
|
|
FROM {context} ctx
|
|
JOIN {course} c ON ctx.instanceid=c.id
|
|
JOIN {context} pctx ON c.category=pctx.instanceid
|
|
WHERE ctx.contextlevel=".CONTEXT_COURSE."
|
|
AND c.id!=".SITEID."
|
|
AND pctx.contextlevel=".CONTEXT_COURSECAT."
|
|
AND NOT EXISTS (SELECT 'x'
|
|
FROM {context_temp} temp
|
|
WHERE temp.id = ctx.id)
|
|
$ctxemptyclause";
|
|
$DB->execute($sql);
|
|
|
|
$DB->execute($updatesql);
|
|
$DB->delete_records('context_temp');
|
|
|
|
// Module instances
|
|
$sql = "INSERT INTO {context_temp} (id, path, depth)
|
|
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
|
|
FROM {context} ctx
|
|
JOIN {course_modules} cm ON ctx.instanceid=cm.id
|
|
JOIN {context} pctx ON cm.course=pctx.instanceid
|
|
WHERE ctx.contextlevel=".CONTEXT_MODULE."
|
|
AND pctx.contextlevel=".CONTEXT_COURSE."
|
|
AND NOT EXISTS (SELECT 'x'
|
|
FROM {context_temp} temp
|
|
WHERE temp.id = ctx.id)
|
|
$ctxemptyclause";
|
|
$DB->execute($sql);
|
|
|
|
$DB->execute($updatesql);
|
|
$DB->delete_records('context_temp');
|
|
|
|
// Blocks - non-pinned course-view only
|
|
$sql = "INSERT INTO {context_temp} (id, path, depth)
|
|
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
|
|
FROM {context} ctx
|
|
JOIN {block_instance} bi ON ctx.instanceid = bi.id
|
|
JOIN {context} pctx ON bi.pageid=pctx.instanceid
|
|
WHERE ctx.contextlevel=".CONTEXT_BLOCK."
|
|
AND pctx.contextlevel=".CONTEXT_COURSE."
|
|
AND bi.pagetype='course-view'
|
|
AND NOT EXISTS (SELECT 'x'
|
|
FROM {context_temp} temp
|
|
WHERE temp.id = ctx.id)
|
|
$ctxemptyclause";
|
|
$DB->execute($sql);
|
|
|
|
$DB->execute($updatesql);
|
|
$DB->delete_records('context_temp');
|
|
|
|
// Blocks - others
|
|
$sql = "UPDATE {context}
|
|
SET depth=2, path=".$DB->sql_concat("'$base/'", 'id')."
|
|
WHERE contextlevel=".CONTEXT_BLOCK."
|
|
AND EXISTS (SELECT 'x'
|
|
FROM {block_instance} bi
|
|
WHERE bi.id = {context}.instanceid
|
|
AND bi.pagetype!='course-view')
|
|
$emptyclause ";
|
|
$DB->execute($sql);
|
|
|
|
// User
|
|
$sql = "UPDATE {context}
|
|
SET depth=2, path=".$DB->sql_concat("'$base/'", 'id')."
|
|
WHERE contextlevel=".CONTEXT_USER."
|
|
AND EXISTS (SELECT 'x'
|
|
FROM {user} u
|
|
WHERE u.id = {context}.instanceid)
|
|
$emptyclause ";
|
|
$DB->execute($sql);
|
|
|
|
// reset static course cache - it might have incorrect cached data
|
|
global $ACCESSLIB_PRIVATE;
|
|
$ACCESSLIB_PRIVATE->contexts = array();
|
|
$ACCESSLIB_PRIVATE->contextsbyid = array();
|
|
}
|
|
|
|
/**
|
|
* Update the path field of the context and
|
|
* all the dependent subcontexts that follow
|
|
* the move.
|
|
*
|
|
* The most important thing here is to be as
|
|
* DB efficient as possible. This op can have a
|
|
* massive impact in the DB.
|
|
*
|
|
* @param obj current context obj
|
|
* @param obj newparent new parent obj
|
|
*
|
|
*/
|
|
function context_moved($context, $newparent) {
|
|
global $DB;
|
|
|
|
$frompath = $context->path;
|
|
$newpath = $newparent->path . '/' . $context->id;
|
|
|
|
$setdepth = '';
|
|
if (($newparent->depth +1) != $context->depth) {
|
|
$diff = $newparent->depth - $context->depth + 1;
|
|
$setdepth = ", depth = depth + $diff";
|
|
}
|
|
$sql = "UPDATE {context}
|
|
SET path = ?
|
|
$setdepth
|
|
WHERE path = ?";
|
|
$params = array($newpath, $frompath);
|
|
$DB->execute($sql, $params);
|
|
|
|
$sql = "UPDATE {context}
|
|
SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath)+1))."
|
|
$setdepth
|
|
WHERE path LIKE ?";
|
|
$params = array($newpath, "{$frompath}/%");
|
|
$DB->execute($sql, $params);
|
|
|
|
mark_context_dirty($frompath);
|
|
mark_context_dirty($newpath);
|
|
}
|
|
|
|
|
|
/**
|
|
* Turn the ctx* fields in an objectlike record
|
|
* into a context subobject. This allows
|
|
* us to SELECT from major tables JOINing with
|
|
* context at no cost, saving a ton of context
|
|
* lookups...
|
|
*/
|
|
function make_context_subobj($rec) {
|
|
$ctx = new StdClass;
|
|
$ctx->id = $rec->ctxid; unset($rec->ctxid);
|
|
$ctx->path = $rec->ctxpath; unset($rec->ctxpath);
|
|
$ctx->depth = $rec->ctxdepth; unset($rec->ctxdepth);
|
|
$ctx->contextlevel = $rec->ctxlevel; unset($rec->ctxlevel);
|
|
$ctx->instanceid = $rec->id;
|
|
|
|
$rec->context = $ctx;
|
|
return $rec;
|
|
}
|
|
|
|
/**
|
|
* Do some basic, quick checks to see whether $rec->context looks like a
|
|
* valid context object.
|
|
*
|
|
* @param object $rec a think that has a context, for example a course,
|
|
* course category, course modules, etc.
|
|
* @param integer $contextlevel the type of thing $rec is, one of the CONTEXT_... constants.
|
|
* @return boolean whether $rec->context looks like the correct context object
|
|
* for this thing.
|
|
*/
|
|
function is_context_subobj_valid($rec, $contextlevel) {
|
|
return isset($rec->context) && isset($rec->context->id) &&
|
|
isset($rec->context->path) && isset($rec->context->depth) &&
|
|
isset($rec->context->contextlevel) && isset($rec->context->instanceid) &&
|
|
$rec->context->contextlevel == $contextlevel && $rec->context->instanceid == $rec->id;
|
|
}
|
|
|
|
/**
|
|
* When you have a record (for example a $category, $course, $user or $cm that may,
|
|
* or may not, have come from a place that does make_context_subobj, you can use
|
|
* this method to ensure that $rec->context is present and correct before you continue.
|
|
*
|
|
* @param object $rec a thing that has an associated context.
|
|
* @param integer $contextlevel the type of thing $rec is, one of the CONTEXT_... constants.
|
|
*/
|
|
function ensure_context_subobj_present(&$rec, $contextlevel) {
|
|
if (!is_context_subobj_valid($rec, $contextlevel)) {
|
|
$rec->context = get_context_instance($contextlevel, $rec->id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch recent dirty contexts to know cheaply whether our $USER->access
|
|
* is stale and needs to be reloaded.
|
|
*
|
|
* Uses cache_flags
|
|
* @param int $time
|
|
* @return array of dirty contexts
|
|
*/
|
|
function get_dirty_contexts($time) {
|
|
return get_cache_flags('accesslib/dirtycontexts', $time-2);
|
|
}
|
|
|
|
/**
|
|
* Mark a context as dirty (with timestamp)
|
|
* so as to force reloading of the context.
|
|
* @param string $path context path
|
|
*/
|
|
function mark_context_dirty($path) {
|
|
global $CFG, $ACCESSLIB_PRIVATE;
|
|
|
|
if (empty($CFG->rolesactive)) {
|
|
return;
|
|
}
|
|
|
|
// only if it is a non-empty string
|
|
if (is_string($path) && $path !== '') {
|
|
set_cache_flag('accesslib/dirtycontexts', $path, 1, time()+$CFG->sessiontimeout);
|
|
if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts[$path] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will walk the contextpath to answer whether
|
|
* the contextpath is dirty
|
|
*
|
|
* @param array $contexts array of strings
|
|
* @param obj/array dirty contexts from get_dirty_contexts()
|
|
* @return bool
|
|
*/
|
|
function is_contextpath_dirty($pathcontexts, $dirty) {
|
|
$path = '';
|
|
foreach ($pathcontexts as $ctx) {
|
|
$path = $path.'/'.$ctx;
|
|
if (isset($dirty[$path])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Fix the roles.sortorder field in the database, so it contains sequential integers,
|
|
* and return an array of roleids in order.
|
|
*
|
|
* @param array $allroles array of roles, as returned by get_all_roles();
|
|
* @return array $role->sortorder =-> $role->id with the keys in ascending order.
|
|
*/
|
|
function fix_role_sortorder($allroles) {
|
|
$rolesort = array();
|
|
$i = 0;
|
|
foreach ($allroles as $role) {
|
|
$rolesort[$i] = $role->id;
|
|
if ($role->sortorder != $i) {
|
|
$r = new object();
|
|
$r->id = $role->id;
|
|
$r->sortorder = $i;
|
|
$DB->update_record('role', $r);
|
|
$allroles[$role->id]->sortorder = $i;
|
|
}
|
|
$i++;
|
|
}
|
|
return $rolesort;
|
|
}
|
|
|
|
/**
|
|
* Switch the sort order of two roles (used in admin/roles/manage.php).
|
|
*
|
|
* @param object $first The first role. Actually, only ->sortorder is used.
|
|
* @param object $second The second role. Actually, only ->sortorder is used.
|
|
* @return boolean success or failure
|
|
*/
|
|
function switch_roles($first, $second) {
|
|
global $DB;
|
|
$temp = $DB->get_field('role', 'MAX(sortorder) + 1', array());
|
|
$result = $DB->set_field('role', 'sortorder', $temp, array('sortorder' => $first->sortorder));
|
|
$result = $result && $DB->set_field('role', 'sortorder', $first->sortorder, array('sortorder' => $second->sortorder));
|
|
$result = $result && $DB->set_field('role', 'sortorder', $second->sortorder, array('sortorder' => $temp));
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* duplicates all the base definitions of a role
|
|
*
|
|
* @param object $sourcerole role to copy from
|
|
* @param int $targetrole id of role to copy to
|
|
*
|
|
* @return void
|
|
*/
|
|
function role_cap_duplicate($sourcerole, $targetrole) {
|
|
global $DB;
|
|
|
|
$systemcontext = get_context_instance(CONTEXT_SYSTEM);
|
|
$caps = $DB->get_records_sql("SELECT *
|
|
FROM {role_capabilities}
|
|
WHERE roleid = ? AND contextid = ?",
|
|
array($sourcerole->id, $systemcontext->id));
|
|
// adding capabilities
|
|
foreach ($caps as $cap) {
|
|
unset($cap->id);
|
|
$cap->roleid = $targetrole;
|
|
$DB->insert_record('role_capabilities', $cap);
|
|
}
|
|
}
|
|
|
|
?>
|