Updated the HEAD build version to 20100307
[moodle.git] / lib / accesslib.php
CommitLineData
46808d7c 1<?php
2
117bd748
PS
3// This file is part of Moodle - http://moodle.org/
4//
46808d7c 5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
117bd748 14//
46808d7c 15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
6cdd0f9c 17
92e53168 18/**
46808d7c 19 * This file contains functions for managing user access
20 *
cc3edaa4 21 * <b>Public API vs internals</b>
5a4e7398 22 *
92e53168 23 * General users probably only care about
24 *
dcd6a775 25 * Context handling
26 * - get_context_instance()
27 * - get_context_instance_by_id()
28 * - get_parent_contexts()
29 * - get_child_contexts()
5a4e7398 30 *
dcd6a775 31 * Whether the user can do something...
92e53168 32 * - has_capability()
8a1b1c32 33 * - has_any_capability()
34 * - has_all_capabilities()
efd6fce5 35 * - require_capability()
dcd6a775 36 * - require_login() (from moodlelib)
37 *
38 * What courses has this user access to?
92e53168 39 * - get_user_courses_bycap()
dcd6a775 40 *
db70c4bd 41 * What users can do X in this context?
42 * - get_users_by_capability()
43 *
dcd6a775 44 * Enrol/unenrol
ad833c42 45 * - enrol_into_course()
46 * - role_assign()/role_unassign()
5a4e7398 47 *
92e53168 48 *
49 * Advanced use
dcd6a775 50 * - load_all_capabilities()
51 * - reload_all_capabilities()
bb2c22bd 52 * - has_capability_in_accessdata()
dcd6a775 53 * - is_siteadmin()
54 * - get_user_access_sitewide()
a2cf7f1b 55 * - load_subcontext()
dcd6a775 56 * - get_role_access_bycontext()
57 *
cc3edaa4 58 * <b>Name conventions</b>
5a4e7398 59 *
cc3edaa4 60 * "ctx" means context
92e53168 61 *
cc3edaa4 62 * <b>accessdata</b>
92e53168 63 *
64 * Access control data is held in the "accessdata" array
65 * which - for the logged-in user, will be in $USER->access
5a4e7398 66 *
d867e696 67 * For other users can be generated and passed around (but may also be cached
68 * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser.
92e53168 69 *
bb2c22bd 70 * $accessdata is a multidimensional array, holding
5a4e7398 71 * role assignments (RAs), role-capabilities-perm sets
51be70d2 72 * (role defs) and a list of courses we have loaded
92e53168 73 * data for.
74 *
5a4e7398 75 * Things are keyed on "contextpaths" (the path field of
92e53168 76 * the context table) for fast walking up/down the tree.
cc3edaa4 77 * <code>
bb2c22bd 78 * $accessdata[ra][$contextpath]= array($roleid)
79 * [$contextpath]= array($roleid)
5a4e7398 80 * [$contextpath]= array($roleid)
117bd748 81 * </code>
92e53168 82 *
83 * Role definitions are stored like this
84 * (no cap merge is done - so it's compact)
85 *
cc3edaa4 86 * <code>
bb2c22bd 87 * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
88 * [mod/forum:editallpost] = -1
89 * [mod/forum:startdiscussion] = -1000
cc3edaa4 90 * </code>
92e53168 91 *
bb2c22bd 92 * See how has_capability_in_accessdata() walks up/down the tree.
92e53168 93 *
94 * Normally - specially for the logged-in user, we only load
95 * rdef and ra down to the course level, but not below. This
96 * keeps accessdata small and compact. Below-the-course ra/rdef
97 * are loaded as needed. We keep track of which courses we
5a4e7398 98 * have loaded ra/rdef in
cc3edaa4 99 * <code>
5a4e7398 100 * $accessdata[loaded] = array($contextpath, $contextpath)
cc3edaa4 101 * </code>
92e53168 102 *
cc3edaa4 103 * <b>Stale accessdata</b>
92e53168 104 *
105 * For the logged-in user, accessdata is long-lived.
106 *
d867e696 107 * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
92e53168 108 * context paths affected by changes. Any check at-or-below
109 * a dirty context will trigger a transparent reload of accessdata.
5a4e7398 110 *
92e53168 111 * Changes at the sytem level will force the reload for everyone.
112 *
cc3edaa4 113 * <b>Default role caps</b>
5a4e7398 114 * The default role assignment is not in the DB, so we
115 * add it manually to accessdata.
92e53168 116 *
117 * This means that functions that work directly off the
118 * DB need to ensure that the default role caps
5a4e7398 119 * are dealt with appropriately.
92e53168 120 *
46808d7c 121 * @package moodlecore
122 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
123 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
92e53168 124 */
bbbf2d40 125
cc3edaa4 126/** permission definitions */
e49e61bf 127define('CAP_INHERIT', 0);
cc3edaa4 128/** permission definitions */
bbbf2d40 129define('CAP_ALLOW', 1);
cc3edaa4 130/** permission definitions */
bbbf2d40 131define('CAP_PREVENT', -1);
cc3edaa4 132/** permission definitions */
bbbf2d40 133define('CAP_PROHIBIT', -1000);
134
cc3edaa4 135/** context definitions */
bbbf2d40 136define('CONTEXT_SYSTEM', 10);
cc3edaa4 137/** context definitions */
4b10f08b 138define('CONTEXT_USER', 30);
cc3edaa4 139/** context definitions */
bbbf2d40 140define('CONTEXT_COURSECAT', 40);
cc3edaa4 141/** context definitions */
bbbf2d40 142define('CONTEXT_COURSE', 50);
cc3edaa4 143/** context definitions */
bbbf2d40 144define('CONTEXT_MODULE', 70);
cc3edaa4 145/** context definitions */
bbbf2d40 146define('CONTEXT_BLOCK', 80);
147
cc3edaa4 148/** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
21b6db6e 149define('RISK_MANAGETRUST', 0x0001);
cc3edaa4 150/** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
a6b02b65 151define('RISK_CONFIG', 0x0002);
cc3edaa4 152/** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
21b6db6e 153define('RISK_XSS', 0x0004);
cc3edaa4 154/** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
21b6db6e 155define('RISK_PERSONAL', 0x0008);
cc3edaa4 156/** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
21b6db6e 157define('RISK_SPAM', 0x0010);
cc3edaa4 158/** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
3a0c6cca 159define('RISK_DATALOSS', 0x0020);
21b6db6e 160
cc3edaa4 161/** rolename displays - the name as defined in the role definition */
162define('ROLENAME_ORIGINAL', 0);
163/** rolename displays - the name as defined by a role alias */
164define('ROLENAME_ALIAS', 1);
165/** rolename displays - Both, like this: Role alias (Original)*/
166define('ROLENAME_BOTH', 2);
167/** rolename displays - the name as defined in the role definition and the shortname in brackets*/
168define('ROLENAME_ORIGINALANDSHORT', 3);
169/** rolename displays - the name as defined by a role alias, in raw form suitable for editing*/
170define('ROLENAME_ALIAS_RAW', 4);
171
172/** size limit for context cache */
117bd748 173if (!defined('MAX_CONTEXT_CACHE_SIZE')) {
4d10247f 174 define('MAX_CONTEXT_CACHE_SIZE', 5000);
175}
176
cc3edaa4 177/**
117bd748 178 * Although this looks like a global variable, it isn't really.
cc3edaa4 179 *
117bd748
PS
180 * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
181 * It is used to cache various bits of data between function calls for performance reasons.
cc3edaa4 182 * Sadly, a PHP global variale is the only way to impleemnt this, withough rewriting everything
183 * as methods of a class, instead of functions.
184 *
185 * @global stdClass $ACCESSLIB_PRIVATE
186 * @name $ACCESSLIB_PRIVATE
187 */
d867e696 188$ACCESSLIB_PRIVATE = new stdClass;
189$ACCESSLIB_PRIVATE->contexts = array(); // Cache of context objects by level and instance
190$ACCESSLIB_PRIVATE->contextsbyid = array(); // Cache of context objects by id
191$ACCESSLIB_PRIVATE->systemcontext = null; // Used in get_system_context
192$ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache
193$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the $accessdata structure for users other than $USER
194$ACCESSLIB_PRIVATE->roledefinitions = array(); // role definitions cache - helps a lot with mem usage in cron
195$ACCESSLIB_PRIVATE->croncache = array(); // Used in get_role_access
196$ACCESSLIB_PRIVATE->preloadedcourses = array(); // Used in preload_course_contexts.
197$ACCESSLIB_PRIVATE->capabilitynames = null; // Used in is_valid_capability (only in developer debug mode)
bbbf2d40 198
d867e696 199/**
46808d7c 200 * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
117bd748 201 *
d867e696 202 * This method should ONLY BE USED BY UNIT TESTS. It clears all of
203 * accesslib's private caches. You need to do this before setting up test data,
204 * and also at the end fo the tests.
cc3edaa4 205 * @global object
206 * @global object
207 * @global object
d867e696 208 */
209function accesslib_clear_all_caches_for_unit_testing() {
210 global $UNITTEST, $USER, $ACCESSLIB_PRIVATE;
211 if (empty($UNITTEST->running)) {
212 throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
213 }
214 $ACCESSLIB_PRIVATE->contexts = array();
215 $ACCESSLIB_PRIVATE->contextsbyid = array();
216 $ACCESSLIB_PRIVATE->systemcontext = null;
217 $ACCESSLIB_PRIVATE->dirtycontexts = null;
218 $ACCESSLIB_PRIVATE->accessdatabyuser = array();
219 $ACCESSLIB_PRIVATE->roledefinitions = array();
220 $ACCESSLIB_PRIVATE->croncache = array();
221 $ACCESSLIB_PRIVATE->preloadedcourses = array();
222 $ACCESSLIB_PRIVATE->capabilitynames = null;
223
224 unset($USER->access);
225}
226
227/**
228 * Private function. Add a context object to accesslib's caches.
cc3edaa4 229 * @global object
46808d7c 230 * @param object $context
d867e696 231 */
232function cache_context($context) {
233 global $ACCESSLIB_PRIVATE;
4d10247f 234
235 // If there are too many items in the cache already, remove items until
236 // there is space
237 while (count($ACCESSLIB_PRIVATE->contextsbyid) >= MAX_CONTEXT_CACHE_SIZE) {
56f3599b 238 $first = reset($ACCESSLIB_PRIVATE->contextsbyid);
239 unset($ACCESSLIB_PRIVATE->contextsbyid[$first->id]);
4d10247f 240 unset($ACCESSLIB_PRIVATE->contexts[$first->contextlevel][$first->instanceid]);
241 }
242
d867e696 243 $ACCESSLIB_PRIVATE->contexts[$context->contextlevel][$context->instanceid] = $context;
244 $ACCESSLIB_PRIVATE->contextsbyid[$context->id] = $context;
245}
7700027f 246
46808d7c 247/**
248 * This is really slow!!! do not use above course context level
249 *
cc3edaa4 250 * @global object
46808d7c 251 * @param int $roleid
252 * @param object $context
253 * @return array
254 */
eef879ec 255function get_role_context_caps($roleid, $context) {
f33e1ed4 256 global $DB;
257
eef879ec 258 //this is really slow!!!! - do not use above course context level!
259 $result = array();
260 $result[$context->id] = array();
e7876c1e 261
eef879ec 262 // first emulate the parent context capabilities merging into context
263 $searchcontexts = array_reverse(get_parent_contexts($context));
264 array_push($searchcontexts, $context->id);
265 foreach ($searchcontexts as $cid) {
f33e1ed4 266 if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
eef879ec 267 foreach ($capabilities as $cap) {
268 if (!array_key_exists($cap->capability, $result[$context->id])) {
269 $result[$context->id][$cap->capability] = 0;
270 }
271 $result[$context->id][$cap->capability] += $cap->permission;
272 }
273 }
274 }
e7876c1e 275
eef879ec 276 // now go through the contexts bellow given context
19bb8a05 277 $searchcontexts = array_keys(get_child_contexts($context));
eef879ec 278 foreach ($searchcontexts as $cid) {
f33e1ed4 279 if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
eef879ec 280 foreach ($capabilities as $cap) {
281 if (!array_key_exists($cap->contextid, $result)) {
282 $result[$cap->contextid] = array();
283 }
284 $result[$cap->contextid][$cap->capability] = $cap->permission;
285 }
286 }
e7876c1e 287 }
288
eef879ec 289 return $result;
290}
291
eef879ec 292/**
46808d7c 293 * Gets the accessdata for role "sitewide" (system down to course)
343effbe 294 *
cc3edaa4 295 * @global object
296 * @global object
46808d7c 297 * @param int $roleid
298 * @param array $accessdata defaults to null
e0376a62 299 * @return array
eef879ec 300 */
bb2c22bd 301function get_role_access($roleid, $accessdata=NULL) {
cdfa3035 302
f33e1ed4 303 global $CFG, $DB;
eef879ec 304
e0376a62 305 /* Get it in 1 cheap DB query...
306 * - relevant role caps at the root and down
307 * to the course level - but not below
308 */
bb2c22bd 309 if (is_null($accessdata)) {
310 $accessdata = array(); // named list
311 $accessdata['ra'] = array();
312 $accessdata['rdef'] = array();
313 $accessdata['loaded'] = array();
e7876c1e 314 }
315
e0376a62 316 //
317 // Overrides for the role IN ANY CONTEXTS
318 // down to COURSE - not below -
319 //
320 $sql = "SELECT ctx.path,
321 rc.capability, rc.permission
f33e1ed4 322 FROM {context} ctx
323 JOIN {role_capabilities} rc
324 ON rc.contextid=ctx.id
325 WHERE rc.roleid = ?
326 AND ctx.contextlevel <= ".CONTEXT_COURSE."
327 ORDER BY ctx.depth, ctx.path";
328 $params = array($roleid);
133d5a97 329
a91b910e 330 // we need extra caching in CLI scripts and cron
331 if (CLI_SCRIPT) {
d867e696 332 global $ACCESSLIB_PRIVATE;
133d5a97 333
d867e696 334 if (!isset($ACCESSLIB_PRIVATE->croncache[$roleid])) {
335 $ACCESSLIB_PRIVATE->croncache[$roleid] = array();
f33e1ed4 336 if ($rs = $DB->get_recordset_sql($sql, $params)) {
337 foreach ($rs as $rd) {
d867e696 338 $ACCESSLIB_PRIVATE->croncache[$roleid][] = $rd;
133d5a97 339 }
f33e1ed4 340 $rs->close();
133d5a97 341 }
342 }
343
d867e696 344 foreach ($ACCESSLIB_PRIVATE->croncache[$roleid] as $rd) {
03cedd62 345 $k = "{$rd->path}:{$roleid}";
346 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
e0376a62 347 }
5a4e7398 348
133d5a97 349 } else {
f33e1ed4 350 if ($rs = $DB->get_recordset_sql($sql, $params)) {
351 foreach ($rs as $rd) {
133d5a97 352 $k = "{$rd->path}:{$roleid}";
353 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
354 }
355 unset($rd);
f33e1ed4 356 $rs->close();
133d5a97 357 }
8d2b18a8 358 }
e0376a62 359
bb2c22bd 360 return $accessdata;
e7876c1e 361}
362
4e1fe7d1 363/**
46808d7c 364 * Gets the accessdata for role "sitewide" (system down to course)
4e1fe7d1 365 *
cc3edaa4 366 * @global object
367 * @global object
46808d7c 368 * @param int $roleid
369 * @param array $accessdata defaults to null
4e1fe7d1 370 * @return array
371 */
372function get_default_frontpage_role_access($roleid, $accessdata=NULL) {
373
f33e1ed4 374 global $CFG, $DB;
5a4e7398 375
4e1fe7d1 376 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
377 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
5a4e7398 378
4e1fe7d1 379 //
380 // Overrides for the role in any contexts related to the course
381 //
382 $sql = "SELECT ctx.path,
383 rc.capability, rc.permission
f33e1ed4 384 FROM {context} ctx
385 JOIN {role_capabilities} rc
386 ON rc.contextid=ctx.id
387 WHERE rc.roleid = ?
388 AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?)
389 AND ctx.contextlevel <= ".CONTEXT_COURSE."
390 ORDER BY ctx.depth, ctx.path";
391 $params = array($roleid, "$base/%");
5a4e7398 392
f33e1ed4 393 if ($rs = $DB->get_recordset_sql($sql, $params)) {
394 foreach ($rs as $rd) {
03cedd62 395 $k = "{$rd->path}:{$roleid}";
396 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
4e1fe7d1 397 }
03cedd62 398 unset($rd);
f33e1ed4 399 $rs->close();
4e1fe7d1 400 }
401
402 return $accessdata;
403}
404
405
8f8ed475 406/**
407 * Get the default guest role
117bd748 408 *
cc3edaa4 409 * @global object
410 * @global object
8f8ed475 411 * @return object role
412 */
413function get_guest_role() {
f33e1ed4 414 global $CFG, $DB;
ebce32b5 415
416 if (empty($CFG->guestroleid)) {
417 if ($roles = get_roles_with_capability('moodle/legacy:guest', CAP_ALLOW)) {
418 $guestrole = array_shift($roles); // Pick the first one
419 set_config('guestroleid', $guestrole->id);
420 return $guestrole;
421 } else {
422 debugging('Can not find any guest role!');
423 return false;
424 }
8f8ed475 425 } else {
f33e1ed4 426 if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
ebce32b5 427 return $guestrole;
428 } else {
429 //somebody is messing with guest roles, remove incorrect setting and try to find a new one
430 set_config('guestroleid', '');
431 return get_guest_role();
432 }
8f8ed475 433 }
434}
435
128f0984 436/**
41e87d30 437 * Check whether a user has a paritcular capability in a given context.
46808d7c 438 *
41e87d30 439 * For example::
440 * $context = get_context_instance(CONTEXT_MODULE, $cm->id);
441 * has_capability('mod/forum:replypost',$context)
46808d7c 442 *
41e87d30 443 * By default checks the capabilties of the current user, but you can pass a
444 * different userid. By default will return true for admin-like users who have the
445 * moodle/site:doanything capability, but you can override that with the fourth argument.
117bd748 446 *
41e87d30 447 * @param string $capability the name of the capability to check. For example mod/forum:view
448 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
449 * @param integer $userid A user id. By default (null) checks the permissions of the current user.
450 * @param boolean $doanything If false, ignore the special moodle/site:doanything capability that admin-like roles have.
451 * @return boolean true if the user has this capability. Otherwise false.
128f0984 452 */
7d0c81b3 453function has_capability($capability, $context, $userid=NULL, $doanything=true) {
d867e696 454 global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE;
18818abf 455
31a99877 456 if (during_initial_install()) {
18818abf 457 if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") {
458 // we are in an installer - roles can not work yet
459 return true;
460 } else {
461 return false;
462 }
463 }
7f97ea29 464
7d0c81b3 465 // the original $CONTEXT here was hiding serious errors
128f0984 466 // for security reasons do not reuse previous context
7d0c81b3 467 if (empty($context)) {
468 debugging('Incorrect context specified');
469 return false;
74ac5b66 470 }
7f97ea29 471
cc3d5e10 472/// Some sanity checks
473 if (debugging('',DEBUG_DEVELOPER)) {
ad944e78 474 if (!is_valid_capability($capability)) {
475 debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
cc3d5e10 476 }
588cd516 477 if (!is_bool($doanything)) {
cc3d5e10 478 debugging('Capability parameter "doanything" is wierd ("'.$doanything.'"). This should be fixed in code.');
479 }
480 }
481
128f0984 482 if (empty($userid)) { // we must accept null, 0, '0', '' etc. in $userid
c84a2dbe 483 if (empty($USER->id)) {
484 // Session not set up yet.
024f3f97 485 $userid = 0;
e92c286c 486 } else {
487 $userid = $USER->id;
c84a2dbe 488 }
7f97ea29 489 }
490
128f0984 491 if (is_null($context->path) or $context->depth == 0) {
492 //this should not happen
493 $contexts = array(SYSCONTEXTID, $context->id);
494 $context->path = '/'.SYSCONTEXTID.'/'.$context->id;
495 debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);
496
7f97ea29 497 } else {
498 $contexts = explode('/', $context->path);
499 array_shift($contexts);
500 }
501
a91b910e 502 if (CLI_SCRIPT && !isset($USER->access)) {
1a9b6787 503 // In cron, some modules setup a 'fake' $USER,
504 // ensure we load the appropriate accessdata.
d867e696 505 if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
506 $ACCESSLIB_PRIVATE->dirtycontexts = NULL; //load fresh dirty contexts
128f0984 507 } else {
1a9b6787 508 load_user_accessdata($userid);
d867e696 509 $ACCESSLIB_PRIVATE->dirtycontexts = array();
1a9b6787 510 }
d867e696 511 $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
7d0c81b3 512
7fde45a7 513 } else if (isset($USER->id) && ($USER->id == $userid) && !isset($USER->access)) {
7d0c81b3 514 // caps not loaded yet - better to load them to keep BC with 1.8
128f0984 515 // not-logged-in user or $USER object set up manually first time here
7d0c81b3 516 load_all_capabilities();
d867e696 517 $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now
518 $ACCESSLIB_PRIVATE->roledefinitions = array();
1a9b6787 519 }
520
128f0984 521 // Load dirty contexts list if needed
d867e696 522 if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
7be3be1b 523 if (isset($USER->access['time'])) {
d867e696 524 $ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']);
7be3be1b 525 }
526 else {
d867e696 527 $ACCESSLIB_PRIVATE->dirtycontexts = array();
7be3be1b 528 }
148eb2a7 529 }
128f0984 530
531 // Careful check for staleness...
d867e696 532 if (count($ACCESSLIB_PRIVATE->dirtycontexts) !== 0 and is_contextpath_dirty($contexts, $ACCESSLIB_PRIVATE->dirtycontexts)) {
ef989bd9 533 // reload all capabilities - preserving loginas, roleswitches, etc
534 // and then cleanup any marks of dirtyness... at least from our short
535 // term memory! :-)
d867e696 536 $ACCESSLIB_PRIVATE->accessdatabyuser = array();
537 $ACCESSLIB_PRIVATE->roledefinitions = array();
128f0984 538
a91b910e 539 if (CLI_SCRIPT) {
128f0984 540 load_user_accessdata($userid);
d867e696 541 $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
542 $ACCESSLIB_PRIVATE->dirtycontexts = array();
128f0984 543
544 } else {
545 reload_all_capabilities();
546 }
148eb2a7 547 }
128f0984 548
13a79475 549 // divulge how many times we are called
550 //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
551
7fde45a7 552 if (isset($USER->id) && ($USER->id == $userid)) { // we must accept strings and integers in $userid
74ac5b66 553 //
554 // For the logged in user, we have $USER->access
555 // which will have all RAs and caps preloaded for
556 // course and above contexts.
557 //
558 // Contexts below courses && contexts that do not
559 // hang from courses are loaded into $USER->access
560 // on demand, and listed in $USER->access[loaded]
561 //
7f97ea29 562 if ($context->contextlevel <= CONTEXT_COURSE) {
563 // Course and above are always preloaded
bb2c22bd 564 return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
7f97ea29 565 }
420bfab1 566 // Load accessdata for below-the-course contexts
31c2de82 567 if (!path_inaccessdata($context->path,$USER->access)) {
6100dad0 568 // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
74ac5b66 569 // $bt = debug_backtrace();
570 // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
a2cf7f1b 571 load_subcontext($USER->id, $context, $USER->access);
74ac5b66 572 }
bb2c22bd 573 return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
7f97ea29 574 }
bb2c22bd 575
d867e696 576 if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
204a369c 577 load_user_accessdata($userid);
578 }
420bfab1 579 if ($context->contextlevel <= CONTEXT_COURSE) {
580 // Course and above are always preloaded
d867e696 581 return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid], $doanything);
420bfab1 582 }
583 // Load accessdata for below-the-course contexts as needed
d867e696 584 if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
6100dad0 585 // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
420bfab1 586 // $bt = debug_backtrace();
587 // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
d867e696 588 load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
420bfab1 589 }
d867e696 590 return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid], $doanything);
7f97ea29 591}
592
3fc3ebf2 593/**
41e87d30 594 * Check if the user has any one of several capabilities from a list.
46808d7c 595 *
41e87d30 596 * This is just a utility method that calls has_capability in a loop. Try to put
597 * the capabilities that most users are likely to have first in the list for best
598 * performance.
3fc3ebf2 599 *
600 * There are probably tricks that could be done to improve the performance here, for example,
601 * check the capabilities that are already cached first.
602 *
46808d7c 603 * @see has_capability()
41e87d30 604 * @param array $capabilities an array of capability names.
605 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
606 * @param integer $userid A user id. By default (null) checks the permissions of the current user.
607 * @param boolean $doanything If false, ignore the special moodle/site:doanything capability that admin-like roles have.
608 * @return boolean true if the user has any of these capabilities. Otherwise false.
3fc3ebf2 609 */
610function has_any_capability($capabilities, $context, $userid=NULL, $doanything=true) {
41e87d30 611 if (!is_array($capabilities)) {
612 debugging('Incorrect $capabilities parameter in has_any_capabilities() call - must be an array');
613 return false;
614 }
3fc3ebf2 615 foreach ($capabilities as $capability) {
59664877 616 if (has_capability($capability, $context, $userid, $doanything)) {
3fc3ebf2 617 return true;
618 }
619 }
620 return false;
621}
622
8a1b1c32 623/**
41e87d30 624 * Check if the user has all the capabilities in a list.
46808d7c 625 *
41e87d30 626 * This is just a utility method that calls has_capability in a loop. Try to put
627 * the capabilities that fewest users are likely to have first in the list for best
628 * performance.
8a1b1c32 629 *
630 * There are probably tricks that could be done to improve the performance here, for example,
631 * check the capabilities that are already cached first.
632 *
46808d7c 633 * @see has_capability()
41e87d30 634 * @param array $capabilities an array of capability names.
635 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
636 * @param integer $userid A user id. By default (null) checks the permissions of the current user.
637 * @param boolean $doanything If false, ignore the special moodle/site:doanything capability that admin-like roles have.
638 * @return boolean true if the user has all of these capabilities. Otherwise false.
8a1b1c32 639 */
640function has_all_capabilities($capabilities, $context, $userid=NULL, $doanything=true) {
3ce50127 641 if (!is_array($capabilities)) {
642 debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
643 return false;
644 }
8a1b1c32 645 foreach ($capabilities as $capability) {
646 if (!has_capability($capability, $context, $userid, $doanything)) {
647 return false;
648 }
649 }
650 return true;
651}
652
128f0984 653/**
46808d7c 654 * Check if the user is an admin at the site level
655 *
39407442 656 * Uses 1 DB query to answer whether a user is an admin at the sitelevel.
657 * It depends on DB schema >=1.7 but does not depend on the new datastructures
658 * in v1.9 (context.path, or $USER->access)
659 *
660 * Will return true if the userid has any of
661 * - moodle/site:config
662 * - moodle/legacy:admin
663 * - moodle/site:doanything
664 *
cc3edaa4 665 * @global object
666 * @global object
39407442 667 * @param int $userid
e3c7f155 668 * @returns bool true is user can administer server settings
39407442 669 */
670function is_siteadmin($userid) {
f33e1ed4 671 global $CFG, $DB;
39407442 672
4827029b 673 $sql = "SELECT SUM(rc.permission)
f33e1ed4 674 FROM {role_capabilities} rc
5a4e7398 675 JOIN {context} ctx
f33e1ed4 676 ON ctx.id=rc.contextid
677 JOIN {role_assignments} ra
678 ON ra.roleid=rc.roleid AND ra.contextid=ctx.id
679 WHERE ctx.contextlevel=10
680 AND ra.userid=?
5a4e7398 681 AND rc.capability IN (?, ?, ?)
f33e1ed4 682 GROUP BY rc.capability
4827029b 683 HAVING SUM(rc.permission) > 0";
5a4e7398 684 $params = array($userid, 'moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');
39407442 685
e3c7f155 686 return $DB->record_exists_sql($sql, $params);
39407442 687}
688
6cab02ac 689/**
46808d7c 690 * Check whether a role is an admin at the site level
691 *
692 * Will return true if the userid has any of
693 * - moodle/site:config
694 * - moodle/legacy:admin
695 * - moodle/site:doanything
696 *
cc3edaa4 697 * @global object
6cab02ac 698 * @param integer $roleid a role id.
699 * @return boolean, whether this role is an admin role.
700 */
701function is_admin_role($roleid) {
bbdb7070 702 global $DB;
6cab02ac 703
704 $sql = "SELECT 1
705 FROM {role_capabilities} rc
706 JOIN {context} ctx ON ctx.id = rc.contextid
707 WHERE ctx.contextlevel = 10
708 AND rc.roleid = ?
709 AND rc.capability IN (?, ?, ?)
710 GROUP BY rc.capability
711 HAVING SUM(rc.permission) > 0";
712 $params = array($roleid, 'moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');
713
714 return $DB->record_exists_sql($sql, $params);
715}
716
bbdb7070 717/**
46808d7c 718 * Returns all the roles for which is_admin_role($role->id) is true.
719 *
cc3edaa4 720 * @global object
46808d7c 721 * @return array
bbdb7070 722 */
723function get_admin_roles() {
724 global $DB;
725
726 $sql = "SELECT *
727 FROM {role} r
728 WHERE EXISTS (
729 SELECT 1
730 FROM {role_capabilities} rc
731 JOIN {context} ctx ON ctx.id = rc.contextid
732 WHERE ctx.contextlevel = 10
733 AND rc.roleid = r.id
734 AND rc.capability IN (?, ?, ?)
735 GROUP BY rc.capability
736 HAVING SUM(rc.permission) > 0
737 )
738 ORDER BY r.sortorder";
739 $params = array('moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');
740
741 return $DB->get_records_sql($sql, $params);
742}
743
46808d7c 744/**
745 * @param string $path
746 * @return string
747 */
7f97ea29 748function get_course_from_path ($path) {
749 // assume that nothing is more than 1 course deep
750 if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
751 return $matches[1];
752 }
753 return false;
754}
755
46808d7c 756/**
757 * @param string $path
758 * @param array $accessdata
759 * @return bool
760 */
bb2c22bd 761function path_inaccessdata($path, $accessdata) {
74ac5b66 762
763 // assume that contexts hang from sys or from a course
764 // this will only work well with stuff that hangs from a course
bb2c22bd 765 if (in_array($path, $accessdata['loaded'], true)) {
b738808b 766 // error_log("found it!");
74ac5b66 767 return true;
768 }
769 $base = '/' . SYSCONTEXTID;
770 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
771 $path = $matches[1];
772 if ($path === $base) {
773 return false;
774 }
bb2c22bd 775 if (in_array($path, $accessdata['loaded'], true)) {
74ac5b66 776 return true;
777 }
778 }
779 return false;
780}
781
128f0984 782/**
46808d7c 783 *
784 * Walk the accessdata array and return true/false
785 *
31c2de82 786 * Walk the accessdata array and return true/false.
6a8d9a38 787 * Deals with prohibits, roleswitching, aggregating
788 * capabilities, etc.
789 *
790 * The main feature of here is being FAST and with no
5a4e7398 791 * side effects.
6a8d9a38 792 *
3ac81bd1 793 * Notes:
794 *
795 * Switch Roles exits early
796 * -----------------------
797 * cap checks within a switchrole need to exit early
798 * in our bottom up processing so they don't "see" that
799 * there are real RAs that can do all sorts of things.
800 *
3d034f77 801 * Switch Role merges with default role
802 * ------------------------------------
803 * If you are a teacher in course X, you have at least
804 * teacher-in-X + defaultloggedinuser-sitewide. So in the
805 * course you'll have techer+defaultloggedinuser.
806 * We try to mimic that in switchrole.
807 *
c7a8ec8c 808 * Local-most role definition and role-assignment wins
809 * ---------------------------------------------------
810 * So if the local context has said 'allow', it wins
811 * over a high-level context that says 'deny'.
812 * This is applied when walking rdefs, and RAs.
813 * Only at the same context the values are SUM()med.
814 *
815 * The exception is CAP_PROHIBIT.
816 *
3ac81bd1 817 * "Guest default role" exception
818 * ------------------------------
819 *
820 * See MDL-7513 and $ignoreguest below for details.
821 *
822 * The rule is that
823 *
824 * IF we are being asked about moodle/legacy:guest
825 * OR moodle/course:view
826 * FOR a real, logged-in user
827 * AND we reached the top of the path in ra and rdef
828 * AND that role has moodle/legacy:guest === 1...
829 * THEN we act as if we hadn't seen it.
830 *
212235d3 831 * Note that this function must be kept in synch with has_capability_in_accessdata.
3ac81bd1 832 *
833 * To Do:
46808d7c 834 * @todo Document how it works
835 * @todo Rewrite in ASM
6a8d9a38 836 *
cc3edaa4 837 * @global object
46808d7c 838 * @param string $capability
839 * @param object $context
840 * @param array $accessdata
841 * @param bool $doanything
842 * @return bool
6a8d9a38 843 */
bb2c22bd 844function has_capability_in_accessdata($capability, $context, $accessdata, $doanything) {
7f97ea29 845
3ac81bd1 846 global $CFG;
847
7f97ea29 848 $path = $context->path;
849
850 // build $contexts as a list of "paths" of the current
851 // contexts and parents with the order top-to-bottom
852 $contexts = array($path);
853 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
854 $path = $matches[1];
855 array_unshift($contexts, $path);
856 }
3ac81bd1 857
858 $ignoreguest = false;
bb2c22bd 859 if (isset($accessdata['dr'])
3ac81bd1 860 && ($capability == 'moodle/course:view'
861 || $capability == 'moodle/legacy:guest')) {
862 // At the base, ignore rdefs where moodle/legacy:guest
863 // is set
bb2c22bd 864 $ignoreguest = $accessdata['dr'];
3ac81bd1 865 }
866
4f957b11 867 // Coerce it to an int
868 $CAP_PROHIBIT = (int)CAP_PROHIBIT;
e0376a62 869
7f97ea29 870 $cc = count($contexts);
871
c7a8ec8c 872 $can = 0;
4f957b11 873 $capdepth = 0;
6a8d9a38 874
3ac81bd1 875 //
876 // role-switches loop
877 //
bb2c22bd 878 if (isset($accessdata['rsw'])) {
5a4e7398 879 // check for isset() is fast
6a8d9a38 880 // empty() is slow...
bb2c22bd 881 if (empty($accessdata['rsw'])) {
882 unset($accessdata['rsw']); // keep things fast and unambiguous
6a8d9a38 883 break;
884 }
885 // From the bottom up...
886 for ($n=$cc-1;$n>=0;$n--) {
887 $ctxp = $contexts[$n];
bb2c22bd 888 if (isset($accessdata['rsw'][$ctxp])) {
6a8d9a38 889 // Found a switchrole assignment
3d034f77 890 // check for that role _plus_ the default user role
bb2c22bd 891 $ras = array($accessdata['rsw'][$ctxp],$CFG->defaultuserroleid);
3d034f77 892 for ($rn=0;$rn<2;$rn++) {
4f957b11 893 $roleid = (int)$ras[$rn];
3d034f77 894 // Walk the path for capabilities
895 // from the bottom up...
896 for ($m=$cc-1;$m>=0;$m--) {
897 $capctxp = $contexts[$m];
bb2c22bd 898 if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
4f957b11 899 $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
900
c7a8ec8c 901 // The most local permission (first to set) wins
902 // the only exception is CAP_PROHIBIT
903 if ($can === 0) {
904 $can = $perm;
4f957b11 905 } elseif ($perm === $CAP_PROHIBIT) {
c7a8ec8c 906 $can = $perm;
907 break;
3d034f77 908 }
6a8d9a38 909 }
910 }
911 }
912 // As we are dealing with a switchrole,
5a4e7398 913 // we return _here_, do _not_ walk up
6a8d9a38 914 // the hierarchy any further
915 if ($can < 1) {
916 if ($doanything) {
917 // didn't find it as an explicit cap,
294c03a9 918 // but maybe the user can doanything in this context...
bb2c22bd 919 return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
6a8d9a38 920 } else {
921 return false;
922 }
923 } else {
924 return true;
925 }
5a4e7398 926
6a8d9a38 927 }
928 }
929 }
930
3ac81bd1 931 //
932 // Main loop for normal RAs
933 // From the bottom up...
934 //
7f97ea29 935 for ($n=$cc-1;$n>=0;$n--) {
936 $ctxp = $contexts[$n];
bb2c22bd 937 if (isset($accessdata['ra'][$ctxp])) {
6cc59cb2 938 // Found role assignments on this leaf
bb2c22bd 939 $ras = $accessdata['ra'][$ctxp];
4f957b11 940
941 $rc = count($ras);
942 $ctxcan = 0;
943 $ctxcapdepth = 0;
6cc59cb2 944 for ($rn=0;$rn<$rc;$rn++) {
4f957b11 945 $roleid = (int)$ras[$rn];
c7a8ec8c 946 $rolecan = 0;
4f957b11 947 $rolecapdepth = 0;
6cc59cb2 948 // Walk the path for capabilities
949 // from the bottom up...
950 for ($m=$cc-1;$m>=0;$m--) {
951 $capctxp = $contexts[$m];
3ac81bd1 952 // ignore some guest caps
953 // at base ra and rdef
954 if ($ignoreguest == $roleid
955 && $n === 0
956 && $m === 0
bb2c22bd 957 && isset($accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'])
958 && $accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'] > 0) {
3ac81bd1 959 continue;
960 }
bb2c22bd 961 if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
4f957b11 962 $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
c7a8ec8c 963 // The most local permission (first to set) wins
964 // the only exception is CAP_PROHIBIT
965 if ($rolecan === 0) {
4f957b11 966 $rolecan = $perm;
967 $rolecapdepth = $m;
968 } elseif ($perm === $CAP_PROHIBIT) {
969 $rolecan = $perm;
970 $rolecapdepth = $m;
c7a8ec8c 971 break;
6cc59cb2 972 }
7f97ea29 973 }
974 }
4f957b11 975 // Rules for RAs at the same context...
976 // - prohibits always wins
977 // - permissions at the same ctxlevel & capdepth are added together
978 // - deeper capdepth wins
979 if ($ctxcan === $CAP_PROHIBIT || $rolecan === $CAP_PROHIBIT) {
980 $ctxcan = $CAP_PROHIBIT;
981 $ctxcapdepth = 0;
982 } elseif ($ctxcapdepth === $rolecapdepth) {
983 $ctxcan += $rolecan;
984 } elseif ($ctxcapdepth < $rolecapdepth) {
985 $ctxcan = $rolecan;
986 $ctxcapdepth = $rolecapdepth;
987 } else { // ctxcaptdepth is deeper
988 // rolecap ignored
989 }
c7a8ec8c 990 }
991 // The most local RAs with a defined
992 // permission ($ctxcan) win, except
993 // for CAP_PROHIBIT
4f957b11 994 // NOTE: If we want the deepest RDEF to
995 // win regardless of the depth of the RA,
996 // change the elseif below to read
997 // ($can === 0 || $capdepth < $ctxcapdepth) {
998 if ($ctxcan === $CAP_PROHIBIT) {
c7a8ec8c 999 $can = $ctxcan;
1000 break;
4f957b11 1001 } elseif ($can === 0) { // see note above
1002 $can = $ctxcan;
1003 $capdepth = $ctxcapdepth;
7f97ea29 1004 }
1005 }
1006 }
1007
1008 if ($can < 1) {
1009 if ($doanything) {
1010 // didn't find it as an explicit cap,
294c03a9 1011 // but maybe the user can doanything in this context...
bb2c22bd 1012 return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
7f97ea29 1013 } else {
1014 return false;
1015 }
1016 } else {
1017 return true;
1018 }
1019
1020}
018d4b52 1021
46808d7c 1022/**
1023 * @param object $context
1024 * @param array $accessdata
1025 * @return array
1026 */
bb2c22bd 1027function aggregate_roles_from_accessdata($context, $accessdata) {
018d4b52 1028
1029 $path = $context->path;
1030
1031 // build $contexts as a list of "paths" of the current
1032 // contexts and parents with the order top-to-bottom
1033 $contexts = array($path);
1034 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
1035 $path = $matches[1];
1036 array_unshift($contexts, $path);
1037 }
018d4b52 1038
1039 $cc = count($contexts);
1040
1041 $roles = array();
1042 // From the bottom up...
1043 for ($n=$cc-1;$n>=0;$n--) {
1044 $ctxp = $contexts[$n];
bb2c22bd 1045 if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
6cc59cb2 1046 // Found assignments on this leaf
bb2c22bd 1047 $addroles = $accessdata['ra'][$ctxp];
6cc59cb2 1048 $roles = array_merge($roles, $addroles);
018d4b52 1049 }
1050 }
1051
1052 return array_unique($roles);
1053}
1054
0468976c 1055/**
41e87d30 1056 * A convenience function that tests has_capability, and displays an error if
1057 * the user does not have that capability.
8a9c1c1c 1058 *
41e87d30 1059 * NOTE before Moodle 2.0, this function attempted to make an appropriate
1060 * require_login call before checking the capability. This is no longer the case.
1061 * You must call require_login (or one of its variants) if you want to check the
1062 * user is logged in, before you call this function.
efd6fce5 1063 *
46808d7c 1064 * @see has_capability()
1065 *
41e87d30 1066 * @param string $capability the name of the capability to check. For example mod/forum:view
1067 * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
1068 * @param integer $userid A user id. By default (null) checks the permissions of the current user.
1069 * @param bool $doanything If false, ignore the special moodle/site:doanything capability that admin-like roles have.
1070 * @param string $errorstring The error string to to user. Defaults to 'nopermissions'.
1071 * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
1072 * @return void terminates with an error if the user does not have the given capability.
0468976c 1073 */
41e87d30 1074function require_capability($capability, $context, $userid = NULL, $doanything = true,
1075 $errormessage = 'nopermissions', $stringfile = '') {
d74067e8 1076 if (!has_capability($capability, $context, $userid, $doanything)) {
9a0df45a 1077 throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
0468976c 1078 }
1079}
1080
128f0984 1081/**
46808d7c 1082 * Get an array of courses where cap requested is available
1083 *
e1d5e5c1 1084 * Get an array of courses (with magic extra bits)
31c2de82 1085 * where the accessdata and in DB enrolments show
573674bf 1086 * that the cap requested is available.
e1d5e5c1 1087 *
1088 * The main use is for get_my_courses().
1089 *
1090 * Notes
1091 *
1092 * - $fields is an array of fieldnames to ADD
1093 * so name the fields you really need, which will
1094 * be added and uniq'd
1095 *
1096 * - the course records have $c->context which is a fully
1097 * valid context object. Saves you a query per course!
1098 *
956b2f10 1099 * - the course records have $c->categorypath to make
1100 * category lookups cheap
1101 *
573674bf 1102 * - current implementation is split in -
1103 *
1104 * - if the user has the cap systemwide, stupidly
1105 * grab *every* course for a capcheck. This eats
5a4e7398 1106 * a TON of bandwidth, specially on large sites
573674bf 1107 * with separate DBs...
1108 *
1109 * - otherwise, fetch "likely" courses with a wide net
1110 * that should get us _cheaply_ at least the courses we need, and some
1111 * we won't - we get courses that...
1112 * - are in a category where user has the cap
1113 * - or where use has a role-assignment (any kind)
1114 * - or where the course has an override on for this cap
1115 *
1116 * - walk the courses recordset checking the caps oneach one
1117 * the checks are all in memory and quite fast
1118 * (though we could implement a specialised variant of the
bb2c22bd 1119 * has_capability_in_accessdata() code to speed it up)
e1d5e5c1 1120 *
cc3edaa4 1121 * @global object
1122 * @global object
e1d5e5c1 1123 * @param string $capability - name of the capability
bb2c22bd 1124 * @param array $accessdata - accessdata session array
e1d5e5c1 1125 * @param bool $doanything - if false, ignore do anything
1126 * @param string $sort - sorting fields - prefix each fieldname with "c."
1127 * @param array $fields - additional fields you are interested in...
1128 * @param int $limit - set if you want to limit the number of courses
1129 * @return array $courses - ordered array of course objects - see notes above
e1d5e5c1 1130 */
bb2c22bd 1131function get_user_courses_bycap($userid, $cap, $accessdata, $doanything, $sort='c.sortorder ASC', $fields=NULL, $limit=0) {
e1d5e5c1 1132
f33e1ed4 1133 global $CFG, $DB;
e1d5e5c1 1134
352f6f74 1135 // Slim base fields, let callers ask for what they need...
1136 $basefields = array('id', 'sortorder', 'shortname', 'idnumber');
e1d5e5c1 1137
1138 if (!is_null($fields)) {
e1d5e5c1 1139 $fields = array_merge($basefields, $fields);
1140 $fields = array_unique($fields);
1141 } else {
1142 $fields = $basefields;
1143 }
09740513 1144 // If any of the fields is '*', leave it alone, discarding the rest
1145 // to avoid ambiguous columns under some silly DBs. See MDL-18746 :-D
1146 if (in_array('*', $fields)) {
1147 $fields = array('*');
1148 }
41709a38 1149 $coursefields = 'c.' .implode(',c.', $fields);
573674bf 1150
d4bec858 1151 $sort = trim($sort);
1152 if ($sort !== '') {
1153 $sort = "ORDER BY $sort";
1154 }
1155
573674bf 1156 $sysctx = get_context_instance(CONTEXT_SYSTEM);
bb2c22bd 1157 if (has_capability_in_accessdata($cap, $sysctx, $accessdata, $doanything)) {
573674bf 1158 //
1159 // Apparently the user has the cap sitewide, so walk *every* course
1160 // (the cap checks are moderately fast, but this moves massive bandwidth w the db)
1161 // Yuck.
1162 //
1163 $sql = "SELECT $coursefields,
45ea1afb 1164 ctx.id AS ctxid, ctx.path AS ctxpath,
1165 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
956b2f10 1166 cc.path AS categorypath
f33e1ed4 1167 FROM {course} c
1168 JOIN {course_categories} cc
1169 ON c.category=cc.id
5a4e7398 1170 JOIN {context} ctx
f33e1ed4 1171 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1172 $sort ";
1173 $rs = $DB->get_recordset_sql($sql);
573674bf 1174 } else {
1175 //
1176 // narrow down where we have the caps to a few contexts
1177 // this will be a combination of
4ce76376 1178 // - courses where user has an explicit enrolment
1179 // - courses that have an override (any status) on that capability
1180 // - categories where user has the rights (granted status) on that capability
5a4e7398 1181 //
573674bf 1182 $sql = "SELECT ctx.*
f33e1ed4 1183 FROM {context} ctx
1184 WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
1185 ORDER BY ctx.depth";
1186 $rs = $DB->get_recordset_sql($sql);
573674bf 1187 $catpaths = array();
f33e1ed4 1188 foreach ($rs as $catctx) {
5a4e7398 1189 if ($catctx->path != ''
03cedd62 1190 && has_capability_in_accessdata($cap, $catctx, $accessdata, $doanything)) {
1191 $catpaths[] = $catctx->path;
573674bf 1192 }
1193 }
f33e1ed4 1194 $rs->close();
573674bf 1195 $catclause = '';
5a4e7398 1196 $params = array();
573674bf 1197 if (count($catpaths)) {
1198 $cc = count($catpaths);
1199 for ($n=0;$n<$cc;$n++) {
4ce76376 1200 $catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'";
573674bf 1201 }
4ce76376 1202 $catclause = 'WHERE (' . implode(' OR ', $catpaths) .')';
573674bf 1203 }
1204 unset($catpaths);
2e059c77 1205
1206 $capany = '';
1207 if ($doanything) {
5a4e7398 1208 $capany = " OR rc.capability=:doany";
1209 $params['doany'] = 'moodle/site:doanything';
2e059c77 1210 }
4ce76376 1211
1212 /// UNION 3 queries:
1213 /// - user role assignments in courses
1214 /// - user capability (override - any status) in courses
1215 /// - user right (granted status) in categories (optionally executed)
1216 /// Enclosing the 3-UNION into an inline_view to avoid column names conflict and making the ORDER BY cross-db
1217 /// and to allow selection of TEXT columns in the query (MSSQL and Oracle limitation). MDL-16209
1218 $sql = "
f689028c 1219 SELECT $coursefields, ctxid, ctxpath, ctxdepth, ctxlevel, categorypath
4ce76376 1220 FROM (
1221 SELECT c.id,
1222 ctx.id AS ctxid, ctx.path AS ctxpath,
1223 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1224 cc.path AS categorypath
ffdd703a 1225 FROM {course} c
1226 JOIN {course_categories} cc
4ce76376 1227 ON c.category=cc.id
ffdd703a 1228 JOIN {context} ctx
4ce76376 1229 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
ffdd703a 1230 JOIN {role_assignments} ra
4ce76376 1231 ON (ra.contextid=ctx.id AND ra.userid=:userid)
1232 UNION
1233 SELECT c.id,
1234 ctx.id AS ctxid, ctx.path AS ctxpath,
1235 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1236 cc.path AS categorypath
ffdd703a 1237 FROM {course} c
1238 JOIN {course_categories} cc
4ce76376 1239 ON c.category=cc.id
ffdd703a 1240 JOIN {context} ctx
4ce76376 1241 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
ffdd703a 1242 JOIN {role_capabilities} rc
4ce76376 1243 ON (rc.contextid=ctx.id AND (rc.capability=:cap $capany)) ";
1244
1245 if (!empty($catclause)) { /// If we have found the right in categories, add child courses here too
1246 $sql .= "
1247 UNION
1248 SELECT c.id,
1249 ctx.id AS ctxid, ctx.path AS ctxpath,
1250 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1251 cc.path AS categorypath
ffdd703a 1252 FROM {course} c
1253 JOIN {course_categories} cc
4ce76376 1254 ON c.category=cc.id
ffdd703a 1255 JOIN {context} ctx
4ce76376 1256 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1257 $catclause";
1258 }
1259
1260 /// Close the inline_view and join with courses table to get requested $coursefields
1261 $sql .= "
1262 ) inline_view
ffdd703a 1263 INNER JOIN {course} c
4ce76376 1264 ON inline_view.id = c.id";
1265
1266 /// To keep cross-db we need to strip any prefix in the ORDER BY clause for queries using UNION
1267 $sql .= "
1268 " . preg_replace('/[a-z]+\./i', '', $sort); /// Add ORDER BY clause
1269
5a4e7398 1270 $params['userid'] = $userid;
1271 $params['cap'] = $cap;
f33e1ed4 1272 $rs = $DB->get_recordset_sql($sql, $params);
573674bf 1273 }
4ce76376 1274
1275/// Confirm rights (granted capability) for each course returned
e1d5e5c1 1276 $courses = array();
1277 $cc = 0; // keep count
f33e1ed4 1278 if ($rs) {
1279 foreach ($rs as $c) {
1280 // build the context obj
1281 $c = make_context_subobj($c);
1282
1283 if (has_capability_in_accessdata($cap, $c->context, $accessdata, $doanything)) {
4a09658e 1284 if ($limit > 0 && $cc >= $limit) {
f33e1ed4 1285 break;
1286 }
117bd748 1287
4a09658e 1288 $courses[] = $c;
1289 $cc++;
e1d5e5c1 1290 }
1291 }
f33e1ed4 1292 $rs->close();
e1d5e5c1 1293 }
5a4e7398 1294
e1d5e5c1 1295 return $courses;
1296}
1297
b5a645b4 1298
a9bee37e 1299/**
46808d7c 1300 * Return a nested array showing role assignments
a9bee37e 1301 * all relevant role capabilities for the user at
1302 * site/metacourse/course_category/course levels
1303 *
1304 * We do _not_ delve deeper than courses because the number of
1305 * overrides at the module/block levels is HUGE.
1306 *
f5930992 1307 * [ra] => [/path/][]=roleid
a9bee37e 1308 * [rdef] => [/path/:roleid][capability]=permission
74ac5b66 1309 * [loaded] => array('/path', '/path')
a9bee37e 1310 *
cc3edaa4 1311 * @global object
1312 * @global object
a9bee37e 1313 * @param $userid integer - the id of the user
a9bee37e 1314 */
74ac5b66 1315function get_user_access_sitewide($userid) {
a9bee37e 1316
f33e1ed4 1317 global $CFG, $DB;
a9bee37e 1318
a9bee37e 1319 /* Get in 3 cheap DB queries...
f5930992 1320 * - role assignments
a9bee37e 1321 * - relevant role caps
f5930992 1322 * - above and within this user's RAs
a9bee37e 1323 * - below this user's RAs - limited to course level
1324 */
1325
bb2c22bd 1326 $accessdata = array(); // named list
1327 $accessdata['ra'] = array();
1328 $accessdata['rdef'] = array();
1329 $accessdata['loaded'] = array();
a9bee37e 1330
a9bee37e 1331 //
f5930992 1332 // Role assignments
a9bee37e 1333 //
f5930992 1334 $sql = "SELECT ctx.path, ra.roleid
f33e1ed4 1335 FROM {role_assignments} ra
f5930992 1336 JOIN {context} ctx ON ctx.id=ra.contextid
1337 WHERE ra.userid = ? AND ctx.contextlevel <= ".CONTEXT_COURSE;
f33e1ed4 1338 $params = array($userid);
1339 $rs = $DB->get_recordset_sql($sql, $params);
f5930992 1340
018d4b52 1341 //
1342 // raparents collects paths & roles we need to walk up
1343 // the parenthood to build the rdef
1344 //
a9bee37e 1345 $raparents = array();
128f0984 1346 if ($rs) {
f33e1ed4 1347 foreach ($rs as $ra) {
03cedd62 1348 // RAs leafs are arrays to support multi
1349 // role assignments...
1350 if (!isset($accessdata['ra'][$ra->path])) {
1351 $accessdata['ra'][$ra->path] = array();
1352 }
f5930992 1353 array_push($accessdata['ra'][$ra->path], $ra->roleid);
1354
1355 // Concatenate as string the whole path (all related context)
1356 // for this role. This is damn faster than using array_merge()
1357 // Will unique them later
1358 if (isset($raparents[$ra->roleid])) {
1359 $raparents[$ra->roleid] .= $ra->path;
1360 } else {
1361 $raparents[$ra->roleid] = $ra->path;
03cedd62 1362 }
a9bee37e 1363 }
03cedd62 1364 unset($ra);
f33e1ed4 1365 $rs->close();
a9bee37e 1366 }
a9bee37e 1367
1368 // Walk up the tree to grab all the roledefs
1369 // of interest to our user...
f5930992 1370 //
a9bee37e 1371 // NOTE: we use a series of IN clauses here - which
1372 // might explode on huge sites with very convoluted nesting of
1373 // categories... - extremely unlikely that the number of categories
1374 // and roletypes is so large that we hit the limits of IN()
f5930992 1375 $clauses = '';
f33e1ed4 1376 $cparams = array();
f5930992 1377 foreach ($raparents as $roleid=>$strcontexts) {
1378 $contexts = implode(',', array_unique(explode('/', trim($strcontexts, '/'))));
a9bee37e 1379 if ($contexts ==! '') {
f5930992 1380 if ($clauses) {
1381 $clauses .= ' OR ';
1382 }
1383 $clauses .= "(roleid=? AND contextid IN ($contexts))";
f33e1ed4 1384 $cparams[] = $roleid;
a9bee37e 1385 }
1386 }
f5930992 1387
d4c4ecb8 1388 if ($clauses !== '') {
1389 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
f33e1ed4 1390 FROM {role_capabilities} rc
1391 JOIN {context} ctx
d4c4ecb8 1392 ON rc.contextid=ctx.id
f5930992 1393 WHERE $clauses";
1394
d4c4ecb8 1395 unset($clauses);
f5930992 1396 $rs = $DB->get_recordset_sql($sql, $cparams);
a9bee37e 1397
0dbb2191 1398 if ($rs) {
f33e1ed4 1399 foreach ($rs as $rd) {
0dbb2191 1400 $k = "{$rd->path}:{$rd->roleid}";
1401 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1402 }
1403 unset($rd);
f33e1ed4 1404 $rs->close();
a9bee37e 1405 }
1406 }
a9bee37e 1407
1408 //
1409 // Overrides for the role assignments IN SUBCONTEXTS
1410 // (though we still do _not_ go below the course level.
1411 //
1412 // NOTE that the JOIN w sctx is with 3-way triangulation to
1413 // catch overrides to the applicable role in any subcontext, based
1414 // on the path field of the parent.
1415 //
1416 $sql = "SELECT sctx.path, ra.roleid,
1417 ctx.path AS parentpath,
1418 rco.capability, rco.permission
f33e1ed4 1419 FROM {role_assignments} ra
1420 JOIN {context} ctx
1421 ON ra.contextid=ctx.id
1422 JOIN {context} sctx
5a4e7398 1423 ON (sctx.path LIKE " . $DB->sql_concat('ctx.path',"'/%'"). " )
f33e1ed4 1424 JOIN {role_capabilities} rco
1425 ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
1426 WHERE ra.userid = ?
004865e2 1427 AND ctx.contextlevel <= ".CONTEXT_COURSECAT."
1428 AND sctx.contextlevel <= ".CONTEXT_COURSE."
f33e1ed4 1429 ORDER BY sctx.depth, sctx.path, ra.roleid";
1430 $params = array($userid);
1431 $rs = $DB->get_recordset_sql($sql, $params);
0dbb2191 1432 if ($rs) {
f33e1ed4 1433 foreach ($rs as $rd) {
0dbb2191 1434 $k = "{$rd->path}:{$rd->roleid}";
1435 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1436 }
1437 unset($rd);
f33e1ed4 1438 $rs->close();
a9bee37e 1439 }
bb2c22bd 1440 return $accessdata;
a9bee37e 1441}
1442
74ac5b66 1443/**
46808d7c 1444 * Add to the access ctrl array the data needed by a user for a given context
74ac5b66 1445 *
cc3edaa4 1446 * @global object
1447 * @global object
46808d7c 1448 * @param integer $userid the id of the user
1449 * @param object $context needs path!
1450 * @param array $accessdata accessdata array
74ac5b66 1451 */
a2cf7f1b 1452function load_subcontext($userid, $context, &$accessdata) {
74ac5b66 1453
f33e1ed4 1454 global $CFG, $DB;
018d4b52 1455
1456 /* Get the additional RAs and relevant rolecaps
74ac5b66 1457 * - role assignments - with role_caps
1458 * - relevant role caps
1459 * - above this user's RAs
1460 * - below this user's RAs - limited to course level
1461 */
1462
74ac5b66 1463 $base = "/" . SYSCONTEXTID;
1464
53fb75dc 1465 //
1466 // Replace $context with the target context we will
1467 // load. Normally, this will be a course context, but
1468 // may be a different top-level context.
1469 //
1470 // We have 3 cases
74ac5b66 1471 //
1472 // - Course
1473 // - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM
1474 // - BLOCK/MODULE/GROUP hanging from a course
1475 //
1476 // For course contexts, we _already_ have the RAs
1477 // but the cost of re-fetching is minimal so we don't care.
74ac5b66 1478 //
5a4e7398 1479 if ($context->contextlevel !== CONTEXT_COURSE
53fb75dc 1480 && $context->path !== "$base/{$context->id}") {
1481 // Case BLOCK/MODULE/GROUP hanging from a course
74ac5b66 1482 // Assumption: the course _must_ be our parent
1483 // If we ever see stuff nested further this needs to
1484 // change to do 1 query over the exploded path to
1485 // find out which one is the course
c2f10673 1486 $courses = explode('/',get_course_from_path($context->path));
1487 $targetid = array_pop($courses);
53fb75dc 1488 $context = get_context_instance_by_id($targetid);
5a4e7398 1489
74ac5b66 1490 }
1491
1492 //
53fb75dc 1493 // Role assignments in the context and below
74ac5b66 1494 //
53fb75dc 1495 $sql = "SELECT ctx.path, ra.roleid
f33e1ed4 1496 FROM {role_assignments} ra
1497 JOIN {context} ctx
1498 ON ra.contextid=ctx.id
1499 WHERE ra.userid = ?
1500 AND (ctx.path = ? OR ctx.path LIKE ?)
082e777a 1501 ORDER BY ctx.depth, ctx.path, ra.roleid";
f33e1ed4 1502 $params = array($userid, $context->path, $context->path."/%");
1503 $rs = $DB->get_recordset_sql($sql, $params);
74ac5b66 1504
5a4e7398 1505 //
082e777a 1506 // Read in the RAs, preventing duplicates
018d4b52 1507 //
f33e1ed4 1508 if ($rs) {
1509 $localroles = array();
082e777a 1510 $lastseen = '';
f33e1ed4 1511 foreach ($rs as $ra) {
1512 if (!isset($accessdata['ra'][$ra->path])) {
1513 $accessdata['ra'][$ra->path] = array();
1514 }
082e777a 1515 // only add if is not a repeat caused
1516 // by capability join...
1517 // (this check is cheaper than in_array())
1518 if ($lastseen !== $ra->path.':'.$ra->roleid) {
1519 $lastseen = $ra->path.':'.$ra->roleid;
1520 array_push($accessdata['ra'][$ra->path], $ra->roleid);
1521 array_push($localroles, $ra->roleid);
1522 }
74ac5b66 1523 }
f33e1ed4 1524 $rs->close();
74ac5b66 1525 }
74ac5b66 1526
018d4b52 1527 //
53fb75dc 1528 // Walk up and down the tree to grab all the roledefs
74ac5b66 1529 // of interest to our user...
018d4b52 1530 //
53fb75dc 1531 // NOTES
1532 // - we use IN() but the number of roles is very limited.
1533 //
bb2c22bd 1534 $courseroles = aggregate_roles_from_accessdata($context, $accessdata);
53fb75dc 1535
1536 // Do we have any interesting "local" roles?
1537 $localroles = array_diff($localroles,$courseroles); // only "new" local roles
1538 $wherelocalroles='';
1539 if (count($localroles)) {
1540 // Role defs for local roles in 'higher' contexts...
1541 $contexts = substr($context->path, 1); // kill leading slash
1542 $contexts = str_replace('/', ',', $contexts);
1543 $localroleids = implode(',',$localroles);
5a4e7398 1544 $wherelocalroles="OR (rc.roleid IN ({$localroleids})
53fb75dc 1545 AND ctx.id IN ($contexts))" ;
74ac5b66 1546 }
1547
53fb75dc 1548 // We will want overrides for all of them
7e17f43b 1549 $whereroles = '';
1550 if ($roleids = implode(',',array_merge($courseroles,$localroles))) {
1551 $whereroles = "rc.roleid IN ($roleids) AND";
1552 }
53fb75dc 1553 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
f33e1ed4 1554 FROM {role_capabilities} rc
1555 JOIN {context} ctx
1556 ON rc.contextid=ctx.id
1557 WHERE ($whereroles
1558 (ctx.id=? OR ctx.path LIKE ?))
1559 $wherelocalroles
1560 ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1561 $params = array($context->id, $context->path."/%");
53fb75dc 1562
a2cf7f1b 1563 $newrdefs = array();
f33e1ed4 1564 if ($rs = $DB->get_recordset_sql($sql, $params)) {
1565 foreach ($rs as $rd) {
03cedd62 1566 $k = "{$rd->path}:{$rd->roleid}";
a2cf7f1b 1567 if (!array_key_exists($k, $newrdefs)) {
1568 $newrdefs[$k] = array();
1569 }
1570 $newrdefs[$k][$rd->capability] = $rd->permission;
74ac5b66 1571 }
f33e1ed4 1572 $rs->close();
7e17f43b 1573 } else {
1574 debugging('Bad SQL encountered!');
74ac5b66 1575 }
74ac5b66 1576
a2cf7f1b 1577 compact_rdefs($newrdefs);
1578 foreach ($newrdefs as $key=>$value) {
1579 $accessdata['rdef'][$key] =& $newrdefs[$key];
1580 }
74ac5b66 1581
6100dad0 1582 // error_log("loaded {$context->path}");
bb2c22bd 1583 $accessdata['loaded'][] = $context->path;
74ac5b66 1584}
2f1a4248 1585
eef879ec 1586/**
46808d7c 1587 * Add to the access ctrl array the data needed by a role for a given context.
6f1bce30 1588 *
1589 * The data is added in the rdef key.
1590 *
1591 * This role-centric function is useful for role_switching
1592 * and to get an overview of what a role gets under a
1593 * given context and below...
1594 *
cc3edaa4 1595 * @global object
1596 * @global object
46808d7c 1597 * @param integer $roleid the id of the user
1598 * @param object $context needs path!
1599 * @param array $accessdata accessdata array null by default
1600 * @return array
6f1bce30 1601 */
bb2c22bd 1602function get_role_access_bycontext($roleid, $context, $accessdata=NULL) {
6f1bce30 1603
f33e1ed4 1604 global $CFG, $DB;
6f1bce30 1605
1606 /* Get the relevant rolecaps into rdef
1607 * - relevant role caps
1608 * - at ctx and above
1609 * - below this ctx
1610 */
1611
bb2c22bd 1612 if (is_null($accessdata)) {
1613 $accessdata = array(); // named list
1614 $accessdata['ra'] = array();
1615 $accessdata['rdef'] = array();
1616 $accessdata['loaded'] = array();
6f1bce30 1617 }
5a4e7398 1618
6f1bce30 1619 $contexts = substr($context->path, 1); // kill leading slash
1620 $contexts = str_replace('/', ',', $contexts);
1621
1622 //
1623 // Walk up and down the tree to grab all the roledefs
1624 // of interest to our role...
1625 //
1626 // NOTE: we use an IN clauses here - which
1627 // might explode on huge sites with very convoluted nesting of
1628 // categories... - extremely unlikely that the number of nested
1629 // categories is so large that we hit the limits of IN()
1630 //
1631 $sql = "SELECT ctx.path, rc.capability, rc.permission
f33e1ed4 1632 FROM {role_capabilities} rc
1633 JOIN {context} ctx
1634 ON rc.contextid=ctx.id
1635 WHERE rc.roleid=? AND
5a4e7398 1636 ( ctx.id IN ($contexts) OR
f33e1ed4 1637 ctx.path LIKE ? )
1638 ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1639 $params = array($roleid, $context->path."/%");
1640
1641 if ($rs = $DB->get_recordset_sql($sql, $params)) {
1642 foreach ($rs as $rd) {
1643 $k = "{$rd->path}:{$roleid}";
1644 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1645 }
1646 $rs->close();
6f1bce30 1647 }
6f1bce30 1648
bb2c22bd 1649 return $accessdata;
6f1bce30 1650}
1651
a2cf7f1b 1652/**
46808d7c 1653 * Load accessdata for a user into the $ACCESSLIB_PRIVATE->accessdatabyuser global
204a369c 1654 *
1655 * Used by has_capability() - but feel free
5a4e7398 1656 * to call it if you are about to run a BIG
204a369c 1657 * cron run across a bazillion users.
1658 *
cc3edaa4 1659 * @global object
1660 * @global object
46808d7c 1661 * @param int $userid
1662 * @return array returns ACCESSLIB_PRIVATE->accessdatabyuser[userid]
5a4e7398 1663 */
204a369c 1664function load_user_accessdata($userid) {
d867e696 1665 global $CFG, $ACCESSLIB_PRIVATE;
6f1bce30 1666
7293b3c6 1667 $base = '/'.SYSCONTEXTID;
204a369c 1668
bb2c22bd 1669 $accessdata = get_user_access_sitewide($userid);
5a4e7398 1670 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
3ac81bd1 1671 //
7293b3c6 1672 // provide "default role" & set 'dr'
3ac81bd1 1673 //
7d0c81b3 1674 if (!empty($CFG->defaultuserroleid)) {
1675 $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1676 if (!isset($accessdata['ra'][$base])) {
1677 $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1678 } else {
1679 array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1680 }
1681 $accessdata['dr'] = $CFG->defaultuserroleid;
204a369c 1682 }
1683
4e1fe7d1 1684 //
1685 // provide "default frontpage role"
1686 //
3d811bc1 1687 if (!empty($CFG->defaultfrontpageroleid)) {
4e1fe7d1 1688 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
3d811bc1 1689 $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
4e1fe7d1 1690 if (!isset($accessdata['ra'][$base])) {
3d811bc1 1691 $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
4e1fe7d1 1692 } else {
3d811bc1 1693 array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
4e1fe7d1 1694 }
1695 }
128f0984 1696 // for dirty timestamps in cron
1697 $accessdata['time'] = time();
1698
d867e696 1699 $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
1700 compact_rdefs($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['rdef']);
a2cf7f1b 1701
d867e696 1702 return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
204a369c 1703}
ef989bd9 1704
a2cf7f1b 1705/**
46808d7c 1706 * Use shared copy of role definistions stored in ACCESSLIB_PRIVATE->roledefinitions;
cc3edaa4 1707 *
1708 * @global object
a2cf7f1b 1709 * @param array $rdefs array of role definitions in contexts
1710 */
1711function compact_rdefs(&$rdefs) {
d867e696 1712 global $ACCESSLIB_PRIVATE;
a2cf7f1b 1713
1714 /*
1715 * This is a basic sharing only, we could also
1716 * use md5 sums of values. The main purpose is to
d867e696 1717 * reduce mem in cron jobs - many users in $ACCESSLIB_PRIVATE->accessdatabyuser array.
a2cf7f1b 1718 */
1719
1720 foreach ($rdefs as $key => $value) {
d867e696 1721 if (!array_key_exists($key, $ACCESSLIB_PRIVATE->roledefinitions)) {
1722 $ACCESSLIB_PRIVATE->roledefinitions[$key] = $rdefs[$key];
a2cf7f1b 1723 }
d867e696 1724 $rdefs[$key] =& $ACCESSLIB_PRIVATE->roledefinitions[$key];
a2cf7f1b 1725 }
1726}
1727
6f1bce30 1728/**
46808d7c 1729 * A convenience function to completely load all the capabilities
1730 * for the current user. This is what gets called from complete_user_login()
1731 * for example. Call it only _after_ you've setup $USER and called
1732 * check_enrolment_plugins();
1733 * @see check_enrolment_plugins()
117bd748 1734 *
cc3edaa4 1735 * @global object
1736 * @global object
1737 * @global object
2f1a4248 1738 */
1739function load_all_capabilities() {
d867e696 1740 global $USER, $CFG, $ACCESSLIB_PRIVATE;
bbbf2d40 1741
18818abf 1742 // roles not installed yet - we are in the middle of installation
31a99877 1743 if (during_initial_install()) {
1045a007 1744 return;
1745 }
1746
e0376a62 1747 $base = '/'.SYSCONTEXTID;
1748
eef879ec 1749 if (isguestuser()) {
e0376a62 1750 $guest = get_guest_role();
1751
1752 // Load the rdefs
1753 $USER->access = get_role_access($guest->id);
1754 // Put the ghost enrolment in place...
33b6014f 1755 $USER->access['ra'][$base] = array($guest->id);
eef879ec 1756
7293b3c6 1757
eef879ec 1758 } else if (isloggedin()) {
eef879ec 1759
bb2c22bd 1760 $accessdata = get_user_access_sitewide($USER->id);
3887fe4a 1761
7293b3c6 1762 //
1763 // provide "default role" & set 'dr'
1764 //
7d0c81b3 1765 if (!empty($CFG->defaultuserroleid)) {
1766 $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1767 if (!isset($accessdata['ra'][$base])) {
1768 $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1769 } else {
1770 array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1771 }
1772 $accessdata['dr'] = $CFG->defaultuserroleid;
c0aa9f09 1773 }
7293b3c6 1774
4e1fe7d1 1775 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
de5e137a 1776
4e1fe7d1 1777 //
1778 // provide "default frontpage role"
1779 //
3d811bc1 1780 if (!empty($CFG->defaultfrontpageroleid)) {
4e1fe7d1 1781 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
1782 $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
1783 if (!isset($accessdata['ra'][$base])) {
1784 $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
1785 } else {
1786 array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
1787 }
5a4e7398 1788 }
4e1fe7d1 1789 $USER->access = $accessdata;
5a4e7398 1790
7d0c81b3 1791 } else if (!empty($CFG->notloggedinroleid)) {
1792 $USER->access = get_role_access($CFG->notloggedinroleid);
1793 $USER->access['ra'][$base] = array($CFG->notloggedinroleid);
2f1a4248 1794 }
55e68c29 1795
128f0984 1796 // Timestamp to read dirty context timestamps later
148eb2a7 1797 $USER->access['time'] = time();
d867e696 1798 $ACCESSLIB_PRIVATE->dirtycontexts = array();
55e68c29 1799
1800 // Clear to force a refresh
1801 unset($USER->mycourses);
bbbf2d40 1802}
1803
ef989bd9 1804/**
5a4e7398 1805 * A convenience function to completely reload all the capabilities
ef989bd9 1806 * for the current user when roles have been updated in a relevant
5a4e7398 1807 * context -- but PRESERVING switchroles and loginas.
ef989bd9 1808 *
1809 * That is - completely transparent to the user.
5a4e7398 1810 *
ef989bd9 1811 * Note: rewrites $USER->access completely.
1812 *
cc3edaa4 1813 * @global object
1814 * @global object
ef989bd9 1815 */
1816function reload_all_capabilities() {
f33e1ed4 1817 global $USER, $DB;
ef989bd9 1818
b738808b 1819 // error_log("reloading");
ef989bd9 1820 // copy switchroles
1821 $sw = array();
1822 if (isset($USER->access['rsw'])) {
1823 $sw = $USER->access['rsw'];
b738808b 1824 // error_log(print_r($sw,1));
ef989bd9 1825 }
1826
1827 unset($USER->access);
54f9d9ae 1828 unset($USER->mycourses);
5a4e7398 1829
ef989bd9 1830 load_all_capabilities();
1831
1832 foreach ($sw as $path => $roleid) {
f33e1ed4 1833 $context = $DB->get_record('context', array('path'=>$path));
ef989bd9 1834 role_switch($roleid, $context);
1835 }
1836
1837}
2f1a4248 1838
f33e1ed4 1839/**
343effbe 1840 * Adds a temp role to an accessdata array.
1841 *
1842 * Useful for the "temporary guest" access
1843 * we grant to logged-in users.
1844 *
1845 * Note - assumes a course context!
1846 *
cc3edaa4 1847 * @global object
1848 * @global object
46808d7c 1849 * @param object $content
1850 * @param int $roleid
1851 * @param array $accessdata
1852 * @return array Returns access data
343effbe 1853 */
bb2c22bd 1854function load_temp_role($context, $roleid, $accessdata) {
343effbe 1855
f33e1ed4 1856 global $CFG, $DB;
343effbe 1857
1858 //
1859 // Load rdefs for the role in -
1860 // - this context
1861 // - all the parents
1862 // - and below - IOWs overrides...
1863 //
5a4e7398 1864
343effbe 1865 // turn the path into a list of context ids
1866 $contexts = substr($context->path, 1); // kill leading slash
1867 $contexts = str_replace('/', ',', $contexts);
1868
f33e1ed4 1869 $sql = "SELECT ctx.path, rc.capability, rc.permission
1870 FROM {context} ctx
1871 JOIN {role_capabilities} rc
1872 ON rc.contextid=ctx.id
1873 WHERE (ctx.id IN ($contexts)
1874 OR ctx.path LIKE ?)
1875 AND rc.roleid = ?
1876 ORDER BY ctx.depth, ctx.path";
5a4e7398 1877 $params = array($context->path."/%", $roleid);
1878 if ($rs = $DB->get_recordset_sql($sql, $params)) {
f33e1ed4 1879 foreach ($rs as $rd) {
1880 $k = "{$rd->path}:{$roleid}";
1881 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1882 }
3bea11c8 1883 $rs->close();
f33e1ed4 1884 }
343effbe 1885
1886 //
1887 // Say we loaded everything for the course context
1888 // - which we just did - if the user gets a proper
1889 // RA in this session, this data will need to be reloaded,
1890 // but that is handled by the complete accessdata reload
1891 //
bb2c22bd 1892 array_push($accessdata['loaded'], $context->path);
343effbe 1893
1894 //
1895 // Add the ghost RA
1896 //
bb2c22bd 1897 if (isset($accessdata['ra'][$context->path])) {
1898 array_push($accessdata['ra'][$context->path], $roleid);
343effbe 1899 } else {
bb2c22bd 1900 $accessdata['ra'][$context->path] = array($roleid);
343effbe 1901 }
1902
bb2c22bd 1903 return $accessdata;
343effbe 1904}
1905
1906
efe12f6c 1907/**
64026e8c 1908 * Check all the login enrolment information for the given user object
eef868d1 1909 * by querying the enrolment plugins
cc3edaa4 1910 *
1911 * @global object
46808d7c 1912 * @param object $user
b7b64ff2 1913 * @return void
64026e8c 1914 */
1915function check_enrolment_plugins(&$user) {
1916 global $CFG;
1917
b7b64ff2 1918 if (empty($user->id) or isguestuser($user)) {
1919 // shortcut - there is no enrolment work for guests and not-logged-in users
1920 return;
1921 }
1922
ac173d3e 1923 static $inprogress = array(); // To prevent this function being called more than once in an invocation
e4ec4e41 1924
218eb651 1925 if (!empty($inprogress[$user->id])) {
e4ec4e41 1926 return;
1927 }
1928
218eb651 1929 $inprogress[$user->id] = true; // Set the flag
e4ec4e41 1930
64026e8c 1931 require_once($CFG->dirroot .'/enrol/enrol.class.php');
eef868d1 1932
64026e8c 1933 if (!($plugins = explode(',', $CFG->enrol_plugins_enabled))) {
1934 $plugins = array($CFG->enrol);
1935 }
1936
1937 foreach ($plugins as $plugin) {
1938 $enrol = enrolment_factory::factory($plugin);
1939 if (method_exists($enrol, 'setup_enrolments')) { /// Plugin supports Roles (Moodle 1.7 and later)
1940 $enrol->setup_enrolments($user);
1941 } else { /// Run legacy enrolment methods
1942 if (method_exists($enrol, 'get_student_courses')) {
1943 $enrol->get_student_courses($user);
1944 }
1945 if (method_exists($enrol, 'get_teacher_courses')) {
1946 $enrol->get_teacher_courses($user);
1947 }
1948
1949 /// deal with $user->students and $user->teachers stuff
1950 unset($user->student);
1951 unset($user->teacher);
1952 }
1953 unset($enrol);
1954 }
e4ec4e41 1955
218eb651 1956 unset($inprogress[$user->id]); // Unset the flag
64026e8c 1957}
1958
3562486b 1959/**
1960 * Returns array of all legacy roles.
cc3edaa4 1961 *
46808d7c 1962 * @return array
3562486b 1963 */
1964function get_legacy_roles() {
1965 return array(
a83addc5 1966 'admin' => 'moodle/legacy:admin',
3562486b 1967 'coursecreator' => 'moodle/legacy:coursecreator',
a83addc5 1968 'editingteacher' => 'moodle/legacy:editingteacher',
1969 'teacher' => 'moodle/legacy:teacher',
1970 'student' => 'moodle/legacy:student',
efe12f6c 1971 'guest' => 'moodle/legacy:guest',
1972 'user' => 'moodle/legacy:user'
3562486b 1973 );
1974}
1975
46808d7c 1976/**
1977 * @param int roleid
1978 * @return string
1979 */
b357ed13 1980function get_legacy_type($roleid) {
1981 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
1982 $legacyroles = get_legacy_roles();
1983
1984 $result = '';
1985 foreach($legacyroles as $ltype=>$lcap) {
1986 $localoverride = get_local_override($roleid, $sitecontext->id, $lcap);
1987 if (!empty($localoverride->permission) and $localoverride->permission == CAP_ALLOW) {
1988 //choose first selected legacy capability - reset the rest
1989 if (empty($result)) {
1990 $result = $ltype;
1991 } else {
66a27728 1992 unassign_capability($lcap, $roleid);
c421ad4b 1993 }
b357ed13 1994 }
1995 }
1996
1997 return $result;
1998}
1999
bbbf2d40 2000/**
2001 * Assign the defaults found in this capabality definition to roles that have
2002 * the corresponding legacy capabilities assigned to them.
cc3edaa4 2003 *
46808d7c 2004 * @param string $capability
2005 * @param array $legacyperms an array in the format (example):
bbbf2d40 2006 * 'guest' => CAP_PREVENT,
2007 * 'student' => CAP_ALLOW,
2008 * 'teacher' => CAP_ALLOW,
2009 * 'editingteacher' => CAP_ALLOW,
2010 * 'coursecreator' => CAP_ALLOW,
2011 * 'admin' => CAP_ALLOW
46808d7c 2012 * @return boolean success or failure.
bbbf2d40 2013 */
2014function assign_legacy_capabilities($capability, $legacyperms) {
eef868d1 2015
3562486b 2016 $legacyroles = get_legacy_roles();
2017
bbbf2d40 2018 foreach ($legacyperms as $type => $perm) {
eef868d1 2019
21c9bace 2020 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
eef868d1 2021
3562486b 2022 if (!array_key_exists($type, $legacyroles)) {
e49ef64a 2023 print_error('invalidlegacy', '', '', $type);
3562486b 2024 }
eef868d1 2025
3562486b 2026 if ($roles = get_roles_with_capability($legacyroles[$type], CAP_ALLOW)) {
2e85fffe 2027 foreach ($roles as $role) {
2028 // Assign a site level capability.
2029 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
2030 return false;
2031 }
bbbf2d40 2032 }
2033 }
2034 }
2035 return true;
2036}
2037
2038
cee0901c 2039/**
46808d7c 2040 * Checks to see if a capability is one of the special capabilities
2041 *
faf75fe7 2042 * Checks to see if a capability is one of the special capabilities
2043 * (either a legacy capability, or moodle/site:doanything).
cc3edaa4 2044 *
faf75fe7 2045 * @param string $capabilityname the capability name, e.g. mod/forum:view.
2046 * @return boolean whether this is one of the special capabilities.
cee0901c 2047 */
faf75fe7 2048function is_legacy($capabilityname) {
2049 if ($capabilityname == 'moodle/site:doanything' || strpos($capabilityname, 'moodle/legacy') === 0) {
eef868d1 2050 return true;
d67de0ca 2051 } else {
2052 return false;
98882637 2053 }
bbbf2d40 2054}
2055
faf75fe7 2056/**
2057 * @param object $capability a capbility - a row from the capabilitites table.
2058 * @return boolean whether this capability is safe - that is, wether people with the
2059 * safeoverrides capability should be allowed to change it.
2060 */
2061function is_safe_capability($capability) {
4659454a 2062 return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
faf75fe7 2063}
cee0901c 2064
2065/**********************************
bbbf2d40 2066 * Context Manipulation functions *
2067 **********************************/
2068
bbbf2d40 2069/**
46808d7c 2070 * Create a new context record for use by all roles-related stuff
2071 *
9991d157 2072 * Create a new context record for use by all roles-related stuff
4881f2d3 2073 * assumes that the caller has done the homework.
2074 *
cc3edaa4 2075 * @global object
2076 * @global object
46808d7c 2077 * @param int $contextlevel
2078 * @param int $instanceid
e40413be 2079 * @return object newly created context
bbbf2d40 2080 */
aad2ba95 2081function create_context($contextlevel, $instanceid) {
e40413be 2082
5a4e7398 2083 global $CFG, $DB;
e40413be 2084
4881f2d3 2085 if ($contextlevel == CONTEXT_SYSTEM) {
2086 return create_system_context();
2087 }
c421ad4b 2088
4881f2d3 2089 $context = new object();
2090 $context->contextlevel = $contextlevel;
2091 $context->instanceid = $instanceid;
e40413be 2092
2093 // Define $context->path based on the parent
2094 // context. In other words... Who is your daddy?
ca92b391 2095 $basepath = '/' . SYSCONTEXTID;
2096 $basedepth = 1;
e40413be 2097
7d0c81b3 2098 $result = true;
f689028c 2099 $error_message = null;
7d0c81b3 2100
e40413be 2101 switch ($contextlevel) {
2102 case CONTEXT_COURSECAT:
5a4e7398 2103 $sql = "SELECT ctx.path, ctx.depth
2104 FROM {context} ctx
2105 JOIN {course_categories} cc
2106 ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
2107 WHERE cc.id=?";
2108 $params = array($instanceid);
2109 if ($p = $DB->get_record_sql($sql, $params)) {
ca92b391 2110 $basepath = $p->path;
2111 $basedepth = $p->depth;
5a4e7398 2112 } else if ($category = $DB->get_record('course_categories', array('id'=>$instanceid))) {
7d0c81b3 2113 if (empty($category->parent)) {
2114 // ok - this is a top category
2115 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) {
2116 $basepath = $parent->path;
2117 $basedepth = $parent->depth;
2118 } else {
2119 // wrong parent category - no big deal, this can be fixed later
2120 $basepath = null;
2121 $basedepth = 0;
2122 }
2123 } else {
2124 // incorrect category id
f689028c 2125 $error_message = "incorrect course category id ($instanceid)";
7d0c81b3 2126 $result = false;
e40413be 2127 }
2128 break;
2129
2130 case CONTEXT_COURSE:
ca92b391 2131 $sql = "SELECT ctx.path, ctx.depth
5a4e7398 2132 FROM {context} ctx
2133 JOIN {course} c
2134 ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
2135 WHERE c.id=? AND c.id !=" . SITEID;
2136 $params = array($instanceid);
2137 if ($p = $DB->get_record_sql($sql, $params)) {
ca92b391 2138 $basepath = $p->path;
2139 $basedepth = $p->depth;
5a4e7398 2140 } else if ($course = $DB->get_record('course', array('id'=>$instanceid))) {
7d0c81b3 2141 if ($course->id == SITEID) {
2142 //ok - no parent category
2143 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) {
2144 $basepath = $parent->path;
2145 $basedepth = $parent->depth;
2146 } else {
2147 // wrong parent category of course - no big deal, this can be fixed later
2148 $basepath = null;
2149 $basedepth = 0;
2150 }
2151 } else if ($instanceid == SITEID) {
2152 // no errors for missing site course during installation
2153 return false;
2154 } else {
2155 // incorrect course id
f689028c 2156 $error_message = "incorrect course id ($instanceid)";
7d0c81b3 2157 $result = false;
e40413be 2158 }
2159 break;
2160
2161 case CONTEXT_MODULE:
ca92b391 2162 $sql = "SELECT ctx.path, ctx.depth
5a4e7398 2163 FROM {context} ctx
2164 JOIN {course_modules} cm
2165 ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
2166 WHERE cm.id=?";
2167 $params = array($instanceid);
2168 if ($p = $DB->get_record_sql($sql, $params)) {
7d0c81b3 2169 $basepath = $p->path;
2170 $basedepth = $p->depth;
5a4e7398 2171 } else if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid))) {
7d0c81b3 2172 if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course)) {
2173 $basepath = $parent->path;
2174 $basedepth = $parent->depth;
2175 } else {
2176 // course does not exist - modules can not exist without a course
f689028c 2177 $error_message = "course does not exist ($cm->course) - modules can not exist without a course";
7d0c81b3 2178 $result = false;
2179 }
2180 } else {
2181 // cm does not exist
837e6a44 2182 $error_message = "cm with id $instanceid does not exist";
7d0c81b3 2183 $result = false;
2184 }
e40413be 2185 break;
2186
2187 case CONTEXT_BLOCK:
ca92b391 2188 $sql = "SELECT ctx.path, ctx.depth
f474a4e5 2189 FROM {context} ctx
13a0d3d3 2190 JOIN {block_instances} bi ON (bi.parentcontextid=ctx.id)
e92c286c 2191 WHERE bi.id = ?";
f474a4e5 2192 $params = array($instanceid, CONTEXT_COURSE);
5a4e7398 2193 if ($p = $DB->get_record_sql($sql, $params)) {
ca92b391 2194 $basepath = $p->path;
2195 $basedepth = $p->depth;
7d0c81b3 2196 } else {
2197 // block does not exist
f474a4e5 2198 $error_message = 'block or parent context does not exist';
7d0c81b3 2199 $result = false;
ca92b391 2200 }
e40413be 2201 break;
2202 case CONTEXT_USER:
2203 // default to basepath
2204 break;
e40413be 2205 }
2206
7d0c81b3 2207 // if grandparents unknown, maybe rebuild_context_path() will solve it later
2208 if ($basedepth != 0) {
2209 $context->depth = $basedepth+1;
2210 }
2211
5a4e7398 2212 if ($result and $id = $DB->insert_record('context', $context)) {
7d0c81b3 2213 // can't set the full path till we know the id!
2214 if ($basedepth != 0 and !empty($basepath)) {
5a4e7398 2215 $DB->set_field('context', 'path', $basepath.'/'. $id, array('id'=>$id));
7d0c81b3 2216 }
2217 return get_context_instance_by_id($id);
ca92b391 2218
3ca2dea5 2219 } else {
4881f2d3 2220 debugging('Error: could not insert new context level "'.
2221 s($contextlevel).'", instance "'.
f689028c 2222 s($instanceid).'". ' . $error_message);
2223
7d0c81b3 2224 return false;
bbbf2d40 2225 }
2226}
2227
efe12f6c 2228/**
0ecff22d 2229 * Returns system context or null if can not be created yet.
46808d7c 2230 *
2231 * @todo can not use get_record() because we do not know if query failed :-(
2232 * switch to get_record() later
2233 *
cc3edaa4 2234 * @global object
2235 * @global object
0ecff22d 2236 * @param bool $cache use caching
2237 * @return mixed system context or null
8ba412da 2238 */
7d0c81b3 2239function get_system_context($cache=true) {
d867e696 2240 global $DB, $ACCESSLIB_PRIVATE;
7d0c81b3 2241 if ($cache and defined('SYSCONTEXTID')) {
d867e696 2242 if (is_null($ACCESSLIB_PRIVATE->systemcontext)) {
2243 $ACCESSLIB_PRIVATE->systemcontext = new object();
2244 $ACCESSLIB_PRIVATE->systemcontext->id = SYSCONTEXTID;
2245 $ACCESSLIB_PRIVATE->systemcontext->contextlevel = CONTEXT_SYSTEM;
2246 $ACCESSLIB_PRIVATE->systemcontext->instanceid = 0;
2247 $ACCESSLIB_PRIVATE->systemcontext->path = '/'.SYSCONTEXTID;
2248 $ACCESSLIB_PRIVATE->systemcontext->depth = 1;
7d0c81b3 2249 }
d867e696 2250 return $ACCESSLIB_PRIVATE->systemcontext;
7d0c81b3 2251 }
c23b0ea1 2252 try {
df97c6ee 2253 $context = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM));
0ecff22d 2254 } catch (dml_exception $e) {
c23b0ea1 2255 //table does not exist yet, sorry
2256 return null;
2257 }
2258
2259 if (!$context) {
8ba412da 2260 $context = new object();
2261 $context->contextlevel = CONTEXT_SYSTEM;
7d0c81b3 2262 $context->instanceid = 0;
2263 $context->depth = 1;
2264 $context->path = NULL; //not known before insert
2265
0ecff22d 2266 try {
a8f3a651 2267 $context->id = $DB->insert_record('context', $context);
0ecff22d 2268 } catch (dml_exception $e) {
a8f3a651 2269 // can not create context yet, sorry
0ecff22d 2270 return null;
8ba412da 2271 }
2272 }
7d0c81b3 2273
2274 if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) {
2275 $context->instanceid = 0;
2276 $context->path = '/'.$context->id;
2277 $context->depth = 1;
5a4e7398 2278 $DB->update_record('context', $context);
7d0c81b3 2279 }
2280
2281 if (!defined('SYSCONTEXTID')) {
2282 define('SYSCONTEXTID', $context->id);
2283 }
2284
d867e696 2285 $ACCESSLIB_PRIVATE->systemcontext = $context;
2286 return $ACCESSLIB_PRIVATE->systemcontext;
8ba412da 2287}
b51ece5b 2288
9991d157 2289/**
b51ece5b 2290 * Remove a context record and any dependent entries,
2291 * removes context from static context cache too
cc3edaa4 2292 *
2293 * @global object
2294 * @global object
46808d7c 2295 * @param int $level
2296 * @param int $instanceid
17b0efae 2297 * @return bool properly deleted
9991d157 2298 */
2299function delete_context($contextlevel, $instanceid) {
8432f5e6 2300 global $DB, $ACCESSLIB_PRIVATE, $CFG;
b51ece5b 2301
2302 // do not use get_context_instance(), because the related object might not exist,
2303 // or the context does not exist yet and it would be created now
5a4e7398 2304 if ($context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
568df475 2305 $result = $DB->delete_records('role_assignments', array('contextid'=>$context->id)) &&
2306 $DB->delete_records('role_capabilities', array('contextid'=>$context->id)) &&
cae83708 2307 $DB->delete_records('context', array('id'=>$context->id)) &&
bf66a674 2308 $DB->delete_records('role_names', array('contextid'=>$context->id));
b51ece5b 2309
2310 // do not mark dirty contexts if parents unknown
2311 if (!is_null($context->path) and $context->depth > 0) {
2312 mark_context_dirty($context->path);
2313 }
2314
2315 // purge static context cache if entry present
d867e696 2316 unset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instanceid]);
2317 unset($ACCESSLIB_PRIVATE->contextsbyid[$context->id]);
b51ece5b 2318
6bf44482 2319 blocks_delete_all_for_context($context->id);
9434fef4 2320 filter_delete_all_for_context($context->id);
2321
b51ece5b 2322 return $result;
2323 } else {
2324
2325 return true;
9991d157 2326 }
9991d157 2327}
2328
9a81a606 2329/**
2330 * Precreates all contexts including all parents
cc3edaa4 2331 *
2332 * @global object
46808d7c 2333 * @param int $contextlevel empty means all
9a81a606 2334 * @param bool $buildpaths update paths and depths
2335 * @return void
2336 */
5a4e7398 2337function create_contexts($contextlevel=null, $buildpaths=true) {
2338 global $DB;
9a81a606 2339
2340 //make sure system context exists
2341 $syscontext = get_system_context(false);
2342
5c8e6cb1 2343 if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT
2344 or $contextlevel == CONTEXT_COURSE
2345 or $contextlevel == CONTEXT_MODULE
2346 or $contextlevel == CONTEXT_BLOCK) {
5a4e7398 2347 $sql = "INSERT INTO {context} (contextlevel, instanceid)
9a81a606 2348 SELECT ".CONTEXT_COURSECAT.", cc.id
5a4e7398 2349 FROM {course}_categories cc
9a81a606 2350 WHERE NOT EXISTS (SELECT 'x'
5a4e7398 2351 FROM {context} cx
9a81a606 2352 WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
5a4e7398 2353 $DB->execute($sql);
9a81a606 2354
2355 }
2356
5c8e6cb1 2357 if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE
2358 or $contextlevel == CONTEXT_MODULE
2359 or $contextlevel == CONTEXT_BLOCK) {
5a4e7398 2360 $sql = "INSERT INTO {context} (contextlevel, instanceid)
9a81a606 2361 SELECT ".CONTEXT_COURSE.", c.id
5a4e7398 2362 FROM {course} c
9a81a606 2363 WHERE NOT EXISTS (SELECT 'x'
5a4e7398 2364 FROM {context} cx
9a81a606 2365 WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
5a4e7398 2366 $DB->execute($sql);
9a81a606 2367
2368 }
2369
e92c286c 2370 if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE
2371 or $contextlevel == CONTEXT_BLOCK) {
5a4e7398 2372 $sql = "INSERT INTO {context} (contextlevel, instanceid)
9a81a606 2373 SELECT ".CONTEXT_MODULE.", cm.id
5a4e7398 2374 FROM {course}_modules cm
9a81a606 2375 WHERE NOT EXISTS (SELECT 'x'
5a4e7398 2376 FROM {context} cx
9a81a606 2377 WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
5a4e7398 2378 $DB->execute($sql);
9a81a606 2379 }
2380
e92c286c 2381 if (empty($contextlevel) or $contextlevel == CONTEXT_USER
2382 or $contextlevel == CONTEXT_BLOCK) {
5a4e7398 2383 $sql = "INSERT INTO {context} (contextlevel, instanceid)
9a81a606 2384 SELECT ".CONTEXT_USER.", u.id
5a4e7398 2385 FROM {user} u
9a81a606 2386 WHERE u.deleted=0
2387 AND NOT EXISTS (SELECT 'x'
5a4e7398 2388 FROM {context} cx
9a81a606 2389 WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
5a4e7398 2390 $DB->execute($sql);
9a81a606 2391
2392 }
2393
e92c286c 2394 if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) {
2395 $sql = "INSERT INTO {context} (contextlevel, instanceid)
2396 SELECT ".CONTEXT_BLOCK.", bi.id
2397 FROM {block_instances} bi
2398 WHERE NOT EXISTS (SELECT 'x'
2399 FROM {context} cx
2400 WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
2401 $DB->execute($sql);
2402 }
2403
9a81a606 2404 if ($buildpaths) {
5a4e7398 2405 build_context_path(false);
9a81a606 2406 }
2407}
2408
17b0efae 2409/**
2410 * Remove stale context records
2411 *
cc3edaa4 2412 * @global object
17b0efae 2413 * @return bool
2414 */
2415function cleanup_contexts() {
5a4e7398 2416 global $DB;
17b0efae 2417
70dd126e 2418 $sql = " SELECT c.contextlevel,
17b0efae 2419 c.instanceid AS instanceid
5a4e7398 2420 FROM {context} c
2421 LEFT OUTER JOIN {course}_categories t
2422 ON c.instanceid = t.id
2423 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
17b0efae 2424 UNION
70dd126e 2425 SELECT c.contextlevel,
2426 c.instanceid
5a4e7398 2427 FROM {context} c
2428 LEFT OUTER JOIN {course} t
2429 ON c.instanceid = t.id
2430 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
17b0efae 2431 UNION
70dd126e 2432 SELECT c.contextlevel,
2433 c.instanceid
5a4e7398 2434 FROM {context} c
2435 LEFT OUTER JOIN {course}_modules t
2436 ON c.instanceid = t.id
2437 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
17b0efae 2438 UNION
70dd126e 2439 SELECT c.contextlevel,
2440 c.instanceid
5a4e7398 2441 FROM {context} c
2442 LEFT OUTER JOIN {user} t
2443 ON c.instanceid = t.id
2444 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
17b0efae 2445 UNION
70dd126e 2446 SELECT c.contextlevel,
2447 c.instanceid
5a4e7398 2448 FROM {context} c
f474a4e5 2449 LEFT OUTER JOIN {block_instances} t
2450 ON c.instanceid = t.id
5a4e7398 2451 WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
17b0efae 2452 ";
d5a8d9aa
PS
2453
2454 // transactions used only for performance reasons here
2455 $transaction = $DB->start_delegated_transaction();
2456
5a4e7398 2457 if ($rs = $DB->get_recordset_sql($sql)) {
5a4e7398 2458 foreach ($rs as $ctx) {
d5a8d9aa 2459 delete_context($ctx->contextlevel, $ctx->instanceid);
17b0efae 2460 }
5a4e7398 2461 $rs->close();
17b0efae 2462 }
d5a8d9aa
PS
2463
2464 $transaction->allow_commit();
17b0efae 2465 return true;
2466}
2467
00653161 2468/**
e92c286c 2469 * Preloads all contexts relating to a course: course, modules. Block contexts
2470 * are no longer loaded here. The contexts for all the blocks on the current
2471 * page are now efficiently loaded by {@link block_manager::load_blocks()}.
00653161 2472 *
2473 * @param int $courseid Course ID
d993468d 2474 * @return void
00653161 2475 */
2476function preload_course_contexts($courseid) {
d867e696 2477 global $DB, $ACCESSLIB_PRIVATE;
00653161 2478
2479 // Users can call this multiple times without doing any harm
d867e696 2480 global $ACCESSLIB_PRIVATE;
2481 if (array_key_exists($courseid, $ACCESSLIB_PRIVATE->preloadedcourses)) {
00653161 2482 return;
2483 }
2484
d993468d 2485 $params = array($courseid, $courseid, $courseid);
2486 $sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2487 FROM {course_modules} cm
2488 JOIN {context} x ON x.instanceid=cm.id
2489 WHERE cm.course=? AND x.contextlevel=".CONTEXT_MODULE."
2490
d993468d 2491 UNION ALL
2492
2493 SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2494 FROM {context} x
2495 WHERE x.instanceid=? AND x.contextlevel=".CONTEXT_COURSE."";
2496
2497 $rs = $DB->get_recordset_sql($sql, $params);
00653161 2498 foreach($rs as $context) {
d867e696 2499 cache_context($context);
00653161 2500 }
2501 $rs->close();
d867e696 2502 $ACCESSLIB_PRIVATE->preloadedcourses[$courseid] = true;
00653161 2503}
2504
bbbf2d40 2505/**
2506 * Get the context instance as an object. This function will create the
2507 * context instance if it does not exist yet.
46808d7c 2508 *
2509 * @todo Remove code branch from previous fix MDL-9016 which is no longer needed
2510 *
e765b5d3 2511 * @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE.
2512 * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id,
46808d7c 2513 * for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0
e765b5d3 2514 * @return object The context object.
bbbf2d40 2515 */
7d0c81b3 2516function get_context_instance($contextlevel, $instance=0) {
e5605780 2517
d867e696 2518 global $DB, $ACCESSLIB_PRIVATE;
8ead7b59 2519 static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_BLOCK);
d9a35e12 2520
56743fab 2521 if ($contextlevel === 'clearcache') {
2522 // TODO: Remove for v2.0
5a4e7398 2523 // No longer needed, but we'll catch it to avoid erroring out on custom code.
2524 // This used to be a fix for MDL-9016
2525 // "Restoring into existing course, deleting first
56743fab 2526 // deletes context and doesn't recreate it"
9251b26f 2527 return false;
2528 }
b7cec865 2529
7d0c81b3 2530/// System context has special cache
8ba412da 2531 if ($contextlevel == CONTEXT_SYSTEM) {
7d0c81b3 2532 return get_system_context();
8ba412da 2533 }
2534
a36a3a3f 2535/// check allowed context levels
2536 if (!in_array($contextlevel, $allowed_contexts)) {
7bfa3101 2537 // fatal error, code must be fixed - probably typo or switched parameters
e49ef64a 2538 print_error('invalidcourselevel');
a36a3a3f 2539 }
2540
65bcf17b 2541 if (!is_array($instance)) {
2542 /// Check the cache
d867e696 2543 if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) { // Already cached
2544 return $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
65bcf17b 2545 }
2546
2547 /// Get it from the database, or create it
5a4e7398 2548 if (!$context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instance))) {
65bcf17b 2549 $context = create_context($contextlevel, $instance);
2550 }
2551
2552 /// Only add to cache if context isn't empty.
2553 if (!empty($context)) {
d867e696 2554 cache_context($context);
65bcf17b 2555 }
2556
2557 return $context;
e5605780 2558 }
2559
65bcf17b 2560
2561/// ok, somebody wants to load several contexts to save some db queries ;-)
2562 $instances = $instance;
2563 $result = array();
2564
2565 foreach ($instances as $key=>$instance) {
2566 /// Check the cache first
d867e696 2567 if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) { // Already cached
2568 $result[$instance] = $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
65bcf17b 2569 unset($instances[$key]);
2570 continue;
2571 }
e5605780 2572 }
2573
65bcf17b 2574 if ($instances) {
5a4e7398 2575 list($instanceids, $params) = $DB->get_in_or_equal($instances, SQL_PARAMS_QM);
2576 array_unshift($params, $contextlevel);
2577 $sql = "SELECT instanceid, id, contextlevel, path, depth
2578 FROM {context}
2579 WHERE contextlevel=? AND instanceid $instanceids";
2580
2581 if (!$contexts = $DB->get_records_sql($sql, $params)) {
65bcf17b 2582 $contexts = array();
2583 }
2584
2585 foreach ($instances as $instance) {
2586 if (isset($contexts[$instance])) {
2587 $context = $contexts[$instance];
2588 } else {
2589 $context = create_context($contextlevel, $instance);
2590 }
2591
2592 if (!empty($context)) {
d867e696 2593 cache_context($context);
65bcf17b 2594 }
2595
2596 $result[$instance] = $context;
2597 }
ccfc5ecc 2598 }
0468976c 2599
65bcf17b 2600 return $result;
bbbf2d40 2601}
2602
cee0901c 2603
340ea4e8 2604/**
e765b5d3 2605 * Get a context instance as an object, from a given context id.
cc3edaa4 2606 *
2607 * @global object
2608 * @global object
65bcf17b 2609 * @param mixed $id a context id or array of ids.
46808d7c 2610 * @return mixed object, array of the context object, or false.
340ea4e8 2611 */
2612function get_context_instance_by_id($id) {
d867e696 2613 global $DB, $ACCESSLIB_PRIVATE;
d9a35e12 2614
7d0c81b3 2615 if ($id == SYSCONTEXTID) {
2616 return get_system_context();
2617 }
2618
d867e696 2619 if (isset($ACCESSLIB_PRIVATE->contextsbyid[$id])) { // Already cached
2620 return $ACCESSLIB_PRIVATE->contextsbyid[$id];
340ea4e8 2621 }
2622
d867e696 2623 if ($context = $DB->get_record('context', array('id'=>$id))) {
2624 cache_context($context);
340ea4e8 2625 return $context;
2626 }
2627
2628 return false;
2629}
2630
bbbf2d40 2631
8737be58 2632/**
2633 * Get the local override (if any) for a given capability in a role in a context
cc3edaa4 2634 *
2635 * @global object
46808d7c 2636 * @param int $roleid
2637 * @param int $contextid
2638 * @param string $capability
8737be58 2639 */
2640function get_local_override($roleid, $contextid, $capability) {
5a4e7398 2641 global $DB;
2642 return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
8737be58 2643}
2644
2645
bbbf2d40 2646
46808d7c 2647//////////////////////////////////////
2648// DB TABLE RELATED FUNCTIONS //
2649//////////////////////////////////////
bbbf2d40 2650
cee0901c 2651/**
bbbf2d40 2652 * function that creates a role
cc3edaa4 2653 *
2654 * @global object
46808d7c 2655 * @param string $name role name
2656 * @param string $shortname role short name
2657 * @param string $description role description
2658 * @param string $legacy optional legacy capability
2659 * @return mixed id or dml_exception
bbbf2d40 2660 */
8420bee9 2661function create_role($name, $shortname, $description, $legacy='') {
f33e1ed4 2662 global $DB;
eef868d1 2663
bbdb7070 2664 // Get the system context.
19a4a32e 2665 $context = get_context_instance(CONTEXT_SYSTEM);
31f26796 2666
bbdb7070 2667 // Insert the role record.
b5959f30 2668 $role = new object();
ac173d3e 2669 $role->name = $name;
2670 $role->shortname = $shortname;
98882637 2671 $role->description = $description;
eef868d1 2672
8420bee9 2673 //find free sortorder number
bbdb7070 2674 $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
716dd163 2675 if (empty($role->sortorder)) {
2676 $role->sortorder = 1;
2677 }
bbdb7070 2678 $id = $DB->insert_record('role', $role);
b5959f30 2679
bbdb7070 2680 if ($legacy) {
2681 assign_capability($legacy, CAP_ALLOW, $id, $context->id);
2682 }
eef868d1 2683
bbdb7070 2684 return $id;
bbbf2d40 2685}
2686
8420bee9 2687/**
46808d7c 2688 * Function that deletes a role and cleanups up after it
cc3edaa4 2689 *
2690 * @global object
2691 * @global object
46808d7c 2692 * @param int $roleid id of role to delete
2693 * @return bool
8420bee9 2694 */
2695function delete_role($roleid) {
f33e1ed4 2696 global $CFG, $DB;
8420bee9 2697 $success = true;
2698
60ace1e1 2699// mdl 10149, check if this is the last active admin role
2700// if we make the admin role not deletable then this part can go
c421ad4b 2701
60ace1e1 2702 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
c421ad4b 2703
f33e1ed4 2704 if ($role = $DB->get_record('role', array('id'=>$roleid))) {
2705 if ($DB->record_exists('role_capabilities', array('contextid'=>$systemcontext->id, 'roleid'=>$roleid, 'capability'=>'moodle/site:doanything'))) {
60ace1e1 2706 // deleting an admin role
2707 $status = false;
2708 if ($adminroles = get_roles_with_capability('moodle/site:doanything', CAP_ALLOW, $systemcontext)) {
2709 foreach ($adminroles as $adminrole) {
2710 if ($adminrole->id != $roleid) {
2711 // some other admin role
f33e1ed4 2712 if ($DB->record_exists('role_assignments', array('roleid'=>$adminrole->id, 'contextid'=>$systemcontext->id))) {
c421ad4b 2713 // found another admin role with at least 1 user assigned
60ace1e1 2714 $status = true;
2715 break;
2716 }
2717 }
c421ad4b 2718 }
2719 }
60ace1e1 2720 if ($status !== true) {
771dc7b2 2721 print_error('cannotdeleterolenoadmin', 'access');
60ace1e1 2722 }
c421ad4b 2723 }
60ace1e1 2724 }
2725
8420bee9 2726// first unssign all users
2727 if (!role_unassign($roleid)) {
2728 debugging("Error while unassigning all users from role with ID $roleid!");
2729 $success = false;
2730 }
2731
2732// cleanup all references to this role, ignore errors
2733 if ($success) {
5a4e7398 2734 $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
2735 $DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
2736 $DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
f33e1ed4 2737 $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
2738 $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
5a4e7398 2739 $DB->delete_records('role_names', array('roleid'=>$roleid));
01a80f51 2740 $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
8420bee9 2741 }
2742
2743// finally delete the role itself
cb8cb8bf 2744 // get this before the name is gone for logging
f33e1ed4 2745 $rolename = $DB->get_field('role', 'name', array('id'=>$roleid));
5a4e7398 2746
f33e1ed4 2747 if ($success and !$DB->delete_records('role', array('id'=>$roleid))) {
ece4945b 2748 debugging("Could not delete role record with ID $roleid!");
8420bee9 2749 $success = false;
2750 }
5a4e7398 2751
cb8cb8bf 2752 if ($success) {
e60d5b26 2753 add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '');
cb8cb8bf 2754 }
8420bee9 2755
2756 return $success;
2757}
2758
bbbf2d40 2759/**
2760 * Function to write context specific overrides, or default capabilities.
46808d7c 2761 *
cc3edaa4 2762 * @global object
2763 * @global object
46808d7c 2764 * @param string module string name
2765 * @param string capability string name
2766 * @param int contextid context id
2767 * @param int roleid role id
2768 * @param int permission int 1,-1 or -1000 should not be writing if permission is 0
2769 * @return bool
bbbf2d40 2770 */
e7876c1e 2771function assign_capability($capability, $permission, $roleid, $contextid, $overwrite=false) {
eef868d1 2772
f33e1ed4 2773 global $USER, $DB;
eef868d1 2774
96986241 2775 if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
eef868d1 2776 unassign_capability($capability, $roleid, $contextid);
96986241 2777 return true;
98882637 2778 }
eef868d1 2779
f33e1ed4 2780 $existing = $DB->get_record('role_capabilities', array('contextid'=>$contextid, 'roleid'=>$roleid, 'capability'=>$capability));
e7876c1e 2781
2782 if ($existing and !$overwrite) { // We want to keep whatever is there already
2783 return true;
2784 }
2785
bbbf2d40 2786 $cap = new object;
2787 $cap->contextid = $contextid;
2788 $cap->roleid = $roleid;
2789 $cap->capability = $capability;
2790 $cap->permission = $permission;
2791 $cap->timemodified = time();
9db12da7 2792 $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
e7876c1e 2793
2794 if ($existing) {
2795 $cap->id = $existing->id;
f33e1ed4 2796 return $DB->update_record('role_capabilities', $cap);
e7876c1e 2797 } else {
f33e1ed4 2798 $c = $DB->get_record('context', array('id'=>$contextid));
2799 return $DB->insert_record('role_capabilities', $cap);
e7876c1e 2800 }
bbbf2d40 2801}
2802
bbbf2d40 2803/**
2804 * Unassign a capability from a role.
117bd748 2805 *
cc3edaa4 2806 * @global object
46808d7c 2807 * @param int $roleid the role id
2808 * @param string $capability the name of the capability
2809 * @return boolean success or failure
bbbf2d40 2810 */
2811function unassign_capability($capability, $roleid, $contextid=NULL) {
f33e1ed4 2812 global $DB;
eef868d1 2813
98882637 2814 if (isset($contextid)) {
c345bb58 2815 // delete from context rel, if this is the last override in this context
f33e1ed4 2816 $status = $DB->delete_records('role_capabilities', array('capability'=>$capability,
2817 'roleid'=>$roleid, 'contextid'=>$contextid));
98882637 2818 } else {
f33e1ed4 2819 $status = $DB->delete_records('role_capabilities', array('capability'=>$capability,
2820 'roleid'=>$roleid));
98882637 2821 }
2822 return $status;
bbbf2d40 2823}
2824
2825
2826/**
46808d7c 2827 * Get the roles that have a given capability assigned to it
759ac72d 2828 * Get the roles that have a given capability assigned to it. This function
2829 * does not resolve the actual permission of the capability. It just checks
2830 * for assignment only.
cc3edaa4 2831 *
2832 * @global object
2833 * @global object
46808d7c 2834 * @param string $capability - capability name (string)
2835 * @param null $permission - optional, the permission defined for this capability
2836 * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to NULL
2837 * @param object $contect
2838 * @return mixed array or role objects
bbbf2d40 2839 */
ec7a8b79 2840function get_roles_with_capability($capability, $permission=NULL, $context='') {
2841
f33e1ed4 2842 global $CFG, $DB;
eef868d1 2843
f33e1ed4 2844 $params = array();
5a4e7398 2845
ec7a8b79 2846 if ($context) {
2847 if ($contexts = get_parent_contexts($context)) {
2848 $listofcontexts = '('.implode(',', $contexts).')';
2849 } else {
21c9bace 2850 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
eef868d1 2851 $listofcontexts = '('.$sitecontext->id.')'; // must be site
2852 }
f33e1ed4 2853 $contextstr = "AND (rc.contextid = ? OR rc.contextid IN $listofcontexts)";
2854 $params[] = $context->id;
ec7a8b79 2855 } else {
2856 $contextstr = '';
2857 }
eef868d1 2858
2859 $selectroles = "SELECT r.*
5a4e7398 2860 FROM {role} r,
2861 {role_capabilities} rc
f33e1ed4 2862 WHERE rc.capability = ?
5a4e7398 2863 AND rc.roleid = r.id $contextstr";
bbbf2d40 2864
f33e1ed4 2865 array_unshift($params, $capability);
2866
bbbf2d40 2867 if (isset($permission)) {
f33e1ed4 2868 $selectroles .= " AND rc.permission = ?";
2869 $params[] = $permission;
bbbf2d40 2870 }
f33e1ed4 2871 return $DB->get_records_sql($selectroles, $params);
bbbf2d40 2872}
2873
2874
2875/**
a9e1c058 2876 * This function makes a role-assignment (a role for a user or group in a particular context)
117bd748 2877 *
cc3edaa4 2878 * @global object
2879 * @global object
2880 * @global object
46808d7c 2881 * @param int $roleid the role of the id
2882 * @param int $userid userid
2883 * @param int $groupid group id
2884 * @param int $contextid id of the context
2885 * @param int $timestart time this assignment becomes effective defaults to 0
2886 * @param int $timeend time this assignemnt ceases to be effective defaults to 0
2887 * @param int $hidden defaults to 0
2888 * @param string $enrol defaults to 'manual'
2889 * @param string $timemodified defaults to ''
2890 * @return int new id of the assigment
bbbf2d40 2891 */
69b0088c 2892function role_assign($roleid, $userid, $groupid, $contextid, $timestart=0, $timeend=0, $hidden=0, $enrol='manual',$timemodified='') {
f33e1ed4 2893 global $USER, $CFG, $DB;
bbbf2d40 2894
a9e1c058 2895/// Do some data validation
2896
bbbf2d40 2897 if (empty($roleid)) {
9d829c68 2898 debugging('Role ID not provided');
a9e1c058 2899 return false;
bbbf2d40 2900 }
2901
2902 if (empty($userid) && empty($groupid)) {
9d829c68 2903 debugging('Either userid or groupid must be provided');
a9e1c058 2904 return false;
bbbf2d40 2905 }
eef868d1 2906
f67cab32 2907 if ($userid && !$DB->record_exists('user', array('id'=>$userid))) {
82396e5b 2908 debugging('User ID '.intval($userid).' does not exist!');
7700027f 2909 return false;
2910 }
bbbf2d40 2911
f3f7610c 2912 if ($groupid && !groups_group_exists($groupid)) {
82396e5b 2913 debugging('Group ID '.intval($groupid).' does not exist!');
dc411d1b 2914 return false;
2915 }
2916
7700027f 2917 if (!$context = get_context_instance_by_id($contextid)) {
82396e5b 2918 debugging('Context ID '.intval($contextid).' does not exist!');
a9e1c058 2919 return false;
bbbf2d40 2920 }
2921
a9e1c058 2922 if (($timestart and $timeend) and ($timestart > $timeend)) {
9d829c68 2923 debugging('The end time can not be earlier than the start time');
a9e1c058 2924 return false;
2925 }
2926
69b0088c 2927 if (!$timemodified) {
c421ad4b 2928 $timemodified = time();
69b0088c 2929 }
7700027f 2930
a9e1c058 2931/// Check for existing entry
2932 if ($userid) {
f33e1ed4 2933 $ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid));
a9e1c058 2934 } else {
f33e1ed4 2935 $ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'groupid'=>$groupid));
a9e1c058 2936 }
2937
a9e1c058 2938 if (empty($ra)) { // Create a new entry
96608a55 2939 $ra = new object();
2940 $ra->roleid = $roleid;
2941 $ra->contextid = $context->id;
2942 $ra->userid = $userid;
2943 $ra->hidden = $hidden;
2944 $ra->enrol = $enrol;
c421ad4b 2945 /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
0616d3e8 2946 /// by repeating queries with the same exact parameters in a 100 secs time window
96608a55 2947 $ra->timestart = round($timestart, -2);
2948 $ra->timeend = $timeend;
2949 $ra->timemodified = $timemodified;
2950 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
a9e1c058 2951
a8f3a651 2952 $ra->id = $DB->insert_record('role_assignments', $ra);
a9e1c058 2953
2954 } else { // We already have one, just update it
96608a55 2955 $ra->id = $ra->id;
2956 $ra->hidden = $hidden;
2957 $ra->enrol = $enrol;
c421ad4b 2958 /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
0616d3e8 2959 /// by repeating queries with the same exact parameters in a 100 secs time window
96608a55 2960 $ra->timestart = round($timestart, -2);
2961 $ra->timeend = $timeend;
2962 $ra->timemodified = $timemodified;
2963 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
a9e1c058 2964
bb4b6010 2965 $DB->update_record('role_assignments', $ra);
9ebcb4d2 2966 }
2967
96608a55 2968/// mark context as dirty - modules might use has_capability() in xxx_role_assing()
2969/// again expensive, but needed
2970 mark_context_dirty($context->path);
128f0984 2971
96608a55 2972 if (!empty($USER->id) && $USER->id == $userid) {
2973/// If the user is the current user, then do full reload of capabilities too.
2974 load_all_capabilities();
2975 }
c421ad4b 2976
96608a55 2977/// Ask all the modules if anything needs to be done for this user
17da2e6f 2978 $mods = get_plugin_list('mod');
2979 foreach ($mods as $mod => $moddir) {
2980 include_once($moddir.'/lib.php');
2981 $functionname = $mod.'_role_assign';
2982 if (function_exists($functionname)) {
2983 $functionname($userid, $context, $roleid);
0f161e1f 2984 }
a9e1c058 2985 }
eef868d1 2986
4e5f3064 2987 /// now handle metacourse role assignments if in course context
96608a55 2988 if ($context->contextlevel == CONTEXT_COURSE) {
f33e1ed4 2989 if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
4e5f3064 2990 foreach ($parents as $parent) {
1aad4310 2991 sync_metacourse($parent->parent_course);
4e5f3064 2992 }
2993 }
2994 }
6eb4f823 2995
96608a55 2996 events_trigger('role_assigned', $ra);
2997
386c151e 2998 return $ra->id;
bbbf2d40 2999}
3000
3001
3002/**
1dc1f037 3003 * Deletes one or more role assignments. You must specify at least one parameter.
cc3edaa4 3004 *
3005 * @global object
3006 * @global object
3007 * @global object
46808d7c 3008 * @param int $roleid defaults to 0
3009 * @param int $userid defaults to 0
3010 * @param int $groupid defaults to 0
3011 * @param int $contextid defaults to 0
3012 * @param mixed $enrol unassign only if enrolment type matches, NULL means anything. Defaults to NULL
3013 * @return boolean success or failure
bbbf2d40 3014 */
6bc1e5d5 3015function role_unassign($roleid=0, $userid=0, $groupid=0, $contextid=0, $enrol=NULL) {
d74067e8 3016
5a4e7398 3017 global $USER, $CFG, $DB;
11dca3ee 3018 require_once($CFG->dirroot.'/group/lib.php');
eef868d1 3019
4e5f3064 3020 $success = true;
d74067e8 3021
1dc1f037 3022 $args = array('roleid', 'userid', 'groupid', 'contextid');
3023 $select = array();
5a4e7398 3024 $params = array();
3025
1dc1f037 3026 foreach ($args as $arg) {
3027 if ($$arg) {
5a4e7398 3028 $select[] = "$arg = ?";
3029 $params[] = $$arg;
1dc1f037 3030 }
3031 }
6bc1e5d5 3032 if (!empty($enrol)) {
5a4e7398 3033 $select[] = "enrol=?";
3034 $params[] = $enrol;
6bc1e5d5 3035 }
d74067e8 3036
1dc1f037 3037 if ($select) {
5a4e7398 3038 if ($ras = $DB->get_records_select('role_assignments', implode(' AND ', $select), $params)) {
17da2e6f 3039 $mods = get_plugin_list('mod');
4e5f3064 3040 foreach($ras as $ra) {
96608a55 3041 $fireevent = false;
86e2c51d 3042 /// infinite loop protection when deleting recursively
5a4e7398 3043 if (!$ra = $DB->get_record('role_assignments', array('id'=>$ra->id))) {
86e2c51d 3044 continue;
3045 }
96608a55 3046 if ($DB->delete_records('role_assignments', array('id'=>$ra->id))) {
3047 $fireevent = true;
3048 } else {
3049 $success = false;
3050 }
86e2c51d 3051
128f0984 3052 if (!$context = get_context_instance_by_id($ra->contextid)) {
3053 // strange error, not much to do
3054 continue;
3055 }
3056
3057 /* mark contexts as dirty here, because we need the refreshed
3058 * caps bellow to delete group membership and user_lastaccess!
3059 * and yes, this is very expensive for bulk operations :-(
3060 */
3061 mark_context_dirty($context->path);
3062
3063 /// If the user is the current user, then do full reload of capabilities too.
4e5f3064 3064 if (!empty($USER->id) && $USER->id == $ra->userid) {
2f1a4248 3065 load_all_capabilities();
4e5f3064 3066 }
0f161e1f 3067
3068 /// Ask all the modules if anything needs to be done for this user
17da2e6f 3069 foreach ($mods as $mod=>$moddir) {
3070 include_once($moddir.'/lib.php');
4e5f3064 3071 $functionname = $mod.'_role_unassign';
3072 if (function_exists($functionname)) {
3073 $functionname($ra->userid, $context); // watch out, $context might be NULL if something goes wrong
3074 }
3075 }
3076
3077 /// now handle metacourse role unassigment and removing from goups if in course context
128f0984 3078 if ($context->contextlevel == CONTEXT_COURSE) {
2515adf9 3079
c421ad4b 3080 // cleanup leftover course groups/subscriptions etc when user has
2515adf9 3081 // no capability to view course
35274646 3082 // this may be slow, but this is the proper way of doing it
3083 if (!has_capability('moodle/course:view', $context, $ra->userid)) {
2515adf9 3084 // remove from groups
ffc670d9 3085 groups_delete_group_members($context->instanceid, $ra->userid);
2515adf9 3086
2515adf9 3087 // delete lastaccess records
5a4e7398 3088 $DB->delete_records('user_lastaccess', array('userid'=>$ra->userid, 'courseid'=>$context->instanceid));
4e5f3064 3089 }
2515adf9 3090
1aad4310 3091 //unassign roles in metacourses if needed
5a4e7398 3092 if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
4e5f3064 3093 foreach ($parents as $parent) {
1aad4310 3094 sync_metacourse($parent->parent_course);
0f161e1f 3095 }
3096 }
0f161e1f 3097 }
96608a55 3098
3099 if ($fireevent) {
3100 events_trigger('role_unassigned', $ra);
3101 }
0f161e1f 3102 }
d74067e8 3103 }
1dc1f037 3104 }
4e5f3064 3105
3106 return $success;
bbbf2d40 3107}
3108
efe12f6c 3109/**
46808d7c 3110 * Enrol someone without using the default role in a course
3111 *
eef868d1 3112 * A convenience function to take care of the common case where you
b963384f 3113 * just want to enrol someone using the default role into a course
3114 *
3115 * @param object $course
3116 * @param object $user
46808d7c 3117 * @param string $enrol the plugin used to do this enrolment
3118 * @return bool
b963384f 3119 */
3120function enrol_into_course($course, $user, $enrol) {
3121
9492291c 3122 $timestart = time();
898d7119 3123 // remove time part from the timestamp and keep only the date part