From e922fe23b684eef2c82f0ade1f7ec4bbad9ac554 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Fri, 14 Oct 2011 12:48:00 +0200 Subject: [PATCH] MDL-29602 accesslib improvements Refactoring and improvements of the accesslib.php library including prevention of access for not-logged-in users when forcelogin enabled, improved context caching, OOP refactoring of contexts, fixed context loading, deduplication of role definitions in user sessions, installation improvements, decoupling of enrolment checking from capability loading, added detection of deleted and non-existent users in has_capability(), new function accesslib test, auth and enrol upgrade notes. More details are available in tracker subtasks. --- admin/report/questioninstances/index.php | 12 +- admin/roles/usersroles.php | 9 +- admin/tool/capability/index.php | 6 +- auth/shibboleth/index.php | 7 +- auth/upgrade.txt | 10 + blog/edit.php | 4 +- blog/edit_form.php | 8 +- blog/locallib.php | 14 +- enrol/guest/lib.php | 4 +- enrol/paypal/return.php | 3 - enrol/upgrade.txt | 8 + lib/accesslib.php | 6178 +++++++++++++--------- lib/datalib.php | 6 +- lib/db/install.php | 69 +- lib/deprecatedlib.php | 27 +- lib/enrollib.php | 10 +- lib/moodlelib.php | 14 +- lib/navigationlib.php | 2 +- lib/sessionlib.php | 12 +- lib/simpletest/fulltestaccesslib.php | 1172 ++++ lib/simpletest/testaccesslib.php | 382 -- lib/upgradelib.php | 10 +- login/token.php | 3 + rss/file.php | 10 +- webservice/lib.php | 1 + webservice/upload.php | 2 + 26 files changed, 4859 insertions(+), 3124 deletions(-) create mode 100644 auth/upgrade.txt create mode 100644 enrol/upgrade.txt create mode 100644 lib/simpletest/fulltestaccesslib.php delete mode 100644 lib/simpletest/testaccesslib.php diff --git a/admin/report/questioninstances/index.php b/admin/report/questioninstances/index.php index 2add057b6df..3719baf229d 100644 --- a/admin/report/questioninstances/index.php +++ b/admin/report/questioninstances/index.php @@ -65,13 +65,15 @@ if ($requestedqtype) { // Get the question counts, and all the context information, for each // context. That is, rows of these results can be used as $context objects. + $ctxpreload = context_helper::get_preload_record_columns_sql('con'); + $ctxgroupby = implode(',', array_keys(context_helper::get_preload_record_columns('con'))); $counts = $DB->get_records_sql(" - SELECT qc.contextid, count(1) as numquestions, sum(hidden) as numhidden, con.id, con.contextlevel, con.instanceid, con.path, con.depth + SELECT qc.contextid, count(1) as numquestions, sum(hidden) as numhidden, $ctxpreload FROM {question} q JOIN {question_categories} qc ON q.category = qc.id JOIN {context} con ON con.id = qc.contextid $sqlqtypetest - GROUP BY contextid, con.id, con.contextlevel, con.instanceid, con.path, con.depth + GROUP BY qc.contextid, $ctxgroupby ORDER BY numquestions DESC, numhidden ASC, con.contextlevel ASC, con.id ASC", $params); // Print the report heading. @@ -94,8 +96,10 @@ if ($requestedqtype) { $totalhidden = 0; foreach ($counts as $count) { // Work out a link for editing questions in this context. - $contextname = print_context_name($count); - $url = question_edit_url($count); + context_helper::preload_from_record($count); + $context = context::instance_by_id($count->contextid); + $contextname = $context->get_context_name(); + $url = question_edit_url($context); if ($url) { $contextname = 'wwwroot/$CFG->admin/roles/override.php?contextid=$contextid"; $title = get_string('changeoverrides', 'tool_capability'); } - echo '

', print_context_name($contexts[$contextid]), '

'; + $context = context::instance_by_id($contextid); + echo '

', $context->get_context_name(), '

'; // If there are any role overrides here, print them. if (!empty($contexts[$contextid]->rolecapabilities)) { diff --git a/auth/shibboleth/index.php b/auth/shibboleth/index.php index ea33b6cc8b8..7bc8f90c892 100644 --- a/auth/shibboleth/index.php +++ b/auth/shibboleth/index.php @@ -39,7 +39,9 @@ if ($shibbolethauth->user_login($frm->username, $frm->password)) { - $USER = authenticate_user_login($frm->username, $frm->password); + $user = authenticate_user_login($frm->username, $frm->password); + enrol_check_plugins($user); + session_set_user($user); $USER->loggedin = true; $USER->site = $CFG->wwwroot; // for added security, store the site in the @@ -75,9 +77,6 @@ } } - enrol_check_plugins($USER); - load_all_capabilities(); /// This is what lets the user do anything on the site :-) - redirect($urltogo); exit; diff --git a/auth/upgrade.txt b/auth/upgrade.txt new file mode 100644 index 00000000000..d3e1761a0cd --- /dev/null +++ b/auth/upgrade.txt @@ -0,0 +1,10 @@ +This files describes API changes in /auth/* - plugins, +information provided here is intended especially for developers. + +=== 2.2 === + +required changes in code: +* the correct sequence to set up global $USER is: + $user = get_complete_user_data('username', $username); // or $user = authenticate_user_login() + enrol_check_plugins($user); + session_set_user($user); diff --git a/blog/edit.php b/blog/edit.php index ba5bdfccf90..1ba7ac496dc 100644 --- a/blog/edit.php +++ b/blog/edit.php @@ -143,9 +143,9 @@ if (!empty($entry->id)) { if ($CFG->useblogassociations && ($blogassociations = $DB->get_records('blog_association', array('blogid' => $entry->id)))) { foreach ($blogassociations as $assocrec) { - $contextrec = $DB->get_record('context', array('id' => $assocrec->contextid)); + $context = get_context_instance_by_id($assocrec->contextid); - switch ($contextrec->contextlevel) { + switch ($context->contextlevel) { case CONTEXT_COURSE: $entry->courseassoc = $assocrec->contextid; break; diff --git a/blog/edit_form.php b/blog/edit_form.php index cbfd4aed4d8..04c67e5b4cc 100644 --- a/blog/edit_form.php +++ b/blog/edit_form.php @@ -95,7 +95,7 @@ class blog_edit_form extends moodleform { $a->modname = $mod->name; $context = get_context_instance(CONTEXT_MODULE, $modid); } else { - $context = $DB->get_record('context', array('id' => $entry->modassoc)); + $context = get_context_instance_by_id($entry->modassoc); $cm = $DB->get_record('course_modules', array('id' => $context->instanceid)); $a = new stdClass(); $a->modtype = $DB->get_field('modules', 'name', array('id' => $cm->module)); @@ -134,7 +134,7 @@ class blog_edit_form extends moodleform { // validate course association if (!empty($data['courseassoc']) && has_capability('moodle/blog:associatecourse', $sitecontext)) { - $coursecontext = $DB->get_record('context', array('id' => $data['courseassoc'], 'contextlevel' => CONTEXT_COURSE)); + $coursecontext = get_context_instance(CONTEXT_COURSE, $data['courseassoc']); if ($coursecontext) { if (!is_enrolled($coursecontext) and !is_viewing($coursecontext)) { @@ -149,12 +149,12 @@ class blog_edit_form extends moodleform { if (!empty($data['modassoc'])) { $modcontextid = $data['modassoc']; - $modcontext = $DB->get_record('context', array('id' => $modcontextid, 'contextlevel' => CONTEXT_MODULE)); + $modcontext = get_context_instance(CONTEXT_MODULE, $modcontextid); if ($modcontext) { // get context of the mod's course $path = explode('/', $modcontext->path); - $coursecontext = $DB->get_record('context', array('id' => $path[(count($path) - 2)])); + $coursecontext = get_context_instance_by_id($path[(count($path) - 2)]); // ensure only one course is associated if (!empty($data['courseassoc'])) { diff --git a/blog/locallib.php b/blog/locallib.php index 51afba8b1f6..19b01ca322c 100644 --- a/blog/locallib.php +++ b/blog/locallib.php @@ -247,10 +247,10 @@ class blog_entry { // First find and show the associated course foreach ($blogassociations as $assocrec) { - $contextrec = $DB->get_record('context', array('id' => $assocrec->contextid)); - if ($contextrec->contextlevel == CONTEXT_COURSE) { - $assocurl = new moodle_url('/course/view.php', array('id' => $contextrec->instanceid)); - $text = $DB->get_field('course', 'shortname', array('id' => $contextrec->instanceid)); //TODO: performance!!!! + $context = get_context_instance_by_id($assocrec->contextid); + if ($context->contextlevel == CONTEXT_COURSE) { + $assocurl = new moodle_url('/course/view.php', array('id' => $context->instanceid)); + $text = $DB->get_field('course', 'shortname', array('id' => $context->instanceid)); //TODO: performance!!!! $assocstr .= $OUTPUT->action_icon($assocurl, new pix_icon('i/course', $text), null, array(), true); $hascourseassocs = true; $assoctype = get_string('course'); @@ -259,15 +259,15 @@ class blog_entry { // Now show mod association foreach ($blogassociations as $assocrec) { - $contextrec = $DB->get_record('context', array('id' => $assocrec->contextid)); + $context = get_context_instance_by_id($assocrec->contextid); - if ($contextrec->contextlevel == CONTEXT_MODULE) { + if ($context->contextlevel == CONTEXT_MODULE) { if ($hascourseassocs) { $assocstr .= ', '; $hascourseassocs = false; } - $modinfo = $DB->get_record('course_modules', array('id' => $contextrec->instanceid)); + $modinfo = $DB->get_record('course_modules', array('id' => $context->instanceid)); $modname = $DB->get_field('modules', 'name', array('id' => $modinfo->module)); $assocurl = new moodle_url('/mod/'.$modname.'/view.php', array('id' => $modinfo->id)); diff --git a/enrol/guest/lib.php b/enrol/guest/lib.php index 570539c6bc0..39516e14ba0 100644 --- a/enrol/guest/lib.php +++ b/enrol/guest/lib.php @@ -77,7 +77,7 @@ class enrol_guest_plugin extends enrol_plugin { if (empty($instance->password)) { // Temporarily assign them some guest role for this context $context = get_context_instance(CONTEXT_COURSE, $instance->courseid); - $USER->access = load_temp_role($context, $CFG->guestroleid, $USER->access); + load_temp_course_role($context, $CFG->guestroleid); return ENROL_REQUIRE_LOGIN_CACHE_PERIOD + time(); } @@ -131,7 +131,7 @@ class enrol_guest_plugin extends enrol_plugin { // add guest role $context = get_context_instance(CONTEXT_COURSE, $instance->courseid); - $USER->access = load_temp_role($context, $CFG->guestroleid, $USER->access); + load_temp_course_role($context, $CFG->guestroleid); // go to the originally requested page if (!empty($SESSION->wantsurl)) { diff --git a/enrol/paypal/return.php b/enrol/paypal/return.php index 89cb2d0069c..a8a65570c4c 100644 --- a/enrol/paypal/return.php +++ b/enrol/paypal/return.php @@ -38,9 +38,6 @@ $context = get_context_instance(CONTEXT_COURSE, $course->id, MUST_EXIST); require_login(); -// Refreshing enrolment data in the USER session -load_all_capabilities(); - if ($SESSION->wantsurl) { $destination = $SESSION->wantsurl; unset($SESSION->wantsurl); diff --git a/enrol/upgrade.txt b/enrol/upgrade.txt new file mode 100644 index 00000000000..4fb4c4996b0 --- /dev/null +++ b/enrol/upgrade.txt @@ -0,0 +1,8 @@ +This files describes API changes in /enrol/* - plugins, +information provided here is intended especially for developers. + +=== 2.2 === + +required changes in code: +* load_temp_role() is deprecated, use load_temp_course_role() instead, temp role not loaded +* remove_temp_role() is deprecated, use remove_temp_course_roles() instead diff --git a/lib/accesslib.php b/lib/accesslib.php index a2b50e23799..21ba4b40753 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -1,5 +1,4 @@ id), context_coursecat::instance($catid) + * - context::instance_by_id($contextid) + * - $context->get_parent_contexts(); + * - $context->get_child_contexts(); * * Whether the user can do something... * - has_capability() @@ -34,26 +33,28 @@ * - has_all_capabilities() * - require_capability() * - require_login() (from moodlelib) + * - is_siteadmin() * * What courses has this user access to? - * - get_user_courses_bycap() + * - get_enrolled_users() * * What users can do X in this context? * - get_users_by_capability() * - * Enrol/unenrol - * - enrol_into_course() - * - role_assign()/role_unassign() + * Modify roles + * - role_assign() + * - role_unassign() + * - role_unassign_all() * * - * Advanced use + * Advanced - for internal use only * - load_all_capabilities() * - reload_all_capabilities() * - has_capability_in_accessdata() - * - is_siteadmin() * - get_user_access_sitewide() - * - load_subcontext() - * - get_role_access_bycontext() + * - load_course_context() + * - load_role_access_by_context() + * - etc. * * Name conventions * @@ -65,7 +66,7 @@ * 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. + * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser). * * $accessdata is a multidimensional array, holding * role assignments (RAs), role-capabilities-perm sets @@ -75,29 +76,27 @@ * 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) + * $accessdata['ra'][$contextpath] = array($roleid=>$roleid) + * [$contextpath] = array($roleid=>$roleid) + * [$contextpath] = array($roleid=>$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 + * $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. + * See how has_capability_in_accessdata() walks up 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 + * First we only load rdef and ra down to the course level, but not below. + * This keeps accessdata small and compact. Below-the-course ra/rdef + * are loaded as needed. We keep track of which courses we have loaded ra/rdef in * - * $accessdata[loaded] = array($contextpath, $contextpath) + * $accessdata['loaded'] = array($courseid1=>1, $courseid2=>1) * * * Stale accessdata @@ -126,172 +125,61 @@ defined('MOODLE_INTERNAL') || die(); -/** permission definitions */ +/** No capability change */ define('CAP_INHERIT', 0); -/** permission definitions */ +/** Allow permission, overrides CAP_PREVENT defined in parent contexts */ define('CAP_ALLOW', 1); -/** permission definitions */ +/** Prevent permission, overrides CAP_ALLOW defined in parent contexts */ define('CAP_PREVENT', -1); -/** permission definitions */ +/** Prohibit permission, overrides everything in current and child contexts */ define('CAP_PROHIBIT', -1000); -/** context definitions */ +/** System context level - only one instance in every system */ define('CONTEXT_SYSTEM', 10); -/** context definitions */ +/** User context level - one instance for each user describing what others can do to user */ define('CONTEXT_USER', 30); -/** context definitions */ +/** Course category context level - one instance for each category */ define('CONTEXT_COURSECAT', 40); -/** context definitions */ +/** Course context level - one instances for each course */ define('CONTEXT_COURSE', 50); -/** context definitions */ +/** Course module context level - one instance for each course module */ define('CONTEXT_MODULE', 70); -/** context definitions */ +/** + * Block context level - one instance for each block, sticky blocks are tricky + * because ppl think they should be able to override them at lower contexts. + * Any other context level instance can be parent of block context. + */ define('CONTEXT_BLOCK', 80); -/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ +/** Capability allow management of trusts - NOT IMPLEMENTED YET - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ define('RISK_MANAGETRUST', 0x0001); -/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ +/** Capability allows changes in system configuration - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ define('RISK_CONFIG', 0x0002); -/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ +/** Capability allows user to add scritped content - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ define('RISK_XSS', 0x0004); -/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ +/** Capability allows access to personal user information - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ define('RISK_PERSONAL', 0x0008); -/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ +/** Capability allows users to add content otehrs may see - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ define('RISK_SPAM', 0x0010); -/** capability risks - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ +/** capability allows mass delete of data belonging to other users - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */ define('RISK_DATALOSS', 0x0020); /** rolename displays - the name as defined in the role definition */ define('ROLENAME_ORIGINAL', 0); /** rolename displays - the name as defined by a role alias */ define('ROLENAME_ALIAS', 1); -/** rolename displays - Both, like this: Role alias (Original)*/ +/** rolename displays - Both, like this: Role alias (Original) */ define('ROLENAME_BOTH', 2); -/** rolename displays - the name as defined in the role definition and the shortname in brackets*/ +/** rolename displays - the name as defined in the role definition and the shortname in brackets */ define('ROLENAME_ORIGINALANDSHORT', 3); -/** rolename displays - the name as defined by a role alias, in raw form suitable for editing*/ +/** rolename displays - the name as defined by a role alias, in raw form suitable for editing */ define('ROLENAME_ALIAS_RAW', 4); -/** rolename displays - the name is simply short role name*/ +/** rolename displays - the name is simply short role name */ define('ROLENAME_SHORT', 5); -/** - * Internal class provides a cache of context information. The cache is - * restricted in size. - * - * This cache should NOT be used outside accesslib.php! - * - * @private - * @author Sam Marshall - */ -class context_cache { - private $contextsbyid; - private $contexts; - private $count; - - /** - * @var int Maximum number of contexts that will be cached. - */ - const MAX_SIZE = 2500; - /** - * @var int Once contexts reach maximum number, this many will be removed from cache. - */ - const REDUCE_SIZE = 1000; - - /** - * Initialises (empty) - */ - public function __construct() { - $this->reset(); - } - - /** - * Resets the cache to remove all data. - */ - public function reset() { - $this->contexts = array(); - $this->contextsbyid = array(); - $this->count = 0; - } - - /** - * Adds a context to the cache. If the cache is full, discards a batch of - * older entries. - * @param stdClass $context New context to add - */ - public function add(stdClass $context) { - if ($this->count >= self::MAX_SIZE) { - for ($i=0; $icontextsbyid)) { - unset($this->contextsbyid[$first->id]); - unset($this->contexts[$first->contextlevel][$first->instanceid]); - } - } - $this->count -= self::REDUCE_SIZE; - if ($this->count < 0) { - // most probably caused by the drift, the reset() above - // might have returned false because there might not be any more elements - $this->count = 0; - } - } - - $this->contexts[$context->contextlevel][$context->instanceid] = $context; - $this->contextsbyid[$context->id] = $context; - - // Note the count may get out of synch slightly if you cache a context - // that is already cached, but it doesn't really matter much and I - // didn't think it was worth the performance hit. - $this->count++; - } - - /** - * Removes a context from the cache. - * @param stdClass $context Context object to remove (must include fields - * ->id, ->contextlevel, ->instanceid at least) - */ - public function remove(stdClass $context) { - unset($this->contexts[$context->contextlevel][$context->instanceid]); - unset($this->contextsbyid[$context->id]); - - // Again the count may get a bit out of synch if you remove things - // that don't exist - $this->count--; - - if ($this->count < 0) { - $this->count = 0; - } - } - - /** - * Gets a context from the cache. - * @param int $contextlevel Context level - * @param int $instance Instance ID - * @return stdClass|bool Context or false if not in cache - */ - public function get($contextlevel, $instance) { - if (isset($this->contexts[$contextlevel][$instance])) { - return $this->contexts[$contextlevel][$instance]; - } - return false; - } - - /** - * Gets a context from the cache based on its id. - * @param int $id Context ID - * @return stdClass|bool Context or false if not in cache - */ - public function get_by_id($id) { - if (isset($this->contextsbyid[$id])) { - return $this->contextsbyid[$id]; - } - return false; - } - - /** - * @return int Count of contexts in cache (approximately) - */ - public function get_approx_count() { - return $this->count; - } +/** maximum size of context cache - it is possible to tweak this config.php or in any script before inclusion of context.php */ +if (!defined('CONTEXT_CACHE_MAX_SIZE')) { + define('CONTEXT_CACHE_MAX_SIZE', 2500); } /** @@ -302,19 +190,16 @@ class context_cache { * Sadly, a PHP global variable is the only way to implement this, without rewriting everything * as methods of a class, instead of functions. * + * @private * @global stdClass $ACCESSLIB_PRIVATE * @name $ACCESSLIB_PRIVATE */ global $ACCESSLIB_PRIVATE; $ACCESSLIB_PRIVATE = new stdClass(); -$ACCESSLIB_PRIVATE->contexcache = new context_cache(); -$ACCESSLIB_PRIVATE->systemcontext = null; // Used in get_system_context -$ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache -$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the $accessdata structure for users other than $USER -$ACCESSLIB_PRIVATE->roledefinitions = array(); // role definitions cache - helps a lot with mem usage in cron -$ACCESSLIB_PRIVATE->croncache = array(); // Used in get_role_access -$ACCESSLIB_PRIVATE->preloadedcourses = array(); // Used in preload_course_contexts. -$ACCESSLIB_PRIVATE->capabilities = null; // detailed information about the capabilities +$ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache, loaded from DB once per page +$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER) +$ACCESSLIB_PRIVATE->rolepermissions = array(); // role permissions cache - helps a lot with mem usage +$ACCESSLIB_PRIVATE->capabilities = null; // detailed information about the capabilities /** * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS @@ -322,88 +207,62 @@ $ACCESSLIB_PRIVATE->capabilities = null; // detailed information about the c * This method should ONLY BE USED BY UNIT TESTS. It clears all of * accesslib's private caches. You need to do this before setting up test data, * and also at the end of the tests. + * + * @return void */ function accesslib_clear_all_caches_for_unit_testing() { - global $UNITTEST, $USER, $ACCESSLIB_PRIVATE; + global $UNITTEST, $USER; if (empty($UNITTEST->running)) { throw new coding_exception('You must not call clear_all_caches outside of unit tests.'); } - $ACCESSLIB_PRIVATE->contexcache = new context_cache(); - $ACCESSLIB_PRIVATE->systemcontext = null; - $ACCESSLIB_PRIVATE->dirtycontexts = null; - $ACCESSLIB_PRIVATE->accessdatabyuser = array(); - $ACCESSLIB_PRIVATE->roledefinitions = array(); - $ACCESSLIB_PRIVATE->croncache = array(); - $ACCESSLIB_PRIVATE->preloadedcourses = array(); - $ACCESSLIB_PRIVATE->capabilities = null; + + accesslib_clear_all_caches(true); unset($USER->access); } /** - * This is really slow!!! do not use above course context level + * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE! * - * @param int $roleid - * @param object $context - * @return array + * This reset does not touch global $USER. + * + * @private + * @param bool $resetcontexts + * @return void */ -function get_role_context_caps($roleid, $context) { - global $DB; +function accesslib_clear_all_caches($resetcontexts) { + global $ACCESSLIB_PRIVATE; - //this is really slow!!!! - do not use above course context level! - $result = array(); - $result[$context->id] = array(); + $ACCESSLIB_PRIVATE->dirtycontexts = null; + $ACCESSLIB_PRIVATE->accessdatabyuser = array(); + $ACCESSLIB_PRIVATE->rolepermissions = array(); + $ACCESSLIB_PRIVATE->capabilities = null; - // 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; - } - } + if ($resetcontexts) { + context_helper::reset_caches(); } - - // 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) * + * @private * @param int $roleid - * @param array $accessdata defaults to null * @return array */ -function get_role_access($roleid, $accessdata = null) { - global $CFG, $DB; +function get_role_access($roleid) { + global $DB, $ACCESSLIB_PRIVATE; - /* Get it in 1 cheap DB query... + /* Get it in 1 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(); - } + + //TODO: MUC - this could be cached in shared memory to speed up first page loading, web crawlers, etc. + + $accessdata = get_empty_accessdata(); + + $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid); // // Overrides for the role IN ANY CONTEXTS @@ -412,92 +271,37 @@ function get_role_access($roleid, $accessdata = null) { $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"; + JOIN {role_capabilities} rc ON rc.contextid = ctx.id + LEFT JOIN {context} cctx + ON (cctx.contextlevel = ".CONTEXT_COURSE." AND ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'").") + WHERE rc.roleid = ? AND cctx.id IS NULL"; $params = array($roleid); // we need extra caching in CLI scripts and cron - if (CLI_SCRIPT) { - global $ACCESSLIB_PRIVATE; - - if (!isset($ACCESSLIB_PRIVATE->croncache[$roleid])) { - $ACCESSLIB_PRIVATE->croncache[$roleid] = array(); - $rs = $DB->get_recordset_sql($sql, $params); - foreach ($rs as $rd) { - $ACCESSLIB_PRIVATE->croncache[$roleid][] = $rd; - } - $rs->close(); - } - - foreach ($ACCESSLIB_PRIVATE->croncache[$roleid] as $rd) { - $k = "{$rd->path}:{$roleid}"; - $accessdata['rdef'][$k][$rd->capability] = $rd->permission; - } - - } else { - $rs = $DB->get_recordset_sql($sql, $params); - if ($rs->valid()) { - foreach ($rs as $rd) { - $k = "{$rd->path}:{$roleid}"; - $accessdata['rdef'][$k][$rd->capability] = $rd->permission; - } - unset($rd); - } - $rs->close(); - } - - return $accessdata; -} - -/** - * Gets the accessdata for role "sitewide" (system down to course) - * - * @param int $roleid - * @param array $accessdata defaults to null - * @return array - */ -function get_default_frontpage_role_access($roleid, $accessdata = null) { - - global $CFG, $DB; - - $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID); - $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id; - - // - // Overrides for the role in any contexts related to the course - // - $sql = "SELECT ctx.path, - rc.capability, rc.permission - FROM {context} ctx - JOIN {role_capabilities} rc - ON rc.contextid=ctx.id - WHERE rc.roleid = ? - AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?) - AND ctx.contextlevel <= ".CONTEXT_COURSE." - ORDER BY ctx.depth, ctx.path"; - $params = array($roleid, "$base/%"); - $rs = $DB->get_recordset_sql($sql, $params); - if ($rs->valid()) { - foreach ($rs as $rd) { - $k = "{$rd->path}:{$roleid}"; - $accessdata['rdef'][$k][$rd->capability] = $rd->permission; - } - unset($rd); + foreach ($rs as $rd) { + $k = "{$rd->path}:{$roleid}"; + $accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission; } $rs->close(); + // share the role definitions + foreach ($accessdata['rdef'] as $k=>$unused) { + if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) { + $ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k]; + } + $accessdata['rdef_count']++; + $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k]; + } + return $accessdata; } - /** - * Get the default guest role + * Get the default guest role, this is used for guest account, + * search engine spiders, etc. * - * @return stdClass role + * @return stdClass role record */ function get_guest_role() { global $CFG, $DB; @@ -515,7 +319,7 @@ function get_guest_role() { 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 + // somebody is messing with guest roles, remove incorrect setting and try to find a new one set_config('guestroleid', ''); return get_guest_role(); } @@ -525,7 +329,7 @@ function get_guest_role() { /** * Check whether a user has a particular capability in a given context. * - * For example:: + * For example: * $context = get_context_instance(CONTEXT_MODULE, $cm->id); * has_capability('mod/forum:replypost',$context) * @@ -536,16 +340,16 @@ function get_guest_role() { * or capabilities with XSS, config or data loss risks. * * @param string $capability the name of the capability to check. For example mod/forum:view - * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}. + * @param context $context the context to check the capability in. You normally get this with {@link get_context_instance}. * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user. * @param boolean $doanything If false, ignores effect of admin role assignment * @return boolean true if the user has this capability. Otherwise false. */ -function has_capability($capability, $context, $user = null, $doanything = true) { - global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE; +function has_capability($capability, context $context, $user = null, $doanything = true) { + global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE; if (during_initial_install()) { - if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") { + if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cli/install.php" or $SCRIPT === "/$CFG->admin/cli/install_database.php") { // we are in an installer - roles can not work yet return true; } else { @@ -557,90 +361,64 @@ function has_capability($capability, $context, $user = null, $doanything = true) throw new coding_exception('Legacy capabilities can not be used any more!'); } - // the original $CONTEXT here was hiding serious errors - // for security reasons do not reuse previous context - if (empty($context)) { - debugging('Incorrect context specified'); - return false; - } if (!is_bool($doanything)) { - throw new coding_exception('Capability parameter "doanything" is wierd ("'.$doanything.'"). This has to be fixed in code.'); - } - - // make sure there is a real user specified - if ($user === null) { - $userid = isset($USER->id) ? $USER->id : 0; - } else { - $userid = is_object($user) ? $user->id : $user; + throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.'); } // capability must exist if (!$capinfo = get_capability_info($capability)) { - debugging('Capability "'.$capability.'" was not found! This should be fixed in code.'); + debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.'); return false; } + + if (!isset($USER->id)) { + // should never happen + $USER->id = 0; + } + + // make sure there is a real user specified + if ($user === null) { + $userid = $USER->id; + } else { + $userid = is_object($user) ? $user->id : $user; + } + + // make sure forcelogin cuts off not-logged-in users if enabled + if (!empty($CFG->forcelogin) and $userid == 0) { + return false; + } + // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are. - if (($capinfo->captype === 'write') or ((int)$capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) { + if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) { if (isguestuser($userid) or $userid == 0) { return false; } } - if (is_null($context->path) or $context->depth == 0) { - //this should not happen - $contexts = array(SYSCONTEXTID, $context->id); - $context->path = '/'.SYSCONTEXTID.'/'.$context->id; - debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER); - - } else { - $contexts = explode('/', $context->path); - array_shift($contexts); - } - - if (CLI_SCRIPT && !isset($USER->access)) { - // In cron, some modules setup a 'fake' $USER, - // ensure we load the appropriate accessdata. - if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { - $ACCESSLIB_PRIVATE->dirtycontexts = null; //load fresh dirty contexts + // somehow make sure the user is not deleted and actually exists + if ($userid != 0) { + if ($userid == $USER->id and isset($USER->deleted)) { + // this prevents one query per page, it is a bit of cheating, + // but hopefully session is terminated properly once user is deleted + if ($USER->deleted) { + return false; + } } else { - load_user_accessdata($userid); - $ACCESSLIB_PRIVATE->dirtycontexts = array(); - } - $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]; - - } else if (isset($USER->id) && ($USER->id == $userid) && !isset($USER->access)) { - // caps not loaded yet - better to load them to keep BC with 1.8 - // not-logged-in user or $USER object set up manually first time here - load_all_capabilities(); - $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now - $ACCESSLIB_PRIVATE->roledefinitions = array(); - } - - // Load dirty contexts list if needed - if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) { - if (isset($USER->access['time'])) { - $ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']); - } - else { - $ACCESSLIB_PRIVATE->dirtycontexts = array(); + if (!context_user::instance($userid, IGNORE_MISSING)) { + // no user context == invalid userid + return false; + } } } - // 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(); - + // context path/depth must be valid + if (empty($context->path) or $context->depth == 0) { + // this should not happen often, each upgrade tries to rebuild the context paths + debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()'); + if (is_siteadmin($userid)) { + return true; } else { - reload_all_capabilities(); + return false; } } @@ -672,49 +450,29 @@ function has_capability($capability, $context, $user = null, $doanything = true) } } - // divulge how many times we are called - //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability"); + // Careful check for staleness... + $context->reload_if_dirty(); - if (isset($USER->id) && ($USER->id == $userid)) { // we must accept strings and integers in $userid - // - // For the logged in user, we have $USER->access - // which will have all RAs and caps preloaded for - // course and above contexts. - // - // Contexts below courses && contexts that do not - // hang from courses are loaded into $USER->access - // on demand, and listed in $USER->access[loaded] - // - if ($context->contextlevel <= CONTEXT_COURSE) { - // Course and above are always preloaded - return has_capability_in_accessdata($capability, $context, $USER->access); + if ($USER->id == $userid) { + if (!isset($USER->access)) { + load_all_capabilities(); } - // 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); + $access =& $USER->access; + + } else { + // make sure user accessdata is really loaded + get_user_accessdata($userid, true); + $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]; } - if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { - load_user_accessdata($userid); + + // Load accessdata for below-the-course context if necessary, + // all contexts at and above all courses are already loaded + if ($context->contextlevel != CONTEXT_COURSE and $coursecontext = $context->get_course_context(false)) { + load_course_context($userid, $coursecontext, $access); } - if ($context->contextlevel <= CONTEXT_COURSE) { - // Course and above are always preloaded - return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]); - } - // Load accessdata for below-the-course contexts as needed - if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { - // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}"); - // $bt = debug_backtrace(); - // error_log("bt {$bt[0]['file']} {$bt[0]['line']}"); - load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]); - } - return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]); + return has_capability_in_accessdata($capability, $context, $access); } /** @@ -724,21 +482,14 @@ function has_capability($capability, $context, $user = null, $doanything = true) * the capabilities that most users are likely to have first in the list for best * performance. * - * There are probably tricks that could be done to improve the performance here, for example, - * check the capabilities that are already cached first. - * * @see has_capability() * @param array $capabilities an array of capability names. - * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}. + * @param context $context the context to check the capability in. You normally get this with {@link get_context_instance}. * @param integer $userid A user id. By default (null) checks the permissions of the current user. * @param boolean $doanything If false, ignore effect of admin role assignment * @return boolean true if the user has any of these capabilities. Otherwise false. */ -function has_any_capability($capabilities, $context, $userid = null, $doanything = true) { - if (!is_array($capabilities)) { - debugging('Incorrect $capabilities parameter in has_any_capabilities() call - must be an array'); - return false; - } +function has_any_capability(array $capabilities, context $context, $userid = null, $doanything = true) { foreach ($capabilities as $capability) { if (has_capability($capability, $context, $userid, $doanything)) { return true; @@ -754,21 +505,14 @@ function has_any_capability($capabilities, $context, $userid = null, $doanything * the capabilities that fewest users are likely to have first in the list for best * performance. * - * There are probably tricks that could be done to improve the performance here, for example, - * check the capabilities that are already cached first. - * * @see has_capability() * @param array $capabilities an array of capability names. - * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}. + * @param context $context the context to check the capability in. You normally get this with {@link get_context_instance}. * @param integer $userid A user id. By default (null) checks the permissions of the current user. * @param boolean $doanything If false, ignore effect of admin role assignment * @return boolean true if the user has all of these capabilities. Otherwise false. */ -function has_all_capabilities($capabilities, $context, $userid = null, $doanything = true) { - if (!is_array($capabilities)) { - debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array'); - return false; - } +function has_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true) { foreach ($capabilities as $capability) { if (!has_capability($capability, $context, $userid, $doanything)) { return false; @@ -783,8 +527,8 @@ function has_all_capabilities($capabilities, $context, $userid = null, $doanythi * Please note that use of proper capabilities is always encouraged, * this function is supposed to be used from core or for temporary hacks. * - * @param int|object $user_or_id user id or user object - * @returns bool true if user is one of the administrators, false otherwise + * @param int|stdClass $user_or_id user id or user object + * @return bool true if user is one of the administrators, false otherwise */ function is_siteadmin($user_or_id = null) { global $CFG, $USER; @@ -797,7 +541,6 @@ function is_siteadmin($user_or_id = null) { return false; } if (!empty($user_or_id->id)) { - // we support $userid = $user_or_id->id; } else { $userid = $user_or_id; @@ -811,8 +554,8 @@ function is_siteadmin($user_or_id = null) { * Returns true if user has at least one role assign * of 'coursecontact' role (is potentially listed in some course descriptions). * - * @param $userid - * @return stdClass + * @param int $userid + * @return bool */ function has_coursecontact_role($userid) { global $DB, $CFG; @@ -826,52 +569,11 @@ function has_coursecontact_role($userid) { return $DB->record_exists_sql($sql, array('userid'=>$userid)); } -/** - * @param string $path - * @return string - */ -function get_course_from_path($path) { - // assume that nothing is more than 1 course deep - if (preg_match('!^(/.+)/\d+$!', $path, $matches)) { - return $matches[1]; - } - return false; -} - -/** - * @param string $path - * @param array $accessdata - * @return bool - */ -function path_inaccessdata($path, $accessdata) { - if (empty($accessdata['loaded'])) { - return false; - } - - // assume that contexts hang from sys or from a course - // this will only work well with stuff that hangs from a course - if (in_array($path, $accessdata['loaded'], true)) { - // error_log("found it!"); - return true; - } - $base = '/' . SYSCONTEXTID; - while (preg_match('!^(/.+)/\d+$!', $path, $matches)) { - $path = $matches[1]; - if ($path === $base) { - return false; - } - if (in_array($path, $accessdata['loaded'], true)) { - return true; - } - } - return false; -} - /** * Does the user have a capability to do something? * * Walk the accessdata array and return true/false. - * Deals with prohibits, roleswitching, aggregating + * Deals with prohibits, role switching, aggregating * capabilities, etc. * * The main feature of here is being FAST and with no @@ -879,12 +581,6 @@ function path_inaccessdata($path, $accessdata) { * * 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 @@ -901,26 +597,25 @@ function path_inaccessdata($path, $accessdata) { * and then verify if user has at least one role with allow * and at the same time no role with prohibit. * + * @private * @param string $capability - * @param object $context + * @param context $context * @param array $accessdata * @return bool */ -function has_capability_in_accessdata($capability, $context, array $accessdata) { +function has_capability_in_accessdata($capability, context $context, array &$accessdata) { global $CFG; - if (empty($context->id)) { - throw new coding_exception('Invalid context specified'); - } - // Build $paths as a list of current + all parent "paths" with order bottom-to-top - $contextids = explode('/', trim($context->path, '/')); - $paths = array($context->path); - while ($contextids) { - array_pop($contextids); - $paths[] = '/' . implode('/', $contextids); + $path = $context->path; + $paths = array($path); + while($path = rtrim($path, '0123456789')) { + $path = rtrim($path, '/'); + if ($path === '') { + break; + } + $paths[] = $path; } - unset($contextids); $roles = array(); $switchedrole = false; @@ -950,56 +645,25 @@ function has_capability_in_accessdata($capability, $context, array $accessdata) } // Now find out what access is given to each role, going bottom-->up direction + $allowed = false; foreach ($roles as $roleid => $ignored) { foreach ($paths as $path) { if (isset($accessdata['rdef']["{$path}:$roleid"][$capability])) { $perm = (int)$accessdata['rdef']["{$path}:$roleid"][$capability]; - if ($perm === CAP_PROHIBIT or is_null($roles[$roleid])) { + if ($perm === CAP_PROHIBIT) { + // any CAP_PROHIBIT found means no permission for the user + return false; + } + if (is_null($roles[$roleid])) { $roles[$roleid] = $perm; } } } - } - // any CAP_PROHIBIT found means no permission for the user - if (array_search(CAP_PROHIBIT, $roles) !== false) { - return false; + // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits + $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW); } - // at least one CAP_ALLOW means the user has a permission - return (array_search(CAP_ALLOW, $roles) !== false); -} - -/** - * @param object $context - * @param array $accessdata - * @return array - */ -function aggregate_roles_from_accessdata($context, $accessdata) { - - $path = $context->path; - - // build $contexts as a list of "paths" of the current - // contexts and parents with the order top-to-bottom - $contexts = array($path); - while (preg_match('!^(/.+)/\d+$!', $path, $matches)) { - $path = $matches[1]; - array_unshift($contexts, $path); - } - - $cc = count($contexts); - - $roles = array(); - // From the bottom up... - for ($n=$cc-1; $n>=0; $n--) { - $ctxp = $contexts[$n]; - if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) { - // Found assignments on this leaf - $addroles = $accessdata['ra'][$ctxp]; - $roles = array_merge($roles, $addroles); - } - } - - return array_unique($roles); + return $allowed; } /** @@ -1014,343 +678,269 @@ function aggregate_roles_from_accessdata($context, $accessdata) { * @see has_capability() * * @param string $capability the name of the capability to check. For example mod/forum:view - * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}. - * @param integer $userid A user id. By default (null) checks the permissions of the current user. + * @param context $context the context to check the capability in. You normally get this with {@link get_context_instance}. + * @param int $userid A user id. By default (null) checks the permissions of the current user. * @param bool $doanything If false, ignore effect of admin role assignment - * @param string $errorstring The error string to to user. Defaults to 'nopermissions'. + * @param string $errormessage The error string to to user. Defaults to 'nopermissions'. * @param string $stringfile The language file to load the error string from. Defaults to 'error'. * @return void terminates with an error if the user does not have the given capability. */ -function require_capability($capability, $context, $userid = null, $doanything = true, +function require_capability($capability, context $context, $userid = null, $doanything = true, $errormessage = 'nopermissions', $stringfile = '') { if (!has_capability($capability, $context, $userid, $doanything)) { throw new required_capability_exception($context, $capability, $errormessage, $stringfile); } } -/** - * Get an array of courses where cap requested is available - * and user is enrolled, this can be relatively slow. - * - * @param string $capability - name of the capability - * @param array $accessdata_ignored - * @param bool $doanything_ignored - * @param string $sort - sorting fields - prefix each fieldname with "c." - * @param array $fields - additional fields you are interested in... - * @param int $limit_ignored - * @return array $courses - ordered array of course objects - see notes above - */ -function get_user_courses_bycap($userid, $cap, $accessdata_ignored, $doanything_ignored, $sort = 'c.sortorder ASC', $fields = null, $limit_ignored = 0) { - - //TODO: this should be most probably deprecated - - $courses = enrol_get_users_courses($userid, true, $fields, $sort); - foreach ($courses as $id=>$course) { - $context = get_context_instance(CONTEXT_COURSE, $id); - if (!has_capability($cap, $context, $userid)) { - unset($courses[$id]); - } - } - - return $courses; -} - - /** * Return a nested array showing role assignments * all relevant role capabilities for the user at * site/course_category/course levels * * We do _not_ delve deeper than courses because the number of - * overrides at the module/block levels is HUGE. + * overrides at the module/block levels can be HUGE. * - * [ra] => [/path/][]=roleid - * [rdef] => [/path/:roleid][capability]=permission - * [loaded] => array('/path', '/path') + * [ra] => [/path][roleid]=roleid + * [rdef] => [/path:roleid][capability]=permission * + * @private * @param int $userid - the id of the user - * @return array + * @return array access info array */ function get_user_access_sitewide($userid) { - global $CFG, $DB; + global $CFG, $DB, $ACCESSLIB_PRIVATE; - /* Get in 3 cheap DB queries... + /* Get in a few cheap DB queries... * - role assignments * - relevant role caps * - above and within this user's RAs * - below this user's RAs - limited to course level */ - $accessdata = array(); // named list - $accessdata['ra'] = array(); - $accessdata['rdef'] = array(); - $accessdata['loaded'] = array(); + // raparents collects paths & roles we need to walk up the parenthood to build the minimal rdef + $raparents = array(); + $accessdata = get_empty_accessdata(); - // - // Role assignments - // + // start with the default role + if (!empty($CFG->defaultuserroleid)) { + $syscontext = context_system::instance(); + $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid; + $raparents[$CFG->defaultuserroleid][$syscontext->path] = $syscontext->path; + } + + // load the "default frontpage role" + if (!empty($CFG->defaultfrontpageroleid)) { + $frontpagecontext = context_course::instance(get_site()->id); + if ($frontpagecontext->path) { + $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid; + $raparents[$CFG->defaultfrontpageroleid][$frontpagecontext->path] = $frontpagecontext->path; + } + } + + // preload every assigned role at and above course context $sql = "SELECT ctx.path, ra.roleid FROM {role_assignments} ra - JOIN {context} ctx ON ctx.id=ra.contextid - WHERE ra.userid = ? AND ctx.contextlevel <= ".CONTEXT_COURSE; - $params = array($userid); + JOIN {context} ctx ON ctx.id = ra.contextid + LEFT JOIN {context} cctx + ON (cctx.contextlevel = ".CONTEXT_COURSE." AND ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'").") + WHERE ra.userid = :userid AND cctx.id IS NULL"; + + + $params = array('userid'=>$userid); $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $ra) { + // RAs leafs are arrays to support multi-role assignments... + $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid; + $raparents[$ra->roleid][$ra->path] = $ra->path; + } + $rs->close(); - // - // raparents collects paths & roles we need to walk up - // the parenthood to build the rdef - // - $raparents = array(); - if ($rs) { - foreach ($rs as $ra) { - // RAs leafs are arrays to support multi - // role assignments... - if (!isset($accessdata['ra'][$ra->path])) { - $accessdata['ra'][$ra->path] = array(); - } - $accessdata['ra'][$ra->path][$ra->roleid] = $ra->roleid; - - // Concatenate as string the whole path (all related context) - // for this role. This is damn faster than using array_merge() - // Will unique them later - if (isset($raparents[$ra->roleid])) { - $raparents[$ra->roleid] .= $ra->path; - } else { - $raparents[$ra->roleid] = $ra->path; - } - } - unset($ra); - $rs->close(); + if (empty($raparents)) { + return $accessdata; } - // Walk up the tree to grab all the roledefs - // of interest to our user... - // - // NOTE: we use a series of IN clauses here - which - // might explode on huge sites with very convoluted nesting of - // categories... - extremely unlikely that the number of categories - // and roletypes is so large that we hit the limits of IN() - $clauses = ''; - $cparams = array(); - foreach ($raparents as $roleid=>$strcontexts) { - $contexts = implode(',', array_unique(explode('/', trim($strcontexts, '/')))); - if ($contexts ==! '') { - if ($clauses) { - $clauses .= ' OR '; - } - $clauses .= "(roleid=? AND contextid IN ($contexts))"; - $cparams[] = $roleid; - } + // now get overrides of interesting roles in all interesting child contexts + // hopefully we will not run out of SQL limits here, + // users would have to have very many roles above course context... + $sqls = array(); + $params = array(); + + static $cp = 0; + foreach ($raparents as $roleid=>$paths) { + $cp++; + list($paths, $rparams) = $DB->get_in_or_equal($paths, SQL_PARAMS_NAMED, 'p'.$cp.'_'); + $params = array_merge($params, $rparams); + $params['r'.$cp] = $roleid; + $sqls[] = "(SELECT ctx.path, rc.roleid, rc.capability, rc.permission + FROM {role_capabilities} rc + JOIN {context} ctx + ON (ctx.id = rc.contextid) + LEFT JOIN {context} cctx + ON (cctx.contextlevel = ".CONTEXT_COURSE." + AND ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'").") + JOIN {context} pctx + ON (pctx.path $paths + AND (ctx.id = pctx.id + OR ctx.path LIKE ".$DB->sql_concat('pctx.path',"'/%'")." + OR pctx.path LIKE ".$DB->sql_concat('ctx.path',"'/%'").")) + WHERE rc.roleid = :r{$cp} + AND cctx.id IS NULL)"; } - 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"; + // fixed capability order is necessary for rdef dedupe + $rs = $DB->get_recordset_sql(implode("\nUNION\n", $sqls). "ORDER BY capability", $params); - unset($clauses); - $rs = $DB->get_recordset_sql($sql, $cparams); + foreach ($rs as $rd) { + $k = $rd->path.':'.$rd->roleid; + $accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission; + } + $rs->close(); - if ($rs) { - foreach ($rs as $rd) { - $k = "{$rd->path}:{$rd->roleid}"; - $accessdata['rdef'][$k][$rd->capability] = $rd->permission; - } - unset($rd); - $rs->close(); + // share the role definitions + foreach ($accessdata['rdef'] as $k=>$unused) { + if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) { + $ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k]; } + $accessdata['rdef_count']++; + $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k]; } - // - // Overrides for the role assignments IN SUBCONTEXTS - // (though we still do _not_ go below the course level. - // - // NOTE that the JOIN w sctx is with 3-way triangulation to - // catch overrides to the applicable role in any subcontext, based - // on the path field of the parent. - // - $sql = "SELECT sctx.path, ra.roleid, - ctx.path AS parentpath, - rco.capability, rco.permission - FROM {role_assignments} ra - JOIN {context} ctx - ON ra.contextid=ctx.id - JOIN {context} sctx - ON (sctx.path LIKE " . $DB->sql_concat('ctx.path',"'/%'"). " ) - JOIN {role_capabilities} rco - ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id) - WHERE ra.userid = ? - AND ctx.contextlevel <= ".CONTEXT_COURSECAT." - AND sctx.contextlevel <= ".CONTEXT_COURSE." - ORDER BY sctx.depth, sctx.path, ra.roleid"; - $params = array($userid); - $rs = $DB->get_recordset_sql($sql, $params); - if ($rs) { - foreach ($rs as $rd) { - $k = "{$rd->path}:{$rd->roleid}"; - $accessdata['rdef'][$k][$rd->capability] = $rd->permission; - } - unset($rd); - $rs->close(); - } return $accessdata; } /** - * Add to the access ctrl array the data needed by a user for a given context + * Add to the access ctrl array the data needed by a user for a given course. * - * @param integer $userid the id of the user - * @param object $context needs path! - * @param array $accessdata accessdata array - * @return void + * This function injects all course related access info into the accessdata array. + * + * @private + * @param int $userid the id of the user + * @param context_course $coursecontext course context + * @param array $accessdata accessdata array (modified) + * @return void modifies $accessdata parameter */ -function load_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); +function load_course_context($userid, context_course $coursecontext, &$accessdata) { + global $DB, $CFG, $ACCESSLIB_PRIVATE; + if (empty($coursecontext->path)) { + // weird, this should not happen + return; } - // - // 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); + if (isset($accessdata['loaded'][$coursecontext->instanceid])) { + // already loaded, great! + return; + } - // - // Read in the RAs, preventing duplicates - // - if ($rs) { - $localroles = array(); - $lastseen = ''; + $roles = array(); + + if (empty($userid)) { + if (!empty($CFG->notloggedinroleid)) { + $roles[$CFG->notloggedinroleid] = $CFG->notloggedinroleid; + } + + } else if (isguestuser($userid)) { + if ($guestrole = get_guest_role()) { + $roles[$guestrole->id] = $guestrole->id; + } + + } else { + // Interesting role assignments at, above and below the course context + list($parentsaself, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_'); + $params['userid'] = $userid; + $params['children'] = $coursecontext->path."/%"; + $sql = "SELECT ra.*, ctx.path + FROM {role_assignments} ra + JOIN {context} ctx ON ra.contextid = ctx.id + WHERE ra.userid = :userid AND (ctx.id $parentsaself OR ctx.path LIKE :children)"; + $rs = $DB->get_recordset_sql($sql, $params); + + // add missing role definitions foreach ($rs as $ra) { - if (!isset($accessdata['ra'][$ra->path])) { - $accessdata['ra'][$ra->path] = array(); - } - // only add if is not a repeat caused - // by capability join... - // (this check is cheaper than in_array()) - if ($lastseen !== $ra->path.':'.$ra->roleid) { - $lastseen = $ra->path.':'.$ra->roleid; - $accessdata['ra'][$ra->path][$ra->roleid] = $ra->roleid; - array_push($localroles, $ra->roleid); - } + $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid; + $roles[$ra->roleid] = $ra->roleid; } $rs->close(); + + // add the "default frontpage role" when on the frontpage + if (!empty($CFG->defaultfrontpageroleid)) { + $frontpagecontext = context_course::instance(get_site()->id); + if ($frontpagecontext->id == $coursecontext->id) { + $roles[$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid; + } + } + + // do not forget the default role + if (!empty($CFG->defaultuserroleid)) { + $roles[$CFG->defaultuserroleid] = $CFG->defaultuserroleid; + } } - // - // 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))" ; + if (!$roles) { + // weird, default roles must be missing... + $accessdata['loaded'][$coursecontext->instanceid] = 1; + return; } - // We will want overrides for all of them - $whereroles = ''; - if ($roleids = implode(',',array_merge($courseroles,$localroles))) { - $whereroles = "rc.roleid IN ($roleids) AND"; - } + // now get overrides of interesting roles in all interesting contexts (this course + children + parents) + $params = array('c'=>$coursecontext->id); + list($parentsaself, $rparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_'); + $params = array_merge($params, $rparams); + list($roleids, $rparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'r_'); + $params = array_merge($params, $rparams); + $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission - FROM {role_capabilities} rc - JOIN {context} ctx - ON 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."/%"); + FROM {role_capabilities} rc + JOIN {context} ctx + ON (ctx.id = rc.contextid) + JOIN {context} cctx + ON (cctx.id = :c + AND (ctx.id $parentsaself OR ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'").")) + WHERE rc.roleid $roleids + ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe + $rs = $DB->get_recordset_sql($sql, $params); $newrdefs = array(); - $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(); + $k = $rd->path.':'.$rd->roleid; + if (isset($accessdata['rdef'][$k])) { + continue; } - $newrdefs[$k][$rd->capability] = $rd->permission; + $newrdefs[$k][$rd->capability] = (int)$rd->permission; } $rs->close(); - compact_rdefs($newrdefs); - foreach ($newrdefs as $key=>$value) { - $accessdata['rdef'][$key] =& $newrdefs[$key]; + // share new role definitions + foreach ($newrdefs as $k=>$unused) { + if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) { + $ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k]; + } + $accessdata['rdef_count']++; + $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k]; } - // error_log("loaded {$context->path}"); - $accessdata['loaded'][] = $context->path; + $accessdata['loaded'][$coursecontext->instanceid] = 1; + + // we want to deduplicate the USER->access from time to time, this looks like a good place, + // because we have to do it before the end of session + dedupe_user_access(); } /** * Add to the access ctrl array the data needed by a role for a given context. * * The data is added in the rdef key. - * * This role-centric function is useful for role_switching - * and to get an overview of what a role gets under a - * given context and below... + * and temporary course roles. * - * @param integer $roleid the id of the user - * @param object $context needs path! - * @param array $accessdata accessdata array null by default + * @private + * @param int $roleid the id of the user + * @param context $context needs path! + * @param array $accessdata accessdata array (is modified) * @return array */ -function get_role_access_bycontext($roleid, $context, $accessdata = null) { - global $CFG, $DB; +function load_role_access_by_context($roleid, context $context, &$accessdata) { + global $DB, $ACCESSLIB_PRIVATE; /* Get the relevant rolecaps into rdef * - relevant role caps @@ -1358,306 +948,277 @@ function get_role_access_bycontext($roleid, $context, $accessdata = null) { * - below this ctx */ - if (is_null($accessdata)) { - $accessdata = array(); // named list - $accessdata['ra'] = array(); - $accessdata['rdef'] = array(); - $accessdata['loaded'] = array(); + if (empty($context->path)) { + // weird, this should not happen + return; } - $contexts = substr($context->path, 1); // kill leading slash - $contexts = str_replace('/', ',', $contexts); + list($parentsaself, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_'); + $params['roleid'] = $roleid; + $params['childpath'] = $context->path.'/%'; - // - // 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."/%"); - + JOIN {context} ctx ON (rc.contextid = ctx.id) + WHERE rc.roleid = :roleid AND (ctx.id $parentsaself OR ctx.path LIKE :childpath) + ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe $rs = $DB->get_recordset_sql($sql, $params); + + $newrdefs = array(); foreach ($rs as $rd) { - $k = "{$rd->path}:{$roleid}"; - $accessdata['rdef'][$k][$rd->capability] = $rd->permission; + $k = $rd->path.':'.$roleid; + if (isset($accessdata['rdef'][$k])) { + continue; + } + $newrdefs[$k][$rd->capability] = (int)$rd->permission; } $rs->close(); + // share new role definitions + foreach ($newrdefs as $k=>$unused) { + if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) { + $ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k]; + } + $accessdata['rdef_count']++; + $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k]; + } +} + +/** + * Returns empty accessdata structure. + * @return array empt accessdata + */ +function get_empty_accessdata() { + $accessdata = array(); // named list + $accessdata['ra'] = array(); + $accessdata['rdef'] = array(); + $accessdata['rdef_count'] = 0; // this bloody hack is necessary because count($array) is slooooowwww in PHP + $accessdata['rdef_lcc'] = 0; // rdef_count during the last compression + $accessdata['loaded'] = array(); // loaded course contexts + $accessdata['time'] = time(); + 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. + * Get accessdata for a given user. * + * @private * @param int $userid - * @return array returns ACCESSLIB_PRIVATE->accessdatabyuser[userid] + * @param bool $preloadonly true means do not return access array + * @return array accessdata */ -function load_user_accessdata($userid) { - global $CFG, $ACCESSLIB_PRIVATE; +function get_user_accessdata($userid, $preloadonly=false) { + global $CFG, $ACCESSLIB_PRIVATE, $USER; - $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(); + if (!empty($USER->acces['rdef']) and empty($ACCESSLIB_PRIVATE->rolepermissions)) { + // share rdef from USER session with rolepermissions cache in order to conserve memory + foreach($USER->acces['rdef'] as $k=>$v) { + $ACCESSLIB_PRIVATE->rolepermissions[$k] =& $USER->acces['rdef'][$k]; } - $accessdata['ra'][$base][$CFG->defaultuserroleid] = $CFG->defaultuserroleid; - $accessdata['dr'] = $CFG->defaultuserroleid; + $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->acces; } - // - // 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(); + if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { + if (empty($userid)) { + if (!empty($CFG->notloggedinroleid)) { + $accessdata = get_role_access($CFG->notloggedinroleid); + } else { + // weird + return get_empty_accessdata(); + } + + } else if (isguestuser($userid)) { + if ($guestrole = get_guest_role()) { + $accessdata = get_role_access($guestrole->id); + } else { + //weird + return get_empty_accessdata(); + } + + } else { + $accessdata = get_user_access_sitewide($userid); // includes default role and frontpage role } - $accessdata['ra'][$base][$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid; + + $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata; } - // 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]; + if ($preloadonly) { + return; + } else { + return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]; + } } /** - * Use shared copy of role definitions stored in ACCESSLIB_PRIVATE->roledefinitions; + * Try to minimise the size of $USER->access by eliminating duplicate override storage, + * this function looks for contexts with the same overrides and shares them. * - * @param array $rdefs array of role definitions in contexts + * @private + * @return void */ -function compact_rdefs(&$rdefs) { - global $ACCESSLIB_PRIVATE; +function dedupe_user_access() { + global $USER; - /* - * 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]; + if (CLI_SCRIPT) { + // no session in CLI --> no compression necessary + return; } + + if (empty($USER->access['rdef_count'])) { + // weird, this should not happen + return; + } + + // the rdef is growing only, we never remove stuff from it, the rdef_lcc helps us to detect new stuff in rdef + if ($USER->access['rdef_count'] - $USER->access['rdef_lcc'] > 10) { + // do not compress after each change, wait till there is more stuff to be done + return; + } + + $hashmap = array(); + foreach ($USER->access['rdef'] as $k=>$def) { + $hash = sha1(serialize($def)); + if (isset($hashmap[$hash])) { + $USER->access['rdef'][$k] =& $hashmap[$hash]; + } else { + $hashmap[$hash] =& $USER->access['rdef'][$k]; + } + } + + $USER->access['rdef_lcc'] = $USER->access['rdef_count']; } /** * A convenience function to completely load all the capabilities - * for the current user. This is what gets called from complete_user_login() - * for example. Call it only _after_ you've setup $USER and called - * check_enrolment_plugins(); + * for the current user. It is called from has_capability() and functions change permissions. + * + * Call it only _after_ you've setup $USER and called check_enrolment_plugins(); * @see check_enrolment_plugins() * + * @private * @return void */ function load_all_capabilities() { - global $CFG, $ACCESSLIB_PRIVATE; - - //NOTE: we can not use $USER here because it may no be linked to $_SESSION['USER'] yet! + global $USER; // roles not installed yet - we are in the middle of installation if (during_initial_install()) { return; } - $base = '/'.SYSCONTEXTID; - - if (isguestuser($_SESSION['USER'])) { - $guest = get_guest_role(); - - // Load the rdefs - $_SESSION['USER']->access = get_role_access($guest->id); - // Put the ghost enrolment in place... - $_SESSION['USER']->access['ra'][$base] = array($guest->id => $guest->id); - - - } else if (!empty($_SESSION['USER']->id)) { // can not use isloggedin() yet - - $accessdata = get_user_access_sitewide($_SESSION['USER']->id); - - // - // provide "default role" & set 'dr' - // - if (!empty($CFG->defaultuserroleid)) { - $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata); - if (!isset($accessdata['ra'][$base])) { - $accessdata['ra'][$base] = array(); - } - $accessdata['ra'][$base][$CFG->defaultuserroleid] = $CFG->defaultuserroleid; - $accessdata['dr'] = $CFG->defaultuserroleid; - } - - $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID); - - // - // provide "default frontpage role" - // - if (!empty($CFG->defaultfrontpageroleid)) { - $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id; - $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata); - if (!isset($accessdata['ra'][$base])) { - $accessdata['ra'][$base] = array(); - } - $accessdata['ra'][$base][$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid; - } - $_SESSION['USER']->access = $accessdata; - - } else if (!empty($CFG->notloggedinroleid)) { - $_SESSION['USER']->access = get_role_access($CFG->notloggedinroleid); - $_SESSION['USER']->access['ra'][$base] = array($CFG->notloggedinroleid => $CFG->notloggedinroleid); + if (!isset($USER->id)) { + // this should not happen + $USER->id = 0; } - // Timestamp to read dirty context timestamps later - $_SESSION['USER']->access['time'] = time(); - $ACCESSLIB_PRIVATE->dirtycontexts = array(); + unset($USER->access); + $USER->access = get_user_accessdata($USER->id); + + // deduplicate the overrides to minimize session size + dedupe_user_access(); // Clear to force a refresh - unset($_SESSION['USER']->mycourses); + unset($USER->mycourses); + unset($USER->enrol); } /** * A convenience function to completely reload all the capabilities * for the current user when roles have been updated in a relevant * context -- but PRESERVING switchroles and loginas. + * This function resets all accesslib and context caches. * * That is - completely transparent to the user. * - * Note: rewrites $USER->access completely. + * Note: reloads $USER->access completely. * + * @private * @return void */ function reload_all_capabilities() { - global $USER, $DB; + global $USER, $DB, $ACCESSLIB_PRIVATE; - // error_log("reloading"); // copy switchroles $sw = array(); if (isset($USER->access['rsw'])) { $sw = $USER->access['rsw']; - // error_log(print_r($sw,1)); } + accesslib_clear_all_caches(true); unset($USER->access); - unset($USER->mycourses); + $ACCESSLIB_PRIVATE->dirtycontexts = array(); // prevent dirty flags refetching on this page load_all_capabilities(); foreach ($sw as $path => $roleid) { - $context = $DB->get_record('context', array('path'=>$path)); - role_switch($roleid, $context); + if ($record = $DB->get_record('context', array('path'=>$path))) { + $context = context::instance_by_id($record->id); + role_switch($roleid, $context); + } } - } /** - * Adds a temp role to an accessdata array. + * Adds a temp role to current USER->access array. * - * Useful for the "temporary guest" access - * we grant to logged-in users. + * Useful for the "temporary guest" access we grant to logged-in users. * - * Note - assumes a course context! - * - * @param object $content + * @param context_course $coursecontext * @param int $roleid - * @param array $accessdata - * @return array Returns access data + * @return void */ -function load_temp_role($context, $roleid, array $accessdata) { - global $CFG, $DB; +function load_temp_course_role(context_course $coursecontext, $roleid) { + global $USER; - // - // Load rdefs for the role in - - // - this context - // - all the parents - // - and below - IOWs overrides... - // + //TODO: this gets removed if there are any dirty contexts, we should probably store list of these temp roles somewhere (skodak) - // turn the path into a list of context ids - $contexts = substr($context->path, 1); // kill leading slash - $contexts = str_replace('/', ',', $contexts); - - $sql = "SELECT ctx.path, rc.capability, rc.permission - FROM {context} ctx - JOIN {role_capabilities} rc - ON rc.contextid=ctx.id - WHERE (ctx.id IN ($contexts) - OR ctx.path LIKE ?) - AND rc.roleid = ? - ORDER BY ctx.depth, ctx.path"; - $params = array($context->path."/%", $roleid); - $rs = $DB->get_recordset_sql($sql, $params); - foreach ($rs as $rd) { - $k = "{$rd->path}:{$roleid}"; - $accessdata['rdef'][$k][$rd->capability] = $rd->permission; + if (empty($roleid)) { + debugging('invalid role specified in load_temp_course_role()'); + return; } - $rs->close(); - // - // Say we loaded everything for the course context - // - which we just did - if the user gets a proper - // RA in this session, this data will need to be reloaded, - // but that is handled by the complete accessdata reload - // - array_push($accessdata['loaded'], $context->path); - - // - // Add the ghost RA - // - if (!isset($accessdata['ra'][$context->path])) { - $accessdata['ra'][$context->path] = array(); + if (!isset($USER->access)) { + load_all_capabilities(); } - $accessdata['ra'][$context->path][$roleid] = $roleid; - return $accessdata; + $coursecontext->reload_if_dirty(); + + if (isset($USER->access['ra'][$coursecontext->path][$roleid])) { + return; + } + + // load course stuff first + load_course_context($USER->id, $coursecontext, $USER->access); + + $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid; + + load_role_access_by_context($roleid, $coursecontext, $USER->access); } /** - * Removes any extra guest roles from accessdata - * @param object $context - * @param array $accessdata - * @return array access data + * Removes any extra guest roles from current USER->access array. + * + * @param context_course $coursecontext + * @return void */ -function remove_temp_roles($context, array $accessdata) { +function remove_temp_course_roles(context_course $coursecontext) { global $DB, $USER; + + if (empty($USER->access['ra'][$coursecontext->path])) { + //no roles here, weird + return; + } + $sql = "SELECT DISTINCT ra.roleid AS id FROM {role_assignments} ra WHERE ra.contextid = :contextid AND ra.userid = :userid"; - $ras = $DB->get_records_sql($sql, array('contextid'=>$context->id, 'userid'=>$USER->id)); + $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id)); - if ($ras) { - $accessdata['ra'][$context->path] = array_combine(array_keys($ras), array_keys($ras)); - } else { - $accessdata['ra'][$context->path] = array(); + $USER->access['ra'][$coursecontext->path] = array(); + foreach($ras as $r) { + $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id; } - - return $accessdata; } /** @@ -1698,7 +1259,7 @@ function assign_legacy_capabilities($capability, $legacyperms) { foreach ($legacyperms as $type => $perm) { - $systemcontext = get_context_instance(CONTEXT_SYSTEM); + $systemcontext = context_system::instance(); if ($type === 'admin') { debugging('Legacy type admin in access.php was renamed to manager, please update the code.'); $type = 'manager'; @@ -1721,6 +1282,8 @@ function assign_legacy_capabilities($capability, $legacyperms) { } /** + * Verify capability risks. + * * @param object $capability a capability - a row from the capabilities table. * @return boolean whether this capability is safe - that is, whether people with the * safeoverrides capability should be allowed to change it. @@ -1729,582 +1292,13 @@ function is_safe_capability($capability) { return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask); } -/********************************** - * Context Manipulation functions * - **********************************/ - -/** - * Context creation - internal implementation. - * - * Create a new context record for use by all roles-related stuff - * assumes that the caller has done the homework. - * - * DO NOT CALL THIS DIRECTLY, instead use {@link get_context_instance}! - * - * @param int $contextlevel - * @param int $instanceid - * @param int $strictness - * @return object newly created context - */ -function create_context($contextlevel, $instanceid, $strictness = IGNORE_MISSING) { - global $CFG, $DB; - - if ($contextlevel == CONTEXT_SYSTEM) { - return get_system_context(); - } - - $context = new stdClass(); - $context->contextlevel = $contextlevel; - $context->instanceid = $instanceid; - - // Define $context->path based on the parent - // context. In other words... Who is your daddy? - $basepath = '/' . SYSCONTEXTID; - $basedepth = 1; - - $result = true; - $error_message = null; - - switch ($contextlevel) { - case CONTEXT_COURSECAT: - $sql = "SELECT ctx.path, ctx.depth - FROM {context} ctx - JOIN {course_categories} cc - ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.") - WHERE cc.id=?"; - $params = array($instanceid); - if ($p = $DB->get_record_sql($sql, $params)) { - $basepath = $p->path; - $basedepth = $p->depth; - } else if ($category = $DB->get_record('course_categories', array('id'=>$instanceid), '*', $strictness)) { - if (empty($category->parent)) { - // ok - this is a top category - } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) { - $basepath = $parent->path; - $basedepth = $parent->depth; - } else { - // wrong parent category - no big deal, this can be fixed later - $basepath = null; - $basedepth = 0; - } - } else { - // incorrect category id - $error_message = "incorrect course category id ($instanceid)"; - $result = false; - } - break; - - case CONTEXT_COURSE: - $sql = "SELECT ctx.path, ctx.depth - FROM {context} ctx - JOIN {course} c - ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.") - WHERE c.id=? AND c.id !=" . SITEID; - $params = array($instanceid); - if ($p = $DB->get_record_sql($sql, $params)) { - $basepath = $p->path; - $basedepth = $p->depth; - } else if ($course = $DB->get_record('course', array('id'=>$instanceid), '*', $strictness)) { - if ($course->id == SITEID) { - //ok - no parent category - } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) { - $basepath = $parent->path; - $basedepth = $parent->depth; - } else { - // wrong parent category of course - no big deal, this can be fixed later - $basepath = null; - $basedepth = 0; - } - } else if ($instanceid == SITEID) { - // no errors for missing site course during installation - return false; - } else { - // incorrect course id - $error_message = "incorrect course id ($instanceid)"; - $result = false; - } - break; - - case CONTEXT_MODULE: - $sql = "SELECT ctx.path, ctx.depth - FROM {context} ctx - JOIN {course_modules} cm - ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.") - WHERE cm.id=?"; - $params = array($instanceid); - if ($p = $DB->get_record_sql($sql, $params)) { - $basepath = $p->path; - $basedepth = $p->depth; - } else if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid), '*', $strictness)) { - if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course, $strictness)) { - $basepath = $parent->path; - $basedepth = $parent->depth; - } else { - // course does not exist - modules can not exist without a course - $error_message = "course does not exist ($cm->course) - modules can not exist without a course"; - $result = false; - } - } else { - // cm does not exist - $error_message = "cm with id $instanceid does not exist"; - $result = false; - } - break; - - case CONTEXT_BLOCK: - $sql = "SELECT ctx.path, ctx.depth - FROM {context} ctx - JOIN {block_instances} bi ON (bi.parentcontextid=ctx.id) - WHERE bi.id = ?"; - $params = array($instanceid, CONTEXT_COURSE); - if ($p = $DB->get_record_sql($sql, $params, '*', $strictness)) { - $basepath = $p->path; - $basedepth = $p->depth; - } else { - // block does not exist - $error_message = 'block or parent context does not exist'; - $result = false; - } - break; - case CONTEXT_USER: - // default to basepath - break; - } - - // if grandparents unknown, maybe rebuild_context_path() will solve it later - if ($basedepth != 0) { - $context->depth = $basedepth+1; - } - - if (!$result) { - debugging('Error: could not insert new context level "'. - s($contextlevel).'", instance "'. - s($instanceid).'". ' . $error_message); - - return false; - } - - $id = $DB->insert_record('context', $context); - // can't set the full path till we know the id! - if ($basedepth != 0 and !empty($basepath)) { - $DB->set_field('context', 'path', $basepath.'/'. $id, array('id'=>$id)); - } - return get_context_instance_by_id($id); -} - -/** - * Returns system context or null if can not be created yet. - * - * @param bool $cache use caching - * @return mixed system context or null - */ -function get_system_context($cache = true) { - global $DB, $ACCESSLIB_PRIVATE; - if ($cache and defined('SYSCONTEXTID')) { - if (is_null($ACCESSLIB_PRIVATE->systemcontext)) { - $ACCESSLIB_PRIVATE->systemcontext = new stdClass(); - $ACCESSLIB_PRIVATE->systemcontext->id = SYSCONTEXTID; - $ACCESSLIB_PRIVATE->systemcontext->contextlevel = CONTEXT_SYSTEM; - $ACCESSLIB_PRIVATE->systemcontext->instanceid = 0; - $ACCESSLIB_PRIVATE->systemcontext->path = '/'.SYSCONTEXTID; - $ACCESSLIB_PRIVATE->systemcontext->depth = 1; - } - return $ACCESSLIB_PRIVATE->systemcontext; - } - try { - $context = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM)); - } catch (dml_exception $e) { - //table does not exist yet, sorry - return null; - } - - if (!$context) { - $context = new stdClass(); - $context->contextlevel = CONTEXT_SYSTEM; - $context->instanceid = 0; - $context->depth = 1; - $context->path = null; //not known before insert - - try { - $context->id = $DB->insert_record('context', $context); - } catch (dml_exception $e) { - // can not create context yet, sorry - return null; - } - } - - if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) { - $context->instanceid = 0; - $context->path = '/'.$context->id; - $context->depth = 1; - $DB->update_record('context', $context); - } - - if (!defined('SYSCONTEXTID')) { - define('SYSCONTEXTID', $context->id); - } - - $ACCESSLIB_PRIVATE->systemcontext = $context; - return $ACCESSLIB_PRIVATE->systemcontext; -} - -/** - * Remove a context record and any dependent entries, - * removes context from static context cache too - * - * @param int $level - * @param int $instanceid - * @param bool $deleterecord false means keep record for now - * @return bool returns true or throws an exception - */ -function delete_context($contextlevel, $instanceid, $deleterecord = true) { - global $DB, $ACCESSLIB_PRIVATE, $CFG; - - // do not use get_context_instance(), because the related object might not exist, - // or the context does not exist yet and it would be created now - if ($context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) { - // delete these first because they might fetch the context and try to recreate it! - blocks_delete_all_for_context($context->id); - filter_delete_all_for_context($context->id); - - require_once($CFG->dirroot . '/comment/lib.php'); - comment::delete_comments(array('contextid'=>$context->id)); - - require_once($CFG->dirroot.'/rating/lib.php'); - $delopt = new stdclass(); - $delopt->contextid = $context->id; - $rm = new rating_manager(); - $rm->delete_ratings($delopt); - - // delete all files attached to this context - $fs = get_file_storage(); - $fs->delete_area_files($context->id); - - // now delete stuff from role related tables, role_unassign_all - // and unenrol should be called earlier to do proper cleanup - $DB->delete_records('role_assignments', array('contextid'=>$context->id)); - $DB->delete_records('role_capabilities', array('contextid'=>$context->id)); - $DB->delete_records('role_names', array('contextid'=>$context->id)); - - // and finally it is time to delete the context record if requested - if ($deleterecord) { - $DB->delete_records('context', array('id'=>$context->id)); - // purge static context cache if entry present - $ACCESSLIB_PRIVATE->contexcache->remove($context); - } - - // do not mark dirty contexts if parents unknown - if (!is_null($context->path) and $context->depth > 0) { - mark_context_dirty($context->path); - } - } - - return true; -} - -/** - * Precreates all contexts including all parents - * - * @param int $contextlevel empty means all - * @param bool $buildpaths update paths and depths - * @return void - */ -function create_contexts($contextlevel = null, $buildpaths = true) { - global $DB; - - //make sure system context exists - $syscontext = get_system_context(false); - - if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT - or $contextlevel == CONTEXT_COURSE - or $contextlevel == CONTEXT_MODULE - or $contextlevel == CONTEXT_BLOCK) { - $sql = "INSERT INTO {context} (contextlevel, instanceid) - SELECT ".CONTEXT_COURSECAT.", cc.id - FROM {course}_categories cc - WHERE NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")"; - $DB->execute($sql); - - } - - if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE - or $contextlevel == CONTEXT_MODULE - or $contextlevel == CONTEXT_BLOCK) { - $sql = "INSERT INTO {context} (contextlevel, instanceid) - SELECT ".CONTEXT_COURSE.", c.id - FROM {course} c - WHERE NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")"; - $DB->execute($sql); - - } - - if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE - or $contextlevel == CONTEXT_BLOCK) { - $sql = "INSERT INTO {context} (contextlevel, instanceid) - SELECT ".CONTEXT_MODULE.", cm.id - FROM {course}_modules cm - WHERE NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")"; - $DB->execute($sql); - } - - if (empty($contextlevel) or $contextlevel == CONTEXT_USER - or $contextlevel == CONTEXT_BLOCK) { - $sql = "INSERT INTO {context} (contextlevel, instanceid) - SELECT ".CONTEXT_USER.", u.id - FROM {user} u - WHERE u.deleted=0 - AND NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")"; - $DB->execute($sql); - - } - - if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) { - $sql = "INSERT INTO {context} (contextlevel, instanceid) - SELECT ".CONTEXT_BLOCK.", bi.id - FROM {block_instances} bi - WHERE NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")"; - $DB->execute($sql); - } - - if ($buildpaths) { - build_context_path(false); - } -} - -/** - * Remove stale context records - * - * @return bool - */ -function cleanup_contexts() { - global $DB; - - $sql = " SELECT c.contextlevel, - c.instanceid AS instanceid - FROM {context} c - LEFT OUTER JOIN {course}_categories t - ON c.instanceid = t.id - WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT." - UNION - SELECT c.contextlevel, - c.instanceid - FROM {context} c - LEFT OUTER JOIN {course} t - ON c.instanceid = t.id - WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE." - UNION - SELECT c.contextlevel, - c.instanceid - FROM {context} c - LEFT OUTER JOIN {course}_modules t - ON c.instanceid = t.id - WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE." - UNION - SELECT c.contextlevel, - c.instanceid - FROM {context} c - LEFT OUTER JOIN {user} t - ON c.instanceid = t.id - WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_USER." - UNION - SELECT c.contextlevel, - c.instanceid - FROM {context} c - LEFT OUTER JOIN {block_instances} t - ON c.instanceid = t.id - WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK." - "; - - // transactions used only for performance reasons here - $transaction = $DB->start_delegated_transaction(); - - $rs = $DB->get_recordset_sql($sql); - foreach ($rs as $ctx) { - delete_context($ctx->contextlevel, $ctx->instanceid); - } - $rs->close(); - - $transaction->allow_commit(); - return true; -} - -/** - * Preloads all contexts relating to a course: course, modules. Block contexts - * are no longer loaded here. The contexts for all the blocks on the current - * page are now efficiently loaded by {@link block_manager::load_blocks()}. - * - * @param int $courseid Course ID - * @return void - */ -function preload_course_contexts($courseid) { - global $DB, $ACCESSLIB_PRIVATE; - - // Users can call this multiple times without doing any harm - global $ACCESSLIB_PRIVATE; - if (array_key_exists($courseid, $ACCESSLIB_PRIVATE->preloadedcourses)) { - return; - } - - $params = array($courseid, $courseid, $courseid); - $sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth - FROM {course_modules} cm - JOIN {context} x ON x.instanceid=cm.id - WHERE cm.course=? AND x.contextlevel=".CONTEXT_MODULE." - - UNION ALL - - SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth - FROM {context} x - WHERE x.instanceid=? AND x.contextlevel=".CONTEXT_COURSE.""; - - $rs = $DB->get_recordset_sql($sql, $params); - foreach($rs as $context) { - $ACCESSLIB_PRIVATE->contexcache->add($context); - } - $rs->close(); - $ACCESSLIB_PRIVATE->preloadedcourses[$courseid] = true; -} - -/** - * Get the context instance as an object. This function will create the - * context instance if it does not exist yet. - * - * @todo Remove code branch from previous fix MDL-9016 which is no longer needed - * - * @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE. - * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id, - * for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0 - * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; - * MUST_EXIST means throw exception if no record or multiple records found - * @return object The context object. - */ -function get_context_instance($contextlevel, $instance = 0, $strictness = IGNORE_MISSING) { - global $DB, $ACCESSLIB_PRIVATE; - static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_BLOCK); - -/// System context has special cache - if ($contextlevel == CONTEXT_SYSTEM) { - return get_system_context(); - } - -/// check allowed context levels - if (!in_array($contextlevel, $allowed_contexts)) { - // fatal error, code must be fixed - probably typo or switched parameters - print_error('invalidcourselevel'); - } - - // Various operations rely on context cache - $cache = $ACCESSLIB_PRIVATE->contexcache; - - if (!is_array($instance)) { - /// Check the cache - $context = $cache->get($contextlevel, $instance); - if ($context) { - return $context; - } - - /// Get it from the database, or create it - if (!$context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instance))) { - $context = create_context($contextlevel, $instance, $strictness); - } - - /// Only add to cache if context isn't empty. - if (!empty($context)) { - $cache->add($context); - } - - return $context; - } - - -/// ok, somebody wants to load several contexts to save some db queries ;-) - $instances = $instance; - $result = array(); - - foreach ($instances as $key=>$instance) { - /// Check the cache first - if ($context = $cache->get($contextlevel, $instance)) { // Already cached - $result[$instance] = $context; - unset($instances[$key]); - continue; - } - } - - if ($instances) { - list($instanceids, $params) = $DB->get_in_or_equal($instances, SQL_PARAMS_QM); - array_unshift($params, $contextlevel); - $sql = "SELECT instanceid, id, contextlevel, path, depth - FROM {context} - WHERE contextlevel=? AND instanceid $instanceids"; - - if (!$contexts = $DB->get_records_sql($sql, $params)) { - $contexts = array(); - } - - foreach ($instances as $instance) { - if (isset($contexts[$instance])) { - $context = $contexts[$instance]; - } else { - $context = create_context($contextlevel, $instance); - } - - if (!empty($context)) { - $cache->add($context); - } - - $result[$instance] = $context; - } - } - - return $result; -} - - -/** - * Get a context instance as an object, from a given context id. - * - * @param int $id context id - * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; - * MUST_EXIST means throw exception if no record or multiple records found - * @return stdClass|bool the context object or false if not found. - */ -function get_context_instance_by_id($id, $strictness = IGNORE_MISSING) { - global $DB, $ACCESSLIB_PRIVATE; - - if ($id == SYSCONTEXTID) { - return get_system_context(); - } - - $cache = $ACCESSLIB_PRIVATE->contexcache; - if ($context = $cache->get_by_id($id)) { - return $context; - } - - if ($context = $DB->get_record('context', array('id'=>$id), '*', $strictness)) { - $cache->add($context); - return $context; - } - - return false; -} - - /** * Get the local override (if any) for a given capability in a role in a context * * @param int $roleid * @param int $contextid * @param string $capability + * @return stdClass local capability override */ function get_local_override($roleid, $contextid, $capability) { global $DB; @@ -2313,13 +1307,14 @@ function get_local_override($roleid, $contextid, $capability) { /** * Returns context instance plus related course and cm instances + * * @param int $contextid * @return array of ($context, $course, $cm) */ function get_context_info_array($contextid) { global $DB; - $context = get_context_instance_by_id($contextid, MUST_EXIST); + $context = context::instance_by_id($contextid, MUST_EXIST); $course = null; $cm = null; @@ -2331,9 +1326,7 @@ function get_context_info_array($contextid) { $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); } else if ($context->contextlevel == CONTEXT_BLOCK) { - $parentcontexts = get_parent_contexts($context, false); - $parent = reset($parentcontexts); - $parent = get_context_instance_by_id($parent); + $parent = $context->get_parent_context(); if ($parent->contextlevel == CONTEXT_COURSE) { $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST); @@ -2347,46 +1340,7 @@ function get_context_info_array($contextid) { } /** - * Returns current course id or null if outside of course based on context parameter. - * @param object $context - * @return int|bool related course id or false - */ -function get_courseid_from_context($context) { - if (empty($context->contextlevel)) { - debugging('Invalid context object specified in get_courseid_from_context() call'); - return false; - } - if ($context->contextlevel == CONTEXT_COURSE) { - return $context->instanceid; - } - - if ($context->contextlevel < CONTEXT_COURSE) { - return false; - } - - if ($context->contextlevel == CONTEXT_MODULE) { - $parentcontexts = get_parent_contexts($context, false); - $parent = reset($parentcontexts); - $parent = get_context_instance_by_id($parent); - return $parent->instanceid; - } - - if ($context->contextlevel == CONTEXT_BLOCK) { - $parentcontexts = get_parent_contexts($context, false); - $parent = reset($parentcontexts); - return get_courseid_from_context(get_context_instance_by_id($parent)); - } - - return false; -} - - -////////////////////////////////////// -// DB TABLE RELATED FUNCTIONS // -////////////////////////////////////// - -/** - * function that creates a role + * Function that creates a role * * @param string $name role name * @param string $shortname role short name @@ -2407,9 +1361,6 @@ function create_role($name, $shortname, $description, $archetype = '') { $archetype = ''; } - // Get the system context. - $context = get_context_instance(CONTEXT_SYSTEM); - // Insert the role record. $role = new stdClass(); $role->name = $name; @@ -2434,7 +1385,7 @@ function create_role($name, $shortname, $description, $archetype = '') { * @return bool always true */ function delete_role($roleid) { - global $CFG, $DB; + global $DB; // first unssign all users role_unassign_all(array('roleid'=>$roleid)); @@ -2462,29 +1413,37 @@ function delete_role($roleid) { /** * Function to write context specific overrides, or default capabilities. * + * NOTE: use $context->mark_dirty() after this + * * @param string $capability string name * @param int $permission CAP_ constants * @param int $roleid role id - * @param int $contextid context id + * @param int|context $contextid context id * @param bool $overwrite * @return bool always true or exception */ function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) { global $USER, $DB; + if ($contextid instanceof context) { + $context = $contextid; + } else { + $context = context::instance_by_id($contextid); + } + if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set - unassign_capability($capability, $roleid, $contextid); + unassign_capability($capability, $roleid, $context->id); return true; } - $existing = $DB->get_record('role_capabilities', array('contextid'=>$contextid, 'roleid'=>$roleid, 'capability'=>$capability)); + $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability)); if ($existing and !$overwrite) { // We want to keep whatever is there already return true; } $cap = new stdClass(); - $cap->contextid = $contextid; + $cap->contextid = $context->id; $cap->roleid = $roleid; $cap->capability = $capability; $cap->permission = $permission; @@ -2495,8 +1454,9 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw $cap->id = $existing->id; $DB->update_record('role_capabilities', $cap); } else { - $c = $DB->get_record('context', array('id'=>$contextid)); - $DB->insert_record('role_capabilities', $cap); + if ($DB->record_exists('context', array('id'=>$context->id))) { + $DB->insert_record('role_capabilities', $cap); + } } return true; } @@ -2504,24 +1464,30 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw /** * Unassign a capability from a role. * + * NOTE: use $context->mark_dirty() after this + * * @param string $capability the name of the capability * @param int $roleid the role id - * @param int $contextid null means all contexts - * @return boolean success or failure + * @param int|context $contextid null means all contexts + * @return boolean true or exception */ function unassign_capability($capability, $roleid, $contextid = null) { global $DB; if (!empty($contextid)) { + if ($contextid instanceof context) { + $context = $contextid; + } else { + $context = context::instance_by_id($contextid); + } // delete from context rel, if this is the last override in this context - $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$contextid)); + $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id)); } else { $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid)); } return true; } - /** * Get the roles that have a given capability assigned to it * @@ -2533,13 +1499,13 @@ function unassign_capability($capability, $roleid, $contextid = null) { * @param string $permission - optional, the permission defined for this capability * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any. * @param stdClass $context, null means any - * @return array of role objects + * @return array of role records */ function get_roles_with_capability($capability, $permission = null, $context = null) { global $DB; if ($context) { - $contexts = get_parent_contexts($context, true); + $contexts = $context->get_parent_context_ids(true); list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx'); $contextsql = "AND rc.contextid $insql"; } else { @@ -2573,14 +1539,14 @@ function get_roles_with_capability($capability, $permission = null, $context = n * * @param int $roleid the role of the id * @param int $userid userid - * @param int $contextid id of the context + * @param int|context $contextid id of the context * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment, - * @prama int $itemid id of enrolment/auth plugin + * @param int $itemid id of enrolment/auth plugin * @param string $timemodified defaults to current time * @return int new/existing id of the assignment */ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') { - global $USER, $CFG, $DB; + global $USER, $DB; // first of all detect if somebody is using old style parameters if ($contextid === 0 or is_numeric($component)) { @@ -2611,7 +1577,11 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid); } - $context = get_context_instance_by_id($contextid, MUST_EXIST); + if ($contextid instanceof context) { + $context = $contextid; + } else { + $context = context::instance_by_id($contextid, MUST_EXIST); + } if (!$timemodified) { $timemodified = time(); @@ -2623,7 +1593,7 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, if ($ras) { // role already assigned - this should not happen if (count($ras) > 1) { - //very weird - remove all duplicates! + // very weird - remove all duplicates! $ra = array_shift($ras); foreach ($ras as $r) { $DB->delete_records('role_assignments', array('id'=>$r->id)); @@ -2649,11 +1619,11 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $ra->id = $DB->insert_record('role_assignments', $ra); // mark context as dirty - again expensive, but needed - mark_context_dirty($context->path); + $context->mark_dirty(); if (!empty($USER->id) && $USER->id == $userid) { // If the user is the current user, then do full reload of capabilities too. - load_all_capabilities(); + reload_all_capabilities(); } events_trigger('role_assigned', $ra); @@ -2666,14 +1636,12 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, * * @param int $roleid * @param int $userid - * @param int $contextid + * @param int|context $contextid * @param string $component * @param int $itemid * @return void */ function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) { - global $USER, $CFG, $DB; - // first make sure the params make sense if ($roleid == 0 or $userid == 0 or $contextid == 0) { throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments'); @@ -2699,7 +1667,7 @@ function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = * * @param array $params role assignment parameters * @param bool $subcontexts unassign in subcontexts too - * @param bool $includmanual include manual role assignments too + * @param bool $includemanual include manual role assignments too * @return void */ function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) { @@ -2735,12 +1703,12 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual = $ras = $DB->get_records('role_assignments', $params); foreach($ras as $ra) { $DB->delete_records('role_assignments', array('id'=>$ra->id)); - if ($context = get_context_instance_by_id($ra->contextid)) { + if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) { // this is a bit expensive but necessary - mark_context_dirty($context->path); + $context->mark_dirty(); /// If the user is the current user, then do full reload of capabilities too. if (!empty($USER->id) && $USER->id == $ra->userid) { - load_all_capabilities(); + reload_all_capabilities(); } } events_trigger('role_unassigned', $ra); @@ -2748,21 +1716,29 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual = unset($ras); // process subcontexts - if ($subcontexts and $context = get_context_instance_by_id($params['contextid'])) { - $contexts = get_child_contexts($context); - $mparams = $params; - foreach($contexts as $context) { - $mparams['contextid'] = $context->id; - $ras = $DB->get_records('role_assignments', $mparams); - foreach($ras as $ra) { - $DB->delete_records('role_assignments', array('id'=>$ra->id)); - // this is a bit expensive but necessary - mark_context_dirty($context->path); - /// If the user is the current user, then do full reload of capabilities too. - if (!empty($USER->id) && $USER->id == $ra->userid) { - load_all_capabilities(); + if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) { + if ($params['contextid'] instanceof context) { + $context = $params['contextid']; + } else { + $context = context::instance_by_id($params['contextid'], IGNORE_MISSING); + } + + if ($context) { + $contexts = $context->get_child_contexts(); + $mparams = $params; + foreach($contexts as $context) { + $mparams['contextid'] = $context->id; + $ras = $DB->get_records('role_assignments', $mparams); + foreach($ras as $ra) { + $DB->delete_records('role_assignments', array('id'=>$ra->id)); + // this is a bit expensive but necessary + $context->mark_dirty(); + /// If the user is the current user, then do full reload of capabilities too. + if (!empty($USER->id) && $USER->id == $ra->userid) { + reload_all_capabilities(); + } + events_trigger('role_unassigned', $ra); } - events_trigger('role_unassigned', $ra); } } } @@ -2774,7 +1750,6 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual = } } - /** * Determines if a user is currently logged in * @@ -2829,15 +1804,15 @@ function isguestuser($user = null) { /** * Does user have a (temporary or real) guest access to course? * - * @param stdClass $context + * @param context $context * @param stdClass|int $user * @return bool */ -function is_guest($context, $user = null) { +function is_guest(context $context, $user = null) { global $USER; // first find the course context - $coursecontext = get_course_context($context); + $coursecontext = $context->get_course_context(); // make sure there is a real user specified if ($user === null) { @@ -2864,19 +1839,18 @@ function is_guest($context, $user = null) { return true; } - /** * Returns true if the user has moodle/course:view capability in the course, * this is intended for admins, managers (aka small admins), inspectors, etc. * - * @param stdClass $context - * @param int|object $user, if null $USER is used + * @param context $context + * @param int|stdClass $user, if null $USER is used * @param string $withcapability extra capability name * @return bool */ -function is_viewing($context, $user = null, $withcapability = '') { +function is_viewing(context $context, $user = null, $withcapability = '') { // first find the course context - $coursecontext = get_course_context($context); + $coursecontext = $context->get_course_context(); if (isguestuser($user)) { // can not inspect @@ -2900,17 +1874,17 @@ function is_viewing($context, $user = null, $withcapability = '') { * Returns true if user is enrolled (is participating) in course * this is intended for students and teachers. * - * @param object $context - * @param int|object $user, if null $USER is used, otherwise user object or id expected + * @param context $context + * @param int|stdClass $user, if null $USER is used, otherwise user object or id expected * @param string $withcapability extra capability name * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions * @return bool */ -function is_enrolled($context, $user = null, $withcapability = '', $onlyactive = false) { +function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) { global $USER, $DB; // first find the course context - $coursecontext = get_course_context($context); + $coursecontext = $context->get_course_context(); // make sure there is a real user specified if ($user === null) { @@ -2992,8 +1966,7 @@ function is_enrolled($context, $user = null, $withcapability = '', $onlyactive = * of this function can_access_course and require_login. Doing so WILL break future * versions. * - * @global moodle_database $DB - * @param stdClass $context + * @param context $context * @param stdClass|null $user * @param string $withcapability Check for this capability as well. * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions @@ -3002,10 +1975,10 @@ function is_enrolled($context, $user = null, $withcapability = '', $onlyactive = * than the cache generated by require_login. * @return boolean Returns true if the user is able to access the course */ -function can_access_course($context, $user = null, $withcapability = '', $onlyactive = false, $trustcache = true) { +function can_access_course(context $context, $user = null, $withcapability = '', $onlyactive = false, $trustcache = true) { global $DB, $USER; - $coursecontext = get_course_context($context); + $coursecontext = $context->get_course_context(); $courseid = $coursecontext->instanceid; // First check the obvious, is the user viewing or is the user enrolled. @@ -3070,13 +2043,13 @@ function can_access_course($context, $user = null, $withcapability = '', $onlyac * * This function is using 'eu[0-9]+_' prefix for table names and parameters. * - * @param object $context + * @param context $context * @param string $withcapability * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions * @return array list($sql, $params) */ -function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $onlyactive = false) { +function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) { global $DB, $CFG; // use unique prefix just in case somebody makes some SQL magic with the result @@ -3085,7 +2058,7 @@ function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $onlyact $prefix = 'eu'.$i.'_'; // first find the course context - $coursecontext = get_course_context($context); + $coursecontext = $context->get_course_context(); $isfrontpage = ($coursecontext->instanceid == SITEID); @@ -3223,16 +2196,17 @@ function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $onlyact $wheres = "WHERE ".implode(" AND ", $wheres); $sql = "SELECT DISTINCT {$prefix}u.id - FROM {user} {$prefix}u - $joins - $wheres"; + FROM {user} {$prefix}u + $joins + $wheres"; return array($sql, $params); } /** * Returns list of users enrolled into course. - * @param object $context + * + * @param context $context * @param string $withcapability * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @param string $userfields requested user record fields @@ -3241,7 +2215,7 @@ function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $onlyact * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). * @return array of user records */ -function get_enrolled_users($context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = '', $limitfrom = 0, $limitnum = 0) { +function get_enrolled_users(context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = '', $limitfrom = 0, $limitnum = 0) { global $DB; list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid); @@ -3261,12 +2235,13 @@ function get_enrolled_users($context, $withcapability = '', $groupid = 0, $userf /** * Counts list of users enrolled into course (as per above function) - * @param object $context + * + * @param context $context * @param string $withcapability * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @return array of user records */ -function count_enrolled_users($context, $withcapability = '', $groupid = 0) { +function count_enrolled_users(context $context, $withcapability = '', $groupid = 0) { global $DB; list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid); @@ -3278,7 +2253,6 @@ function count_enrolled_users($context, $withcapability = '', $groupid = 0) { return $DB->count_records_sql($sql, $params); } - /** * Loads the capability definitions for the component (from file). * @@ -3305,9 +2279,9 @@ function load_capability_def($component) { return $capabilities; } - /** * Gets the capabilities that have been cached in the database for this component. + * * @param string $component - examples: 'moodle', 'mod_forum' * @return array array of capabilities */ @@ -3318,6 +2292,7 @@ function get_cached_capabilities($component = 'moodle') { /** * Returns default capabilities for given role archetype. + * * @param string $archetype role archetype * @return array */ @@ -3359,6 +2334,7 @@ function get_default_capabilities($archetype) { /** * Reset role capabilities to default according to selected role archetype. * If no archetype selected, removes all capabilities. + * * @param int $roleid * @return void */ @@ -3368,12 +2344,12 @@ function reset_role_capabilities($roleid) { $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST); $defaultcaps = get_default_capabilities($role->archetype); - $sitecontext = get_context_instance(CONTEXT_SYSTEM); + $systemcontext = context_system::instance(); $DB->delete_records('role_capabilities', array('roleid'=>$roleid)); foreach($defaultcaps as $cap=>$permission) { - assign_capability($cap, $permission, $roleid, $sitecontext->id); + assign_capability($cap, $permission, $roleid, $systemcontext->id); } } @@ -3390,7 +2366,7 @@ function reset_role_capabilities($roleid) { * @return boolean true if success, exception in case of any problems */ function update_capabilities($component = 'moodle') { - global $DB, $OUTPUT, $ACCESSLIB_PRIVATE; + global $DB, $OUTPUT; $storedcaps = array(); @@ -3483,12 +2459,11 @@ function update_capabilities($component = 'moodle') { capabilities_cleanup($component, $filecaps); // reset static caches - $ACCESSLIB_PRIVATE->capabilities = null; + accesslib_clear_all_caches(false); return true; } - /** * Deletes cached capabilities that are no longer needed by the component. * Also unassigns these capabilities from any roles that have them. @@ -3531,161 +2506,6 @@ function capabilities_cleanup($component, $newcapdef = null) { // 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: - $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: - if ($category = $DB->get_record('course_categories', array('id'=>$context->instanceid))) { - if ($withprefix){ - $name = get_string('category').': '; - } - $name .= format_string($category->name, true, array('context' => $context)); - } - break; - - case CONTEXT_COURSE: - 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, true, array('context' => $context)); - } else { - $name .= format_string($course->fullname); - } - } - } - break; - - case CONTEXT_MODULE: - 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: - if ($blockinstance = $DB->get_record('block_instances', array('id'=>$context->instanceid))) { - global $CFG; - require_once("$CFG->dirroot/blocks/moodleblock.class.php"); - require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php"); - $blockname = "block_$blockinstance->blockname"; - 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. - * - * @param object $context the context. - * @return moodle_url - */ -function get_context_url($context) { - global $COURSE, $DB; - - switch ($context->contextlevel) { - case CONTEXT_USER: - if ($COURSE->id == SITEID) { - $url = new moodle_url('/user/profile.php', array('id'=>$context->instanceid)); - } else { - $url = new moodle_url('/user/view.php', array('id'=>$context->instanceid, 'courseid'=>$COURSE->id)); - } - return $url;; - - case CONTEXT_COURSECAT: // Coursecat -> coursecat or site - return new moodle_url('/course/category.php', array('id'=>$context->instanceid)); - - case CONTEXT_COURSE: // 1 to 1 to course cat - if ($context->instanceid != SITEID) { - return new moodle_url('/course/view.php', array('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))) { - return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$context->instanceid)); - } - break; - - case CONTEXT_BLOCK: - $parentcontexts = get_parent_contexts($context, false); - $parent = reset($parentcontexts); - $parent = get_context_instance_by_id($parent); - return get_context_url($parent); - } - - return new moodle_url('/'); -} - /** * Returns an array of all the known types of risk * The array keys can be used, for example as CSS class names, or in calls to @@ -3711,154 +2531,24 @@ function get_all_risks() { * @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 '' . get_capability_string($capability->name) . ''; } -/** - * 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 - * - * capabilities - * `name` varchar(150) NOT NULL, - * `captype` varchar(50) NOT NULL, - * `contextlevel` int(10) NOT NULL, - * `component` varchar(100) NOT NULL, - * - * @param object context - * @return array - */ -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, 'cap'); - $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)); - - $subcaps = array(); - $subpluginsfile = "$CFG->dirroot/mod/$module->name/db/subplugins.php"; - if (file_exists($subpluginsfile)) { - $subplugins = array(); // should be redefined in the file - include($subpluginsfile); - if (!empty($subplugins)) { - foreach (array_keys($subplugins) as $subplugintype) { - foreach (array_keys(get_plugin_list($subplugintype)) as $subpluginname) { - $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname))); - } - } - } - } - - $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)) { - $extracaps = $modfunction(); - } - } - if (empty($extracaps)) { - $extracaps = array(); - } - - $extracaps = array_merge($subcaps, $extracaps); - - // All modules allow viewhiddenactivities. This is so you can hide - // the module then override to allow specific roles to see it. - // The actual check is in course page so not module-specific - $extracaps[]="moodle/course:viewhiddenactivities"; - 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 - $bi = $DB->get_record('block_instances', array('id' => $context->instanceid)); - - $extra = ''; - $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities'); - if ($extracaps) { - list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); - $extra = "OR name $extra"; - } - - $SQL = "SELECT * - FROM {capabilities} - WHERE (contextlevel = ".CONTEXT_BLOCK." - AND component = :component) - $extra"; - $params['component'] = 'block_' . $bi->blockname; - 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 context $context * @param int $roleid * @param string $cap capability, optional, defaults to '' * @return array of capabilities */ -function role_context_capabilities($roleid, $context, $cap = '') { +function role_context_capabilities($roleid, context $context, $cap = '') { global $DB; - $contexts = get_parent_contexts($context); - $contexts[] = $context->id; + $contexts = $context->get_parent_context_ids(true); $contexts = '('.implode(',', $contexts).')'; $params = array($roleid); @@ -3891,53 +2581,15 @@ function role_context_capabilities($roleid, $context, $cap = '') { 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 - * @param bool $capability optional, defaults to false - * @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); -} - /** * Constructs array with contextids as first parameter and context paths, * in both cases bottom top including self. * - * @param object $context + * @private + * @param context $context * @return array */ -function get_context_info_list($context) { +function get_context_info_list(context $context) { $contextids = explode('/', ltrim($context->path, '/')); $contextpaths = array(); $contextids2 = $contextids; @@ -3948,207 +2600,20 @@ function get_context_info_list($context) { return array($contextids, $contextpaths); } -/** - * Find course context - * @param object $context - course or lower context - * @return object context of the enclosing course, throws exception when related course context can not be found - */ -function get_course_context($context) { - if (empty($context->contextlevel)) { - throw new coding_exception('Invalid context parameter.'); - - } if ($context->contextlevel == CONTEXT_COURSE) { - return $context; - - } else if ($context->contextlevel == CONTEXT_MODULE) { - return get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST)); - - } else if ($context->contextlevel == CONTEXT_BLOCK) { - $parentcontext = get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST)); - if ($parentcontext->contextlevel == CONTEXT_COURSE) { - return $parentcontext; - } else if ($parentcontext->contextlevel == CONTEXT_MODULE) { - return get_context_instance_by_id(get_parent_contextid($parentcontext, MUST_EXIST)); - } else { - throw new coding_exception('Invalid level of block context parameter.'); - } - } - - throw new coding_exception('Invalid context level of parameter.'); -} - /** * Check if context is the front page context or a context inside it * * Returns true if this context is the front page context, or a context inside it, * otherwise false. * - * @param object $context a context object. + * @param context $context a context object. * @return bool */ -function is_inside_frontpage($context) { - $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID); +function is_inside_frontpage(context $context) { + $frontpagecontext = context_course::instance(SITEID); return strpos($context->path . '/', $frontpagecontext->path . '/') === 0; } -/** - * Runs get_records select on context table and returns the result - * 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 = " . CONTEXT_USER . " AND u.id = ctx.instanceid - LEFT JOIN {course_categories} cat ON ctx.contextlevel = " . CONTEXT_COURSECAT . " AND cat.id = ctx.instanceid - LEFT JOIN {course} c ON ctx.contextlevel = " . CONTEXT_COURSE . " AND c.id = ctx.instanceid - LEFT JOIN {course_modules} cm ON ctx.contextlevel = " . CONTEXT_MODULE . " AND cm.id = ctx.instanceid - LEFT JOIN {block_instances} bi ON ctx.contextlevel = " . CONTEXT_BLOCK . " AND bi.id = ctx.instanceid - $select - ORDER BY ctx.contextlevel, bi.defaultregion, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.defaultweight), 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, nor return blocks on the category pages. 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 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 ;-) - - $array = array(); - $cache = $ACCESSLIB_PRIVATE->contexcache; - - switch ($context->contextlevel) { - - case CONTEXT_BLOCK: - // No children. - break; - - case CONTEXT_MODULE: - // Find - // - blocks under this context path. - $sql = " SELECT ctx.* - FROM {context} ctx - WHERE ctx.path LIKE ? - AND ctx.contextlevel = ".CONTEXT_BLOCK; - $params = array("{$context->path}/%", $context->instanceid); - $records = $DB->get_recordset_sql($sql, $params); - foreach ($records as $rec) { - $cache->add($rec); - $array[$rec->id] = $rec; - } - break; - - case CONTEXT_COURSE: - // Find - // - modules and blocks under this context path. - $sql = " SELECT ctx.* - FROM {context} ctx - WHERE ctx.path LIKE ? - AND ctx.contextlevel IN (".CONTEXT_MODULE.",".CONTEXT_BLOCK.")"; - $params = array("{$context->path}/%", $context->instanceid); - $records = $DB->get_recordset_sql($sql, $params); - foreach ($records as $rec) { - $cache->add($rec); - $array[$rec->id] = $rec; - } - 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->add($rec); - $array[$rec->id] = $rec; - } - break; - - case CONTEXT_USER: - // Find - // - blocks under this context path. - $sql = " SELECT ctx.* - FROM {context} ctx - WHERE ctx.path LIKE ? - AND ctx.contextlevel = ".CONTEXT_BLOCK; - $params = array("{$context->path}/%", $context->instanceid); - $records = $DB->get_recordset_sql($sql, $params); - foreach ($records as $rec) { - $cache->add($rec); - $array[$rec->id] = $rec; - } - 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; - - $records = $DB->get_records_sql($sql); - foreach ($records as $rec) { - $array[$rec->id] = $rec; - } - break; - - default: - print_error('unknowcontext', '', '', $context->contextlevel); - return false; - } - return $array; -} - - -/** - * 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); - } -} - /** * Returns capability information (cached) * @@ -4158,7 +2623,7 @@ function get_related_contexts_string($context) { function get_capability_info($capabilityname) { global $ACCESSLIB_PRIVATE, $DB; // one request per page only - // TODO: cache this in shared memory if available, use new $CFG->roledefrev for version check + //TODO: MUC - this could be cached in shared memory, it would eliminate 1 query per page if (empty($ACCESSLIB_PRIVATE->capabilities)) { $ACCESSLIB_PRIVATE->capabilities = array(); @@ -4167,6 +2632,7 @@ function get_capability_info($capabilityname) { $capname = $cap->name; unset($cap->id); unset($cap->name); + $cap->riskbitmask = (int)$cap->riskbitmask; $ACCESSLIB_PRIVATE->capabilities[$capname] = $cap; } } @@ -4211,7 +2677,6 @@ function get_capability_string($capabilityname) { return get_string($stringname, $component); } - /** * This gets the mod/block/course/core etc strings. * @@ -4223,6 +2688,7 @@ function get_component_string($component, $contextlevel) { if ($component === 'moodle' or $component === 'core') { switch ($contextlevel) { + // TODO: this should probably use context level names instead case CONTEXT_SYSTEM: return get_string('coresystem'); case CONTEXT_USER: return get_string('users'); case CONTEXT_COURSECAT: return get_string('categories'); @@ -4241,7 +2707,7 @@ function get_component_string($component, $contextlevel) { } switch ($type) { - // TODO this is really hacky + // TODO: this is really hacky, anyway it should be probably moved to lib/pluginlib.php case 'quiz': return get_string($name.':componentname', $component);// insane hack!!! case 'repository': return get_string('repository', 'repository').': '.get_string('pluginname', $component); case 'gradeimport': return get_string('gradeimport', 'grades').': '.get_string('pluginname', $component); @@ -4264,20 +2730,19 @@ function get_component_string($component, $contextlevel) { * from the list of roles that are visible on user profile page * and participants page. * - * @param object $context + * @param context $context * @return array */ -function get_profile_roles($context) { +function get_profile_roles(context $context) { global $CFG, $DB; if (empty($CFG->profileroles)) { return array(); } - $allowed = explode(',', $CFG->profileroles); - list($rallowed, $params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED); - - $contextlist = get_related_contexts_string($context); + list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a'); + list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p'); + $params = array_merge($params, $cparams); $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder FROM {role_assignments} ra, {role} r @@ -4292,13 +2757,13 @@ function get_profile_roles($context) { /** * Gets the list of roles assigned to this context and up (parents) * - * @param object $context + * @param context $context * @return array */ -function get_roles_used_in_context($context) { +function get_roles_used_in_context(context $context) { global $DB; - $contextlist = get_related_contexts_string($context); + list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true)); $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder FROM {role_assignments} ra, {role} r @@ -4306,7 +2771,7 @@ function get_roles_used_in_context($context) { AND ra.contextid $contextlist ORDER BY r.sortorder ASC"; - return $DB->get_records_sql($sql); + return $DB->get_records_sql($sql, $params); } /** @@ -4319,26 +2784,25 @@ function get_roles_used_in_context($context) { * @return string */ function get_user_roles_in_course($userid, $courseid) { - global $CFG, $DB,$USER; + global $CFG, $DB; if (empty($CFG->profileroles)) { return ''; } if ($courseid == SITEID) { - $context = get_context_instance(CONTEXT_SYSTEM); + $context = context_system::instance(); } else { - $context = get_context_instance(CONTEXT_COURSE, $courseid); + $context = context_course::instance($courseid); } if (empty($CFG->profileroles)) { return array(); } - $allowed = explode(',', $CFG->profileroles); - list($rallowed, $params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED); - - $contextlist = get_related_contexts_string($context); + list($rallowed, $params) = $DB->get_in_or_equal(explode(',', $CFG->profileroles), SQL_PARAMS_NAMED, 'a'); + list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p'); + $params = array_merge($params, $cparams); $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder FROM {role_assignments} ra, {role} r @@ -4370,11 +2834,11 @@ function get_user_roles_in_course($userid, $courseid) { /** * Checks if a user can assign users to a particular role in this context * - * @param object $context + * @param context $context * @param int $targetroleid - the id of the role you want to assign users to * @return boolean */ -function user_can_assign($context, $targetroleid) { +function user_can_assign(context $context, $targetroleid) { global $DB; // first check if user has override capability @@ -4407,6 +2871,7 @@ function get_all_roles() { /** * Returns roles of a specified archetype + * * @param string $archetype * @return array of full role records */ @@ -4421,13 +2886,13 @@ function get_archetype_roles($archetype) { * 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 context $context * @param int $userid * @param bool $checkparentcontexts defaults to true * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC' * @return array */ -function get_user_roles($context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') { +function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') { global $USER, $DB; if (empty($userid)) { @@ -4438,7 +2903,7 @@ function get_user_roles($context, $userid = 0, $checkparentcontexts = true, $ord } if ($checkparentcontexts) { - $contextids = get_parent_contexts($context); + $contextids = $context->get_parent_context_ids(); } else { $contextids = array(); } @@ -4478,8 +2943,8 @@ function allow_override($sroleid, $troleid) { /** * Creates a record in the role_allow_assign table * - * @param int $sroleid source roleid - * @param int $troleid target roleid + * @param int $fromroleid source roleid + * @param int $targetroleid target roleid * @return void */ function allow_assign($fromroleid, $targetroleid) { @@ -4494,8 +2959,8 @@ function allow_assign($fromroleid, $targetroleid) { /** * Creates a record in the role_allow_switch table * - * @param int $sroleid source roleid - * @param int $troleid target roleid + * @param int $fromroleid source roleid + * @param int $targetroleid target roleid * @return void */ function allow_switch($fromroleid, $targetroleid) { @@ -4510,7 +2975,7 @@ function allow_switch($fromroleid, $targetroleid) { /** * Gets a list of roles that this user can assign in this context * - * @param object $context the context. + * @param context $context the context. * @param int $rolenamedisplay the type of role name to display. One of the * ROLENAME_X constants. Default ROLENAME_ALIAS. * @param bool $withusercounts if true, count the number of users with each role. @@ -4519,7 +2984,7 @@ function allow_switch($fromroleid, $targetroleid) { * if $withusercounts is true, returns a list of three arrays, * $rolenames, $rolecounts, and $nameswithcounts. */ -function get_assignable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) { +function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) { global $USER, $DB; // make sure there is a real user specified @@ -4537,7 +3002,7 @@ function get_assignable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $with } } - $parents = get_parent_contexts($context, true); + $parents = $context->get_parent_context_ids(true); $contexts = implode(',' , $parents); $params = array(); @@ -4609,14 +3074,12 @@ function get_assignable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $with * This function just process the contents of the role_allow_switch table. You also need to * test the moodle/role:switchroles to see if the user is allowed to switch in the first place. * - * @param object $context a context. + * @param context $context a context. * @return array an array $roleid => $rolename. */ -function get_switchable_roles($context) { +function get_switchable_roles(context $context) { global $USER, $DB; - $systemcontext = get_context_instance(CONTEXT_SYSTEM); - $params = array(); $extrajoins = ''; $extrawhere = ''; @@ -4624,8 +3087,7 @@ function get_switchable_roles($context) { // Admins are allowed to switch to any role with. // Others are subject to the additional constraint that the switch-to role must be allowed by // 'role_allow_switch' for some role they have assigned in this context or any parent. - $parents = get_parent_contexts($context); - $parents[] = $context->id; + $parents = $context->get_parent_context_ids(true); $contexts = implode(',' , $parents); $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid @@ -4650,7 +3112,7 @@ function get_switchable_roles($context) { /** * Gets a list of roles that this user can override in this context. * - * @param object $context the context. + * @param context $context the context. * @param int $rolenamedisplay the type of role name to display. One of the * ROLENAME_X constants. Default ROLENAME_ALIAS. * @param bool $withcounts if true, count the number of overrides that are set for each role. @@ -4658,7 +3120,7 @@ function get_switchable_roles($context) { * if $withusercounts is true, returns a list of three arrays, * $rolenames, $rolecounts, and $nameswithcounts. */ -function get_overridable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) { +function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) { global $USER, $DB; if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) { @@ -4669,8 +3131,7 @@ function get_overridable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $wit } } - $parents = get_parent_contexts($context); - $parents[] = $context->id; + $parents = $context->get_parent_context_ids(true); $contexts = implode(',' , $parents); $params = array(); @@ -4681,7 +3142,7 @@ function get_overridable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $wit $params['userid'] = $USER->id; if ($withcounts) { - $extrafields = ', (SELECT count(rc.id) FROM {role_capabilities} rc + $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount'; $params['conid'] = $context->id; } @@ -4732,11 +3193,11 @@ function get_overridable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $wit /** * Create a role menu suitable for default role selection in enrol plugins. - * @param object $context + * @param context $context * @param int $addroleid current or default role - always added to list * @return array roleid=>localised role name */ -function get_default_enrol_roles($context, $addroleid = null) { +function get_default_enrol_roles(context $context, $addroleid = null) { global $DB; $params = array('contextlevel'=>CONTEXT_COURSE); @@ -4759,6 +3220,7 @@ function get_default_enrol_roles($context, $addroleid = null) { } /** + * Return context levels where this role is assignable. * @param integer $roleid the id of a role. * @return array list of the context levels at which this role may be assigned. */ @@ -4769,6 +3231,10 @@ function get_role_contextlevels($roleid) { } /** + * Return roles suitable for assignment at the specified context level. + * + * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel() + * * @param integer $contextlevel a contextlevel. * @return array list of role ids that are assignable at this context level. */ @@ -4779,6 +3245,8 @@ function get_roles_for_contextlevels($contextlevel) { } /** + * Returns default context levels where roles can be assigned. + * * @param string $rolearchetype one of the role archetypes - that is, one of the keys * from the array returned by get_role_archetypes(); * @return array list of the context levels at which this type of role may be assigned by default. @@ -4832,7 +3300,7 @@ function set_role_contextlevels($roleid, array $contextlevels) { * which can get rather large - and has a serious perf impact * on some DBs. * - * @param object $context + * @param context $context * @param string|array $capability - capability name(s) * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included. * @param string $sort - the sort order. Default is lastaccess time. @@ -4849,14 +3317,10 @@ function set_role_contextlevels($roleid, array $contextlevels) { * in $groups. * @return mixed */ -function get_users_by_capability($context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '', +function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '', $groups = '', $exceptions = '', $doanything_ignored = null, $view_ignored = null, $useviewallgroups = false) { global $CFG, $DB; - if (empty($context->id)) { - throw new coding_exception('Invalid context specified'); - } - $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0; $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0; @@ -4982,7 +3446,6 @@ function get_users_by_capability($context, $capability, $fields = '', $sort = '' $sort = 'u.lastaccess'; } } - $sortby = "ORDER BY $sort"; // Prepare query clauses $wherecond = array(); @@ -5133,12 +3596,12 @@ function get_users_by_capability($context, $capability, $fields = '', $sort = '' * variables like $CFG->coursecontact . * * @param array $users Users array, keyed on userid - * @param object $context + * @param context $context * @param array $roles ids of the roles to include, optional - * @param string $policy defaults to locality, more about + * @param string $sortpolicy defaults to locality, more about * @return array sorted copy of the array */ -function sort_by_roleassignment_authority($users, $context, $roles = array(), $sortpolicy = 'locality') { +function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') { global $DB; $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')'; @@ -5192,7 +3655,7 @@ function sort_by_roleassignment_authority($users, $context, $roles = array(), $s * Gets all the users assigned this role in this context or higher * * @param int $roleid (can also be an array of ints!) - * @param stdClass $context + * @param context $context * @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.) @@ -5204,7 +3667,7 @@ function sort_by_roleassignment_authority($users, $context, $roles = array(), $s * @param string|array $whereparams defaults to '' * @return array */ -function get_role_users($roleid, $context, $parent = false, $fields = '', +function get_role_users($roleid, context $context, $parent = false, $fields = '', $sort = 'u.lastname, u.firstname', $gethidden_ignored = null, $group = '', $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereparams = array()) { global $DB; @@ -5266,16 +3729,16 @@ function get_role_users($roleid, $context, $parent = false, $fields = '', /** * Counts all the users assigned this role in this context or higher * - * @param mixed $roleid either int or an array of ints - * @param object $context + * @param int|array $roleid either int or an array of ints + * @param context $context * @param bool $parent if true, get list of users assigned in higher context too * @return int Returns the result count */ -function count_role_users($roleid, $context, $parent = false) { +function count_role_users($roleid, context $context, $parent = false) { global $DB; if ($parent) { - if ($contexts = get_parent_contexts($context)) { + if ($contexts = $context->get_parent_context_ids()) { $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')'; } else { $parentcontexts = ''; @@ -5294,7 +3757,7 @@ function count_role_users($roleid, $context, $parent = false) { array_unshift($params, $context->id); - $sql = "SELECT count(u.id) + $sql = "SELECT COUNT(u.id) FROM {role_assignments} r JOIN {user} u ON u.id = r.userid WHERE (r.contextid = ? $parentcontexts) @@ -5368,12 +3831,12 @@ function get_user_capability_course($capability, $userid = null, $doanything = t /** * This function finds the roles assigned directly to this context only - * i.e. no parents role + * i.e. no roles in parent contexts * - * @param object $context + * @param context $context * @return array */ -function get_roles_on_exact_context($context) { +function get_roles_on_exact_context(context $context) { global $DB; return $DB->get_records_sql("SELECT r.* @@ -5397,10 +3860,10 @@ function get_roles_on_exact_context($context) { * 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. + * @param context $context the context in which to perform the switch. * @return bool success or failure. */ -function role_switch($roleid, $context) { +function role_switch($roleid, context $context) { global $USER; // @@ -5440,11 +3903,10 @@ function role_switch($roleid, $context) { return true; } - $USER->access['rsw'][$context->path]=$roleid; + $USER->access['rsw'][$context->path] = $roleid; // Load roledefs - $USER->access = get_role_access_bycontext($roleid, $context, - $USER->access); + load_role_access_by_context($roleid, $context, $USER->access); return true; } @@ -5461,18 +3923,17 @@ function role_switch($roleid, $context) { */ function is_role_switched($courseid) { global $USER; - $context = get_context_instance(CONTEXT_COURSE, $courseid, MUST_EXIST); + $context = context_course::instance($courseid, MUST_EXIST); return (!empty($USER->access['rsw'][$context->path])); } /** * Get any role that has an override on exact context * - * @global moodle_database - * @param stdClass $context The context to check + * @param context $context The context to check * @return array An array of roles */ -function get_roles_with_override_on_context($context) { +function get_roles_with_override_on_context(context $context) { global $DB; return $DB->get_records_sql("SELECT r.* @@ -5484,11 +3945,11 @@ function get_roles_with_override_on_context($context) { /** * Get all capabilities for this role on this context (overrides) * - * @param object $role - * @param object $context + * @param stdClass $role + * @param context $context * @return array */ -function get_capabilities_from_role_on_context($role, $context) { +function get_capabilities_from_role_on_context($role, context $context) { global $DB; return $DB->get_records_sql("SELECT * @@ -5500,11 +3961,11 @@ function get_capabilities_from_role_on_context($role, $context) { /** * Find out which roles has assignment on this context * - * @param object $context + * @param context $context * @return array * */ -function get_roles_with_assignment_on_context($context) { +function get_roles_with_assignment_on_context(context $context) { global $DB; return $DB->get_records_sql("SELECT r.* @@ -5513,16 +3974,14 @@ function get_roles_with_assignment_on_context($context) { array($context->id)); } - - /** * Find all user assignment of users for this role, on this context * - * @param object $role - * @param object $context + * @param stdClass $role + * @param context $context * @return array */ -function get_users_from_role_on_context($role, $context) { +function get_users_from_role_on_context($role, context $context) { global $DB; return $DB->get_records_sql("SELECT * @@ -5544,10 +4003,10 @@ function user_has_role_assignment($userid, $roleid, $contextid = 0) { global $DB; if ($contextid) { - if (!$context = get_context_instance_by_id($contextid)) { + if (!$context = context::instance_by_id($contextid, IGNORE_MISSING)) { return false; } - $parents = get_parent_contexts($context, true); + $parents = $context->get_parent_context_ids(true); list($contexts, $params) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED, 'r'); $params['userid'] = $userid; $params['roleid'] = $roleid; @@ -5567,11 +4026,11 @@ function user_has_role_assignment($userid, $roleid, $contextid = 0) { /** * Get role name or alias if exists and format the text. * - * @param object $role role object - * @param object $coursecontext + * @param stdClass $role role object + * @param context_course $coursecontext * @return string name of role in course context */ -function role_get_name($role, $coursecontext) { +function role_get_name($role, context_course $coursecontext) { global $DB; if ($r = $DB->get_record('role_names', array('roleid'=>$role->id, 'contextid'=>$coursecontext->id))) { @@ -5585,12 +4044,16 @@ function role_get_name($role, $coursecontext) { * 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 + * @param context $context a context + * @param int $rolenamedisplay * @return array Array of context-specific role names, or role objexts with a ->localname field added. */ -function role_fix_names($roleoptions, $context, $rolenamedisplay = ROLENAME_ALIAS) { +function role_fix_names($roleoptions, context $context, $rolenamedisplay = ROLENAME_ALIAS) { global $DB; + // Make sure we have a course context. + $coursecontext = $context->get_course_context(false); + // 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(); @@ -5607,22 +4070,9 @@ function role_fix_names($roleoptions, $context, $rolenamedisplay = ROLENAME_ALIA } // 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) { - if ($parentcontextid = array_shift(get_parent_contexts($context))) { - $context = get_context_instance_by_id($parentcontextid); - } - } else if ($context->contextlevel == CONTEXT_BLOCK) { - do { - if ($parentcontextid = array_shift(get_parent_contexts($context))) { - $context = get_context_instance_by_id($parentcontextid); - } - } while ($parentcontextid && $context->contextlevel != CONTEXT_COURSE); - } - + if ($rolenamedisplay != ROLENAME_ORIGINAL && !empty($coursecontext->id)) { // The get the relevant renames, and use them. - $aliasnames = $DB->get_records('role_names', array('contextid'=>$context->id)); + $aliasnames = $DB->get_records('role_names', array('contextid'=>$coursecontext->id)); foreach ($aliasnames as $alias) { if (isset($newnames[$alias->roleid])) { if ($rolenamedisplay == ROLENAME_ALIAS || $rolenamedisplay == ROLENAME_ALIAS_RAW) { @@ -5658,7 +4108,7 @@ function role_fix_names($roleoptions, $context, $rolenamedisplay = ROLENAME_ALIA * * @param string $cap component string a * @param string $comp component string b - * @param mixed $contextlevel + * @param int $contextlevel * @return bool whether 2 component are in different "sections" */ function component_level_changed($cap, $comp, $contextlevel) { @@ -5686,331 +4136,6 @@ function component_level_changed($cap, $comp, $contextlevel) { return ($cap->component != $comp || $cap->contextlevel != $contextlevel); } -/** - * Rebuild all related context depth and path caches - * - * @param array $fixcontexts array of contexts, strongtyped - */ -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, defaults to false - */ -function build_context_path($force = false) { - global $CFG, $DB, $ACCESSLIB_PRIVATE; - - // 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 {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'); - - // 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); - - // Blocks - // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent - $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_instances} bi ON ctx.instanceid = bi.id - JOIN {context} pctx ON bi.parentcontextid = pctx.id - WHERE ctx.contextlevel=".CONTEXT_BLOCK." - AND pctx.path IS NOT NULL - 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'); - - // reset static course cache - it might have incorrect cached data - $ACCESSLIB_PRIVATE->contexcache->reset(); -} - -/** - * Update the path field of the context and all dep. subcontexts that follow - * - * 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 stdClass $current context obj - * @param stdClass $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); -} - -/** - * Preloads context information together with instances. - * NOTE: in future this function may return empty strings - * if we implement different caching. - * - * @param string $joinon for example 'u.id' - * @param string $contextlevel context level of instance in $joinon - * @param string $tablealias context table alias - * @return array with two values - select and join part - */ -function context_instance_preload_sql($joinon, $contextlevel, $tablealias) { - $select = ", $tablealias.id AS ctxid, $tablealias.path AS ctxpath, $tablealias.depth AS ctxdepth, $tablealias.contextlevel AS ctxlevel, $tablealias.instanceid AS ctxinstance"; - $join = "LEFT JOIN {context} $tablealias ON ($tablealias.instanceid = $joinon AND $tablealias.contextlevel = $contextlevel)"; - return array($select, $join); -} - -/** - * Preloads context information from db record and strips the cached info. - * The db request has to ontain both the $join and $select from context_instance_preload_sql() - * - * @param object $rec - * @return void (modifies $rec) - */ -function context_instance_preload(stdClass $rec) { - global $ACCESSLIB_PRIVATE; - if (empty($rec->ctxid)) { - // $rec does not have enough data, passed here repeatedly or context does not exist yet - return; - } - - // note: in PHP5 the objects are passed by reference, no need to return $rec - $context = new stdClass(); - $context->id = $rec->ctxid; unset($rec->ctxid); - $context->path = $rec->ctxpath; unset($rec->ctxpath); - $context->depth = $rec->ctxdepth; unset($rec->ctxdepth); - $context->contextlevel = $rec->ctxlevel; unset($rec->ctxlevel); - $context->instanceid = $rec->ctxinstance; unset($rec->ctxinstance); - - $ACCESSLIB_PRIVATE->contexcache->add($context); -} - -/** - * 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 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 (during_initial_install()) { - 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 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. @@ -6054,7 +4179,7 @@ function switch_roles($first, $second) { } /** - * duplicates all the base definitions of a role + * Duplicates all the base definitions of a role * * @param object $sourcerole role to copy from * @param int $targetrole id of role to copy to @@ -6062,7 +4187,7 @@ function switch_roles($first, $second) { function role_cap_duplicate($sourcerole, $targetrole) { global $DB; - $systemcontext = get_context_instance(CONTEXT_SYSTEM); + $systemcontext = context_system::instance(); $caps = $DB->get_records_sql("SELECT * FROM {role_capabilities} WHERE roleid = ? AND contextid = ?", @@ -6075,7 +4200,6 @@ function role_cap_duplicate($sourcerole, $targetrole) { } } - /** * Returns two lists, this can be used to find out if user has capability. * Having any needed role and no forbidden role in this context means @@ -6202,11 +4326,13 @@ function get_role_names_with_caps_in_context($context, $capabilities) { /** * This function verifies the prohibit comes from this context * and there are no more prohibits in parent contexts. - * @param object $context + * + * @param int $roleid + * @param context $context * @param string $capability name * @return bool */ -function prohibit_is_removable($roleid, $context, $capability) { +function prohibit_is_removable($roleid, context $context, $capability) { global $DB; $ctxids = trim($context->path, '/'); // kill leading slash @@ -6247,7 +4373,7 @@ function role_change_permission($roleid, $context, $capname, $permission) { if ($permission == CAP_INHERIT) { unassign_capability($capname, $roleid, $context->id); - mark_context_dirty($context->path); + $context->mark_dirty(); return; } @@ -6280,7 +4406,7 @@ function role_change_permission($roleid, $context, $capname, $permission) { // permission already set in parent context or parent - just unset in this context // we do this because we want as few overrides as possible for performance reasons unassign_capability($capname, $roleid, $context->id); - mark_context_dirty($context->path); + $context->mark_dirty(); return; } } @@ -6296,6 +4422,2768 @@ function role_change_permission($roleid, $context, $capname, $permission) { assign_capability($capname, $permission, $roleid, $context->id, true); // force cap reloading - mark_context_dirty($context->path); + $context->mark_dirty(); } + +/** + * Context maintenance and helper methods. + * + * This is "extends context" is a bloody hack that tires to work around the deficiencies + * in the "protected" keyword in PHP, this helps us to hide all the internals of context + * level implementation from the rest of code, the code completion returns what developers need. + * + * Thank you Tim Hunt for helping me with this nasty trick. + * + * @author Petr Skoda + */ +class context_helper extends context { + + private static $alllevels = array( + CONTEXT_SYSTEM => 'context_system', + CONTEXT_USER => 'context_user', + CONTEXT_COURSECAT => 'context_coursecat', + CONTEXT_COURSE => 'context_course', + CONTEXT_MODULE => 'context_module', + CONTEXT_BLOCK => 'context_block', + ); + + /** + * Instance does not make sense here, only static use + */ + protected function __construct() { + } + + /** + * Returns a class name of the context level class + * + * @static + * @param int $contextlevel (CONTEXT_SYSTEM, etc.) + * @return string class name of the context class + */ + public static function get_class_for_level($contextlevel) { + if (isset(self::$alllevels[$contextlevel])) { + return self::$alllevels[$contextlevel]; + } else { + throw new coding_exception('Invalid context level specified'); + } + } + + /** + * Returns a list of all context levels + * + * @static + * @return array int=>string (level=>level class name) + */ + public static function get_all_levels() { + return self::$alllevels; + } + + /** + * Remove stale contexts that belonged to deleted instances. + * Ideally all code should cleanup contexts properly, unfortunately accidents happen... + * + * @static + * @return void + */ + public static function cleanup_instances() { + global $DB; + $sqls = array(); + foreach (self::$alllevels as $level=>$classname) { + $sqls[] = $classname::get_cleanup_sql(); + } + + $sql = implode(" UNION ", $sqls); + + // it is probably better to use transactions, it might be faster too + $transaction = $DB->start_delegated_transaction(); + + $rs = $DB->get_recordset_sql($sql); + foreach ($rs as $record) { + $context = context::create_instance_from_record($record); + $context->delete(); + } + $rs->close(); + + $transaction->allow_commit(); + } + + /** + * Create all context instances at the given level and above. + * + * @static + * @param int $contextlevel null means all levels + * @param bool $buildpaths + * @return void + */ + public static function create_instances($contextlevel = null, $buildpaths = true) { + foreach (self::$alllevels as $level=>$classname) { + if ($contextlevel and $level > $contextlevel) { + // skip potential sub-contexts + continue; + } + $classname::create_level_instances(); + if ($buildpaths) { + $classname::build_paths(false); + } + } + } + + /** + * Rebuild paths and depths in all context levels. + * + * @static + * @param bool $force false means add missing only + * @return void + */ + public static function build_all_paths($force = false) { + foreach (self::$alllevels as $classname) { + $classname::build_paths($force); + } + + // reset static course cache - it might have incorrect cached data + accesslib_clear_all_caches(true); + } + + /** + * Resets the cache to remove all data. + * @static + */ + public static function reset_caches() { + context::reset_caches(); + } + + /** + * Returns all fields necessary for context preloading from user $rec. + * + * This helps with performance when dealing with hundreds of contexts. + * + * @static + * @param string $tablealias context table alias in the query + * @return array (table.column=>alias, ...) + */ + public static function get_preload_record_columns($tablealias) { + return array("$tablealias.id"=>"ctxid", "$tablealias.path"=>"ctxpath", "$tablealias.depth"=>"ctxdepth", "$tablealias.contextlevel"=>"ctxlevel", "$tablealias.instanceid"=>"ctxinstance"); + } + + /** + * Returns all fields necessary for context preloading from user $rec. + * + * This helps with performance when dealing with hundreds of contexts. + * + * @static + * @param string $tablealias context table alias in the query + * @return string + */ + public static function get_preload_record_columns_sql($tablealias) { + return "$tablealias.id AS ctxid, $tablealias.path AS ctxpath, $tablealias.depth AS ctxdepth, $tablealias.contextlevel AS ctxlevel, $tablealias.instanceid AS ctxinstance"; + } + + /** + * Preloads context information from db record and strips the cached info. + * + * The db request has to contain all columns from context_helper::get_preload_record_columns(). + * + * @static + * @param stdClass $rec + * @return void (modifies $rec) + */ + public static function preload_from_record(stdClass $rec) { + context::preload_from_record($rec); + } + + /** + * Preload all contexts instances from course. + * + * To be used if you expect multiple queries for course activities... + * + * @static + * @param $courseid + */ + public static function preload_course($courseid) { + // Users can call this multiple times without doing any harm + if (isset(context::$cache_preloaded[$courseid])) { + return; + } + $coursecontext = context_course::instance($courseid); + $coursecontext->get_child_contexts(); + + context::$cache_preloaded[$courseid] = true; + } + + /** + * Delete context instance + * + * @static + * @param int $contextlevel + * @param int $instanceid + * @return void + */ + public static function delete_instance($contextlevel, $instanceid) { + global $DB; + + // double check the context still exists + if ($record = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) { + $context = context::create_instance_from_record($record); + $context->delete(); + } else { + // we should try to purge the cache anyway + } + } + + /** + * Returns the name of specified context level + * + * @static + * @param int $contextlevel + * @return string name of the context level + */ + public static function get_level_name($contextlevel) { + $classname = context_helper::get_class_for_level($contextlevel); + return $classname::get_level_name(); + } + + /** + * not used + */ + public function get_url() { + } + + /** + * not used + */ + public function get_capabilities() { + } +} + + +/** + * Basic moodle context abstraction class. + * + * @author Petr Skoda + * + * @property-read int $id context id + * @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc. + * @property-read int $instanceid id of related instance in each context + * @property-read string $path path to context, starts with system context + * @property-read dept $depth + */ +abstract class context extends stdClass { + + /* + * Google confirms that no other important framework is using "context" class, + * we could use something else like mcontext or moodle_context, but we need to type + * this very often which would be annoying and it would take too much space... + * + * This class is derived from stdClass for backwards compatibility with + * odl $context record that was returned from DML $DB->get_record() + */ + + protected $_id; + protected $_contextlevel; + protected $_instanceid; + protected $_path; + protected $_depth; + + /* context caching info */ + + private static $cache_contextsbyid = array(); + private static $cache_contexts = array(); + protected static $cache_count = 0; // why do we do count contexts? Because count($array) is horribly slow for large arrays + + protected static $cache_preloaded = array(); + protected static $systemcontext = null; + + /** + * Resets the cache to remove all data. + */ + protected static function reset_caches() { + self::$cache_contextsbyid = array(); + self::$cache_contexts = array(); + self::$cache_count = 0; + self::$cache_preloaded = array(); + + self::$systemcontext = null; + } + + /** + * Adds a context to the cache. If the cache is full, discards a batch of + * older entries. + * + * @static + * @param context $context New context to add + * @return void + */ + protected static function cache_add(context $context) { + if (isset(self::$cache_contextsbyid[$context->id])) { + // already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow + return; + } + + if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) { + $i = 0; + foreach(self::$cache_contextsbyid as $ctx) { + $i++; + if ($i <= 100) { + // we want to keep the first contexts to be loaded on this page, hopefully they will be needed again later + continue; + } + if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) { + // we remove oldest third of the contexts to make room for more contexts + break; + } + unset(self::$cache_contextsbyid[$ctx->id]); + unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]); + self::$cache_count--; + } + } + + self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context; + self::$cache_contextsbyid[$context->id] = $context; + self::$cache_count++; + } + + /** + * Removes a context from the cache. + * + * @static + * @param context $context Context object to remove + * @return void + */ + protected static function cache_remove(context $context) { + if (!isset(self::$cache_contextsbyid[$context->id])) { + // not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow + return; + } + unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]); + unset(self::$cache_contextsbyid[$context->id]); + + self::$cache_count--; + + if (self::$cache_count < 0) { + self::$cache_count = 0; + } + } + + /** + * Gets a context from the cache. + * + * @static + * @param int $contextlevel Context level + * @param int $instance Instance ID + * @return context|bool Context or false if not in cache + */ + protected static function cache_get($contextlevel, $instance) { + if (isset(self::$cache_contexts[$contextlevel][$instance])) { + return self::$cache_contexts[$contextlevel][$instance]; + } + return false; + } + + /** + * Gets a context from the cache based on its id. + * + * @static + * @param int $id Context ID + * @return context|bool Context or false if not in cache + */ + protected static function cache_get_by_id($id) { + if (isset(self::$cache_contextsbyid[$id])) { + return self::$cache_contextsbyid[$id]; + } + return false; + } + + /** + * Preloads context information from db record and strips the cached info. + * + * @static + * @param stdClass $rec + * @return void (modifies $rec) + */ + protected static function preload_from_record(stdClass $rec) { + if (empty($rec->ctxid) or empty($rec->ctxlevel) or empty($rec->ctxinstance) or empty($rec->ctxpath) or empty($rec->ctxdepth)) { + // $rec does not have enough data, passed here repeatedly or context does not exist yet + return; + } + + // note: in PHP5 the objects are passed by reference, no need to return $rec + $record = new stdClass(); + $record->id = $rec->ctxid; unset($rec->ctxid); + $record->contextlevel = $rec->ctxlevel; unset($rec->ctxlevel); + $record->instanceid = $rec->ctxinstance; unset($rec->ctxinstance); + $record->path = $rec->ctxpath; unset($rec->ctxpath); + $record->depth = $rec->ctxdepth; unset($rec->ctxdepth); + + return context::create_instance_from_record($record); + } + + + // ====== magic methods ======= + + /** + * Magic setter method, we do not want anybody to modify properties from the outside + * @param string $name + * @param mixed @value + */ + public function __set($name, $value) { + debugging('Can not change context instance properties!'); + } + + /** + * Magic method getter, redirects to read only values. + * @param string $name + * @return mixed + */ + public function __get($name) { + switch ($name) { + case 'id': return $this->_id; + case 'contextlevel': return $this->_contextlevel; + case 'instanceid': return $this->_instanceid; + case 'path': return $this->_path; + case 'depth': return $this->_depth; + + default: + debugging('Invalid context property accessed! '.$name); + return null; + } + } + + /** + * Full support for isset on our magic read only properties. + * @param $name + * @return bool + */ + public function __isset($name) { + switch ($name) { + case 'id': return isset($this->_id); + case 'contextlevel': return isset($this->_contextlevel); + case 'instanceid': return isset($this->_instanceid); + case 'path': return isset($this->_path); + case 'depth': return isset($this->_depth); + + default: return false; + } + + } + + /** + * ALl properties are read only, sorry. + * @param string $name + */ + public function __unset($name) { + debugging('Can not unset context instance properties!'); + } + + // ====== general context methods ====== + + /** + * Constructor is protected so that devs are forced to + * use context_xxx::instance() or context::instance_by_id(). + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + $this->_id = $record->id; + $this->_contextlevel = (int)$record->contextlevel; + $this->_instanceid = $record->instanceid; + $this->_path = $record->path; + $this->_depth = $record->depth; + } + + /** + * This function is also used to work around 'protected' keyword problems in context_helper. + * @param stdClass $record + * @return context instance + */ + protected static function create_instance_from_record(stdClass $record) { + $classname = context_helper::get_class_for_level($record->contextlevel); + + if ($context = context::cache_get_by_id($record->id)) { + return $context; + } + + $context = new $classname($record); + context::cache_add($context); + + return $context; + } + + /** + * Copy prepared new contexts from temp table to context table, + * we do this in db specific way for perf reasons only. + * @static + */ + protected static function merge_context_temp_table() { + global $DB; + + /* 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 {context_temp})"; + } + + $DB->execute($updatesql); + } + + /** + * Get a context instance as an object, from a given context id. + * + * @static + * @param int $id context id + * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; + * MUST_EXIST means throw exception if no record found + * @return context|bool the context object or false if not found + */ + public static function instance_by_id($id, $strictness = MUST_EXIST) { + global $DB; + + if (get_called_class() !== 'context' and get_called_class() !== 'context_helper') { + // some devs might confuse context->id and instanceid, better prevent these mistakes completely + throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods'); + } + + if ($id == SYSCONTEXTID) { + return context_system::instance(0, $strictness); + } + + if (is_array($id) or is_object($id) or empty($id)) { + throw new coding_exception('Invalid context id specified context::instance_by_id()'); + } + + if ($context = context::cache_get_by_id($id)) { + return $context; + } + + if ($record = $DB->get_record('context', array('id'=>$id), '*', $strictness)) { + return context::create_instance_from_record($record); + } + + return false; + } + + /** + * Update context info after moving context in the tree structure. + * + * @param context $newparent + * @return void + */ + public function update_moved(context $newparent) { + global $DB; + + $frompath = $this->_path; + $newpath = $newparent->path . '/' . $this->_id; + + $trans = $DB->start_delegated_transaction(); + + $this->mark_dirty(); + + $setdepth = ''; + if (($newparent->depth +1) != $this->_depth) { + $diff = $newparent->depth - $this->_depth + 1; + $setdepth = ", depth = depth + $diff"; + } + $sql = "UPDATE {context} + SET path = ? + $setdepth + WHERE id = ?"; + $params = array($newpath, $this->_id); + $DB->execute($sql, $params); + + $this->_path = $newpath; + $this->_depth = $newparent->depth + 1; + + $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); + + $this->mark_dirty(); + + context::reset_caches(); + + $trans->allow_commit(); + } + + /** + * Remove all context path info and optionally rebuild it. + * + * @param bool $rebuild + * @return void + */ + public function reset_paths($rebuild = true) { + global $DB; + + if ($this->_path) { + $this->mark_dirty(); + } + $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'"); + $DB->set_field_select('context', 'path', NULL, "path LIKE '%/$this->_id/%'"); + if ($this->_contextlevel != CONTEXT_SYSTEM) { + $DB->set_field('context', 'depth', 0, array('id'=>$this->_id)); + $DB->set_field('context', 'path', NULL, array('id'=>$this->_id)); + $this->_depth = 0; + $this->_path = null; + } + + if ($rebuild) { + context_helper::build_all_paths(false); + } + + context::reset_caches(); + } + + /** + * Delete all data linked to content, do not delete the context record itself + */ + public function delete_content() { + global $CFG, $DB; + + blocks_delete_all_for_context($this->_id); + filter_delete_all_for_context($this->_id); + + require_once($CFG->dirroot . '/comment/lib.php'); + comment::delete_comments(array('contextid'=>$this->_id)); + + require_once($CFG->dirroot.'/rating/lib.php'); + $delopt = new stdclass(); + $delopt->contextid = $this->_id; + $rm = new rating_manager(); + $rm->delete_ratings($delopt); + + // delete all files attached to this context + $fs = get_file_storage(); + $fs->delete_area_files($this->_id); + + // now delete stuff from role related tables, role_unassign_all + // and unenrol should be called earlier to do proper cleanup + $DB->delete_records('role_assignments', array('contextid'=>$this->_id)); + $DB->delete_records('role_capabilities', array('contextid'=>$this->_id)); + $DB->delete_records('role_names', array('contextid'=>$this->_id)); + } + + /** + * Delete the context content and the context record itself + */ + public function delete() { + global $DB; + + // double check the context still exists + if (!$DB->record_exists('context', array('id'=>$this->_id))) { + context::cache_remove($this); + return; + } + + $this->delete_content(); + $DB->delete_records('context', array('id'=>$this->_id)); + // purge static context cache if entry present + context::cache_remove($this); + + // do not mark dirty contexts if parents unknown + if (!is_null($this->_path) and $this->_depth > 0) { + $this->mark_dirty(); + } + } + + // ====== context level related methods ====== + + /** + * Utility method for context creation + * + * @static + * @param int $contextlevel + * @param int $instanceid + * @param string $parentpath + * @return stdClass context record + */ + protected static function insert_context_record($contextlevel, $instanceid, $parentpath) { + global $DB; + + $record = new stdClass(); + $record->contextlevel = $contextlevel; + $record->instanceid = $instanceid; + $record->depth = 0; + $record->path = null; //not known before insert + + $record->id = $DB->insert_record('context', $record); + + // now add path if known - it can be added later + if (!is_null($parentpath)) { + $record->path = $parentpath.'/'.$record->id; + $record->depth = substr_count($record->path, '/'); + $DB->update_record('context', $record); + } + + return $record; + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + protected static function get_level_name() { + // must be implemented in all context levels + throw new coding_exception('can not get level name of abstract context'); + } + + /** + * Returns human readable context identifier. + * + * @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 use the short name of the thing. Only applies + * to course contexts + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false) { + // must be implemented in all context levels + throw new coding_exception('can not get name of abstract context'); + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public abstract function get_url(); + + /** + * Returns array of relevant context capability records. + * + * @return array + */ + public abstract function get_capabilities(); + + /** + * Recursive function which, given a context, find all its children context ids. + * + * For course category contexts it will return immediate children and all subcategory contexts. + * It will NOT recurse into courses or subcategories categories. + * If you want to do that, call it on the returned courses/categories. + * + * When called for a course context, it will return the modules and blocks + * displayed in the course page and blocks displayed on the module pages. + * + * If called on a user/course/module context it _will_ populate the cache with the appropriate + * contexts ;-) + * + * @return array Array of child records + */ + public function get_child_contexts() { + global $DB; + + $sql = "SELECT ctx.* + FROM {context} ctx + WHERE ctx.path LIKE ?"; + $params = array($this->_path.'/%'); + $records = $DB->get_records_sql($sql, $params); + + $result = array(); + foreach ($records as $record) { + $result[$record->id] = context::create_instance_from_record($record); + } + + return $result; + } + + /** + * Returns parent contexts of this context in reversed order, i.e. parent first, + * then grand parent, etc. + * + * @param bool $includeself tre means include self too + * @return array of context instances + */ + public function get_parent_contexts($includeself = false) { + if (!$contextids = $this->get_parent_context_ids($includeself)) { + return array(); + } + + $result = array(); + foreach ($contextids as $contextid) { + $parent = context::instance_by_id($contextid, MUST_EXIST); + $result[$parent->id] = $parent; + } + + return $result; + } + + /** + * Returns parent contexts of this context in reversed order, i.e. parent first, + * then grand parent, etc. + * + * @param bool $includeself tre means include self too + * @return array of context ids + */ + public function get_parent_context_ids($includeself = false) { + if (empty($this->_path)) { + return array(); + } + + $parentcontexts = trim($this->_path, '/'); // kill leading slash + $parentcontexts = explode('/', $parentcontexts); + if (!$includeself) { + array_pop($parentcontexts); // and remove its own id + } + + return array_reverse($parentcontexts); + } + + /** + * Returns parent context + * + * @return context + */ + public function get_parent_context() { + if (empty($this->_path) or $this->_id == SYSCONTEXTID) { + return false; + } + + $parentcontexts = trim($this->_path, '/'); // kill leading slash + $parentcontexts = explode('/', $parentcontexts); + array_pop($parentcontexts); // self + $contextid = array_pop($parentcontexts); // immediate parent + + return context::instance_by_id($contextid, MUST_EXIST); + } + + /** + * Is this context part of any course? If yes return course context. + * + * @param bool $strict true means throw exception if not found, false means return false if not found + * @return course_context context of the enclosing course, null if not found or exception + */ + public function get_course_context($strict = true) { + if ($strict) { + throw new coding_exception('Context does not belong to any course.'); + } else { + return false; + } + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels'); + } + + /** + * Rebuild context paths and depths at context level. + * + * @static + * @param $force + * @return void + */ + protected static function build_paths($force) { + throw new coding_exception('build_paths() method must be implemented in all context levels'); + } + + /** + * Create missing context instances at given level + * + * @static + * @return void + */ + protected static function create_level_instances() { + throw new coding_exception('create_level_instances() method must be implemented in all context levels'); + } + + /** + * Reset all cached permissions and definitions if the necessary. + * @return void + */ + public function reload_if_dirty() { + global $ACCESSLIB_PRIVATE, $USER; + + // Load dirty contexts list if needed + if (CLI_SCRIPT) { + if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) { + // we do not load dirty flags in CLI and cron + $ACCESSLIB_PRIVATE->dirtycontexts = array(); + } + } else { + if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) { + if (!isset($USER->access['time'])) { + // nothing was loaded yet, we do not need to check dirty contexts now + return; + } + // no idea why -2 is there, server cluster time difference maybe... (skodak) + $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time']-2); + } + } + + foreach ($ACCESSLIB_PRIVATE->dirtycontexts as $path=>$unused) { + if ($path === $this->_path or strpos($this->_path, $path.'/') === 0) { + // reload all capabilities of USER and others - preserving loginas, roleswitches, etc + // and then cleanup any marks of dirtyness... at least from our short term memory! :-) + reload_all_capabilities(); + break; + } + } + } + + /** + * Mark a context as dirty (with timestamp) so as to force reloading of the context. + */ + public function mark_dirty() { + global $CFG, $USER, $ACCESSLIB_PRIVATE; + + if (during_initial_install()) { + return; + } + + // only if it is a non-empty string + if (is_string($this->_path) && $this->_path !== '') { + set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time()+$CFG->sessiontimeout); + if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) { + $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1; + } else { + if (CLI_SCRIPT) { + $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1); + } else { + if (isset($USER->access['time'])) { + $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time']-2); + } else { + $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1); + } + // flags not loaded yet, it will be done later in $context->reload_if_dirty() + } + } + } + } +} + + +/** + * Basic context class + * @author Petr Skoda (http://skodak.org) + */ +class context_system extends context { + /** + * Please use context_system::instance() if you need the instance of context. + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_SYSTEM) { + throw new coding_exception('Invalid $record->contextlevel in context_system constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + protected static function get_level_name() { + return get_string('coresystem'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix does not apply to system context + * @param boolean $short does not apply to system context + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false) { + return self::get_level_name(); + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + return new moodle_url('/'); + } + + /** + * Returns array of relevant context capability records. + * + * @return array + */ + public function get_capabilities() { + global $DB; + + $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display + + $params = array(); + $sql = "SELECT * + FROM {capabilities}"; + + return $DB->get_records_sql($sql.' '.$sort, $params); + } + + /** + * Create missing context instances at system context + * @static + */ + protected static function create_level_instances() { + // nothing to do here, the system context is created automatically in installer + self::instance(0); + } + + /** + * Returns system context instance. + * + * @static + * @param int $instanceid + * @param int $strictness + * @param bool $cache + * @return context_system context instance + */ + public static function instance($instanceid = 0, $strictness = MUST_EXIST, $cache = true) { + global $DB; + + if ($instanceid != 0) { + debugging('context_system::instance(): invalid $id parameter detected, should be 0'); + } + + if (defined('SYSCONTEXTID') and $cache) { // dangerous: define this in config.php to eliminate 1 query/page + if (!isset(context::$systemcontext)) { + $record = new stdClass(); + $record->id = SYSCONTEXTID; + $record->contextlevel = CONTEXT_SYSTEM; + $record->instanceid = 0; + $record->path = '/'.SYSCONTEXTID; + $record->depth = 1; + context::$systemcontext = new context_system($record); + } + return context::$systemcontext; + } + + + try { + // we ignore the strictness completely because system context must except except during install + $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); + } catch (dml_exception $e) { + //table or record does not exist + if (!during_initial_install()) { + // do not mess with system context after install, it simply must exist + throw $e; + } + $record = null; + } + + if (!$record) { + $record = new stdClass(); + $record->contextlevel = CONTEXT_SYSTEM; + $record->instanceid = 0; + $record->depth = 1; + $record->path = null; //not known before insert + + try { + if ($DB->count_records('context')) { + // contexts already exist, this is very weird, system must be first!!! + return null; + } + if (defined('SYSCONTEXTID')) { + // this would happen only in unittest on sites that went through weird 1.7 upgrade + $record->id = SYSCONTEXTID; + $DB->import_record('context', $record); + $DB->get_manager()->reset_sequence('context'); + } else { + $record->id = $DB->insert_record('context', $record); + } + } catch (dml_exception $e) { + // can not create context - table does not exist yet, sorry + return null; + } + } + + if ($record->instanceid != 0) { + // this is very weird, somebody must be messing with context table + debugging('Invalid system context detected'); + } + + if ($record->depth != 1 or $record->path != '/'.$record->id) { + // fix path if necessary, initial install or path reset + $record->depth = 1; + $record->path = '/'.$record->id; + $DB->update_record('context', $record); + } + + if (!defined('SYSCONTEXTID')) { + define('SYSCONTEXTID', $record->id); + } + + context::$systemcontext = new context_system($record); + return context::$systemcontext; + } + + /** + * Returns all site contexts except the system context, DO NOT call on production servers!! + * + * Contexts are not cached. + * + * @return array + */ + public function get_child_contexts() { + global $DB; + + debugging('Fetching of system context child courses is strongly discouraged on production servers (it may eat all available memory)!'); + + // 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; + $records = $DB->get_records_sql($sql); + + $result = array(); + foreach ($records as $record) { + $result[$record->id] = context::create_instance_from_record($record); + } + + return $result; + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + WHERE 1=2 + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at system context level. + * + * @static + * @param $force + */ + protected static function build_paths($force) { + global $DB; + + /* note: ignore $force here, we always do full test of system context */ + + // exactly one record must exist + $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); + + if ($record->instanceid != 0) { + debugging('Invalid system context detected'); + } + + if (defined('SYSCONTEXTID') and $record->id != SYSCONTEXTID) { + debugging('Invalid SYSCONTEXTID detected'); + } + + if ($record->depth != 1 or $record->path != '/'.$record->id) { + // fix path if necessary, initial install or path reset + $record->depth = 1; + $record->path = '/'.$record->id; + $DB->update_record('context', $record); + } + } +} + + +/** + * User context class + * @author Petr Skoda (http://skodak.org) + */ +class context_user extends context { + /** + * Please use context_user::instance($userid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_USER) { + throw new coding_exception('Invalid $record->contextlevel in context_user constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + protected static function get_level_name() { + return get_string('user'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with User + * @param boolean $short does not apply to user context + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false) { + global $DB; + + $name = ''; + if ($user = $DB->get_record('user', array('id'=>$this->_instanceid, 'deleted'=>0))) { + if ($withprefix){ + $name = get_string('user').': '; + } + $name .= fullname($user); + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + global $COURSE; + + if ($COURSE->id == SITEID) { + $url = new moodle_url('/user/profile.php', array('id'=>$this->_instanceid)); + } else { + $url = new moodle_url('/user/view.php', array('id'=>$this->_instanceid, 'courseid'=>$COURSE->id)); + } + return $url; + } + + /** + * Returns array of relevant context capability records. + * + * @return array + */ + public function get_capabilities() { + global $DB; + + $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display + + $extracaps = array('moodle/grade:viewall'); + list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); + $sql = "SELECT * + FROM {capabilities} + WHERE contextlevel = ".CONTEXT_USER." + OR name $extra"; + + return $records = $DB->get_records_sql($sql.' '.$sort, $params); + } + + /** + * Returns user context instance. + * + * @static + * @param int $instanceid + * @param int $strictness + * @return context_user context instance + */ + public static function instance($instanceid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_USER, $instanceid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_USER, 'instanceid'=>$instanceid))) { + if ($user = $DB->get_record('user', array('id'=>$instanceid, 'deleted'=>0), 'id', $strictness)) { + $record = context::insert_context_record(CONTEXT_USER, $user->id, '/'.SYSCONTEXTID, 0); + } + } + + if ($record) { + $context = new context_user($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Create missing context instances at user context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $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); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0) + WHERE u.id IS NULL AND c.contextlevel = ".CONTEXT_USER." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at user context level. + * + * @static + * @param $force + */ + protected static function build_paths($force) { + global $DB; + + // first update normal users + $sql = "UPDATE {context} + SET depth = 2, + path = ".$DB->sql_concat("'/".SYSCONTEXTID."/'", 'id')." + WHERE contextlevel=".CONTEXT_USER; + $DB->execute($sql); + } +} + + +/** + * Course category context class + * @author Petr Skoda (http://skodak.org) + */ +class context_coursecat extends context { + /** + * Please use context_coursecat::instance($coursecatid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_COURSECAT) { + throw new coding_exception('Invalid $record->contextlevel in context_coursecat constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + protected static function get_level_name() { + return get_string('category'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with Category + * @param boolean $short does not apply to course categories + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false) { + global $DB; + + $name = ''; + if ($category = $DB->get_record('course_categories', array('id'=>$this->_instanceid))) { + if ($withprefix){ + $name = get_string('category').': '; + } + $name .= format_string($category->name, true, array('context' => $this)); + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + return new moodle_url('/course/category.php', array('id'=>$this->_instanceid)); + } + + /** + * Returns array of relevant context capability records. + * + * @return array + */ + public function get_capabilities() { + global $DB; + + $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display + + $params = array(); + $sql = "SELECT * + FROM {capabilities} + WHERE contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")"; + + return $DB->get_records_sql($sql.' '.$sort, $params); + } + + /** + * Returns course category context instance. + * + * @static + * @param int $instanceid + * @param int $strictness + * @return context_coursecat context instance + */ + public static function instance($instanceid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_COURSECAT, $instanceid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSECAT, 'instanceid'=>$instanceid))) { + if ($category = $DB->get_record('course_categories', array('id'=>$instanceid), 'id,parent', $strictness)) { + if ($category->parent) { + $parentcontext = context_coursecat::instance($category->parent); + $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, $parentcontext->path); + } else { + $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, '/'.SYSCONTEXTID, 0); + } + } + } + + if ($record) { + $context = new context_coursecat($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Returns immediate child contexts of category and all subcategories, + * children of subcategories and courses are not returned. + * + * @return array + */ + public function get_child_contexts() { + global $DB; + + $sql = "SELECT ctx.* + FROM {context} ctx + WHERE ctx.path LIKE ? AND (ctx.depth = ? OR ctx.contextlevel = ?)"; + $params = array($this->_path.'/%', $this->depth+1, CONTEXT_COURSECAT); + $records = $DB->get_records_sql($sql, $params); + + $result = array(); + foreach ($records as $record) { + $result[$record->id] = context::create_instance_from_record($record); + } + + return $result; + } + + /** + * Create missing context instances at course category context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $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); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id + WHERE cc.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at course category context level. + * + * @static + * @param $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSECAT." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = $emptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; + } + + $base = '/'.SYSCONTEXTID; + + // Normal 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); + + // 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').", pctx.depth+1 + FROM {context} ctx + JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT." AND cc.depth = $n) + JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".CONTEXT_COURSECAT.") + WHERE pctx.path IS NOT NULL AND pctx.depth > 0 + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + + } + } + } +} + + +/** + * Course context class + * @author Petr Skoda (http://skodak.org) + */ +class context_course extends context { + /** + * Please use context_course::instance($courseid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_COURSE) { + throw new coding_exception('Invalid $record->contextlevel in context_course constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + protected static function get_level_name() { + return get_string('course'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with Course + * @param boolean $short whether to use the short name of the thing. + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false) { + global $DB; + + $name = ''; + if ($this->_instanceid == SITEID) { + $name = get_string('frontpage', 'admin'); + } else { + if ($course = $DB->get_record('course', array('id'=>$this->_instanceid))) { + if ($withprefix){ + $name = get_string('course').': '; + } + if ($short){ + $name .= format_string($course->shortname, true, array('context' => $this)); + } else { + $name .= format_string($course->fullname); + } + } + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + if ($this->_instanceid != SITEID) { + return new moodle_url('/course/view.php', array('id'=>$this->_instanceid)); + } + + return new moodle_url('/'); + } + + /** + * Returns array of relevant context capability records. + * + * @return array + */ + public function get_capabilities() { + global $DB; + + $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display + + $params = array(); + $sql = "SELECT * + FROM {capabilities} + WHERE contextlevel IN (".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")"; + + return $DB->get_records_sql($sql.' '.$sort, $params); + } + + /** + * Is this context part of any course? If yes return course context. + * + * @param bool $strict true means throw exception if not found, false means return false if not found + * @return course_context context of the enclosing course, null if not found or exception + */ + public function get_course_context($strict = true) { + return $this; + } + + /** + * Returns course context instance. + * + * @static + * @param int $instanceid + * @param int $strictness + * @return context_course context instance + */ + public static function instance($instanceid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_COURSE, $instanceid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>$instanceid))) { + if ($course = $DB->get_record('course', array('id'=>$instanceid), 'id,category', $strictness)) { + if ($course->category) { + $parentcontext = context_coursecat::instance($course->category); + $record = context::insert_context_record(CONTEXT_COURSE, $course->id, $parentcontext->path); + } else { + $record = context::insert_context_record(CONTEXT_COURSE, $course->id, '/'.SYSCONTEXTID, 0); + } + } + } + + if ($record) { + $context = new context_course($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Create missing context instances at course context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $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); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {course} co ON c.instanceid = co.id + WHERE co.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at course context level. + * + * @static + * @param $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSE." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = $emptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; + } + + $base = '/'.SYSCONTEXTID; + + // Standard frontpage + $sql = "UPDATE {context} + SET depth = 2, + path = ".$DB->sql_concat("'$base/'", 'id')." + WHERE contextlevel = ".CONTEXT_COURSE." + AND EXISTS (SELECT 'x' + FROM {course} c + WHERE c.id = {context}.instanceid AND c.category = 0) + $emptyclause"; + $DB->execute($sql); + + // standard courses + $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 (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE." AND c.category <> 0) + JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".CONTEXT_COURSECAT.") + WHERE pctx.path IS NOT NULL AND pctx.depth > 0 + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + } + } +} + + +/** + * Course module context class + * @author Petr Skoda (http://skodak.org) + */ +class context_module extends context { + /** + * Please use context_module::instance($cmid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_MODULE) { + throw new coding_exception('Invalid $record->contextlevel in context_module constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + protected static function get_level_name() { + return get_string('activitymodule'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with the + * module name, e.g. Forum, Glossary, etc. + * @param boolean $short does not apply to module context + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false) { + global $DB; + + $name = ''; + 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($this->_instanceid))) { + if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) { + if ($withprefix){ + $name = get_string('modulename', $cm->modname).': '; + } + $name .= $mod->name; + } + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + global $DB; + + 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($this->_instanceid))) { + return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$this->_instanceid)); + } + + return new moodle_url('/'); + } + + /** + * Returns array of relevant context capability records. + * + * @return array + */ + public function get_capabilities() { + global $DB, $CFG; + + $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display + + $cm = $DB->get_record('course_modules', array('id'=>$this->_instanceid)); + $module = $DB->get_record('modules', array('id'=>$cm->module)); + + $subcaps = array(); + $subpluginsfile = "$CFG->dirroot/mod/$module->name/db/subplugins.php"; + if (file_exists($subpluginsfile)) { + $subplugins = array(); // should be redefined in the file + include($subpluginsfile); + if (!empty($subplugins)) { + foreach (array_keys($subplugins) as $subplugintype) { + foreach (array_keys(get_plugin_list($subplugintype)) as $subpluginname) { + $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname))); + } + } + } + } + + $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)) { + $extracaps = $modfunction(); + } + } + if (empty($extracaps)) { + $extracaps = array(); + } + + $extracaps = array_merge($subcaps, $extracaps); + + // All modules allow viewhiddenactivities. This is so you can hide + // the module then override to allow specific roles to see it. + // The actual check is in course page so not module-specific + $extracaps[] = "moodle/course:viewhiddenactivities"; + 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"; + + return $DB->get_records_sql($sql.' '.$sort, $params); + } + + /** + * Is this context part of any course? If yes return course context. + * + * @param bool $strict true means throw exception if not found, false means return false if not found + * @return course_context context of the enclosing course, null if not found or exception + */ + public function get_course_context($strict = true) { + return $this->get_parent_context(); + } + + /** + * Returns module context instance. + * + * @static + * @param int $instanceid + * @param int $strictness + * @return context_module context instance + */ + public static function instance($instanceid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_MODULE, $instanceid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_MODULE, 'instanceid'=>$instanceid))) { + if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid), 'id,course', $strictness)) { + $parentcontext = context_course::instance($cm->course); + $record = context::insert_context_record(CONTEXT_MODULE, $cm->id, $parentcontext->path); + } + } + + if ($record) { + $context = new context_module($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Create missing context instances at module context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $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); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id + WHERE cm.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at module context level. + * + * @static + * @param $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_MODULE." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + } + + $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 (cm.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_MODULE.") + JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = ".CONTEXT_COURSE.") + WHERE pctx.path IS NOT NULL AND pctx.depth > 0 + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + } + } +} + + +/** + * Block context class + * @author Petr Skoda (http://skodak.org) + */ +class context_block extends context { + /** + * Please use context_block::instance($blockinstanceid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_BLOCK) { + throw new coding_exception('Invalid $record->contextlevel in context_block constructor'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + protected static function get_level_name() { + return get_string('block'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with Block + * @param boolean $short does not apply to block context + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false) { + global $DB, $CFG; + + $name = ''; + if ($blockinstance = $DB->get_record('block_instances', array('id'=>$this->_instanceid))) { + global $CFG; + require_once("$CFG->dirroot/blocks/moodleblock.class.php"); + require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php"); + $blockname = "block_$blockinstance->blockname"; + if ($blockobject = new $blockname()) { + if ($withprefix){ + $name = get_string('block').': '; + } + $name .= $blockobject->title; + } + } + + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + $parentcontexts = $this->get_parent_context(); + return $parentcontexts->get_url(); + } + + /** + * Returns array of relevant context capability records. + * + * @return array + */ + public function get_capabilities() { + global $DB; + + $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display + + $params = array(); + $bi = $DB->get_record('block_instances', array('id' => $this->_instanceid)); + + $extra = ''; + $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities'); + if ($extracaps) { + list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); + $extra = "OR name $extra"; + } + + $sql = "SELECT * + FROM {capabilities} + WHERE (contextlevel = ".CONTEXT_BLOCK." + AND component = :component) + $extra"; + $params['component'] = 'block_' . $bi->blockname; + + return $DB->get_records_sql($sql.' '.$sort, $params); + } + + /** + * Is this context part of any course? If yes return course context. + * + * @param bool $strict true means throw exception if not found, false means return false if not found + * @return course_context context of the enclosing course, null if not found or exception + */ + public function get_course_context($strict = true) { + $parentcontext = $this->get_parent_context(); + return $parentcontext->get_course_context($strict); + } + + /** + * Returns block context instance. + * + * @static + * @param int $instanceid + * @param int $strictness + * @return context_block context instance + */ + public static function instance($instanceid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_BLOCK, $instanceid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel'=>CONTEXT_BLOCK, 'instanceid'=>$instanceid))) { + if ($bi = $DB->get_record('block_instances', array('id'=>$instanceid), 'id,parentcontextid', $strictness)) { + $parentcontext = context::instance_by_id($bi->parentcontextid); + $record = context::insert_context_record(CONTEXT_BLOCK, $bi->id, $parentcontext->path); + } + } + + if ($record) { + $context = new context_block($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Block do not have child contexts... + * @return array + */ + public function get_child_contexts() { + return array(); + } + + /** + * Create missing context instances at block context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $sql = "INSERT INTO {context} (contextlevel, instanceid) + SELECT ".CONTEXT_BLOCK.", bi.id + FROM {block_instances} bi + WHERE NOT EXISTS (SELECT 'x' + FROM {context} cx + WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")"; + $DB->execute($sql); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id + WHERE bi.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at block context level. + * + * @static + * @param $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_BLOCK." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + } + + // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent + $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_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.") + JOIN {context} pctx ON (pctx.id = bi.parentcontextid) + WHERE (pctx.path IS NOT NULL AND pctx.depth > 0) + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + } + } +} + + +// ============== DEPRECATED ======================== + + +/** + * Use load_temp_course_role() instead. + * @deprecated + * @param stdClass $context + * @param int $roleid + * @param array $accessdata + * @return array + */ +function load_temp_role($context, $roleid, array $accessdata) { + debugging('load_temp_role() is deprecated, please use load_temp_course_role() instead, temp role not loaded.'); + return $accessdata; +} + +/** + * Use remove_temp_course_roles() instead + * @deprecated + * @param object $context + * @param array $accessdata + * @return array access data + */ +function remove_temp_roles($context, array $accessdata) { + debugging('remove_temp_role() is deprecated, please use remove_temp_course_roles() instead.'); + return $accessdata; +} + +/** + * Returns system context or null if can not be created yet. + * + * @deprecated + * @param bool $cache use caching + * @return context system context (null if context table not created yet) + */ +function get_system_context($cache = true) { + return context_system::instance(0, IGNORE_MISSING, $cache); +} + +/** + * Get the context instance as an object. This function will create the + * context instance if it does not exist yet. + * + * @deprecated + * @param integer $contextlevel The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE. + * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id, + * for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0 + * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; + * MUST_EXIST means throw exception if no record or multiple records found + * @return context The context object. + */ +function get_context_instance($contextlevel, $instance = 0, $strictness = IGNORE_MISSING) { + $instances = (array)$instance; + $contexts = array(); + + $classname = context_helper::get_class_for_level($contextlevel); + + // we do not load multiple contexts any more, PAGE should be responsible for any preloading + foreach ($instances as $inst) { + $contexts[$inst] = $classname::instance($inst, $strictness); + } + + if (is_array($instance)) { + return $contexts; + } else { + return $contexts[$instance]; + } +} + +/** + * Get a context instance as an object, from a given context id. + * + * @deprecated + * @param int $id context id + * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; + * MUST_EXIST means throw exception if no record or multiple records found + * @return context|bool the context object or false if not found. + */ +function get_context_instance_by_id($id, $strictness = IGNORE_MISSING) { + return context::instance_by_id($id, $strictness); +} + +/** + * 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. + * + * @deprecated + * @param context $context + * @param bool $includeself optional, defaults to false + * @return array + */ +function get_parent_contexts(context $context, $includeself = false) { + return $context->get_parent_context_ids($includeself); +} + +/** + * Return the id of the parent of this context, or false if there is no parent (only happens if this + * is the site context.) + * + * @deprecated + * @param context $context + * @return integer the id of the parent context. + */ +function get_parent_contextid(context $context) { + if ($parent = $context->get_parent_context()) { + return $parent->id; + } else { + return false; + } +} + +/** + * Recursive function which, given a context, find all its children context ids. + * + * For course category contexts it will return immediate children only categories and courses. + * It will NOT recurse into courses or child categories. + * If you want to do that, call it on the returned courses/categories. + * + * When called for a course context, it will return the modules and blocks + * displayed in the course page. + * + * If called on a user/course/module context it _will_ populate the cache with the appropriate + * contexts ;-) + * + * @deprecated + * @param context $context. + * @return array Array of child records + */ +function get_child_contexts(context $context) { + return $context->get_child_contexts(); +} + +/** + * Precreates all contexts including all parents + * + * @deprecated + * @param int $contextlevel empty means all + * @param bool $buildpaths update paths and depths + * @return void + */ +function create_contexts($contextlevel = null, $buildpaths = true) { + context_helper::create_instances($contextlevel, $buildpaths); +} + +/** + * Remove stale context records + * + * @deprecated + * @return bool + */ +function cleanup_contexts() { + context_helper::cleanup_instances(); + return true; +} + +/** + * Populate context.path and context.depth where missing. + * + * @deprecated + * @param bool $force force a complete rebuild of the path and depth fields, defaults to false + * @return void + */ +function build_context_path($force = false) { + context_helper::build_all_paths($force); +} + +/** + * Rebuild all related context depth and path caches + * + * @deprecated + * @param array $fixcontexts array of contexts, strongtyped + * @return void + */ +function rebuild_contexts(array $fixcontexts) { + foreach ($fixcontexts as $fixcontext) { + $fixcontext->reset_paths(false); + } + context_helper::build_all_paths(false); +} + +/** + * Preloads all contexts relating to a course: course, modules. Block contexts + * are no longer loaded here. The contexts for all the blocks on the current + * page are now efficiently loaded by {@link block_manager::load_blocks()}. + * + * @deprecated + * @param int $courseid Course ID + * @return void + */ +function preload_course_contexts($courseid) { + context_helper::preload_course($courseid); +} + +/** + * Preloads context information together with instances. + * Use context_instance_preload() to strip the context info from the record and cache the context instance. + * + * @deprecated + * @param string $joinon for example 'u.id' + * @param string $contextlevel context level of instance in $joinon + * @param string $tablealias context table alias + * @return array with two values - select and join part + */ +function context_instance_preload_sql($joinon, $contextlevel, $tablealias) { + $select = ", ".context_helper::get_preload_record_columns_sql($tablealias); + $join = "LEFT JOIN {context} $tablealias ON ($tablealias.instanceid = $joinon AND $tablealias.contextlevel = $contextlevel)"; + return array($select, $join); +} + +/** + * Preloads context information from db record and strips the cached info. + * The db request has to contain both the $join and $select from context_instance_preload_sql() + * + * @deprecated + * @param stdClass $rec + * @return void (modifies $rec) + */ +function context_instance_preload(stdClass $rec) { + context_helper::preload_from_record($rec); +} + +/** + * Mark a context as dirty (with timestamp) so as to force reloading of the context. + * + * NOTE: use $context->mark_dirty() instead + * + * @deprecated + * @param string $path context path + */ +function mark_context_dirty($path) { + global $CFG, $USER, $ACCESSLIB_PRIVATE; + + if (during_initial_install()) { + 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; + } else { + if (CLI_SCRIPT) { + $ACCESSLIB_PRIVATE->dirtycontexts = array($path => 1); + } else { + if (isset($USER->access['time'])) { + $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time']-2); + } else { + $ACCESSLIB_PRIVATE->dirtycontexts = array($path => 1); + } + // flags not loaded yet, it will be done later in $context->reload_if_dirty() + } + } + } +} + +/** + * Update the path field of the context and all dep. subcontexts that follow + * + * 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. + * + * @deprecated + * @param context $context context obj + * @param context $newparent new parent obj + * @return void + */ +function context_moved(context $context, context $newparent) { + $context->update_moved($newparent); +} + +/** + * Remove a context record and any dependent entries, + * removes context from static context cache too + * + * @deprecated + * @param int $contextlevel + * @param int $instanceid + * @param bool $deleterecord false means keep record for now + * @return bool returns true or throws an exception + */ +function delete_context($contextlevel, $instanceid, $deleterecord = true) { + if ($deleterecord) { + context_helper::delete_instance($contextlevel, $instanceid); + } else { + $classname = context_helper::get_class_for_level($contextlevel); + if ($context = $classname::instance($instanceid, IGNORE_MISSING)) { + $context->delete_content(); + } + } + + return true; +} + +/** + * + * @deprecated + * @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) { + return context_helper::get_level_name($contextlevel); +} + +/** + * Prints human readable context identifier. + * + * @deprecated + * @param context $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 $context, $withprefix = true, $short = false) { + return $context->get_context_name($withprefix, $short); +} + +/** + * 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. + * + * @deprecated + * @param context $context the context. + * @return moodle_url + */ +function get_context_url(context $context) { + return $context->get_url(); +} + +/** + * Is this context part of any course? if yes return course context, + * if not return null or throw exception. + * + * @deprecated + * @param context $context + * @return course_context context of the enclosing course, null if not found or exception + */ +function get_course_context(context $context) { + return $context->get_course_context(true); +} + +/** + * Returns current course id or null if outside of course based on context parameter. + * + * @deprecated + * @param context $context + * @return int|bool related course id or false + */ +function get_courseid_from_context(context $context) { + if ($coursecontext = $context->get_course_context(false)) { + return $coursecontext->instanceid; + } else { + return false; + } +} + +/** + * Get an array of courses where cap requested is available + * and user is enrolled, this can be relatively slow. + * + * @deprecated + * @param int $userid A user id. By default (null) checks the permissions of the current user. + * @param string $cap - name of the capability + * @param array $accessdata_ignored + * @param bool $doanything_ignored + * @param string $sort - sorting fields - prefix each fieldname with "c." + * @param array $fields - additional fields you are interested in... + * @param int $limit_ignored + * @return array $courses - ordered array of course objects - see notes above + */ +function get_user_courses_bycap($userid, $cap, $accessdata_ignored, $doanything_ignored, $sort = 'c.sortorder ASC', $fields = null, $limit_ignored = 0) { + + $courses = enrol_get_users_courses($userid, true, $fields, $sort); + foreach ($courses as $id=>$course) { + $context = context_course::instance($id); + if (!has_capability($cap, $context, $userid)) { + unset($courses[$id]); + } + } + + return $courses; +} + +/** + * 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 + * + * capabilities + * `name` varchar(150) NOT NULL, + * `captype` varchar(50) NOT NULL, + * `contextlevel` int(10) NOT NULL, + * `component` varchar(100) NOT NULL, + * + * @deprecated + * @param context $context + * @return array + */ +function fetch_context_capabilities(context $context) { + return $context->get_capabilities(); +} + +/** + * Runs get_records select on context table and returns the result + * 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'); + * + * @deprecated + * @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()) { + + //TODO: we should probably rewrite all the code that is using this thing, the trouble is we MUST NOT modify the context instances... + + global $DB; + if ($select) { + $select = 'WHERE ' . $select; + } + return $DB->get_records_sql(" + SELECT ctx.* + FROM {context} ctx + LEFT JOIN {user} u ON ctx.contextlevel = " . CONTEXT_USER . " AND u.id = ctx.instanceid + LEFT JOIN {course_categories} cat ON ctx.contextlevel = " . CONTEXT_COURSECAT . " AND cat.id = ctx.instanceid + LEFT JOIN {course} c ON ctx.contextlevel = " . CONTEXT_COURSE . " AND c.id = ctx.instanceid + LEFT JOIN {course_modules} cm ON ctx.contextlevel = " . CONTEXT_MODULE . " AND cm.id = ctx.instanceid + LEFT JOIN {block_instances} bi ON ctx.contextlevel = " . CONTEXT_BLOCK . " AND bi.id = ctx.instanceid + $select + ORDER BY ctx.contextlevel, bi.defaultregion, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.defaultweight), u.lastname, u.firstname, cm.id + ", $params); +} + +/** + * This is really slow!!! do not use above course context level + * + * @deprecated + * @param int $roleid + * @param context $context + * @return array + */ +function get_role_context_caps($roleid, context $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($context->get_parent_context_ids(true)); + 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($context->get_child_contexts()); + 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 a string for sql calls, searching for stuff in this context or above + * + * NOTE: use $DB->get_in_or_equal($context->get_parent_context_ids()... + * + * @deprecated + * @param context $context + * @return string + */ +function get_related_contexts_string(context $context) { + + if ($parents = $context->get_parent_context_ids()) { + return (' IN ('.$context->id.','.implode(',', $parents).')'); + } else { + return (' ='.$context->id); + } +} diff --git a/lib/datalib.php b/lib/datalib.php index 478156cd712..ef75c4651a6 100644 --- a/lib/datalib.php +++ b/lib/datalib.php @@ -1014,7 +1014,11 @@ function fix_course_sortorder() { // now fix the paths and depths in context table if needed if ($fixcontexts) { - rebuild_contexts($fixcontexts); + foreach ($fixcontexts as $fixcontext) { + $fixcontext->reset_paths(false); + } + context_helper::build_all_paths(false); + unset($fixcontexts); } // release memory diff --git a/lib/db/install.php b/lib/db/install.php index 03797855fe0..de4c45b8547 100644 --- a/lib/db/install.php +++ b/lib/db/install.php @@ -29,14 +29,17 @@ defined('MOODLE_INTERNAL') || die(); function xmldb_main_install() { global $CFG, $DB, $SITE; - /// make sure system context exists - $syscontext = get_system_context(false); - if ($syscontext->id != 1) { + /// Make sure system context exists + $syscontext = context_system::instance(0, MUST_EXIST, false); + if ($syscontext->id != SYSCONTEXTID) { throw new moodle_exception('generalexceptionmessage', 'error', '', 'Unexpected new system context id!'); } - /// create site course + /// Create site course + if ($DB->record_exists('course', array())) { + throw new moodle_exception('generalexceptionmessage', 'error', '', 'Can not create frontpage course, courses already exist.'); + } $newsite = new stdClass(); $newsite->fullname = ''; $newsite->shortname = ''; @@ -48,18 +51,38 @@ function xmldb_main_install() { $newsite->timecreated = time(); $newsite->timemodified = $newsite->timecreated; - $newsite->id = $DB->insert_record('course', $newsite); + if (defined('SITEID')) { + $newsite->id = SITEID; + $DB->import_record('course', $newsite); + $DB->get_manager()->reset_sequence('course'); + } else { + $newsite->id = $DB->insert_record('course', $newsite); + define('SITEID', $newsite->id); + } $SITE = get_site(); - if ($newsite->id != 1 or $SITE->id != 1) { + if ($newsite->id != $SITE->id) { throw new moodle_exception('generalexceptionmessage', 'error', '', 'Unexpected new site course id!'); } + // Make sure site course context exists + context_course::instance($SITE->id); + // Update the global frontpage cache + $SITE = $DB->get_record('course', array('id'=>$newsite->id), '*', MUST_EXIST); - /// make sure site course context exists - get_context_instance(CONTEXT_COURSE, $SITE->id); + /// Create default course category + if ($DB->record_exists('course_categories', array())) { + throw new moodle_exception('generalexceptionmessage', 'error', '', 'Can not create default course category, categories already exist.'); + } + $cat = new stdClass(); + $cat->name = get_string('miscellaneous'); + $cat->depth = 1; + $cat->sortorder = MAX_COURSES_IN_CATEGORY; + $cat->timemodified = time(); + $catid = $DB->insert_record('course_categories', $cat); + $DB->set_field('course_categories', 'path', '/'.$catid, array('id'=>$catid)); + // Make sure category context exists + context_coursecat::instance($catid); - /// create default course category - $cat = get_course_category(); $defaults = array( 'rolesactive' => '0', // marks fully set up system @@ -82,7 +105,7 @@ function xmldb_main_install() { } - /// bootstrap mnet + /// Bootstrap mnet $mnethost = new stdClass(); $mnethost->wwwroot = $CFG->wwwroot; $mnethost->name = ''; @@ -137,7 +160,10 @@ function xmldb_main_install() { $mnetallhosts->id = $DB->insert_record('mnet_host', $mnetallhosts, true); set_config('mnet_all_hosts_id', $mnetallhosts->id); - /// Create guest record - do not assign any role, guest user get's the default guest role automatically on the fly + /// Create guest record - do not assign any role, guest user gets the default guest role automatically on the fly + if ($DB->record_exists('user', array())) { + throw new moodle_exception('generalexceptionmessage', 'error', '', 'Can not create default users, users already exist.'); + } $guest = new stdClass(); $guest->auth = 'manual'; $guest->username = 'guest'; @@ -156,6 +182,8 @@ function xmldb_main_install() { } // Store guest id set_config('siteguest', $guest->id); + // Make sure user context exists + context_user::instance($guest->id); /// Now create admin user @@ -176,8 +204,10 @@ function xmldb_main_install() { if ($admin->id != 2) { throw new moodle_exception('generalexceptionmessage', 'error', '', 'Unexpected new admin user id!'); } - /// Store list of admins + // Store list of admins set_config('siteadmins', $admin->id); + // Make sure user context exists + context_user::instance($admin->id); /// Install the roles system. @@ -264,17 +294,16 @@ function xmldb_main_install() { require_once($CFG->libdir . '/licenselib.php'); license_manager::install_licenses(); - /// Add two lines of data into this new table + // Init profile pages defaults + if ($DB->record_exists('my_pages', array())) { + throw new moodle_exception('generalexceptionmessage', 'error', '', 'Can not create default profile pages, records already exist.'); + } $mypage = new stdClass(); $mypage->userid = NULL; $mypage->name = '__default'; $mypage->private = 0; $mypage->sortorder = 0; - if (!$DB->record_exists('my_pages', array('userid'=>NULL, 'private'=>0))) { - $DB->insert_record('my_pages', $mypage); - } + $DB->insert_record('my_pages', $mypage); $mypage->private = 1; - if (!$DB->record_exists('my_pages', array('userid'=>NULL, 'private'=>1))) { - $DB->insert_record('my_pages', $mypage); - } + $DB->insert_record('my_pages', $mypage); } diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index d822bda4a78..5b3c9ca860b 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -280,28 +280,28 @@ function isteacher() { * @deprecated */ function isteacherinanycourse() { - error('Function isteacherinanycourse() was removed, please use capabilities instead!'); + throw new coding_Exception('Function isteacherinanycourse() was removed, please use capabilities instead!'); } /** * @deprecated */ function get_guest() { - error('Function get_guest() was removed, please use capabilities instead!'); + throw new coding_Exception('Function get_guest() was removed, please use capabilities instead!'); } /** * @deprecated */ function isguest() { - error('Function isguest() was removed, please use capabilities instead!'); + throw new coding_Exception('Function isguest() was removed, please use capabilities instead!'); } /** * @deprecated */ function get_teacher() { - error('Function get_teacher() was removed, please use capabilities instead!'); + throw new coding_Exception('Function get_teacher() was removed, please use capabilities instead!'); } /** @@ -371,15 +371,7 @@ function get_recent_enrolments($courseid, $timestart) { * @return object */ 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; + throw new coding_Exception('make_context_subobj() was removed, use new context preloading'); } /** @@ -395,10 +387,7 @@ function make_context_subobj($rec) { * 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; + throw new coding_Exception('is_context_subobj_valid() was removed, use new context preloading'); } /** @@ -415,9 +404,7 @@ function is_context_subobj_valid($rec, $contextlevel) { * @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); - } + throw new coding_Exception('ensure_context_subobj_present() was removed, use new context preloading'); } ########### FROM weblib.php ########################################################################## diff --git a/lib/enrollib.php b/lib/enrollib.php index 9f6795daf19..fe3dce2a2e3 100644 --- a/lib/enrollib.php +++ b/lib/enrollib.php @@ -188,7 +188,9 @@ function enrol_is_enabled($enrol) { * Check all the login enrolment information for the given user object * by querying the enrolment plugins * - * @param object $user + * This function may be very slow, use only once after log-in or login-as. + * + * @param stdClass $user * @return void */ function enrol_check_plugins($user) { @@ -201,7 +203,7 @@ function enrol_check_plugins($user) { if (is_siteadmin()) { // no sync for admin user, please use admin accounts only for admin tasks like the unix root user! - // if plugin fails on sync admins need to be able to log in + // if plugin fails on sync admins need to be able to log in and fix the settings return; } @@ -1150,7 +1152,7 @@ abstract class enrol_plugin { } if (isset($USER->enrol['tempguest'][$courseid])) { unset($USER->enrol['tempguest'][$courseid]); - $USER->access = remove_temp_roles($context, $USER->access); + remove_temp_course_roles($context); } } } @@ -1272,7 +1274,7 @@ abstract class enrol_plugin { } if (isset($USER->enrol['tempguest'][$courseid])) { unset($USER->enrol['tempguest'][$courseid]); - $USER->access = remove_temp_roles($context, $USER->access); + remove_temp_course_roles($context); } } } diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 5f5e9350ed7..49a0cda2c42 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -2790,7 +2790,7 @@ function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $ } else { //expired unset($USER->enrol['tempguest'][$course->id]); - $USER->access = remove_temp_roles($coursecontext, $USER->access); + remove_temp_course_roles($coursecontext); } } @@ -2817,7 +2817,7 @@ function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $ $access = true; // remove traces of previous temp guest access - $USER->access = remove_temp_roles($coursecontext, $USER->access); + remove_temp_course_roles($coursecontext); } else { $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC'); @@ -2831,7 +2831,7 @@ function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $ $until = $enrols[$instance->enrol]->try_autoenrol($instance); if ($until !== false) { $USER->enrol['enrolled'][$course->id] = $until; - $USER->access = remove_temp_roles($coursecontext, $USER->access); + remove_temp_course_roles($coursecontext); $access = true; break; } @@ -3031,6 +3031,7 @@ function require_user_key_login($script, $instance=null) { } /// emulate normal session + enrol_check_plugins($user); session_set_user($user); /// note we are not using normal login @@ -3882,12 +3883,15 @@ function complete_user_login($user) { // this helps prevent session fixation attacks from the same domain session_regenerate_id(true); + // let enrol plugins deal with new enrolments if necessary + enrol_check_plugins($user); + // check enrolments, load caps and setup $USER object session_set_user($user); // reload preferences from DB - unset($user->preference); - check_user_preferences_loaded($user); + unset($USER->preference); + check_user_preferences_loaded($USER); // update login times update_user_login_times(); diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 43ff1a2d114..e5463fd1e20 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -2868,7 +2868,7 @@ class settings_navigation extends navigation_node { $context = $this->context; if ($context->contextlevel == CONTEXT_BLOCK) { $this->load_block_settings(); - $context = $DB->get_record_sql('SELECT ctx.* FROM {block_instances} bi LEFT JOIN {context} ctx ON ctx.id=bi.parentcontextid WHERE bi.id=?', array($context->instanceid)); + $context = $context->get_parent_context(); } switch ($context->contextlevel) { diff --git a/lib/sessionlib.php b/lib/sessionlib.php index 53f03155f47..c869ba86afa 100644 --- a/lib/sessionlib.php +++ b/lib/sessionlib.php @@ -894,7 +894,8 @@ function get_moodle_cookie() { /** * Setup $USER object - called during login, loginas, etc. - * Preloads capabilities and checks enrolment plugins + * + * Call sync_user_enrolments() manually after log-in, or log-in-as. * * @param stdClass $user full user record object * @return void @@ -902,11 +903,6 @@ function get_moodle_cookie() { function session_set_user($user) { $_SESSION['USER'] = $user; unset($_SESSION['USER']->description); // conserve memory - if (!isset($_SESSION['USER']->access)) { - // check enrolments and load caps only once - enrol_check_plugins($_SESSION['USER']); - load_all_capabilities(); - } sesskey(); // init session key } @@ -950,6 +946,10 @@ function session_loginas($userid, $context) { $user = get_complete_user_data('id', $userid); $user->realuser = $_SESSION['REALUSER']->id; $user->loginascontext = $context; + + // let enrol plugins deal with new enrolments if necessary + enrol_check_plugins($user); + // set up global $USER session_set_user($user); } diff --git a/lib/simpletest/fulltestaccesslib.php b/lib/simpletest/fulltestaccesslib.php new file mode 100644 index 00000000000..b92e2c0fc68 --- /dev/null +++ b/lib/simpletest/fulltestaccesslib.php @@ -0,0 +1,1172 @@ +. + +/** + * Full functional accesslib test + * + * It is implemented as one test case because it would take hours + * to prepare the fake test site for each test, at the same time + * we have to work around multiple problems in UnitTestCaseUsingDatabase. + * + * @package core + * @copyright 2011 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Context caching fixture + */ +class context_inspection extends context_helper { + public static function test_context_cache_size() { + return self::$cache_count; + } +} + + +/** + * Functional test for accesslib.php + * + * Note: execution may take many minutes especially on slower servers. + */ +class accesslib_test extends UnitTestCaseUsingDatabase { + // NOTE: The UnitTestCaseUsingDatabase is problematic and VERY dangerous especially for production servers! + // + // 1. We could create full install without plugins and use the test DB since the very beginning in lib/setup.php. + // 2. We should not execute the tests from the web UI because the static caches all over the place collide, instead + // we could use CLI + // 3. SimpleTest appears to be dead - unfortunately nobody else thinks we should switch to PHPUnit asap + // 4. switching of USER->id alone is not acceptable at all, we must switch the complete USER record + // and probably deal with _SESSION['USER'] too + // 5. we must close session before messing with anything in $USER or $SESSION + // 6. $SITE is not switched + // 7. tons of other static caches are not reset + // 8. etc. + // + // -- + // skodak + + //TODO: add more tests for the remaining accesslib parts that were not touched by the refactoring in 2.2dev + + public static $includecoverage = array('lib/accesslib.php'); + + protected $accesslibprevuser = null; + protected $accesslibprevsite = null; + + /** + * A small functional test of accesslib functions and classes. + */ + public function test_everything_in_accesslib() { + global $USER, $SITE, $CFG, $DB, $ACCESSLIB_PRIVATE; + + // First of all finalize the session, we must not carry over any of this mess to the next page via SESSION!!! + session_get_instance()->write_close(); + + // hack - this is going to take very long time + set_time_limit(3600); + + // Get rid of current user that would not work anyway, + // do NOT even think about using $this->switch_global_user_id()! + if (!isset($this->accesslibprevuser)) { + $this->accesslibprevuser = clone($USER); + $USER = new stdClass(); + $USER->id = 0; + } + + // Backup $SITE global + if (!isset($this->accesslibprevsite)) { + $this->accesslibprevsite = $SITE; + } + + // Switch DB, if it somehow fails or you specified wrong unittest prefix it will nuke real data, you have been warned! + $this->switch_to_test_db(); + + // Let's switch the CFG + $prevcfg = clone($CFG); + $this->switch_to_test_cfg(); + $CFG->config_php_settings = $prevcfg->config_php_settings; + $CFG->noemailever = true; + $CFG->admin = $prevcfg->admin; + $CFG->lang = 'en'; + $CFG->tempdir = $prevcfg->tempdir; + $CFG->cachedir = $prevcfg->cachedir; + $CFG->directorypermissions = $prevcfg->directorypermissions; + $CFG->filepermissions = $prevcfg->filepermissions; + + // Reset all caches + accesslib_clear_all_caches_for_unit_testing(); + + // Add some core tables - this is known to break constantly because we keep adding dependencies... + $tablenames = array('config', 'config_plugins', 'modules', 'course', 'course_modules', 'course_sections', 'course_categories', 'mnet_host', 'mnet_application', + 'capabilities', 'context', 'context_temp', 'role', 'role_capabilities', 'role_allow_switch', 'license', 'my_pages', 'block', 'block_instances', 'block_positions', + 'role_allow_assign', 'role_allow_override', 'role_assignments', 'role_context_levels' ,'enrol', 'user_enrolments', 'filter_active', 'filter_config', 'comments', + 'user', 'groups_members', 'cache_flags', 'events_handlers', 'user_lastaccess', 'rating', 'files', 'role_names', 'user_preferences'); + $this->create_test_tables($tablenames, 'lib'); + + // Create all core default records and default settings + require_once("$CFG->libdir/db/install.php"); + xmldb_main_install(); // installs the capabilities too + + // Fake mod_page install + $tablenames = array('page'); + $this->create_test_tables($tablenames, 'mod/page'); + $module = new stdClass(); + require($CFG->dirroot .'/mod/page/version.php'); + $module->name = 'page'; + $pagemoduleid = $DB->insert_record('modules', $module); + update_capabilities('mod_page'); + + // Fake block_online_users install + $plugin = new stdClass(); + $plugin->version = NULL; + $plugin->cron = 0; + include($CFG->dirroot.'/blocks/online_users/version.php'); + $plugin->name = 'online_users'; + $onlineusersblockid = $DB->insert_record('block', $plugin); + update_capabilities('block_online_users'); + + // Finish roles setup + set_config('defaultfrontpageroleid', $DB->get_field('role', 'id', array('archetype'=>'frontpage'))); + set_config('defaultuserroleid', $DB->get_field('role', 'id', array('archetype'=>'user'))); + set_config('notloggedinroleid', $DB->get_field('role', 'id', array('archetype'=>'guest'))); + set_config('rolesactive', 1); + + // Init manual enrol + set_config('status', ENROL_INSTANCE_ENABLED, 'enrol_manual'); + set_config('defaultperiod', 0, 'enrol_manual'); + $manualenrol = enrol_get_plugin('manual'); + + // Fill the site with some real data + $testcategories = array(); + $testcourses = array(); + $testpages = array(); + $testblocks = array(); + $allroles = $DB->get_records_menu('role', array(), 'id', 'archetype, id'); + + $systemcontext = context_system::instance(); + $frontpagecontext = context_course::instance(SITEID); + + // Add block to system context + $bi = new stdClass(); + $bi->blockname = 'online_users'; + $bi->parentcontextid = $systemcontext->id; + $bi->showinsubcontexts = 1; + $bi->pagetypepattern = ''; + $bi->subpagepattern = ''; + $bi->defaultregion = ''; + $bi->defaultweight = 0; + $bi->configdata = ''; + $biid = $DB->insert_record('block_instances', $bi); + context_block::instance($biid); + $testblocks[] = $biid; + + // Some users + $testusers = array(); + for($i=0; $i<20; $i++) { + $user = new stdClass(); + $user->auth = 'manual'; + $user->firstname = 'user'.$i; + $user->lastname = 'user'.$i; + $user->username = 'user'.$i; + $user->password = 'doesnotexist'; + $user->email = "user$i@example.com"; + $user->confirmed = 1; + $user->mnethostid = $CFG->mnet_localhost_id; + $user->lang = $CFG->lang; + $user->maildisplay = 1; + $user->timemodified = time(); + $user->deleted = 0; + $user->lastip = '0.0.0.0'; + $userid = $DB->insert_record('user', $user); + $testusers[$i] = $userid; + $usercontext = context_user::instance($userid); + + // Add block to user profile + $bi->parentcontextid = $usercontext->id; + $biid = $DB->insert_record('block_instances', $bi); + context_block::instance($biid); + $testblocks[] = $biid; + } + // Deleted user - should be ignored everywhere, can not have context + $user = new stdClass(); + $user->auth = 'manual'; + $user->firstname = ''; + $user->lastname = ''; + $user->username = 'user@example.com121132132'; + $user->password = ''; + $user->email = ''; + $user->confirmed = 1; + $user->mnethostid = $CFG->mnet_localhost_id; + $user->lang = $CFG->lang; + $user->maildisplay = 1; + $user->timemodified = time(); + $user->deleted = 1; + $user->lastip = '0.0.0.0'; + $DB->insert_record('user', $user); + unset($user); + + // Add block to frontpage + $bi->parentcontextid = $frontpagecontext->id; + $biid = $DB->insert_record('block_instances', $bi); + $frontpageblockcontext = context_block::instance($biid); + $testblocks[] = $biid; + + // Add a resource to frontpage + $page = new stdClass(); + $page->course = $SITE->id; + $page->intro = '...'; + $page->introformat = FORMAT_HTML; + $page->content = '...'; + $page->contentformat = FORMAT_HTML; + $pageid = $DB->insert_record('page', $page); + $testpages[] = $pageid; + $cm = new stdClass(); + $cm->course = $SITE->id; + $cm->module = $pagemoduleid; + $cm->section = 1; + $cm->instance = $pageid; + $cmid = $DB->insert_record('course_modules', $cm); + $frontpagepagecontext = context_module::instance($cmid); + + // Add block to frontpage resource + $bi->parentcontextid = $frontpagepagecontext->id; + $biid = $DB->insert_record('block_instances', $bi); + $frontpagepageblockcontext = context_block::instance($biid); + $testblocks[] = $biid; + + // Some nested course categories with courses + require_once("$CFG->dirroot/course/lib.php"); + $path = ''; + $parentcat = 0; + for($i=0; $i<5; $i++) { + $cat = new stdClass(); + $cat->name = 'category'.$i; + $cat->parent = $parentcat; + $cat->depth = $i+1; + $cat->sortorder = MAX_COURSES_IN_CATEGORY * ($i+2); + $cat->timemodified = time(); + $catid = $DB->insert_record('course_categories', $cat); + $path = $path . '/' . $catid; + $DB->set_field('course_categories', 'path', $path, array('id'=>$catid)); + $parentcat = $catid; + $testcategories[] = $catid; + $catcontext = context_coursecat::instance($catid); + + if ($i >=4) { + continue; + } + + // Add resource to each category + $bi->parentcontextid = $catcontext->id; + $biid = $DB->insert_record('block_instances', $bi); + context_block::instance($biid); + + // Add a few courses to each category + for($j=0; $j<6; $j++) { + $course = new stdClass(); + $course->fullname = 'course'.$j; + $course->shortname = 'c'.$j; + $course->summary = 'bah bah bah'; + $course->newsitems = 0; + $course->numsections = 1; + $course->category = $catid; + $course->format = 'topics'; + $course->timecreated = time(); + $course->visible = 1; + $course->timemodified = $course->timecreated; + $courseid = $DB->insert_record('course', $course); + $section = new stdClass(); + $section->course = $courseid; + $section->section = 0; + $section->summaryformat = FORMAT_HTML; + $DB->insert_record('course_sections', $section); + $section->section = 1; + $DB->insert_record('course_sections', $section); + $testcourses[] = $courseid; + $coursecontext = context_course::instance($courseid); + + if ($j >= 5) { + continue; + } + // Add manual enrol instance + $manualenrol->add_default_instance($DB->get_record('course', array('id'=>$courseid))); + + // Add block to each course + $bi->parentcontextid = $coursecontext->id; + $biid = $DB->insert_record('block_instances', $bi); + context_block::instance($biid); + $testblocks[] = $biid; + + // Add a resource to each course + $page->course = $courseid; + $pageid = $DB->insert_record('page', $page); + $testpages[] = $pageid; + $cm->course = $courseid; + $cm->instance = $pageid; + $cm->id = $DB->insert_record('course_modules', $cm); + $modcontext = context_module::instance($cm->id); + + // Add block to each module + $bi->parentcontextid = $modcontext->id; + $biid = $DB->insert_record('block_instances', $bi); + context_block::instance($biid); + $testblocks[] = $biid; + } + } + + // Make sure all contexts were created properly + $count = 1; //system + $count += $DB->count_records('user', array('deleted'=>0)); + $count += $DB->count_records('course_categories'); + $count += $DB->count_records('course'); + $count += $DB->count_records('course_modules'); + $count += $DB->count_records('block_instances'); + $this->assertEqual($DB->count_records('context'), $count); + $this->assertEqual($DB->count_records('context', array('depth'=>0)), 0); + $this->assertEqual($DB->count_records('context', array('path'=>NULL)), 0); + + + // ====== context_helper::get_level_name() ================================ + + $levels = context_helper::get_all_levels(); + foreach ($levels as $level=>$classname) { + $name = context_helper::get_level_name($level); + $this->assertFalse(empty($name)); + } + + + // ======= context::instance_by_id(), context_xxx::instance(); + + $context = context::instance_by_id($frontpagecontext->id); + $this->assertidentical($context->contextlevel, CONTEXT_COURSE); + $this->assertFalse(context::instance_by_id(-1, IGNORE_MISSING)); + try { + context::instance_by_id(-1); + $this->fail('exception expected'); + } catch (Exception $e) { + $this->assertTrue(true); + } + $this->assertTrue(context_system::instance() instanceof context_system); + $this->assertTrue(context_coursecat::instance($testcategories[0]) instanceof context_coursecat); + $this->assertTrue(context_course::instance($testcourses[0]) instanceof context_course); + $this->assertTrue(context_module::instance($testpages[0]) instanceof context_module); + $this->assertTrue(context_block::instance($testblocks[0]) instanceof context_block); + + $this->assertFalse(context_coursecat::instance(-1, IGNORE_MISSING)); + $this->assertFalse(context_course::instance(-1, IGNORE_MISSING)); + $this->assertFalse(context_module::instance(-1, IGNORE_MISSING)); + $this->assertFalse(context_block::instance(-1, IGNORE_MISSING)); + try { + context_coursecat::instance(-1); + $this->fail('exception expected'); + } catch (Exception $e) { + $this->assertTrue(true); + } + try { + context_course::instance(-1); + $this->fail('exception expected'); + } catch (Exception $e) { + $this->assertTrue(true); + } + try { + context_module::instance(-1); + $this->fail('exception expected'); + } catch (Exception $e) { + $this->assertTrue(true); + } + try { + context_block::instance(-1); + $this->fail('exception expected'); + } catch (Exception $e) { + $this->assertTrue(true); + } + + + // ======= $context->get_url(), $context->get_context_name(), $context->get_capabilities() ========= + + $testcontexts = array(); + $testcontexts[CONTEXT_SYSTEM] = context_system::instance(); + $testcontexts[CONTEXT_COURSECAT] = context_coursecat::instance($testcategories[0]); + $testcontexts[CONTEXT_COURSE] = context_course::instance($testcourses[0]); + $testcontexts[CONTEXT_MODULE] = context_module::instance($testpages[0]); + $testcontexts[CONTEXT_BLOCK] = context_block::instance($testblocks[0]); + + foreach ($testcontexts as $context) { + $name = $context->get_context_name(true, true); + $this->assertFalse(empty($name)); + + $this->assertTrue($context->get_url() instanceof moodle_url); + + $caps = $context->get_capabilities(); + $this->assertTrue(is_array($caps)); + foreach ($caps as $cap) { + $cap = (array)$cap; + $this->assertIdentical(array_keys($cap), array('id', 'name', 'captype', 'contextlevel', 'component', 'riskbitmask')); + } + } + unset($testcontexts); + + // ===== $context->get_course_context() ========================================= + + $this->assertFalse($systemcontext->get_course_context(false)); + try { + $systemcontext->get_course_context(); + $this->fail('exception expected'); + } catch (Exception $e) { + $this->assertTrue(true); + } + $context = context_coursecat::instance($testcategories[0]); + $this->assertFalse($context->get_course_context(false)); + try { + $context->get_course_context(); + $this->fail('exception expected'); + } catch (Exception $e) { + $this->assertTrue(true); + } + $this->assertIdentical($frontpagecontext->get_course_context(true), $frontpagecontext); + $this->assertIdentical($frontpagepagecontext->get_course_context(true), $frontpagecontext); + $this->assertIdentical($frontpagepageblockcontext->get_course_context(true), $frontpagecontext); + + + // ======= $context->get_parent_context(), $context->get_parent_contexts(), $context->get_parent_context_ids() ======= + + $userid = reset($testusers); + $usercontext = context_user::instance($userid); + $this->assertIdentical($usercontext->get_parent_context(), $systemcontext); + $this->assertIdentical($usercontext->get_parent_contexts(), array($systemcontext->id=>$systemcontext)); + $this->assertIdentical($usercontext->get_parent_contexts(true), array($usercontext->id=>$usercontext, $systemcontext->id=>$systemcontext)); + + $this->assertIdentical($systemcontext->get_parent_contexts(), array()); + $this->assertIdentical($systemcontext->get_parent_contexts(true), array($systemcontext->id=>$systemcontext)); + $this->assertIdentical($systemcontext->get_parent_context_ids(), array()); + $this->assertIdentical($systemcontext->get_parent_context_ids(true), array($systemcontext->id)); + + $this->assertIdentical($frontpagecontext->get_parent_context(), $systemcontext); + $this->assertIdentical($frontpagecontext->get_parent_contexts(), array($systemcontext->id=>$systemcontext)); + $this->assertIdentical($frontpagecontext->get_parent_contexts(true), array($frontpagecontext->id=>$frontpagecontext, $systemcontext->id=>$systemcontext)); + $this->assertIdentical($frontpagecontext->get_parent_context_ids(), array($systemcontext->id)); + $this->assertEqual($frontpagecontext->get_parent_context_ids(true), array($frontpagecontext->id, $systemcontext->id)); + + $this->assertIdentical($systemcontext->get_parent_context(), false); + $frontpagecontext = context_course::instance($SITE->id); + $parent = $systemcontext; + foreach ($testcategories as $catid) { + $catcontext = context_coursecat::instance($catid); + $this->assertIdentical($catcontext->get_parent_context(), $parent); + $parent = $catcontext; + } + $this->assertIdentical($frontpagepagecontext->get_parent_context(), $frontpagecontext); + $this->assertIdentical($frontpageblockcontext->get_parent_context(), $frontpagecontext); + $this->assertIdentical($frontpagepageblockcontext->get_parent_context(), $frontpagepagecontext); + + + // ====== $context->get_child_contexts() ================================ + + $children = $systemcontext->get_child_contexts(); + $this->assertEqual(count($children)+1, $DB->count_records('context')); + + $context = context_coursecat::instance($testcategories[3]); + $children = $context->get_child_contexts(); + $countcats = 0; + $countcourses = 0; + $countblocks = 0; + foreach ($children as $child) { + if ($child->contextlevel == CONTEXT_COURSECAT) { + $countcats++; + } + if ($child->contextlevel == CONTEXT_COURSE) { + $countcourses++; + } + if ($child->contextlevel == CONTEXT_BLOCK) { + $countblocks++; + } + } + $this->assertEqual(count($children), 8); + $this->assertEqual($countcats, 1); + $this->assertEqual($countcourses, 6); + $this->assertEqual($countblocks, 1); + + $context = context_course::instance($testcourses[2]); + $children = $context->get_child_contexts(); + $this->assertEqual(count($children), 3); + + $context = context_module::instance($testpages[3]); + $children = $context->get_child_contexts(); + $this->assertEqual(count($children), 1); + + $context = context_block::instance($testblocks[1]); + $children = $context->get_child_contexts(); + $this->assertEqual(count($children), 0); + + unset($children); + unset($countcats); + unset($countcourses); + unset($countblocks); + + + // ======= context_helper::reset_caches() ============================ + + context_helper::reset_caches(); + $this->assertEqual(context_inspection::test_context_cache_size(), 0); + context_course::instance($SITE->id); + $this->assertEqual(context_inspection::test_context_cache_size(), 1); + + + // ======= context preloading ======================================== + + context_helper::reset_caches(); + $sql = "SELECT ".context_helper::get_preload_record_columns_sql('c')." + FROM {context} c + WHERE c.contextlevel <> ".CONTEXT_SYSTEM; + $records = $DB->get_records_sql($sql); + $firstrecord = reset($records); + $columns = context_helper::get_preload_record_columns('c'); + $firstrecord = (array)$firstrecord; + $this->assertIdentical(array_keys($firstrecord), array_values($columns)); + context_helper::reset_caches(); + foreach ($records as $record) { + context_helper::preload_from_record($record); + $this->assertIdentical($record, new stdClass()); + } + $this->assertEqual(context_inspection::test_context_cache_size(), count($records)); + unset($records); + unset($columns); + + context_helper::reset_caches(); + context_helper::preload_course($SITE->id); + $this->assertEqual(context_inspection::test_context_cache_size(), 4); + + // ====== assign_capability(), unassign_capability() ==================== + + $rc = $DB->get_record('role_capabilities', array('contextid'=>$frontpagecontext->id, 'roleid'=>$allroles['teacher'], 'capability'=>'moodle/site:accessallgroups')); + $this->assertFalse($rc); + assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $allroles['teacher'], $frontpagecontext->id); + $rc = $DB->get_record('role_capabilities', array('contextid'=>$frontpagecontext->id, 'roleid'=>$allroles['teacher'], 'capability'=>'moodle/site:accessallgroups')); + $this->assertEqual($rc->permission, CAP_ALLOW); + assign_capability('moodle/site:accessallgroups', CAP_PREVENT, $allroles['teacher'], $frontpagecontext->id); + $rc = $DB->get_record('role_capabilities', array('contextid'=>$frontpagecontext->id, 'roleid'=>$allroles['teacher'], 'capability'=>'moodle/site:accessallgroups')); + $this->assertEqual($rc->permission, CAP_ALLOW); + assign_capability('moodle/site:accessallgroups', CAP_PREVENT, $allroles['teacher'], $frontpagecontext, true); + $rc = $DB->get_record('role_capabilities', array('contextid'=>$frontpagecontext->id, 'roleid'=>$allroles['teacher'], 'capability'=>'moodle/site:accessallgroups')); + $this->assertEqual($rc->permission, CAP_PREVENT); + + assign_capability('moodle/site:accessallgroups', CAP_INHERIT, $allroles['teacher'], $frontpagecontext); + $rc = $DB->get_record('role_capabilities', array('contextid'=>$frontpagecontext->id, 'roleid'=>$allroles['teacher'], 'capability'=>'moodle/site:accessallgroups')); + $this->assertFalse($rc); + assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $allroles['teacher'], $frontpagecontext); + unassign_capability('moodle/site:accessallgroups', $allroles['teacher'], $frontpagecontext, true); + $rc = $DB->get_record('role_capabilities', array('contextid'=>$frontpagecontext->id, 'roleid'=>$allroles['teacher'], 'capability'=>'moodle/site:accessallgroups')); + $this->assertFalse($rc); + unassign_capability('moodle/site:accessallgroups', $allroles['teacher'], $frontpagecontext->id, true); + unset($rc); + + accesslib_clear_all_caches(false); // must be done after assign_capability() + + + // ======= role_assign(), role_unassign(), role_unassign_all() ============== + + $context = context_course::instance($testcourses[1]); + $this->assertEqual($DB->count_records('role_assignments', array('contextid'=>$context->id)), 0); + role_assign($allroles['teacher'], $testusers[1], $context->id); + role_assign($allroles['teacher'], $testusers[2], $context->id); + role_assign($allroles['manager'], $testusers[1], $context->id); + $this->assertEqual($DB->count_records('role_assignments', array('contextid'=>$context->id)), 3); + role_unassign($allroles['teacher'], $testusers[1], $context->id); + $this->assertEqual($DB->count_records('role_assignments', array('contextid'=>$context->id)), 2); + role_unassign_all(array('contextid'=>$context->id)); + $this->assertEqual($DB->count_records('role_assignments', array('contextid'=>$context->id)), 0); + unset($context); + + accesslib_clear_all_caches(false); // just in case + + + // ====== has_capability(), get_users_by_capability(), role_switch(), reload_all_capabilities() and friends ======================== + + $adminid = get_admin()->id; + $guestid = $CFG->siteguest; + + // Enrol some users into some courses + $course1 = $DB->get_record('course', array('id'=>$testcourses[22]), '*', MUST_EXIST); + $course2 = $DB->get_record('course', array('id'=>$testcourses[7]), '*', MUST_EXIST); + $cms = $DB->get_records('course_modules', array('course'=>$course1->id), 'id'); + $cm1 = reset($cms); + $blocks = $DB->get_records('block_instances', array('parentcontextid'=>context_module::instance($cm1->id)->id), 'id'); + $block1 = reset($blocks); + $instance1 = $DB->get_record('enrol', array('enrol'=>'manual', 'courseid'=>$course1->id)); + $instance2 = $DB->get_record('enrol', array('enrol'=>'manual', 'courseid'=>$course2->id)); + for($i=0; $i<9; $i++) { + $manualenrol->enrol_user($instance1, $testusers[$i], $allroles['student']); + } + $manualenrol->enrol_user($instance1, $testusers[8], $allroles['teacher']); + $manualenrol->enrol_user($instance1, $testusers[9], $allroles['editingteacher']); + + for($i=10; $i<15; $i++) { + $manualenrol->enrol_user($instance2, $testusers[$i], $allroles['student']); + } + $manualenrol->enrol_user($instance2, $testusers[15], $allroles['editingteacher']); + + // Add tons of role assignments - the more the better + role_assign($allroles['coursecreator'], $testusers[11], context_coursecat::instance($testcategories[2])); + role_assign($allroles['manager'], $testusers[12], context_coursecat::instance($testcategories[1])); + role_assign($allroles['student'], $testusers[9], context_module::instance($cm1->id)); + role_assign($allroles['teacher'], $testusers[8], context_module::instance($cm1->id)); + role_assign($allroles['guest'], $testusers[13], context_course::instance($course1->id)); + role_assign($allroles['teacher'], $testusers[7], context_block::instance($block1->id)); + role_assign($allroles['manager'], $testusers[9], context_block::instance($block1->id)); + role_assign($allroles['editingteacher'], $testusers[9], context_course::instance($course1->id)); + + role_assign($allroles['teacher'], $adminid, context_course::instance($course1->id)); + role_assign($allroles['editingteacher'], $adminid, context_block::instance($block1->id)); + + // Add tons of overrides - the more the better + assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultuserroleid, $frontpageblockcontext, true); + assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultfrontpageroleid, $frontpageblockcontext, true); + assign_capability('moodle/block:view', CAP_PROHIBIT, $allroles['guest'], $frontpageblockcontext, true); + assign_capability('block/online_users:viewlist', CAP_PREVENT, $allroles['user'], $frontpageblockcontext, true); + assign_capability('block/online_users:viewlist', CAP_PREVENT, $allroles['student'], $frontpageblockcontext, true); + + assign_capability('moodle/site:accessallgroups', CAP_PREVENT, $CFG->defaultuserroleid, $frontpagepagecontext, true); + assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultfrontpageroleid, $frontpagepagecontext, true); + assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $frontpagepagecontext, true); + assign_capability('mod/page:view', CAP_ALLOW, $allroles['user'], $frontpagepagecontext, true); + assign_capability('moodle/page:view', CAP_ALLOW, $allroles['student'], $frontpagepagecontext, true); + + assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultuserroleid, $frontpagecontext, true); + assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultfrontpageroleid, $frontpagecontext, true); + assign_capability('mod/page:view', CAP_ALLOW, $allroles['guest'], $frontpagecontext, true); + assign_capability('mod/page:view', CAP_PROHIBIT, $allroles['user'], $frontpagecontext, true); + + assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $systemcontext, true); + + accesslib_clear_all_caches(false); // must be done after assign_capability() + + // Extra tests for guests and not-logged-in users because they can not be verified by cross checking + // with get_users_by_capability() where they are ignored + $this->assertFalse(has_capability('moodle/block:view', $frontpageblockcontext, $guestid)); + $this->assertFalse(has_capability('mod/page:view', $frontpagepagecontext, $guestid)); + $this->assertTrue(has_capability('mod/page:view', $frontpagecontext, $guestid)); + $this->assertFalse(has_capability('mod/page:view', $systemcontext, $guestid)); + + $this->assertFalse(has_capability('moodle/block:view', $frontpageblockcontext, 0)); + $this->assertFalse(has_capability('mod/page:view', $frontpagepagecontext, 0)); + $this->assertTrue(has_capability('mod/page:view', $frontpagecontext, 0)); + $this->assertFalse(has_capability('mod/page:view', $systemcontext, 0)); + + $this->assertFalse(has_capability('moodle/course:create', $systemcontext, $testusers[11])); + $this->assertTrue(has_capability('moodle/course:create', context_coursecat::instance($testcategories[2]), $testusers[11])); + $this->assertFalse(has_capability('moodle/course:create', context_course::instance($testcourses[1]), $testusers[11])); + $this->assertTrue(has_capability('moodle/course:create', context_course::instance($testcourses[19]), $testusers[11])); + + $this->assertFalse(has_capability('moodle/course:update', context_course::instance($testcourses[1]), $testusers[9])); + $this->assertFalse(has_capability('moodle/course:update', context_course::instance($testcourses[19]), $testusers[9])); + $this->assertFalse(has_capability('moodle/course:update', $systemcontext, $testusers[9])); + + // Test the list of enrolled users + $coursecontext = context_course::instance($course1->id); + $enrolled = get_enrolled_users($coursecontext); + $this->assertEqual(count($enrolled), 10); + for($i=0; $i<10; $i++) { + $this->assertTrue(isset($enrolled[$testusers[$i]])); + } + $enrolled = get_enrolled_users($coursecontext, 'moodle/course:update'); + $this->assertEqual(count($enrolled), 1); + $this->assertTrue(isset($enrolled[$testusers[9]])); + unset($enrolled); + + // role switching + $userid = $testusers[9]; + $USER = $DB->get_record('user', array('id'=>$userid)); + load_all_capabilities(); + $coursecontext = context_course::instance($course1->id); + $this->assertTrue(has_capability('moodle/course:update', $coursecontext)); + $this->assertFalse(is_role_switched($course1->id)); + role_switch($allroles['student'], $coursecontext); + $this->assertTrue(is_role_switched($course1->id)); + $this->assertEqual($USER->access['rsw'][$coursecontext->path], $allroles['student']); + $this->assertFalse(has_capability('moodle/course:update', $coursecontext)); + reload_all_capabilities(); + $this->assertFalse(has_capability('moodle/course:update', $coursecontext)); + role_switch(0, $coursecontext); + $this->assertTrue(has_capability('moodle/course:update', $coursecontext)); + $userid = $adminid; + $USER = $DB->get_record('user', array('id'=>$userid)); + load_all_capabilities(); + $coursecontext = context_course::instance($course1->id); + $blockcontext = context_block::instance($block1->id); + $this->assertTrue(has_capability('moodle/course:update', $blockcontext)); + role_switch($allroles['student'], $coursecontext); + $this->assertEqual($USER->access['rsw'][$coursecontext->path], $allroles['student']); + $this->assertFalse(has_capability('moodle/course:update', $blockcontext)); + reload_all_capabilities(); + $this->assertFalse(has_capability('moodle/course:update', $blockcontext)); + load_all_capabilities(); + $this->assertTrue(has_capability('moodle/course:update', $blockcontext)); + + // temp course role for enrol + $DB->delete_records('cache_flags', array()); // this prevents problem with dirty contexts immediately resetting the temp role - this is a known problem... + $userid = $testusers[5]; + $roleid = $allroles['editingteacher']; + $USER = $DB->get_record('user', array('id'=>$userid)); + load_all_capabilities(); + $coursecontext = context_course::instance($course1->id); + $this->assertFalse(has_capability('moodle/course:update', $coursecontext)); + $this->assertFalse(isset($USER->access['ra'][$coursecontext->path][$roleid])); + load_temp_course_role($coursecontext, $roleid); + $this->assertEqual($USER->access['ra'][$coursecontext->path][$roleid], $roleid); + $this->assertTrue(has_capability('moodle/course:update', $coursecontext)); + remove_temp_course_roles($coursecontext); + $this->assertFalse(has_capability('moodle/course:update', $coursecontext, $userid)); + load_temp_course_role($coursecontext, $roleid); + reload_all_capabilities(); + $this->assertFalse(has_capability('moodle/course:update', $coursecontext, $userid)); + $USER = new stdClass(); + $USER->id = 0; + + // Now cross check has_capability() with get_users_by_capability(), each using different code paths, + // they have to be kept in sync, usually only one of them breaks, so we know when something is wrong, + // at the same time validate extra restrictions (guest read only no risks, admin exception, non existent and deleted users) + $contexts = $DB->get_records('context', array(), 'id'); + $contexts = array_values($contexts); + $capabilities = $DB->get_records('capabilities', array(), 'id'); + $capabilities = array_values($capabilities); + $roles = array($allroles['guest'], $allroles['user'], $allroles['teacher'], $allroles['editingteacher'], $allroles['coursecreator'], $allroles['manager']); + + // Random time! + srand(666); + foreach($testusers as $userid) { // no guest or deleted + // each user gets 0-20 random roles + $rcount = rand(0, 10); + for($j=0; $j<$rcount; $j++) { + $roleid = $roles[rand(0, count($roles)-1)]; + $contextid = $contexts[rand(0, count($contexts)-1)]->id; + role_assign($roleid, $userid, $contextid); + } + } + $permissions = array(CAP_ALLOW, CAP_PREVENT, CAP_INHERIT, CAP_PREVENT); + for($j=0; $j<10000; $j++) { + $roleid = $roles[rand(0, count($roles)-1)]; + $contextid = $contexts[rand(0, count($contexts)-1)]->id; + $permission = $permissions[rand(0,count($permissions)-1)]; + $capname = $capabilities[rand(0, count($capabilities)-1)]->name; + assign_capability($capname, $permission, $roleid, $contextid, true); + } + unset($permissions); + unset($roles); + unset($contexts); + unset($users); + unset($capabilities); + + accesslib_clear_all_caches(false); // must be done after assign_capability() + + // Test time - let's set up some real user, just in case the logic for USER affects the others... + $USER = $DB->get_record('user', array('id'=>$testusers[3])); + load_all_capabilities(); + + $contexts = $DB->get_records('context', array(), 'id'); + $users = $DB->get_records('user', array(), 'id', 'id'); + $capabilities = $DB->get_records('capabilities', array(), 'id'); + $users[0] = null; // not-logged-in user + $users[-1] = null; // non-existent user + + foreach ($contexts as $crecord) { + $context = context::instance_by_id($crecord->id); + if ($coursecontext = $context->get_course_context(false)) { + $enrolled = get_enrolled_users($context); + } else { + $enrolled = array(); + } + foreach ($capabilities as $cap) { + $allowed = get_users_by_capability($context, $cap->name, 'u.id, u.username'); + if ($enrolled) { + $enrolledwithcap = get_enrolled_users($context, $cap->name); + } else { + $enrolledwithcap = array(); + } + foreach ($users as $userid=>$unused) { + if ($userid == 0 or isguestuser($userid)) { + if ($userid == 0) { + $CFG->forcelogin = true; + $this->assertFalse(has_capability($cap->name, $context, $userid)); + unset($CFG->forcelogin); + } + if (($cap->captype === 'write') or ($cap->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) { + $this->assertFalse(has_capability($cap->name, $context, $userid)); + } + $this->assertFalse(isset($allowed[$userid])); + } else { + if (is_siteadmin($userid)) { + $this->assertTrue(has_capability($cap->name, $context, $userid, true)); + } + $hascap = has_capability($cap->name, $context, $userid, false); + $this->assertIdentical($hascap, isset($allowed[$userid]), "Capability result mismatch user:$userid, context:$context->id, $cap->name, hascap: ".(int)$hascap." "); + if (isset($enrolled[$userid])) { + $this->assertIdentical(isset($allowed[$userid]), isset($enrolledwithcap[$userid]), "Enrolment with capability result mismatch user:$userid, context:$context->id, $cap->name, hascap: ".(int)$hascap." "); + } + } + } + } + } + // Back to nobody + $USER = new stdClass(); + $USER->id = 0; + unset($contexts); + unset($users); + unset($capabilities); + + // Now let's do all the remaining tests that break our carefully prepared fake site + + + + // ======= $context->mark_dirty() ======================================= + + $DB->delete_records('cache_flags', array()); + accesslib_clear_all_caches(false); + $systemcontext->mark_dirty(); + $dirty = get_cache_flags('accesslib/dirtycontexts', time()-2); + $this->assertTrue(isset($dirty[$systemcontext->path])); + $this->assertTrue(isset($ACCESSLIB_PRIVATE->dirtycontexts[$systemcontext->path])); + + + // ======= $context->reload_if_dirty(); ================================= + + $DB->delete_records('cache_flags', array()); + accesslib_clear_all_caches(false); + load_all_capabilities(); + $context = context_course::instance($testcourses[2]); + $page = $DB->get_record('page', array('course'=>$testcourses[2])); + $pagecontext = context_module::instance($page->id); + + $context->mark_dirty(); + $this->assertTrue(isset($ACCESSLIB_PRIVATE->dirtycontexts[$context->path])); + $USER->access['test'] = true; + $context->reload_if_dirty(); + $this->assertFalse(isset($USER->access['test'])); + + $context->mark_dirty(); + $this->assertTrue(isset($ACCESSLIB_PRIVATE->dirtycontexts[$context->path])); + $USER->access['test'] = true; + $pagecontext->reload_if_dirty(); + $this->assertFalse(isset($USER->access['test'])); + + + // ======= context_helper::build_all_paths() ============================ + + $oldcontexts = $DB->get_records('context', array(), 'id'); + $DB->set_field_select('context', 'path', NULL, "contextlevel <> ".CONTEXT_SYSTEM); + $DB->set_field_select('context', 'depth', 0, "contextlevel <> ".CONTEXT_SYSTEM); + context_helper::build_all_paths(); + $newcontexts = $DB->get_records('context', array(), 'id'); + $this->assertIdentical($oldcontexts, $newcontexts); + unset($oldcontexts); + unset($newcontexts); + + + // ======= $context->reset_paths() ====================================== + + $context = context_course::instance($testcourses[2]); + $children = $context->get_child_contexts(); + $context->reset_paths(false); + $this->assertIdentical($DB->get_field('context', 'path', array('id'=>$context->id)), NULL); + $this->assertEqual($DB->get_field('context', 'depth', array('id'=>$context->id)), 0); + foreach ($children as $child) { + $this->assertIdentical($DB->get_field('context', 'path', array('id'=>$child->id)), NULL); + $this->assertEqual($DB->get_field('context', 'depth', array('id'=>$child->id)), 0); + } + $this->assertEqual(count($children)+1, $DB->count_records('context', array('depth'=>0))); + $this->assertEqual(count($children)+1, $DB->count_records('context', array('path'=>NULL))); + + $context = context_course::instance($testcourses[2]); + $context->reset_paths(true); + $context = context_course::instance($testcourses[2]); + $this->assertEqual($DB->get_field('context', 'path', array('id'=>$context->id)), $context->path); + $this->assertEqual($DB->get_field('context', 'depth', array('id'=>$context->id)), $context->depth); + $this->assertEqual(0, $DB->count_records('context', array('depth'=>0))); + $this->assertEqual(0, $DB->count_records('context', array('path'=>NULL))); + + + // ====== $context->update_moved(); ====================================== + + accesslib_clear_all_caches(false); + $DB->delete_records('cache_flags', array()); + $course = $DB->get_record('course', array('id'=>$testcourses[0])); + $context = context_course::instance($course->id); + $oldpath = $context->path; + $miscid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); + $categorycontext = context_coursecat::instance($miscid); + $course->category = $miscid; + $DB->update_record('course', $course); + $context->update_moved($categorycontext); + + $context = context_course::instance($course->id); + $this->assertIdentical($context->get_parent_context(), $categorycontext); + $dirty = get_cache_flags('accesslib/dirtycontexts', time()-2); + $this->assertTrue(isset($dirty[$oldpath])); + $this->assertTrue(isset($dirty[$context->path])); + + + // ====== $context->delete_content() ===================================== + + context_helper::reset_caches(); + $context = context_module::instance($testpages[3]); + $this->assertTrue($DB->record_exists('context', array('id'=>$context->id))); + $this->assertEqual(1, $DB->count_records('block_instances', array('parentcontextid'=>$context->id))); + $context->delete_content(); + $this->assertTrue($DB->record_exists('context', array('id'=>$context->id))); + $this->assertEqual(0, $DB->count_records('block_instances', array('parentcontextid'=>$context->id))); + + + // ====== $context->delete() ============================= + + context_helper::reset_caches(); + $context = context_module::instance($testpages[4]); + $this->assertTrue($DB->record_exists('context', array('id'=>$context->id))); + $this->assertEqual(1, $DB->count_records('block_instances', array('parentcontextid'=>$context->id))); + $bi = $DB->get_record('block_instances', array('parentcontextid'=>$context->id)); + $bicontext = context_block::instance($bi->id); + $DB->delete_records('cache_flags', array()); + $context->delete(); // should delete also linked blocks + $dirty = get_cache_flags('accesslib/dirtycontexts', time()-2); + $this->assertTrue(isset($dirty[$context->path])); + $this->assertFalse($DB->record_exists('context', array('id'=>$context->id))); + $this->assertFalse($DB->record_exists('context', array('id'=>$bicontext->id))); + $this->assertFalse($DB->record_exists('context', array('contextlevel'=>CONTEXT_MODULE, 'instanceid'=>$testpages[4]))); + $this->assertFalse($DB->record_exists('context', array('contextlevel'=>CONTEXT_BLOCK, 'instanceid'=>$bi->id))); + $this->assertEqual(0, $DB->count_records('block_instances', array('parentcontextid'=>$context->id))); + context_module::instance($testpages[4]); + + + // ====== context_helper::delete_instance() ============================= + + context_helper::reset_caches(); + $lastcourse = array_pop($testcourses); + $this->assertTrue($DB->record_exists('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>$lastcourse))); + $coursecontext = context_course::instance($lastcourse); + $this->assertEqual(context_inspection::test_context_cache_size(), 1); + $this->assertFalse($coursecontext->instanceid == CONTEXT_COURSE); + $DB->delete_records('cache_flags', array()); + context_helper::delete_instance(CONTEXT_COURSE, $lastcourse); + $dirty = get_cache_flags('accesslib/dirtycontexts', time()-2); + $this->assertTrue(isset($dirty[$coursecontext->path])); + $this->assertEqual(context_inspection::test_context_cache_size(), 0); + $this->assertFalse($DB->record_exists('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>$lastcourse))); + context_course::instance($lastcourse); + + + // ======= context_helper::create_instances() ========================== + + $prevcount = $DB->count_records('context'); + $DB->delete_records('context', array('contextlevel'=>CONTEXT_BLOCK)); + context_helper::create_instances(null, true); + $this->assertIdentical($DB->count_records('context'), $prevcount); + $this->assertEqual($DB->count_records('context', array('depth'=>0)), 0); + $this->assertEqual($DB->count_records('context', array('path'=>NULL)), 0); + + $DB->delete_records('context', array('contextlevel'=>CONTEXT_BLOCK)); + $DB->delete_records('block_instances', array()); + $prevcount = $DB->count_records('context'); + $DB->delete_records_select('context', 'contextlevel <> '.CONTEXT_SYSTEM); + context_helper::create_instances(null, true); + $this->assertIdentical($DB->count_records('context'), $prevcount); + $this->assertEqual($DB->count_records('context', array('depth'=>0)), 0); + $this->assertEqual($DB->count_records('context', array('path'=>NULL)), 0); + + + // ======= context_helper::cleanup_instances() ========================== + + $lastcourse = $DB->get_field_sql("SELECT MAX(id) FROM {course}"); + $DB->delete_records('course', array('id'=>$lastcourse)); + $lastcategory = $DB->get_field_sql("SELECT MAX(id) FROM {course_categories}"); + $DB->delete_records('course_categories', array('id'=>$lastcategory)); + $lastuser = $DB->get_field_sql("SELECT MAX(id) FROM {user} WHERE deleted=0"); + $DB->delete_records('user', array('id'=>$lastuser)); + $DB->delete_records('block_instances', array('parentcontextid'=>$frontpagepagecontext->id)); + $DB->delete_records('course_modules', array('id'=>$frontpagepagecontext->instanceid)); + context_helper::cleanup_instances(); + $count = 1; //system + $count += $DB->count_records('user', array('deleted'=>0)); + $count += $DB->count_records('course_categories'); + $count += $DB->count_records('course'); + $count += $DB->count_records('course_modules'); + $count += $DB->count_records('block_instances'); + $this->assertEqual($DB->count_records('context'), $count); + + + // ======= context cache size restrictions ============================== + + $testusers= array(); + for ($i=0; $iauth = 'manual'; + $user->firstname = 'xuser'.$i; + $user->lastname = 'xuser'.$i; + $user->username = 'xuser'.$i; + $user->password = 'doesnotexist'; + $user->email = "xuser$i@example.com"; + $user->confirmed = 1; + $user->mnethostid = $CFG->mnet_localhost_id; + $user->lang = $CFG->lang; + $user->maildisplay = 1; + $user->timemodified = time(); + $user->lastip = '0.0.0.0'; + $userid = $DB->insert_record('user', $user); + $testusers[$i] = $userid; + } + context_helper::create_instances(null, true); + context_helper::reset_caches(); + for ($i=0; $iassertEqual(context_inspection::test_context_cache_size(), CONTEXT_CACHE_MAX_SIZE); + } else if ($i == CONTEXT_CACHE_MAX_SIZE) { + // once the limit is reached roughly 1/3 of records should be removed from cache + $this->assertEqual(context_inspection::test_context_cache_size(), (int)(CONTEXT_CACHE_MAX_SIZE * (2/3) +102)); + } + } + // We keep the first 100 cached + $prevsize = context_inspection::test_context_cache_size(); + for ($i=0; $i<100; $i++) { + context_user::instance($testusers[$i]); + $this->assertEqual(context_inspection::test_context_cache_size(), $prevsize); + } + context_user::instance($testusers[102]); + $this->assertEqual(context_inspection::test_context_cache_size(), $prevsize+1); + unset($testusers); + + + + // ================================================================= + // ======= basic test of legacy functions ========================== + // ================================================================= + // note: watch out, the fake site might be pretty borked already + + $this->assertIdentical(get_system_context(), context_system::instance()); + + foreach ($DB->get_records('context') as $contextid=>$record) { + $context = context::instance_by_id($contextid); + $this->assertIdentical(get_context_instance_by_id($contextid), $context); + $this->assertIdentical(get_context_instance($record->contextlevel, $record->instanceid), $context); + $this->assertIdentical(get_parent_contexts($context), $context->get_parent_context_ids()); + if ($context->id == SYSCONTEXTID) { + $this->assertIdentical(get_parent_contextid($context), false); + } else { + $this->assertIdentical(get_parent_contextid($context), $context->get_parent_context()->id); + } + } + + $children = get_child_contexts($systemcontext); + $this->assertEqual(count($children), $DB->count_records('context')-1); + unset($children); + + $DB->delete_records('context', array('contextlevel'=>CONTEXT_BLOCK)); + create_contexts(); + $this->assertFalse($DB->record_exists('context', array('contextlevel'=>CONTEXT_BLOCK))); + + $DB->set_field('context', 'depth', 0, array('contextlevel'=>CONTEXT_BLOCK)); + build_context_path(); + $this->assertFalse($DB->record_exists('context', array('depth'=>0))); + + $lastcourse = $DB->get_field_sql("SELECT MAX(id) FROM {course}"); + $DB->delete_records('course', array('id'=>$lastcourse)); + $lastcategory = $DB->get_field_sql("SELECT MAX(id) FROM {course_categories}"); + $DB->delete_records('course_categories', array('id'=>$lastcategory)); + $lastuser = $DB->get_field_sql("SELECT MAX(id) FROM {user} WHERE deleted=0"); + $DB->delete_records('user', array('id'=>$lastuser)); + $DB->delete_records('block_instances', array('parentcontextid'=>$frontpagepagecontext->id)); + $DB->delete_records('course_modules', array('id'=>$frontpagepagecontext->instanceid)); + cleanup_contexts(); + $count = 1; //system + $count += $DB->count_records('user', array('deleted'=>0)); + $count += $DB->count_records('course_categories'); + $count += $DB->count_records('course'); + $count += $DB->count_records('course_modules'); + $count += $DB->count_records('block_instances'); + $this->assertEqual($DB->count_records('context'), $count); + + context_helper::reset_caches(); + preload_course_contexts($SITE->id); + $this->assertEqual(context_inspection::test_context_cache_size(), 1); + + context_helper::reset_caches(); + list($select, $join) = context_instance_preload_sql('c.id', CONTEXT_COURSECAT, 'ctx'); + $sql = "SELECT c.id $select FROM {course_categories} c $join"; + $records = $DB->get_records_sql($sql); + foreach ($records as $record) { + context_instance_preload($record); + $record = (array)$record; + $this->assertEqual(1, count($record)); // only id left + } + $this->assertEqual(count($records), context_inspection::test_context_cache_size()); + + accesslib_clear_all_caches(true); + $DB->delete_records('cache_flags', array()); + mark_context_dirty($systemcontext->path); + $dirty = get_cache_flags('accesslib/dirtycontexts', time()-2); + $this->assertTrue(isset($dirty[$systemcontext->path])); + + accesslib_clear_all_caches(false); + $DB->delete_records('cache_flags', array()); + $course = $DB->get_record('course', array('id'=>$testcourses[2])); + $context = get_context_instance(CONTEXT_COURSE, $course->id); + $oldpath = $context->path; + $miscid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); + $categorycontext = context_coursecat::instance($miscid); + $course->category = $miscid; + $DB->update_record('course', $course); + context_moved($context, $categorycontext); + $context = get_context_instance(CONTEXT_COURSE, $course->id); + $this->assertIdentical($context->get_parent_context(), $categorycontext); + + $this->assertTrue($DB->record_exists('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>$testcourses[2]))); + delete_context(CONTEXT_COURSE, $testcourses[2]); + $this->assertFalse($DB->record_exists('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>$testcourses[2]))); + + $name = get_contextlevel_name(CONTEXT_COURSE); + $this->assertFalse(empty($name)); + + $context = get_context_instance(CONTEXT_COURSE, $testcourses[2]); + $name = print_context_name($context); + $this->assertFalse(empty($name)); + + $url = get_context_url($coursecontext); + $this->assertFalse($url instanceof modole_url); + + $page = $DB->get_record('page', array('id'=>$testpages[7])); + $context = get_context_instance(CONTEXT_MODULE, $page->id); + $coursecontext = get_course_context($context); + $this->assertEqual($coursecontext->contextlevel, CONTEXT_COURSE); + $this->assertEqual(get_courseid_from_context($context), $page->course); + + $caps = fetch_context_capabilities($systemcontext); + $this->assertTrue(is_array($caps)); + unset($caps); + } + + public function tearDown() { + global $USER, $SITE; + if (isset($this->accesslibprevuser)) { + $USER = $this->accesslibprevuser; + } + if (isset($this->accesslibprevsite)) { + $SITE = $this->accesslibprevsite; + } + + + parent::tearDown(); + } +} + diff --git a/lib/simpletest/testaccesslib.php b/lib/simpletest/testaccesslib.php deleted file mode 100644 index 44818797461..00000000000 --- a/lib/simpletest/testaccesslib.php +++ /dev/null @@ -1,382 +0,0 @@ -assertEqual(get_parent_contexts($context), array()); - - $context = new stdClass; - $context->path = '/1/25'; - $this->assertEqual(get_parent_contexts($context), array(1)); - - $context = new stdClass; - $context->path = '/1/123/234/345/456'; - $this->assertEqual(get_parent_contexts($context), array(345, 234, 123, 1)); - } - - function test_get_parent_contextid() { - $context = get_context_instance(CONTEXT_SYSTEM); - $this->assertFalse(get_parent_contextid($context)); - - $context = new stdClass; - $context->path = '/1/25'; - $this->assertEqual(get_parent_contextid($context), 1); - - $context = new stdClass; - $context->path = '/1/123/234/345/456'; - $this->assertEqual(get_parent_contextid($context), 345); - } - - function test_get_users_by_capability() { - global $CFG; - - $tablenames = array('capabilities', 'context', 'role', 'role_capabilities', - 'role_allow_assign', 'role_allow_override', 'role_assignments', 'role_context_levels', - 'user', 'groups_members', 'cache_flags', 'events_handlers', 'user_lastaccess', 'course'); - $this->create_test_tables($tablenames, 'lib'); - - accesslib_clear_all_caches_for_unit_testing(); - $this->switch_to_test_db(); - $this->switch_to_test_cfg(); - - $course = new stdClass(); - $course->category = 0; - $this->testdb->insert_record('course', $course); - $syscontext = get_system_context(false); - - /// Install the roles system. - $coursecreatorrole = create_role(get_string('coursecreators'), 'coursecreator', - get_string('coursecreatorsdescription'), 'coursecreator'); - $editteacherrole = create_role(get_string('defaultcourseteacher'), 'editingteacher', - get_string('defaultcourseteacherdescription'), 'editingteacher'); - $noneditteacherrole = create_role(get_string('noneditingteacher'), 'teacher', - get_string('noneditingteacherdescription'), 'teacher'); - $studentrole = create_role(get_string('defaultcoursestudent'), 'student', - get_string('defaultcoursestudentdescription'), 'student'); - $guestrole = create_role(get_string('guest'), 'guest', - get_string('guestdescription'), 'guest'); - $userrole = create_role(get_string('authenticateduser'), 'user', - get_string('authenticateduserdescription'), 'user'); - - /// Now is the correct moment to install capabilities - after creation of legacy roles, but before assigning of roles - update_capabilities('moodle'); - update_capabilities('mod_forum'); - update_capabilities('mod_quiz'); - - // Create some nested contexts. instanceid does not matter for this. Just - // ensure we don't violate any unique keys by using an unlikely number. - // We will fix paths in a second. - $contexts = $this->load_test_data('context', - array('contextlevel', 'instanceid', 'path', 'depth'), array( - 1 => array(40, 666, '', 2), - 2 => array(50, 666, '', 3), - 3 => array(70, 666, '', 4), - )); - $contexts[0] = $syscontext; - $contexts[1]->path = $contexts[0]->path . '/' . $contexts[1]->id; - $this->testdb->set_field('context', 'path', $contexts[1]->path, array('id' => $contexts[1]->id)); - $contexts[2]->path = $contexts[1]->path . '/' . $contexts[2]->id; - $this->testdb->set_field('context', 'path', $contexts[2]->path, array('id' => $contexts[2]->id)); - $contexts[3]->path = $contexts[2]->path . '/' . $contexts[3]->id; - $this->testdb->set_field('context', 'path', $contexts[3]->path, array('id' => $contexts[3]->id)); - - // Now make some test users. - $users = $this->load_test_data('user', - array('username', 'confirmed', 'deleted'), array( - 'a' => array('a', 1, 0), - 'cc' => array('cc', 1, 0), - 't1' => array('t1', 1, 0), - 's1' => array('s1', 1, 0), - 's2' => array('s2', 1, 0), - 'del' => array('del', 1, 1), - 'unc' => array('unc', 0, 0), - )); - - // Get some of the standard roles. - $creator = $this->testdb->get_record('role', array('shortname' => 'coursecreator')); - $teacher = $this->testdb->get_record('role', array('shortname' => 'editingteacher')); - $student = $this->testdb->get_record('role', array('shortname' => 'student')); - $authuser = $this->testdb->get_record('role', array('shortname' => 'user')); - - // And some role assignments. - $ras = $this->load_test_data('role_assignments', - array('userid', 'roleid', 'contextid'), array( - 'cc' => array($users['cc']->id, $creator->id, $contexts[1]->id), - 't1' => array($users['t1']->id, $teacher->id, $contexts[2]->id), - 's1' => array($users['s1']->id, $student->id, $contexts[2]->id), - 's2' => array($users['s2']->id, $student->id, $contexts[2]->id), - )); - - // And make user a into admin - $CFG->siteadmins = $users['a']->id; - $CFG->defaultuserroleid = $userrole; - - // And some group memebership. - $gms = $this->load_test_data('groups_members', - array('userid', 'groupid'), array( - array($users['t1']->id, 666), - array($users['s1']->id, 666), - array($users['s2']->id, 667), - )); - - // Test some simple cases - check that looking in coruse and module contextlevel gives the same answer. - foreach (array(2, 3) as $conindex) { - $results = get_users_by_capability($contexts[$conindex], 'mod/forum:replypost'); - // note: admin accounts are never returned, so no admin return here - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id, $users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - $results)); - // Paging. - $firstuser = reset($results); - $this->assertEqual(array($firstuser->id => $firstuser), get_users_by_capability($contexts[$conindex], 'mod/forum:replypost', '', '', 0, 1)); - $seconduser = next($results); - $this->assertEqual(array($seconduser->id => $seconduser), get_users_by_capability($contexts[$conindex], 'mod/forum:replypost', '', '', 1, 1)); - // $doanything = false (ignored now) - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id, $users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[$conindex], 'mod/forum:replypost', '', '', '', '', '', '', false))); - // group - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id, $users['s1']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[$conindex], 'mod/forum:replypost', '', '', '', '', 666))); - // exceptions - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[$conindex], 'mod/forum:replypost', '', '', '', '', '', array($users['t1']->id)))); - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['s1']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[$conindex], 'mod/forum:replypost', '', '', '', '', 666, array($users['t1']->id)))); - // $useviewallgroups - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[$conindex], 'mod/forum:replypost', '', '', '', '', 667, '', false, false, true))); - // More than one capability. - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[$conindex], array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts')))); - } - -// For reference: get_users_by_capability argument order: -// $context, $capability, $fields='', $sort='', $limitfrom='', $limitnum='', -// $groups='', $exceptions='', $doanything=true, $view=false, $useviewallgroups=false - - // Now add some role overrides. - $rcs = $this->load_test_data('role_capabilities', - array('capability', 'roleid', 'contextid', 'permission'), array( - array('mod/forum:replypost', $student->id, $contexts[1]->id, CAP_PREVENT), - array('mod/forum:replypost', $student->id, $contexts[3]->id, CAP_ALLOW), - array('mod/quiz:attempt', $student->id, $contexts[2]->id, CAP_PREVENT), - array('mod/forum:startdiscussion', $student->id, $contexts[1]->id, CAP_PROHIBIT), - array('mod/forum:startdiscussion', $student->id, $contexts[3]->id, CAP_ALLOW), - array('mod/forum:viewrating', $authuser->id, $contexts[1]->id, CAP_PROHIBIT), - array('mod/forum:createattachment', $authuser->id, $contexts[3]->id, CAP_PREVENT), - )); - - // Now test the overridden cases. - // Students prevented at category level, with and without doanything. - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[2], 'mod/forum:replypost'))); - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[2], 'mod/forum:replypost', '', '', '', '', '', '', false))); - // Students prevented at category level, but re-allowed at module level, with and without doanything. - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id, $users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[3], 'mod/forum:replypost', '', '', '', '', '', '', false))); - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id, $users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[3], 'mod/forum:replypost'))); - // Students prohibited at category level, re-allowed at module level should have no effect. - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[2], 'mod/forum:startdiscussion'))); - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[3], 'mod/forum:startdiscussion'))); - // Prevent on logged-in user should be overridden by student allow. - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['t1']->id, $users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[3], 'mod/forum:createattachment'))); - - // Prohibit on logged-in user should trump student/teacher allow. - $this->assert(new ArraysHaveSameValuesExpectation( - array()), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[3], 'mod/forum:viewrating'))); - - // More than one capability, where students have one, but not the other. - $this->assert(new ArraysHaveSameValuesExpectation( - array($users['s1']->id, $users['s2']->id)), - array_map(create_function('$o', 'return $o->id;'), - get_users_by_capability($contexts[3], array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts'), '', '', '', '', '', '', false))); - } - - function test_get_switchable_roles() { - global $USER; - - $tablenames = array('role' , 'role_capabilities', 'role_assignments', 'role_allow_switch', - 'capabilities', 'context', 'role_names'); - $this->create_test_tables($tablenames, 'lib'); - - $this->switch_to_test_db(); - $this->switch_to_test_cfg(); - - // Ensure SYSCONTEXTID is set. - get_context_instance(CONTEXT_SYSTEM); - - $contexts = $this->load_test_data('context', - array('contextlevel', 'instanceid', 'path', 'depth'), array( - 'sys' => array(CONTEXT_SYSTEM, 0, '/' . SYSCONTEXTID, 1), - 'cat' => array(CONTEXT_COURSECAT, 66, '/' . SYSCONTEXTID . '/' . (SYSCONTEXTID + 1), 2), - 'cou' => array(CONTEXT_COURSE, 666, '/' . SYSCONTEXTID . '/' . (SYSCONTEXTID + 1) . '/' . (SYSCONTEXTID + 2), 3), - 'fp' => array(CONTEXT_COURSE, SITEID, '/' . SYSCONTEXTID . '/' . SITEID, 2))); - $this->testdb->set_field('context', 'id', SYSCONTEXTID, array('id' => $contexts['sys']->id)); - $this->testdb->set_field('context', 'id', SYSCONTEXTID + 1, array('id' => $contexts['cat']->id)); - $this->testdb->set_field('context', 'id', SYSCONTEXTID + 2, array('id' => $contexts['cou']->id)); - $syscontext = $contexts['sys']; - $syscontext->id = SYSCONTEXTID; - $context = $contexts['cou']; - $context->id = SYSCONTEXTID + 2; - - $roles = $this->load_test_data('role', - array( 'name', 'shortname', 'description', 'sortorder'), array( - 'r1' => array( 'r1', 'r1', 'not null', 2), - 'r2' => array( 'r2', 'r2', 'not null', 3), - 'funny' => array('funny', 'funny', 'not null', 4))); - $r1id = $roles['r1']->id; - $r2id = $roles['r2']->id; - $funnyid = $roles['funny']->id; // strange role - - // Note that get_switchable_roles requires at least one capability for - // each role. I am not really clear why it was implemented that way - // but this makes the test work. - $roles = $this->load_test_data('role_capabilities', - array('roleid', 'capability'), array( - array($r1id, 'moodle/say:hello'), - array($r2id, 'moodle/say:hello'), - array($funnyid, 'moodle/say:hello'), - )); - - $this->load_test_data('role_assignments', - array('userid', 'contextid', 'roleid'), array( - array( 2, SYSCONTEXTID + 1 , $r1id), - array( 3, SYSCONTEXTID + 2 , $r2id))); - - $this->load_test_data('role_allow_switch', - array('roleid', 'allowswitch'), array( - array( $r1id , $r2id), - array( $r2id , $r1id), - array( $r2id , $r2id), - array( $r2id , $funnyid))); - - // r1 should be able to switch to r2, but this user only has r1 in $context, not $syscontext. - $this->switch_global_user_id(2); - accesslib_clear_all_caches_for_unit_testing(); - $this->assert(new ArraysHaveSameValuesExpectation(array()), array_keys(get_switchable_roles($syscontext))); - $this->assert(new ArraysHaveSameValuesExpectation(array($r2id)), array_keys(get_switchable_roles($context))); - $this->revert_global_user_id(); - - // The table says r2 should be able to switch to all of r1, r2 and funny; - // this used to be restricted further beyond the switch table (for example - // to prevent you switching to roles with doanything) but is not any more - // (for example because doanything does not exist). - $this->switch_global_user_id(3); - accesslib_clear_all_caches_for_unit_testing(); - $this->assert(new ArraysHaveSameValuesExpectation(array()), array_keys(get_switchable_roles($syscontext))); - $this->assert(new ArraysHaveSameValuesExpectation(array($r2id, $r1id, $funnyid)), array_keys(get_switchable_roles($context))); - } - - function test_context_cache() { - // Create cache, empty - $cache = new context_cache(); - $this->assertEqual(0, $cache->get_approx_count()); - - // Put context in cache - $context = (object)array('id'=>37,'contextlevel'=>50,'instanceid'=>13); - $cache->add($context); - $this->assertEqual(1, $cache->get_approx_count()); - - // Get context out of cache - $this->assertEqual($context, $cache->get(50, 13)); - $this->assertEqual($context, $cache->get_by_id(37)); - - // Try to get context that isn't there - $this->assertEqual(false, $cache->get(50, 99)); - $this->assertEqual(false, $cache->get(99, 13)); - $this->assertEqual(false, $cache->get_by_id(99)); - - // Remove context from cache - $cache->remove($context); - $this->assertEqual(0, $cache->get_approx_count()); - $this->assertEqual(false, $cache->get(50, 13)); - $this->assertEqual(false, $cache->get_by_id(37)); - - // Add a stack of contexts to cache - for($i=0; $i$i, 'contextlevel'=>50, - 'instanceid'=>$i+1); - $cache->add($context); - } - $this->assertEqual(context_cache::MAX_SIZE, $cache->get_approx_count()); - - // Get a random sample from the middle - $sample = (object)array( - 'id'=>174, 'contextlevel'=>50, 'instanceid'=>175); - $this->assertEqual($sample, $cache->get(50, 175)); - $this->assertEqual($sample, $cache->get_by_id(174)); - - // Now add one more; should result in size being reduced - $context = (object)array('id'=>99999, 'contextlevel'=>50, - 'instanceid'=>99999); - $cache->add($context); - $this->assertEqual(context_cache::MAX_SIZE - context_cache::REDUCE_SIZE + 1, - $cache->get_approx_count()); - - // Check that the first ones are no longer there - $this->assertEqual(false, $cache->get(50, 175)); - $this->assertEqual(false, $cache->get_by_id(174)); - - // Check that the last ones are still there - $bigid = context_cache::MAX_SIZE - 10; - $sample = (object)array( - 'id'=>$bigid, 'contextlevel'=>50, 'instanceid'=>$bigid+1); - $this->assertEqual($sample, $cache->get(50, $bigid+1)); - $this->assertEqual($sample, $cache->get_by_id($bigid)); - - // Reset the cache - $cache->reset(); - $this->assertEqual(0, $cache->get_approx_count()); - $this->assertEqual(false, $cache->get(50, $bigid+1)); - } -} - diff --git a/lib/upgradelib.php b/lib/upgradelib.php index 8ea6abd93ba..edf8f0b3823 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -1417,11 +1417,11 @@ function upgrade_core($version, $verbose) { purge_all_caches(); // Clean up contexts - more and more stuff depends on existence of paths and contexts - cleanup_contexts(); - create_contexts(); - build_context_path(); - $syscontext = get_context_instance(CONTEXT_SYSTEM); - mark_context_dirty($syscontext->path); + context_helper::cleanup_instances(); + context_helper::create_instances(null, false); + context_helper::build_all_paths(false); + $syscontext = context_system::instance(); + $syscontext->mark_dirty(); print_upgrade_part_end('moodle', false, $verbose); } catch (Exception $ex) { diff --git a/login/token.php b/login/token.php index ef1a5101715..6ca05fa64f4 100644 --- a/login/token.php +++ b/login/token.php @@ -56,6 +56,9 @@ if (!empty($user)) { } } + // let enrol plugins deal with new enrolments if necessary + enrol_check_plugins($user); + // setup user session to check capability session_set_user($user); diff --git a/rss/file.php b/rss/file.php index 7d2f9f5da26..2b9c9906c9f 100644 --- a/rss/file.php +++ b/rss/file.php @@ -16,9 +16,9 @@ /** * rss/file.php - entry point to serve rss streams - * + * * This script simply checks the parameters to construct a $USER - * then finds and calls a function in the relevant component to + * then finds and calls a function in the relevant component to * actually check security and create the RSS stream * * @package moodlecore @@ -81,7 +81,7 @@ if ($token==="$inttoken") { if (!isset($modinfo->instances[$componentname])) { $modinfo->instances[$componentname] = array(); } - + foreach ($modinfo->instances[$componentname] as $modinstanceid=>$cm) { if ($modinstanceid==$instanceid) { $context = get_context_instance(CONTEXT_MODULE, $cm->id); @@ -122,6 +122,10 @@ if ($token==="$inttoken") { } $user = get_complete_user_data('id', $userid); + +// let enrol plugins deal with new enrolments if necessary +enrol_check_plugins($user); + session_set_user($user); //for login and capability checks // Check the context actually exists diff --git a/webservice/lib.php b/webservice/lib.php index 53f7f596cb0..84c0927caae 100644 --- a/webservice/lib.php +++ b/webservice/lib.php @@ -654,6 +654,7 @@ abstract class webservice_server implements webservice_server_interface { } // now fake user login, the session is completely empty too + enrol_check_plugins($user); session_set_user($user); $this->userid = $user->id; diff --git a/webservice/upload.php b/webservice/upload.php index 9886b64947e..874b03ef1ae 100644 --- a/webservice/upload.php +++ b/webservice/upload.php @@ -66,6 +66,8 @@ $user = $DB->get_record('user', array('id'=>$token->userid, 'deleted'=>0), '*', // log token access $DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id)); +// let enrol plugins deal with new enrolments if necessary +enrol_check_plugins($user); session_set_user($user); $context = get_context_instance(CONTEXT_USER, $USER->id); require_capability('moodle/user:manageownfiles', $context);