Updated the 19 build version to 20090123
[moodle.git] / lib / accesslib.php
blob5c8812c9b831f0d4aec8da90e0c5119fe303f7c8
1 <?php // $Id$
3 ///////////////////////////////////////////////////////////////////////////
4 // //
5 // NOTICE OF COPYRIGHT //
6 // //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.org //
9 // //
10 // Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
11 // //
12 // This program is free software; you can redistribute it and/or modify //
13 // it under the terms of the GNU General Public License as published by //
14 // the Free Software Foundation; either version 2 of the License, or //
15 // (at your option) any later version. //
16 // //
17 // This program is distributed in the hope that it will be useful, //
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of //
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
20 // GNU General Public License for more details: //
21 // //
22 // http://www.gnu.org/copyleft/gpl.html //
23 // //
24 ///////////////////////////////////////////////////////////////////////////
26 /**
27 * Public API vs internals
28 * -----------------------
30 * General users probably only care about
32 * Context handling
33 * - get_context_instance()
34 * - get_context_instance_by_id()
35 * - get_parent_contexts()
36 * - get_child_contexts()
38 * Whether the user can do something...
39 * - has_capability()
40 * - has_any_capability()
41 * - has_all_capabilities()
42 * - require_capability()
43 * - require_login() (from moodlelib)
45 * What courses has this user access to?
46 * - get_user_courses_bycap()
48 * What users can do X in this context?
49 * - get_users_by_capability()
51 * Enrol/unenrol
52 * - enrol_into_course()
53 * - role_assign()/role_unassign()
56 * Advanced use
57 * - load_all_capabilities()
58 * - reload_all_capabilities()
59 * - $ACCESS global
60 * - has_capability_in_accessdata()
61 * - is_siteadmin()
62 * - get_user_access_sitewide()
63 * - load_subcontext()
64 * - get_role_access_bycontext()
66 * Name conventions
67 * ----------------
69 * - "ctx" means context
71 * accessdata
72 * ----------
74 * Access control data is held in the "accessdata" array
75 * which - for the logged-in user, will be in $USER->access
77 * For other users can be generated and passed around (but see
78 * the $ACCESS global).
80 * $accessdata is a multidimensional array, holding
81 * role assignments (RAs), role-capabilities-perm sets
82 * (role defs) and a list of courses we have loaded
83 * data for.
85 * Things are keyed on "contextpaths" (the path field of
86 * the context table) for fast walking up/down the tree.
88 * $accessdata[ra][$contextpath]= array($roleid)
89 * [$contextpath]= array($roleid)
90 * [$contextpath]= array($roleid)
92 * Role definitions are stored like this
93 * (no cap merge is done - so it's compact)
95 * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
96 * [mod/forum:editallpost] = -1
97 * [mod/forum:startdiscussion] = -1000
99 * See how has_capability_in_accessdata() walks up/down the tree.
101 * Normally - specially for the logged-in user, we only load
102 * rdef and ra down to the course level, but not below. This
103 * keeps accessdata small and compact. Below-the-course ra/rdef
104 * are loaded as needed. We keep track of which courses we
105 * have loaded ra/rdef in
107 * $accessdata[loaded] = array($contextpath, $contextpath)
109 * Stale accessdata
110 * ----------------
112 * For the logged-in user, accessdata is long-lived.
114 * On each pageload we load $DIRTYPATHS which lists
115 * context paths affected by changes. Any check at-or-below
116 * a dirty context will trigger a transparent reload of accessdata.
118 * Changes at the sytem level will force the reload for everyone.
120 * Default role caps
121 * -----------------
122 * The default role assignment is not in the DB, so we
123 * add it manually to accessdata.
125 * This means that functions that work directly off the
126 * DB need to ensure that the default role caps
127 * are dealt with appropriately.
131 require_once $CFG->dirroot.'/lib/blocklib.php';
133 // permission definitions
134 define('CAP_INHERIT', 0);
135 define('CAP_ALLOW', 1);
136 define('CAP_PREVENT', -1);
137 define('CAP_PROHIBIT', -1000);
139 // context definitions
140 define('CONTEXT_SYSTEM', 10);
141 define('CONTEXT_USER', 30);
142 define('CONTEXT_COURSECAT', 40);
143 define('CONTEXT_COURSE', 50);
144 define('CONTEXT_GROUP', 60);
145 define('CONTEXT_MODULE', 70);
146 define('CONTEXT_BLOCK', 80);
148 // capability risks - see http://docs.moodle.org/en/Development:Hardening_new_Roles_system
149 define('RISK_MANAGETRUST', 0x0001);
150 define('RISK_CONFIG', 0x0002);
151 define('RISK_XSS', 0x0004);
152 define('RISK_PERSONAL', 0x0008);
153 define('RISK_SPAM', 0x0010);
154 define('RISK_DATALOSS', 0x0020);
156 // rolename displays
157 define('ROLENAME_ORIGINAL', 0);// the name as defined in the role definition
158 define('ROLENAME_ALIAS', 1); // the name as defined by a role alias
159 define('ROLENAME_BOTH', 2); // Both, like this: Role alias (Original)
161 require_once($CFG->dirroot.'/group/lib.php');
163 $context_cache = array(); // Cache of all used context objects for performance (by level and instance)
164 $context_cache_id = array(); // Index to above cache by id
166 $DIRTYCONTEXTS = null; // dirty contexts cache
167 $ACCESS = array(); // cache of caps for cron user switching and has_capability for other users (==not $USER)
168 $RDEFS = array(); // role definitions cache - helps a lot with mem usage in cron
170 function get_role_context_caps($roleid, $context) {
171 //this is really slow!!!! - do not use above course context level!
172 $result = array();
173 $result[$context->id] = array();
175 // first emulate the parent context capabilities merging into context
176 $searchcontexts = array_reverse(get_parent_contexts($context));
177 array_push($searchcontexts, $context->id);
178 foreach ($searchcontexts as $cid) {
179 if ($capabilities = get_records_select('role_capabilities', "roleid = $roleid AND contextid = $cid")) {
180 foreach ($capabilities as $cap) {
181 if (!array_key_exists($cap->capability, $result[$context->id])) {
182 $result[$context->id][$cap->capability] = 0;
184 $result[$context->id][$cap->capability] += $cap->permission;
189 // now go through the contexts bellow given context
190 $searchcontexts = array_keys(get_child_contexts($context));
191 foreach ($searchcontexts as $cid) {
192 if ($capabilities = get_records_select('role_capabilities', "roleid = $roleid AND contextid = $cid")) {
193 foreach ($capabilities as $cap) {
194 if (!array_key_exists($cap->contextid, $result)) {
195 $result[$cap->contextid] = array();
197 $result[$cap->contextid][$cap->capability] = $cap->permission;
202 return $result;
206 * Gets the accessdata for role "sitewide"
207 * (system down to course)
209 * @return array
211 function get_role_access($roleid, $accessdata=NULL) {
213 global $CFG;
215 /* Get it in 1 cheap DB query...
216 * - relevant role caps at the root and down
217 * to the course level - but not below
219 if (is_null($accessdata)) {
220 $accessdata = array(); // named list
221 $accessdata['ra'] = array();
222 $accessdata['rdef'] = array();
223 $accessdata['loaded'] = array();
227 // Overrides for the role IN ANY CONTEXTS
228 // down to COURSE - not below -
230 $sql = "SELECT ctx.path,
231 rc.capability, rc.permission
232 FROM {$CFG->prefix}context ctx
233 JOIN {$CFG->prefix}role_capabilities rc
234 ON rc.contextid=ctx.id
235 WHERE rc.roleid = {$roleid}
236 AND ctx.contextlevel <= ".CONTEXT_COURSE."
237 ORDER BY ctx.depth, ctx.path";
239 // we need extra caching in cron only
240 if (defined('FULLME') and FULLME === 'cron') {
241 static $cron_cache = array();
243 if (!isset($cron_cache[$roleid])) {
244 $cron_cache[$roleid] = array();
245 if ($rs = get_recordset_sql($sql)) {
246 while ($rd = rs_fetch_next_record($rs)) {
247 $cron_cache[$roleid][] = $rd;
249 rs_close($rs);
253 foreach ($cron_cache[$roleid] as $rd) {
254 $k = "{$rd->path}:{$roleid}";
255 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
258 } else {
259 if ($rs = get_recordset_sql($sql)) {
260 while ($rd = rs_fetch_next_record($rs)) {
261 $k = "{$rd->path}:{$roleid}";
262 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
264 unset($rd);
265 rs_close($rs);
269 return $accessdata;
273 * Gets the accessdata for role "sitewide"
274 * (system down to course)
276 * @return array
278 function get_default_frontpage_role_access($roleid, $accessdata=NULL) {
280 global $CFG;
282 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
283 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
286 // Overrides for the role in any contexts related to the course
288 $sql = "SELECT ctx.path,
289 rc.capability, rc.permission
290 FROM {$CFG->prefix}context ctx
291 JOIN {$CFG->prefix}role_capabilities rc
292 ON rc.contextid=ctx.id
293 WHERE rc.roleid = {$roleid}
294 AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE '$base/%')
295 AND ctx.contextlevel <= ".CONTEXT_COURSE."
296 ORDER BY ctx.depth, ctx.path";
298 if ($rs = get_recordset_sql($sql)) {
299 while ($rd = rs_fetch_next_record($rs)) {
300 $k = "{$rd->path}:{$roleid}";
301 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
303 unset($rd);
304 rs_close($rs);
307 return $accessdata;
312 * Get the default guest role
313 * @return object role
315 function get_guest_role() {
316 global $CFG;
318 if (empty($CFG->guestroleid)) {
319 if ($roles = get_roles_with_capability('moodle/legacy:guest', CAP_ALLOW)) {
320 $guestrole = array_shift($roles); // Pick the first one
321 set_config('guestroleid', $guestrole->id);
322 return $guestrole;
323 } else {
324 debugging('Can not find any guest role!');
325 return false;
327 } else {
328 if ($guestrole = get_record('role','id', $CFG->guestroleid)) {
329 return $guestrole;
330 } else {
331 //somebody is messing with guest roles, remove incorrect setting and try to find a new one
332 set_config('guestroleid', '');
333 return get_guest_role();
339 * This function returns whether the current user has the capability of performing a function
340 * For example, we can do has_capability('mod/forum:replypost',$context) in forum
341 * @param string $capability - name of the capability (or debugcache or clearcache)
342 * @param object $context - a context object (record from context table)
343 * @param integer $userid - a userid number, empty if current $USER
344 * @param bool $doanything - if false, ignore do anything
345 * @return bool
347 function has_capability($capability, $context, $userid=NULL, $doanything=true) {
348 global $USER, $ACCESS, $CFG, $DIRTYCONTEXTS;
350 // the original $CONTEXT here was hiding serious errors
351 // for security reasons do not reuse previous context
352 if (empty($context)) {
353 debugging('Incorrect context specified');
354 return false;
357 /// Some sanity checks
358 if (debugging('',DEBUG_DEVELOPER)) {
359 if (!is_valid_capability($capability)) {
360 debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
362 if (!is_bool($doanything)) {
363 debugging('Capability parameter "doanything" is wierd ("'.$doanything.'"). This should be fixed in code.');
367 if (empty($userid)) { // we must accept null, 0, '0', '' etc. in $userid
368 $userid = $USER->id;
371 if (is_null($context->path) or $context->depth == 0) {
372 //this should not happen
373 $contexts = array(SYSCONTEXTID, $context->id);
374 $context->path = '/'.SYSCONTEXTID.'/'.$context->id;
375 debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);
377 } else {
378 $contexts = explode('/', $context->path);
379 array_shift($contexts);
382 if (defined('FULLME') && FULLME === 'cron' && !isset($USER->access)) {
383 // In cron, some modules setup a 'fake' $USER,
384 // ensure we load the appropriate accessdata.
385 if (isset($ACCESS[$userid])) {
386 $DIRTYCONTEXTS = NULL; //load fresh dirty contexts
387 } else {
388 load_user_accessdata($userid);
389 $DIRTYCONTEXTS = array();
391 $USER->access = $ACCESS[$userid];
393 } else if ($USER->id == $userid && !isset($USER->access)) {
394 // caps not loaded yet - better to load them to keep BC with 1.8
395 // not-logged-in user or $USER object set up manually first time here
396 load_all_capabilities();
397 $ACCESS = array(); // reset the cache for other users too, the dirty contexts are empty now
398 $RDEFS = array();
401 // Load dirty contexts list if needed
402 if (!isset($DIRTYCONTEXTS)) {
403 if (isset($USER->access['time'])) {
404 $DIRTYCONTEXTS = get_dirty_contexts($USER->access['time']);
406 else {
407 $DIRTYCONTEXTS = array();
411 // Careful check for staleness...
412 if (count($DIRTYCONTEXTS) !== 0 and is_contextpath_dirty($contexts, $DIRTYCONTEXTS)) {
413 // reload all capabilities - preserving loginas, roleswitches, etc
414 // and then cleanup any marks of dirtyness... at least from our short
415 // term memory! :-)
416 $ACCESS = array();
417 $RDEFS = array();
419 if (defined('FULLME') && FULLME === 'cron') {
420 load_user_accessdata($userid);
421 $USER->access = $ACCESS[$userid];
422 $DIRTYCONTEXTS = array();
424 } else {
425 reload_all_capabilities();
429 // divulge how many times we are called
430 //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
432 if ($USER->id == $userid) { // we must accept strings and integers in $userid
434 // For the logged in user, we have $USER->access
435 // which will have all RAs and caps preloaded for
436 // course and above contexts.
438 // Contexts below courses && contexts that do not
439 // hang from courses are loaded into $USER->access
440 // on demand, and listed in $USER->access[loaded]
442 if ($context->contextlevel <= CONTEXT_COURSE) {
443 // Course and above are always preloaded
444 return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
446 // Load accessdata for below-the-course contexts
447 if (!path_inaccessdata($context->path,$USER->access)) {
448 // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
449 // $bt = debug_backtrace();
450 // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
451 load_subcontext($USER->id, $context, $USER->access);
453 return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
456 if (!isset($ACCESS[$userid])) {
457 load_user_accessdata($userid);
459 if ($context->contextlevel <= CONTEXT_COURSE) {
460 // Course and above are always preloaded
461 return has_capability_in_accessdata($capability, $context, $ACCESS[$userid], $doanything);
463 // Load accessdata for below-the-course contexts as needed
464 if (!path_inaccessdata($context->path, $ACCESS[$userid])) {
465 // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
466 // $bt = debug_backtrace();
467 // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
468 load_subcontext($userid, $context, $ACCESS[$userid]);
470 return has_capability_in_accessdata($capability, $context, $ACCESS[$userid], $doanything);
474 * This function returns whether the current user has any of the capabilities in the
475 * $capabilities array. This is a simple wrapper around has_capability for convinience.
477 * There are probably tricks that could be done to improve the performance here, for example,
478 * check the capabilities that are already cached first.
480 * @param array $capabilities - an array of capability names.
481 * @param object $context - a context object (record from context table)
482 * @param integer $userid - a userid number, empty if current $USER
483 * @param bool $doanything - if false, ignore do anything
484 * @return bool
486 function has_any_capability($capabilities, $context, $userid=NULL, $doanything=true) {
487 foreach ($capabilities as $capability) {
488 if (has_capability($capability, $context, $userid, $doanything)) {
489 return true;
492 return false;
496 * This function returns whether the current user has all of the capabilities in the
497 * $capabilities array. This is a simple wrapper around has_capability for convinience.
499 * There are probably tricks that could be done to improve the performance here, for example,
500 * check the capabilities that are already cached first.
502 * @param array $capabilities - an array of capability names.
503 * @param object $context - a context object (record from context table)
504 * @param integer $userid - a userid number, empty if current $USER
505 * @param bool $doanything - if false, ignore do anything
506 * @return bool
508 function has_all_capabilities($capabilities, $context, $userid=NULL, $doanything=true) {
509 if (!is_array($capabilities)) {
510 debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
511 return false;
513 foreach ($capabilities as $capability) {
514 if (!has_capability($capability, $context, $userid, $doanything)) {
515 return false;
518 return true;
522 * Uses 1 DB query to answer whether a user is an admin at the sitelevel.
523 * It depends on DB schema >=1.7 but does not depend on the new datastructures
524 * in v1.9 (context.path, or $USER->access)
526 * Will return true if the userid has any of
527 * - moodle/site:config
528 * - moodle/legacy:admin
529 * - moodle/site:doanything
531 * @param int $userid
532 * @returns bool $isadmin
534 function is_siteadmin($userid) {
535 global $CFG;
537 $sql = "SELECT SUM(rc.permission)
538 FROM " . $CFG->prefix . "role_capabilities rc
539 JOIN " . $CFG->prefix . "context ctx
540 ON ctx.id=rc.contextid
541 JOIN " . $CFG->prefix . "role_assignments ra
542 ON ra.roleid=rc.roleid AND ra.contextid=ctx.id
543 WHERE ctx.contextlevel=10
544 AND ra.userid={$userid}
545 AND rc.capability IN ('moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything')
546 GROUP BY rc.capability
547 HAVING SUM(rc.permission) > 0";
549 $isadmin = record_exists_sql($sql);
550 return $isadmin;
553 function get_course_from_path ($path) {
554 // assume that nothing is more than 1 course deep
555 if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
556 return $matches[1];
558 return false;
561 function path_inaccessdata($path, $accessdata) {
563 // assume that contexts hang from sys or from a course
564 // this will only work well with stuff that hangs from a course
565 if (in_array($path, $accessdata['loaded'], true)) {
566 // error_log("found it!");
567 return true;
569 $base = '/' . SYSCONTEXTID;
570 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
571 $path = $matches[1];
572 if ($path === $base) {
573 return false;
575 if (in_array($path, $accessdata['loaded'], true)) {
576 return true;
579 return false;
583 * Walk the accessdata array and return true/false.
584 * Deals with prohibits, roleswitching, aggregating
585 * capabilities, etc.
587 * The main feature of here is being FAST and with no
588 * side effects.
590 * Notes:
592 * Switch Roles exits early
593 * -----------------------
594 * cap checks within a switchrole need to exit early
595 * in our bottom up processing so they don't "see" that
596 * there are real RAs that can do all sorts of things.
598 * Switch Role merges with default role
599 * ------------------------------------
600 * If you are a teacher in course X, you have at least
601 * teacher-in-X + defaultloggedinuser-sitewide. So in the
602 * course you'll have techer+defaultloggedinuser.
603 * We try to mimic that in switchrole.
605 * Local-most role definition and role-assignment wins
606 * ---------------------------------------------------
607 * So if the local context has said 'allow', it wins
608 * over a high-level context that says 'deny'.
609 * This is applied when walking rdefs, and RAs.
610 * Only at the same context the values are SUM()med.
612 * The exception is CAP_PROHIBIT.
614 * "Guest default role" exception
615 * ------------------------------
617 * See MDL-7513 and $ignoreguest below for details.
619 * The rule is that
621 * IF we are being asked about moodle/legacy:guest
622 * OR moodle/course:view
623 * FOR a real, logged-in user
624 * AND we reached the top of the path in ra and rdef
625 * AND that role has moodle/legacy:guest === 1...
626 * THEN we act as if we hadn't seen it.
628 * Note that this function must be kept in synch with has_capability_in_accessdata.
630 * To Do:
632 * - Document how it works
633 * - Rewrite in ASM :-)
636 function has_capability_in_accessdata($capability, $context, $accessdata, $doanything) {
638 global $CFG;
640 $path = $context->path;
642 // build $contexts as a list of "paths" of the current
643 // contexts and parents with the order top-to-bottom
644 $contexts = array($path);
645 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
646 $path = $matches[1];
647 array_unshift($contexts, $path);
650 $ignoreguest = false;
651 if (isset($accessdata['dr'])
652 && ($capability == 'moodle/course:view'
653 || $capability == 'moodle/legacy:guest')) {
654 // At the base, ignore rdefs where moodle/legacy:guest
655 // is set
656 $ignoreguest = $accessdata['dr'];
659 // Coerce it to an int
660 $CAP_PROHIBIT = (int)CAP_PROHIBIT;
662 $cc = count($contexts);
664 $can = 0;
665 $capdepth = 0;
668 // role-switches loop
670 if (isset($accessdata['rsw'])) {
671 // check for isset() is fast
672 // empty() is slow...
673 if (empty($accessdata['rsw'])) {
674 unset($accessdata['rsw']); // keep things fast and unambiguous
675 break;
677 // From the bottom up...
678 for ($n=$cc-1;$n>=0;$n--) {
679 $ctxp = $contexts[$n];
680 if (isset($accessdata['rsw'][$ctxp])) {
681 // Found a switchrole assignment
682 // check for that role _plus_ the default user role
683 $ras = array($accessdata['rsw'][$ctxp],$CFG->defaultuserroleid);
684 for ($rn=0;$rn<2;$rn++) {
685 $roleid = (int)$ras[$rn];
686 // Walk the path for capabilities
687 // from the bottom up...
688 for ($m=$cc-1;$m>=0;$m--) {
689 $capctxp = $contexts[$m];
690 if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
691 $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
693 // The most local permission (first to set) wins
694 // the only exception is CAP_PROHIBIT
695 if ($can === 0) {
696 $can = $perm;
697 } elseif ($perm === $CAP_PROHIBIT) {
698 $can = $perm;
699 break;
704 // As we are dealing with a switchrole,
705 // we return _here_, do _not_ walk up
706 // the hierarchy any further
707 if ($can < 1) {
708 if ($doanything) {
709 // didn't find it as an explicit cap,
710 // but maybe the user can doanything in this context...
711 return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
712 } else {
713 return false;
715 } else {
716 return true;
724 // Main loop for normal RAs
725 // From the bottom up...
727 for ($n=$cc-1;$n>=0;$n--) {
728 $ctxp = $contexts[$n];
729 if (isset($accessdata['ra'][$ctxp])) {
730 // Found role assignments on this leaf
731 $ras = $accessdata['ra'][$ctxp];
733 $rc = count($ras);
734 $ctxcan = 0;
735 $ctxcapdepth = 0;
736 for ($rn=0;$rn<$rc;$rn++) {
737 $roleid = (int)$ras[$rn];
738 $rolecan = 0;
739 $rolecapdepth = 0;
740 // Walk the path for capabilities
741 // from the bottom up...
742 for ($m=$cc-1;$m>=0;$m--) {
743 $capctxp = $contexts[$m];
744 // ignore some guest caps
745 // at base ra and rdef
746 if ($ignoreguest == $roleid
747 && $n === 0
748 && $m === 0
749 && isset($accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'])
750 && $accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'] > 0) {
751 continue;
753 if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
754 $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
755 // The most local permission (first to set) wins
756 // the only exception is CAP_PROHIBIT
757 if ($rolecan === 0) {
758 $rolecan = $perm;
759 $rolecapdepth = $m;
760 } elseif ($perm === $CAP_PROHIBIT) {
761 $rolecan = $perm;
762 $rolecapdepth = $m;
763 break;
767 // Rules for RAs at the same context...
768 // - prohibits always wins
769 // - permissions at the same ctxlevel & capdepth are added together
770 // - deeper capdepth wins
771 if ($ctxcan === $CAP_PROHIBIT || $rolecan === $CAP_PROHIBIT) {
772 $ctxcan = $CAP_PROHIBIT;
773 $ctxcapdepth = 0;
774 } elseif ($ctxcapdepth === $rolecapdepth) {
775 $ctxcan += $rolecan;
776 } elseif ($ctxcapdepth < $rolecapdepth) {
777 $ctxcan = $rolecan;
778 $ctxcapdepth = $rolecapdepth;
779 } else { // ctxcaptdepth is deeper
780 // rolecap ignored
783 // The most local RAs with a defined
784 // permission ($ctxcan) win, except
785 // for CAP_PROHIBIT
786 // NOTE: If we want the deepest RDEF to
787 // win regardless of the depth of the RA,
788 // change the elseif below to read
789 // ($can === 0 || $capdepth < $ctxcapdepth) {
790 if ($ctxcan === $CAP_PROHIBIT) {
791 $can = $ctxcan;
792 break;
793 } elseif ($can === 0) { // see note above
794 $can = $ctxcan;
795 $capdepth = $ctxcapdepth;
800 if ($can < 1) {
801 if ($doanything) {
802 // didn't find it as an explicit cap,
803 // but maybe the user can doanything in this context...
804 return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
805 } else {
806 return false;
808 } else {
809 return true;
814 function aggregate_roles_from_accessdata($context, $accessdata) {
816 $path = $context->path;
818 // build $contexts as a list of "paths" of the current
819 // contexts and parents with the order top-to-bottom
820 $contexts = array($path);
821 while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
822 $path = $matches[1];
823 array_unshift($contexts, $path);
826 $cc = count($contexts);
828 $roles = array();
829 // From the bottom up...
830 for ($n=$cc-1;$n>=0;$n--) {
831 $ctxp = $contexts[$n];
832 if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
833 // Found assignments on this leaf
834 $addroles = $accessdata['ra'][$ctxp];
835 $roles = array_merge($roles, $addroles);
839 return array_unique($roles);
843 * This is an easy to use function, combining has_capability() with require_course_login().
844 * And will call those where needed.
846 * It checks for a capability assertion being true. If it isn't
847 * then the page is terminated neatly with a standard error message.
849 * If the user is not logged in, or is using 'guest' access or other special "users,
850 * it provides a logon prompt.
852 * @param string $capability - name of the capability
853 * @param object $context - a context object (record from context table)
854 * @param integer $userid - a userid number
855 * @param bool $doanything - if false, ignore do anything
856 * @param string $errorstring - an errorstring
857 * @param string $stringfile - which stringfile to get it from
859 function require_capability($capability, $context, $userid=NULL, $doanything=true,
860 $errormessage='nopermissions', $stringfile='') {
862 global $USER, $CFG;
864 /* Empty $userid means current user, if the current user is not logged in,
865 * then make sure they are (if needed).
866 * Originally there was a check for loaded permissions - it is not needed here.
867 * Context is now required parameter, the cached $CONTEXT was only hiding errors.
869 $errorlink = '';
871 if (empty($userid)) {
872 if ($context->contextlevel == CONTEXT_COURSE) {
873 require_login($context->instanceid);
875 } else if ($context->contextlevel == CONTEXT_MODULE) {
876 if (!$cm = get_record('course_modules', 'id', $context->instanceid)) {
877 error('Incorrect module');
879 if (!$course = get_record('course', 'id', $cm->course)) {
880 error('Incorrect course.');
882 require_course_login($course, true, $cm);
883 $errorlink = $CFG->wwwroot.'/course/view.php?id='.$cm->course;
885 } else if ($context->contextlevel == CONTEXT_SYSTEM) {
886 if (!empty($CFG->forcelogin)) {
887 require_login();
890 } else {
891 require_login();
895 /// OK, if they still don't have the capability then print a nice error message
897 if (!has_capability($capability, $context, $userid, $doanything)) {
898 $capabilityname = get_capability_string($capability);
899 print_error($errormessage, $stringfile, $errorlink, $capabilityname);
904 * Get an array of courses (with magic extra bits)
905 * where the accessdata and in DB enrolments show
906 * that the cap requested is available.
908 * The main use is for get_my_courses().
910 * Notes
912 * - $fields is an array of fieldnames to ADD
913 * so name the fields you really need, which will
914 * be added and uniq'd
916 * - the course records have $c->context which is a fully
917 * valid context object. Saves you a query per course!
919 * - the course records have $c->categorypath to make
920 * category lookups cheap
922 * - current implementation is split in -
924 * - if the user has the cap systemwide, stupidly
925 * grab *every* course for a capcheck. This eats
926 * a TON of bandwidth, specially on large sites
927 * with separate DBs...
929 * - otherwise, fetch "likely" courses with a wide net
930 * that should get us _cheaply_ at least the courses we need, and some
931 * we won't - we get courses that...
932 * - are in a category where user has the cap
933 * - or where use has a role-assignment (any kind)
934 * - or where the course has an override on for this cap
936 * - walk the courses recordset checking the caps oneach one
937 * the checks are all in memory and quite fast
938 * (though we could implement a specialised variant of the
939 * has_capability_in_accessdata() code to speed it up)
941 * @param string $capability - name of the capability
942 * @param array $accessdata - accessdata session array
943 * @param bool $doanything - if false, ignore do anything
944 * @param string $sort - sorting fields - prefix each fieldname with "c."
945 * @param array $fields - additional fields you are interested in...
946 * @param int $limit - set if you want to limit the number of courses
947 * @return array $courses - ordered array of course objects - see notes above
950 function get_user_courses_bycap($userid, $cap, $accessdata, $doanything, $sort='c.sortorder ASC', $fields=NULL, $limit=0) {
952 global $CFG;
954 // Slim base fields, let callers ask for what they need...
955 $basefields = array('id', 'sortorder', 'shortname', 'idnumber');
957 if (!is_null($fields)) {
958 $fields = array_merge($basefields, $fields);
959 $fields = array_unique($fields);
960 } else {
961 $fields = $basefields;
963 $coursefields = 'c.' .implode(',c.', $fields);
965 $sort = trim($sort);
966 if ($sort !== '') {
967 $sort = "ORDER BY $sort";
970 $sysctx = get_context_instance(CONTEXT_SYSTEM);
971 if (has_capability_in_accessdata($cap, $sysctx, $accessdata, $doanything)) {
973 // Apparently the user has the cap sitewide, so walk *every* course
974 // (the cap checks are moderately fast, but this moves massive bandwidth w the db)
975 // Yuck.
977 $sql = "SELECT $coursefields,
978 ctx.id AS ctxid, ctx.path AS ctxpath,
979 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
980 cc.path AS categorypath
981 FROM {$CFG->prefix}course c
982 JOIN {$CFG->prefix}course_categories cc
983 ON c.category=cc.id
984 JOIN {$CFG->prefix}context ctx
985 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
986 $sort ";
987 $rs = get_recordset_sql($sql);
988 } else {
990 // narrow down where we have the caps to a few contexts
991 // this will be a combination of
992 // - courses where user has an explicit enrolment
993 // - courses that have an override (any status) on that capability
994 // - categories where user has the rights (granted status) on that capability
996 $sql = "SELECT ctx.*
997 FROM {$CFG->prefix}context ctx
998 WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
999 ORDER BY ctx.depth";
1000 $rs = get_recordset_sql($sql);
1001 $catpaths = array();
1002 while ($catctx = rs_fetch_next_record($rs)) {
1003 if ($catctx->path != ''
1004 && has_capability_in_accessdata($cap, $catctx, $accessdata, $doanything)) {
1005 $catpaths[] = $catctx->path;
1008 rs_close($rs);
1009 $catclause = '';
1010 if (count($catpaths)) {
1011 $cc = count($catpaths);
1012 for ($n=0;$n<$cc;$n++) {
1013 $catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'";
1015 $catclause = 'WHERE (' . implode(' OR ', $catpaths) .')';
1017 unset($catpaths);
1019 $capany = '';
1020 if ($doanything) {
1021 $capany = " OR rc.capability='moodle/site:doanything'";
1024 /// UNION 3 queries:
1025 /// - user role assignments in courses
1026 /// - user capability (override - any status) in courses
1027 /// - user right (granted status) in categories (optionally executed)
1028 /// Enclosing the 3-UNION into an inline_view to avoid column names conflict and making the ORDER BY cross-db
1029 /// and to allow selection of TEXT columns in the query (MSSQL and Oracle limitation). MDL-16209
1030 $sql = "
1031 SELECT $coursefields, ctxid, ctxpath, ctxdepth, ctxlevel, categorypath
1032 FROM (
1033 SELECT c.id,
1034 ctx.id AS ctxid, ctx.path AS ctxpath,
1035 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1036 cc.path AS categorypath
1037 FROM {$CFG->prefix}course c
1038 JOIN {$CFG->prefix}course_categories cc
1039 ON c.category=cc.id
1040 JOIN {$CFG->prefix}context ctx
1041 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1042 JOIN {$CFG->prefix}role_assignments ra
1043 ON (ra.contextid=ctx.id AND ra.userid=$userid)
1044 UNION
1045 SELECT c.id,
1046 ctx.id AS ctxid, ctx.path AS ctxpath,
1047 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1048 cc.path AS categorypath
1049 FROM {$CFG->prefix}course c
1050 JOIN {$CFG->prefix}course_categories cc
1051 ON c.category=cc.id
1052 JOIN {$CFG->prefix}context ctx
1053 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1054 JOIN {$CFG->prefix}role_capabilities rc
1055 ON (rc.contextid=ctx.id AND (rc.capability='$cap' $capany)) ";
1057 if (!empty($catclause)) { /// If we have found the right in categories, add child courses here too
1058 $sql .= "
1059 UNION
1060 SELECT c.id,
1061 ctx.id AS ctxid, ctx.path AS ctxpath,
1062 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1063 cc.path AS categorypath
1064 FROM {$CFG->prefix}course c
1065 JOIN {$CFG->prefix}course_categories cc
1066 ON c.category=cc.id
1067 JOIN {$CFG->prefix}context ctx
1068 ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1069 $catclause";
1072 /// Close the inline_view and join with courses table to get requested $coursefields
1073 $sql .= "
1074 ) inline_view
1075 INNER JOIN {$CFG->prefix}course c
1076 ON inline_view.id = c.id";
1078 /// To keep cross-db we need to strip any prefix in the ORDER BY clause for queries using UNION
1079 $sql .= "
1080 " . preg_replace('/[a-z]+\./i', '', $sort); /// Add ORDER BY clause
1082 $rs = get_recordset_sql($sql);
1085 /// Confirm rights (granted capability) for each course returned
1086 $courses = array();
1087 $cc = 0; // keep count
1088 while ($c = rs_fetch_next_record($rs)) {
1089 // build the context obj
1090 $c = make_context_subobj($c);
1092 if (has_capability_in_accessdata($cap, $c->context, $accessdata, $doanything)) {
1093 $courses[] = $c;
1094 if ($limit > 0 && $cc++ > $limit) {
1095 break;
1099 rs_close($rs);
1100 return $courses;
1105 * It will return a nested array showing role assignments
1106 * all relevant role capabilities for the user at
1107 * site/metacourse/course_category/course levels
1109 * We do _not_ delve deeper than courses because the number of
1110 * overrides at the module/block levels is HUGE.
1112 * [ra] => [/path/] = array(roleid, roleid)
1113 * [rdef] => [/path/:roleid][capability]=permission
1114 * [loaded] => array('/path', '/path')
1116 * @param $userid integer - the id of the user
1119 function get_user_access_sitewide($userid) {
1121 global $CFG;
1123 // this flag has not been set!
1124 // (not clean install, or upgraded successfully to 1.7 and up)
1125 if (empty($CFG->rolesactive)) {
1126 return false;
1129 /* Get in 3 cheap DB queries...
1130 * - role assignments - with role_caps
1131 * - relevant role caps
1132 * - above this user's RAs
1133 * - below this user's RAs - limited to course level
1136 $accessdata = array(); // named list
1137 $accessdata['ra'] = array();
1138 $accessdata['rdef'] = array();
1139 $accessdata['loaded'] = array();
1141 $sitectx = get_system_context();
1142 $base = '/'.$sitectx->id;
1145 // Role assignments - and any rolecaps directly linked
1146 // because it's cheap to read rolecaps here over many
1147 // RAs
1149 $sql = "SELECT ctx.path, ra.roleid, rc.capability, rc.permission
1150 FROM {$CFG->prefix}role_assignments ra
1151 JOIN {$CFG->prefix}context ctx
1152 ON ra.contextid=ctx.id
1153 LEFT OUTER JOIN {$CFG->prefix}role_capabilities rc
1154 ON (rc.roleid=ra.roleid AND rc.contextid=ra.contextid)
1155 WHERE ra.userid = $userid AND ctx.contextlevel <= ".CONTEXT_COURSE."
1156 ORDER BY ctx.depth, ctx.path, ra.roleid";
1157 $rs = get_recordset_sql($sql);
1159 // raparents collects paths & roles we need to walk up
1160 // the parenthood to build the rdef
1162 // the array will bulk up a bit with dups
1163 // which we'll later clear up
1165 $raparents = array();
1166 $lastseen = '';
1167 if ($rs) {
1168 while ($ra = rs_fetch_next_record($rs)) {
1169 // RAs leafs are arrays to support multi
1170 // role assignments...
1171 if (!isset($accessdata['ra'][$ra->path])) {
1172 $accessdata['ra'][$ra->path] = array();
1174 // only add if is not a repeat caused
1175 // by capability join...
1176 // (this check is cheaper than in_array())
1177 if ($lastseen !== $ra->path.':'.$ra->roleid) {
1178 $lastseen = $ra->path.':'.$ra->roleid;
1179 array_push($accessdata['ra'][$ra->path], $ra->roleid);
1180 $parentids = explode('/', $ra->path);
1181 array_shift($parentids); // drop empty leading "context"
1182 array_pop($parentids); // drop _this_ context
1184 if (isset($raparents[$ra->roleid])) {
1185 $raparents[$ra->roleid] = array_merge($raparents[$ra->roleid],
1186 $parentids);
1187 } else {
1188 $raparents[$ra->roleid] = $parentids;
1191 // Always add the roleded
1192 if (!empty($ra->capability)) {
1193 $k = "{$ra->path}:{$ra->roleid}";
1194 $accessdata['rdef'][$k][$ra->capability] = $ra->permission;
1197 unset($ra);
1198 rs_close($rs);
1201 // Walk up the tree to grab all the roledefs
1202 // of interest to our user...
1203 // NOTE: we use a series of IN clauses here - which
1204 // might explode on huge sites with very convoluted nesting of
1205 // categories... - extremely unlikely that the number of categories
1206 // and roletypes is so large that we hit the limits of IN()
1207 $clauses = array();
1208 foreach ($raparents as $roleid=>$contexts) {
1209 $contexts = implode(',', array_unique($contexts));
1210 if ($contexts ==! '') {
1211 $clauses[] = "(roleid=$roleid AND contextid IN ($contexts))";
1214 $clauses = implode(" OR ", $clauses);
1215 if ($clauses !== '') {
1216 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
1217 FROM {$CFG->prefix}role_capabilities rc
1218 JOIN {$CFG->prefix}context ctx
1219 ON rc.contextid=ctx.id
1220 WHERE $clauses
1221 ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1223 $rs = get_recordset_sql($sql);
1224 unset($clauses);
1226 if ($rs) {
1227 while ($rd = rs_fetch_next_record($rs)) {
1228 $k = "{$rd->path}:{$rd->roleid}";
1229 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1231 unset($rd);
1232 rs_close($rs);
1237 // Overrides for the role assignments IN SUBCONTEXTS
1238 // (though we still do _not_ go below the course level.
1240 // NOTE that the JOIN w sctx is with 3-way triangulation to
1241 // catch overrides to the applicable role in any subcontext, based
1242 // on the path field of the parent.
1244 $sql = "SELECT sctx.path, ra.roleid,
1245 ctx.path AS parentpath,
1246 rco.capability, rco.permission
1247 FROM {$CFG->prefix}role_assignments ra
1248 JOIN {$CFG->prefix}context ctx
1249 ON ra.contextid=ctx.id
1250 JOIN {$CFG->prefix}context sctx
1251 ON (sctx.path LIKE " . sql_concat('ctx.path',"'/%'"). " )
1252 JOIN {$CFG->prefix}role_capabilities rco
1253 ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
1254 WHERE ra.userid = $userid
1255 AND sctx.contextlevel <= ".CONTEXT_COURSE."
1256 ORDER BY sctx.depth, sctx.path, ra.roleid";
1258 $rs = get_recordset_sql($sql);
1259 if ($rs) {
1260 while ($rd = rs_fetch_next_record($rs)) {
1261 $k = "{$rd->path}:{$rd->roleid}";
1262 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1264 unset($rd);
1265 rs_close($rs);
1267 return $accessdata;
1271 * It add to the access ctrl array the data
1272 * needed by a user for a given context
1274 * @param $userid integer - the id of the user
1275 * @param $context context obj - needs path!
1276 * @param $accessdata array accessdata array
1278 function load_subcontext($userid, $context, &$accessdata) {
1280 global $CFG;
1284 /* Get the additional RAs and relevant rolecaps
1285 * - role assignments - with role_caps
1286 * - relevant role caps
1287 * - above this user's RAs
1288 * - below this user's RAs - limited to course level
1291 $base = "/" . SYSCONTEXTID;
1294 // Replace $context with the target context we will
1295 // load. Normally, this will be a course context, but
1296 // may be a different top-level context.
1298 // We have 3 cases
1300 // - Course
1301 // - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM
1302 // - BLOCK/MODULE/GROUP hanging from a course
1304 // For course contexts, we _already_ have the RAs
1305 // but the cost of re-fetching is minimal so we don't care.
1307 if ($context->contextlevel !== CONTEXT_COURSE
1308 && $context->path !== "$base/{$context->id}") {
1309 // Case BLOCK/MODULE/GROUP hanging from a course
1310 // Assumption: the course _must_ be our parent
1311 // If we ever see stuff nested further this needs to
1312 // change to do 1 query over the exploded path to
1313 // find out which one is the course
1314 $courses = explode('/',get_course_from_path($context->path));
1315 $targetid = array_pop($courses);
1316 $context = get_context_instance_by_id($targetid);
1321 // Role assignments in the context and below
1323 $sql = "SELECT ctx.path, ra.roleid
1324 FROM {$CFG->prefix}role_assignments ra
1325 JOIN {$CFG->prefix}context ctx
1326 ON ra.contextid=ctx.id
1327 WHERE ra.userid = $userid
1328 AND (ctx.path = '{$context->path}' OR ctx.path LIKE '{$context->path}/%')
1329 ORDER BY ctx.depth, ctx.path, ra.roleid";
1330 $rs = get_recordset_sql($sql);
1333 // Read in the RAs, preventing duplicates
1335 $localroles = array();
1336 $lastseen = '';
1337 while ($ra = rs_fetch_next_record($rs)) {
1338 if (!isset($accessdata['ra'][$ra->path])) {
1339 $accessdata['ra'][$ra->path] = array();
1341 // only add if is not a repeat caused
1342 // by capability join...
1343 // (this check is cheaper than in_array())
1344 if ($lastseen !== $ra->path.':'.$ra->roleid) {
1345 $lastseen = $ra->path.':'.$ra->roleid;
1346 array_push($accessdata['ra'][$ra->path], $ra->roleid);
1347 array_push($localroles, $ra->roleid);
1350 rs_close($rs);
1353 // Walk up and down the tree to grab all the roledefs
1354 // of interest to our user...
1356 // NOTES
1357 // - we use IN() but the number of roles is very limited.
1359 $courseroles = aggregate_roles_from_accessdata($context, $accessdata);
1361 // Do we have any interesting "local" roles?
1362 $localroles = array_diff($localroles,$courseroles); // only "new" local roles
1363 $wherelocalroles='';
1364 if (count($localroles)) {
1365 // Role defs for local roles in 'higher' contexts...
1366 $contexts = substr($context->path, 1); // kill leading slash
1367 $contexts = str_replace('/', ',', $contexts);
1368 $localroleids = implode(',',$localroles);
1369 $wherelocalroles="OR (rc.roleid IN ({$localroleids})
1370 AND ctx.id IN ($contexts))" ;
1373 // We will want overrides for all of them
1374 $whereroles = '';
1375 if ($roleids = implode(',',array_merge($courseroles,$localroles))) {
1376 $whereroles = "rc.roleid IN ($roleids) AND";
1378 $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
1379 FROM {$CFG->prefix}role_capabilities rc
1380 JOIN {$CFG->prefix}context ctx
1381 ON rc.contextid=ctx.id
1382 WHERE ($whereroles
1383 (ctx.id={$context->id} OR ctx.path LIKE '{$context->path}/%'))
1384 $wherelocalroles
1385 ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1387 $newrdefs = array();
1388 if ($rs = get_recordset_sql($sql)) {
1389 while ($rd = rs_fetch_next_record($rs)) {
1390 $k = "{$rd->path}:{$rd->roleid}";
1391 if (!array_key_exists($k, $newrdefs)) {
1392 $newrdefs[$k] = array();
1394 $newrdefs[$k][$rd->capability] = $rd->permission;
1396 rs_close($rs);
1397 } else {
1398 debugging('Bad SQL encountered!');
1401 compact_rdefs($newrdefs);
1402 foreach ($newrdefs as $key=>$value) {
1403 $accessdata['rdef'][$key] =& $newrdefs[$key];
1406 // error_log("loaded {$context->path}");
1407 $accessdata['loaded'][] = $context->path;
1411 * It add to the access ctrl array the data
1412 * needed by a role for a given context.
1414 * The data is added in the rdef key.
1416 * This role-centric function is useful for role_switching
1417 * and to get an overview of what a role gets under a
1418 * given context and below...
1420 * @param $roleid integer - the id of the user
1421 * @param $context context obj - needs path!
1422 * @param $accessdata accessdata array
1425 function get_role_access_bycontext($roleid, $context, $accessdata=NULL) {
1427 global $CFG;
1429 /* Get the relevant rolecaps into rdef
1430 * - relevant role caps
1431 * - at ctx and above
1432 * - below this ctx
1435 if (is_null($accessdata)) {
1436 $accessdata = array(); // named list
1437 $accessdata['ra'] = array();
1438 $accessdata['rdef'] = array();
1439 $accessdata['loaded'] = array();
1442 $contexts = substr($context->path, 1); // kill leading slash
1443 $contexts = str_replace('/', ',', $contexts);
1446 // Walk up and down the tree to grab all the roledefs
1447 // of interest to our role...
1449 // NOTE: we use an IN clauses here - which
1450 // might explode on huge sites with very convoluted nesting of
1451 // categories... - extremely unlikely that the number of nested
1452 // categories is so large that we hit the limits of IN()
1454 $sql = "SELECT ctx.path, rc.capability, rc.permission
1455 FROM {$CFG->prefix}role_capabilities rc
1456 JOIN {$CFG->prefix}context ctx
1457 ON rc.contextid=ctx.id
1458 WHERE rc.roleid=$roleid AND
1459 ( ctx.id IN ($contexts) OR
1460 ctx.path LIKE '{$context->path}/%' )
1461 ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1463 $rs = get_recordset_sql($sql);
1464 while ($rd = rs_fetch_next_record($rs)) {
1465 $k = "{$rd->path}:{$roleid}";
1466 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1468 rs_close($rs);
1470 return $accessdata;
1474 * Load accessdata for a user
1475 * into the $ACCESS global
1477 * Used by has_capability() - but feel free
1478 * to call it if you are about to run a BIG
1479 * cron run across a bazillion users.
1482 function load_user_accessdata($userid) {
1483 global $ACCESS,$CFG;
1485 $base = '/'.SYSCONTEXTID;
1487 $accessdata = get_user_access_sitewide($userid);
1488 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
1490 // provide "default role" & set 'dr'
1492 if (!empty($CFG->defaultuserroleid)) {
1493 $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1494 if (!isset($accessdata['ra'][$base])) {
1495 $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1496 } else {
1497 array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1499 $accessdata['dr'] = $CFG->defaultuserroleid;
1503 // provide "default frontpage role"
1505 if (!empty($CFG->defaultfrontpageroleid)) {
1506 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
1507 $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
1508 if (!isset($accessdata['ra'][$base])) {
1509 $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
1510 } else {
1511 array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
1514 // for dirty timestamps in cron
1515 $accessdata['time'] = time();
1517 $ACCESS[$userid] = $accessdata;
1518 compact_rdefs($ACCESS[$userid]['rdef']);
1520 return true;
1524 * Use shared copy of role definistions stored in $RDEFS;
1525 * @param array $rdefs array of role definitions in contexts
1527 function compact_rdefs(&$rdefs) {
1528 global $RDEFS;
1531 * This is a basic sharing only, we could also
1532 * use md5 sums of values. The main purpose is to
1533 * reduce mem in cron jobs - many users in $ACCESS array.
1536 foreach ($rdefs as $key => $value) {
1537 if (!array_key_exists($key, $RDEFS)) {
1538 $RDEFS[$key] = $rdefs[$key];
1540 $rdefs[$key] =& $RDEFS[$key];
1545 * A convenience function to completely load all the capabilities
1546 * for the current user. This is what gets called from complete_user_login()
1547 * for example. Call it only _after_ you've setup $USER and called
1548 * check_enrolment_plugins();
1551 function load_all_capabilities() {
1552 global $USER, $CFG, $DIRTYCONTEXTS;
1554 $base = '/'.SYSCONTEXTID;
1556 if (isguestuser()) {
1557 $guest = get_guest_role();
1559 // Load the rdefs
1560 $USER->access = get_role_access($guest->id);
1561 // Put the ghost enrolment in place...
1562 $USER->access['ra'][$base] = array($guest->id);
1565 } else if (isloggedin()) {
1567 $accessdata = get_user_access_sitewide($USER->id);
1570 // provide "default role" & set 'dr'
1572 if (!empty($CFG->defaultuserroleid)) {
1573 $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1574 if (!isset($accessdata['ra'][$base])) {
1575 $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1576 } else {
1577 array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1579 $accessdata['dr'] = $CFG->defaultuserroleid;
1582 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
1585 // provide "default frontpage role"
1587 if (!empty($CFG->defaultfrontpageroleid)) {
1588 $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
1589 $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
1590 if (!isset($accessdata['ra'][$base])) {
1591 $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
1592 } else {
1593 array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
1596 $USER->access = $accessdata;
1598 } else if (!empty($CFG->notloggedinroleid)) {
1599 $USER->access = get_role_access($CFG->notloggedinroleid);
1600 $USER->access['ra'][$base] = array($CFG->notloggedinroleid);
1603 // Timestamp to read dirty context timestamps later
1604 $USER->access['time'] = time();
1605 $DIRTYCONTEXTS = array();
1607 // Clear to force a refresh
1608 unset($USER->mycourses);
1612 * A convenience function to completely reload all the capabilities
1613 * for the current user when roles have been updated in a relevant
1614 * context -- but PRESERVING switchroles and loginas.
1616 * That is - completely transparent to the user.
1618 * Note: rewrites $USER->access completely.
1621 function reload_all_capabilities() {
1622 global $USER,$CFG;
1624 // error_log("reloading");
1625 // copy switchroles
1626 $sw = array();
1627 if (isset($USER->access['rsw'])) {
1628 $sw = $USER->access['rsw'];
1629 // error_log(print_r($sw,1));
1632 unset($USER->access);
1633 unset($USER->mycourses);
1635 load_all_capabilities();
1637 foreach ($sw as $path => $roleid) {
1638 $context = get_record('context', 'path', $path);
1639 role_switch($roleid, $context);
1645 * Adds a temp role to an accessdata array.
1647 * Useful for the "temporary guest" access
1648 * we grant to logged-in users.
1650 * Note - assumes a course context!
1653 function load_temp_role($context, $roleid, $accessdata) {
1655 global $CFG;
1658 // Load rdefs for the role in -
1659 // - this context
1660 // - all the parents
1661 // - and below - IOWs overrides...
1664 // turn the path into a list of context ids
1665 $contexts = substr($context->path, 1); // kill leading slash
1666 $contexts = str_replace('/', ',', $contexts);
1668 $sql = "SELECT ctx.path,
1669 rc.capability, rc.permission
1670 FROM {$CFG->prefix}context ctx
1671 JOIN {$CFG->prefix}role_capabilities rc
1672 ON rc.contextid=ctx.id
1673 WHERE (ctx.id IN ($contexts)
1674 OR ctx.path LIKE '{$context->path}/%')
1675 AND rc.roleid = {$roleid}
1676 ORDER BY ctx.depth, ctx.path";
1677 $rs = get_recordset_sql($sql);
1678 while ($rd = rs_fetch_next_record($rs)) {
1679 $k = "{$rd->path}:{$roleid}";
1680 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1682 rs_close($rs);
1685 // Say we loaded everything for the course context
1686 // - which we just did - if the user gets a proper
1687 // RA in this session, this data will need to be reloaded,
1688 // but that is handled by the complete accessdata reload
1690 array_push($accessdata['loaded'], $context->path);
1693 // Add the ghost RA
1695 if (isset($accessdata['ra'][$context->path])) {
1696 array_push($accessdata['ra'][$context->path], $roleid);
1697 } else {
1698 $accessdata['ra'][$context->path] = array($roleid);
1701 return $accessdata;
1706 * Check all the login enrolment information for the given user object
1707 * by querying the enrolment plugins
1709 function check_enrolment_plugins(&$user) {
1710 global $CFG;
1712 static $inprogress; // To prevent this function being called more than once in an invocation
1714 if (!empty($inprogress[$user->id])) {
1715 return;
1718 $inprogress[$user->id] = true; // Set the flag
1720 require_once($CFG->dirroot .'/enrol/enrol.class.php');
1722 if (!($plugins = explode(',', $CFG->enrol_plugins_enabled))) {
1723 $plugins = array($CFG->enrol);
1726 foreach ($plugins as $plugin) {
1727 $enrol = enrolment_factory::factory($plugin);
1728 if (method_exists($enrol, 'setup_enrolments')) { /// Plugin supports Roles (Moodle 1.7 and later)
1729 $enrol->setup_enrolments($user);
1730 } else { /// Run legacy enrolment methods
1731 if (method_exists($enrol, 'get_student_courses')) {
1732 $enrol->get_student_courses($user);
1734 if (method_exists($enrol, 'get_teacher_courses')) {
1735 $enrol->get_teacher_courses($user);
1738 /// deal with $user->students and $user->teachers stuff
1739 unset($user->student);
1740 unset($user->teacher);
1742 unset($enrol);
1745 unset($inprogress[$user->id]); // Unset the flag
1749 * Installs the roles system.
1750 * This function runs on a fresh install as well as on an upgrade from the old
1751 * hard-coded student/teacher/admin etc. roles to the new roles system.
1753 function moodle_install_roles() {
1755 global $CFG, $db;
1757 /// Create a system wide context for assignemnt.
1758 $systemcontext = $context = get_context_instance(CONTEXT_SYSTEM);
1761 /// Create default/legacy roles and capabilities.
1762 /// (1 legacy capability per legacy role at system level).
1764 $adminrole = create_role(addslashes(get_string('administrator')), 'admin',
1765 addslashes(get_string('administratordescription')), 'moodle/legacy:admin');
1766 $coursecreatorrole = create_role(addslashes(get_string('coursecreators')), 'coursecreator',
1767 addslashes(get_string('coursecreatorsdescription')), 'moodle/legacy:coursecreator');
1768 $editteacherrole = create_role(addslashes(get_string('defaultcourseteacher')), 'editingteacher',
1769 addslashes(get_string('defaultcourseteacherdescription')), 'moodle/legacy:editingteacher');
1770 $noneditteacherrole = create_role(addslashes(get_string('noneditingteacher')), 'teacher',
1771 addslashes(get_string('noneditingteacherdescription')), 'moodle/legacy:teacher');
1772 $studentrole = create_role(addslashes(get_string('defaultcoursestudent')), 'student',
1773 addslashes(get_string('defaultcoursestudentdescription')), 'moodle/legacy:student');
1774 $guestrole = create_role(addslashes(get_string('guest')), 'guest',
1775 addslashes(get_string('guestdescription')), 'moodle/legacy:guest');
1776 $userrole = create_role(addslashes(get_string('authenticateduser')), 'user',
1777 addslashes(get_string('authenticateduserdescription')), 'moodle/legacy:user');
1779 /// Now is the correct moment to install capabilities - after creation of legacy roles, but before assigning of roles
1781 if (!assign_capability('moodle/site:doanything', CAP_ALLOW, $adminrole, $systemcontext->id)) {
1782 error('Could not assign moodle/site:doanything to the admin role');
1784 if (!update_capabilities()) {
1785 error('Had trouble upgrading the core capabilities for the Roles System');
1788 /// Look inside user_admin, user_creator, user_teachers, user_students and
1789 /// assign above new roles. If a user has both teacher and student role,
1790 /// only teacher role is assigned. The assignment should be system level.
1792 $dbtables = $db->MetaTables('TABLES');
1794 /// Set up the progress bar
1796 $usertables = array('user_admins', 'user_coursecreators', 'user_teachers', 'user_students');
1798 $totalcount = $progresscount = 0;
1799 foreach ($usertables as $usertable) {
1800 if (in_array($CFG->prefix.$usertable, $dbtables)) {
1801 $totalcount += count_records($usertable);
1805 print_progress(0, $totalcount, 5, 1, 'Processing role assignments');
1807 /// Upgrade the admins.
1808 /// Sort using id ASC, first one is primary admin.
1810 if (in_array($CFG->prefix.'user_admins', $dbtables)) {
1811 if ($rs = get_recordset_sql('SELECT * from '.$CFG->prefix.'user_admins ORDER BY ID ASC')) {
1812 while ($admin = rs_fetch_next_record($rs)) {
1813 role_assign($adminrole, $admin->userid, 0, $systemcontext->id);
1814 $progresscount++;
1815 print_progress($progresscount, $totalcount, 5, 1, 'Processing role assignments');
1817 rs_close($rs);
1819 } else {
1820 // This is a fresh install.
1824 /// Upgrade course creators.
1825 if (in_array($CFG->prefix.'user_coursecreators', $dbtables)) {
1826 if ($rs = get_recordset('user_coursecreators')) {
1827 while ($coursecreator = rs_fetch_next_record($rs)) {
1828 role_assign($coursecreatorrole, $coursecreator->userid, 0, $systemcontext->id);
1829 $progresscount++;
1830 print_progress($progresscount, $totalcount, 5, 1, 'Processing role assignments');
1832 rs_close($rs);
1837 /// Upgrade editting teachers and non-editting teachers.
1838 if (in_array($CFG->prefix.'user_teachers', $dbtables)) {
1839 if ($rs = get_recordset('user_teachers')) {
1840 while ($teacher = rs_fetch_next_record($rs)) {
1842 // removed code here to ignore site level assignments
1843 // since the contexts are separated now
1845 // populate the user_lastaccess table
1846 $access = new object();
1847 $access->timeaccess = $teacher->timeaccess;
1848 $access->userid = $teacher->userid;
1849 $access->courseid = $teacher->course;
1850 insert_record('user_lastaccess', $access);
1852 // assign the default student role
1853 $coursecontext = get_context_instance(CONTEXT_COURSE, $teacher->course); // needs cache
1854 // hidden teacher
1855 if ($teacher->authority == 0) {
1856 $hiddenteacher = 1;
1857 } else {
1858 $hiddenteacher = 0;
1861 if ($teacher->editall) { // editting teacher
1862 role_assign($editteacherrole, $teacher->userid, 0, $coursecontext->id, $teacher->timestart, $teacher->timeend, $hiddenteacher, $teacher->enrol, $teacher->timemodified);
1863 } else {
1864 role_assign($noneditteacherrole, $teacher->userid, 0, $coursecontext->id, $teacher->timestart, $teacher->timeend, $hiddenteacher, $teacher->enrol, $teacher->timemodified);
1866 $progresscount++;
1867 print_progress($progresscount, $totalcount, 5, 1, 'Processing role assignments');
1869 rs_close($rs);
1874 /// Upgrade students.
1875 if (in_array($CFG->prefix.'user_students', $dbtables)) {
1876 if ($rs = get_recordset('user_students')) {
1877 while ($student = rs_fetch_next_record($rs)) {
1879 // populate the user_lastaccess table
1880 $access = new object;
1881 $access->timeaccess = $student->timeaccess;
1882 $access->userid = $student->userid;
1883 $access->courseid = $student->course;
1884 insert_record('user_lastaccess', $access);
1886 // assign the default student role
1887 $coursecontext = get_context_instance(CONTEXT_COURSE, $student->course);
1888 role_assign($studentrole, $student->userid, 0, $coursecontext->id, $student->timestart, $student->timeend, 0, $student->enrol, $student->time);
1889 $progresscount++;
1890 print_progress($progresscount, $totalcount, 5, 1, 'Processing role assignments');
1892 rs_close($rs);
1897 /// Upgrade guest (only 1 entry).
1898 if ($guestuser = get_record('user', 'username', 'guest')) {
1899 role_assign($guestrole, $guestuser->id, 0, $systemcontext->id);
1901 print_progress($totalcount, $totalcount, 5, 1, 'Processing role assignments');
1904 /// Insert the correct records for legacy roles
1905 allow_assign($adminrole, $adminrole);
1906 allow_assign($adminrole, $coursecreatorrole);
1907 allow_assign($adminrole, $noneditteacherrole);
1908 allow_assign($adminrole, $editteacherrole);
1909 allow_assign($adminrole, $studentrole);
1910 allow_assign($adminrole, $guestrole);
1912 allow_assign($coursecreatorrole, $noneditteacherrole);
1913 allow_assign($coursecreatorrole, $editteacherrole);
1914 allow_assign($coursecreatorrole, $studentrole);
1915 allow_assign($coursecreatorrole, $guestrole);
1917 allow_assign($editteacherrole, $noneditteacherrole);
1918 allow_assign($editteacherrole, $studentrole);
1919 allow_assign($editteacherrole, $guestrole);
1921 /// Set up default allow override matrix
1922 allow_override($adminrole, $adminrole);
1923 allow_override($adminrole, $coursecreatorrole);
1924 allow_override($adminrole, $noneditteacherrole);
1925 allow_override($adminrole, $editteacherrole);
1926 allow_override($adminrole, $studentrole);
1927 allow_override($adminrole, $guestrole);
1928 allow_override($adminrole, $userrole);
1930 //See MDL-15841
1931 //allow_override($editteacherrole, $noneditteacherrole);
1932 //allow_override($editteacherrole, $studentrole);
1933 //allow_override($editteacherrole, $guestrole);
1936 /// Delete the old user tables when we are done
1938 $tables = array('user_students', 'user_teachers', 'user_coursecreators', 'user_admins');
1939 foreach ($tables as $tablename) {
1940 $table = new XMLDBTable($tablename);
1941 if (table_exists($table)) {
1942 drop_table($table);
1948 * Returns array of all legacy roles.
1950 function get_legacy_roles() {
1951 return array(
1952 'admin' => 'moodle/legacy:admin',
1953 'coursecreator' => 'moodle/legacy:coursecreator',
1954 'editingteacher' => 'moodle/legacy:editingteacher',
1955 'teacher' => 'moodle/legacy:teacher',
1956 'student' => 'moodle/legacy:student',
1957 'guest' => 'moodle/legacy:guest',
1958 'user' => 'moodle/legacy:user'
1962 function get_legacy_type($roleid) {
1963 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
1964 $legacyroles = get_legacy_roles();
1966 $result = '';
1967 foreach($legacyroles as $ltype=>$lcap) {
1968 $localoverride = get_local_override($roleid, $sitecontext->id, $lcap);
1969 if (!empty($localoverride->permission) and $localoverride->permission == CAP_ALLOW) {
1970 //choose first selected legacy capability - reset the rest
1971 if (empty($result)) {
1972 $result = $ltype;
1973 } else {
1974 unassign_capability($lcap, $roleid);
1979 return $result;
1983 * Assign the defaults found in this capabality definition to roles that have
1984 * the corresponding legacy capabilities assigned to them.
1985 * @param $legacyperms - an array in the format (example):
1986 * 'guest' => CAP_PREVENT,
1987 * 'student' => CAP_ALLOW,
1988 * 'teacher' => CAP_ALLOW,
1989 * 'editingteacher' => CAP_ALLOW,
1990 * 'coursecreator' => CAP_ALLOW,
1991 * 'admin' => CAP_ALLOW
1992 * @return boolean - success or failure.
1994 function assign_legacy_capabilities($capability, $legacyperms) {
1996 $legacyroles = get_legacy_roles();
1998 foreach ($legacyperms as $type => $perm) {
2000 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
2002 if (!array_key_exists($type, $legacyroles)) {
2003 error('Incorrect legacy role definition for type: '.$type);
2006 if ($roles = get_roles_with_capability($legacyroles[$type], CAP_ALLOW)) {
2007 foreach ($roles as $role) {
2008 // Assign a site level capability.
2009 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
2010 return false;
2015 return true;
2020 * Checks to see if a capability is a legacy capability.
2021 * @param $capabilityname
2022 * @return boolean
2024 function islegacy($capabilityname) {
2025 if (strpos($capabilityname, 'moodle/legacy') === 0) {
2026 return true;
2027 } else {
2028 return false;
2034 /**********************************
2035 * Context Manipulation functions *
2036 **********************************/
2039 * Create a new context record for use by all roles-related stuff
2040 * assumes that the caller has done the homework.
2042 * @param $level
2043 * @param $instanceid
2045 * @return object newly created context
2047 function create_context($contextlevel, $instanceid) {
2049 global $CFG;
2051 if ($contextlevel == CONTEXT_SYSTEM) {
2052 return create_system_context();
2055 $context = new object();
2056 $context->contextlevel = $contextlevel;
2057 $context->instanceid = $instanceid;
2059 // Define $context->path based on the parent
2060 // context. In other words... Who is your daddy?
2061 $basepath = '/' . SYSCONTEXTID;
2062 $basedepth = 1;
2064 $result = true;
2066 switch ($contextlevel) {
2067 case CONTEXT_COURSECAT:
2068 $sql = "SELECT ctx.path, ctx.depth
2069 FROM {$CFG->prefix}context ctx
2070 JOIN {$CFG->prefix}course_categories cc
2071 ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
2072 WHERE cc.id={$instanceid}";
2073 if ($p = get_record_sql($sql)) {
2074 $basepath = $p->path;
2075 $basedepth = $p->depth;
2076 } else if ($category = get_record('course_categories', 'id', $instanceid)) {
2077 if (empty($category->parent)) {
2078 // ok - this is a top category
2079 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) {
2080 $basepath = $parent->path;
2081 $basedepth = $parent->depth;
2082 } else {
2083 // wrong parent category - no big deal, this can be fixed later
2084 $basepath = null;
2085 $basedepth = 0;
2087 } else {
2088 // incorrect category id
2089 $result = false;
2091 break;
2093 case CONTEXT_COURSE:
2094 $sql = "SELECT ctx.path, ctx.depth
2095 FROM {$CFG->prefix}context ctx
2096 JOIN {$CFG->prefix}course c
2097 ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
2098 WHERE c.id={$instanceid} AND c.id !=" . SITEID;
2099 if ($p = get_record_sql($sql)) {
2100 $basepath = $p->path;
2101 $basedepth = $p->depth;
2102 } else if ($course = get_record('course', 'id', $instanceid)) {
2103 if ($course->id == SITEID) {
2104 //ok - no parent category
2105 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) {
2106 $basepath = $parent->path;
2107 $basedepth = $parent->depth;
2108 } else {
2109 // wrong parent category of course - no big deal, this can be fixed later
2110 $basepath = null;
2111 $basedepth = 0;
2113 } else if ($instanceid == SITEID) {
2114 // no errors for missing site course during installation
2115 return false;
2116 } else {
2117 // incorrect course id
2118 $result = false;
2120 break;
2122 case CONTEXT_MODULE:
2123 $sql = "SELECT ctx.path, ctx.depth
2124 FROM {$CFG->prefix}context ctx
2125 JOIN {$CFG->prefix}course_modules cm
2126 ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
2127 WHERE cm.id={$instanceid}";
2128 if ($p = get_record_sql($sql)) {
2129 $basepath = $p->path;
2130 $basedepth = $p->depth;
2131 } else if ($cm = get_record('course_modules', 'id', $instanceid)) {
2132 if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course)) {
2133 $basepath = $parent->path;
2134 $basedepth = $parent->depth;
2135 } else {
2136 // course does not exist - modules can not exist without a course
2137 $result = false;
2139 } else {
2140 // cm does not exist
2141 $result = false;
2143 break;
2145 case CONTEXT_BLOCK:
2146 // Only non-pinned & course-page based
2147 $sql = "SELECT ctx.path, ctx.depth
2148 FROM {$CFG->prefix}context ctx
2149 JOIN {$CFG->prefix}block_instance bi
2150 ON (bi.pageid=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
2151 WHERE bi.id={$instanceid} AND bi.pagetype='course-view'";
2152 if ($p = get_record_sql($sql)) {
2153 $basepath = $p->path;
2154 $basedepth = $p->depth;
2155 } else if ($bi = get_record('block_instance', 'id', $instanceid)) {
2156 if ($bi->pagetype != 'course-view') {
2157 // ok - not a course block
2158 } else if ($parent = get_context_instance(CONTEXT_COURSE, $bi->pageid)) {
2159 $basepath = $parent->path;
2160 $basedepth = $parent->depth;
2161 } else {
2162 // parent course does not exist - course blocks can not exist without a course
2163 $result = false;
2165 } else {
2166 // block does not exist
2167 $result = false;
2169 break;
2170 case CONTEXT_USER:
2171 // default to basepath
2172 break;
2175 // if grandparents unknown, maybe rebuild_context_path() will solve it later
2176 if ($basedepth != 0) {
2177 $context->depth = $basedepth+1;
2180 if ($result and $id = insert_record('context', $context)) {
2181 // can't set the full path till we know the id!
2182 if ($basedepth != 0 and !empty($basepath)) {
2183 set_field('context', 'path', $basepath.'/'. $id, 'id', $id);
2185 return get_context_instance_by_id($id);
2187 } else {
2188 debugging('Error: could not insert new context level "'.
2189 s($contextlevel).'", instance "'.
2190 s($instanceid).'".');
2191 return false;
2196 * This hacky function is needed because we can not change system context instanceid using normal upgrade routine.
2198 function get_system_context($cache=true) {
2199 static $cached = null;
2200 if ($cache and defined('SYSCONTEXTID')) {
2201 if (is_null($cached)) {
2202 $cached = new object();
2203 $cached->id = SYSCONTEXTID;
2204 $cached->contextlevel = CONTEXT_SYSTEM;
2205 $cached->instanceid = 0;
2206 $cached->path = '/'.SYSCONTEXTID;
2207 $cached->depth = 1;
2209 return $cached;
2212 if (!$context = get_record('context', 'contextlevel', CONTEXT_SYSTEM)) {
2213 $context = new object();
2214 $context->contextlevel = CONTEXT_SYSTEM;
2215 $context->instanceid = 0;
2216 $context->depth = 1;
2217 $context->path = NULL; //not known before insert
2219 if (!$context->id = insert_record('context', $context)) {
2220 // better something than nothing - let's hope it will work somehow
2221 if (!defined('SYSCONTEXTID')) {
2222 define('SYSCONTEXTID', 1);
2224 debugging('Can not create system context');
2225 $context->id = SYSCONTEXTID;
2226 $context->path = '/'.SYSCONTEXTID;
2227 return $context;
2231 if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) {
2232 $context->instanceid = 0;
2233 $context->path = '/'.$context->id;
2234 $context->depth = 1;
2235 update_record('context', $context);
2238 if (!defined('SYSCONTEXTID')) {
2239 define('SYSCONTEXTID', $context->id);
2242 $cached = $context;
2243 return $cached;
2247 * Remove a context record and any dependent entries,
2248 * removes context from static context cache too
2249 * @param $level
2250 * @param $instanceid
2252 * @return bool properly deleted
2254 function delete_context($contextlevel, $instanceid) {
2255 global $context_cache, $context_cache_id;
2257 // do not use get_context_instance(), because the related object might not exist,
2258 // or the context does not exist yet and it would be created now
2259 if ($context = get_record('context', 'contextlevel', $contextlevel, 'instanceid', $instanceid)) {
2260 $result = delete_records('role_assignments', 'contextid', $context->id) &&
2261 delete_records('role_capabilities', 'contextid', $context->id) &&
2262 delete_records('role_names', 'contextid', $context->id) &&
2263 delete_records('context', 'id', $context->id);
2265 // do not mark dirty contexts if parents unknown
2266 if (!is_null($context->path) and $context->depth > 0) {
2267 mark_context_dirty($context->path);
2270 // purge static context cache if entry present
2271 unset($context_cache[$contextlevel][$instanceid]);
2272 unset($context_cache_id[$context->id]);
2274 return $result;
2275 } else {
2277 return true;
2282 * Precreates all contexts including all parents
2283 * @param int $contextlevel, empty means all
2284 * @param bool $buildpaths update paths and depths
2285 * @param bool $feedback show sql feedback
2286 * @return void
2288 function create_contexts($contextlevel=null, $buildpaths=true, $feedback=false) {
2289 global $CFG;
2291 //make sure system context exists
2292 $syscontext = get_system_context(false);
2294 if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT
2295 or $contextlevel == CONTEXT_COURSE
2296 or $contextlevel == CONTEXT_MODULE
2297 or $contextlevel == CONTEXT_BLOCK) {
2298 $sql = "INSERT INTO {$CFG->prefix}context (contextlevel, instanceid)
2299 SELECT ".CONTEXT_COURSECAT.", cc.id
2300 FROM {$CFG->prefix}course_categories cc
2301 WHERE NOT EXISTS (SELECT 'x'
2302 FROM {$CFG->prefix}context cx
2303 WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
2304 execute_sql($sql, $feedback);
2308 if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE
2309 or $contextlevel == CONTEXT_MODULE
2310 or $contextlevel == CONTEXT_BLOCK) {
2311 $sql = "INSERT INTO {$CFG->prefix}context (contextlevel, instanceid)
2312 SELECT ".CONTEXT_COURSE.", c.id
2313 FROM {$CFG->prefix}course c
2314 WHERE NOT EXISTS (SELECT 'x'
2315 FROM {$CFG->prefix}context cx
2316 WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
2317 execute_sql($sql, $feedback);
2321 if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE) {
2322 $sql = "INSERT INTO {$CFG->prefix}context (contextlevel, instanceid)
2323 SELECT ".CONTEXT_MODULE.", cm.id
2324 FROM {$CFG->prefix}course_modules cm
2325 WHERE NOT EXISTS (SELECT 'x'
2326 FROM {$CFG->prefix}context cx
2327 WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
2328 execute_sql($sql, $feedback);
2331 if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) {
2332 $sql = "INSERT INTO {$CFG->prefix}context (contextlevel, instanceid)
2333 SELECT ".CONTEXT_BLOCK.", bi.id
2334 FROM {$CFG->prefix}block_instance bi
2335 WHERE NOT EXISTS (SELECT 'x'
2336 FROM {$CFG->prefix}context cx
2337 WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
2338 execute_sql($sql, $feedback);
2341 if (empty($contextlevel) or $contextlevel == CONTEXT_USER) {
2342 $sql = "INSERT INTO {$CFG->prefix}context (contextlevel, instanceid)
2343 SELECT ".CONTEXT_USER.", u.id
2344 FROM {$CFG->prefix}user u
2345 WHERE u.deleted=0
2346 AND NOT EXISTS (SELECT 'x'
2347 FROM {$CFG->prefix}context cx
2348 WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
2349 execute_sql($sql, $feedback);
2353 if ($buildpaths) {
2354 build_context_path(false, $feedback);
2359 * Remove stale context records
2361 * @return bool
2363 function cleanup_contexts() {
2364 global $CFG;
2366 $sql = " SELECT c.contextlevel,
2367 c.instanceid AS instanceid
2368 FROM {$CFG->prefix}context c
2369 LEFT OUTER JOIN {$CFG->prefix}course_categories t
2370 ON c.instanceid = t.id
2371 WHERE t.id IS NULL AND c.contextlevel = " . CONTEXT_COURSECAT . "
2372 UNION
2373 SELECT c.contextlevel,
2374 c.instanceid
2375 FROM {$CFG->prefix}context c
2376 LEFT OUTER JOIN {$CFG->prefix}course t
2377 ON c.instanceid = t.id
2378 WHERE t.id IS NULL AND c.contextlevel = " . CONTEXT_COURSE . "
2379 UNION
2380 SELECT c.contextlevel,
2381 c.instanceid
2382 FROM {$CFG->prefix}context c
2383 LEFT OUTER JOIN {$CFG->prefix}course_modules t
2384 ON c.instanceid = t.id
2385 WHERE t.id IS NULL AND c.contextlevel = " . CONTEXT_MODULE . "
2386 UNION
2387 SELECT c.contextlevel,
2388 c.instanceid
2389 FROM {$CFG->prefix}context c
2390 LEFT OUTER JOIN {$CFG->prefix}user t
2391 ON c.instanceid = t.id
2392 WHERE t.id IS NULL AND c.contextlevel = " . CONTEXT_USER . "
2393 UNION
2394 SELECT c.contextlevel,
2395 c.instanceid
2396 FROM {$CFG->prefix}context c
2397 LEFT OUTER JOIN {$CFG->prefix}block_instance t
2398 ON c.instanceid = t.id
2399 WHERE t.id IS NULL AND c.contextlevel = " . CONTEXT_BLOCK . "
2400 UNION
2401 SELECT c.contextlevel,
2402 c.instanceid
2403 FROM {$CFG->prefix}context c
2404 LEFT OUTER JOIN {$CFG->prefix}groups t
2405 ON c.instanceid = t.id
2406 WHERE t.id IS NULL AND c.contextlevel = " . CONTEXT_GROUP . "
2408 if ($rs = get_recordset_sql($sql)) {
2409 begin_sql();
2410 $tx = true;
2411 while ($tx && $ctx = rs_fetch_next_record($rs)) {
2412 $tx = $tx && delete_context($ctx->contextlevel, $ctx->instanceid);
2414 rs_close($rs);
2415 if ($tx) {
2416 commit_sql();
2417 return true;
2419 rollback_sql();
2420 return false;
2421 rs_close($rs);
2423 return true;
2427 * Preloads all contexts relating to a course: course, modules, and blocks.
2429 * @param int $courseid Course ID
2430 * @return void
2432 function preload_course_contexts($courseid) {
2433 global $context_cache, $context_cache_id, $CFG;
2435 // Users can call this multiple times without doing any harm
2436 static $preloadedcourses = array();
2437 if (array_key_exists($courseid, $preloadedcourses)) {
2438 return;
2441 $sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2442 FROM {$CFG->prefix}course_modules cm
2443 JOIN {$CFG->prefix}context x ON x.instanceid=cm.id
2444 WHERE cm.course={$courseid}
2445 AND x.contextlevel=".CONTEXT_MODULE."
2447 UNION ALL
2449 SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2450 FROM {$CFG->prefix}block_instance bi
2451 JOIN {$CFG->prefix}context x ON x.instanceid=bi.id
2452 WHERE bi.pageid={$courseid}
2453 AND bi.pagetype='course-view'
2454 AND x.contextlevel=".CONTEXT_BLOCK."
2456 UNION ALL
2458 SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2459 FROM {$CFG->prefix}context x
2460 WHERE x.instanceid={$courseid}
2461 AND x.contextlevel=".CONTEXT_COURSE."";
2463 $rs = get_recordset_sql($sql);
2464 while($context = rs_fetch_next_record($rs)) {
2465 $context_cache[$context->contextlevel][$context->instanceid] = $context;
2466 $context_cache_id[$context->id] = $context;
2468 rs_close($rs);
2469 $preloadedcourses[$courseid] = true;
2473 * Get the context instance as an object. This function will create the
2474 * context instance if it does not exist yet.
2475 * @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE.
2476 * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id,
2477 * for $level = CONTEXT_MODULE, this would be $cm->id. And so on.
2478 * @return object The context object.
2480 function get_context_instance($contextlevel, $instance=0) {
2482 global $context_cache, $context_cache_id, $CFG;
2483 static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_GROUP, CONTEXT_MODULE, CONTEXT_BLOCK);
2485 if ($contextlevel === 'clearcache') {
2486 // TODO: Remove for v2.0
2487 // No longer needed, but we'll catch it to avoid erroring out on custom code.
2488 // This used to be a fix for MDL-9016
2489 // "Restoring into existing course, deleting first
2490 // deletes context and doesn't recreate it"
2491 return false;
2494 /// System context has special cache
2495 if ($contextlevel == CONTEXT_SYSTEM) {
2496 return get_system_context();
2499 /// check allowed context levels
2500 if (!in_array($contextlevel, $allowed_contexts)) {
2501 // fatal error, code must be fixed - probably typo or switched parameters
2502 error('Error: get_context_instance() called with incorrect context level "'.s($contextlevel).'"');
2505 if (!is_array($instance)) {
2506 /// Check the cache
2507 if (isset($context_cache[$contextlevel][$instance])) { // Already cached
2508 return $context_cache[$contextlevel][$instance];
2511 /// Get it from the database, or create it
2512 if (!$context = get_record('context', 'contextlevel', $contextlevel, 'instanceid', $instance)) {
2513 $context = create_context($contextlevel, $instance);
2516 /// Only add to cache if context isn't empty.
2517 if (!empty($context)) {
2518 $context_cache[$contextlevel][$instance] = $context; // Cache it for later
2519 $context_cache_id[$context->id] = $context; // Cache it for later
2522 return $context;
2526 /// ok, somebody wants to load several contexts to save some db queries ;-)
2527 $instances = $instance;
2528 $result = array();
2530 foreach ($instances as $key=>$instance) {
2531 /// Check the cache first
2532 if (isset($context_cache[$contextlevel][$instance])) { // Already cached
2533 $result[$instance] = $context_cache[$contextlevel][$instance];
2534 unset($instances[$key]);
2535 continue;
2539 if ($instances) {
2540 if (count($instances) > 1) {
2541 $instanceids = implode(',', $instances);
2542 $instanceids = "instanceid IN ($instanceids)";
2543 } else {
2544 $instance = reset($instances);
2545 $instanceids = "instanceid = $instance";
2548 if (!$contexts = get_records_sql("SELECT instanceid, id, contextlevel, path, depth
2549 FROM {$CFG->prefix}context
2550 WHERE contextlevel=$contextlevel AND $instanceids")) {
2551 $contexts = array();
2554 foreach ($instances as $instance) {
2555 if (isset($contexts[$instance])) {
2556 $context = $contexts[$instance];
2557 } else {
2558 $context = create_context($contextlevel, $instance);
2561 if (!empty($context)) {
2562 $context_cache[$contextlevel][$instance] = $context; // Cache it for later
2563 $context_cache_id[$context->id] = $context; // Cache it for later
2566 $result[$instance] = $context;
2570 return $result;
2575 * Get a context instance as an object, from a given context id.
2576 * @param mixed $id a context id or array of ids.
2577 * @return mixed object or array of the context object.
2579 function get_context_instance_by_id($id) {
2581 global $context_cache, $context_cache_id;
2583 if ($id == SYSCONTEXTID) {
2584 return get_system_context();
2587 if (isset($context_cache_id[$id])) { // Already cached
2588 return $context_cache_id[$id];
2591 if ($context = get_record('context', 'id', $id)) { // Update the cache and return
2592 $context_cache[$context->contextlevel][$context->instanceid] = $context;
2593 $context_cache_id[$context->id] = $context;
2594 return $context;
2597 return false;
2602 * Get the local override (if any) for a given capability in a role in a context
2603 * @param $roleid
2604 * @param $contextid
2605 * @param $capability
2607 function get_local_override($roleid, $contextid, $capability) {
2608 return get_record('role_capabilities', 'roleid', $roleid, 'capability', $capability, 'contextid', $contextid);
2613 /************************************
2614 * DB TABLE RELATED FUNCTIONS *
2615 ************************************/
2618 * function that creates a role
2619 * @param name - role name
2620 * @param shortname - role short name
2621 * @param description - role description
2622 * @param legacy - optional legacy capability
2623 * @return id or false
2625 function create_role($name, $shortname, $description, $legacy='') {
2627 // check for duplicate role name
2629 if ($role = get_record('role','name', $name)) {
2630 error('there is already a role with this name!');
2633 if ($role = get_record('role','shortname', $shortname)) {
2634 error('there is already a role with this shortname!');
2637 $role = new object();
2638 $role->name = $name;
2639 $role->shortname = $shortname;
2640 $role->description = $description;
2642 //find free sortorder number
2643 $role->sortorder = count_records('role');
2644 while (get_record('role','sortorder', $role->sortorder)) {
2645 $role->sortorder += 1;
2648 if (!$context = get_context_instance(CONTEXT_SYSTEM)) {
2649 return false;
2652 if ($id = insert_record('role', $role)) {
2653 if ($legacy) {
2654 assign_capability($legacy, CAP_ALLOW, $id, $context->id);
2657 /// By default, users with role:manage at site level
2658 /// should be able to assign users to this new role, and override this new role's capabilities
2660 // find all admin roles
2661 if ($adminroles = get_roles_with_capability('moodle/role:manage', CAP_ALLOW, $context)) {
2662 // foreach admin role
2663 foreach ($adminroles as $arole) {
2664 // write allow_assign and allow_overrid
2665 allow_assign($arole->id, $id);
2666 allow_override($arole->id, $id);
2670 return $id;
2671 } else {
2672 return false;
2678 * function that deletes a role and cleanups up after it
2679 * @param roleid - id of role to delete
2680 * @return success
2682 function delete_role($roleid) {
2683 global $CFG, $USER;
2684 $success = true;
2686 // mdl 10149, check if this is the last active admin role
2687 // if we make the admin role not deletable then this part can go
2689 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
2691 if ($role = get_record('role', 'id', $roleid)) {
2692 if (record_exists('role_capabilities', 'contextid', $systemcontext->id, 'roleid', $roleid, 'capability', 'moodle/site:doanything')) {
2693 // deleting an admin role
2694 $status = false;
2695 if ($adminroles = get_roles_with_capability('moodle/site:doanything', CAP_ALLOW, $systemcontext)) {
2696 foreach ($adminroles as $adminrole) {
2697 if ($adminrole->id != $roleid) {
2698 // some other admin role
2699 if (record_exists('role_assignments', 'roleid', $adminrole->id, 'contextid', $systemcontext->id)) {
2700 // found another admin role with at least 1 user assigned
2701 $status = true;
2702 break;
2707 if ($status !== true) {
2708 error ('You can not delete this role because there is no other admin roles with users assigned');
2713 // first unssign all users
2714 if (!role_unassign($roleid)) {
2715 debugging("Error while unassigning all users from role with ID $roleid!");
2716 $success = false;
2719 // cleanup all references to this role, ignore errors
2720 if ($success) {
2722 // MDL-10679 find all contexts where this role has an override
2723 $contexts = get_records_sql("SELECT contextid, contextid
2724 FROM {$CFG->prefix}role_capabilities
2725 WHERE roleid = $roleid");
2727 delete_records('role_capabilities', 'roleid', $roleid);
2729 delete_records('role_allow_assign', 'roleid', $roleid);
2730 delete_records('role_allow_assign', 'allowassign', $roleid);
2731 delete_records('role_allow_override', 'roleid', $roleid);
2732 delete_records('role_allow_override', 'allowoverride', $roleid);
2733 delete_records('role_names', 'roleid', $roleid);
2736 // finally delete the role itself
2737 // get this before the name is gone for logging
2738 $rolename = get_field('role', 'name', 'id', $roleid);
2740 if ($success and !delete_records('role', 'id', $roleid)) {
2741 debugging("Could not delete role record with ID $roleid!");
2742 $success = false;
2745 if ($success) {
2746 add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '', $USER->id);
2749 return $success;
2753 * Function to write context specific overrides, or default capabilities.
2754 * @param module - string name
2755 * @param capability - string name
2756 * @param contextid - context id
2757 * @param roleid - role id
2758 * @param permission - int 1,-1 or -1000
2759 * should not be writing if permission is 0
2761 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite=false) {
2763 global $USER;
2765 if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
2766 unassign_capability($capability, $roleid, $contextid);
2767 return true;
2770 $existing = get_record('role_capabilities', 'contextid', $contextid, 'roleid', $roleid, 'capability', $capability);
2772 if ($existing and !$overwrite) { // We want to keep whatever is there already
2773 return true;
2776 $cap = new object;
2777 $cap->contextid = $contextid;
2778 $cap->roleid = $roleid;
2779 $cap->capability = $capability;
2780 $cap->permission = $permission;
2781 $cap->timemodified = time();
2782 $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
2784 if ($existing) {
2785 $cap->id = $existing->id;
2786 return update_record('role_capabilities', $cap);
2787 } else {
2788 $c = get_record('context', 'id', $contextid);
2789 return insert_record('role_capabilities', $cap);
2794 * Unassign a capability from a role.
2795 * @param $roleid - the role id
2796 * @param $capability - the name of the capability
2797 * @return boolean - success or failure
2799 function unassign_capability($capability, $roleid, $contextid=NULL) {
2801 if (isset($contextid)) {
2802 // delete from context rel, if this is the last override in this context
2803 $status = delete_records('role_capabilities', 'capability', $capability,
2804 'roleid', $roleid, 'contextid', $contextid);
2805 } else {
2806 $status = delete_records('role_capabilities', 'capability', $capability,
2807 'roleid', $roleid);
2809 return $status;
2814 * Get the roles that have a given capability assigned to it. This function
2815 * does not resolve the actual permission of the capability. It just checks
2816 * for assignment only.
2817 * @param $capability - capability name (string)
2818 * @param $permission - optional, the permission defined for this capability
2819 * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT
2820 * @return array or role objects
2822 function get_roles_with_capability($capability, $permission=NULL, $context='') {
2824 global $CFG;
2826 if ($context) {
2827 if ($contexts = get_parent_contexts($context)) {
2828 $listofcontexts = '('.implode(',', $contexts).')';
2829 } else {
2830 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
2831 $listofcontexts = '('.$sitecontext->id.')'; // must be site
2833 $contextstr = "AND (rc.contextid = '$context->id' OR rc.contextid IN $listofcontexts)";
2834 } else {
2835 $contextstr = '';
2838 $selectroles = "SELECT r.*
2839 FROM {$CFG->prefix}role r,
2840 {$CFG->prefix}role_capabilities rc
2841 WHERE rc.capability = '$capability'
2842 AND rc.roleid = r.id $contextstr";
2844 if (isset($permission)) {
2845 $selectroles .= " AND rc.permission = '$permission'";
2847 return get_records_sql($selectroles);
2852 * This function makes a role-assignment (a role for a user or group in a particular context)
2853 * @param $roleid - the role of the id
2854 * @param $userid - userid
2855 * @param $groupid - group id
2856 * @param $contextid - id of the context
2857 * @param $timestart - time this assignment becomes effective
2858 * @param $timeend - time this assignemnt ceases to be effective
2859 * @uses $USER
2860 * @return id - new id of the assigment
2862 function role_assign($roleid, $userid, $groupid, $contextid, $timestart=0, $timeend=0, $hidden=0, $enrol='manual',$timemodified='') {
2863 global $USER, $CFG;
2865 /// Do some data validation
2867 if (empty($roleid)) {
2868 debugging('Role ID not provided');
2869 return false;
2872 if (empty($userid) && empty($groupid)) {
2873 debugging('Either userid or groupid must be provided');
2874 return false;
2877 if ($userid && !record_exists('user', 'id', $userid)) {
2878 debugging('User ID '.intval($userid).' does not exist!');
2879 return false;
2882 if ($groupid && !groups_group_exists($groupid)) {
2883 debugging('Group ID '.intval($groupid).' does not exist!');
2884 return false;
2887 if (!$context = get_context_instance_by_id($contextid)) {
2888 debugging('Context ID '.intval($contextid).' does not exist!');
2889 return false;
2892 if (($timestart and $timeend) and ($timestart > $timeend)) {
2893 debugging('The end time can not be earlier than the start time');
2894 return false;
2897 if (!$timemodified) {
2898 $timemodified = time();
2901 /// Check for existing entry
2902 if ($userid) {
2903 $ra = get_record('role_assignments', 'roleid', $roleid, 'contextid', $context->id, 'userid', $userid);
2904 } else {
2905 $ra = get_record('role_assignments', 'roleid', $roleid, 'contextid', $context->id, 'groupid', $groupid);
2908 if (empty($ra)) { // Create a new entry
2909 $ra = new object();
2910 $ra->roleid = $roleid;
2911 $ra->contextid = $context->id;
2912 $ra->userid = $userid;
2913 $ra->hidden = $hidden;
2914 $ra->enrol = $enrol;
2915 /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
2916 /// by repeating queries with the same exact parameters in a 100 secs time window
2917 $ra->timestart = round($timestart, -2);
2918 $ra->timeend = $timeend;
2919 $ra->timemodified = $timemodified;
2920 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
2922 if (!$ra->id = insert_record('role_assignments', $ra)) {
2923 return false;
2926 } else { // We already have one, just update it
2927 $ra->id = $ra->id;
2928 $ra->hidden = $hidden;
2929 $ra->enrol = $enrol;
2930 /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
2931 /// by repeating queries with the same exact parameters in a 100 secs time window
2932 $ra->timestart = round($timestart, -2);
2933 $ra->timeend = $timeend;
2934 $ra->timemodified = $timemodified;
2935 $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
2937 if (!update_record('role_assignments', $ra)) {
2938 return false;
2942 /// mark context as dirty - modules might use has_capability() in xxx_role_assing()
2943 /// again expensive, but needed
2944 mark_context_dirty($context->path);
2946 if (!empty($USER->id) && $USER->id == $userid) {
2947 /// If the user is the current user, then do full reload of capabilities too.
2948 load_all_capabilities();
2951 /// Ask all the modules if anything needs to be done for this user
2952 if ($mods = get_list_of_plugins('mod')) {
2953 foreach ($mods as $mod) {
2954 include_once($CFG->dirroot.'/mod/'.$mod.'/lib.php');
2955 $functionname = $mod.'_role_assign';
2956 if (function_exists($functionname)) {
2957 $functionname($userid, $context, $roleid);
2962 /// now handle metacourse role assignments if in course context
2963 if ($context->contextlevel == CONTEXT_COURSE) {
2964 if ($parents = get_records('course_meta', 'child_course', $context->instanceid)) {
2965 foreach ($parents as $parent) {
2966 sync_metacourse($parent->parent_course);
2971 events_trigger('role_assigned', $ra);
2973 return true;
2978 * Deletes one or more role assignments. You must specify at least one parameter.
2979 * @param $roleid
2980 * @param $userid
2981 * @param $groupid
2982 * @param $contextid
2983 * @param $enrol unassign only if enrolment type matches, NULL means anything
2984 * @return boolean - success or failure
2986 function role_unassign($roleid=0, $userid=0, $groupid=0, $contextid=0, $enrol=NULL) {
2987 global $USER, $CFG;
2988 require_once($CFG->dirroot.'/group/lib.php');
2990 $success = true;
2992 $args = array('roleid', 'userid', 'groupid', 'contextid');
2993 $select = array();
2994 foreach ($args as $arg) {
2995 if ($$arg) {
2996 $select[] = $arg.' = '.$$arg;
2999 if (!empty($enrol)) {
3000 $select[] = "enrol='$enrol'";
3003 if ($select) {
3004 if ($ras = get_records_select('role_assignments', implode(' AND ', $select))) {
3005 $mods = get_list_of_plugins('mod');
3006 foreach($ras as $ra) {
3007 $fireevent = false;
3008 /// infinite loop protection when deleting recursively
3009 if (!$ra = get_record('role_assignments', 'id', $ra->id)) {
3010 continue;
3012 if (delete_records('role_assignments', 'id', $ra->id)) {
3013 $fireevent = true;
3014 } else {
3015 $success = false;
3018 if (!$context = get_context_instance_by_id($ra->contextid)) {
3019 // strange error, not much to do
3020 continue;
3023 /* mark contexts as dirty here, because we need the refreshed
3024 * caps bellow to delete group membership and user_lastaccess!
3025 * and yes, this is very expensive for bulk operations :-(
3027 mark_context_dirty($context->path);
3029 /// If the user is the current user, then do full reload of capabilities too.
3030 if (!empty($USER->id) && $USER->id == $ra->userid) {
3031 load_all_capabilities();
3034 /// Ask all the modules if anything needs to be done for this user
3035 foreach ($mods as $mod) {
3036 include_once($CFG->dirroot.'/mod/'.$mod.'/lib.php');
3037 $functionname = $mod.'_role_unassign';
3038 if (function_exists($functionname)) {
3039 $functionname($ra->userid, $context); // watch out, $context might be NULL if something goes wrong
3043 /// now handle metacourse role unassigment and removing from goups if in course context
3044 if ($context->contextlevel == CONTEXT_COURSE) {
3046 // cleanup leftover course groups/subscriptions etc when user has
3047 // no capability to view course
3048 // this may be slow, but this is the proper way of doing it
3049 if (!has_capability('moodle/course:view', $context, $ra->userid)) {
3050 // remove from groups
3051 groups_delete_group_members($context->instanceid, $ra->userid);
3053 // delete lastaccess records
3054 delete_records('user_lastaccess', 'userid', $ra->userid, 'courseid', $context->instanceid);
3057 //unassign roles in metacourses if needed
3058 if ($parents = get_records('course_meta', 'child_course', $context->instanceid)) {
3059 foreach ($parents as $parent) {
3060 sync_metacourse($parent->parent_course);
3065 if ($fireevent) {
3066 events_trigger('role_unassigned', $ra);
3072 return $success;
3076 * A convenience function to take care of the common case where you
3077 * just want to enrol someone using the default role into a course
3079 * @param object $course
3080 * @param object $user
3081 * @param string $enrol - the plugin used to do this enrolment
3083 function enrol_into_course($course, $user, $enrol) {
3085 $timestart = time();
3086 // remove time part from the timestamp and keep only the date part
3087 $timestart = make_timestamp(date('Y', $timestart), date('m', $timestart), date('d', $timestart), 0, 0, 0);
3088 if ($course->enrolperiod) {
3089 $timeend = $timestart + $course->enrolperiod;
3090 } else {
3091 $timeend = 0;
3094 if ($role = get_default_course_role($course)) {
3096 $context = get_context_instance(CONTEXT_COURSE, $course->id);
3098 if (!role_assign($role->id, $user->id, 0, $context->id, $timestart, $timeend, 0, $enrol)) {
3099 return false;
3102 // force accessdata refresh for users visiting this context...
3103 mark_context_dirty($context->path);
3105 email_welcome_message_to_user($course, $user);
3107 add_to_log($course->id, 'course', 'enrol',
3108 'view.php?id='.$course->id, $course->id);
3110 return true;
3113 return false;
3117 * Loads the capability definitions for the component (from file). If no
3118 * capabilities are defined for the component, we simply return an empty array.
3119 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
3120 * @return array of capabilities
3122 function load_capability_def($component) {
3123 global $CFG;
3125 if ($component == 'moodle') {
3126 $defpath = $CFG->libdir.'/db/access.php';
3127 $varprefix = 'moodle';
3128 } else {
3129 $compparts = explode('/', $component);
3131 if ($compparts[0] == 'report') {
3132 $defpath = $CFG->dirroot.'/'.$CFG->admin.'/report/'.$compparts[1].'/db/access.php';
3133 $varprefix = $compparts[0].'_'.$compparts[1];
3135 } else if ($compparts[0] == 'block') {
3136 // Blocks are an exception. Blocks directory is 'blocks', and not
3137 // 'block'. So we need to jump through hoops.
3138 $defpath = $CFG->dirroot.'/'.$compparts[0].
3139 's/'.$compparts[1].'/db/access.php';
3140 $varprefix = $compparts[0].'_'.$compparts[1];
3142 } else if ($compparts[0] == 'format') {
3143 // Similar to the above, course formats are 'format' while they
3144 // are stored in 'course/format'.
3145 $defpath = $CFG->dirroot.'/course/'.$component.'/db/access.php';
3146 $varprefix = $compparts[0].'_'.$compparts[1];
3148 } else if ($compparts[0] == 'gradeimport') {
3149 $defpath = $CFG->dirroot.'/grade/import/'.$compparts[1].'/db/access.php';
3150 $varprefix = $compparts[0].'_'.$compparts[1];
3152 } else if ($compparts[0] == 'gradeexport') {
3153 $defpath = $CFG->dirroot.'/grade/export/'.$compparts[1].'/db/access.php';
3154 $varprefix = $compparts[0].'_'.$compparts[1];
3156 } else if ($compparts[0] == 'gradereport') {
3157 $defpath = $CFG->dirroot.'/grade/report/'.$compparts[1].'/db/access.php';
3158 $varprefix = $compparts[0].'_'.$compparts[1];
3160 } else if ($compparts[0] == 'coursereport') {
3161 $defpath = $CFG->dirroot.'/course/report/'.$compparts[1].'/db/access.php';
3162 $varprefix = $compparts[0].'_'.$compparts[1];
3164 } else {
3165 $defpath = $CFG->dirroot.'/'.$component.'/db/access.php';
3166 $varprefix = str_replace('/', '_', $component);
3169 $capabilities = array();
3171 if (file_exists($defpath)) {
3172 require($defpath);
3173 $capabilities = ${$varprefix.'_capabilities'};
3175 return $capabilities;
3180 * Gets the capabilities that have been cached in the database for this
3181 * component.
3182 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
3183 * @return array of capabilities
3185 function get_cached_capabilities($component='moodle') {
3186 if ($component == 'moodle') {
3187 $storedcaps = get_records_select('capabilities',
3188 "name LIKE 'moodle/%:%'");
3189 } else if ($component == 'local') {
3190 $storedcaps = get_records_select('capabilities',
3191 "name LIKE 'moodle/local:%'");
3192 } else {
3193 $storedcaps = get_records_select('capabilities',
3194 "name LIKE '$component:%'");
3196 return $storedcaps;
3200 * Returns default capabilities for given legacy role type.
3202 * @param string legacy role name
3203 * @return array
3205 function get_default_capabilities($legacyrole) {
3206 if (!$allcaps = get_records('capabilities')) {
3207 error('Error: no capabilitites defined!');
3209 $alldefs = array();
3210 $defaults = array();
3211 $components = array();
3212 foreach ($allcaps as $cap) {
3213 if (!in_array($cap->component, $components)) {
3214 $components[] = $cap->component;
3215 $alldefs = array_merge($alldefs, load_capability_def($cap->component));
3218 foreach($alldefs as $name=>$def) {
3219 if (isset($def['legacy'][$legacyrole])) {
3220 $defaults[$name] = $def['legacy'][$legacyrole];
3224 //some exceptions
3225 $defaults['moodle/legacy:'.$legacyrole] = CAP_ALLOW;
3226 if ($legacyrole == 'admin') {
3227 $defaults['moodle/site:doanything'] = CAP_ALLOW;
3229 return $defaults;
3233 * Reset role capabilitites to default according to selected legacy capability.
3234 * If several legacy caps selected, use the first from get_default_capabilities.
3235 * If no legacy selected, removes all capabilities.
3237 * @param int @roleid
3239 function reset_role_capabilities($roleid) {
3240 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
3241 $legacyroles = get_legacy_roles();
3243 $defaultcaps = array();
3244 foreach($legacyroles as $ltype=>$lcap) {
3245 $localoverride = get_local_override($roleid, $sitecontext->id, $lcap);
3246 if (!empty($localoverride->permission) and $localoverride->permission == CAP_ALLOW) {
3247 //choose first selected legacy capability
3248 $defaultcaps = get_default_capabilities($ltype);
3249 break;
3253 delete_records('role_capabilities', 'roleid', $roleid);
3254 if (!empty($defaultcaps)) {
3255 foreach($defaultcaps as $cap=>$permission) {
3256 assign_capability($cap, $permission, $roleid, $sitecontext->id);
3262 * Updates the capabilities table with the component capability definitions.
3263 * If no parameters are given, the function updates the core moodle
3264 * capabilities.
3266 * Note that the absence of the db/access.php capabilities definition file
3267 * will cause any stored capabilities for the component to be removed from
3268 * the database.
3270 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
3271 * @return boolean
3273 function update_capabilities($component='moodle') {
3275 $storedcaps = array();
3277 $filecaps = load_capability_def($component);
3278 $cachedcaps = get_cached_capabilities($component);
3279 if ($cachedcaps) {
3280 foreach ($cachedcaps as $cachedcap) {
3281 array_push($storedcaps, $cachedcap->name);
3282 // update risk bitmasks and context levels in existing capabilities if needed
3283 if (array_key_exists($cachedcap->name, $filecaps)) {
3284 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
3285 $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
3287 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
3288 $updatecap = new object();
3289 $updatecap->id = $cachedcap->id;
3290 $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
3291 if (!update_record('capabilities', $updatecap)) {
3292 return false;
3296 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
3297 $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
3299 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
3300 $updatecap = new object();
3301 $updatecap->id = $cachedcap->id;
3302 $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
3303 if (!update_record('capabilities', $updatecap)) {
3304 return false;
3311 // Are there new capabilities in the file definition?
3312 $newcaps = array();
3314 foreach ($filecaps as $filecap => $def) {
3315 if (!$storedcaps ||
3316 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
3317 if (!array_key_exists('riskbitmask', $def)) {
3318 $def['riskbitmask'] = 0; // no risk if not specified
3320 $newcaps[$filecap] = $def;
3323 // Add new capabilities to the stored definition.
3324 foreach ($newcaps as $capname => $capdef) {
3325 $capability = new object;
3326 $capability->name = $capname;
3327 $capability->captype = $capdef['captype'];
3328 $capability->contextlevel = $capdef['contextlevel'];
3329 $capability->component = $component;
3330 $capability->riskbitmask = $capdef['riskbitmask'];
3332 if (!insert_record('capabilities', $capability, false, 'id')) {
3333 return false;
3337 if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $storedcaps)){
3338 if ($rolecapabilities = get_records('role_capabilities', 'capability', $capdef['clonepermissionsfrom'])){
3339 foreach ($rolecapabilities as $rolecapability){
3340 //assign_capability will update rather than insert if capability exists
3341 if (!assign_capability($capname, $rolecapability->permission,
3342 $rolecapability->roleid, $rolecapability->contextid, true)){
3343 notify('Could not clone capabilities for '.$capname);
3347 // Do we need to assign the new capabilities to roles that have the
3348 // legacy capabilities moodle/legacy:* as well?
3349 // we ignore legacy key if we have cloned permissions
3350 } else if (isset($capdef['legacy']) && is_array($capdef['legacy']) &&
3351 !assign_legacy_capabilities($capname, $capdef['legacy'])) {
3352 notify('Could not assign legacy capabilities for '.$capname);
3355 // Are there any capabilities that have been removed from the file
3356 // definition that we need to delete from the stored capabilities and
3357 // role assignments?
3358 capabilities_cleanup($component, $filecaps);
3360 // reset static caches
3361 is_valid_capability('reset', false);
3363 return true;
3368 * Deletes cached capabilities that are no longer needed by the component.
3369 * Also unassigns these capabilities from any roles that have them.
3370 * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results'
3371 * @param $newcapdef - array of the new capability definitions that will be
3372 * compared with the cached capabilities
3373 * @return int - number of deprecated capabilities that have been removed
3375 function capabilities_cleanup($component, $newcapdef=NULL) {
3377 $removedcount = 0;
3379 if ($cachedcaps = get_cached_capabilities($component)) {
3380 foreach ($cachedcaps as $cachedcap) {
3381 if (empty($newcapdef) ||
3382 array_key_exists($cachedcap->name, $newcapdef) === false) {
3384 // Remove from capabilities cache.
3385 if (!delete_records('capabilities', 'name', $cachedcap->name)) {
3386 error('Could not delete deprecated capability '.$cachedcap->name);
3387 } else {
3388 $removedcount++;
3390 // Delete from roles.
3391 if($roles = get_roles_with_capability($cachedcap->name)) {
3392 foreach($roles as $role) {
3393 if (!unassign_capability($cachedcap->name, $role->id)) {
3394 error('Could not unassign deprecated capability '.
3395 $cachedcap->name.' from role '.$role->name);
3399 } // End if.
3402 return $removedcount;
3407 /****************
3408 * UI FUNCTIONS *
3409 ****************/
3413 * prints human readable context identifier.
3415 function print_context_name($context, $withprefix = true, $short = false) {
3417 $name = '';
3418 switch ($context->contextlevel) {
3420 case CONTEXT_SYSTEM: // by now it's a definite an inherit
3421 $name = get_string('coresystem');
3422 break;
3424 case CONTEXT_USER:
3425 if ($user = get_record('user', 'id', $context->instanceid)) {
3426 if ($withprefix){
3427 $name = get_string('user').': ';
3429 $name .= fullname($user);
3431 break;
3433 case CONTEXT_COURSECAT: // Coursecat -> coursecat or site
3434 if ($category = get_record('course_categories', 'id', $context->instanceid)) {
3435 if ($withprefix){
3436 $name = get_string('category').': ';
3438 $name .=format_string($category->name);
3440 break;
3442 case CONTEXT_COURSE: // 1 to 1 to course cat
3443 if ($context->instanceid == SITEID) {
3444 $name = get_string('frontpage', 'admin');
3445 } else {
3446 if ($course = get_record('course', 'id', $context->instanceid)) {
3447 if ($withprefix){
3448 $name = get_string('course').': ';
3450 if (!$short){
3451 $name .= format_string($course->shortname);
3452 } else {
3453 $name .= format_string($course->fullname);
3457 break;
3459 case CONTEXT_GROUP: // 1 to 1 to course
3460 if ($name = groups_get_group_name($context->instanceid)) {
3461 if ($withprefix){
3462 $name = get_string('group').': '. $name;
3465 break;
3467 case CONTEXT_MODULE: // 1 to 1 to course
3468 if ($cm = get_record('course_modules','id',$context->instanceid)) {
3469 if ($module = get_record('modules','id',$cm->module)) {
3470 if ($mod = get_record($module->name, 'id', $cm->instance)) {
3471 if ($withprefix){
3472 $name = get_string('activitymodule').': ';
3474 $name .= $mod->name;
3478 break;
3480 case CONTEXT_BLOCK: // not necessarily 1 to 1 to course
3481 if ($blockinstance = get_record('block_instance','id',$context->instanceid)) {
3482 if ($block = get_record('block','id',$blockinstance->blockid)) {
3483 global $CFG;
3484 require_once("$CFG->dirroot/blocks/moodleblock.class.php");
3485 require_once("$CFG->dirroot/blocks/$block->name/block_$block->name.php");
3486 $blockname = "block_$block->name";
3487 if ($blockobject = new $blockname()) {
3488 if ($withprefix){
3489 $name = get_string('block').': ';
3491 $name .= $blockobject->title;
3495 break;
3497 default:
3498 error ('This is an unknown context (' . $context->contextlevel . ') in print_context_name!');
3499 return false;
3502 return $name;
3507 * Extracts the relevant capabilities given a contextid.
3508 * All case based, example an instance of forum context.
3509 * Will fetch all forum related capabilities, while course contexts
3510 * Will fetch all capabilities
3511 * @param object context
3512 * @return array();
3514 * capabilities
3515 * `name` varchar(150) NOT NULL,
3516 * `captype` varchar(50) NOT NULL,
3517 * `contextlevel` int(10) NOT NULL,
3518 * `component` varchar(100) NOT NULL,
3520 function fetch_context_capabilities($context) {
3522 global $CFG;
3524 $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
3526 switch ($context->contextlevel) {
3528 case CONTEXT_SYSTEM: // all
3529 $SQL = "SELECT *
3530 FROM {$CFG->prefix}capabilities";
3531 break;
3533 case CONTEXT_USER:
3534 $extracaps = array('moodle/grade:viewall');
3535 foreach ($extracaps as $key=>$value) {
3536 $extracaps[$key]= "'$value'";
3538 $extra = implode(',', $extracaps);
3539 $SQL = "SELECT *
3540 FROM {$CFG->prefix}capabilities
3541 WHERE contextlevel = ".CONTEXT_USER."
3542 OR name IN ($extra)";
3543 break;
3545 case CONTEXT_COURSECAT: // course category context and bellow
3546 $SQL = "SELECT *
3547 FROM {$CFG->prefix}capabilities
3548 WHERE contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
3549 break;
3551 case CONTEXT_COURSE: // course context and bellow
3552 $SQL = "SELECT *
3553 FROM {$CFG->prefix}capabilities
3554 WHERE contextlevel IN (".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
3555 break;
3557 case CONTEXT_MODULE: // mod caps
3558 $cm = get_record('course_modules', 'id', $context->instanceid);
3559 $module = get_record('modules', 'id', $cm->module);
3561 $extra = "";
3562 $modfile = "$CFG->dirroot/mod/$module->name/lib.php";
3563 if (file_exists($modfile)) {
3564 include_once($modfile);
3565 $modfunction = $module->name.'_get_extra_capabilities';
3566 if (function_exists($modfunction)) {
3567 if ($extracaps = $modfunction()) {
3568 foreach ($extracaps as $key=>$value) {
3569 $extracaps[$key]= "'$value'";
3571 $extra = implode(',', $extracaps);
3572 $extra = "OR name IN ($extra)";
3577 $SQL = "SELECT *
3578 FROM {$CFG->prefix}capabilities
3579 WHERE (contextlevel = ".CONTEXT_MODULE."
3580 AND component = 'mod/$module->name')
3581 $extra";
3582 break;
3584 case CONTEXT_BLOCK: // block caps
3585 $cb = get_record('block_instance', 'id', $context->instanceid);
3586 $block = get_record('block', 'id', $cb->blockid);
3588 $extra = "";
3589 if ($blockinstance = block_instance($block->name)) {
3590 if ($extracaps = $blockinstance->get_extra_capabilities()) {
3591 foreach ($extracaps as $key=>$value) {
3592 $extracaps[$key]= "'$value'";
3594 $extra = implode(',', $extracaps);
3595 $extra = "OR name IN ($extra)";
3599 $SQL = "SELECT *
3600 FROM {$CFG->prefix}capabilities
3601 WHERE (contextlevel = ".CONTEXT_BLOCK."
3602 AND component = 'block/$block->name')
3603 $extra";
3604 break;
3606 default:
3607 return false;
3610 if (!$records = get_records_sql($SQL.' '.$sort)) {
3611 $records = array();
3614 return $records;
3619 * This function pulls out all the resolved capabilities (overrides and
3620 * defaults) of a role used in capability overrides in contexts at a given
3621 * context.
3622 * @param obj $context
3623 * @param int $roleid
3624 * @param bool self - if set to true, resolve till this level, else stop at immediate parent level
3625 * @return array
3627 function role_context_capabilities($roleid, $context, $cap='') {
3628 global $CFG;
3630 $contexts = get_parent_contexts($context);
3631 $contexts[] = $context->id;
3632 $contexts = '('.implode(',', $contexts).')';
3634 if ($cap) {
3635 $search = " AND rc.capability = '$cap' ";
3636 } else {
3637 $search = '';
3640 $SQL = "SELECT rc.*
3641 FROM {$CFG->prefix}role_capabilities rc,
3642 {$CFG->prefix}context c
3643 WHERE rc.contextid in $contexts
3644 AND rc.roleid = $roleid
3645 AND rc.contextid = c.id $search
3646 ORDER BY c.contextlevel DESC,
3647 rc.capability DESC";
3649 $capabilities = array();
3651 if ($records = get_records_sql($SQL)) {
3652 // We are traversing via reverse order.
3653 foreach ($records as $record) {
3654 // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
3655 if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
3656 $capabilities[$record->capability] = $record->permission;
3660 return $capabilities;
3664 * Recursive function which, given a context, find all parent context ids,
3665 * and return the array in reverse order, i.e. parent first, then grand
3666 * parent, etc.
3668 * @param object $context
3669 * @return array()
3671 function get_parent_contexts($context) {
3673 if ($context->path == '') {
3674 return array();
3677 $parentcontexts = substr($context->path, 1); // kill leading slash
3678 $parentcontexts = explode('/', $parentcontexts);
3679 array_pop($parentcontexts); // and remove its own id
3681 return array_reverse($parentcontexts);
3685 * Return the id of the parent of this context, or false if there is no parent (only happens if this
3686 * is the site context.)
3688 * @param object $context
3689 * @return integer the id of the parent context.
3691 function get_parent_contextid($context) {
3692 $parentcontexts = get_parent_contexts($context);
3693 if (count($parentcontexts) == 0) {
3694 return false;
3696 return array_shift($parentcontexts);
3700 * @param object $context a context object.
3701 * @return true if this context is the front page context, or a context inside it,
3702 * otherwise false.
3704 function is_inside_frontpage($context) {
3705 $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
3706 return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
3710 * Recursive function which, given a context, find all its children context ids.
3712 * When called for a course context, it will return the modules and blocks
3713 * displayed in the course page.
3715 * For course category contexts it will return categories and courses. It will
3716 * NOT recurse into courses - if you want to do that, call it on the returned
3717 * courses.
3719 * If called on a course context it _will_ populate the cache with the appropriate
3720 * contexts ;-)
3722 * @param object $context.
3723 * @return array of child records
3725 function get_child_contexts($context) {
3727 global $CFG, $context_cache;
3729 // We *MUST* populate the context_cache as the callers
3730 // will probably ask for the full record anyway soon after
3731 // soon after calling us ;-)
3733 switch ($context->contextlevel) {
3735 case CONTEXT_BLOCK:
3736 // No children.
3737 return array();
3738 break;
3740 case CONTEXT_MODULE:
3741 // No children.
3742 return array();
3743 break;
3745 case CONTEXT_GROUP:
3746 // No children.
3747 return array();
3748 break;
3750 case CONTEXT_COURSE:
3751 // Find
3752 // - module instances - easy
3753 // - groups
3754 // - blocks assigned to the course-view page explicitly - easy
3755 // - blocks pinned (note! we get all of them here, regardless of vis)
3756 $sql = " SELECT ctx.*
3757 FROM {$CFG->prefix}context ctx
3758 WHERE ctx.path LIKE '{$context->path}/%'
3759 AND ctx.contextlevel IN (".CONTEXT_MODULE.",".CONTEXT_BLOCK.")
3760 UNION
3761 SELECT ctx.*
3762 FROM {$CFG->prefix}context ctx
3763 JOIN {$CFG->prefix}groups g
3764 ON (ctx.instanceid=g.id AND ctx.contextlevel=".CONTEXT_GROUP.")
3765 WHERE g.courseid={$context->instanceid}
3766 UNION
3767 SELECT ctx.*
3768 FROM {$CFG->prefix}context ctx
3769 JOIN {$CFG->prefix}block_pinned b
3770 ON (ctx.instanceid=b.blockid AND ctx.contextlevel=".CONTEXT_BLOCK.")
3771 WHERE b.pagetype='course-view'
3773 $rs = get_recordset_sql($sql);
3774 $records = array();
3775 while ($rec = rs_fetch_next_record($rs)) {
3776 $records[$rec->id] = $rec;
3777 $context_cache[$rec->contextlevel][$rec->instanceid] = $rec;
3779 rs_close($rs);
3780 return $records;
3781 break;
3783 case CONTEXT_COURSECAT:
3784 // Find
3785 // - categories
3786 // - courses
3787 $sql = " SELECT ctx.*
3788 FROM {$CFG->prefix}context ctx
3789 WHERE ctx.path LIKE '{$context->path}/%'
3790 AND ctx.contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.")
3792 $rs = get_recordset_sql($sql);
3793 $records = array();
3794 while ($rec = rs_fetch_next_record($rs)) {
3795 $records[$rec->id] = $rec;
3796 $context_cache[$rec->contextlevel][$rec->instanceid] = $rec;
3798 rs_close($rs);
3799 return $records;
3800 break;
3802 case CONTEXT_USER:
3803 // No children.
3804 return array();
3805 break;
3807 case CONTEXT_SYSTEM:
3808 // Just get all the contexts except for CONTEXT_SYSTEM level
3809 // and hope we don't OOM in the process - don't cache
3810 $sql = 'SELECT c.*'.
3811 'FROM '.$CFG->prefix.'context c '.
3812 'WHERE contextlevel != '.CONTEXT_SYSTEM;
3814 return get_records_sql($sql);
3815 break;
3817 default:
3818 error('This is an unknown context (' . $context->contextlevel . ') in get_child_contexts!');
3819 return false;
3825 * Gets a string for sql calls, searching for stuff in this context or above
3826 * @param object $context
3827 * @return string
3829 function get_related_contexts_string($context) {
3830 if ($parents = get_parent_contexts($context)) {
3831 return (' IN ('.$context->id.','.implode(',', $parents).')');
3832 } else {
3833 return (' ='.$context->id);
3838 * Verifies if given capability installed.
3840 * @param string $capabilityname
3841 * @param bool $cached
3842 * @return book true if capability exists
3844 function is_valid_capability($capabilityname, $cached=true) {
3845 static $capsnames = null; // one request per page only
3847 if (is_null($capsnames) or !$cached) {
3848 $capsnames = get_records_menu('capabilities', '', '', '', 'name, 1');
3851 return array_key_exists($capabilityname, $capsnames);
3855 * Returns the human-readable, translated version of the capability.
3856 * Basically a big switch statement.
3857 * @param $capabilityname - e.g. mod/choice:readresponses
3859 function get_capability_string($capabilityname) {
3861 // Typical capabilityname is mod/choice:readresponses
3863 $names = split('/', $capabilityname);
3864 $stringname = $names[1]; // choice:readresponses
3865 $components = split(':', $stringname);
3866 $componentname = $components[0]; // choice
3868 switch ($names[0]) {
3869 case 'report':
3870 $string = get_string($stringname, 'report_'.$componentname);
3871 break;
3873 case 'mod':
3874 $string = get_string($stringname, $componentname);
3875 break;
3877 case 'block':
3878 $string = get_string($stringname, 'block_'.$componentname);
3879 break;
3881 case 'moodle':
3882 if ($componentname == 'local') {
3883 $string = get_string($stringname, 'local');
3884 } else {
3885 $string = get_string($stringname, 'role');
3887 break;
3889 case 'enrol':
3890 $string = get_string($stringname, 'enrol_'.$componentname);
3891 break;
3893 case 'format':
3894 $string = get_string($stringname, 'format_'.$componentname);
3895 break;
3897 case 'gradeexport':
3898 $string = get_string($stringname, 'gradeexport_'.$componentname);
3899 break;
3901 case 'gradeimport':
3902 $string = get_string($stringname, 'gradeimport_'.$componentname);
3903 break;
3905 case 'gradereport':
3906 $string = get_string($stringname, 'gradereport_'.$componentname);
3907 break;
3909 case 'coursereport':
3910 $string = get_string($stringname, 'coursereport_'.$componentname);
3911 break;
3913 default:
3914 $string = get_string($stringname);
3915 break;
3918 return $string;
3923 * This gets the mod/block/course/core etc strings.
3924 * @param $component
3925 * @param $contextlevel
3927 function get_component_string($component, $contextlevel) {
3929 switch ($contextlevel) {
3931 case CONTEXT_SYSTEM:
3932 if (preg_match('|^enrol/|', $component)) {
3933 $langname = str_replace('/', '_', $component);
3934 $string = get_string('enrolname', $langname);
3935 } else if (preg_match('|^block/|', $component)) {
3936 $langname = str_replace('/', '_', $component);
3937 $string = get_string('blockname', $langname);
3938 } else if (preg_match('|^local|', $component)) {
3939 $langname = str_replace('/', '_', $component);
3940 $string = get_string('local');
3941 } else if (preg_match('|^report/|', $component)) {
3942 $string = get_string('reports');
3943 } else {
3944 $string = get_string('coresystem');
3946 break;
3948 case CONTEXT_USER:
3949 $string = get_string('users');
3950 break;
3952 case CONTEXT_COURSECAT:
3953 $string = get_string('categories');
3954 break;
3956 case CONTEXT_COURSE:
3957 if (preg_match('|^gradeimport/|', $component)
3958 || preg_match('|^gradeexport/|', $component)
3959 || preg_match('|^gradereport/|', $component)) {
3960 $string = get_string('gradebook', 'admin');
3961 } else if (preg_match('|^coursereport/|', $component)) {
3962 $string = get_string('coursereports');
3963 } else {
3964 $string = get_string('course');
3966 break;
3968 case CONTEXT_GROUP:
3969 $string = get_string('group');
3970 break;
3972 case CONTEXT_MODULE:
3973 $string = get_string('modulename', basename($component));
3974 break;
3976 case CONTEXT_BLOCK:
3977 if( $component == 'moodle' ){
3978 $string = get_string('block');
3979 }else{
3980 $string = get_string('blockname', 'block_'.basename($component));
3982 break;
3984 default:
3985 error ('This is an unknown context $contextlevel (' . $contextlevel . ') in get_component_string!');
3986 return false;
3989 return $string;
3993 * Gets the list of roles assigned to this context and up (parents)
3994 * @param object $context
3995 * @param view - set to true when roles are pulled for display only
3996 * this is so that we can filter roles with no visible
3997 * assignment, for example, you might want to "hide" all
3998 * course creators when browsing the course participants
3999 * list.
4000 * @return array
4002 function get_roles_used_in_context($context, $view = false) {
4004 global $CFG;
4006 // filter for roles with all hidden assignments
4007 // no need to return when only pulling roles for reviewing
4008 // e.g. participants page.
4009 $hiddensql = ($view && !has_capability('moodle/role:viewhiddenassigns', $context))? ' AND ra.hidden = 0 ':'';
4010 $contextlist = get_related_contexts_string($context);
4012 $sql = "SELECT DISTINCT r.id,
4013 r.name,
4014 r.shortname,
4015 r.sortorder
4016 FROM {$CFG->prefix}role_assignments ra,
4017 {$CFG->prefix}role r
4018 WHERE r.id = ra.roleid
4019 AND ra.contextid $contextlist
4020 $hiddensql
4021 ORDER BY r.sortorder ASC";
4023 return get_records_sql($sql);
4027 * This function is used to print roles column in user profile page.
4028 * @param int userid
4029 * @param object context
4030 * @return string
4032 function get_user_roles_in_context($userid, $context, $view=true){
4033 global $CFG, $USER;
4035 $rolestring = '';
4036 $SQL = 'select * from '.$CFG->prefix.'role_assignments ra, '.$CFG->prefix.'role r where ra.userid='.$userid.' and ra.contextid='.$context->id.' and ra.roleid = r.id';
4037 $rolenames = array();
4038 if ($roles = get_records_sql($SQL)) {
4039 foreach ($roles as $userrole) {
4040 // MDL-12544, if we are in view mode and current user has no capability to view hidden assignment, skip it
4041 if ($userrole->hidden && $view && !has_capability('moodle/role:viewhiddenassigns', $context)) {
4042 continue;
4044 $rolenames[$userrole->roleid] = $userrole->name;
4047 $rolenames = role_fix_names($rolenames, $context); // Substitute aliases
4049 foreach ($rolenames as $roleid => $rolename) {
4050 $rolenames[$roleid] = '<a href="'.$CFG->wwwroot.'/user/index.php?contextid='.$context->id.'&amp;roleid='.$roleid.'">'.$rolename.'</a>';
4052 $rolestring = implode(',', $rolenames);
4054 return $rolestring;
4059 * Checks if a user can override capabilities of a particular role in this context
4060 * @param object $context
4061 * @param int targetroleid - the id of the role you want to override
4062 * @return boolean
4064 function user_can_override($context, $targetroleid) {
4066 // TODO: not needed anymore, remove in 2.0
4068 // first check if user has override capability
4069 // if not return false;
4070 if (!has_capability('moodle/role:override', $context)) {
4071 return false;
4073 // pull out all active roles of this user from this context(or above)
4074 if ($userroles = get_user_roles($context)) {
4075 foreach ($userroles as $userrole) {
4076 // if any in the role_allow_override table, then it's ok
4077 if (get_record('role_allow_override', 'roleid', $userrole->roleid, 'allowoverride', $targetroleid)) {
4078 return true;
4083 return false;
4088 * Checks if a user can assign users to a particular role in this context
4089 * @param object $context
4090 * @param int targetroleid - the id of the role you want to assign users to
4091 * @return boolean
4093 function user_can_assign($context, $targetroleid) {
4095 // first check if user has override capability
4096 // if not return false;
4097 if (!has_capability('moodle/role:assign', $context)) {
4098 return false;
4100 // pull out all active roles of this user from this context(or above)
4101 if ($userroles = get_user_roles($context)) {
4102 foreach ($userroles as $userrole) {
4103 // if any in the role_allow_override table, then it's ok
4104 if (get_record('role_allow_assign', 'roleid', $userrole->roleid, 'allowassign', $targetroleid)) {
4105 return true;
4110 return false;
4113 /** Returns all site roles in correct sort order.
4116 function get_all_roles() {
4117 return get_records('role', '', '', 'sortorder ASC');
4121 * gets all the user roles assigned in this context, or higher contexts
4122 * this is mainly used when checking if a user can assign a role, or overriding a role
4123 * i.e. we need to know what this user holds, in order to verify against allow_assign and
4124 * allow_override tables
4125 * @param object $context
4126 * @param int $userid
4127 * @param view - set to true when roles are pulled for display only
4128 * this is so that we can filter roles with no visible
4129 * assignment, for example, you might want to "hide" all
4130 * course creators when browsing the course participants
4131 * list.
4132 * @return array
4134 function get_user_roles($context, $userid=0, $checkparentcontexts=true, $order='c.contextlevel DESC, r.sortorder ASC', $view=false) {
4136 global $USER, $CFG, $db;
4138 if (empty($userid)) {
4139 if (empty($USER->id)) {
4140 return array();
4142 $userid = $USER->id;
4144 // set up hidden sql
4145 $hiddensql = ($view && !has_capability('moodle/role:viewhiddenassigns', $context))? ' AND ra.hidden = 0 ':'';
4147 if ($checkparentcontexts && ($parents = get_parent_contexts($context))) {
4148 $contexts = ' ra.contextid IN ('.implode(',' , $parents).','.$context->id.')';
4149 } else {
4150 $contexts = ' ra.contextid = \''.$context->id.'\'';
4153 if (!$return = get_records_sql('SELECT ra.*, r.name, r.shortname
4154 FROM '.$CFG->prefix.'role_assignments ra,
4155 '.$CFG->prefix.'role r,
4156 '.$CFG->prefix.'context c
4157 WHERE ra.userid = '.$userid.'
4158 AND ra.roleid = r.id
4159 AND ra.contextid = c.id
4160 AND '.$contexts . $hiddensql .'
4161 ORDER BY '.$order)) {
4162 $return = array();
4165 return $return;
4169 * Creates a record in the allow_override table
4170 * @param int sroleid - source roleid
4171 * @param int troleid - target roleid
4172 * @return int - id or false
4174 function allow_override($sroleid, $troleid) {
4175 $record = new object();
4176 $record->roleid = $sroleid;
4177 $record->allowoverride = $troleid;
4178 return insert_record('role_allow_override', $record);
4182 * Creates a record in the allow_assign table
4183 * @param int sroleid - source roleid
4184 * @param int troleid - target roleid
4185 * @return int - id or false
4187 function allow_assign($sroleid, $troleid) {
4188 $record = new object;
4189 $record->roleid = $sroleid;
4190 $record->allowassign = $troleid;
4191 return insert_record('role_allow_assign', $record);
4195 * Gets a list of roles that this user can assign in this context
4196 * @param object $context
4197 * @param string $field
4198 * @param int $rolenamedisplay
4199 * @return array
4201 function get_assignable_roles($context, $field='name', $rolenamedisplay=ROLENAME_ALIAS) {
4202 global $USER, $CFG;
4204 if (!has_capability('moodle/role:assign', $context)) {
4205 return array();
4208 $parents = get_parent_contexts($context);
4209 $parents[] = $context->id;
4210 $contexts = implode(',' , $parents);
4212 if (!$roles = get_records_sql("SELECT ro.*
4213 FROM {$CFG->prefix}role ro,
4215 SELECT DISTINCT r.id
4216 FROM {$CFG->prefix}role r,
4217 {$CFG->prefix}role_assignments ra,
4218 {$CFG->prefix}role_allow_assign raa
4219 WHERE ra.userid = $USER->id AND ra.contextid IN ($contexts)
4220 AND raa.roleid = ra.roleid AND r.id = raa.allowassign
4221 ) inline_view
4222 WHERE ro.id = inline_view.id
4223 ORDER BY ro.sortorder ASC")) {
4224 return array();
4227 foreach ($roles as $role) {
4228 $roles[$role->id] = $role->$field;
4231 return role_fix_names($roles, $context, $rolenamedisplay);
4235 * Gets a list of roles that this user can assign in this context, for the switchrole menu
4237 * @param object $context
4238 * @param string $field
4239 * @param int $rolenamedisplay
4240 * @return array
4242 function get_assignable_roles_for_switchrole($context, $field='name', $rolenamedisplay=ROLENAME_ALIAS) {
4243 global $USER, $CFG;
4245 if (!has_capability('moodle/role:assign', $context)) {
4246 return array();
4249 $parents = get_parent_contexts($context);
4250 $parents[] = $context->id;
4251 $contexts = implode(',' , $parents);
4253 if (!$roles = get_records_sql("SELECT ro.*
4254 FROM {$CFG->prefix}role ro,
4256 SELECT DISTINCT r.id
4257 FROM {$CFG->prefix}role r,
4258 {$CFG->prefix}role_assignments ra,
4259 {$CFG->prefix}role_allow_assign raa,
4260 {$CFG->prefix}role_capabilities rc
4261 WHERE ra.userid = $USER->id AND ra.contextid IN ($contexts)
4262 AND raa.roleid = ra.roleid AND r.id = raa.allowassign
4263 AND r.id = rc.roleid AND rc.capability = 'moodle/course:view' AND rc.capability != 'moodle/site:doanything'
4264 ) inline_view
4265 WHERE ro.id = inline_view.id
4266 ORDER BY ro.sortorder ASC")) {
4267 return array();
4270 foreach ($roles as $role) {
4271 $roles[$role->id] = $role->$field;
4274 return role_fix_names($roles, $context, $rolenamedisplay);
4278 * Gets a list of roles that this user can override or safeoverride in this context
4279 * @param object $context
4280 * @param string $field
4281 * @param int $rolenamedisplay
4282 * @return array
4284 function get_overridable_roles($context, $field='name', $rolenamedisplay=ROLENAME_ALIAS) {
4285 global $USER, $CFG;
4287 if (!has_capability('moodle/role:override', $context) and !has_capability('moodle/role:safeoverride', $context)) {
4288 return array();
4291 $parents = get_parent_contexts($context);
4292 $parents[] = $context->id;
4293 $contexts = implode(',' , $parents);
4295 if (!$roles = get_records_sql("SELECT ro.*
4296 FROM {$CFG->prefix}role ro,
4298 SELECT DISTINCT r.id
4299 FROM {$CFG->prefix}role r,
4300 {$CFG->prefix}role_assignments ra,
4301 {$CFG->prefix}role_allow_override rao
4302 WHERE ra.userid = $USER->id AND ra.contextid IN ($contexts)
4303 AND rao.roleid = ra.roleid AND r.id = rao.allowoverride
4304 ) inline_view
4305 WHERE ro.id = inline_view.id
4306 ORDER BY ro.sortorder ASC")) {
4307 return array();
4310 foreach ($roles as $role) {
4311 $roles[$role->id] = $role->$field;
4314 return role_fix_names($roles, $context, $rolenamedisplay);
4318 * Returns a role object that is the default role for new enrolments
4319 * in a given course
4321 * @param object $course
4322 * @return object $role
4324 function get_default_course_role($course) {
4325 global $CFG;
4327 /// First let's take the default role the course may have
4328 if (!empty($course->defaultrole)) {
4329 if ($role = get_record('role', 'id', $course->defaultrole)) {
4330 return $role;
4334 /// Otherwise the site setting should tell us
4335 if ($CFG->defaultcourseroleid) {
4336 if ($role = get_record('role', 'id', $CFG->defaultcourseroleid)) {
4337 return $role;
4341 /// It's unlikely we'll get here, but just in case, try and find a student role
4342 if ($studentroles = get_roles_with_capability('moodle/legacy:student', CAP_ALLOW)) {
4343 return array_shift($studentroles); /// Take the first one
4346 return NULL;
4351 * Who has this capability in this context?
4353 * This can be a very expensive call - use sparingly and keep
4354 * the results if you are going to need them again soon.
4356 * Note if $fields is empty this function attempts to get u.*
4357 * which can get rather large - and has a serious perf impact
4358 * on some DBs.
4360 * @param $context - object
4361 * @param $capability - string capability, or an array of capabilities, in which
4362 * case users having any of those capabilities will be returned.
4363 * For performance reasons, you are advised to put the capability
4364 * that the user is most likely to have first.
4365 * @param $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
4366 * @param $sort - the sort order. Default is lastaccess time.
4367 * @param $limitfrom - number of records to skip (offset)
4368 * @param $limitnum - number of records to fetch
4369 * @param $groups - single group or array of groups - only return
4370 * users who are in one of these group(s).
4371 * @param $exceptions - list of users to exclude
4372 * @param view - set to true when roles are pulled for display only
4373 * this is so that we can filter roles with no visible
4374 * assignment, for example, you might want to "hide" all
4375 * course creators when browsing the course participants
4376 * list.
4377 * @param boolean $useviewallgroups if $groups is set the return users who
4378 * have capability both $capability and moodle/site:accessallgroups
4379 * in this context, as well as users who have $capability and who are
4380 * in $groups.
4382 function get_users_by_capability($context, $capability, $fields='', $sort='',
4383 $limitfrom='', $limitnum='', $groups='', $exceptions='', $doanything=true,
4384 $view=false, $useviewallgroups=false) {
4385 global $CFG;
4387 $ctxids = substr($context->path, 1); // kill leading slash
4388 $ctxids = str_replace('/', ',', $ctxids);
4390 // Context is the frontpage
4391 $isfrontpage = false;
4392 $iscoursepage = false; // coursepage other than fp
4393 if ($context->contextlevel == CONTEXT_COURSE) {
4394 if ($context->instanceid == SITEID) {
4395 $isfrontpage = true;
4396 } else {
4397 $iscoursepage = true;
4401 // What roles/rolecaps are interesting?
4402 if (is_array($capability)) {
4403 $caps = "'" . implode("','", $capability) . "'";
4404 $capabilities = $capability;
4405 } else {
4406 $caps = "'" . $capability . "'";
4407 $capabilities = array($capability);
4409 if ($doanything===true) {
4410 $caps .= ",'moodle/site:doanything'";
4411 $capabilities[] = 'moodle/site:doanything';
4412 $doanything_join='';
4413 $doanything_cond='';
4414 } else {
4415 // This is an outer join against
4416 // admin-ish roleids. Any row that succeeds
4417 // in JOINing here ends up removed from
4418 // the resultset. This means we remove
4419 // rolecaps from roles that also have
4420 // 'doanything' capabilities.
4421 $doanything_join="LEFT OUTER JOIN (
4422 SELECT DISTINCT rc.roleid
4423 FROM {$CFG->prefix}role_capabilities rc
4424 WHERE rc.capability='moodle/site:doanything'
4425 AND rc.permission=".CAP_ALLOW."
4426 AND rc.contextid IN ($ctxids)
4427 ) dar
4428 ON rc.roleid=dar.roleid";
4429 $doanything_cond="AND dar.roleid IS NULL";
4432 // fetch all capability records - we'll walk several
4433 // times over them, and should be a small set
4435 $negperm = false; // has any negative (<0) permission?
4436 $roleids = array();
4438 $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability,
4439 ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel
4440 FROM {$CFG->prefix}role_capabilities rc
4441 JOIN {$CFG->prefix}context ctx on rc.contextid = ctx.id
4442 $doanything_join
4443 WHERE rc.capability IN ($caps) AND ctx.id IN ($ctxids)
4444 $doanything_cond
4445 ORDER BY rc.roleid ASC, ctx.depth ASC";
4446 if ($capdefs = get_records_sql($sql)) {
4447 foreach ($capdefs AS $rcid=>$rc) {
4448 $roleids[] = (int)$rc->roleid;
4449 if ($rc->permission < 0) {
4450 $negperm = true;
4455 $roleids = array_unique($roleids);
4457 if (count($roleids)===0) { // noone here!
4458 return false;
4461 // is the default role interesting? does it have
4462 // a relevant rolecap? (we use this a lot later)
4463 if (in_array((int)$CFG->defaultuserroleid, $roleids, true)) {
4464 $defaultroleinteresting = true;
4465 } else {
4466 $defaultroleinteresting = false;
4470 // Prepare query clauses
4472 $wherecond = array();
4474 // Non-deleted users. We never return deleted users.
4475 $wherecond['nondeleted'] = 'u.deleted = 0';
4477 /// Groups
4478 if ($groups) {
4479 if (is_array($groups)) {
4480 $grouptest = 'gm.groupid IN (' . implode(',', $groups) . ')';
4481 } else {
4482 $grouptest = 'gm.groupid = ' . $groups;
4484 $grouptest = 'ra.userid IN (SELECT userid FROM ' .
4485 $CFG->prefix . 'groups_members gm WHERE ' . $grouptest . ')';
4487 if ($useviewallgroups) {
4488 $viewallgroupsusers = get_users_by_capability($context,
4489 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
4490 $wherecond['groups'] = '('. $grouptest . ' OR ra.userid IN (' .
4491 implode(',', array_keys($viewallgroupsusers)) . '))';
4492 } else {
4493 $wherecond['groups'] = '(' . $grouptest .')';
4497 /// User exceptions
4498 if (!empty($exceptions)) {
4499 $wherecond['userexceptions'] = ' u.id NOT IN ('.$exceptions.')';
4502 /// Set up hidden role-assignments sql
4503 if ($view && !has_capability('moodle/role:viewhiddenassigns', $context)) {
4504 $condhiddenra = 'AND ra.hidden = 0 ';
4505 $sscondhiddenra = 'AND ssra.hidden = 0 ';
4506 } else {
4507 $condhiddenra = '';
4508 $sscondhiddenra = '';
4511 // Collect WHERE conditions
4512 $where = implode(' AND ', array_values($wherecond));
4513 if ($where != '') {
4514 $where = 'WHERE ' . $where;
4517 /// Set up default fields
4518 if (empty($fields)) {
4519 if ($iscoursepage) {
4520 $fields = 'u.*, ul.timeaccess as lastaccess';
4521 } else {
4522 $fields = 'u.*';
4524 } else {
4525 if (debugging('', DEBUG_DEVELOPER) && strpos($fields, 'u.*') === false &&
4526 strpos($fields, 'u.id') === false) {
4527 debugging('u.id must be included in the list of fields passed to get_users_by_capability.', DEBUG_DEVELOPER);
4531 /// Set up default sort
4532 if (empty($sort)) { // default to course lastaccess or just lastaccess
4533 if ($iscoursepage) {
4534 $sort = 'ul.timeaccess';
4535 } else {
4536 $sort = 'u.lastaccess';
4539 $sortby = $sort ? " ORDER BY $sort " : '';
4541 // User lastaccess JOIN
4542 if ((strpos($sort, 'ul.timeaccess') === FALSE) and (strpos($fields, 'ul.timeaccess') === FALSE)) { // user_lastaccess is not required MDL-13810
4543 $uljoin = '';
4544 } else {
4545 $uljoin = "LEFT OUTER JOIN {$CFG->prefix}user_lastaccess ul
4546 ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
4550 // Simple cases - No negative permissions means we can take shortcuts
4552 if (!$negperm) {
4554 // at the frontpage, and all site users have it - easy!
4555 if ($isfrontpage && !empty($CFG->defaultfrontpageroleid)
4556 && in_array((int)$CFG->defaultfrontpageroleid, $roleids, true)) {
4558 return get_records_sql("SELECT $fields
4559 FROM {$CFG->prefix}user u
4560 WHERE u.deleted = 0
4561 ORDER BY $sort",
4562 $limitfrom, $limitnum);
4565 // all site users have it, anyway
4566 // TODO: NOT ALWAYS! Check this case because this gets run for cases like this:
4567 // 1) Default role has the permission for a module thing like mod/choice:choose
4568 // 2) We are checking for an activity module context in a course
4569 // 3) Thus all users are returned even though course:view is also required
4570 if ($defaultroleinteresting) {
4571 $sql = "SELECT $fields
4572 FROM {$CFG->prefix}user u
4573 $uljoin
4574 $where
4575 ORDER BY $sort";
4576 return get_records_sql($sql, $limitfrom, $limitnum);
4579 /// Simple SQL assuming no negative rolecaps.
4580 /// We use a subselect to grab the role assignments
4581 /// ensuring only one row per user -- even if they
4582 /// have many "relevant" role assignments.
4583 $select = " SELECT $fields";
4584 $from = " FROM {$CFG->prefix}user u
4585 JOIN (SELECT DISTINCT ssra.userid
4586 FROM {$CFG->prefix}role_assignments ssra
4587 WHERE ssra.contextid IN ($ctxids)
4588 AND ssra.roleid IN (".implode(',',$roleids) .")
4589 $sscondhiddenra
4590 ) ra ON ra.userid = u.id
4591 $uljoin ";
4592 return get_records_sql($select.$from.$where.$sortby, $limitfrom, $limitnum);
4596 // If there are any negative rolecaps, we need to
4597 // work through a subselect that will bring several rows
4598 // per user (one per RA).
4599 // Since we cannot do the job in pure SQL (not without SQL stored
4600 // procedures anyway), we end up tied to processing the data in PHP
4601 // all the way down to pagination.
4603 // In some cases, this will mean bringing across a ton of data --
4604 // when paginating, we have to walk the permisisons of all the rows
4605 // in the _previous_ pages to get the pagination correct in the case
4606 // of users that end up not having the permission - this removed.
4609 // Prepare the role permissions datastructure for fast lookups
4610 $roleperms = array(); // each role cap and depth
4611 foreach ($capdefs AS $rcid=>$rc) {
4613 $rid = (int)$rc->roleid;
4614 $perm = (int)$rc->permission;
4615 $rcdepth = (int)$rc->ctxdepth;
4616 if (!isset($roleperms[$rc->capability][$rid])) {
4617 $roleperms[$rc->capability][$rid] = (object)array('perm' => $perm,
4618 'rcdepth' => $rcdepth);
4619 } else {
4620 if ($roleperms[$rc->capability][$rid]->perm == CAP_PROHIBIT) {
4621 continue;
4623 // override - as we are going
4624 // from general to local perms
4625 // (as per the ORDER BY...depth ASC above)
4626 // and local perms win...
4627 $roleperms[$rc->capability][$rid] = (object)array('perm' => $perm,
4628 'rcdepth' => $rcdepth);
4633 if ($context->contextlevel == CONTEXT_SYSTEM
4634 || $isfrontpage
4635 || $defaultroleinteresting) {
4637 // Handle system / sitecourse / defaultrole-with-perhaps-neg-overrides
4638 // with a SELECT FROM user LEFT OUTER JOIN against ra -
4639 // This is expensive on the SQL and PHP sides -
4640 // moves a ton of data across the wire.
4641 $ss = "SELECT u.id as userid, ra.roleid,
4642 ctx.depth
4643 FROM {$CFG->prefix}user u
4644 LEFT OUTER JOIN {$CFG->prefix}role_assignments ra
4645 ON (ra.userid = u.id
4646 AND ra.contextid IN ($ctxids)
4647 AND ra.roleid IN (".implode(',',$roleids) .")
4648 $condhiddenra)
4649 LEFT OUTER JOIN {$CFG->prefix}context ctx
4650 ON ra.contextid=ctx.id
4651 WHERE u.deleted=0";
4652 } else {
4653 // "Normal complex case" - the rolecaps we are after will
4654 // be defined in a role assignment somewhere.
4655 $ss = "SELECT ra.userid as userid, ra.roleid,
4656 ctx.depth
4657 FROM {$CFG->prefix}role_assignments ra
4658 JOIN {$CFG->prefix}context ctx
4659 ON ra.contextid=ctx.id
4660 WHERE ra.contextid IN ($ctxids)
4661 $condhiddenra
4662 AND ra.roleid IN (".implode(',',$roleids) .")";
4665 $select = "SELECT $fields ,ra.roleid, ra.depth ";
4666 $from = "FROM ($ss) ra
4667 JOIN {$CFG->prefix}user u
4668 ON ra.userid=u.id
4669 $uljoin ";
4671 // Each user's entries MUST come clustered together
4672 // and RAs ordered in depth DESC - the role/cap resolution
4673 // code depends on this.
4674 $sort .= ' , ra.userid ASC, ra.depth DESC';
4675 $sortby .= ' , ra.userid ASC, ra.depth DESC ';
4677 $rs = get_recordset_sql($select.$from.$where.$sortby);
4680 // Process the user accounts+RAs, folding repeats together...
4682 // The processing for this recordset is tricky - to fold
4683 // the role/perms of users with multiple role-assignments
4684 // correctly while still processing one-row-at-a-time
4685 // we need to add a few additional 'private' fields to
4686 // the results array - so we can treat the rows as a
4687 // state machine to track the cap/perms and at what RA-depth
4688 // and RC-depth they were defined.
4690 // So what we do here is:
4691 // - loop over rows, checking pagination limits
4692 // - when we find a new user, if we are in the page add it to the
4693 // $results, and start building $ras array with its role-assignments
4694 // - when we are dealing with the next user, or are at the end of the userlist
4695 // (last rec or last in page), trigger the check-permission idiom
4696 // - the check permission idiom will
4697 // - add the default enrolment if needed
4698 // - call has_any_capability_from_rarc(), which based on RAs and RCs will return a bool
4699 // (should be fairly tight code ;-) )
4700 // - if the user has permission, all is good, just $c++ (counter)
4701 // - ...else, decrease the counter - so pagination is kept straight,
4702 // and (if we are in the page) remove from the results
4704 $results = array();
4706 // pagination controls
4707 $c = 0;
4708 $limitfrom = (int)$limitfrom;
4709 $limitnum = (int)$limitnum;
4712 // Track our last user id so we know when we are dealing
4713 // with a new user...
4715 $lastuserid = 0;
4717 // In this loop, we
4718 // $ras: role assignments, multidimensional array
4719 // treat as a stack - going from local to general
4720 // $ras = (( roleid=> x, $depth=>y) , ( roleid=> x, $depth=>y))
4722 while ($user = rs_fetch_next_record($rs)) {
4724 //error_log(" Record: " . print_r($user,1));
4727 // Pagination controls
4728 // Note that we might end up removing a user
4729 // that ends up _not_ having the rights,
4730 // therefore rolling back $c
4732 if ($lastuserid != $user->id) {
4734 // Did the last user end up with a positive permission?
4735 if ($lastuserid !=0) {
4736 if ($defaultroleinteresting) {
4737 // add the role at the end of $ras
4738 $ras[] = array( 'roleid' => $CFG->defaultuserroleid,
4739 'depth' => 1 );
4741 if (has_any_capability_from_rarc($ras, $roleperms, $capabilities)) {
4742 $c++;
4743 } else {
4744 // remove the user from the result set,
4745 // only if we are 'in the page'
4746 if ($limitfrom === 0 || $c >= $limitfrom) {
4747 unset($results[$lastuserid]);
4752 // Did we hit pagination limit?
4753 if ($limitnum !==0 && $c >= ($limitfrom+$limitnum)) { // we are done!
4754 break;
4757 // New user setup, and $ras reset
4758 $lastuserid = $user->id;
4759 $ras = array();
4760 if (!empty($user->roleid)) {
4761 $ras[] = array( 'roleid' => (int)$user->roleid,
4762 'depth' => (int)$user->depth );
4765 // if we are 'in the page', also add the rec
4766 // to the results...
4767 if ($limitfrom === 0 || $c >= $limitfrom) {
4768 $results[$user->id] = $user; // trivial
4770 } else {
4771 // Additional RA for $lastuserid
4772 $ras[] = array( 'roleid'=>(int)$user->roleid,
4773 'depth'=>(int)$user->depth );
4776 } // end while(fetch)
4778 // Prune last entry if necessary
4779 if ($lastuserid !=0) {
4780 if ($defaultroleinteresting) {
4781 // add the role at the end of $ras
4782 $ras[] = array( 'roleid' => $CFG->defaultuserroleid,
4783 'depth' => 1 );
4785 if (!has_any_capability_from_rarc($ras, $roleperms, $capabilities)) {
4786 // remove the user from the result set,
4787 // only if we are 'in the page'
4788 if ($limitfrom === 0 || $c >= $limitfrom) {
4789 if (isset($results[$lastuserid])) {
4790 unset($results[$lastuserid]);
4796 return $results;
4800 * Fast (fast!) utility function to resolve if any of a list of capabilities is
4801 * granted, based on Role Assignments and Role Capabilities.
4803 * Used (at least) by get_users_by_capability().
4805 * If PHP had fast built-in memoize functions, we could
4806 * add a $contextid parameter and memoize the return values.
4808 * Note that this function must be kept in synch with has_capability_in_accessdata.
4810 * @param array $ras - role assignments
4811 * @param array $roleperms - role permissions
4812 * @param string $capabilities - array of capability names
4813 * @return boolean
4815 function has_any_capability_from_rarc($ras, $roleperms, $caps) {
4816 // Mini-state machine, using $hascap
4817 // $hascap[ 'moodle/foo:bar' ]->perm = CAP_SOMETHING (numeric constant)
4818 // $hascap[ 'moodle/foo:bar' ]->radepth = depth of the role assignment that set it
4819 // $hascap[ 'moodle/foo:bar' ]->rcdepth = depth of the rolecap that set it
4820 // -- when resolving conflicts, we need to look into radepth first, if unresolved
4822 $hascap = array();
4825 // Compute which permission/roleassignment/rolecap
4826 // wins for each capability we are walking
4828 foreach ($ras as $ra) {
4829 foreach ($caps as $cap) {
4830 if (!isset($roleperms[$cap][$ra['roleid']])) {
4831 // nothing set for this cap - skip
4832 continue;
4834 // We explicitly clone here as we
4835 // add more properties to it
4836 // that must stay separate from the
4837 // original roleperm data structure
4838 $rp = clone($roleperms[$cap][$ra['roleid']]);
4839 $rp->radepth = $ra['depth'];
4841 // Trivial case, we are the first to set
4842 if (!isset($hascap[$cap])) {
4843 $hascap[$cap] = $rp;
4847 // Resolve who prevails, in order of precendence
4848 // - Prohibits always wins
4849 // - Locality of RA
4850 // - Locality of RC
4852 //// Prohibits...
4853 if ($rp->perm === CAP_PROHIBIT) {
4854 $hascap[$cap] = $rp;
4855 continue;
4857 if ($hascap[$cap]->perm === CAP_PROHIBIT) {
4858 continue;
4861 // Locality of RA - the look is ordered by depth DESC
4862 // so from local to general -
4863 // Higher RA loses to local RA... unless perm===0
4864 /// Thanks to the order of the records, $rp->radepth <= $hascap[$cap]->radepth
4865 if ($rp->radepth > $hascap[$cap]->radepth) {
4866 error_log('Should not happen @ ' . __FUNCTION__.':'.__LINE__);
4868 if ($rp->radepth < $hascap[$cap]->radepth) {
4869 if ($hascap[$cap]->perm!==0) {
4870 // Wider RA loses to local RAs...
4871 continue;
4872 } else {
4873 // "Higher RA resolves conflict" case,
4874 // local RAs had cancelled eachother
4875 $hascap[$cap] = $rp;
4876 continue;
4879 // Same ralevel - locality of RC wins
4880 if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
4881 $hascap[$cap] = $rp;
4882 continue;
4884 if ($rp->rcdepth > $hascap[$cap]->rcdepth) {
4885 continue;
4887 // We match depth - add them
4888 $hascap[$cap]->perm += $rp->perm;
4891 foreach ($caps as $capability) {
4892 if (isset($hascap[$capability]) && $hascap[$capability]->perm > 0) {
4893 return true;
4896 return false;
4900 * Will re-sort a $users results array (from get_users_by_capability(), usually)
4901 * based on a sorting policy. This is to support the odd practice of
4902 * sorting teachers by 'authority', where authority was "lowest id of the role
4903 * assignment".
4905 * Will execute 1 database query. Only suitable for small numbers of users, as it
4906 * uses an u.id IN() clause.
4908 * Notes about the sorting criteria.
4910 * As a default, we cannot rely on role.sortorder because then
4911 * admins/coursecreators will always win. That is why the sane
4912 * rule "is locality matters most", with sortorder as 2nd
4913 * consideration.
4915 * If you want role.sortorder, use the 'sortorder' policy, and
4916 * name explicitly what roles you want to cover. It's probably
4917 * a good idea to see what roles have the capabilities you want
4918 * (array_diff() them against roiles that have 'can-do-anything'
4919 * to weed out admin-ish roles. Or fetch a list of roles from
4920 * variables like $CFG->coursemanagers .
4922 * @param array users Users' array, keyed on userid
4923 * @param object context
4924 * @param array roles - ids of the roles to include, optional
4925 * @param string policy - defaults to locality, more about
4926 * @return array - sorted copy of the array
4928 function sort_by_roleassignment_authority($users, $context, $roles=array(), $sortpolicy='locality') {
4929 global $CFG;
4931 $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
4932 $contextwhere = ' ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
4933 if (empty($roles)) {
4934 $roleswhere = '';
4935 } else {
4936 $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
4939 $sql = "SELECT ra.userid
4940 FROM {$CFG->prefix}role_assignments ra
4941 JOIN {$CFG->prefix}role r
4942 ON ra.roleid=r.id
4943 JOIN {$CFG->prefix}context ctx
4944 ON ra.contextid=ctx.id
4945 WHERE
4946 $userswhere
4947 AND $contextwhere
4948 $roleswhere
4951 // Default 'locality' policy -- read PHPDoc notes
4952 // about sort policies...
4953 $orderby = 'ORDER BY
4954 ctx.depth DESC, /* locality wins */
4955 r.sortorder ASC, /* rolesorting 2nd criteria */
4956 ra.id /* role assignment order tie-breaker */';
4957 if ($sortpolicy === 'sortorder') {
4958 $orderby = 'ORDER BY
4959 r.sortorder ASC, /* rolesorting 2nd criteria */
4960 ra.id /* role assignment order tie-breaker */';
4963 $sortedids = get_fieldset_sql($sql . $orderby);
4964 $sortedusers = array();
4965 $seen = array();
4967 foreach ($sortedids as $id) {
4968 // Avoid duplicates
4969 if (isset($seen[$id])) {
4970 continue;
4972 $seen[$id] = true;
4974 // assign
4975 $sortedusers[$id] = $users[$id];
4977 return $sortedusers;
4981 * gets all the users assigned this role in this context or higher
4982 * @param int roleid (can also be an array of ints!)
4983 * @param int contextid
4984 * @param bool parent if true, get list of users assigned in higher context too
4985 * @param string fields - fields from user (u.) , role assignment (ra) or role (r.)
4986 * @param string sort - sort from user (u.) , role assignment (ra) or role (r.)
4987 * @param bool gethidden - whether to fetch hidden enrolments too
4988 * @return array()
4990 function get_role_users($roleid, $context, $parent=false, $fields='', $sort='u.lastname ASC', $gethidden=true, $group='', $limitfrom='', $limitnum='') {
4991 global $CFG;
4993 if (empty($fields)) {
4994 $fields = 'u.id, u.confirmed, u.username, u.firstname, u.lastname, '.
4995 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.city, '.
4996 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
4997 'u.emailstop, u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name as rolename';
5000 // whether this assignment is hidden
5001 $hiddensql = $gethidden ? '': ' AND ra.hidden = 0 ';
5003 $parentcontexts = '';
5004 if ($parent) {
5005 $parentcontexts = substr($context->path, 1); // kill leading slash
5006 $parentcontexts = str_replace('/', ',', $parentcontexts);
5007 if ($parentcontexts !== '') {
5008 $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
5012 if (is_array($roleid)) {
5013 $roleselect = ' AND ra.roleid IN (' . implode(',',$roleid) .')';
5014 } elseif (!empty($roleid)) { // should not test for int, because it can come in as a string
5015 $roleselect = "AND ra.roleid = $roleid";
5016 } else {
5017 $roleselect = '';
5020 if ($group) {
5021 $groupjoin = "JOIN {$CFG->prefix}groups_members gm
5022 ON gm.userid = u.id";
5023 $groupselect = " AND gm.groupid = $group ";
5024 } else {
5025 $groupjoin = '';
5026 $groupselect = '';
5029 $SQL = "SELECT $fields, ra.roleid
5030 FROM {$CFG->prefix}role_assignments ra
5031 JOIN {$CFG->prefix}user u
5032 ON u.id = ra.userid
5033 JOIN {$CFG->prefix}role r
5034 ON ra.roleid = r.id
5035 $groupjoin
5036 WHERE (ra.contextid = $context->id $parentcontexts)
5037 $roleselect
5038 $groupselect
5039 $hiddensql
5040 ORDER BY $sort
5041 "; // join now so that we can just use fullname() later
5043 return get_records_sql($SQL, $limitfrom, $limitnum);
5047 * Counts all the users assigned this role in this context or higher
5048 * @param int roleid (can also be an array of ints!)
5049 * @param int contextid
5050 * @param bool parent if true, get list of users assigned in higher context too
5051 * @return array()
5053 function count_role_users($roleid, $context, $parent=false) {
5054 global $CFG;
5056 if ($parent) {
5057 if ($contexts = get_parent_contexts($context)) {
5058 $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
5059 } else {
5060 $parentcontexts = '';
5062 } else {
5063 $parentcontexts = '';
5066 $rolesql = '';
5067 if (is_numeric($roleid)) {
5068 $rolesql = "AND r.roleid = $roleid";
5070 else if (is_array($roleid)) {
5071 $rolesql = "AND r.roleid IN (" . implode(',', $roleid) . ")";
5074 $SQL = "SELECT count(u.id)
5075 FROM {$CFG->prefix}role_assignments r
5076 JOIN {$CFG->prefix}user u
5077 ON u.id = r.userid
5078 WHERE (r.contextid = $context->id $parentcontexts)
5079 $rolesql
5080 AND u.deleted = 0";
5082 return count_records_sql($SQL);
5086 * This function gets the list of courses that this user has a particular capability in.
5087 * It is still not very efficient.
5088 * @param string $capability Capability in question
5089 * @param int $userid User ID or null for current user
5090 * @param bool $doanything True if 'doanything' is permitted (default)
5091 * @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
5092 * otherwise use a comma-separated list of the fields you require, not including id
5093 * @param string $orderby If set, use a comma-separated list of fields from course
5094 * table with sql modifiers (DESC) if needed
5095 * @return array Array of courses, may have zero entries. Or false if query failed.
5097 function get_user_capability_course($capability, $userid=NULL,$doanything=true,$fieldsexceptid='',$orderby='') {
5098 // Convert fields list and ordering
5099 $fieldlist='';
5100 if($fieldsexceptid) {
5101 $fields=explode(',',$fieldsexceptid);
5102 foreach($fields as $field) {
5103 $fieldlist.=',c.'.$field;
5106 if($orderby) {
5107 $fields=explode(',',$orderby);
5108 $orderby='';
5109 foreach($fields as $field) {
5110 if($orderby) {
5111 $orderby.=',';
5113 $orderby.='c.'.$field;
5115 $orderby='ORDER BY '.$orderby;
5118 // Obtain a list of everything relevant about all courses including context.
5119 // Note the result can be used directly as a context (we are going to), the course
5120 // fields are just appended.
5121 global $CFG;
5122 $rs=get_recordset_sql("
5123 SELECT
5124 x.*,c.id AS courseid$fieldlist
5125 FROM
5126 {$CFG->prefix}course c
5127 INNER JOIN {$CFG->prefix}context x ON c.id=x.instanceid AND x.contextlevel=".CONTEXT_COURSE."
5128 $orderby
5130 if(!$rs) {
5131 return false;
5134 // Check capability for each course in turn
5135 $courses=array();
5136 while($coursecontext=rs_fetch_next_record($rs)) {
5137 if(has_capability($capability,$coursecontext,$userid,$doanything)) {
5138 // We've got the capability. Make the record look like a course record
5139 // and store it
5140 $coursecontext->id=$coursecontext->courseid;
5141 unset($coursecontext->courseid);
5142 unset($coursecontext->contextlevel);
5143 unset($coursecontext->instanceid);
5144 $courses[]=$coursecontext;
5147 return $courses;
5150 /** This function finds the roles assigned directly to this context only
5151 * i.e. no parents role
5152 * @param object $context
5153 * @return array
5155 function get_roles_on_exact_context($context) {
5157 global $CFG;
5159 return get_records_sql("SELECT r.*
5160 FROM {$CFG->prefix}role_assignments ra,
5161 {$CFG->prefix}role r
5162 WHERE ra.roleid = r.id
5163 AND ra.contextid = $context->id");
5168 * Switches the current user to another role for the current session and only
5169 * in the given context.
5171 * The caller *must* check
5172 * - that this op is allowed
5173 * - that the requested role can be assigned in this ctx
5174 * (hint, use get_assignable_roles_for_switchrole())
5175 * - that the requested role is NOT $CFG->defaultuserroleid
5177 * To "unswitch" pass 0 as the roleid.
5179 * This function *will* modify $USER->access - beware
5181 * @param integer $roleid
5182 * @param object $context
5183 * @return bool
5185 function role_switch($roleid, $context) {
5186 global $USER, $CFG;
5189 // Plan of action
5191 // - Add the ghost RA to $USER->access
5192 // as $USER->access['rsw'][$path] = $roleid
5194 // - Make sure $USER->access['rdef'] has the roledefs
5195 // it needs to honour the switcheroo
5197 // Roledefs will get loaded "deep" here - down to the last child
5198 // context. Note that
5200 // - When visiting subcontexts, our selective accessdata loading
5201 // will still work fine - though those ra/rdefs will be ignored
5202 // appropriately while the switch is in place
5204 // - If a switcheroo happens at a category with tons of courses
5205 // (that have many overrides for switched-to role), the session
5206 // will get... quite large. Sometimes you just can't win.
5208 // To un-switch just unset($USER->access['rsw'][$path])
5211 // Add the switch RA
5212 if (!isset($USER->access['rsw'])) {
5213 $USER->access['rsw'] = array();
5216 if ($roleid == 0) {
5217 unset($USER->access['rsw'][$context->path]);
5218 if (empty($USER->access['rsw'])) {
5219 unset($USER->access['rsw']);
5221 return true;
5224 $USER->access['rsw'][$context->path]=$roleid;
5226 // Load roledefs
5227 $USER->access = get_role_access_bycontext($roleid, $context,
5228 $USER->access);
5230 /* DO WE NEED THIS AT ALL???
5231 // Add some permissions we are really going
5232 // to always need, even if the role doesn't have them!
5234 $USER->capabilities[$context->id]['moodle/course:view'] = CAP_ALLOW;
5237 return true;
5241 // get any role that has an override on exact context
5242 function get_roles_with_override_on_context($context) {
5244 global $CFG;
5246 return get_records_sql("SELECT r.*
5247 FROM {$CFG->prefix}role_capabilities rc,
5248 {$CFG->prefix}role r
5249 WHERE rc.roleid = r.id
5250 AND rc.contextid = $context->id");
5253 // get all capabilities for this role on this context (overrids)
5254 function get_capabilities_from_role_on_context($role, $context) {
5256 global $CFG;
5258 return get_records_sql("SELECT *
5259 FROM {$CFG->prefix}role_capabilities
5260 WHERE contextid = $context->id
5261 AND roleid = $role->id");
5264 // find out which roles has assignment on this context
5265 function get_roles_with_assignment_on_context($context) {
5267 global $CFG;
5269 return get_records_sql("SELECT r.*
5270 FROM {$CFG->prefix}role_assignments ra,
5271 {$CFG->prefix}role r
5272 WHERE ra.roleid = r.id
5273 AND ra.contextid = $context->id");
5279 * Find all user assignemnt of users for this role, on this context
5281 function get_users_from_role_on_context($role, $context) {
5283 global $CFG;
5285 return get_records_sql("SELECT *
5286 FROM {$CFG->prefix}role_assignments
5287 WHERE contextid = $context->id
5288 AND roleid = $role->id");
5292 * Simple function returning a boolean true if roles exist, otherwise false
5294 function user_has_role_assignment($userid, $roleid, $contextid=0) {
5296 if ($contextid) {
5297 return record_exists('role_assignments', 'userid', $userid, 'roleid', $roleid, 'contextid', $contextid);
5298 } else {
5299 return record_exists('role_assignments', 'userid', $userid, 'roleid', $roleid);
5304 * Get role name or alias if exists and format the text.
5305 * @param object $role role object
5306 * @param object $coursecontext
5307 * @return $string name of role in course context
5309 function role_get_name($role, $coursecontext) {
5310 if ($r = get_record('role_names','roleid', $role->id,'contextid', $coursecontext->id)) {
5311 return strip_tags(format_string($r->name));
5312 } else {
5313 return strip_tags(format_string($role->name));
5318 * Prepare list of roles for display, apply aliases and format text
5319 * @param array $roleoptions array roleid=>rolename
5320 * @param object $context
5321 * @return array of role names
5323 function role_fix_names($roleoptions, $context, $rolenamedisplay=ROLENAME_ALIAS) {
5324 if ($rolenamedisplay != ROLENAME_ORIGINAL && !empty($context->id)) {
5325 if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_BLOCK) { // find the parent course context
5326 if ($parentcontextid = array_shift(get_parent_contexts($context))) {
5327 $context = get_context_instance_by_id($parentcontextid);
5330 if ($aliasnames = get_records('role_names', 'contextid', $context->id)) {
5331 if ($rolenamedisplay == ROLENAME_ALIAS) {
5332 foreach ($aliasnames as $alias) {
5333 if (isset($roleoptions[$alias->roleid])) {
5334 $roleoptions[$alias->roleid] = format_string($alias->name);
5337 } else if ($rolenamedisplay == ROLENAME_BOTH) {
5338 foreach ($aliasnames as $alias) {
5339 if (isset($roleoptions[$alias->roleid])) {
5340 $roleoptions[$alias->roleid] = format_string($alias->name).' ('.format_string($roleoptions[$alias->roleid]).')';
5346 foreach ($roleoptions as $rid => $name) {
5347 $roleoptions[$rid] = strip_tags(format_string($name));
5349 return $roleoptions;
5353 * This function helps admin/roles/manage.php etc to detect if a new line should be printed
5354 * when we read in a new capability
5355 * most of the time, if the 2 components are different we should print a new line, (e.g. course system->rss client)
5356 * but when we are in grade, all reports/import/export capabilites should be together
5357 * @param string a - component string a
5358 * @param string b - component string b
5359 * @return bool - whether 2 component are in different "sections"
5361 function component_level_changed($cap, $comp, $contextlevel) {
5363 if ($cap->component == 'enrol/authorize' && $comp =='enrol/authorize') {
5364 return false;
5367 if (strstr($cap->component, '/') && strstr($comp, '/')) {
5368 $compsa = explode('/', $cap->component);
5369 $compsb = explode('/', $comp);
5371 // list of system reports
5372 if (($compsa[0] == 'report') && ($compsb[0] == 'report')) {
5373 return false;
5376 // we are in gradebook, still
5377 if (($compsa[0] == 'gradeexport' || $compsa[0] == 'gradeimport' || $compsa[0] == 'gradereport') &&
5378 ($compsb[0] == 'gradeexport' || $compsb[0] == 'gradeimport' || $compsb[0] == 'gradereport')) {
5379 return false;
5382 if (($compsa[0] == 'coursereport') && ($compsb[0] == 'coursereport')) {
5383 return false;
5387 return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
5391 * Populate context.path and context.depth where missing.
5392 * @param bool $force force a complete rebuild of the path and depth fields.
5393 * @param bool $feedback display feedback (during upgrade usually)
5394 * @return void
5396 function build_context_path($force=false, $feedback=false) {
5397 global $CFG;
5398 require_once($CFG->libdir.'/ddllib.php');
5400 // System context
5401 $sitectx = get_system_context(!$force);
5402 $base = '/'.$sitectx->id;
5404 // Sitecourse
5405 $sitecoursectx = get_record('context',
5406 'contextlevel', CONTEXT_COURSE,
5407 'instanceid', SITEID);
5408 if ($force || $sitecoursectx->path !== "$base/{$sitecoursectx->id}") {
5409 set_field('context', 'path', "$base/{$sitecoursectx->id}",
5410 'id', $sitecoursectx->id);
5411 set_field('context', 'depth', 2,
5412 'id', $sitecoursectx->id);
5413 $sitecoursectx = get_record('context',
5414 'contextlevel', CONTEXT_COURSE,
5415 'instanceid', SITEID);
5418 $ctxemptyclause = " AND (ctx.path IS NULL
5419 OR ctx.depth=0) ";
5420 $emptyclause = " AND ({$CFG->prefix}context.path IS NULL
5421 OR {$CFG->prefix}context.depth=0) ";
5422 if ($force) {
5423 $ctxemptyclause = $emptyclause = '';
5426 /* MDL-11347:
5427 * - mysql does not allow to use FROM in UPDATE statements
5428 * - using two tables after UPDATE works in mysql, but might give unexpected
5429 * results in pg 8 (depends on configuration)
5430 * - using table alias in UPDATE does not work in pg < 8.2
5432 if ($CFG->dbfamily == 'mysql') {
5433 $updatesql = "UPDATE {$CFG->prefix}context ct, {$CFG->prefix}context_temp temp
5434 SET ct.path = temp.path,
5435 ct.depth = temp.depth
5436 WHERE ct.id = temp.id";
5437 } else if ($CFG->dbfamily == 'oracle') {
5438 $updatesql = "UPDATE {$CFG->prefix}context ct
5439 SET (ct.path, ct.depth) =
5440 (SELECT temp.path, temp.depth
5441 FROM {$CFG->prefix}context_temp temp
5442 WHERE temp.id=ct.id)
5443 WHERE EXISTS (SELECT 'x'
5444 FROM {$CFG->prefix}context_temp temp
5445 WHERE temp.id = ct.id)";
5446 } else {
5447 $updatesql = "UPDATE {$CFG->prefix}context
5448 SET path = temp.path,
5449 depth = temp.depth
5450 FROM {$CFG->prefix}context_temp temp
5451 WHERE temp.id={$CFG->prefix}context.id";
5454 $udelsql = "TRUNCATE TABLE {$CFG->prefix}context_temp";
5456 // Top level categories
5457 $sql = "UPDATE {$CFG->prefix}context
5458 SET depth=2, path=" . sql_concat("'$base/'", 'id') . "
5459 WHERE contextlevel=".CONTEXT_COURSECAT."
5460 AND EXISTS (SELECT 'x'
5461 FROM {$CFG->prefix}course_categories cc
5462 WHERE cc.id = {$CFG->prefix}context.instanceid
5463 AND cc.depth=1)
5464 $emptyclause";
5466 execute_sql($sql, $feedback);
5468 execute_sql($udelsql, $feedback);
5470 // Deeper categories - one query per depthlevel
5471 $maxdepth = get_field_sql("SELECT MAX(depth)
5472 FROM {$CFG->prefix}course_categories");
5473 for ($n=2;$n<=$maxdepth;$n++) {
5474 $sql = "INSERT INTO {$CFG->prefix}context_temp (id, path, depth)
5475 SELECT ctx.id, ".sql_concat('pctx.path', "'/'", 'ctx.id').", $n+1
5476 FROM {$CFG->prefix}context ctx
5477 JOIN {$CFG->prefix}course_categories c ON ctx.instanceid=c.id
5478 JOIN {$CFG->prefix}context pctx ON c.parent=pctx.instanceid
5479 WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
5480 AND pctx.contextlevel=".CONTEXT_COURSECAT."
5481 AND c.depth=$n
5482 AND NOT EXISTS (SELECT 'x'
5483 FROM {$CFG->prefix}context_temp temp
5484 WHERE temp.id = ctx.id)
5485 $ctxemptyclause";
5486 execute_sql($sql, $feedback);
5488 // this is needed after every loop
5489 // MDL-11532
5490 execute_sql($updatesql, $feedback);
5491 execute_sql($udelsql, $feedback);
5494 // Courses -- except sitecourse
5495 $sql = "INSERT INTO {$CFG->prefix}context_temp (id, path, depth)
5496 SELECT ctx.id, ".sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
5497 FROM {$CFG->prefix}context ctx
5498 JOIN {$CFG->prefix}course c ON ctx.instanceid=c.id
5499 JOIN {$CFG->prefix}context pctx ON c.category=pctx.instanceid
5500 WHERE ctx.contextlevel=".CONTEXT_COURSE."
5501 AND c.id!=".SITEID."
5502 AND pctx.contextlevel=".CONTEXT_COURSECAT."
5503 AND NOT EXISTS (SELECT 'x'
5504 FROM {$CFG->prefix}context_temp temp
5505 WHERE temp.id = ctx.id)
5506 $ctxemptyclause";
5507 execute_sql($sql, $feedback);
5509 execute_sql($updatesql, $feedback);
5510 execute_sql($udelsql, $feedback);
5512 // Module instances
5513 $sql = "INSERT INTO {$CFG->prefix}context_temp (id, path, depth)
5514 SELECT ctx.id, ".sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
5515 FROM {$CFG->prefix}context ctx
5516 JOIN {$CFG->prefix}course_modules cm ON ctx.instanceid=cm.id
5517 JOIN {$CFG->prefix}context pctx ON cm.course=pctx.instanceid
5518 WHERE ctx.contextlevel=".CONTEXT_MODULE."
5519 AND pctx.contextlevel=".CONTEXT_COURSE."
5520 AND NOT EXISTS (SELECT 'x'
5521 FROM {$CFG->prefix}context_temp temp
5522 WHERE temp.id = ctx.id)
5523 $ctxemptyclause";
5524 execute_sql($sql, $feedback);
5526 execute_sql($updatesql, $feedback);
5527 execute_sql($udelsql, $feedback);
5529 // Blocks - non-pinned course-view only
5530 $sql = "INSERT INTO {$CFG->prefix}context_temp (id, path, depth)
5531 SELECT ctx.id, ".sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
5532 FROM {$CFG->prefix}context ctx
5533 JOIN {$CFG->prefix}block_instance bi ON ctx.instanceid = bi.id
5534 JOIN {$CFG->prefix}context pctx ON bi.pageid=pctx.instanceid
5535 WHERE ctx.contextlevel=".CONTEXT_BLOCK."
5536 AND pctx.contextlevel=".CONTEXT_COURSE."
5537 AND bi.pagetype='course-view'
5538 AND NOT EXISTS (SELECT 'x'
5539 FROM {$CFG->prefix}context_temp temp
5540 WHERE temp.id = ctx.id)
5541 $ctxemptyclause";
5542 execute_sql($sql, $feedback);
5544 execute_sql($updatesql, $feedback);
5545 execute_sql($udelsql, $feedback);
5547 // Blocks - others
5548 $sql = "UPDATE {$CFG->prefix}context
5549 SET depth=2, path=".sql_concat("'$base/'", 'id')."
5550 WHERE contextlevel=".CONTEXT_BLOCK."
5551 AND EXISTS (SELECT 'x'
5552 FROM {$CFG->prefix}block_instance bi
5553 WHERE bi.id = {$CFG->prefix}context.instanceid
5554 AND bi.pagetype!='course-view')
5555 $emptyclause ";
5556 execute_sql($sql, $feedback);
5558 // User
5559 $sql = "UPDATE {$CFG->prefix}context
5560 SET depth=2, path=".sql_concat("'$base/'", 'id')."
5561 WHERE contextlevel=".CONTEXT_USER."
5562 AND EXISTS (SELECT 'x'
5563 FROM {$CFG->prefix}user u
5564 WHERE u.id = {$CFG->prefix}context.instanceid)
5565 $emptyclause ";
5566 execute_sql($sql, $feedback);
5568 // Personal TODO
5570 //TODO: fix group contexts
5572 // reset static course cache - it might have incorrect cached data
5573 global $context_cache, $context_cache_id;
5574 $context_cache = array();
5575 $context_cache_id = array();
5580 * Update the path field of the context and
5581 * all the dependent subcontexts that follow
5582 * the move.
5584 * The most important thing here is to be as
5585 * DB efficient as possible. This op can have a
5586 * massive impact in the DB.
5588 * @param obj current context obj
5589 * @param obj newparent new parent obj
5592 function context_moved($context, $newparent) {
5593 global $CFG;
5595 $frompath = $context->path;
5596 $newpath = $newparent->path . '/' . $context->id;
5598 $setdepth = '';
5599 if (($newparent->depth +1) != $context->depth) {
5600 $setdepth = ", depth= depth + ({$newparent->depth} - {$context->depth}) + 1";
5602 $sql = "UPDATE {$CFG->prefix}context
5603 SET path='$newpath'
5604 $setdepth
5605 WHERE path='$frompath'";
5606 execute_sql($sql,false);
5608 $len = strlen($frompath);
5609 /// MDL-16655 - Substring MSSQL function *requires* 3rd parameter
5610 $substr3rdparam = '';
5611 if ($CFG->dbfamily == 'mssql') {
5612 $substr3rdparam = ', len(path)';
5614 $sql = "UPDATE {$CFG->prefix}context
5615 SET path = ".sql_concat("'$newpath'", sql_substr() .'(path, '.$len.' +1'.$substr3rdparam.')')."
5616 $setdepth
5617 WHERE path LIKE '{$frompath}/%'";
5618 execute_sql($sql,false);
5620 mark_context_dirty($frompath);
5621 mark_context_dirty($newpath);
5626 * Turn the ctx* fields in an objectlike record
5627 * into a context subobject. This allows
5628 * us to SELECT from major tables JOINing with
5629 * context at no cost, saving a ton of context
5630 * lookups...
5632 function make_context_subobj($rec) {
5633 $ctx = new StdClass;
5634 $ctx->id = $rec->ctxid; unset($rec->ctxid);
5635 $ctx->path = $rec->ctxpath; unset($rec->ctxpath);
5636 $ctx->depth = $rec->ctxdepth; unset($rec->ctxdepth);
5637 $ctx->contextlevel = $rec->ctxlevel; unset($rec->ctxlevel);
5638 $ctx->instanceid = $rec->id;
5640 $rec->context = $ctx;
5641 return $rec;
5645 * Do some basic, quick checks to see whether $rec->context looks like a
5646 * valid context object.
5648 * @param object $rec a think that has a context, for example a course,
5649 * course category, course modules, etc.
5650 * @param integer $contextlevel the type of thing $rec is, one of the CONTEXT_... constants.
5651 * @return boolean whether $rec->context looks like the correct context object
5652 * for this thing.
5654 function is_context_subobj_valid($rec, $contextlevel) {
5655 return isset($rec->context) && isset($rec->context->id) &&
5656 isset($rec->context->path) && isset($rec->context->depth) &&
5657 isset($rec->context->contextlevel) && isset($rec->context->instanceid) &&
5658 $rec->context->contextlevel == $contextlevel && $rec->context->instanceid == $rec->id;
5662 * When you have a record (for example a $category, $course, $user or $cm that may,
5663 * or may not, have come from a place that does make_context_subobj, you can use
5664 * this method to ensure that $rec->context is present and correct before you continue.
5666 * @param object $rec a thing that has an associated context.
5667 * @param integer $contextlevel the type of thing $rec is, one of the CONTEXT_... constants.
5669 function ensure_context_subobj_present(&$rec, $contextlevel) {
5670 if (!is_context_subobj_valid($rec, $contextlevel)) {
5671 $rec->context = get_context_instance($contextlevel, $rec->id);
5676 * Fetch recent dirty contexts to know cheaply whether our $USER->access
5677 * is stale and needs to be reloaded.
5679 * Uses cache_flags
5680 * @param int $time
5681 * @return array of dirty contexts
5683 function get_dirty_contexts($time) {
5684 return get_cache_flags('accesslib/dirtycontexts', $time-2);
5688 * Mark a context as dirty (with timestamp)
5689 * so as to force reloading of the context.
5690 * @param string $path context path
5692 function mark_context_dirty($path) {
5693 global $CFG, $DIRTYCONTEXTS;
5694 // only if it is a non-empty string
5695 if (is_string($path) && $path !== '') {
5696 set_cache_flag('accesslib/dirtycontexts', $path, 1, time()+$CFG->sessiontimeout);
5697 if (isset($DIRTYCONTEXTS)) {
5698 $DIRTYCONTEXTS[$path] = 1;
5704 * Will walk the contextpath to answer whether
5705 * the contextpath is dirty
5707 * @param array $contexts array of strings
5708 * @param obj/array dirty contexts from get_dirty_contexts()
5709 * @return bool
5711 function is_contextpath_dirty($pathcontexts, $dirty) {
5712 $path = '';
5713 foreach ($pathcontexts as $ctx) {
5714 $path = $path.'/'.$ctx;
5715 if (isset($dirty[$path])) {
5716 return true;
5719 return false;
5724 * switch role order (used in admin/roles/manage.php)
5726 * @param int $first id of role to move down
5727 * @param int $second id of role to move up
5729 * @return bool success or failure
5731 function switch_roles($first, $second) {
5732 $status = true;
5733 //first find temorary sortorder number
5734 $tempsort = count_records('role') + 3;
5735 while (get_record('role','sortorder', $tempsort)) {
5736 $tempsort += 3;
5739 $r1 = new object();
5740 $r1->id = $first->id;
5741 $r1->sortorder = $tempsort;
5742 $r2 = new object();
5743 $r2->id = $second->id;
5744 $r2->sortorder = $first->sortorder;
5746 if (!update_record('role', $r1)) {
5747 debugging("Can not update role with ID $r1->id!");
5748 $status = false;
5751 if (!update_record('role', $r2)) {
5752 debugging("Can not update role with ID $r2->id!");
5753 $status = false;
5756 $r1->sortorder = $second->sortorder;
5757 if (!update_record('role', $r1)) {
5758 debugging("Can not update role with ID $r1->id!");
5759 $status = false;
5762 return $status;
5766 * duplicates all the base definitions of a role
5768 * @param object $sourcerole role to copy from
5769 * @param int $targetrole id of role to copy to
5771 * @return void
5773 function role_cap_duplicate($sourcerole, $targetrole) {
5774 global $CFG;
5775 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
5776 $caps = get_records_sql("SELECT * FROM {$CFG->prefix}role_capabilities
5777 WHERE roleid = $sourcerole->id
5778 AND contextid = $systemcontext->id");
5779 // adding capabilities
5780 foreach ($caps as $cap) {
5781 unset($cap->id);
5782 $cap->roleid = $targetrole;
5783 insert_record('role_capabilities', $cap);