MDL-19118, comments api improvement
[moodle.git] / lib / accesslib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * This file contains functions for managing user access
20  *
21  * <b>Public API vs internals</b>
22  *
23  * General users probably only care about
24  *
25  * Context handling
26  * - get_context_instance()
27  * - get_context_instance_by_id()
28  * - get_parent_contexts()
29  * - get_child_contexts()
30  *
31  * Whether the user can do something...
32  * - has_capability()
33  * - has_any_capability()
34  * - has_all_capabilities()
35  * - require_capability()
36  * - require_login() (from moodlelib)
37  *
38  * What courses has this user access to?
39  * - get_user_courses_bycap()
40  *
41  * What users can do X in this context?
42  * - get_users_by_capability()
43  *
44  * Enrol/unenrol
45  * - enrol_into_course()
46  * - role_assign()/role_unassign()
47  *
48  *
49  * Advanced use
50  * - load_all_capabilities()
51  * - reload_all_capabilities()
52  * - has_capability_in_accessdata()
53  * - is_siteadmin()
54  * - get_user_access_sitewide()
55  * - load_subcontext()
56  * - get_role_access_bycontext()
57  *
58  * <b>Name conventions</b>
59  *
60  * "ctx" means context
61  *
62  * <b>accessdata</b>
63  *
64  * Access control data is held in the "accessdata" array
65  * which - for the logged-in user, will be in $USER->access
66  *
67  * For other users can be generated and passed around (but may also be cached
68  * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser.
69  *
70  * $accessdata is a multidimensional array, holding
71  * role assignments (RAs), role-capabilities-perm sets
72  * (role defs) and a list of courses we have loaded
73  * data for.
74  *
75  * Things are keyed on "contextpaths" (the path field of
76  * the context table) for fast walking up/down the tree.
77  * <code>
78  * $accessdata[ra][$contextpath]= array($roleid)
79  *                [$contextpath]= array($roleid)
80  *                [$contextpath]= array($roleid)
81  * </code>
82  *
83  * Role definitions are stored like this
84  * (no cap merge is done - so it's compact)
85  *
86  * <code>
87  * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
88  *                                        [mod/forum:editallpost] = -1
89  *                                        [mod/forum:startdiscussion] = -1000
90  * </code>
91  *
92  * See how has_capability_in_accessdata() walks up/down the tree.
93  *
94  * Normally - specially for the logged-in user, we only load
95  * rdef and ra down to the course level, but not below. This
96  * keeps accessdata small and compact. Below-the-course ra/rdef
97  * are loaded as needed. We keep track of which courses we
98  * have loaded ra/rdef in
99  * <code>
100  * $accessdata[loaded] = array($contextpath, $contextpath)
101  * </code>
102  *
103  * <b>Stale accessdata</b>
104  *
105  * For the logged-in user, accessdata is long-lived.
106  *
107  * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
108  * context paths affected by changes. Any check at-or-below
109  * a dirty context will trigger a transparent reload of accessdata.
110  *
111  * Changes at the sytem level will force the reload for everyone.
112  *
113  * <b>Default role caps</b>
114  * The default role assignment is not in the DB, so we
115  * add it manually to accessdata.
116  *
117  * This means that functions that work directly off the
118  * DB need to ensure that the default role caps
119  * are dealt with appropriately.
120  *
121  * @package   moodlecore
122  * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
123  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
124  */
126 /** permission definitions */
127 define('CAP_INHERIT', 0);
128 /** permission definitions */
129 define('CAP_ALLOW', 1);
130 /** permission definitions */
131 define('CAP_PREVENT', -1);
132 /** permission definitions */
133 define('CAP_PROHIBIT', -1000);
135 /** context definitions */
136 define('CONTEXT_SYSTEM', 10);
137 /** context definitions */
138 define('CONTEXT_USER', 30);
139 /** context definitions */
140 define('CONTEXT_COURSECAT', 40);
141 /** context definitions */
142 define('CONTEXT_COURSE', 50);
143 /** context definitions */
144 define('CONTEXT_MODULE', 70);
145 /** context definitions */
146 define('CONTEXT_BLOCK', 80);
148 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
149 define('RISK_MANAGETRUST', 0x0001);
150 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
151 define('RISK_CONFIG',      0x0002);
152 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
153 define('RISK_XSS',         0x0004);
154 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
155 define('RISK_PERSONAL',    0x0008);
156 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
157 define('RISK_SPAM',        0x0010);
158 /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
159 define('RISK_DATALOSS',    0x0020);
161 /** rolename displays - the name as defined in the role definition */
162 define('ROLENAME_ORIGINAL', 0);
163 /** rolename displays - the name as defined by a role alias */
164 define('ROLENAME_ALIAS', 1);
165 /** rolename displays - Both, like this:  Role alias (Original)*/
166 define('ROLENAME_BOTH', 2);
167 /** rolename displays - the name as defined in the role definition and the shortname in brackets*/
168 define('ROLENAME_ORIGINALANDSHORT', 3);
169 /** rolename displays - the name as defined by a role alias, in raw form suitable for editing*/
170 define('ROLENAME_ALIAS_RAW', 4);
172 /** size limit for context cache */
173 if (!defined('MAX_CONTEXT_CACHE_SIZE')) {
174     define('MAX_CONTEXT_CACHE_SIZE', 5000);
177 /**
178  * Although this looks like a global variable, it isn't really.
179  *
180  * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
181  * It is used to cache various bits of data between function calls for performance reasons.
182  * Sadly, a PHP global variale is the only way to impleemnt this, withough rewriting everything
183  * as methods of a class, instead of functions.
184  *
185  * @global stdClass $ACCESSLIB_PRIVATE
186  * @name $ACCESSLIB_PRIVATE
187  */
188 $ACCESSLIB_PRIVATE = new stdClass;
189 $ACCESSLIB_PRIVATE->contexts = array(); // Cache of context objects by level and instance
190 $ACCESSLIB_PRIVATE->contextsbyid = array(); // Cache of context objects by id
191 $ACCESSLIB_PRIVATE->systemcontext = NULL; // Used in get_system_context
192 $ACCESSLIB_PRIVATE->dirtycontexts = NULL; // Dirty contexts cache
193 $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the $accessdata structure for users other than $USER
194 $ACCESSLIB_PRIVATE->roledefinitions = array(); // role definitions cache - helps a lot with mem usage in cron
195 $ACCESSLIB_PRIVATE->croncache = array(); // Used in get_role_access
196 $ACCESSLIB_PRIVATE->preloadedcourses = array(); // Used in preload_course_contexts.
197 $ACCESSLIB_PRIVATE->capabilities = NULL; // detailed information about the capabilities
199 /**
200  * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
201  *
202  * This method should ONLY BE USED BY UNIT TESTS. It clears all of
203  * accesslib's private caches. You need to do this before setting up test data,
204  * and also at the end fo the tests.
205  * @global object
206  * @global object
207  * @global object
208  */
209 function accesslib_clear_all_caches_for_unit_testing() {
210     global $UNITTEST, $USER, $ACCESSLIB_PRIVATE;
211     if (empty($UNITTEST->running)) {
212         throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
213     }
214     $ACCESSLIB_PRIVATE->contexts = array();
215     $ACCESSLIB_PRIVATE->contextsbyid = array();
216     $ACCESSLIB_PRIVATE->systemcontext = NULL;
217     $ACCESSLIB_PRIVATE->dirtycontexts = NULL;
218     $ACCESSLIB_PRIVATE->accessdatabyuser = array();
219     $ACCESSLIB_PRIVATE->roledefinitions = array();
220     $ACCESSLIB_PRIVATE->croncache = array();
221     $ACCESSLIB_PRIVATE->preloadedcourses = array();
222     $ACCESSLIB_PRIVATE->capabilities = NULL;
224     unset($USER->access);
227 /**
228  * Private function. Add a context object to accesslib's caches.
229  * @global object
230  * @param object $context
231  */
232 function cache_context($context) {
233     global $ACCESSLIB_PRIVATE;
235     // If there are too many items in the cache already, remove items until
236     // there is space
237     while (count($ACCESSLIB_PRIVATE->contextsbyid) >= MAX_CONTEXT_CACHE_SIZE) {
238         $first = reset($ACCESSLIB_PRIVATE->contextsbyid);
239         unset($ACCESSLIB_PRIVATE->contextsbyid[$first->id]);
240         unset($ACCESSLIB_PRIVATE->contexts[$first->contextlevel][$first->instanceid]);
241     }
243     $ACCESSLIB_PRIVATE->contexts[$context->contextlevel][$context->instanceid] = $context;
244     $ACCESSLIB_PRIVATE->contextsbyid[$context->id] = $context;
247 /**
248  * This is really slow!!! do not use above course context level
249  *
250  * @global object
251  * @param int $roleid
252  * @param object $context
253  * @return array
254  */
255 function get_role_context_caps($roleid, $context) {
256     global $DB;
258     //this is really slow!!!! - do not use above course context level!
259     $result = array();
260     $result[$context->id] = array();
262     // first emulate the parent context capabilities merging into context
263     $searchcontexts = array_reverse(get_parent_contexts($context));
264     array_push($searchcontexts, $context->id);
265     foreach ($searchcontexts as $cid) {
266         if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
267             foreach ($capabilities as $cap) {
268                 if (!array_key_exists($cap->capability, $result[$context->id])) {
269                     $result[$context->id][$cap->capability] = 0;
270                 }
271                 $result[$context->id][$cap->capability] += $cap->permission;
272             }
273         }
274     }
276     // now go through the contexts bellow given context
277     $searchcontexts = array_keys(get_child_contexts($context));
278     foreach ($searchcontexts as $cid) {
279         if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
280             foreach ($capabilities as $cap) {
281                 if (!array_key_exists($cap->contextid, $result)) {
282                     $result[$cap->contextid] = array();
283                 }
284                 $result[$cap->contextid][$cap->capability] = $cap->permission;
285             }
286         }
287     }
289     return $result;
292 /**
293  * Gets the accessdata for role "sitewide" (system down to course)
294  *
295  * @global object
296  * @global object
297  * @param int $roleid
298  * @param array $accessdata defaults to NULL
299  * @return array
300  */
301 function get_role_access($roleid, $accessdata=NULL) {
303     global $CFG, $DB;
305     /* Get it in 1 cheap DB query...
306      * - relevant role caps at the root and down
307      *   to the course level - but not below
308      */
309     if (is_null($accessdata)) {
310         $accessdata           = array(); // named list
311         $accessdata['ra']     = array();
312         $accessdata['rdef']   = array();
313         $accessdata['loaded'] = array();
314     }
316     //
317     // Overrides for the role IN ANY CONTEXTS
318     // down to COURSE - not below -
319     //
320     $sql = "SELECT ctx.path,
321                    rc.capability, rc.permission
322               FROM {context} ctx
323               JOIN {role_capabilities} rc
324                    ON rc.contextid=ctx.id
325              WHERE rc.roleid = ?
326                    AND ctx.contextlevel <= ".CONTEXT_COURSE."
327           ORDER BY ctx.depth, ctx.path";
328     $params = array($roleid);
330     // we need extra caching in CLI scripts and cron
331     if (CLI_SCRIPT) {
332         global $ACCESSLIB_PRIVATE;
334         if (!isset($ACCESSLIB_PRIVATE->croncache[$roleid])) {
335             $ACCESSLIB_PRIVATE->croncache[$roleid] = array();
336             if ($rs = $DB->get_recordset_sql($sql, $params)) {
337                 foreach ($rs as $rd) {
338                     $ACCESSLIB_PRIVATE->croncache[$roleid][] = $rd;
339                 }
340                 $rs->close();
341             }
342         }
344         foreach ($ACCESSLIB_PRIVATE->croncache[$roleid] as $rd) {
345             $k = "{$rd->path}:{$roleid}";
346             $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
347         }
349     } else {
350         if ($rs = $DB->get_recordset_sql($sql, $params)) {
351             foreach ($rs as $rd) {
352                 $k = "{$rd->path}:{$roleid}";
353                 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
354             }
355             unset($rd);
356             $rs->close();
357         }
358     }
360     return $accessdata;
363 /**
364  * Gets the accessdata for role "sitewide" (system down to course)
365  *
366  * @global object
367  * @global object
368  * @param int $roleid
369  * @param array $accessdata defaults to NULL
370  * @return array
371  */
372 function get_default_frontpage_role_access($roleid, $accessdata=NULL) {
374     global $CFG, $DB;
376     $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
377     $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
379     //
380     // Overrides for the role in any contexts related to the course
381     //
382     $sql = "SELECT ctx.path,
383                    rc.capability, rc.permission
384               FROM {context} ctx
385               JOIN {role_capabilities} rc
386                    ON rc.contextid=ctx.id
387              WHERE rc.roleid = ?
388                    AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?)
389                    AND ctx.contextlevel <= ".CONTEXT_COURSE."
390           ORDER BY ctx.depth, ctx.path";
391     $params = array($roleid, "$base/%");
393     if ($rs = $DB->get_recordset_sql($sql, $params)) {
394         foreach ($rs as $rd) {
395             $k = "{$rd->path}:{$roleid}";
396             $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
397         }
398         unset($rd);
399         $rs->close();
400     }
402     return $accessdata;
406 /**
407  * Get the default guest role
408  *
409  * @global object
410  * @global object
411  * @return object role
412  */
413 function get_guest_role() {
414     global $CFG, $DB;
416     if (empty($CFG->guestroleid)) {
417         if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
418             $guestrole = array_shift($roles);   // Pick the first one
419             set_config('guestroleid', $guestrole->id);
420             return $guestrole;
421         } else {
422             debugging('Can not find any guest role!');
423             return false;
424         }
425     } else {
426         if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
427             return $guestrole;
428         } else {
429             //somebody is messing with guest roles, remove incorrect setting and try to find a new one
430             set_config('guestroleid', '');
431             return get_guest_role();
432         }
433     }
436 /**
437  * Check whether a user has a paritcular capability in a given context.
438  *
439  * For example::
440  *      $context = get_context_instance(CONTEXT_MODULE, $cm->id);
441  *      has_capability('mod/forum:replypost',$context)
442  *
443  * By default checks the capabilties of the current user, but you can pass a
444  * different userid. By default will return true for admin users, but you can override that with the fourth argument.
445  *
446  * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
447  * or capabilities with XSS, config or data loss risks.
448  *
449  * @param string $capability the name of the capability to check. For example mod/forum:view
450  * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
451  * @param integer|object $user A user id or object. By default (NULL) checks the permissions of the current user.
452  * @param boolean $doanything If false, ignores effect of admin role assignment
453  * @return boolean true if the user has this capability. Otherwise false.
454  */
455 function has_capability($capability, $context, $user = NULL, $doanything=true) {
456     global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE;
458     if (during_initial_install()) {
459         if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") {
460             // we are in an installer - roles can not work yet
461             return true;
462         } else {
463             return false;
464         }
465     }
467     if (strpos($capability, 'moodle/legacy:') === 0) {
468         throw new coding_exception('Legacy capabilities can not be used any more!');
469     }
471     // the original $CONTEXT here was hiding serious errors
472     // for security reasons do not reuse previous context
473     if (empty($context)) {
474         debugging('Incorrect context specified');
475         return false;
476     }
477     if (!is_bool($doanything)) {
478         throw new coding_exception('Capability parameter "doanything" is wierd ("'.$doanything.'"). This has to be fixed in code.');
479     }
481     // make sure there is a real user specified
482     if ($user === NULL) {
483         $userid = !empty($USER->id) ? $USER->id : 0;
484     } else {
485         $userid = !empty($user->id) ? $user->id : $user;
486     }
488     // capability must exist
489     if (!$capinfo = get_capability_info($capability)) {
490         debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
491         return false;
492     }
493     // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
494     if (($capinfo->captype === 'write') or ((int)$capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
495         if (isguestuser($userid) or $userid == 0) {
496             return false;
497         }
498     }
500     if (is_null($context->path) or $context->depth == 0) {
501         //this should not happen
502         $contexts = array(SYSCONTEXTID, $context->id);
503         $context->path = '/'.SYSCONTEXTID.'/'.$context->id;
504         debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);
506     } else {
507         $contexts = explode('/', $context->path);
508         array_shift($contexts);
509     }
511     if (CLI_SCRIPT && !isset($USER->access)) {
512         // In cron, some modules setup a 'fake' $USER,
513         // ensure we load the appropriate accessdata.
514         if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
515             $ACCESSLIB_PRIVATE->dirtycontexts = NULL; //load fresh dirty contexts
516         } else {
517             load_user_accessdata($userid);
518             $ACCESSLIB_PRIVATE->dirtycontexts = array();
519         }
520         $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
522     } else if (isset($USER->id) && ($USER->id == $userid) && !isset($USER->access)) {
523         // caps not loaded yet - better to load them to keep BC with 1.8
524         // not-logged-in user or $USER object set up manually first time here
525         load_all_capabilities();
526         $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now
527         $ACCESSLIB_PRIVATE->roledefinitions = array();
528     }
530     // Load dirty contexts list if needed
531     if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
532         if (isset($USER->access['time'])) {
533             $ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']);
534         }
535         else {
536             $ACCESSLIB_PRIVATE->dirtycontexts = array();
537         }
538     }
540     // Careful check for staleness...
541     if (count($ACCESSLIB_PRIVATE->dirtycontexts) !== 0 and is_contextpath_dirty($contexts, $ACCESSLIB_PRIVATE->dirtycontexts)) {
542         // reload all capabilities - preserving loginas, roleswitches, etc
543         // and then cleanup any marks of dirtyness... at least from our short
544         // term memory! :-)
545         $ACCESSLIB_PRIVATE->accessdatabyuser = array();
546         $ACCESSLIB_PRIVATE->roledefinitions = array();
548         if (CLI_SCRIPT) {
549             load_user_accessdata($userid);
550             $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
551             $ACCESSLIB_PRIVATE->dirtycontexts = array();
553         } else {
554             reload_all_capabilities();
555         }
556     }
558     // Find out if user is admin - it is not possible to override the doanything in any way
559     // and it is not possible to switch to admin role either.
560     if ($doanything) {
561         if (is_siteadmin($userid)) {
562             return true;
563         }
564     }
566     // divulge how many times we are called
567     //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
569     if (isset($USER->id) && ($USER->id == $userid)) { // we must accept strings and integers in $userid
570         //
571         // For the logged in user, we have $USER->access
572         // which will have all RAs and caps preloaded for
573         // course and above contexts.
574         //
575         // Contexts below courses && contexts that do not
576         // hang from courses are loaded into $USER->access
577         // on demand, and listed in $USER->access[loaded]
578         //
579         if ($context->contextlevel <= CONTEXT_COURSE) {
580             // Course and above are always preloaded
581             return has_capability_in_accessdata($capability, $context, $USER->access);
582         }
583         // Load accessdata for below-the-course contexts
584         if (!path_inaccessdata($context->path,$USER->access)) {
585             // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
586             // $bt = debug_backtrace();
587             // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
588             load_subcontext($USER->id, $context, $USER->access);
589         }
590         return has_capability_in_accessdata($capability, $context, $USER->access);
591     }
593     if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
594         load_user_accessdata($userid);
595     }
597     if ($context->contextlevel <= CONTEXT_COURSE) {
598         // Course and above are always preloaded
599         return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
600     }
601     // Load accessdata for below-the-course contexts as needed
602     if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
603         // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
604         // $bt = debug_backtrace();
605         // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
606         load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
607     }
608     return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
611 /**
612  * Check if the user has any one of several capabilities from a list.
613  *
614  * This is just a utility method that calls has_capability in a loop. Try to put
615  * the capabilities that most users are likely to have first in the list for best
616  * performance.
617  *
618  * There are probably tricks that could be done to improve the performance here, for example,
619  * check the capabilities that are already cached first.
620  *
621  * @see has_capability()
622  * @param array $capabilities an array of capability names.
623  * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
624  * @param integer $userid A user id. By default (NULL) checks the permissions of the current user.
625  * @param boolean $doanything If false, ignore effect of admin role assignment
626  * @return boolean true if the user has any of these capabilities. Otherwise false.
627  */
628 function has_any_capability($capabilities, $context, $userid=NULL, $doanything=true) {
629     if (!is_array($capabilities)) {
630         debugging('Incorrect $capabilities parameter in has_any_capabilities() call - must be an array');
631         return false;
632     }
633     foreach ($capabilities as $capability) {
634         if (has_capability($capability, $context, $userid, $doanything)) {
635             return true;
636         }
637     }
638     return false;
641 /**
642  * Check if the user has all the capabilities in a list.
643  *
644  * This is just a utility method that calls has_capability in a loop. Try to put
645  * the capabilities that fewest users are likely to have first in the list for best
646  * performance.
647  *
648  * There are probably tricks that could be done to improve the performance here, for example,
649  * check the capabilities that are already cached first.
650  *
651  * @see has_capability()
652  * @param array $capabilities an array of capability names.
653  * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
654  * @param integer $userid A user id. By default (NULL) checks the permissions of the current user.
655  * @param boolean $doanything If false, ignore effect of admin role assignment
656  * @return boolean true if the user has all of these capabilities. Otherwise false.
657  */
658 function has_all_capabilities($capabilities, $context, $userid=NULL, $doanything=true) {
659     if (!is_array($capabilities)) {
660         debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
661         return false;
662     }
663     foreach ($capabilities as $capability) {
664         if (!has_capability($capability, $context, $userid, $doanything)) {
665             return false;
666         }
667     }
668     return true;
671 /**
672  * Check if the user is an admin at the site level.
673  *
674  * Please note that use of proper capabilities is always encouraged,
675  * this function is supposed to be used from core or for temporary hacks.
676  *
677  * @param   int|object  $user_or_id user id or user object
678  * @returns bool true if user is one of the administrators, false otherwise
679  */
680 function is_siteadmin($user_or_id = NULL) {
681     global $CFG, $USER;
683     if ($user_or_id === NULL) {
684         $user_or_id = $USER;
685     }
687     if (empty($user_or_id)) {
688         return false;
689     }
690     if (!empty($user_or_id->id)) {
691         // we support
692         $userid = $user_or_id->id;
693     } else {
694         $userid = $user_or_id;
695     }
697     $siteadmins = explode(',', $CFG->siteadmins);
698     return in_array($userid, $siteadmins);
701 /**
702  * Returns true if user has at least one role assign
703  * of 'coursemanager' role (is potentially listed in some course descriptions).
704  * @param $userid
705  * @return unknown_type
706  */
707 function has_coursemanager_role($userid) {
708     global $DB;
710     if (empty($CFG->coursemanager)) {
711         return false;
712     }
713     $sql = "SELECT 1
714               FROM {role_assignments}
715              WHERE userid = :userid AND roleid IN ($CFG->coursemanager)";
716     return $DB->record_exists($sql, array('userid'=>$userid));
719 /**
720  * @param string $path
721  * @return string
722  */
723 function get_course_from_path($path) {
724     // assume that nothing is more than 1 course deep
725     if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
726         return $matches[1];
727     }
728     return false;
731 /**
732  * @param string $path
733  * @param array $accessdata
734  * @return bool
735  */
736 function path_inaccessdata($path, $accessdata) {
737     if (empty($accessdata['loaded'])) {
738         return false;
739     }
741     // assume that contexts hang from sys or from a course
742     // this will only work well with stuff that hangs from a course
743     if (in_array($path, $accessdata['loaded'], true)) {
744             // error_log("found it!");
745         return true;
746     }
747     $base = '/' . SYSCONTEXTID;
748     while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
749         $path = $matches[1];
750         if ($path === $base) {
751             return false;
752         }
753         if (in_array($path, $accessdata['loaded'], true)) {
754             return true;
755         }
756     }
757     return false;
760 /**
761  * Does the user have a capability to do something?
762  *
763  * Walk the accessdata array and return true/false.
764  * Deals with prohibits, roleswitching, aggregating
765  * capabilities, etc.
766  *
767  * The main feature of here is being FAST and with no
768  * side effects.
769  *
770  * Notes:
771  *
772  * Switch Roles exits early
773  * ------------------------
774  * cap checks within a switchrole need to exit early
775  * in our bottom up processing so they don't "see" that
776  * there are real RAs that can do all sorts of things.
777  *
778  * Switch Role merges with default role
779  * ------------------------------------
780  * If you are a teacher in course X, you have at least
781  * teacher-in-X + defaultloggedinuser-sitewide. So in the
782  * course you'll have techer+defaultloggedinuser.
783  * We try to mimic that in switchrole.
784  *
785  * Permission evaluation
786  * ---------------------
787  * Originaly there was an extremely complicated way
788  * to determine the user access that dealt with
789  * "locality" or role assignemnts and role overrides.
790  * Now we simply evaluate access for each roel separately
791  * and then verify if user has at least one role with allow
792  * and at the same time no role with prohibit.
793  *
794  * @param string $capability
795  * @param object $context
796  * @param array $accessdata
797  * @return bool
798  */
799 function has_capability_in_accessdata($capability, $context, array $accessdata) {
800     global $CFG;
802     if (empty($context->id)) {
803         throw new coding_exception('Invalid context specified');
804     }
806     // Build $paths as a list of current + all parent "paths" with order bottom-to-top
807     $contextids = explode('/', trim($context->path, '/'));
808     $paths = array($context->path);
809     while ($contextids) {
810         array_pop($contextids);
811         $paths[] = '/' . implode('/', $contextids);
812     }
813     unset($contextids);
815     $roles = array();
816     $switchedrole = false;
818     // Find out if role switched
819     if (!empty($accessdata['rsw'])) {
820         // From the bottom up...
821         foreach ($paths as $path) {
822             if (isset($accessdata['rsw'][$path])) {
823                 // Found a switchrole assignment - check for that role _plus_ the default user role
824                 $roles = array($accessdata['rsw'][$path]=>NULL, $CFG->defaultuserroleid=>NULL);
825                 $switchedrole = true;
826                 break;
827             }
828         }
829     }
831     if (!$switchedrole) {
832         // get all users roles in this context and above
833         foreach ($paths as $path) {
834             if (isset($accessdata['ra'][$path])) {
835                 foreach ($accessdata['ra'][$path] as $roleid) {
836                     $roles[$roleid] = NULL;
837                 }
838             }
839         }
840     }
842     // Now find out what access is given to each role, going bottom-->up direction
843     foreach ($roles as $roleid => $ignored) {
844         foreach ($paths as $path) {
845             if (isset($accessdata['rdef']["{$path}:$roleid"][$capability])) {
846                 $perm = (int)$accessdata['rdef']["{$path}:$roleid"][$capability];
847                 if ($perm === CAP_PROHIBIT or is_null($roles[$roleid])) {
848                     $roles[$roleid] = $perm;
849                 }
850             }
851         }
852     }
853     // any CAP_PROHIBIT found means no permission for the user
854     if (array_search(CAP_PROHIBIT, $roles) !== false) {
855         return false;
856     }
858     // at least one CAP_ALLOW means the user has a permission
859     return (array_search(CAP_ALLOW, $roles) !== false);
862 /**
863  * @param object $context
864  * @param array $accessdata
865  * @return array
866  */
867 function aggregate_roles_from_accessdata($context, $accessdata) {
869     $path = $context->path;
871     // build $contexts as a list of "paths" of the current
872     // contexts and parents with the order top-to-bottom
873     $contexts = array($path);
874     while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
875         $path = $matches[1];
876         array_unshift($contexts, $path);
877     }
879     $cc = count($contexts);
881     $roles = array();
882     // From the bottom up...
883     for ($n=$cc-1;$n>=0;$n--) {
884         $ctxp = $contexts[$n];
885         if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
886             // Found assignments on this leaf
887             $addroles = $accessdata['ra'][$ctxp];
888             $roles    = array_merge($roles, $addroles);
889         }
890     }
892     return array_unique($roles);
895 /**
896  * A convenience function that tests has_capability, and displays an error if
897  * the user does not have that capability.
898  *
899  * NOTE before Moodle 2.0, this function attempted to make an appropriate
900  * require_login call before checking the capability. This is no longer the case.
901  * You must call require_login (or one of its variants) if you want to check the
902  * user is logged in, before you call this function.
903  *
904  * @see has_capability()
905  *
906  * @param string $capability the name of the capability to check. For example mod/forum:view
907  * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
908  * @param integer $userid A user id. By default (NULL) checks the permissions of the current user.
909  * @param bool $doanything If false, ignore effect of admin role assignment
910  * @param string $errorstring The error string to to user. Defaults to 'nopermissions'.
911  * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
912  * @return void terminates with an error if the user does not have the given capability.
913  */
914 function require_capability($capability, $context, $userid = NULL, $doanything = true,
915                             $errormessage = 'nopermissions', $stringfile = '') {
916     if (!has_capability($capability, $context, $userid, $doanything)) {
917         throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
918     }
921 /**
922  * Get an array of courses where cap requested is available
923  *
924  * Get an array of courses (with magic extra bits)
925  * where the accessdata and in DB enrolments show
926  * that the cap requested is available.
927  *
928  * The main use is for get_my_courses().
929  *
930  * Notes
931  *
932  * - $fields is an array of fieldnames to ADD
933  *   so name the fields you really need, which will
934  *   be added and uniq'd
935  *
936  * - the course records have $c->context which is a fully
937  *   valid context object. Saves you a query per course!
938  *
939  * - the course records have $c->categorypath to make
940  *   category lookups cheap
941  *
942  * - current implementation is split in -
943  *
944  *   - if the user has the cap systemwide, stupidly
945  *     grab *every* course for a capcheck. This eats
946  *     a TON of bandwidth, specially on large sites
947  *     with separate DBs...
948  *
949  *   - otherwise, fetch "likely" courses with a wide net
950  *     that should get us _cheaply_ at least the courses we need, and some
951  *     we won't - we get courses that...
952  *      - are in a category where user has the cap
953  *      - or where use has a role-assignment (any kind)
954  *      - or where the course has an override on for this cap
955  *
956  *   - walk the courses recordset checking the caps oneach one
957  *     the checks are all in memory and quite fast
958  *     (though we could implement a specialised variant of the
959  *     has_capability_in_accessdata() code to speed it up)
960  *
961  * @global object
962  * @global object
963  * @param string $capability - name of the capability
964  * @param array  $accessdata - accessdata session array
965  * @param bool   $doanything_ignored - admin roles are completely ignored here
966  * @param string $sort - sorting fields - prefix each fieldname with "c."
967  * @param array  $fields - additional fields you are interested in...
968  * @param int    $limit  - set if you want to limit the number of courses
969  * @return array $courses - ordered array of course objects - see notes above
970  */
971 function get_user_courses_bycap($userid, $cap, $accessdata, $doanything_ignored, $sort='c.sortorder ASC', $fields=NULL, $limit=0) {
973     global $CFG, $DB;
975     // Slim base fields, let callers ask for what they need...
976     $basefields = array('id', 'sortorder', 'shortname', 'idnumber');
978     if (!is_null($fields)) {
979         $fields = array_merge($basefields, $fields);
980         $fields = array_unique($fields);
981     } else {
982         $fields = $basefields;
983     }
984     // If any of the fields is '*', leave it alone, discarding the rest
985     // to avoid ambiguous columns under some silly DBs. See MDL-18746 :-D
986     if (in_array('*', $fields)) {
987         $fields = array('*');
988     }
989     $coursefields = 'c.' .implode(',c.', $fields);
991     $sort = trim($sort);
992     if ($sort !== '') {
993         $sort = "ORDER BY $sort";
994     }
996     $sysctx = get_context_instance(CONTEXT_SYSTEM);
997     if (has_capability_in_accessdata($cap, $sysctx, $accessdata)) {
998         //
999         // Apparently the user has the cap sitewide, so walk *every* course
1000         // (the cap checks are moderately fast, but this moves massive bandwidth w the db)
1001         // Yuck.
1002         //
1003         $sql = "SELECT $coursefields,
1004                        ctx.id AS ctxid, ctx.path AS ctxpath,
1005                        ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,
1006                        cc.path AS categorypath
1007                   FROM {course} c
1008                   JOIN {course_categories} cc
1009                        ON c.category=cc.id
1010                   JOIN {context} ctx
1011                        ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1012                  $sort ";
1013         $rs = $DB->get_recordset_sql($sql);
1014     } else {
1015         //
1016         // narrow down where we have the caps to a few contexts
1017         // this will be a combination of
1018         // - courses    where user has an explicit enrolment
1019         // - courses    that have an override (any status) on that capability
1020         // - categories where user has the rights (granted status) on that capability
1021         //
1022         $sql = "SELECT ctx.*
1023                   FROM {context} ctx
1024                  WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
1025               ORDER BY ctx.depth";
1026         $rs = $DB->get_recordset_sql($sql);
1027         $catpaths = array();
1028         foreach ($rs as $catctx) {
1029             if ($catctx->path != ''
1030                 && has_capability_in_accessdata($cap, $catctx, $accessdata)) {
1031                 $catpaths[] = $catctx->path;
1032             }
1033         }
1034         $rs->close();
1035         $catclause = '';
1036         $params = array();
1037         if (count($catpaths)) {
1038             $cc = count($catpaths);
1039             for ($n=0;$n<$cc;$n++) {
1040                 $catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'";
1041             }
1042             $catclause = 'WHERE (' . implode(' OR ', $catpaths) .')';
1043         }
1044         unset($catpaths);
1046         /// UNION 3 queries:
1047         /// - user role assignments in courses
1048         /// - user capability (override - any status) in courses
1049         /// - user right (granted status) in categories (optionally executed)
1050         /// Enclosing the 3-UNION into an inline_view to avoid column names conflict and making the ORDER BY cross-db
1051         /// and to allow selection of TEXT columns in the query (MSSQL and Oracle limitation). MDL-16209
1052         $sql = "
1053             SELECT $coursefields, ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance, categorypath
1054               FROM (
1055                     SELECT c.id,
1056                            ctx.id AS ctxid, ctx.path AS ctxpath,
1057                            ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel, ctx.instanceid AS ctxinstance,
1058                            cc.path AS categorypath
1059                     FROM {course} c
1060                     JOIN {course_categories} cc
1061                       ON c.category=cc.id
1062                     JOIN {context} ctx
1063                       ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1064                     JOIN {role_assignments} ra
1065                       ON (ra.contextid=ctx.id AND ra.userid=:userid)
1066                     UNION
1067                     SELECT c.id,
1068                            ctx.id AS ctxid, ctx.path AS ctxpath,
1069                            ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel, ctx.instanceid AS ctxinstance,
1070                            cc.path AS categorypath
1071                     FROM {course} c
1072                     JOIN {course_categories} cc
1073                       ON c.category=cc.id
1074                     JOIN {context} ctx
1075                       ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1076                     JOIN {role_capabilities} rc
1077                       ON (rc.contextid=ctx.id AND (rc.capability=:cap)) ";
1079         if (!empty($catclause)) { /// If we have found the right in categories, add child courses here too
1080             $sql .= "
1081                     UNION
1082                     SELECT c.id,
1083                            ctx.id AS ctxid, ctx.path AS ctxpath,
1084                            ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel, ctx.instanceid AS ctxinstance,
1085                            cc.path AS categorypath
1086                     FROM {course} c
1087                     JOIN {course_categories} cc
1088                       ON c.category=cc.id
1089                     JOIN {context} ctx
1090                       ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1091                     $catclause";
1092         }
1094     /// Close the inline_view and join with courses table to get requested $coursefields
1095         $sql .= "
1096                 ) inline_view
1097                 INNER JOIN {course} c
1098                     ON inline_view.id = c.id";
1100     /// To keep cross-db we need to strip any prefix in the ORDER BY clause for queries using UNION
1101         $sql .= "
1102                 " . preg_replace('/[a-z]+\./i', '', $sort); /// Add ORDER BY clause
1104         $params['userid'] = $userid;
1105         $params['cap']    = $cap;
1106         $rs = $DB->get_recordset_sql($sql, $params);
1107     }
1109 /// Confirm rights (granted capability) for each course returned
1110     $courses = array();
1111     $cc = 0; // keep count
1112     if ($rs) {
1113         foreach ($rs as $c) {
1114             // build the context obj
1115             context_instance_preload($c);
1116             $context = get_context_instance(CONTEXT_COURSE, $c->id);
1118             if (has_capability_in_accessdata($cap, $context, $accessdata)) {
1119                 if ($limit > 0 && $cc >= $limit) {
1120                     break;
1121                 }
1123                 $courses[] = $c;
1124                 $cc++;
1125             }
1126         }
1127         $rs->close();
1128     }
1130     return $courses;
1134 /**
1135  * Return a nested array showing role assignments
1136  * all relevant role capabilities for the user at
1137  * site/metacourse/course_category/course levels
1138  *
1139  * We do _not_ delve deeper than courses because the number of
1140  * overrides at the module/block levels is HUGE.
1141  *
1142  * [ra]   => [/path/][]=roleid
1143  * [rdef] => [/path/:roleid][capability]=permission
1144  * [loaded] => array('/path', '/path')
1145  *
1146  * @global object
1147  * @global object
1148  * @param $userid integer - the id of the user
1149  */
1150 function get_user_access_sitewide($userid) {
1152     global $CFG, $DB;
1154     /* Get in 3 cheap DB queries...
1155      * - role assignments
1156      * - relevant role caps
1157      *   - above and within this user's RAs
1158      *   - below this user's RAs - limited to course level
1159      */
1161     $accessdata           = array(); // named list
1162     $accessdata['ra']     = array();
1163     $accessdata['rdef']   = array();
1164     $accessdata['loaded'] = array();
1166     //
1167     // Role assignments
1168     //
1169     $sql = "SELECT ctx.path, ra.roleid
1170               FROM {role_assignments} ra
1171               JOIN {context} ctx ON ctx.id=ra.contextid
1172              WHERE ra.userid = ? AND ctx.contextlevel <= ".CONTEXT_COURSE;
1173     $params = array($userid);
1174     $rs = $DB->get_recordset_sql($sql, $params);
1176     //
1177     // raparents collects paths & roles we need to walk up
1178     // the parenthood to build the rdef
1179     //
1180     $raparents = array();
1181     if ($rs) {
1182         foreach ($rs as $ra) {
1183             // RAs leafs are arrays to support multi
1184             // role assignments...
1185             if (!isset($accessdata['ra'][$ra->path])) {
1186                 $accessdata['ra'][$ra->path] = array();
1187             }
1188             array_push($accessdata['ra'][$ra->path], $ra->roleid);
1190             // Concatenate as string the whole path (all related context)
1191             // for this role. This is damn faster than using array_merge()
1192             // Will unique them later
1193             if (isset($raparents[$ra->roleid])) {
1194                 $raparents[$ra->roleid] .= $ra->path;
1195             } else {
1196                 $raparents[$ra->roleid] = $ra->path;
1197             }
1198         }
1199         unset($ra);
1200         $rs->close();
1201     }
1203     // Walk up the tree to grab all the roledefs
1204     // of interest to our user...
1205     //
1206     // NOTE: we use a series of IN clauses here - which
1207     // might explode on huge sites with very convoluted nesting of
1208     // categories... - extremely unlikely that the number of categories
1209     // and roletypes is so large that we hit the limits of IN()
1210     $clauses = '';
1211     $cparams = array();
1212     foreach ($raparents as $roleid=>$strcontexts) {
1213         $contexts = implode(',', array_unique(explode('/', trim($strcontexts, '/'))));
1214         if ($contexts ==! '') {
1215             if ($clauses) {
1216                 $clauses .= ' OR ';
1217             }
1218             $clauses .= "(roleid=? AND contextid IN ($contexts))";
1219             $cparams[] = $roleid;
1220         }
1221     }
1223     if ($clauses !== '') {
1224         $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
1225                 FROM {role_capabilities} rc
1226                 JOIN {context} ctx
1227                   ON rc.contextid=ctx.id
1228                 WHERE $clauses";
1230         unset($clauses);
1231         $rs = $DB->get_recordset_sql($sql, $cparams);
1233         if ($rs) {
1234             foreach ($rs as $rd) {
1235                 $k = "{$rd->path}:{$rd->roleid}";
1236                 $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1237             }
1238             unset($rd);
1239             $rs->close();
1240         }
1241     }
1243     //
1244     // Overrides for the role assignments IN SUBCONTEXTS
1245     // (though we still do _not_ go below the course level.
1246     //
1247     // NOTE that the JOIN w sctx is with 3-way triangulation to
1248     // catch overrides to the applicable role in any subcontext, based
1249     // on the path field of the parent.
1250     //
1251     $sql = "SELECT sctx.path, ra.roleid,
1252                    ctx.path AS parentpath,
1253                    rco.capability, rco.permission
1254               FROM {role_assignments} ra
1255               JOIN {context} ctx
1256                    ON ra.contextid=ctx.id
1257               JOIN {context} sctx
1258                    ON (sctx.path LIKE " . $DB->sql_concat('ctx.path',"'/%'"). " )
1259               JOIN {role_capabilities} rco
1260                    ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
1261              WHERE ra.userid = ?
1262                AND ctx.contextlevel <= ".CONTEXT_COURSECAT."
1263                AND sctx.contextlevel <= ".CONTEXT_COURSE."
1264           ORDER BY sctx.depth, sctx.path, ra.roleid";
1265     $params = array($userid);
1266     $rs = $DB->get_recordset_sql($sql, $params);
1267     if ($rs) {
1268         foreach ($rs as $rd) {
1269             $k = "{$rd->path}:{$rd->roleid}";
1270             $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1271         }
1272         unset($rd);
1273         $rs->close();
1274     }
1275     return $accessdata;
1278 /**
1279  * Add to the access ctrl array the data needed by a user for a given context
1280  *
1281  * @global object
1282  * @global object
1283  * @param integer $userid the id of the user
1284  * @param object $context needs path!
1285  * @param array $accessdata accessdata array
1286  */
1287 function load_subcontext($userid, $context, &$accessdata) {
1289     global $CFG, $DB;
1291     /* Get the additional RAs and relevant rolecaps
1292      * - role assignments - with role_caps
1293      * - relevant role caps
1294      *   - above this user's RAs
1295      *   - below this user's RAs - limited to course level
1296      */
1298     $base = "/" . SYSCONTEXTID;
1300     //
1301     // Replace $context with the target context we will
1302     // load. Normally, this will be a course context, but
1303     // may be a different top-level context.
1304     //
1305     // We have 3 cases
1306     //
1307     // - Course
1308     // - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM
1309     // - BLOCK/MODULE/GROUP hanging from a course
1310     //
1311     // For course contexts, we _already_ have the RAs
1312     // but the cost of re-fetching is minimal so we don't care.
1313     //
1314     if ($context->contextlevel !== CONTEXT_COURSE
1315         && $context->path !== "$base/{$context->id}") {
1316         // Case BLOCK/MODULE/GROUP hanging from a course
1317         // Assumption: the course _must_ be our parent
1318         // If we ever see stuff nested further this needs to
1319         // change to do 1 query over the exploded path to
1320         // find out which one is the course
1321         $courses = explode('/',get_course_from_path($context->path));
1322         $targetid = array_pop($courses);
1323         $context = get_context_instance_by_id($targetid);
1325     }
1327     //
1328     // Role assignments in the context and below
1329     //
1330     $sql = "SELECT ctx.path, ra.roleid
1331               FROM {role_assignments} ra
1332               JOIN {context} ctx
1333                    ON ra.contextid=ctx.id
1334              WHERE ra.userid = ?
1335                    AND (ctx.path = ? OR ctx.path LIKE ?)
1336           ORDER BY ctx.depth, ctx.path, ra.roleid";
1337     $params = array($userid, $context->path, $context->path."/%");
1338     $rs = $DB->get_recordset_sql($sql, $params);
1340     //
1341     // Read in the RAs, preventing duplicates
1342     //
1343     if ($rs) {
1344         $localroles = array();
1345         $lastseen  = '';
1346         foreach ($rs as $ra) {
1347             if (!isset($accessdata['ra'][$ra->path])) {
1348                 $accessdata['ra'][$ra->path] = array();
1349             }
1350             // only add if is not a repeat caused
1351             // by capability join...
1352             // (this check is cheaper than in_array())
1353             if ($lastseen !== $ra->path.':'.$ra->roleid) {
1354                 $lastseen = $ra->path.':'.$ra->roleid;
1355                 array_push($accessdata['ra'][$ra->path], $ra->roleid);
1356                 array_push($localroles,           $ra->roleid);
1357             }
1358         }
1359         $rs->close();
1360     }
1362     //
1363     // Walk up and down the tree to grab all the roledefs
1364     // of interest to our user...
1365     //
1366     // NOTES
1367     // - we use IN() but the number of roles is very limited.
1368     //
1369     $courseroles    = aggregate_roles_from_accessdata($context, $accessdata);
1371     // Do we have any interesting "local" roles?
1372     $localroles = array_diff($localroles,$courseroles); // only "new" local roles
1373     $wherelocalroles='';
1374     if (count($localroles)) {
1375         // Role defs for local roles in 'higher' contexts...
1376         $contexts = substr($context->path, 1); // kill leading slash
1377         $contexts = str_replace('/', ',', $contexts);
1378         $localroleids = implode(',',$localroles);
1379         $wherelocalroles="OR (rc.roleid IN ({$localroleids})
1380                               AND ctx.id IN ($contexts))" ;
1381     }
1383     // We will want overrides for all of them
1384     $whereroles = '';
1385     if ($roleids  = implode(',',array_merge($courseroles,$localroles))) {
1386         $whereroles = "rc.roleid IN ($roleids) AND";
1387     }
1388     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
1389               FROM {role_capabilities} rc
1390               JOIN {context} ctx
1391                    ON rc.contextid=ctx.id
1392              WHERE ($whereroles
1393                     (ctx.id=? OR ctx.path LIKE ?))
1394                    $wherelocalroles
1395           ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1396     $params = array($context->id, $context->path."/%");
1398     $newrdefs = array();
1399     if ($rs = $DB->get_recordset_sql($sql, $params)) {
1400         foreach ($rs as $rd) {
1401             $k = "{$rd->path}:{$rd->roleid}";
1402             if (!array_key_exists($k, $newrdefs)) {
1403                 $newrdefs[$k] = array();
1404             }
1405             $newrdefs[$k][$rd->capability] = $rd->permission;
1406         }
1407         $rs->close();
1408     } else {
1409         debugging('Bad SQL encountered!');
1410     }
1412     compact_rdefs($newrdefs);
1413     foreach ($newrdefs as $key=>$value) {
1414         $accessdata['rdef'][$key] =& $newrdefs[$key];
1415     }
1417     // error_log("loaded {$context->path}");
1418     $accessdata['loaded'][] = $context->path;
1421 /**
1422  * Add to the access ctrl array the data needed by a role for a given context.
1423  *
1424  * The data is added in the rdef key.
1425  *
1426  * This role-centric function is useful for role_switching
1427  * and to get an overview of what a role gets under a
1428  * given context and below...
1429  *
1430  * @global object
1431  * @global object
1432  * @param integer $roleid the id of the user
1433  * @param object $context needs path!
1434  * @param array $accessdata accessdata array NULL by default
1435  * @return array
1436  */
1437 function get_role_access_bycontext($roleid, $context, $accessdata=NULL) {
1439     global $CFG, $DB;
1441     /* Get the relevant rolecaps into rdef
1442      * - relevant role caps
1443      *   - at ctx and above
1444      *   - below this ctx
1445      */
1447     if (is_null($accessdata)) {
1448         $accessdata           = array(); // named list
1449         $accessdata['ra']     = array();
1450         $accessdata['rdef']   = array();
1451         $accessdata['loaded'] = array();
1452     }
1454     $contexts = substr($context->path, 1); // kill leading slash
1455     $contexts = str_replace('/', ',', $contexts);
1457     //
1458     // Walk up and down the tree to grab all the roledefs
1459     // of interest to our role...
1460     //
1461     // NOTE: we use an IN clauses here - which
1462     // might explode on huge sites with very convoluted nesting of
1463     // categories... - extremely unlikely that the number of nested
1464     // categories is so large that we hit the limits of IN()
1465     //
1466     $sql = "SELECT ctx.path, rc.capability, rc.permission
1467               FROM {role_capabilities} rc
1468               JOIN {context} ctx
1469                    ON rc.contextid=ctx.id
1470              WHERE rc.roleid=? AND
1471                    ( ctx.id IN ($contexts) OR
1472                     ctx.path LIKE ? )
1473           ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
1474     $params = array($roleid, $context->path."/%");
1476     if ($rs = $DB->get_recordset_sql($sql, $params)) {
1477         foreach ($rs as $rd) {
1478             $k = "{$rd->path}:{$roleid}";
1479             $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1480         }
1481         $rs->close();
1482     }
1484     return $accessdata;
1487 /**
1488  * Load accessdata for a user into the $ACCESSLIB_PRIVATE->accessdatabyuser global
1489  *
1490  * Used by has_capability() - but feel free
1491  * to call it if you are about to run a BIG
1492  * cron run across a bazillion users.
1493  *
1494  * @global object
1495  * @global object
1496  * @param int $userid
1497  * @return array returns ACCESSLIB_PRIVATE->accessdatabyuser[userid]
1498  */
1499 function load_user_accessdata($userid) {
1500     global $CFG, $ACCESSLIB_PRIVATE;
1502     $base = '/'.SYSCONTEXTID;
1504     $accessdata = get_user_access_sitewide($userid);
1505     $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
1506     //
1507     // provide "default role" & set 'dr'
1508     //
1509     if (!empty($CFG->defaultuserroleid)) {
1510         $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1511         if (!isset($accessdata['ra'][$base])) {
1512             $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1513         } else {
1514             array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1515         }
1516         $accessdata['dr'] = $CFG->defaultuserroleid;
1517     }
1519     //
1520     // provide "default frontpage role"
1521     //
1522     if (!empty($CFG->defaultfrontpageroleid)) {
1523         $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
1524         $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
1525         if (!isset($accessdata['ra'][$base])) {
1526             $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
1527         } else {
1528             array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
1529         }
1530     }
1531     // for dirty timestamps in cron
1532     $accessdata['time'] = time();
1534     $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
1535     compact_rdefs($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['rdef']);
1537     return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
1540 /**
1541  * Use shared copy of role definistions stored in ACCESSLIB_PRIVATE->roledefinitions;
1542  *
1543  * @global object
1544  * @param array $rdefs array of role definitions in contexts
1545  */
1546 function compact_rdefs(&$rdefs) {
1547     global $ACCESSLIB_PRIVATE;
1549     /*
1550      * This is a basic sharing only, we could also
1551      * use md5 sums of values. The main purpose is to
1552      * reduce mem in cron jobs - many users in $ACCESSLIB_PRIVATE->accessdatabyuser array.
1553      */
1555     foreach ($rdefs as $key => $value) {
1556         if (!array_key_exists($key, $ACCESSLIB_PRIVATE->roledefinitions)) {
1557             $ACCESSLIB_PRIVATE->roledefinitions[$key] = $rdefs[$key];
1558         }
1559         $rdefs[$key] =& $ACCESSLIB_PRIVATE->roledefinitions[$key];
1560     }
1563 /**
1564  * A convenience function to completely load all the capabilities
1565  * for the current user.   This is what gets called from complete_user_login()
1566  * for example. Call it only _after_ you've setup $USER and called
1567  * check_enrolment_plugins();
1568  * @see check_enrolment_plugins()
1569  *
1570  * @global object
1571  * @global object
1572  * @global object
1573  */
1574 function load_all_capabilities() {
1575     global $USER, $CFG, $ACCESSLIB_PRIVATE;
1577     // roles not installed yet - we are in the middle of installation
1578     if (during_initial_install()) {
1579         return;
1580     }
1582     $base = '/'.SYSCONTEXTID;
1584     if (isguestuser()) {
1585         $guest = get_guest_role();
1587         // Load the rdefs
1588         $USER->access = get_role_access($guest->id);
1589         // Put the ghost enrolment in place...
1590         $USER->access['ra'][$base] = array($guest->id);
1593     } else if (isloggedin()) {
1595         $accessdata = get_user_access_sitewide($USER->id);
1597         //
1598         // provide "default role" & set 'dr'
1599         //
1600         if (!empty($CFG->defaultuserroleid)) {
1601             $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
1602             if (!isset($accessdata['ra'][$base])) {
1603                 $accessdata['ra'][$base] = array($CFG->defaultuserroleid);
1604             } else {
1605                 array_push($accessdata['ra'][$base], $CFG->defaultuserroleid);
1606             }
1607             $accessdata['dr'] = $CFG->defaultuserroleid;
1608         }
1610         $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
1612         //
1613         // provide "default frontpage role"
1614         //
1615         if (!empty($CFG->defaultfrontpageroleid)) {
1616             $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
1617             $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
1618             if (!isset($accessdata['ra'][$base])) {
1619                 $accessdata['ra'][$base] = array($CFG->defaultfrontpageroleid);
1620             } else {
1621                 array_push($accessdata['ra'][$base], $CFG->defaultfrontpageroleid);
1622             }
1623         }
1624         $USER->access = $accessdata;
1626     } else if (!empty($CFG->notloggedinroleid)) {
1627         $USER->access = get_role_access($CFG->notloggedinroleid);
1628         $USER->access['ra'][$base] = array($CFG->notloggedinroleid);
1629     }
1631     // Timestamp to read dirty context timestamps later
1632     $USER->access['time'] = time();
1633     $ACCESSLIB_PRIVATE->dirtycontexts = array();
1635     // Clear to force a refresh
1636     unset($USER->mycourses);
1639 /**
1640  * A convenience function to completely reload all the capabilities
1641  * for the current user when roles have been updated in a relevant
1642  * context -- but PRESERVING switchroles and loginas.
1643  *
1644  * That is - completely transparent to the user.
1645  *
1646  * Note: rewrites $USER->access completely.
1647  *
1648  * @global object
1649  * @global object
1650  */
1651 function reload_all_capabilities() {
1652     global $USER, $DB;
1654     // error_log("reloading");
1655     // copy switchroles
1656     $sw = array();
1657     if (isset($USER->access['rsw'])) {
1658         $sw = $USER->access['rsw'];
1659         // error_log(print_r($sw,1));
1660     }
1662     unset($USER->access);
1663     unset($USER->mycourses);
1665     load_all_capabilities();
1667     foreach ($sw as $path => $roleid) {
1668         $context = $DB->get_record('context', array('path'=>$path));
1669         role_switch($roleid, $context);
1670     }
1674 /**
1675  * Adds a temp role to an accessdata array.
1676  *
1677  * Useful for the "temporary guest" access
1678  * we grant to logged-in users.
1679  *
1680  * Note - assumes a course context!
1681  *
1682  * @global object
1683  * @global object
1684  * @param object $content
1685  * @param int $roleid
1686  * @param array $accessdata
1687  * @return array Returns access data
1688  */
1689 function load_temp_role($context, $roleid, $accessdata) {
1691     global $CFG, $DB;
1693     //
1694     // Load rdefs for the role in -
1695     // - this context
1696     // - all the parents
1697     // - and below - IOWs overrides...
1698     //
1700     // turn the path into a list of context ids
1701     $contexts = substr($context->path, 1); // kill leading slash
1702     $contexts = str_replace('/', ',', $contexts);
1704     $sql = "SELECT ctx.path, rc.capability, rc.permission
1705               FROM {context} ctx
1706               JOIN {role_capabilities} rc
1707                    ON rc.contextid=ctx.id
1708              WHERE (ctx.id IN ($contexts)
1709                     OR ctx.path LIKE ?)
1710                    AND rc.roleid = ?
1711           ORDER BY ctx.depth, ctx.path";
1712     $params = array($context->path."/%", $roleid);
1713     if ($rs = $DB->get_recordset_sql($sql, $params)) {
1714         foreach ($rs as $rd) {
1715             $k = "{$rd->path}:{$roleid}";
1716             $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
1717         }
1718         $rs->close();
1719     }
1721     //
1722     // Say we loaded everything for the course context
1723     // - which we just did - if the user gets a proper
1724     // RA in this session, this data will need to be reloaded,
1725     // but that is handled by the complete accessdata reload
1726     //
1727     array_push($accessdata['loaded'], $context->path);
1729     //
1730     // Add the ghost RA
1731     //
1732     if (isset($accessdata['ra'][$context->path])) {
1733         array_push($accessdata['ra'][$context->path], $roleid);
1734     } else {
1735         $accessdata['ra'][$context->path] = array($roleid);
1736     }
1738     return $accessdata;
1742 /**
1743  * Check all the login enrolment information for the given user object
1744  * by querying the enrolment plugins
1745  *
1746  * @global object
1747  * @param object $user
1748  * @return void
1749  */
1750 function check_enrolment_plugins(&$user) {
1751     global $CFG;
1753     if (empty($user->id) or isguestuser($user)) {
1754         // shortcut - there is no enrolment work for guests and not-logged-in users
1755         return;
1756     }
1758     static $inprogress = array();  // To prevent this function being called more than once in an invocation
1760     if (!empty($inprogress[$user->id])) {
1761         return;
1762     }
1764     $inprogress[$user->id] = true;  // Set the flag
1766     require_once($CFG->dirroot .'/enrol/enrol.class.php');
1768     if (!($plugins = explode(',', $CFG->enrol_plugins_enabled))) {
1769         $plugins = array($CFG->enrol);
1770     }
1772     foreach ($plugins as $plugin) {
1773         $enrol = enrolment_factory::factory($plugin);
1774         if (method_exists($enrol, 'setup_enrolments')) {  /// Plugin supports Roles (Moodle 1.7 and later)
1775             $enrol->setup_enrolments($user);
1776         } else {                                          /// Run legacy enrolment methods
1777             if (method_exists($enrol, 'get_student_courses')) {
1778                 $enrol->get_student_courses($user);
1779             }
1780             if (method_exists($enrol, 'get_teacher_courses')) {
1781                 $enrol->get_teacher_courses($user);
1782             }
1784         /// deal with $user->students and $user->teachers stuff
1785             unset($user->student);
1786             unset($user->teacher);
1787         }
1788         unset($enrol);
1789     }
1791     unset($inprogress[$user->id]);  // Unset the flag
1794 /**
1795  * Returns array of all role archetypes.
1796  *
1797  * @return array
1798  */
1799 function get_role_archetypes() {
1800     return array(
1801         'manager'        => 'manager',
1802         'coursecreator'  => 'coursecreator',
1803         'editingteacher' => 'editingteacher',
1804         'teacher'        => 'teacher',
1805         'student'        => 'student',
1806         'guest'          => 'guest',
1807         'user'           => 'user',
1808         'frontpage'      => 'frontpage'
1809     );
1812 /**
1813  * Assign the defaults found in this capabality definition to roles that have
1814  * the corresponding legacy capabilities assigned to them.
1815  *
1816  * @param string $capability
1817  * @param array $legacyperms an array in the format (example):
1818  *                      'guest' => CAP_PREVENT,
1819  *                      'student' => CAP_ALLOW,
1820  *                      'teacher' => CAP_ALLOW,
1821  *                      'editingteacher' => CAP_ALLOW,
1822  *                      'coursecreator' => CAP_ALLOW,
1823  *                      'manager' => CAP_ALLOW
1824  * @return boolean success or failure.
1825  */
1826 function assign_legacy_capabilities($capability, $legacyperms) {
1828     $archetypes = get_role_archetypes();
1830     foreach ($legacyperms as $type => $perm) {
1832         $systemcontext = get_context_instance(CONTEXT_SYSTEM);
1833         if ($type === 'admin') {
1834             debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1835             $type = 'manager';
1836         }
1838         if (!array_key_exists($type, $archetypes)) {
1839             print_error('invalidlegacy', '', '', $type);
1840         }
1842         if ($roles = get_archetype_roles($type)) {
1843             foreach ($roles as $role) {
1844                 // Assign a site level capability.
1845                 if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1846                     return false;
1847                 }
1848             }
1849         }
1850     }
1851     return true;
1854 /**
1855  * @param object $capability a capbility - a row from the capabilitites table.
1856  * @return boolean whether this capability is safe - that is, wether people with the
1857  *      safeoverrides capability should be allowed to change it.
1858  */
1859 function is_safe_capability($capability) {
1860     return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1863 /**********************************
1864  * Context Manipulation functions *
1865  **********************************/
1867 /**
1868  * Create a new context record for use by all roles-related stuff
1869  *
1870  * Create a new context record for use by all roles-related stuff
1871  * assumes that the caller has done the homework.
1872  *
1873  * @global object
1874  * @global object
1875  * @param int $contextlevel
1876  * @param int $instanceid
1877  * @param int $strictness
1878  * @return object newly created context
1879  */
1880 function create_context($contextlevel, $instanceid, $strictness=IGNORE_MISSING) {
1882     global $CFG, $DB;
1884     if ($contextlevel == CONTEXT_SYSTEM) {
1885         return create_system_context();
1886     }
1888     $context = new object();
1889     $context->contextlevel = $contextlevel;
1890     $context->instanceid = $instanceid;
1892     // Define $context->path based on the parent
1893     // context. In other words... Who is your daddy?
1894     $basepath  = '/' . SYSCONTEXTID;
1895     $basedepth = 1;
1897     $result = true;
1898     $error_message = NULL;
1900     switch ($contextlevel) {
1901         case CONTEXT_COURSECAT:
1902             $sql = "SELECT ctx.path, ctx.depth
1903                       FROM {context}           ctx
1904                       JOIN {course_categories} cc
1905                            ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
1906                      WHERE cc.id=?";
1907             $params = array($instanceid);
1908             if ($p = $DB->get_record_sql($sql, $params)) {
1909                 $basepath  = $p->path;
1910                 $basedepth = $p->depth;
1911             } else if ($category = $DB->get_record('course_categories', array('id'=>$instanceid), '*', $strictness)) {
1912                 if (empty($category->parent)) {
1913                     // ok - this is a top category
1914                 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) {
1915                     $basepath  = $parent->path;
1916                     $basedepth = $parent->depth;
1917                 } else {
1918                     // wrong parent category - no big deal, this can be fixed later
1919                     $basepath  = NULL;
1920                     $basedepth = 0;
1921                 }
1922             } else {
1923                 // incorrect category id
1924                 $error_message = "incorrect course category id ($instanceid)";
1925                 $result = false;
1926             }
1927             break;
1929         case CONTEXT_COURSE:
1930             $sql = "SELECT ctx.path, ctx.depth
1931                       FROM {context} ctx
1932                       JOIN {course}  c
1933                            ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
1934                      WHERE c.id=? AND c.id !=" . SITEID;
1935             $params = array($instanceid);
1936             if ($p = $DB->get_record_sql($sql, $params)) {
1937                 $basepath  = $p->path;
1938                 $basedepth = $p->depth;
1939             } else if ($course = $DB->get_record('course', array('id'=>$instanceid), '*', $strictness)) {
1940                 if ($course->id == SITEID) {
1941                     //ok - no parent category
1942                 } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) {
1943                     $basepath  = $parent->path;
1944                     $basedepth = $parent->depth;
1945                 } else {
1946                     // wrong parent category of course - no big deal, this can be fixed later
1947                     $basepath  = NULL;
1948                     $basedepth = 0;
1949                 }
1950             } else if ($instanceid == SITEID) {
1951                 // no errors for missing site course during installation
1952                 return false;
1953             } else {
1954                 // incorrect course id
1955                 $error_message = "incorrect course id ($instanceid)";
1956                 $result = false;
1957             }
1958             break;
1960         case CONTEXT_MODULE:
1961             $sql = "SELECT ctx.path, ctx.depth
1962                       FROM {context}        ctx
1963                       JOIN {course_modules} cm
1964                            ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
1965                      WHERE cm.id=?";
1966             $params = array($instanceid);
1967             if ($p = $DB->get_record_sql($sql, $params)) {
1968                 $basepath  = $p->path;
1969                 $basedepth = $p->depth;
1970             } else if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid), '*', $strictness)) {
1971                 if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course, $strictness)) {
1972                     $basepath  = $parent->path;
1973                     $basedepth = $parent->depth;
1974                 } else {
1975                     // course does not exist - modules can not exist without a course
1976                     $error_message = "course does not exist ($cm->course) - modules can not exist without a course";
1977                     $result = false;
1978                 }
1979             } else {
1980                 // cm does not exist
1981                 $error_message = "cm with id $instanceid does not exist";
1982                 $result = false;
1983             }
1984             break;
1986         case CONTEXT_BLOCK:
1987             $sql = "SELECT ctx.path, ctx.depth
1988                       FROM {context} ctx
1989                       JOIN {block_instances} bi ON (bi.parentcontextid=ctx.id)
1990                      WHERE bi.id = ?";
1991             $params = array($instanceid, CONTEXT_COURSE);
1992             if ($p = $DB->get_record_sql($sql, $params, '*', $strictness)) {
1993                 $basepath  = $p->path;
1994                 $basedepth = $p->depth;
1995             } else {
1996                 // block does not exist
1997                 $error_message = 'block or parent context does not exist';
1998                 $result = false;
1999             }
2000             break;
2001         case CONTEXT_USER:
2002             // default to basepath
2003             break;
2004     }
2006     // if grandparents unknown, maybe rebuild_context_path() will solve it later
2007     if ($basedepth != 0) {
2008         $context->depth = $basedepth+1;
2009     }
2011     if (!$result) {
2012         debugging('Error: could not insert new context level "'.
2013                   s($contextlevel).'", instance "'.
2014                   s($instanceid).'". ' . $error_message);
2016         return false;
2017     }
2019     $id = $DB->insert_record('context', $context);
2020     // can't set the full path till we know the id!
2021     if ($basedepth != 0 and !empty($basepath)) {
2022         $DB->set_field('context', 'path', $basepath.'/'. $id, array('id'=>$id));
2023     }
2024     return get_context_instance_by_id($id);
2027 /**
2028  * Returns system context or NULL if can not be created yet.
2029  *
2030  * @todo can not use get_record() because we do not know if query failed :-(
2031  * switch to get_record() later
2032  *
2033  * @global object
2034  * @global object
2035  * @param bool $cache use caching
2036  * @return mixed system context or NULL
2037  */
2038 function get_system_context($cache=true) {
2039     global $DB, $ACCESSLIB_PRIVATE;
2040     if ($cache and defined('SYSCONTEXTID')) {
2041         if (is_null($ACCESSLIB_PRIVATE->systemcontext)) {
2042             $ACCESSLIB_PRIVATE->systemcontext = new object();
2043             $ACCESSLIB_PRIVATE->systemcontext->id           = SYSCONTEXTID;
2044             $ACCESSLIB_PRIVATE->systemcontext->contextlevel = CONTEXT_SYSTEM;
2045             $ACCESSLIB_PRIVATE->systemcontext->instanceid   = 0;
2046             $ACCESSLIB_PRIVATE->systemcontext->path         = '/'.SYSCONTEXTID;
2047             $ACCESSLIB_PRIVATE->systemcontext->depth        = 1;
2048         }
2049         return $ACCESSLIB_PRIVATE->systemcontext;
2050     }
2051     try {
2052         $context = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM));
2053     } catch (dml_exception $e) {
2054         //table does not exist yet, sorry
2055         return NULL;
2056     }
2058     if (!$context) {
2059         $context = new object();
2060         $context->contextlevel = CONTEXT_SYSTEM;
2061         $context->instanceid   = 0;
2062         $context->depth        = 1;
2063         $context->path         = NULL; //not known before insert
2065         try {
2066             $context->id = $DB->insert_record('context', $context);
2067         } catch (dml_exception $e) {
2068             // can not create context yet, sorry
2069             return NULL;
2070         }
2071     }
2073     if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) {
2074         $context->instanceid   = 0;
2075         $context->path         = '/'.$context->id;
2076         $context->depth        = 1;
2077         $DB->update_record('context', $context);
2078     }
2080     if (!defined('SYSCONTEXTID')) {
2081         define('SYSCONTEXTID', $context->id);
2082     }
2084     $ACCESSLIB_PRIVATE->systemcontext = $context;
2085     return $ACCESSLIB_PRIVATE->systemcontext;
2088 /**
2089  * Remove a context record and any dependent entries,
2090  * removes context from static context cache too
2091  *
2092  * @global object
2093  * @global object
2094  * @param int $level
2095  * @param int $instanceid
2096  * @return bool properly deleted
2097  */
2098 function delete_context($contextlevel, $instanceid) {
2099     global $DB, $ACCESSLIB_PRIVATE, $CFG;
2101     // do not use get_context_instance(), because the related object might not exist,
2102     // or the context does not exist yet and it would be created now
2103     if ($context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
2104         $result = $DB->delete_records('role_assignments', array('contextid'=>$context->id)) &&
2105                   $DB->delete_records('role_capabilities', array('contextid'=>$context->id)) &&
2106                   $DB->delete_records('context', array('id'=>$context->id)) &&
2107                   $DB->delete_records('role_names', array('contextid'=>$context->id));
2109         // do not mark dirty contexts if parents unknown
2110         if (!is_null($context->path) and $context->depth > 0) {
2111             mark_context_dirty($context->path);
2112         }
2114         // purge static context cache if entry present
2115         unset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instanceid]);
2116         unset($ACCESSLIB_PRIVATE->contextsbyid[$context->id]);
2118         blocks_delete_all_for_context($context->id);
2119         filter_delete_all_for_context($context->id);
2121         return $result;
2122     } else {
2124         return true;
2125     }
2128 /**
2129  * Precreates all contexts including all parents
2130  *
2131  * @global object
2132  * @param int $contextlevel empty means all
2133  * @param bool $buildpaths update paths and depths
2134  * @return void
2135  */
2136 function create_contexts($contextlevel=NULL, $buildpaths=true) {
2137     global $DB;
2139     //make sure system context exists
2140     $syscontext = get_system_context(false);
2142     if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT
2143                              or $contextlevel == CONTEXT_COURSE
2144                              or $contextlevel == CONTEXT_MODULE
2145                              or $contextlevel == CONTEXT_BLOCK) {
2146         $sql = "INSERT INTO {context} (contextlevel, instanceid)
2147                 SELECT ".CONTEXT_COURSECAT.", cc.id
2148                   FROM {course}_categories cc
2149                  WHERE NOT EXISTS (SELECT 'x'
2150                                      FROM {context} cx
2151                                     WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
2152         $DB->execute($sql);
2154     }
2156     if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE
2157                              or $contextlevel == CONTEXT_MODULE
2158                              or $contextlevel == CONTEXT_BLOCK) {
2159         $sql = "INSERT INTO {context} (contextlevel, instanceid)
2160                 SELECT ".CONTEXT_COURSE.", c.id
2161                   FROM {course} c
2162                  WHERE NOT EXISTS (SELECT 'x'
2163                                      FROM {context} cx
2164                                     WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
2165         $DB->execute($sql);
2167     }
2169     if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE
2170                              or $contextlevel == CONTEXT_BLOCK) {
2171         $sql = "INSERT INTO {context} (contextlevel, instanceid)
2172                 SELECT ".CONTEXT_MODULE.", cm.id
2173                   FROM {course}_modules cm
2174                  WHERE NOT EXISTS (SELECT 'x'
2175                                      FROM {context} cx
2176                                     WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
2177         $DB->execute($sql);
2178     }
2180     if (empty($contextlevel) or $contextlevel == CONTEXT_USER
2181                              or $contextlevel == CONTEXT_BLOCK) {
2182         $sql = "INSERT INTO {context} (contextlevel, instanceid)
2183                 SELECT ".CONTEXT_USER.", u.id
2184                   FROM {user} u
2185                  WHERE u.deleted=0
2186                    AND NOT EXISTS (SELECT 'x'
2187                                      FROM {context} cx
2188                                     WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
2189         $DB->execute($sql);
2191     }
2193     if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) {
2194         $sql = "INSERT INTO {context} (contextlevel, instanceid)
2195                 SELECT ".CONTEXT_BLOCK.", bi.id
2196                   FROM {block_instances} bi
2197                  WHERE NOT EXISTS (SELECT 'x'
2198                                      FROM {context} cx
2199                                     WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
2200         $DB->execute($sql);
2201     }
2203     if ($buildpaths) {
2204         build_context_path(false);
2205     }
2208 /**
2209  * Remove stale context records
2210  *
2211  * @global object
2212  * @return bool
2213  */
2214 function cleanup_contexts() {
2215     global $DB;
2217     $sql = "  SELECT c.contextlevel,
2218                      c.instanceid AS instanceid
2219                 FROM {context} c
2220                 LEFT OUTER JOIN {course}_categories t
2221                      ON c.instanceid = t.id
2222                WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
2223             UNION
2224               SELECT c.contextlevel,
2225                      c.instanceid
2226                 FROM {context} c
2227                 LEFT OUTER JOIN {course} t
2228                      ON c.instanceid = t.id
2229                WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
2230             UNION
2231               SELECT c.contextlevel,
2232                      c.instanceid
2233                 FROM {context} c
2234                 LEFT OUTER JOIN {course}_modules t
2235                      ON c.instanceid = t.id
2236                WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
2237             UNION
2238               SELECT c.contextlevel,
2239                      c.instanceid
2240                 FROM {context} c
2241                 LEFT OUTER JOIN {user} t
2242                      ON c.instanceid = t.id
2243                WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
2244             UNION
2245               SELECT c.contextlevel,
2246                      c.instanceid
2247                 FROM {context} c
2248                 LEFT OUTER JOIN {block_instances} t
2249                      ON c.instanceid = t.id
2250                WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
2251            ";
2253     // transactions used only for performance reasons here
2254     $transaction = $DB->start_delegated_transaction();
2256     if ($rs = $DB->get_recordset_sql($sql)) {
2257         foreach ($rs as $ctx) {
2258             delete_context($ctx->contextlevel, $ctx->instanceid);
2259         }
2260         $rs->close();
2261     }
2263     $transaction->allow_commit();
2264     return true;
2267 /**
2268  * Preloads all contexts relating to a course: course, modules. Block contexts
2269  * are no longer loaded here. The contexts for all the blocks on the current
2270  * page are now efficiently loaded by {@link block_manager::load_blocks()}.
2271  *
2272  * @param int $courseid Course ID
2273  * @return void
2274  */
2275 function preload_course_contexts($courseid) {
2276     global $DB, $ACCESSLIB_PRIVATE;
2278     // Users can call this multiple times without doing any harm
2279     global $ACCESSLIB_PRIVATE;
2280     if (array_key_exists($courseid, $ACCESSLIB_PRIVATE->preloadedcourses)) {
2281         return;
2282     }
2284     $params = array($courseid, $courseid, $courseid);
2285     $sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2286               FROM {course_modules} cm
2287               JOIN {context} x ON x.instanceid=cm.id
2288              WHERE cm.course=? AND x.contextlevel=".CONTEXT_MODULE."
2290          UNION ALL
2292             SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
2293               FROM {context} x
2294              WHERE x.instanceid=? AND x.contextlevel=".CONTEXT_COURSE."";
2296     $rs = $DB->get_recordset_sql($sql, $params);
2297     foreach($rs as $context) {
2298         cache_context($context);
2299     }
2300     $rs->close();
2301     $ACCESSLIB_PRIVATE->preloadedcourses[$courseid] = true;
2304 /**
2305  * Get the context instance as an object. This function will create the
2306  * context instance if it does not exist yet.
2307  *
2308  * @todo Remove code branch from previous fix MDL-9016 which is no longer needed
2309  *
2310  * @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE.
2311  * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id,
2312  *      for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0
2313  * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
2314  *      MUST_EXIST means throw exception if no record or multiple records found
2315  * @return object The context object.
2316  */
2317 function get_context_instance($contextlevel, $instance=0, $strictness=IGNORE_MISSING) {
2319     global $DB, $ACCESSLIB_PRIVATE;
2320     static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_BLOCK);
2322     if ($contextlevel === 'clearcache') {
2323         // TODO: Remove for v2.0
2324         // No longer needed, but we'll catch it to avoid erroring out on custom code.
2325         // This used to be a fix for MDL-9016
2326         // "Restoring into existing course, deleting first
2327         //  deletes context and doesn't recreate it"
2328         return false;
2329     }
2331 /// System context has special cache
2332     if ($contextlevel == CONTEXT_SYSTEM) {
2333         return get_system_context();
2334     }
2336 /// check allowed context levels
2337     if (!in_array($contextlevel, $allowed_contexts)) {
2338         // fatal error, code must be fixed - probably typo or switched parameters
2339         print_error('invalidcourselevel');
2340     }
2342     if (!is_array($instance)) {
2343     /// Check the cache
2344         if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) {  // Already cached
2345             return $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
2346         }
2348     /// Get it from the database, or create it
2349         if (!$context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instance))) {
2350             $context = create_context($contextlevel, $instance, $strictness);
2351         }
2353     /// Only add to cache if context isn't empty.
2354         if (!empty($context)) {
2355             cache_context($context);
2356         }
2358         return $context;
2359     }
2362 /// ok, somebody wants to load several contexts to save some db queries ;-)
2363     $instances = $instance;
2364     $result = array();
2366     foreach ($instances as $key=>$instance) {
2367     /// Check the cache first
2368         if (isset($ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance])) {  // Already cached
2369             $result[$instance] = $ACCESSLIB_PRIVATE->contexts[$contextlevel][$instance];
2370             unset($instances[$key]);
2371             continue;
2372         }
2373     }
2375     if ($instances) {
2376         list($instanceids, $params) = $DB->get_in_or_equal($instances, SQL_PARAMS_QM);
2377         array_unshift($params, $contextlevel);
2378         $sql = "SELECT instanceid, id, contextlevel, path, depth
2379                   FROM {context}
2380                  WHERE contextlevel=? AND instanceid $instanceids";
2382         if (!$contexts = $DB->get_records_sql($sql, $params)) {
2383             $contexts = array();
2384         }
2386         foreach ($instances as $instance) {
2387             if (isset($contexts[$instance])) {
2388                 $context = $contexts[$instance];
2389             } else {
2390                 $context = create_context($contextlevel, $instance);
2391             }
2393             if (!empty($context)) {
2394                 cache_context($context);
2395             }
2397             $result[$instance] = $context;
2398         }
2399     }
2401     return $result;
2405 /**
2406  * Get a context instance as an object, from a given context id.
2407  *
2408  * @param mixed $id a context id or array of ids.
2409  * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
2410  *                        MUST_EXIST means throw exception if no record or multiple records found
2411  * @return mixed object, array of the context object, or false.
2412  */
2413 function get_context_instance_by_id($id, $strictness=IGNORE_MISSING) {
2414     global $DB, $ACCESSLIB_PRIVATE;
2416     if ($id == SYSCONTEXTID) {
2417         return get_system_context();
2418     }
2420     if (isset($ACCESSLIB_PRIVATE->contextsbyid[$id])) {  // Already cached
2421         return $ACCESSLIB_PRIVATE->contextsbyid[$id];
2422     }
2424     if ($context = $DB->get_record('context', array('id'=>$id), '*', $strictness)) {
2425         cache_context($context);
2426         return $context;
2427     }
2429     return false;
2433 /**
2434  * Get the local override (if any) for a given capability in a role in a context
2435  *
2436  * @global object
2437  * @param int $roleid
2438  * @param int $contextid
2439  * @param string $capability
2440  */
2441 function get_local_override($roleid, $contextid, $capability) {
2442     global $DB;
2443     return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
2446 /**
2447  * Returns context instance plus related course and cm instances
2448  * @param int $contextid
2449  * @return array of ($context, $course, $cm)
2450  */
2451 function get_context_info_array($contextid) {
2452     global $DB;
2454     $context = get_context_instance_by_id($contextid, MUST_EXIST);
2455     $course  = NULL;
2456     $cm      = NULL;
2458     if ($context->contextlevel == CONTEXT_COURSE) {
2459         $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
2461     } else if ($context->contextlevel == CONTEXT_MODULE) {
2462         $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
2463         $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
2465     } else if ($context->contextlevel == CONTEXT_BLOCK) {
2466         $parentcontexts = get_parent_contexts($context, false);
2467         $parent = reset($parentcontexts);
2468         $parent = get_context_instance_by_id($parent);
2470         if ($parent->contextlevel == CONTEXT_COURSE) {
2471             $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
2472         } else if ($parent->contextlevel == CONTEXT_MODULE) {
2473             $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
2474             $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
2475         }
2476     }
2478     return array($context, $course, $cm);
2482 //////////////////////////////////////
2483 //    DB TABLE RELATED FUNCTIONS    //
2484 //////////////////////////////////////
2486 /**
2487  * function that creates a role
2488  *
2489  * @global object
2490  * @param string $name role name
2491  * @param string $shortname role short name
2492  * @param string $description role description
2493  * @param string $archetype
2494  * @return mixed id or dml_exception
2495  */
2496 function create_role($name, $shortname, $description, $archetype='') {
2497     global $DB;
2499     if (strpos($archetype, 'moodle/legacy:') !== false) {
2500         throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
2501     }
2503     // verify role archetype actually exists
2504     $archetypes = get_role_archetypes();
2505     if (empty($archetypes[$archetype])) {
2506         $archetype = '';
2507     }
2509     // Get the system context.
2510     $context = get_context_instance(CONTEXT_SYSTEM);
2512     // Insert the role record.
2513     $role = new object();
2514     $role->name        = $name;
2515     $role->shortname   = $shortname;
2516     $role->description = $description;
2517     $role->archetype   = $archetype;
2519     //find free sortorder number
2520     $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
2521     if (empty($role->sortorder)) {
2522         $role->sortorder = 1;
2523     }
2524     $id = $DB->insert_record('role', $role);
2526     return $id;
2529 /**
2530  * Function that deletes a role and cleanups up after it
2531  *
2532  * @param int $roleid id of role to delete
2533  * @return bool lways true
2534  */
2535 function delete_role($roleid) {
2536     global $CFG, $DB;
2538     // first unssign all users
2539     role_unassign($roleid);
2541     // cleanup all references to this role, ignore errors
2542     $DB->delete_records('role_capabilities',   array('roleid'=>$roleid));
2543     $DB->delete_records('role_allow_assign',   array('roleid'=>$roleid));
2544     $DB->delete_records('role_allow_assign',   array('allowassign'=>$roleid));
2545     $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
2546     $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
2547     $DB->delete_records('role_names',          array('roleid'=>$roleid));
2548     $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
2550     // finally delete the role itself
2551     // get this before the name is gone for logging
2552     $rolename = $DB->get_field('role', 'name', array('id'=>$roleid));
2554     $DB->delete_records('role', array('id'=>$roleid));
2556     add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '');
2558     return true;
2561 /**
2562  * Function to write context specific overrides, or default capabilities.
2563  *
2564  * @global object
2565  * @global object
2566  * @param string module string name
2567  * @param string capability string name
2568  * @param int contextid context id
2569  * @param int roleid role id
2570  * @param int permission int 1,-1 or -1000 should not be writing if permission is 0
2571  * @return bool
2572  */
2573 function assign_capability($capability, $permission, $roleid, $contextid, $overwrite=false) {
2574     global $USER, $DB;
2576     if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
2577         unassign_capability($capability, $roleid, $contextid);
2578         return true;
2579     }
2581     $existing = $DB->get_record('role_capabilities', array('contextid'=>$contextid, 'roleid'=>$roleid, 'capability'=>$capability));
2583     if ($existing and !$overwrite) {   // We want to keep whatever is there already
2584         return true;
2585     }
2587     $cap = new object;
2588     $cap->contextid = $contextid;
2589     $cap->roleid = $roleid;
2590     $cap->capability = $capability;
2591     $cap->permission = $permission;
2592     $cap->timemodified = time();
2593     $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
2595     if ($existing) {
2596         $cap->id = $existing->id;
2597         $DB->update_record('role_capabilities', $cap);
2598     } else {
2599         $c = $DB->get_record('context', array('id'=>$contextid));
2600         $DB->insert_record('role_capabilities', $cap);
2601     }
2602     return true;
2605 /**
2606  * Unassign a capability from a role.
2607  *
2608  * @global object
2609  * @param int $roleid the role id
2610  * @param string $capability the name of the capability
2611  * @return boolean success or failure
2612  */
2613 function unassign_capability($capability, $roleid, $contextid=NULL) {
2614     global $DB;
2616     if (!empty($contextid)) {
2617         // delete from context rel, if this is the last override in this context
2618         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$contextid));
2619     } else {
2620         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
2621     }
2622     return true;
2626 /**
2627  * Get the roles that have a given capability assigned to it
2628  * Get the roles that have a given capability assigned to it. This function
2629  * does not resolve the actual permission of the capability. It just checks
2630  * for assignment only.
2631  *
2632  * @global object
2633  * @global object
2634  * @param string $capability - capability name (string)
2635  * @param string $permission - optional, the permission defined for this capability
2636  *                      either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to NULL
2637  * @param object $contect
2638  * @return mixed array or role objects
2639  */
2640 function get_roles_with_capability($capability, $permission=NULL, $context=NULL) {
2641     global $CFG, $DB;
2643     $params = array();
2645     if ($context) {
2646         if ($contexts = get_parent_contexts($context)) {
2647             $listofcontexts = '('.implode(',', $contexts).')';
2648         } else {
2649             $sitecontext = get_context_instance(CONTEXT_SYSTEM);
2650             $listofcontexts = '('.$sitecontext->id.')'; // must be site
2651         }
2652         $contextstr = "AND (rc.contextid = ? OR  rc.contextid IN $listofcontexts)";
2653         $params[] = $context->id;
2654     } else {
2655         $contextstr = '';
2656     }
2658     $selectroles = "SELECT r.*
2659                       FROM {role} r,
2660                            {role_capabilities} rc
2661                      WHERE rc.capability = ?
2662                            AND rc.roleid = r.id $contextstr";
2664     array_unshift($params, $capability);
2666     if (isset($permission)) {
2667         $selectroles .= " AND rc.permission = ?";
2668         $params[] = $permission;
2669     }
2670     return $DB->get_records_sql($selectroles, $params);
2674 /**
2675  * This function makes a role-assignment (a role for a user or group in a particular context)
2676  *
2677  * @param int $roleid the role of the id
2678  * @param int $userid userid
2679  * @param int $groupid group id
2680  * @param int $contextid id of the context
2681  * @param int $timestart time this assignment becomes effective defaults to 0
2682  * @param int $timeend time this assignemnt ceases to be effective defaults to 0
2683  * @param int $hidden_ignored - use roels with moodle/course:view capability or enrolemnt instead
2684  * @param string $enrol defaults to 'manual'
2685  * @param string $timemodified defaults to ''
2686  * @return int new id of the assigment
2687  */
2688 function role_assign($roleid, $userid, $groupid, $contextid, $timestart=0, $timeend=0, $hidden_ignored=0, $enrol='manual',$timemodified='') {
2689     global $USER, $CFG, $DB;
2691 /// Do some data validation
2693     if (empty($roleid)) {
2694         debugging('Role ID not provided');
2695         return false;
2696     }
2698     if (empty($userid) && empty($groupid)) {
2699         debugging('Either userid or groupid must be provided');
2700         return false;
2701     }
2703     if ($userid && !$DB->record_exists('user', array('id'=>$userid))) {
2704         debugging('User ID '.intval($userid).' does not exist!');
2705         return false;
2706     }
2708     if ($groupid && !groups_group_exists($groupid)) {
2709         debugging('Group ID '.intval($groupid).' does not exist!');
2710         return false;
2711     }
2713     if (!$context = get_context_instance_by_id($contextid)) {
2714         debugging('Context ID '.intval($contextid).' does not exist!');
2715         return false;
2716     }
2718     if (($timestart and $timeend) and ($timestart > $timeend)) {
2719         debugging('The end time can not be earlier than the start time');
2720         return false;
2721     }
2723     if (!$timemodified) {
2724         $timemodified = time();
2725     }
2727 /// Check for existing entry
2728     if ($userid) {
2729         $ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid));
2730     } else {
2731         $ra = $DB->get_record('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'groupid'=>$groupid));
2732     }
2734     if (empty($ra)) {             // Create a new entry
2735         $ra = new object();
2736         $ra->roleid = $roleid;
2737         $ra->contextid = $context->id;
2738         $ra->userid = $userid;
2739         $ra->enrol = $enrol;
2740     /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
2741     /// by repeating queries with the same exact parameters in a 100 secs time window
2742         $ra->timestart = round($timestart, -2);
2743         $ra->timeend = $timeend;
2744         $ra->timemodified = $timemodified;
2745         $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
2747         $ra->id = $DB->insert_record('role_assignments', $ra);
2749     } else {                      // We already have one, just update it
2750         $ra->id = $ra->id;
2751         $ra->enrol = $enrol;
2752     /// Always round timestart downto 100 secs to help DBs to use their own caching algorithms
2753     /// by repeating queries with the same exact parameters in a 100 secs time window
2754         $ra->timestart = round($timestart, -2);
2755         $ra->timeend = $timeend;
2756         $ra->timemodified = $timemodified;
2757         $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
2759         $DB->update_record('role_assignments', $ra);
2760     }
2762 /// mark context as dirty - modules might use has_capability() in xxx_role_assing()
2763 /// again expensive, but needed
2764     mark_context_dirty($context->path);
2766     if (!empty($USER->id) && $USER->id == $userid) {
2767 /// If the user is the current user, then do full reload of capabilities too.
2768         load_all_capabilities();
2769     }
2771 /// Ask all the modules if anything needs to be done for this user
2772     $mods = get_plugin_list('mod');
2773     foreach ($mods as $mod => $moddir) {
2774         include_once($moddir.'/lib.php');
2775         $functionname = $mod.'_role_assign';
2776         if (function_exists($functionname)) {
2777             $functionname($userid, $context, $roleid);
2778         }
2779     }
2781     /// now handle metacourse role assignments if in course context
2782     if ($context->contextlevel == CONTEXT_COURSE) {
2783         if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
2784             foreach ($parents as $parent) {
2785                 sync_metacourse($parent->parent_course);
2786             }
2787         }
2788     }
2790     events_trigger('role_assigned', $ra);
2792     return $ra->id;
2796 /**
2797  * Deletes one or more role assignments.   You must specify at least one parameter.
2798  *
2799  * @global object
2800  * @global object
2801  * @global object
2802  * @param int $roleid defaults to 0
2803  * @param int $userid defaults to 0
2804  * @param int $groupid defaults to 0
2805  * @param int $contextid defaults to 0
2806  * @param mixed $enrol unassign only if enrolment type matches, NULL means anything. Defaults to NULL
2807  * @return boolean success or failure
2808  */
2809 function role_unassign($roleid=0, $userid=0, $groupid=0, $contextid=0, $enrol=NULL) {
2811     global $USER, $CFG, $DB;
2812     require_once($CFG->dirroot.'/group/lib.php');
2814     $args = array('roleid', 'userid', 'groupid', 'contextid');
2815     $select = array();
2816     $params = array();
2818     foreach ($args as $arg) {
2819         if ($$arg) {
2820             $select[] = "$arg = ?";
2821             $params[] = $$arg;
2822         }
2823     }
2824     if (!empty($enrol)) {
2825         $select[] = "enrol=?";
2826         $params[] = $enrol;
2827     }
2829     if ($select) {
2830         if ($ras = $DB->get_records_select('role_assignments', implode(' AND ', $select), $params)) {
2831             $mods = get_plugin_list('mod');
2832             foreach($ras as $ra) {
2833                 /// infinite loop protection when deleting recursively
2834                 if (!$ra = $DB->get_record('role_assignments', array('id'=>$ra->id))) {
2835                     continue;
2836                 }
2837                 $DB->delete_records('role_assignments', array('id'=>$ra->id));
2839                 if (!$context = get_context_instance_by_id($ra->contextid)) {
2840                     // strange error, not much to do
2841                     continue;
2842                 }
2844                 /* mark contexts as dirty here, because we need the refreshed
2845                  * caps bellow to delete group membership and user_lastaccess!
2846                  * and yes, this is very expensive for bulk operations :-(
2847                  */
2848                 mark_context_dirty($context->path);
2850                 /// If the user is the current user, then do full reload of capabilities too.
2851                 if (!empty($USER->id) && $USER->id == $ra->userid) {
2852                     load_all_capabilities();
2853                 }
2855                 /// Ask all the modules if anything needs to be done for this user
2856                 foreach ($mods as $mod=>$moddir) {
2857                     include_once($moddir.'/lib.php');
2858                     $functionname = $mod.'_role_unassign';
2859                     if (function_exists($functionname)) {
2860                         $functionname($ra->userid, $context); // watch out, $context might be NULL if something goes wrong
2861                     }
2862                 }
2864                 /// now handle metacourse role unassigment and removing from goups if in course context
2865                 if ($context->contextlevel == CONTEXT_COURSE) {
2867                     // cleanup leftover course groups/subscriptions etc when user has
2868                     // no capability to view course
2869                     // this may be slow, but this is the proper way of doing it
2870                     if (!has_capability('moodle/course:participate', $context, $ra->userid)) {
2871                         // remove from groups
2872                         groups_delete_group_members($context->instanceid, $ra->userid);
2874                         // delete lastaccess records
2875                         $DB->delete_records('user_lastaccess', array('userid'=>$ra->userid, 'courseid'=>$context->instanceid));
2876                     }
2878                     //unassign roles in metacourses if needed
2879                     if ($parents = $DB->get_records('course_meta', array('child_course'=>$context->instanceid))) {
2880                         foreach ($parents as $parent) {
2881                             sync_metacourse($parent->parent_course);
2882                         }
2883                     }
2884                 }
2886                 events_trigger('role_unassigned', $ra);
2887             }
2888         }
2889     }
2891     return true;
2894 /**
2895  * Enrol someone without using the default role in a course
2896  *
2897  * A convenience function to take care of the common case where you
2898  * just want to enrol someone using the default role into a course
2899  *
2900  * @param object $course
2901  * @param object $user
2902  * @param string $enrol the plugin used to do this enrolment
2903  * @return bool
2904  */
2905 function enrol_into_course($course, $user, $enrol) {
2907     $timestart = time();
2908     // remove time part from the timestamp and keep only the date part
2909     $timestart = make_timestamp(date('Y', $timestart), date('m', $timestart), date('d', $timestart), 0, 0, 0);
2910     if ($course->enrolperiod) {
2911         $timeend = $timestart + $course->enrolperiod;
2912     } else {
2913         $timeend = 0;
2914     }
2916     if ($role = get_default_course_role($course)) {
2918         $context = get_context_instance(CONTEXT_COURSE, $course->id);
2920         if (!role_assign($role->id, $user->id, 0, $context->id, $timestart, $timeend, 0, $enrol)) {
2921             return false;
2922         }
2924         // force accessdata refresh for users visiting this context...
2925         mark_context_dirty($context->path);
2927         email_welcome_message_to_user($course, $user);
2929         add_to_log($course->id, 'course', 'enrol',
2930                 'view.php?id='.$course->id, $course->id);
2932         return true;
2933     }
2935     return false;
2938 /**
2939  * Determines if a user is currently logged in
2940  *
2941  * @return bool
2942  */
2943 function isloggedin() {
2944     global $USER;
2946     return (!empty($USER->id));
2949 /**
2950  * Determines if a user is logged in as real guest user with username 'guest'.
2951  *
2952  * @param int $user mixed user object or id, $USER if not specified
2953  * @return bool true if user is the real guest user, false if not logged in or other user
2954  */
2955 function isguestuser($user = NULL) {
2956     global $USER, $DB, $CFG;
2958     // make sure we have the user id cached in config table, because we are going to use it a lot
2959     if (empty($CFG->siteguest)) {
2960         if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
2961             // guest does not exist yet, weird
2962             return false;
2963         }
2964         set_config('siteguest', $guestid);
2965     }
2966     if ($user === NULL) {
2967         $user = $USER;
2968     }
2970     if ($user === NULL) {
2971         // happens when setting the $USER
2972         return false;
2974     } else if (is_numeric($user)) {
2975         return ($CFG->siteguest == $user);
2977     } else if (is_object($user)) {
2978         if (empty($user->id)) {
2979             return false; // not logged in means is not be guest
2980         } else {
2981             return ($CFG->siteguest == $user->id);
2982         }
2984     } else {
2985         throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
2986     }
2989 /**
2990  * Does user have a (temporary or real) guest access to course?
2991  *
2992  * @param object $context
2993  * @param object|int $user
2994  * @return bool
2995  */
2996 function is_guest($context, $user = NULL) {
2997     // first find the course context
2998     $coursecontext = get_course_context($context);
3000     // make sure there is a real user specified
3001     if ($user === NULL) {
3002         $userid = !empty($USER->id) ? $USER->id : 0;
3003     } else {
3004         $userid = !empty($user->id) ? $user->id : $user;
3005     }
3007     if (isguestuser($userid)) {
3008         // can not inspect or be enrolled
3009         return true;
3010     }
3012     if (has_capability('moodle/course:view', $coursecontext, $user)) {
3013         // viewing users appear out of nowhere, they are neither guests nor participants
3014         return false;
3015     }
3017     if (has_capability('moodle/course:participate', $coursecontext, $userid, false)) {
3018         return false;
3019     }
3021     return true;
3025 /**
3026  * Returns true if user has course:inspect capability in course,
3027  * this is intended for admins, managers (aka small admins), inspectors, etc.
3028  *
3029  * @param object $context
3030  * @param int|object $user, if NULL $USER is used
3031  * @param string $withcapability extra capability name
3032  * @return bool
3033  */
3034 function is_viewing($context, $user = NULL, $withcapability = '') {
3035     global $USER;
3037     // first find the course context
3038     $coursecontext = get_course_context($context);
3040     if (isguestuser($user)) {
3041         // can not inspect
3042         return true;
3043     }
3045     if (!has_capability('moodle/course:view', $coursecontext, $user)) {
3046         // admins are allowed to inspect courses
3047         return false;
3048     }
3050     if ($withcapability and !has_capability($withcapability, $context, $user)) {
3051         // site admins always have the capability, but the enrolment above blocks
3052         return false;
3053     }
3055     return true;
3058 /**
3059  * Returns true if user is enrolled (is participating) in course
3060  * this is intended for students and teachers.
3061  *
3062  * @param object $context
3063  * @param int|object $user, if NULL $USER is used, oherwise user object or id expected
3064  * @param string $withcapability extra capability name
3065  * @return bool
3066  */
3067 function is_enrolled($context, $user = NULL, $withcapability = '') {
3068     global $USER;
3070     // first find the course context
3071     $coursecontext = get_course_context($context);
3073     // make sure there is a real user specified
3074     if ($user === NULL) {
3075         $userid = !empty($USER->id) ? $USER->id : 0;
3076     } else {
3077         $userid = !empty($user->id) ? $user->id : $user;
3078     }
3080     if (empty($userid)) {
3081         // not-logged-in!
3082         return false;
3083     } else if (isguestuser($userid)) {
3084         // guest account can not be enrolled anywhere
3085         return false;
3086     }
3088     if ($coursecontext->instanceid != SITEID and !has_capability('moodle/course:participate', $coursecontext, $userid, false)) {
3089         // admins are not enrolled, everybody is "enrolled" in the frontpage course
3090         return false;
3091     }
3093     if ($withcapability and !has_capability($withcapability, $context, $userid)) {
3094         return false;
3095     }
3097     return true;
3100 /**
3101  * Returns array with sql code and parameters returning all ids
3102  * of users enrolled into course.
3103  * @param object $context
3104  * @param string $withcapability
3105  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3106  * @param string $prefix used for alias of user table, parameter names and in aliases of other used tables
3107  * @return array list($sql, $params)
3108  */
3109 function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $prefix = 'eu') {
3110     global $DB;
3112     if ($context->contextlevel < CONTEXT_COURSE) {
3113         throw new coding_exception('get_enrolled_sql() expects course context and bellow!');
3114     }
3116     // first find the course context
3117     if ($context->contextlevel == CONTEXT_COURSE) {
3118         $coursecontext = $context;
3120     } else if ($context->contextlevel == CONTEXT_MODULE) {
3121         $coursecontext = get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST));
3123     } else if ($context->contextlevel == CONTEXT_BLOCK) {
3124         $parentcontext = get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST));
3125         if ($parentcontext->contextlevel == CONTEXT_COURSE) {
3126             $coursecontext = $parentcontext;
3127         } else if ($parentcontext->contextlevel == CONTEXT_MODULE) {
3128             $coursecontext = get_context_instance_by_id(get_parent_contextid($parentcontext, MUST_EXIST));
3129         } else {
3130             throw new coding_exception('Invalid context supplied to get_enrolled_sql()!');
3131         }
3133     } else {
3134         throw new coding_exception('Invalid context supplied to get_enrolled_sql()!');
3135     }
3137     list($contextids, $contextpaths) = get_context_info_list($context);
3138     list($coursecontextids, $coursecontextpaths) = get_context_info_list($coursecontext);
3140     // get all relevant capability info for all roles
3141     if ($withcapability) {
3142         list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con00');
3143         $incaps = "IN (:participate, :withcap)";
3144         $params['participate'] = 'moodle/course:participate';
3145         $params['withcap']     = $withcapability;
3146     } else {
3147         list($incontexts, $params) = $DB->get_in_or_equal($coursecontextids, SQL_PARAMS_NAMED, 'con00');
3148         $incaps = "= :participate";
3149         $params['participate'] = 'moodle/course:participate';
3150     }
3151     $defs = array();
3152     $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3153               FROM {role_capabilities} rc
3154               JOIN {context} ctx on rc.contextid = ctx.id
3155              WHERE rc.contextid $incontexts AND rc.capability $incaps";
3156     $rcs = $DB->get_records_sql($sql, $params);
3157     foreach ($rcs as $rc) {
3158         $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3159     }
3161     $courseaccess = array();
3162     if (!empty($defs['moodle/course:participate'])) {
3163         foreach ($coursecontextpaths as $path) {
3164             if (empty($defs['moodle/course:participate'][$path])) {
3165                 continue;
3166             }
3168             foreach($defs['moodle/course:participate'][$path] as $roleid => $perm) {
3169                 if ($perm == CAP_PROHIBIT) {
3170                     $courseaccess[$roleid] = CAP_PROHIBIT;
3171                     continue;
3172                 }
3173                 if (!isset($courseaccess[$roleid])) {
3174                     $courseaccess[$roleid] = (int)$perm;
3175                 }
3176             }
3177         }
3178     }
3180     $access = array();
3181     if (!empty($defs[$withcapability])) {
3182         foreach ($contextpaths as $path) {
3183             if (empty($defs[$withcapability][$path])) {
3184                 continue;
3185             }
3186             foreach($defs[$withcapability][$path] as $roleid => $perm) {
3187                 if ($perm == CAP_PROHIBIT) {
3188                     $access[$roleid] = CAP_PROHIBIT;
3189                     continue;
3190                 }
3191                 if (!isset($access[$roleid])) {
3192                     $access[$roleid] = (int)$perm;
3193                 }
3194             }
3195         }
3196     }
3198     unset($defs);
3200     // make lists of roles that are needed and prohibited
3201     $courseneeded     = array(); // one of these is enough
3202     $courseprohibited = array(); // must not have any of these
3203     foreach ($courseaccess as $roleid => $perm) {
3204         if ($perm == CAP_PROHIBIT) {
3205             unset($courseneeded[$roleid]);
3206             $courseprohibited[$roleid] = true;
3207         } else if ($perm == CAP_ALLOW and empty($courseprohibited[$roleid])) {
3208             $courseneeded[$roleid] = true;
3209         }
3210     }
3211     $needed     = array(); // one of these is enough
3212     $prohibited = array(); // must not have any of these
3213     if ($withcapability) {
3214         foreach ($access as $roleid => $perm) {
3215             if ($perm == CAP_PROHIBIT) {
3216                 unset($needed[$roleid]);
3217                 $prohibited[$roleid] = true;
3218             } else if ($perm == CAP_ALLOW and empty($prohibited[$roleid])) {
3219                 $needed[$roleid] = true;
3220             }
3221         }
3222     }
3224     $isfrontpage = ($coursecontext->instanceid == SITEID);
3226     $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : NULL;
3227     $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : NULL;
3229     $nobody = false;
3231     if ($isfrontpage) {
3232         // on the frontpage all users are kind of enrolled, we have to respect only the prohibits
3233         $courseneeded = array();
3234     } else {
3235         if (empty($courseneeded)) {
3236             $nobody = true;
3237         }
3238     }
3240     if ($withcapability and !$nobody) {
3241         if ($isfrontpage) {
3242             if (!empty($prohibited[$defaultuserroleid]) or !empty($prohibited[$defaultfrontpageroleid])) {
3243                 $nobody = true;
3244             } else if (!empty($neded[$defaultuserroleid]) or !empty($neded[$defaultfrontpageroleid])) {
3245                 // everybody not having prohibit has the capability
3246                 $needed = array();
3247             } else if (empty($needed)) {
3248                 $nobody = true;
3249             }
3250         } else {
3251             if (!empty($prohibited[$defaultuserroleid])) {
3252                 $nobody = true;
3253             } else if (!empty($neded[$defaultuserroleid])) {
3254                 // everybody not having prohibit has the capability
3255                 $needed = array();
3256             } else if (empty($needed)) {
3257                 $nobody = true;
3258             }
3259         }
3260     }
3262     if ($nobody) {
3263         // nobody can match so return some SQL that does not return any results
3264         return array("SELECT {$prefix}.id FROM {user} {$prefix} WHERE 1=2", array());
3265     }
3267     $joins  = array();
3268     $params = array();
3269     $wheres = array("{$prefix}.deleted = 0 AND {$prefix}.username <> 'guest'");
3271     if ($courseneeded) {
3272         $ctxids = implode(',', $coursecontextids);
3273         $roleids = implode(',', array_keys($courseneeded));
3274         $joins[] = "JOIN {role_assignments} {$prefix}_ra1 ON ({$prefix}_ra1.userid = {$prefix}.id AND {$prefix}_ra1.roleid IN ($roleids) AND {$prefix}_ra1.contextid IN ($ctxids))";
3275     }
3277     if ($courseprohibited) {
3278         $ctxids = implode(',', $coursecontextids);
3279         $roleids = implode(',', array_keys($courseprohibited));
3280         $joins[] = "LEFT JOIN {role_assignments} {$prefix}_ra2 ON ({$prefix}_ra2.userid = {$prefix}.id AND {$prefix}_ra2.roleid IN ($roleids) AND {$prefix}_ra2.contextid IN ($ctxids))";
3281         $wheres[] = "{$prefix}_ra2 IS NULL";
3282     }
3284     if ($needed) {
3285         $ctxids = implode(',', $contextids);
3286         $roleids = implode(',', array_keys($needed));
3287         $joins[] = "JOIN {role_assignments} {$prefix}_ra3 ON ({$prefix}_ra3.userid = {$prefix}.id AND {$prefix}_ra3.roleid IN ($roleids) AND {$prefix}_ra3.contextid IN ($ctxids))";
3288     }
3290     if ($prohibited) {
3291         $ctxids = implode(',', $contextids);
3292         $roleids = implode(',', array_keys($prohibited));
3293         $joins[] = "LEFT JOIN {role_assignments} {$prefix}_ra4 ON ({$prefix}_ra4.userid = {$prefix}.id AND {$prefix}_ra4.roleid IN ($roleids) AND {$prefix}_ra4.contextid IN ($ctxids))";
3294         $wheres[] = "{$prefix}_ra4 IS NULL";
3295     }
3297     if ($groupid) {
3298         $joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}.id AND {$prefix}.roleid = :{$prefix}gmid)";
3299         $params["{$prefix}gmid"] = $groupid;
3300     }
3302     $joins = implode("\n", $joins);
3303     $wheres = "WHERE ".implode(" AND ", $wheres);
3305     $sql = "SELECT DISTINCT {$prefix}.id
3306                FROM {user} {$prefix}
3307              $joins
3308             $wheres";
3310     return array($sql, $params);
3313 /**
3314  * Returns list of users enrolled into course.
3315  * @param object $context
3316  * @param string $withcapability
3317  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3318  * @param string $userfields requested user record fields
3319  * @param string $orderby
3320  * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
3321  * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
3322  * @return array of user records
3323  */
3324 function get_enrolled_users($context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = '', $limitfrom = 0, $limitnum = 0) {
3325     global $DB;
3327     list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid);
3328     $sql = "SELECT $userfields
3329               FROM {user} u
3330               JOIN ($esql) je ON je.id = u.id
3331              WHERE u.deleted = 0";
3333     if ($orderby) {
3334         $sql = "$sql ORDER BY $orderby";
3335     } else {
3336         $sql = "$sql ORDER BY u.lastname ASC, u.firstname ASC";
3337     }
3339     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3342 /**
3343  * Counts list of users enrolled into course (as per above function)
3344  * @param object $context
3345  * @param string $withcapability
3346  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3347  * @return array of user records
3348  */
3349 function count_enrolled_users($context, $withcapability = '', $groupid = 0) {
3350     global $DB;
3352     list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid);
3353     $sql = "SELECT count(u.id)
3354               FROM {user} u
3355               JOIN ($esql) je ON je.id = u.id
3356              WHERE u.deleted = 0";
3358     return $DB->count_records_sql($sql, $params);
3362 /**
3363  * Loads the capability definitions for the component (from file).
3364  *
3365  * Loads the capability definitions for the component (from file). If no
3366  * capabilities are defined for the component, we simply return an empty array.
3367  *
3368  * @global object
3369  * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
3370  * @return array array of capabilities
3371  */
3372 function load_capability_def($component) {
3373     $defpath = get_component_directory($component).'/db/access.php';
3375     $capabilities = array();
3376     if (file_exists($defpath)) {
3377         require($defpath);
3378         if (!empty(${$component.'_capabilities'})) {
3379             // BC capability array name
3380             // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
3381             debugging('componentname_capabilities array is deprecated, please use capabilities array only in access.php files');
3382             $capabilities = ${$component.'_capabilities'};
3383         }
3384     }
3386     return $capabilities;
3390 /**
3391  * Gets the capabilities that have been cached in the database for this component.
3392  * @param string $component - examples: 'moodle', 'mod_forum'
3393  * @return array array of capabilities
3394  */
3395 function get_cached_capabilities($component='moodle') {
3396     global $DB;
3397     return $DB->get_records('capabilities', array('component'=>$component));
3400 /**
3401  * Returns default capabilities for given role archetype.
3402  * @param string $archetype role archetype
3403  * @return array
3404  */
3405 function get_default_capabilities($archetype) {
3406     global $DB;
3408     if (!$archetype) {
3409         return array();
3410     }
3412     $alldefs = array();
3413     $defaults = array();
3414     $components = array();
3415     $allcaps = $DB->get_records('capabilities');
3417     foreach ($allcaps as $cap) {
3418         if (!in_array($cap->component, $components)) {
3419             $components[] = $cap->component;
3420             $alldefs = array_merge($alldefs, load_capability_def($cap->component));
3421         }
3422     }
3423     foreach($alldefs as $name=>$def) {
3424         if (isset($def['legacy'][$archetype])) {
3425             $defaults[$name] = $def['legacy'][$archetype];
3426         }
3427     }
3429     return $defaults;
3432 /**
3433  * Reset role capabilitites to default according to selected role archetype.
3434  * If no archetype selected, removes all capabilities.
3435  * @param int @roleid
3436  */
3437 function reset_role_capabilities($roleid) {
3438     global $DB;
3440     $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
3441     $defaultcaps = get_default_capabilities($role->archetype);
3443     $sitecontext = get_context_instance(CONTEXT_SYSTEM);
3445     $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
3447     foreach($defaultcaps as $cap=>$permission) {
3448         assign_capability($cap, $permission, $roleid, $sitecontext->id);
3449     }
3452 /**
3453  * Updates the capabilities table with the component capability definitions.
3454  * If no parameters are given, the function updates the core moodle
3455  * capabilities.
3456  *
3457  * Note that the absence of the db/access.php capabilities definition file
3458  * will cause any stored capabilities for the component to be removed from
3459  * the database.
3460  *
3461  * @global object
3462  * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
3463  * @return boolean true if success, exception in case of any problems
3464  */
3465 function update_capabilities($component='moodle') {
3466     global $DB, $OUTPUT;
3468     $storedcaps = array();
3470     $filecaps = load_capability_def($component);
3471     $cachedcaps = get_cached_capabilities($component);
3472     if ($cachedcaps) {
3473         foreach ($cachedcaps as $cachedcap) {
3474             array_push($storedcaps, $cachedcap->name);
3475             // update risk bitmasks and context levels in existing capabilities if needed
3476             if (array_key_exists($cachedcap->name, $filecaps)) {
3477                 if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
3478                     $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
3479                 }
3480                 if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
3481                     $updatecap = new object();
3482                     $updatecap->id = $cachedcap->id;
3483                     $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
3484                     $DB->update_record('capabilities', $updatecap);
3485                 }
3486                 if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
3487                     $updatecap = new object();
3488                     $updatecap->id = $cachedcap->id;
3489                     $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
3490                     $DB->update_record('capabilities', $updatecap);
3491                 }
3493                 if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
3494                     $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
3495                 }
3496                 if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
3497                     $updatecap = new object();
3498                     $updatecap->id = $cachedcap->id;
3499                     $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
3500                     $DB->update_record('capabilities', $updatecap);
3501                 }
3502             }
3503         }
3504     }
3506     // Are there new capabilities in the file definition?
3507     $newcaps = array();
3509     foreach ($filecaps as $filecap => $def) {
3510         if (!$storedcaps ||
3511                 ($storedcaps && in_array($filecap, $storedcaps) === false)) {
3512             if (!array_key_exists('riskbitmask', $def)) {
3513                 $def['riskbitmask'] = 0; // no risk if not specified
3514             }
3515             $newcaps[$filecap] = $def;
3516         }
3517     }
3518     // Add new capabilities to the stored definition.
3519     foreach ($newcaps as $capname => $capdef) {
3520         $capability = new object();
3521         $capability->name = $capname;
3522         $capability->captype = $capdef['captype'];
3523         $capability->contextlevel = $capdef['contextlevel'];
3524         $capability->component = $component;
3525         $capability->riskbitmask = $capdef['riskbitmask'];
3527         $DB->insert_record('capabilities', $capability, false);
3529         if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $storedcaps)){
3530             if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
3531                 foreach ($rolecapabilities as $rolecapability){
3532                     //assign_capability will update rather than insert if capability exists
3533                     if (!assign_capability($capname, $rolecapability->permission,
3534                                             $rolecapability->roleid, $rolecapability->contextid, true)){
3535                          echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
3536                     }
3537                 }
3538             }
3539         // we ignore legacy key if we have cloned permissions
3540         } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
3541             assign_legacy_capabilities($capname, $capdef['legacy']);
3542         }
3543     }
3544     // Are there any capabilities that have been removed from the file
3545     // definition that we need to delete from the stored capabilities and
3546     // role assignments?
3547     capabilities_cleanup($component, $filecaps);
3549     // reset static caches
3550     $ACCESSLIB_PRIVATE->capabilities = NULL;
3552     return true;
3556 /**
3557  * Deletes cached capabilities that are no longer needed by the component.
3558  * Also unassigns these capabilities from any roles that have them.
3559  *
3560  * @global object
3561  * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
3562  * @param array $newcapdef array of the new capability definitions that will be
3563  *                     compared with the cached capabilities
3564  * @return int number of deprecated capabilities that have been removed
3565  */
3566 function capabilities_cleanup($component, $newcapdef=NULL) {
3567     global $DB;
3569     $removedcount = 0;
3571     if ($cachedcaps = get_cached_capabilities($component)) {
3572         foreach ($cachedcaps as $cachedcap) {
3573             if (empty($newcapdef) ||
3574                         array_key_exists($cachedcap->name, $newcapdef) === false) {
3576                 // Remove from capabilities cache.
3577                 $DB->delete_records('capabilities', array('name'=>$cachedcap->name));
3578                 $removedcount++;
3579                 // Delete from roles.
3580                 if ($roles = get_roles_with_capability($cachedcap->name)) {
3581                     foreach($roles as $role) {
3582                         if (!unassign_capability($cachedcap->name, $role->id)) {
3583                             print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
3584                         }
3585                     }
3586                 }
3587             } // End if.
3588         }
3589     }
3590     return $removedcount;
3595 //////////////////
3596 // UI FUNCTIONS //
3597 //////////////////
3599 /**
3600  * @param integer $contextlevel $context->context level. One of the CONTEXT_... constants.
3601  * @return string the name for this type of context.
3602  */
3603 function get_contextlevel_name($contextlevel) {
3604     static $strcontextlevels = NULL;
3605     if (is_null($strcontextlevels)) {
3606         $strcontextlevels = array(
3607             CONTEXT_SYSTEM => get_string('coresystem'),
3608             CONTEXT_USER => get_string('user'),
3609             CONTEXT_COURSECAT => get_string('category'),
3610             CONTEXT_COURSE => get_string('course'),
3611             CONTEXT_MODULE => get_string('activitymodule'),
3612             CONTEXT_BLOCK => get_string('block')
3613         );
3614     }
3615     return $strcontextlevels[$contextlevel];
3618 /**
3619  * Prints human readable context identifier.
3620  *
3621  * @global object
3622  * @param object $context the context.
3623  * @param boolean $withprefix whether to prefix the name of the context with the
3624  *      type of context, e.g. User, Course, Forum, etc.
3625  * @param boolean $short whether to user the short name of the thing. Only applies
3626  *      to course contexts
3627  * @return string the human readable context name.
3628  */
3629 function print_context_name($context, $withprefix = true, $short = false) {
3630     global $DB;
3632     $name = '';
3633     switch ($context->contextlevel) {
3635         case CONTEXT_SYSTEM:
3636             $name = get_string('coresystem');
3637             break;
3639         case CONTEXT_USER:
3640             if ($user = $DB->get_record('user', array('id'=>$context->instanceid))) {