Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * This file contains functions for managing user access
19
 *
20
 * <b>Public API vs internals</b>
21
 *
22
 * General users probably only care about
23
 *
24
 * Context handling
25
 * - context_course::instance($courseid), context_module::instance($cm->id), context_coursecat::instance($catid)
26
 * - context::instance_by_id($contextid)
27
 * - $context->get_parent_contexts();
28
 * - $context->get_child_contexts();
29
 *
30
 * Whether the user can do something...
31
 * - has_capability()
32
 * - has_any_capability()
33
 * - has_all_capabilities()
34
 * - require_capability()
35
 * - require_login() (from moodlelib)
36
 * - is_enrolled()
37
 * - is_viewing()
38
 * - is_guest()
39
 * - is_siteadmin()
40
 * - isguestuser()
41
 * - isloggedin()
42
 *
43
 * What courses has this user access to?
44
 * - get_enrolled_users()
45
 *
46
 * What users can do X in this context?
47
 * - get_enrolled_users() - at and bellow course context
48
 * - get_users_by_capability() - above course context
49
 *
50
 * Modify roles
51
 * - role_assign()
52
 * - role_unassign()
53
 * - role_unassign_all()
54
 *
55
 * Advanced - for internal use only
56
 * - load_all_capabilities()
57
 * - reload_all_capabilities()
58
 * - has_capability_in_accessdata()
59
 * - get_user_roles_sitewide_accessdata()
60
 * - etc.
61
 *
62
 * <b>Name conventions</b>
63
 *
64
 * "ctx" means context
65
 * "ra" means role assignment
66
 * "rdef" means role definition
67
 *
68
 * <b>accessdata</b>
69
 *
70
 * Access control data is held in the "accessdata" array
71
 * which - for the logged-in user, will be in $USER->access
72
 *
73
 * For other users can be generated and passed around (but may also be cached
74
 * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser).
75
 *
76
 * $accessdata is a multidimensional array, holding
77
 * role assignments (RAs), role switches and initialization time.
78
 *
79
 * Things are keyed on "contextpaths" (the path field of
80
 * the context table) for fast walking up/down the tree.
81
 * <code>
82
 * $accessdata['ra'][$contextpath] = array($roleid=>$roleid)
83
 *                  [$contextpath] = array($roleid=>$roleid)
84
 *                  [$contextpath] = array($roleid=>$roleid)
85
 * </code>
86
 *
87
 * <b>Stale accessdata</b>
88
 *
89
 * For the logged-in user, accessdata is long-lived.
90
 *
91
 * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
92
 * context paths affected by changes. Any check at-or-below
93
 * a dirty context will trigger a transparent reload of accessdata.
94
 *
95
 * Changes at the system level will force the reload for everyone.
96
 *
97
 * <b>Default role caps</b>
98
 * The default role assignment is not in the DB, so we
99
 * add it manually to accessdata.
100
 *
101
 * This means that functions that work directly off the
102
 * DB need to ensure that the default role caps
103
 * are dealt with appropriately.
104
 *
105
 * @package    core_access
106
 * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
107
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
108
 */
109
 
110
defined('MOODLE_INTERNAL') || die();
111
 
112
/** No capability change */
113
define('CAP_INHERIT', 0);
114
/** Allow permission, overrides CAP_PREVENT defined in parent contexts */
115
define('CAP_ALLOW', 1);
116
/** Prevent permission, overrides CAP_ALLOW defined in parent contexts */
117
define('CAP_PREVENT', -1);
118
/** Prohibit permission, overrides everything in current and child contexts */
119
define('CAP_PROHIBIT', -1000);
120
 
121
/** System context level - only one instance in every system */
122
define('CONTEXT_SYSTEM', 10);
123
/** User context level -  one instance for each user describing what others can do to user */
124
define('CONTEXT_USER', 30);
125
/** Course category context level - one instance for each category */
126
define('CONTEXT_COURSECAT', 40);
127
/** Course context level - one instances for each course */
128
define('CONTEXT_COURSE', 50);
129
/** Course module context level - one instance for each course module */
130
define('CONTEXT_MODULE', 70);
131
/**
132
 * Block context level - one instance for each block, sticky blocks are tricky
133
 * because ppl think they should be able to override them at lower contexts.
134
 * Any other context level instance can be parent of block context.
135
 */
136
define('CONTEXT_BLOCK', 80);
137
 
138
/** Capability allow management of trusts - NOT IMPLEMENTED YET - see {@link https://moodledev.io/docs/apis/subsystems/roles} */
139
define('RISK_MANAGETRUST', 0x0001);
140
/** Capability allows changes in system configuration - see {@link https://moodledev.io/docs/apis/subsystems/roles} */
141
define('RISK_CONFIG',      0x0002);
142
/** Capability allows user to add scripted content - see {@link https://moodledev.io/docs/apis/subsystems/roles} */
143
define('RISK_XSS',         0x0004);
144
/** Capability allows access to personal user information - see {@link https://moodledev.io/docs/apis/subsystems/roles} */
145
define('RISK_PERSONAL',    0x0008);
146
/** Capability allows users to add content others may see - see {@link https://moodledev.io/docs/apis/subsystems/roles} */
147
define('RISK_SPAM',        0x0010);
148
/** capability allows mass delete of data belonging to other users - see {@link https://moodledev.io/docs/apis/subsystems/roles} */
149
define('RISK_DATALOSS',    0x0020);
150
 
151
/** rolename displays - the name as defined in the role definition, localised if name empty */
152
define('ROLENAME_ORIGINAL', 0);
153
/** rolename displays - the name as defined by a role alias at the course level, falls back to ROLENAME_ORIGINAL if alias not present */
154
define('ROLENAME_ALIAS', 1);
155
/** rolename displays - Both, like this:  Role alias (Original) */
156
define('ROLENAME_BOTH', 2);
157
/** rolename displays - the name as defined in the role definition and the shortname in brackets */
158
define('ROLENAME_ORIGINALANDSHORT', 3);
159
/** rolename displays - the name as defined by a role alias, in raw form suitable for editing */
160
define('ROLENAME_ALIAS_RAW', 4);
161
/** rolename displays - the name is simply short role name */
162
define('ROLENAME_SHORT', 5);
163
 
164
if (!defined('CONTEXT_CACHE_MAX_SIZE')) {
165
    /** maximum size of context cache - it is possible to tweak this config.php or in any script before inclusion of context.php */
166
    define('CONTEXT_CACHE_MAX_SIZE', 2500);
167
}
168
 
169
/** Performance hint for assign_capability: the contextid is known to exist */
170
define('ACCESSLIB_HINT_CONTEXT_EXISTS', 'contextexists');
171
/** Performance hint for assign_capability: there is no existing entry in role_capabilities */
172
define('ACCESSLIB_HINT_NO_EXISTING', 'notexists');
173
 
174
/**
175
 * Although this looks like a global variable, it isn't really.
176
 *
177
 * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
178
 * It is used to cache various bits of data between function calls for performance reasons.
179
 * Sadly, a PHP global variable is the only way to implement this, without rewriting everything
180
 * as methods of a class, instead of functions.
181
 *
182
 * @access private
183
 * @global stdClass $ACCESSLIB_PRIVATE
184
 * @name $ACCESSLIB_PRIVATE
185
 */
186
global $ACCESSLIB_PRIVATE;
187
$ACCESSLIB_PRIVATE = new stdClass();
188
$ACCESSLIB_PRIVATE->cacheroledefs    = array(); // Holds site-wide role definitions.
189
$ACCESSLIB_PRIVATE->dirtycontexts    = null;    // Dirty contexts cache, loaded from DB once per page
190
$ACCESSLIB_PRIVATE->dirtyusers       = null;    // Dirty users cache, loaded from DB once per $USER->id
191
$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
192
 
193
/**
194
 * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
195
 *
196
 * This method should ONLY BE USED BY UNIT TESTS. It clears all of
197
 * accesslib's private caches. You need to do this before setting up test data,
198
 * and also at the end of the tests.
199
 *
200
 * @access private
201
 * @return void
202
 */
203
function accesslib_clear_all_caches_for_unit_testing() {
204
    global $USER;
205
    if (!PHPUNIT_TEST) {
206
        throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
207
    }
208
 
209
    accesslib_clear_all_caches(true);
210
    accesslib_reset_role_cache();
211
 
212
    unset($USER->access);
213
}
214
 
215
/**
216
 * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
217
 *
218
 * This reset does not touch global $USER.
219
 *
220
 * @access private
221
 * @param bool $resetcontexts
222
 * @return void
223
 */
224
function accesslib_clear_all_caches($resetcontexts) {
225
    global $ACCESSLIB_PRIVATE;
226
 
227
    $ACCESSLIB_PRIVATE->dirtycontexts    = null;
228
    $ACCESSLIB_PRIVATE->dirtyusers       = null;
229
    $ACCESSLIB_PRIVATE->accessdatabyuser = array();
230
 
231
    if ($resetcontexts) {
232
        context_helper::reset_caches();
233
    }
234
}
235
 
236
/**
237
 * Full reset of accesslib's private role cache. ONLY TO BE USED FROM THIS LIBRARY FILE!
238
 *
239
 * This reset does not touch global $USER.
240
 *
241
 * Note: Only use this when the roles that need a refresh are unknown.
242
 *
243
 * @see accesslib_clear_role_cache()
244
 *
245
 * @access private
246
 * @return void
247
 */
248
function accesslib_reset_role_cache() {
249
    global $ACCESSLIB_PRIVATE;
250
 
251
    $ACCESSLIB_PRIVATE->cacheroledefs = array();
252
    $cache = cache::make('core', 'roledefs');
253
    $cache->purge();
254
}
255
 
256
/**
257
 * Clears accesslib's private cache of a specific role or roles. ONLY BE USED FROM THIS LIBRARY FILE!
258
 *
259
 * This reset does not touch global $USER.
260
 *
261
 * @access private
262
 * @param int|array $roles
263
 * @return void
264
 */
265
function accesslib_clear_role_cache($roles) {
266
    global $ACCESSLIB_PRIVATE;
267
 
268
    if (!is_array($roles)) {
269
        $roles = [$roles];
270
    }
271
 
272
    foreach ($roles as $role) {
273
        if (isset($ACCESSLIB_PRIVATE->cacheroledefs[$role])) {
274
            unset($ACCESSLIB_PRIVATE->cacheroledefs[$role]);
275
        }
276
    }
277
 
278
    $cache = cache::make('core', 'roledefs');
279
    $cache->delete_many($roles);
280
}
281
 
282
/**
283
 * Role is assigned at system context.
284
 *
285
 * @access private
286
 * @param int $roleid
287
 * @return array
288
 */
289
function get_role_access($roleid) {
290
    $accessdata = get_empty_accessdata();
291
    $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
292
    return $accessdata;
293
}
294
 
295
/**
296
 * Fetch raw "site wide" role definitions.
297
 * Even MUC static acceleration cache appears a bit slow for this.
298
 * Important as can be hit hundreds of times per page.
299
 *
300
 * @param array $roleids List of role ids to fetch definitions for.
301
 * @return array Complete definition for each requested role.
302
 */
303
function get_role_definitions(array $roleids) {
304
    global $ACCESSLIB_PRIVATE;
305
 
306
    if (empty($roleids)) {
307
        return array();
308
    }
309
 
310
    // Grab all keys we have not yet got in our static cache.
311
    if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
312
        $cache = cache::make('core', 'roledefs');
313
        foreach ($cache->get_many($uncached) as $roleid => $cachedroledef) {
314
            if (is_array($cachedroledef)) {
315
                $ACCESSLIB_PRIVATE->cacheroledefs[$roleid] = $cachedroledef;
316
            }
317
        }
318
 
319
        // Check we have the remaining keys from the MUC.
320
        if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
321
            $uncached = get_role_definitions_uncached($uncached);
322
            $ACCESSLIB_PRIVATE->cacheroledefs += $uncached;
323
            $cache->set_many($uncached);
324
        }
325
    }
326
 
327
    // Return just the roles we need.
328
    return array_intersect_key($ACCESSLIB_PRIVATE->cacheroledefs, array_flip($roleids));
329
}
330
 
331
/**
332
 * Query raw "site wide" role definitions.
333
 *
334
 * @param array $roleids List of role ids to fetch definitions for.
335
 * @return array Complete definition for each requested role.
336
 */
337
function get_role_definitions_uncached(array $roleids) {
338
    global $DB;
339
 
340
    if (empty($roleids)) {
341
        return array();
342
    }
343
 
344
    // Create a blank results array: even if a role has no capabilities,
345
    // we need to ensure it is included in the results to show we have
346
    // loaded all the capabilities that there are.
347
    $rdefs = array();
348
    foreach ($roleids as $roleid) {
349
        $rdefs[$roleid] = array();
350
    }
351
 
352
    // Load all the capabilities for these roles in all contexts.
353
    list($sql, $params) = $DB->get_in_or_equal($roleids);
354
    $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
355
              FROM {role_capabilities} rc
356
              JOIN {context} ctx ON rc.contextid = ctx.id
357
              JOIN {capabilities} cap ON rc.capability = cap.name
358
             WHERE rc.roleid $sql";
359
    $rs = $DB->get_recordset_sql($sql, $params);
360
 
361
    // Store the capabilities into the expected data structure.
362
    foreach ($rs as $rd) {
363
        if (!isset($rdefs[$rd->roleid][$rd->path])) {
364
            $rdefs[$rd->roleid][$rd->path] = array();
365
        }
366
        $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
367
    }
368
 
369
    $rs->close();
370
 
371
    // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
372
    // we process role definitinons in a way that requires we see parent contexts
373
    // before child contexts. This sort ensures that works (and is faster than
374
    // sorting in the SQL query).
375
    foreach ($rdefs as $roleid => $rdef) {
376
        ksort($rdefs[$roleid]);
377
    }
378
 
379
    return $rdefs;
380
}
381
 
382
/**
383
 * Get the default guest role, this is used for guest account,
384
 * search engine spiders, etc.
385
 *
386
 * @return stdClass|false role record
387
 */
388
function get_guest_role() {
389
    global $CFG, $DB;
390
 
391
    if (empty($CFG->guestroleid)) {
392
        if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
393
            $guestrole = array_shift($roles);   // Pick the first one
394
            set_config('guestroleid', $guestrole->id);
395
            return $guestrole;
396
        } else {
397
            debugging('Can not find any guest role!');
398
            return false;
399
        }
400
    } else {
401
        if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
402
            return $guestrole;
403
        } else {
404
            // somebody is messing with guest roles, remove incorrect setting and try to find a new one
405
            set_config('guestroleid', '');
406
            return get_guest_role();
407
        }
408
    }
409
}
410
 
411
/**
412
 * Check whether a user has a particular capability in a given context.
413
 *
414
 * For example:
415
 *      $context = context_module::instance($cm->id);
416
 *      has_capability('mod/forum:replypost', $context)
417
 *
418
 * By default checks the capabilities of the current user, but you can pass a
419
 * different userid. By default will return true for admin users, but you can override that with the fourth argument.
420
 *
421
 * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
422
 * or capabilities with XSS, config or data loss risks.
423
 *
424
 * @category access
425
 *
426
 * @param string $capability the name of the capability to check. For example mod/forum:view
427
 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
428
 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
429
 * @param boolean $doanything If false, ignores effect of admin role assignment
430
 * @return boolean true if the user has this capability. Otherwise false.
431
 */
432
function has_capability($capability, context $context, $user = null, $doanything = true) {
433
    global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
434
 
435
    if (during_initial_install()) {
436
        if ($SCRIPT === "/$CFG->admin/index.php"
437
                or $SCRIPT === "/$CFG->admin/cli/install.php"
438
                or $SCRIPT === "/$CFG->admin/cli/install_database.php"
439
                or (defined('BEHAT_UTIL') and BEHAT_UTIL)
440
                or (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL)) {
441
            // we are in an installer - roles can not work yet
442
            return true;
443
        } else {
444
            return false;
445
        }
446
    }
447
 
448
    if (strpos($capability, 'moodle/legacy:') === 0) {
449
        throw new coding_exception('Legacy capabilities can not be used any more!');
450
    }
451
 
452
    if (!is_bool($doanything)) {
453
        throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
454
    }
455
 
456
    // capability must exist
457
    if (!$capinfo = get_capability_info($capability)) {
458
        debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
459
        return false;
460
    }
461
 
462
    if (!isset($USER->id)) {
463
        // should never happen
464
        $USER->id = 0;
465
        debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
466
    }
467
 
468
    // make sure there is a real user specified
469
    if ($user === null) {
470
        $userid = $USER->id;
471
    } else {
472
        $userid = is_object($user) ? $user->id : $user;
473
    }
474
 
475
    // make sure forcelogin cuts off not-logged-in users if enabled
476
    if (!empty($CFG->forcelogin) and $userid == 0) {
477
        return false;
478
    }
479
 
480
    // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
481
    if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
482
        if (isguestuser($userid) or $userid == 0) {
483
            return false;
484
        }
485
    }
486
 
487
    // Check whether context locking is enabled.
488
    if (!empty($CFG->contextlocking)) {
489
        if ($capinfo->captype === 'write' && $context->locked) {
490
            // Context locking applies to any write capability in a locked context.
491
            // It does not apply to moodle/site:managecontextlocks - this is to allow context locking to be unlocked.
492
            if ($capinfo->name !== 'moodle/site:managecontextlocks') {
493
                // It applies to all users who are not site admins.
494
                // It also applies to site admins when contextlockappliestoadmin is set.
495
                if (!is_siteadmin($userid) || !empty($CFG->contextlockappliestoadmin)) {
496
                    return false;
497
                }
498
            }
499
        }
500
    }
501
 
502
    // somehow make sure the user is not deleted and actually exists
503
    if ($userid != 0) {
504
        if ($userid == $USER->id and isset($USER->deleted)) {
505
            // this prevents one query per page, it is a bit of cheating,
506
            // but hopefully session is terminated properly once user is deleted
507
            if ($USER->deleted) {
508
                return false;
509
            }
510
        } else {
511
            if (!context_user::instance($userid, IGNORE_MISSING)) {
512
                // no user context == invalid userid
513
                return false;
514
            }
515
        }
516
    }
517
 
518
    // context path/depth must be valid
519
    if (empty($context->path) or $context->depth == 0) {
520
        // this should not happen often, each upgrade tries to rebuild the context paths
521
        debugging('Context id '.$context->id.' does not have valid path, please use context_helper::build_all_paths()');
522
        if (is_siteadmin($userid)) {
523
            return true;
524
        } else {
525
            return false;
526
        }
527
    }
528
 
529
    if (!empty($USER->loginascontext)) {
530
        // The current user is logged in as another user and can assume their identity at or below the `loginascontext`
531
        // defined in the USER session.
532
        // The user may not assume their identity at any other location.
533
        if (!$USER->loginascontext->is_parent_of($context, true)) {
534
            // The context being checked is not the specified context, or one of its children.
535
            return false;
536
        }
537
    }
538
 
539
    // Find out if user is admin - it is not possible to override the doanything in any way
540
    // and it is not possible to switch to admin role either.
541
    if ($doanything) {
542
        if (is_siteadmin($userid)) {
543
            if ($userid != $USER->id) {
544
                return true;
545
            }
546
            // make sure switchrole is not used in this context
547
            if (empty($USER->access['rsw'])) {
548
                return true;
549
            }
550
            $parts = explode('/', trim($context->path, '/'));
551
            $path = '';
552
            $switched = false;
553
            foreach ($parts as $part) {
554
                $path .= '/' . $part;
555
                if (!empty($USER->access['rsw'][$path])) {
556
                    $switched = true;
557
                    break;
558
                }
559
            }
560
            if (!$switched) {
561
                return true;
562
            }
563
            //ok, admin switched role in this context, let's use normal access control rules
564
        }
565
    }
566
 
567
    // Careful check for staleness...
568
    $context->reload_if_dirty();
569
 
570
    if ($USER->id == $userid) {
571
        if (!isset($USER->access)) {
572
            load_all_capabilities();
573
        }
574
        $access =& $USER->access;
575
 
576
    } else {
577
        // make sure user accessdata is really loaded
578
        get_user_accessdata($userid, true);
579
        $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
580
    }
581
 
582
    return has_capability_in_accessdata($capability, $context, $access);
583
}
584
 
585
/**
586
 * Check if the user has any one of several capabilities from a list.
587
 *
588
 * This is just a utility method that calls has_capability in a loop. Try to put
589
 * the capabilities that most users are likely to have first in the list for best
590
 * performance.
591
 *
592
 * @category access
593
 * @see has_capability()
594
 *
595
 * @param array $capabilities an array of capability names.
596
 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
597
 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
598
 * @param boolean $doanything If false, ignore effect of admin role assignment
599
 * @return boolean true if the user has any of these capabilities. Otherwise false.
600
 */
601
function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
602
    foreach ($capabilities as $capability) {
603
        if (has_capability($capability, $context, $user, $doanything)) {
604
            return true;
605
        }
606
    }
607
    return false;
608
}
609
 
610
/**
611
 * Check if the user has all the capabilities in a list.
612
 *
613
 * This is just a utility method that calls has_capability in a loop. Try to put
614
 * the capabilities that fewest users are likely to have first in the list for best
615
 * performance.
616
 *
617
 * @category access
618
 * @see has_capability()
619
 *
620
 * @param array $capabilities an array of capability names.
621
 * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
622
 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
623
 * @param boolean $doanything If false, ignore effect of admin role assignment
624
 * @return boolean true if the user has all of these capabilities. Otherwise false.
625
 */
626
function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
627
    foreach ($capabilities as $capability) {
628
        if (!has_capability($capability, $context, $user, $doanything)) {
629
            return false;
630
        }
631
    }
632
    return true;
633
}
634
 
635
/**
636
 * Is course creator going to have capability in a new course?
637
 *
638
 * This is intended to be used in enrolment plugins before or during course creation,
639
 * do not use after the course is fully created.
640
 *
641
 * @category access
642
 *
643
 * @param string $capability the name of the capability to check.
644
 * @param context $context course or category context where is course going to be created
645
 * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
646
 * @return boolean true if the user will have this capability.
647
 *
648
 * @throws coding_exception if different type of context submitted
649
 */
650
function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
651
    global $CFG;
652
 
653
    if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
654
        throw new coding_exception('Only course or course category context expected');
655
    }
656
 
657
    if (has_capability($capability, $context, $user)) {
658
        // User already has the capability, it could be only removed if CAP_PROHIBIT
659
        // was involved here, but we ignore that.
660
        return true;
661
    }
662
 
663
    if (!has_capability('moodle/course:create', $context, $user)) {
664
        return false;
665
    }
666
 
667
    if (!enrol_is_enabled('manual')) {
668
        return false;
669
    }
670
 
671
    if (empty($CFG->creatornewroleid)) {
672
        return false;
673
    }
674
 
675
    if ($context->contextlevel == CONTEXT_COURSE) {
676
        if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
677
            return false;
678
        }
679
    } else {
680
        if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
681
            return false;
682
        }
683
    }
684
 
685
    // Most likely they will be enrolled after the course creation is finished,
686
    // does the new role have the required capability?
687
    list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
688
    return isset($neededroles[$CFG->creatornewroleid]);
689
}
690
 
691
/**
692
 * Check if the user is an admin at the site level.
693
 *
694
 * Please note that use of proper capabilities is always encouraged,
695
 * this function is supposed to be used from core or for temporary hacks.
696
 *
697
 * @category access
698
 *
699
 * @param  int|stdClass  $user_or_id user id or user object
700
 * @return bool true if user is one of the administrators, false otherwise
701
 */
702
function is_siteadmin($user_or_id = null) {
703
    global $CFG, $USER;
704
 
705
    if ($user_or_id === null) {
706
        $user_or_id = $USER;
707
    }
708
 
709
    if (empty($user_or_id)) {
710
        return false;
711
    }
712
    if (!empty($user_or_id->id)) {
713
        $userid = $user_or_id->id;
714
    } else {
715
        $userid = $user_or_id;
716
    }
717
 
718
    // Because this script is called many times (150+ for course page) with
719
    // the same parameters, it is worth doing minor optimisations. This static
720
    // cache stores the value for a single userid, saving about 2ms from course
721
    // page load time without using significant memory. As the static cache
722
    // also includes the value it depends on, this cannot break unit tests.
723
    static $knownid, $knownresult, $knownsiteadmins;
724
    if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
725
        return $knownresult;
726
    }
727
    $knownid = $userid;
728
    $knownsiteadmins = $CFG->siteadmins;
729
 
730
    $siteadmins = explode(',', $CFG->siteadmins);
731
    $knownresult = in_array($userid, $siteadmins);
732
    return $knownresult;
733
}
734
 
735
/**
736
 * Returns true if user has at least one role assign
737
 * of 'coursecontact' role (is potentially listed in some course descriptions).
738
 *
739
 * @param int $userid
740
 * @return bool
741
 */
742
function has_coursecontact_role($userid) {
743
    global $DB, $CFG;
744
 
745
    if (empty($CFG->coursecontact)) {
746
        return false;
747
    }
748
    $sql = "SELECT 1
749
              FROM {role_assignments}
750
             WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
751
    return $DB->record_exists_sql($sql, array('userid'=>$userid));
752
}
753
 
754
/**
755
 * Does the user have a capability to do something?
756
 *
757
 * Walk the accessdata array and return true/false.
758
 * Deals with prohibits, role switching, aggregating
759
 * capabilities, etc.
760
 *
761
 * The main feature of here is being FAST and with no
762
 * side effects.
763
 *
764
 * Notes:
765
 *
766
 * Switch Role merges with default role
767
 * ------------------------------------
768
 * If you are a teacher in course X, you have at least
769
 * teacher-in-X + defaultloggedinuser-sitewide. So in the
770
 * course you'll have techer+defaultloggedinuser.
771
 * We try to mimic that in switchrole.
772
 *
773
 * Permission evaluation
774
 * ---------------------
775
 * Originally there was an extremely complicated way
776
 * to determine the user access that dealt with
777
 * "locality" or role assignments and role overrides.
778
 * Now we simply evaluate access for each role separately
779
 * and then verify if user has at least one role with allow
780
 * and at the same time no role with prohibit.
781
 *
782
 * @access private
783
 * @param string $capability
784
 * @param context $context
785
 * @param array $accessdata
786
 * @return bool
787
 */
788
function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
789
    global $CFG;
790
 
791
    // Build $paths as a list of current + all parent "paths" with order bottom-to-top
792
    $path = $context->path;
793
    $paths = array($path);
794
    while ($path = rtrim($path, '0123456789')) {
795
        $path = rtrim($path, '/');
796
        if ($path === '') {
797
            break;
798
        }
799
        $paths[] = $path;
800
    }
801
 
802
    $roles = array();
803
    $switchedrole = false;
804
 
805
    // Find out if role switched
806
    if (!empty($accessdata['rsw'])) {
807
        // From the bottom up...
808
        foreach ($paths as $path) {
809
            if (isset($accessdata['rsw'][$path])) {
810
                // Found a switchrole assignment - check for that role _plus_ the default user role
811
                $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
812
                $switchedrole = true;
813
                break;
814
            }
815
        }
816
    }
817
 
818
    if (!$switchedrole) {
819
        // get all users roles in this context and above
820
        foreach ($paths as $path) {
821
            if (isset($accessdata['ra'][$path])) {
822
                foreach ($accessdata['ra'][$path] as $roleid) {
823
                    $roles[$roleid] = null;
824
                }
825
            }
826
        }
827
    }
828
 
829
    // Now find out what access is given to each role, going bottom-->up direction
830
    $rdefs = get_role_definitions(array_keys($roles));
831
    $allowed = false;
832
 
833
    foreach ($roles as $roleid => $ignored) {
834
        foreach ($paths as $path) {
835
            if (isset($rdefs[$roleid][$path][$capability])) {
836
                $perm = (int)$rdefs[$roleid][$path][$capability];
837
                if ($perm === CAP_PROHIBIT) {
838
                    // any CAP_PROHIBIT found means no permission for the user
839
                    return false;
840
                }
841
                if (is_null($roles[$roleid])) {
842
                    $roles[$roleid] = $perm;
843
                }
844
            }
845
        }
846
        // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
847
        $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
848
    }
849
 
850
    return $allowed;
851
}
852
 
853
/**
854
 * A convenience function that tests has_capability, and displays an error if
855
 * the user does not have that capability.
856
 *
857
 * NOTE before Moodle 2.0, this function attempted to make an appropriate
858
 * require_login call before checking the capability. This is no longer the case.
859
 * You must call require_login (or one of its variants) if you want to check the
860
 * user is logged in, before you call this function.
861
 *
862
 * @see has_capability()
863
 *
864
 * @param string $capability the name of the capability to check. For example mod/forum:view
865
 * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
866
 * @param int $userid A user id. By default (null) checks the permissions of the current user.
867
 * @param bool $doanything If false, ignore effect of admin role assignment
868
 * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
869
 * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
870
 * @return void terminates with an error if the user does not have the given capability.
871
 */
872
function require_capability($capability, context $context, $userid = null, $doanything = true,
873
                            $errormessage = 'nopermissions', $stringfile = '') {
874
    if (!has_capability($capability, $context, $userid, $doanything)) {
875
        throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
876
    }
877
}
878
 
879
/**
880
 * A convenience function that tests has_capability for a list of capabilities, and displays an error if
881
 * the user does not have that capability.
882
 *
883
 * This is just a utility method that calls has_capability in a loop. Try to put
884
 * the capabilities that fewest users are likely to have first in the list for best
885
 * performance.
886
 *
887
 * @category access
888
 * @see has_capability()
889
 *
890
 * @param array $capabilities an array of capability names.
891
 * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
892
 * @param int $userid A user id. By default (null) checks the permissions of the current user.
893
 * @param bool $doanything If false, ignore effect of admin role assignment
894
 * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
895
 * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
896
 * @return void terminates with an error if the user does not have the given capability.
897
 */
898
function require_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true,
899
                                  $errormessage = 'nopermissions', $stringfile = ''): void {
900
    foreach ($capabilities as $capability) {
901
        if (!has_capability($capability, $context, $userid, $doanything)) {
902
            throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
903
        }
904
    }
905
}
906
 
907
/**
908
 * Return a nested array showing all role assignments for the user.
909
 * [ra] => [contextpath][roleid] = roleid
910
 *
911
 * @access private
912
 * @param int $userid - the id of the user
913
 * @return array access info array
914
 */
915
function get_user_roles_sitewide_accessdata($userid) {
916
    global $CFG, $DB;
917
 
918
    $accessdata = get_empty_accessdata();
919
 
920
    // start with the default role
921
    if (!empty($CFG->defaultuserroleid)) {
922
        $syscontext = context_system::instance();
923
        $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
924
    }
925
 
926
    // load the "default frontpage role"
927
    if (!empty($CFG->defaultfrontpageroleid)) {
928
        $frontpagecontext = context_course::instance(get_site()->id);
929
        if ($frontpagecontext->path) {
930
            $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
931
        }
932
    }
933
 
934
    // Preload every assigned role.
935
    $sql = "SELECT ctx.path, ra.roleid, ra.contextid
936
              FROM {role_assignments} ra
937
              JOIN {context} ctx ON ctx.id = ra.contextid
938
             WHERE ra.userid = :userid";
939
 
940
    $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
941
 
942
    foreach ($rs as $ra) {
943
        // RAs leafs are arrays to support multi-role assignments...
944
        $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
945
    }
946
 
947
    $rs->close();
948
 
949
    return $accessdata;
950
}
951
 
952
/**
953
 * Returns empty accessdata structure.
954
 *
955
 * @access private
956
 * @return array empt accessdata
957
 */
958
function get_empty_accessdata() {
959
    $accessdata               = array(); // named list
960
    $accessdata['ra']         = array();
961
    $accessdata['time']       = time();
962
    $accessdata['rsw']        = array();
963
 
964
    return $accessdata;
965
}
966
 
967
/**
968
 * Get accessdata for a given user.
969
 *
970
 * @access private
971
 * @param int $userid
972
 * @param bool $preloadonly true means do not return access array
973
 * @return ?array accessdata
974
 */
975
function get_user_accessdata($userid, $preloadonly=false) {
976
    global $CFG, $ACCESSLIB_PRIVATE, $USER;
977
 
978
    if (isset($USER->access)) {
979
        $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
980
    }
981
 
11 efrain 982
    // Unfortunately, we can't use the $ACCESSLIB_PRIVATE->dirtyusers array because it is not available in CLI.
983
    // So we need to check if the user has been marked as dirty or not in the cache directly.
984
    // This will add additional queries to the database, but it is the best we can do.
985
    if (CLI_SCRIPT && !empty($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
986
        if (get_cache_flag('accesslib/dirtyusers', $userid, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['time'])) {
987
            unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
988
        }
989
    }
990
 
1 efrain 991
    if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
992
        if (empty($userid)) {
993
            if (!empty($CFG->notloggedinroleid)) {
994
                $accessdata = get_role_access($CFG->notloggedinroleid);
995
            } else {
996
                // weird
997
                return get_empty_accessdata();
998
            }
999
 
1000
        } else if (isguestuser($userid)) {
1001
            if ($guestrole = get_guest_role()) {
1002
                $accessdata = get_role_access($guestrole->id);
1003
            } else {
1004
                //weird
1005
                return get_empty_accessdata();
1006
            }
1007
 
1008
        } else {
1009
            // Includes default role and frontpage role.
1010
            $accessdata = get_user_roles_sitewide_accessdata($userid);
1011
        }
1012
 
1013
        $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
1014
    }
1015
 
1016
    if ($preloadonly) {
1017
        return;
1018
    } else {
1019
        return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
1020
    }
1021
}
1022
 
1023
/**
1024
 * A convenience function to completely load all the capabilities
1025
 * for the current user. It is called from has_capability() and functions change permissions.
1026
 *
1027
 * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
1028
 * @see check_enrolment_plugins()
1029
 *
1030
 * @access private
1031
 * @return void
1032
 */
1033
function load_all_capabilities() {
1034
    global $USER;
1035
 
1036
    // roles not installed yet - we are in the middle of installation
1037
    if (during_initial_install()) {
1038
        return;
1039
    }
1040
 
1041
    if (!isset($USER->id)) {
1042
        // this should not happen
1043
        $USER->id = 0;
1044
    }
1045
 
1046
    unset($USER->access);
1047
    $USER->access = get_user_accessdata($USER->id);
1048
 
1049
    // Clear to force a refresh
1050
    unset($USER->mycourses);
1051
 
1052
    // init/reset internal enrol caches - active course enrolments and temp access
1053
    $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
1054
}
1055
 
1056
/**
1057
 * A convenience function to completely reload all the capabilities
1058
 * for the current user when roles have been updated in a relevant
1059
 * context -- but PRESERVING switchroles and loginas.
1060
 * This function resets all accesslib and context caches.
1061
 *
1062
 * That is - completely transparent to the user.
1063
 *
1064
 * Note: reloads $USER->access completely.
1065
 *
1066
 * @access private
1067
 * @return void
1068
 */
1069
function reload_all_capabilities() {
1070
    global $USER, $DB, $ACCESSLIB_PRIVATE;
1071
 
1072
    // copy switchroles
1073
    $sw = array();
1074
    if (!empty($USER->access['rsw'])) {
1075
        $sw = $USER->access['rsw'];
1076
    }
1077
 
1078
    accesslib_clear_all_caches(true);
1079
    unset($USER->access);
1080
 
1081
    // Prevent dirty flags refetching on this page.
1082
    $ACCESSLIB_PRIVATE->dirtycontexts = array();
1083
    $ACCESSLIB_PRIVATE->dirtyusers    = array($USER->id => false);
1084
 
1085
    load_all_capabilities();
1086
 
1087
    foreach ($sw as $path => $roleid) {
1088
        if ($record = $DB->get_record('context', array('path'=>$path))) {
1089
            $context = context::instance_by_id($record->id);
1090
            if (has_capability('moodle/role:switchroles', $context)) {
1091
                role_switch($roleid, $context);
1092
            }
1093
        }
1094
    }
1095
}
1096
 
1097
/**
1098
 * Adds a temp role to current USER->access array.
1099
 *
1100
 * Useful for the "temporary guest" access we grant to logged-in users.
1101
 * This is useful for enrol plugins only.
1102
 *
1103
 * @since Moodle 2.2
1104
 * @param context_course $coursecontext
1105
 * @param int $roleid
1106
 * @return void
1107
 */
1108
function load_temp_course_role(context_course $coursecontext, $roleid) {
1109
    global $USER, $SITE;
1110
 
1111
    if (empty($roleid)) {
1112
        debugging('invalid role specified in load_temp_course_role()');
1113
        return;
1114
    }
1115
 
1116
    if ($coursecontext->instanceid == $SITE->id) {
1117
        debugging('Can not use temp roles on the frontpage');
1118
        return;
1119
    }
1120
 
1121
    if (!isset($USER->access)) {
1122
        load_all_capabilities();
1123
    }
1124
 
1125
    $coursecontext->reload_if_dirty();
1126
 
1127
    if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
1128
        return;
1129
    }
1130
 
1131
    $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
1132
}
1133
 
1134
/**
1135
 * Removes any extra guest roles from current USER->access array.
1136
 * This is useful for enrol plugins only.
1137
 *
1138
 * @since Moodle 2.2
1139
 * @param context_course $coursecontext
1140
 * @return void
1141
 */
1142
function remove_temp_course_roles(context_course $coursecontext) {
1143
    global $DB, $USER, $SITE;
1144
 
1145
    if ($coursecontext->instanceid == $SITE->id) {
1146
        debugging('Can not use temp roles on the frontpage');
1147
        return;
1148
    }
1149
 
1150
    if (empty($USER->access['ra'][$coursecontext->path])) {
1151
        //no roles here, weird
1152
        return;
1153
    }
1154
 
1155
    $sql = "SELECT DISTINCT ra.roleid AS id
1156
              FROM {role_assignments} ra
1157
             WHERE ra.contextid = :contextid AND ra.userid = :userid";
1158
    $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
1159
 
1160
    $USER->access['ra'][$coursecontext->path] = array();
1161
    foreach ($ras as $r) {
1162
        $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
1163
    }
1164
}
1165
 
1166
/**
1167
 * Returns array of all role archetypes.
1168
 *
1169
 * @return array
1170
 */
1171
function get_role_archetypes() {
1172
    return array(
1173
        'manager'        => 'manager',
1174
        'coursecreator'  => 'coursecreator',
1175
        'editingteacher' => 'editingteacher',
1176
        'teacher'        => 'teacher',
1177
        'student'        => 'student',
1178
        'guest'          => 'guest',
1179
        'user'           => 'user',
1180
        'frontpage'      => 'frontpage'
1181
    );
1182
}
1183
 
1184
/**
1185
 * Assign the defaults found in this capability definition to roles that have
1186
 * the corresponding legacy capabilities assigned to them.
1187
 *
1188
 * @param string $capability
1189
 * @param array $legacyperms an array in the format (example):
1190
 *                      'guest' => CAP_PREVENT,
1191
 *                      'student' => CAP_ALLOW,
1192
 *                      'teacher' => CAP_ALLOW,
1193
 *                      'editingteacher' => CAP_ALLOW,
1194
 *                      'coursecreator' => CAP_ALLOW,
1195
 *                      'manager' => CAP_ALLOW
1196
 * @return boolean success or failure.
1197
 */
1198
function assign_legacy_capabilities($capability, $legacyperms) {
1199
 
1200
    $archetypes = get_role_archetypes();
1201
 
1202
    foreach ($legacyperms as $type => $perm) {
1203
 
1204
        $systemcontext = context_system::instance();
1205
        if ($type === 'admin') {
1206
            debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
1207
            $type = 'manager';
1208
        }
1209
 
1210
        if (!array_key_exists($type, $archetypes)) {
1211
            throw new \moodle_exception('invalidlegacy', '', '', $type);
1212
        }
1213
 
1214
        if ($roles = get_archetype_roles($type)) {
1215
            foreach ($roles as $role) {
1216
                // Assign a site level capability.
1217
                if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
1218
                    return false;
1219
                }
1220
            }
1221
        }
1222
    }
1223
    return true;
1224
}
1225
 
1226
/**
1227
 * Verify capability risks.
1228
 *
1229
 * @param stdClass $capability a capability - a row from the capabilities table.
1230
 * @return boolean whether this capability is safe - that is, whether people with the
1231
 *      safeoverrides capability should be allowed to change it.
1232
 */
1233
function is_safe_capability($capability) {
1234
    return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
1235
}
1236
 
1237
/**
1238
 * Get the local override (if any) for a given capability in a role in a context
1239
 *
1240
 * @param int $roleid
1241
 * @param int $contextid
1242
 * @param string $capability
1243
 * @return stdClass local capability override
1244
 */
1245
function get_local_override($roleid, $contextid, $capability) {
1246
    global $DB;
1247
 
1248
    return $DB->get_record_sql("
1249
        SELECT rc.*
1250
          FROM {role_capabilities} rc
1251
          JOIN {capability} cap ON rc.capability = cap.name
1252
         WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
1253
            'roleid' => $roleid,
1254
            'contextid' => $contextid,
1255
            'capability' => $capability,
1256
 
1257
        ]);
1258
}
1259
 
1260
/**
1261
 * Returns context instance plus related course and cm instances
1262
 *
1263
 * @param int $contextid
1264
 * @return array of ($context, $course, $cm)
1265
 */
1266
function get_context_info_array($contextid) {
1267
    global $DB;
1268
 
1269
    $context = context::instance_by_id($contextid, MUST_EXIST);
1270
    $course  = null;
1271
    $cm      = null;
1272
 
1273
    if ($context->contextlevel == CONTEXT_COURSE) {
1274
        $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
1275
 
1276
    } else if ($context->contextlevel == CONTEXT_MODULE) {
1277
        $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
1278
        $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1279
 
1280
    } else if ($context->contextlevel == CONTEXT_BLOCK) {
1281
        $parent = $context->get_parent_context();
1282
 
1283
        if ($parent->contextlevel == CONTEXT_COURSE) {
1284
            $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
1285
        } else if ($parent->contextlevel == CONTEXT_MODULE) {
1286
            $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
1287
            $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
1288
        }
1289
    }
1290
 
1291
    return array($context, $course, $cm);
1292
}
1293
 
1294
/**
1295
 * Function that creates a role
1296
 *
1297
 * @param string $name role name
1298
 * @param string $shortname role short name
1299
 * @param string $description role description
1300
 * @param string $archetype
1301
 * @return int id or dml_exception
1302
 */
1303
function create_role($name, $shortname, $description, $archetype = '') {
1304
    global $DB;
1305
 
1306
    if (strpos($archetype, 'moodle/legacy:') !== false) {
1307
        throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
1308
    }
1309
 
1310
    // verify role archetype actually exists
1311
    $archetypes = get_role_archetypes();
1312
    if (empty($archetypes[$archetype])) {
1313
        $archetype = '';
1314
    }
1315
 
1316
    // Insert the role record.
1317
    $role = new stdClass();
1318
    $role->name        = $name;
1319
    $role->shortname   = $shortname;
1320
    $role->description = $description;
1321
    $role->archetype   = $archetype;
1322
 
1323
    //find free sortorder number
1324
    $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
1325
    if (empty($role->sortorder)) {
1326
        $role->sortorder = 1;
1327
    }
1328
    $role->id = $DB->insert_record('role', $role);
1329
    $event = \core\event\role_created::create([
1330
        'objectid' => $role->id,
1331
        'context' => context_system::instance(),
1332
        'other' => [
1333
            'name' => $role->name,
1334
            'shortname' => $role->shortname,
1335
            'archetype' => $role->archetype,
1336
        ]
1337
    ]);
1338
 
1339
    $event->add_record_snapshot('role', $role);
1340
    $event->trigger();
1341
 
1342
    return $role->id;
1343
}
1344
 
1345
/**
1346
 * Function that deletes a role and cleanups up after it
1347
 *
1348
 * @param int $roleid id of role to delete
1349
 * @return bool always true
1350
 */
1351
function delete_role($roleid) {
1352
    global $DB;
1353
 
1354
    // first unssign all users
1355
    role_unassign_all(array('roleid'=>$roleid));
1356
 
1357
    // cleanup all references to this role, ignore errors
1358
    $DB->delete_records('role_capabilities',   array('roleid'=>$roleid));
1359
    $DB->delete_records('role_allow_assign',   array('roleid'=>$roleid));
1360
    $DB->delete_records('role_allow_assign',   array('allowassign'=>$roleid));
1361
    $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
1362
    $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
1363
    $DB->delete_records('role_names',          array('roleid'=>$roleid));
1364
    $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
1365
 
1366
    // Get role record before it's deleted.
1367
    $role = $DB->get_record('role', array('id'=>$roleid));
1368
 
1369
    // Finally delete the role itself.
1370
    $DB->delete_records('role', array('id'=>$roleid));
1371
 
1372
    // Trigger event.
1373
    $event = \core\event\role_deleted::create(
1374
        array(
1375
            'context' => context_system::instance(),
1376
            'objectid' => $roleid,
1377
            'other' =>
1378
                array(
1379
                    'shortname' => $role->shortname,
1380
                    'description' => $role->description,
1381
                    'archetype' => $role->archetype
1382
                )
1383
            )
1384
        );
1385
    $event->add_record_snapshot('role', $role);
1386
    $event->trigger();
1387
 
1388
    // Reset any cache of this role, including MUC.
1389
    accesslib_clear_role_cache($roleid);
1390
 
1391
    return true;
1392
}
1393
 
1394
/**
1395
 * Function to write context specific overrides, or default capabilities.
1396
 *
1397
 * The $performancehints array can currently contain two values intended to make this faster when
1398
 * this function is being called in a loop, if you have already checked certain details:
1399
 * 'contextexists' - if we already know the contextid exists in context table
1400
 * ASSIGN_HINT_NO_EXISTING - if we already know there is no entry in role_capabilities matching
1401
 *   contextid, roleid, and capability
1402
 *
1403
 * @param string $capability string name
1404
 * @param int $permission CAP_ constants
1405
 * @param int $roleid role id
1406
 * @param int|context $contextid context id
1407
 * @param bool $overwrite
1408
 * @param string[] $performancehints Performance hints - leave blank unless needed
1409
 * @return bool always true or exception
1410
 */
1411
function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false, array $performancehints = []) {
1412
    global $USER, $DB;
1413
 
1414
    if ($contextid instanceof context) {
1415
        $context = $contextid;
1416
    } else {
1417
        $context = context::instance_by_id($contextid);
1418
    }
1419
 
1420
    // Capability must exist.
1421
    if (!$capinfo = get_capability_info($capability)) {
1422
        throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1423
    }
1424
 
1425
    if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
1426
        unassign_capability($capability, $roleid, $context->id);
1427
        return true;
1428
    }
1429
 
1430
    if (in_array(ACCESSLIB_HINT_NO_EXISTING, $performancehints)) {
1431
        $existing = false;
1432
    } else {
1433
        $existing = $DB->get_record('role_capabilities',
1434
                ['contextid' => $context->id, 'roleid' => $roleid, 'capability' => $capability]);
1435
    }
1436
 
1437
    if ($existing and !$overwrite) {   // We want to keep whatever is there already
1438
        return true;
1439
    }
1440
 
1441
    $cap = new stdClass();
1442
    $cap->contextid    = $context->id;
1443
    $cap->roleid       = $roleid;
1444
    $cap->capability   = $capability;
1445
    $cap->permission   = $permission;
1446
    $cap->timemodified = time();
1447
    $cap->modifierid   = empty($USER->id) ? 0 : $USER->id;
1448
 
1449
    if ($existing) {
1450
        $cap->id = $existing->id;
1451
        $DB->update_record('role_capabilities', $cap);
1452
    } else {
1453
        if (in_array(ACCESSLIB_HINT_CONTEXT_EXISTS, $performancehints) ||
1454
                $DB->record_exists('context', ['id' => $context->id])) {
1455
            $DB->insert_record('role_capabilities', $cap);
1456
        }
1457
    }
1458
 
1459
    // Trigger capability_assigned event.
1460
    \core\event\capability_assigned::create([
1461
        'userid' => $cap->modifierid,
1462
        'context' => $context,
1463
        'objectid' => $roleid,
1464
        'other' => [
1465
            'capability' => $capability,
1466
            'oldpermission' => $existing->permission ?? CAP_INHERIT,
1467
            'permission' => $permission
1468
        ]
1469
    ])->trigger();
1470
 
1471
    // Reset any cache of this role, including MUC.
1472
    accesslib_clear_role_cache($roleid);
1473
 
1474
    return true;
1475
}
1476
 
1477
/**
1478
 * Unassign a capability from a role.
1479
 *
1480
 * @param string $capability the name of the capability
1481
 * @param int $roleid the role id
1482
 * @param int|context $contextid null means all contexts
1483
 * @return boolean true or exception
1484
 */
1485
function unassign_capability($capability, $roleid, $contextid = null) {
1486
    global $DB, $USER;
1487
 
1488
    // Capability must exist.
1489
    if (!$capinfo = get_capability_info($capability)) {
1490
        throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
1491
    }
1492
 
1493
    if (!empty($contextid)) {
1494
        if ($contextid instanceof context) {
1495
            $context = $contextid;
1496
        } else {
1497
            $context = context::instance_by_id($contextid);
1498
        }
1499
        // delete from context rel, if this is the last override in this context
1500
        $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
1501
    } else {
1502
        $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
1503
    }
1504
 
1505
    // Trigger capability_assigned event.
1506
    \core\event\capability_unassigned::create([
1507
        'userid' => $USER->id,
1508
        'context' => $context ?? context_system::instance(),
1509
        'objectid' => $roleid,
1510
        'other' => [
1511
            'capability' => $capability,
1512
        ]
1513
    ])->trigger();
1514
 
1515
    // Reset any cache of this role, including MUC.
1516
    accesslib_clear_role_cache($roleid);
1517
 
1518
    return true;
1519
}
1520
 
1521
/**
1522
 * Get the roles that have a given capability assigned to it
1523
 *
1524
 * This function does not resolve the actual permission of the capability.
1525
 * It just checks for permissions and overrides.
1526
 * Use get_roles_with_cap_in_context() if resolution is required.
1527
 *
1528
 * @param string $capability capability name (string)
1529
 * @param string $permission optional, the permission defined for this capability
1530
 *                      either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
1531
 * @param context|null $context null means any
1532
 * @return array of role records
1533
 */
1534
function get_roles_with_capability($capability, $permission = null, $context = null) {
1535
    global $DB;
1536
 
1537
    if ($context) {
1538
        $contexts = $context->get_parent_context_ids(true);
1539
        list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
1540
        $contextsql = "AND rc.contextid $insql";
1541
    } else {
1542
        $params = array();
1543
        $contextsql = '';
1544
    }
1545
 
1546
    if ($permission) {
1547
        $permissionsql = " AND rc.permission = :permission";
1548
        $params['permission'] = $permission;
1549
    } else {
1550
        $permissionsql = '';
1551
    }
1552
 
1553
    $sql = "SELECT r.*
1554
              FROM {role} r
1555
             WHERE r.id IN (SELECT rc.roleid
1556
                              FROM {role_capabilities} rc
1557
                              JOIN {capabilities} cap ON rc.capability = cap.name
1558
                             WHERE rc.capability = :capname
1559
                                   $contextsql
1560
                                   $permissionsql)";
1561
    $params['capname'] = $capability;
1562
 
1563
 
1564
    return $DB->get_records_sql($sql, $params);
1565
}
1566
 
1567
/**
1568
 * This function makes a role-assignment (a role for a user in a particular context)
1569
 *
1570
 * @param int $roleid the role of the id
1571
 * @param int $userid userid
1572
 * @param int|context $contextid id of the context
1573
 * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
1574
 * @param int $itemid id of enrolment/auth plugin
1575
 * @param string $timemodified defaults to current time
1576
 * @return int new/existing id of the assignment
1577
 */
1578
function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
1579
    global $USER, $DB;
1580
 
1581
    // first of all detect if somebody is using old style parameters
1582
    if ($contextid === 0 or is_numeric($component)) {
1583
        throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
1584
    }
1585
 
1586
    // now validate all parameters
1587
    if (empty($roleid)) {
1588
        throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
1589
    }
1590
 
1591
    if (empty($userid)) {
1592
        throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
1593
    }
1594
 
1595
    if ($itemid) {
1596
        if (strpos($component, '_') === false) {
1597
            throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
1598
        }
1599
    } else {
1600
        $itemid = 0;
1601
        if ($component !== '' and strpos($component, '_') === false) {
1602
            throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1603
        }
1604
    }
1605
 
1606
    if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
1607
        throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
1608
    }
1609
 
1610
    if ($contextid instanceof context) {
1611
        $context = $contextid;
1612
    } else {
1613
        $context = context::instance_by_id($contextid, MUST_EXIST);
1614
    }
1615
 
1616
    if (!$timemodified) {
1617
        $timemodified = time();
1618
    }
1619
 
1620
    // Check for existing entry
1621
    $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
1622
 
1623
    if ($ras) {
1624
        // role already assigned - this should not happen
1625
        if (count($ras) > 1) {
1626
            // very weird - remove all duplicates!
1627
            $ra = array_shift($ras);
1628
            foreach ($ras as $r) {
1629
                $DB->delete_records('role_assignments', array('id'=>$r->id));
1630
            }
1631
        } else {
1632
            $ra = reset($ras);
1633
        }
1634
 
1635
        // actually there is no need to update, reset anything or trigger any event, so just return
1636
        return $ra->id;
1637
    }
1638
 
1639
    // Create a new entry
1640
    $ra = new stdClass();
1641
    $ra->roleid       = $roleid;
1642
    $ra->contextid    = $context->id;
1643
    $ra->userid       = $userid;
1644
    $ra->component    = $component;
1645
    $ra->itemid       = $itemid;
1646
    $ra->timemodified = $timemodified;
1647
    $ra->modifierid   = empty($USER->id) ? 0 : $USER->id;
1648
    $ra->sortorder    = 0;
1649
 
1650
    $ra->id = $DB->insert_record('role_assignments', $ra);
1651
 
1652
    // Role assignments have changed, so mark user as dirty.
1653
    mark_user_dirty($userid);
1654
 
1655
    core_course_category::role_assignment_changed($roleid, $context);
1656
 
1657
    $event = \core\event\role_assigned::create(array(
1658
        'context' => $context,
1659
        'objectid' => $ra->roleid,
1660
        'relateduserid' => $ra->userid,
1661
        'other' => array(
1662
            'id' => $ra->id,
1663
            'component' => $ra->component,
1664
            'itemid' => $ra->itemid
1665
        )
1666
    ));
1667
    $event->add_record_snapshot('role_assignments', $ra);
1668
    $event->trigger();
1669
 
1670
    // Dispatch the hook for post role assignment actions.
1671
    $hook = new \core\hook\access\after_role_assigned(
1672
        context: $context,
1673
        userid: $userid,
1674
    );
1675
    \core\di::get(\core\hook\manager::class)->dispatch($hook);
1676
 
1677
    return $ra->id;
1678
}
1679
 
1680
/**
1681
 * Removes one role assignment
1682
 *
1683
 * @param int $roleid
1684
 * @param int  $userid
1685
 * @param int  $contextid
1686
 * @param string $component
1687
 * @param int  $itemid
1688
 * @return void
1689
 */
1690
function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
1691
    // first make sure the params make sense
1692
    if ($roleid == 0 or $userid == 0 or $contextid == 0) {
1693
        throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
1694
    }
1695
 
1696
    if ($itemid) {
1697
        if (strpos($component, '_') === false) {
1698
            throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
1699
        }
1700
    } else {
1701
        $itemid = 0;
1702
        if ($component !== '' and strpos($component, '_') === false) {
1703
            throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
1704
        }
1705
    }
1706
 
1707
    role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
1708
}
1709
 
1710
/**
1711
 * Removes multiple role assignments, parameters may contain:
1712
 *   'roleid', 'userid', 'contextid', 'component', 'enrolid'.
1713
 *
1714
 * @param array $params role assignment parameters
1715
 * @param bool $subcontexts unassign in subcontexts too
1716
 * @param bool $includemanual include manual role assignments too
1717
 * @return void
1718
 */
1719
function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
1720
    global $USER, $CFG, $DB;
1721
 
1722
    if (!$params) {
1723
        throw new coding_exception('Missing parameters in role_unsassign_all() call');
1724
    }
1725
 
1726
    $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
1727
    foreach ($params as $key=>$value) {
1728
        if (!in_array($key, $allowed)) {
1729
            throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
1730
        }
1731
    }
1732
 
1733
    if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
1734
        throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
1735
    }
1736
 
1737
    if ($includemanual) {
1738
        if (!isset($params['component']) or $params['component'] === '') {
1739
            throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
1740
        }
1741
    }
1742
 
1743
    if ($subcontexts) {
1744
        if (empty($params['contextid'])) {
1745
            throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
1746
        }
1747
    }
1748
 
1749
    $ras = $DB->get_records('role_assignments', $params);
1750
    foreach ($ras as $ra) {
1751
        $DB->delete_records('role_assignments', array('id'=>$ra->id));
1752
        if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
1753
            // Role assignments have changed, so mark user as dirty.
1754
            mark_user_dirty($ra->userid);
1755
 
1756
            $event = \core\event\role_unassigned::create(array(
1757
                'context' => $context,
1758
                'objectid' => $ra->roleid,
1759
                'relateduserid' => $ra->userid,
1760
                'other' => array(
1761
                    'id' => $ra->id,
1762
                    'component' => $ra->component,
1763
                    'itemid' => $ra->itemid
1764
                )
1765
            ));
1766
            $event->add_record_snapshot('role_assignments', $ra);
1767
            $event->trigger();
1768
            core_course_category::role_assignment_changed($ra->roleid, $context);
1769
 
1770
            // Dispatch the hook for post role assignment actions.
1771
            $hook = new \core\hook\access\after_role_unassigned(
1772
                context: $context,
1773
                userid: $ra->userid,
1774
            );
1775
            \core\di::get(\core\hook\manager::class)->dispatch($hook);
1776
        }
1777
    }
1778
    unset($ras);
1779
 
1780
    // process subcontexts
1781
    if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
1782
        if ($params['contextid'] instanceof context) {
1783
            $context = $params['contextid'];
1784
        } else {
1785
            $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
1786
        }
1787
 
1788
        if ($context) {
1789
            $contexts = $context->get_child_contexts();
1790
            $mparams = $params;
1791
            foreach ($contexts as $context) {
1792
                $mparams['contextid'] = $context->id;
1793
                $ras = $DB->get_records('role_assignments', $mparams);
1794
                foreach ($ras as $ra) {
1795
                    $DB->delete_records('role_assignments', array('id'=>$ra->id));
1796
                    // Role assignments have changed, so mark user as dirty.
1797
                    mark_user_dirty($ra->userid);
1798
 
1799
                    $event = \core\event\role_unassigned::create(
1800
                        array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
1801
                            'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
1802
                    $event->add_record_snapshot('role_assignments', $ra);
1803
                    $event->trigger();
1804
                    core_course_category::role_assignment_changed($ra->roleid, $context);
1805
                }
1806
            }
1807
        }
1808
    }
1809
 
1810
    // do this once more for all manual role assignments
1811
    if ($includemanual) {
1812
        $params['component'] = '';
1813
        role_unassign_all($params, $subcontexts, false);
1814
    }
1815
}
1816
 
1817
/**
1818
 * Mark a user as dirty (with timestamp) so as to force reloading of the user session.
1819
 *
1820
 * @param int $userid
1821
 * @return void
1822
 */
1823
function mark_user_dirty($userid) {
1824
    global $CFG, $ACCESSLIB_PRIVATE;
1825
 
1826
    if (during_initial_install()) {
1827
        return;
1828
    }
1829
 
1830
    // Throw exception if invalid userid is provided.
1831
    if (empty($userid)) {
1832
        throw new coding_exception('Invalid user parameter supplied for mark_user_dirty() function!');
1833
    }
1834
 
1835
    // Set dirty flag in database, set dirty field locally, and clear local accessdata cache.
1836
    set_cache_flag('accesslib/dirtyusers', $userid, 1, time() + $CFG->sessiontimeout);
1837
    $ACCESSLIB_PRIVATE->dirtyusers[$userid] = 1;
1838
    unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
1839
}
1840
 
1841
/**
1842
 * Determines if a user is currently logged in
1843
 *
1844
 * @category   access
1845
 *
1846
 * @return bool
1847
 */
1848
function isloggedin() {
1849
    global $USER;
1850
 
1851
    return (!empty($USER->id));
1852
}
1853
 
1854
/**
1855
 * Determines if a user is logged in as real guest user with username 'guest'.
1856
 *
1857
 * @category   access
1858
 *
1859
 * @param int|object $user mixed user object or id, $USER if not specified
1860
 * @return bool true if user is the real guest user, false if not logged in or other user
1861
 */
1862
function isguestuser($user = null) {
1863
    global $USER, $DB, $CFG;
1864
 
1865
    // make sure we have the user id cached in config table, because we are going to use it a lot
1866
    if (empty($CFG->siteguest)) {
1867
        if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
1868
            // guest does not exist yet, weird
1869
            return false;
1870
        }
1871
        set_config('siteguest', $guestid);
1872
    }
1873
    if ($user === null) {
1874
        $user = $USER;
1875
    }
1876
 
1877
    if ($user === null) {
1878
        // happens when setting the $USER
1879
        return false;
1880
 
1881
    } else if (is_numeric($user)) {
1882
        return ($CFG->siteguest == $user);
1883
 
1884
    } else if (is_object($user)) {
1885
        if (empty($user->id)) {
1886
            return false; // not logged in means is not be guest
1887
        } else {
1888
            return ($CFG->siteguest == $user->id);
1889
        }
1890
 
1891
    } else {
1892
        throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
1893
    }
1894
}
1895
 
1896
/**
1897
 * Does user have a (temporary or real) guest access to course?
1898
 *
1899
 * @category   access
1900
 *
1901
 * @param context $context
1902
 * @param stdClass|int $user
1903
 * @return bool
1904
 */
1905
function is_guest(context $context, $user = null) {
1906
    global $USER;
1907
 
1908
    // first find the course context
1909
    $coursecontext = $context->get_course_context();
1910
 
1911
    // make sure there is a real user specified
1912
    if ($user === null) {
1913
        $userid = isset($USER->id) ? $USER->id : 0;
1914
    } else {
1915
        $userid = is_object($user) ? $user->id : $user;
1916
    }
1917
 
1918
    if (isguestuser($userid)) {
1919
        // can not inspect or be enrolled
1920
        return true;
1921
    }
1922
 
1923
    if (has_capability('moodle/course:view', $coursecontext, $user)) {
1924
        // viewing users appear out of nowhere, they are neither guests nor participants
1925
        return false;
1926
    }
1927
 
1928
    // consider only real active enrolments here
1929
    if (is_enrolled($coursecontext, $user, '', true)) {
1930
        return false;
1931
    }
1932
 
1933
    return true;
1934
}
1935
 
1936
/**
1937
 * Returns true if the user has moodle/course:view capability in the course,
1938
 * this is intended for admins, managers (aka small admins), inspectors, etc.
1939
 *
1940
 * @category   access
1941
 *
1942
 * @param context $context
1943
 * @param int|stdClass $user if null $USER is used
1944
 * @param string $withcapability extra capability name
1945
 * @return bool
1946
 */
1947
function is_viewing(context $context, $user = null, $withcapability = '') {
1948
    // first find the course context
1949
    $coursecontext = $context->get_course_context();
1950
 
1951
    if (isguestuser($user)) {
1952
        // can not inspect
1953
        return false;
1954
    }
1955
 
1956
    if (!has_capability('moodle/course:view', $coursecontext, $user)) {
1957
        // admins are allowed to inspect courses
1958
        return false;
1959
    }
1960
 
1961
    if ($withcapability and !has_capability($withcapability, $context, $user)) {
1962
        // site admins always have the capability, but the enrolment above blocks
1963
        return false;
1964
    }
1965
 
1966
    return true;
1967
}
1968
 
1969
/**
1970
 * Returns true if the user is able to access the course.
1971
 *
1972
 * This function is in no way, shape, or form a substitute for require_login.
1973
 * It should only be used in circumstances where it is not possible to call require_login
1974
 * such as the navigation.
1975
 *
1976
 * This function checks many of the methods of access to a course such as the view
1977
 * capability, enrollments, and guest access. It also makes use of the cache
1978
 * generated by require_login for guest access.
1979
 *
1980
 * The flags within the $USER object that are used here should NEVER be used outside
1981
 * of this function can_access_course and require_login. Doing so WILL break future
1982
 * versions.
1983
 *
1984
 * @param stdClass $course record
1985
 * @param stdClass|int|null $user user record or id, current user if null
1986
 * @param string $withcapability Check for this capability as well.
1987
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1988
 * @return boolean Returns true if the user is able to access the course
1989
 */
1990
function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
1991
    global $DB, $USER;
1992
 
1993
    // this function originally accepted $coursecontext parameter
1994
    if ($course instanceof context) {
1995
        if ($course instanceof context_course) {
1996
            debugging('deprecated context parameter, please use $course record');
1997
            $coursecontext = $course;
1998
            $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
1999
        } else {
2000
            debugging('Invalid context parameter, please use $course record');
2001
            return false;
2002
        }
2003
    } else {
2004
        $coursecontext = context_course::instance($course->id);
2005
    }
2006
 
2007
    if (!isset($USER->id)) {
2008
        // should never happen
2009
        $USER->id = 0;
2010
        debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
2011
    }
2012
 
2013
    // make sure there is a user specified
2014
    if ($user === null) {
2015
        $userid = $USER->id;
2016
    } else {
2017
        $userid = is_object($user) ? $user->id : $user;
2018
    }
2019
    unset($user);
2020
 
2021
    if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
2022
        return false;
2023
    }
2024
 
2025
    if ($userid == $USER->id) {
2026
        if (!empty($USER->access['rsw'][$coursecontext->path])) {
2027
            // the fact that somebody switched role means they can access the course no matter to what role they switched
2028
            return true;
2029
        }
2030
    }
2031
 
2032
    if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
2033
        return false;
2034
    }
2035
 
2036
    if (is_viewing($coursecontext, $userid)) {
2037
        return true;
2038
    }
2039
 
2040
    if ($userid != $USER->id) {
2041
        // for performance reasons we do not verify temporary guest access for other users, sorry...
2042
        return is_enrolled($coursecontext, $userid, '', $onlyactive);
2043
    }
2044
 
2045
    // === from here we deal only with $USER ===
2046
 
2047
    $coursecontext->reload_if_dirty();
2048
 
2049
    if (isset($USER->enrol['enrolled'][$course->id])) {
2050
        if ($USER->enrol['enrolled'][$course->id] > time()) {
2051
            return true;
2052
        }
2053
    }
2054
    if (isset($USER->enrol['tempguest'][$course->id])) {
2055
        if ($USER->enrol['tempguest'][$course->id] > time()) {
2056
            return true;
2057
        }
2058
    }
2059
 
2060
    if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
2061
        return true;
2062
    }
2063
 
2064
    if (!core_course_category::can_view_course_info($course)) {
2065
        // No guest access if user does not have capability to browse courses.
2066
        return false;
2067
    }
2068
 
2069
    // if not enrolled try to gain temporary guest access
2070
    $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
2071
    $enrols = enrol_get_plugins(true);
2072
    foreach ($instances as $instance) {
2073
        if (!isset($enrols[$instance->enrol])) {
2074
            continue;
2075
        }
2076
        // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
2077
        $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2078
        if ($until !== false and $until > time()) {
2079
            $USER->enrol['tempguest'][$course->id] = $until;
2080
            return true;
2081
        }
2082
    }
2083
    if (isset($USER->enrol['tempguest'][$course->id])) {
2084
        unset($USER->enrol['tempguest'][$course->id]);
2085
        remove_temp_course_roles($coursecontext);
2086
    }
2087
 
2088
    return false;
2089
}
2090
 
2091
/**
2092
 * Loads the capability definitions for the component (from file).
2093
 *
2094
 * Loads the capability definitions for the component (from file). If no
2095
 * capabilities are defined for the component, we simply return an empty array.
2096
 *
2097
 * @access private
2098
 * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
2099
 * @return array array of capabilities
2100
 */
2101
function load_capability_def($component) {
2102
    $defpath = core_component::get_component_directory($component).'/db/access.php';
2103
 
2104
    $capabilities = array();
2105
    if (file_exists($defpath)) {
2106
        require($defpath);
2107
        if (!empty(${$component.'_capabilities'})) {
2108
            // BC capability array name
2109
            // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
2110
            debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
2111
            $capabilities = ${$component.'_capabilities'};
2112
        }
2113
    }
2114
 
2115
    return $capabilities;
2116
}
2117
 
2118
/**
2119
 * Gets the capabilities that have been cached in the database for this component.
2120
 *
2121
 * @access private
2122
 * @param string $component - examples: 'moodle', 'mod_forum'
2123
 * @return array array of capabilities
2124
 */
2125
function get_cached_capabilities($component = 'moodle') {
2126
    global $DB;
2127
    $caps = get_all_capabilities();
2128
    $componentcaps = array();
2129
    foreach ($caps as $cap) {
2130
        if ($cap['component'] == $component) {
2131
            $componentcaps[] = (object) $cap;
2132
        }
2133
    }
2134
    return $componentcaps;
2135
}
2136
 
2137
/**
2138
 * Returns default capabilities for given role archetype.
2139
 *
2140
 * @param string $archetype role archetype
2141
 * @return array
2142
 */
2143
function get_default_capabilities($archetype) {
2144
    global $DB;
2145
 
2146
    if (!$archetype) {
2147
        return array();
2148
    }
2149
 
2150
    $alldefs = array();
2151
    $defaults = array();
2152
    $components = array();
2153
    $allcaps = get_all_capabilities();
2154
 
2155
    foreach ($allcaps as $cap) {
2156
        if (!in_array($cap['component'], $components)) {
2157
            $components[] = $cap['component'];
2158
            $alldefs = array_merge($alldefs, load_capability_def($cap['component']));
2159
        }
2160
    }
2161
    foreach ($alldefs as $name=>$def) {
2162
        // Use array 'archetypes if available. Only if not specified, use 'legacy'.
2163
        if (isset($def['archetypes'])) {
2164
            if (isset($def['archetypes'][$archetype])) {
2165
                $defaults[$name] = $def['archetypes'][$archetype];
2166
            }
2167
        // 'legacy' is for backward compatibility with 1.9 access.php
2168
        } else {
2169
            if (isset($def['legacy'][$archetype])) {
2170
                $defaults[$name] = $def['legacy'][$archetype];
2171
            }
2172
        }
2173
    }
2174
 
2175
    return $defaults;
2176
}
2177
 
2178
/**
2179
 * Return default roles that can be assigned, overridden or switched
2180
 * by give role archetype.
2181
 *
2182
 * @param string $type  assign|override|switch|view
2183
 * @param string $archetype
2184
 * @return array of role ids
2185
 */
2186
function get_default_role_archetype_allows($type, $archetype) {
2187
    global $DB;
2188
 
2189
    if (empty($archetype)) {
2190
        return array();
2191
    }
2192
 
2193
    $roles = $DB->get_records('role');
2194
    $archetypemap = array();
2195
    foreach ($roles as $role) {
2196
        if ($role->archetype) {
2197
            $archetypemap[$role->archetype][$role->id] = $role->id;
2198
        }
2199
    }
2200
 
2201
    $defaults = array(
2202
        'assign' => array(
2203
            'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
2204
            'coursecreator'  => array(),
2205
            'editingteacher' => array('teacher', 'student'),
2206
            'teacher'        => array(),
2207
            'student'        => array(),
2208
            'guest'          => array(),
2209
            'user'           => array(),
2210
            'frontpage'      => array(),
2211
        ),
2212
        'override' => array(
2213
            'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2214
            'coursecreator'  => array(),
2215
            'editingteacher' => array('teacher', 'student', 'guest'),
2216
            'teacher'        => array(),
2217
            'student'        => array(),
2218
            'guest'          => array(),
2219
            'user'           => array(),
2220
            'frontpage'      => array(),
2221
        ),
2222
        'switch' => array(
2223
            'manager'        => array('editingteacher', 'teacher', 'student', 'guest'),
2224
            'coursecreator'  => array(),
2225
            'editingteacher' => array('teacher', 'student', 'guest'),
2226
            'teacher'        => array('student', 'guest'),
2227
            'student'        => array(),
2228
            'guest'          => array(),
2229
            'user'           => array(),
2230
            'frontpage'      => array(),
2231
        ),
2232
        'view' => array(
2233
            'manager'        => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
2234
            'coursecreator'  => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2235
            'editingteacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2236
            'teacher'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2237
            'student'        => array('coursecreator', 'editingteacher', 'teacher', 'student'),
2238
            'guest'          => array(),
2239
            'user'           => array(),
2240
            'frontpage'      => array(),
2241
        ),
2242
    );
2243
 
2244
    if (!isset($defaults[$type][$archetype])) {
2245
        debugging("Unknown type '$type'' or archetype '$archetype''");
2246
        return array();
2247
    }
2248
 
2249
    $return = array();
2250
    foreach ($defaults[$type][$archetype] as $at) {
2251
        if (isset($archetypemap[$at])) {
2252
            foreach ($archetypemap[$at] as $roleid) {
2253
                $return[$roleid] = $roleid;
2254
            }
2255
        }
2256
    }
2257
 
2258
    return $return;
2259
}
2260
 
2261
/**
2262
 * Reset role capabilities to default according to selected role archetype.
2263
 * If no archetype selected, removes all capabilities.
2264
 *
2265
 * This applies to capabilities that are assigned to the role (that you could
2266
 * edit in the 'define roles' interface), and not to any capability overrides
2267
 * in different locations.
2268
 *
2269
 * @param int $roleid ID of role to reset capabilities for
2270
 */
2271
function reset_role_capabilities($roleid) {
2272
    global $DB;
2273
 
2274
    $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
2275
    $defaultcaps = get_default_capabilities($role->archetype);
2276
 
2277
    $systemcontext = context_system::instance();
2278
 
2279
    $DB->delete_records('role_capabilities',
2280
            array('roleid' => $roleid, 'contextid' => $systemcontext->id));
2281
 
2282
    foreach ($defaultcaps as $cap=>$permission) {
2283
        assign_capability($cap, $permission, $roleid, $systemcontext->id);
2284
    }
2285
 
2286
    // Reset any cache of this role, including MUC.
2287
    accesslib_clear_role_cache($roleid);
2288
}
2289
 
2290
/**
2291
 * Updates the capabilities table with the component capability definitions.
2292
 * If no parameters are given, the function updates the core moodle
2293
 * capabilities.
2294
 *
2295
 * Note that the absence of the db/access.php capabilities definition file
2296
 * will cause any stored capabilities for the component to be removed from
2297
 * the database.
2298
 *
2299
 * @access private
2300
 * @param string $component examples: 'moodle', 'mod_forum', 'block_activity_results'
2301
 * @return boolean true if success, exception in case of any problems
2302
 */
2303
function update_capabilities($component = 'moodle') {
2304
    global $DB, $OUTPUT;
2305
 
2306
    // Allow temporary caches to be used during install, dramatically boosting performance.
2307
    $token = new \core_cache\allow_temporary_caches();
2308
 
2309
    $storedcaps = array();
2310
 
2311
    $filecaps = load_capability_def($component);
2312
    foreach ($filecaps as $capname=>$unused) {
2313
        if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
2314
            debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
2315
        }
2316
    }
2317
 
2318
    // It is possible somebody directly modified the DB (according to accesslib_test anyway).
2319
    // So ensure our updating is based on fresh data.
2320
    cache::make('core', 'capabilities')->delete('core_capabilities');
2321
 
2322
    $cachedcaps = get_cached_capabilities($component);
2323
    if ($cachedcaps) {
2324
        foreach ($cachedcaps as $cachedcap) {
2325
            array_push($storedcaps, $cachedcap->name);
2326
            // update risk bitmasks and context levels in existing capabilities if needed
2327
            if (array_key_exists($cachedcap->name, $filecaps)) {
2328
                if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
2329
                    $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
2330
                }
2331
                if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
2332
                    $updatecap = new stdClass();
2333
                    $updatecap->id = $cachedcap->id;
2334
                    $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
2335
                    $DB->update_record('capabilities', $updatecap);
2336
                }
2337
                if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
2338
                    $updatecap = new stdClass();
2339
                    $updatecap->id = $cachedcap->id;
2340
                    $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
2341
                    $DB->update_record('capabilities', $updatecap);
2342
                }
2343
 
2344
                if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
2345
                    $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
2346
                }
2347
                if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
2348
                    $updatecap = new stdClass();
2349
                    $updatecap->id = $cachedcap->id;
2350
                    $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
2351
                    $DB->update_record('capabilities', $updatecap);
2352
                }
2353
            }
2354
        }
2355
    }
2356
 
2357
    // Flush the cached again, as we have changed DB.
2358
    cache::make('core', 'capabilities')->delete('core_capabilities');
2359
 
2360
    // Are there new capabilities in the file definition?
2361
    $newcaps = array();
2362
 
2363
    foreach ($filecaps as $filecap => $def) {
2364
        if (!$storedcaps ||
2365
                ($storedcaps && in_array($filecap, $storedcaps) === false)) {
2366
            if (!array_key_exists('riskbitmask', $def)) {
2367
                $def['riskbitmask'] = 0; // no risk if not specified
2368
            }
2369
            $newcaps[$filecap] = $def;
2370
        }
2371
    }
2372
    // Add new capabilities to the stored definition.
2373
    $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
2374
    $capabilityobjects = [];
2375
    foreach ($newcaps as $capname => $capdef) {
2376
        $capability = new stdClass();
2377
        $capability->name         = $capname;
2378
        $capability->captype      = $capdef['captype'];
2379
        $capability->contextlevel = $capdef['contextlevel'];
2380
        $capability->component    = $component;
2381
        $capability->riskbitmask  = $capdef['riskbitmask'];
2382
        $capabilityobjects[] = $capability;
2383
    }
2384
    $DB->insert_records('capabilities', $capabilityobjects);
2385
 
2386
    // Flush the cache, as we have changed DB.
2387
    cache::make('core', 'capabilities')->delete('core_capabilities');
2388
 
2389
    foreach ($newcaps as $capname => $capdef) {
2390
        if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
2391
            if ($rolecapabilities = $DB->get_records_sql('
2392
                    SELECT rc.*,
2393
                           CASE WHEN EXISTS(SELECT 1
2394
                                    FROM {role_capabilities} rc2
2395
                                   WHERE rc2.capability = ?
2396
                                         AND rc2.contextid = rc.contextid
2397
                                         AND rc2.roleid = rc.roleid) THEN 1 ELSE 0 END AS entryexists,
2398
                            ' . context_helper::get_preload_record_columns_sql('x') .'
2399
                      FROM {role_capabilities} rc
2400
                      JOIN {context} x ON x.id = rc.contextid
2401
                     WHERE rc.capability = ?',
2402
                    [$capname, $capdef['clonepermissionsfrom']])) {
2403
                foreach ($rolecapabilities as $rolecapability) {
2404
                    // Preload the context and add performance hints based on the SQL query above.
2405
                    context_helper::preload_from_record($rolecapability);
2406
                    $performancehints = [ACCESSLIB_HINT_CONTEXT_EXISTS];
2407
                    if (!$rolecapability->entryexists) {
2408
                        $performancehints[] = ACCESSLIB_HINT_NO_EXISTING;
2409
                    }
2410
                    //assign_capability will update rather than insert if capability exists
2411
                    if (!assign_capability($capname, $rolecapability->permission,
2412
                            $rolecapability->roleid, $rolecapability->contextid, true, $performancehints)) {
2413
                         echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
2414
                    }
2415
                }
2416
            }
2417
        // we ignore archetype key if we have cloned permissions
2418
        } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
2419
            assign_legacy_capabilities($capname, $capdef['archetypes']);
2420
        // 'legacy' is for backward compatibility with 1.9 access.php
2421
        } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
2422
            assign_legacy_capabilities($capname, $capdef['legacy']);
2423
        }
2424
    }
2425
    // Are there any capabilities that have been removed from the file
2426
    // definition that we need to delete from the stored capabilities and
2427
    // role assignments?
2428
    capabilities_cleanup($component, $filecaps);
2429
 
2430
    // reset static caches
2431
    accesslib_reset_role_cache();
2432
 
2433
    // Flush the cached again, as we have changed DB.
2434
    cache::make('core', 'capabilities')->delete('core_capabilities');
2435
 
2436
    return true;
2437
}
2438
 
2439
/**
2440
 * Deletes cached capabilities that are no longer needed by the component.
2441
 * Also unassigns these capabilities from any roles that have them.
2442
 * NOTE: this function is called from lib/db/upgrade.php
2443
 *
2444
 * @access private
2445
 * @param string $component examples: 'moodle', 'mod_forum', 'block_activity_results'
2446
 * @param array $newcapdef array of the new capability definitions that will be
2447
 *                     compared with the cached capabilities
2448
 * @return int number of deprecated capabilities that have been removed
2449
 */
2450
function capabilities_cleanup($component, $newcapdef = null) {
2451
    global $DB;
2452
 
2453
    $removedcount = 0;
2454
 
2455
    if ($cachedcaps = get_cached_capabilities($component)) {
2456
        foreach ($cachedcaps as $cachedcap) {
2457
            if (empty($newcapdef) ||
2458
                        array_key_exists($cachedcap->name, $newcapdef) === false) {
2459
 
2460
                // Delete from roles.
2461
                if ($roles = get_roles_with_capability($cachedcap->name)) {
2462
                    foreach ($roles as $role) {
2463
                        if (!unassign_capability($cachedcap->name, $role->id)) {
2464
                            throw new \moodle_exception('cannotunassigncap', 'error', '',
2465
                                (object)array('cap' => $cachedcap->name, 'role' => $role->name));
2466
                        }
2467
                    }
2468
                }
2469
 
2470
                // Remove from role_capabilities for any old ones.
2471
                $DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
2472
 
2473
                // Remove from capabilities cache.
2474
                $DB->delete_records('capabilities', array('name' => $cachedcap->name));
2475
                $removedcount++;
2476
            } // End if.
2477
        }
2478
    }
2479
    if ($removedcount) {
2480
        cache::make('core', 'capabilities')->delete('core_capabilities');
2481
    }
2482
    return $removedcount;
2483
}
2484
 
2485
/**
2486
 * Returns an array of all the known types of risk
2487
 * The array keys can be used, for example as CSS class names, or in calls to
2488
 * print_risk_icon. The values are the corresponding RISK_ constants.
2489
 *
2490
 * @return array all the known types of risk.
2491
 */
2492
function get_all_risks() {
2493
    return array(
2494
        'riskmanagetrust' => RISK_MANAGETRUST,
2495
        'riskconfig'      => RISK_CONFIG,
2496
        'riskxss'         => RISK_XSS,
2497
        'riskpersonal'    => RISK_PERSONAL,
2498
        'riskspam'        => RISK_SPAM,
2499
        'riskdataloss'    => RISK_DATALOSS,
2500
    );
2501
}
2502
 
2503
/**
2504
 * Return a link to moodle docs for a given capability name
2505
 *
2506
 * @param stdClass $capability a capability - a row from the mdl_capabilities table.
2507
 * @return string the human-readable capability name as a link to Moodle Docs.
2508
 */
2509
function get_capability_docs_link($capability) {
2510
    $url = get_docs_url('Capabilities/' . $capability->name);
2511
    return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
2512
}
2513
 
2514
/**
2515
 * This function pulls out all the resolved capabilities (overrides and
2516
 * defaults) of a role used in capability overrides in contexts at a given
2517
 * context.
2518
 *
2519
 * @param int $roleid
2520
 * @param context $context
2521
 * @param string $cap capability, optional, defaults to ''
2522
 * @return array Array of capabilities
2523
 */
2524
function role_context_capabilities($roleid, context $context, $cap = '') {
2525
    global $DB;
2526
 
2527
    $contexts = $context->get_parent_context_ids(true);
2528
    $contexts = '('.implode(',', $contexts).')';
2529
 
2530
    $params = array($roleid);
2531
 
2532
    if ($cap) {
2533
        $search = " AND rc.capability = ? ";
2534
        $params[] = $cap;
2535
    } else {
2536
        $search = '';
2537
    }
2538
 
2539
    $sql = "SELECT rc.*
2540
              FROM {role_capabilities} rc
2541
              JOIN {context} c ON rc.contextid = c.id
2542
              JOIN {capabilities} cap ON rc.capability = cap.name
2543
             WHERE rc.contextid in $contexts
2544
                   AND rc.roleid = ?
2545
                   $search
2546
          ORDER BY c.contextlevel DESC, rc.capability DESC";
2547
 
2548
    $capabilities = array();
2549
 
2550
    if ($records = $DB->get_records_sql($sql, $params)) {
2551
        // We are traversing via reverse order.
2552
        foreach ($records as $record) {
2553
            // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
2554
            if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
2555
                $capabilities[$record->capability] = $record->permission;
2556
            }
2557
        }
2558
    }
2559
    return $capabilities;
2560
}
2561
 
2562
/**
2563
 * Constructs array with contextids as first parameter and context paths,
2564
 * in both cases bottom top including self.
2565
 *
2566
 * @access private
2567
 * @param context $context
2568
 * @return array
2569
 */
2570
function get_context_info_list(context $context) {
2571
    $contextids = explode('/', ltrim($context->path, '/'));
2572
    $contextpaths = array();
2573
    $contextids2 = $contextids;
2574
    while ($contextids2) {
2575
        $contextpaths[] = '/' . implode('/', $contextids2);
2576
        array_pop($contextids2);
2577
    }
2578
    return array($contextids, $contextpaths);
2579
}
2580
 
2581
/**
2582
 * Check if context is the front page context or a context inside it
2583
 *
2584
 * Returns true if this context is the front page context, or a context inside it,
2585
 * otherwise false.
2586
 *
2587
 * @param context $context a context object.
2588
 * @return bool
2589
 */
2590
function is_inside_frontpage(context $context) {
2591
    $frontpagecontext = context_course::instance(SITEID);
2592
    return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
2593
}
2594
 
2595
/**
2596
 * Returns capability information (cached)
2597
 *
2598
 * @param string $capabilityname
2599
 * @return ?stdClass object or null if capability not found
2600
 */
2601
function get_capability_info($capabilityname) {
2602
    $caps = get_all_capabilities();
2603
 
2604
    // Check for deprecated capability.
2605
    if ($deprecatedinfo = get_deprecated_capability_info($capabilityname)) {
2606
        if (!empty($deprecatedinfo['replacement'])) {
2607
            // Let's try again with this capability if it exists.
2608
            if (isset($caps[$deprecatedinfo['replacement']])) {
2609
                $capabilityname = $deprecatedinfo['replacement'];
2610
            } else {
2611
                debugging("Capability '{$capabilityname}' was supposed to be replaced with ".
2612
                    "'{$deprecatedinfo['replacement']}', which does not exist !");
2613
            }
2614
        }
2615
        $fullmessage = $deprecatedinfo['fullmessage'];
2616
        debugging($fullmessage, DEBUG_DEVELOPER);
2617
    }
2618
    if (!isset($caps[$capabilityname])) {
2619
        return null;
2620
    }
2621
 
2622
    return (object) $caps[$capabilityname];
2623
}
2624
 
2625
/**
2626
 * Returns deprecation info for this particular capabilty (cached)
2627
 *
2628
 * Do not use this function except in the get_capability_info
2629
 *
2630
 * @param string $capabilityname
2631
 * @return array|null with deprecation message and potential replacement if not null
2632
 */
2633
function get_deprecated_capability_info($capabilityname) {
2634
    $cache = cache::make('core', 'capabilities');
2635
    $alldeprecatedcaps = $cache->get('deprecated_capabilities');
2636
    if ($alldeprecatedcaps === false) {
2637
        // Look for deprecated capabilities in each component.
2638
        $allcaps = get_all_capabilities();
2639
        $components = [];
2640
        $alldeprecatedcaps = [];
2641
        foreach ($allcaps as $cap) {
2642
            if (!in_array($cap['component'], $components)) {
2643
                $components[] = $cap['component'];
2644
                $defpath = core_component::get_component_directory($cap['component']).'/db/access.php';
2645
                if (file_exists($defpath)) {
2646
                    $deprecatedcapabilities = [];
2647
                    require($defpath);
2648
                    if (!empty($deprecatedcapabilities)) {
2649
                        foreach ($deprecatedcapabilities as $cname => $cdef) {
2650
                            $alldeprecatedcaps[$cname] = $cdef;
2651
                        }
2652
                    }
2653
                }
2654
            }
2655
        }
2656
        $cache->set('deprecated_capabilities', $alldeprecatedcaps);
2657
    }
2658
 
2659
    if (!isset($alldeprecatedcaps[$capabilityname])) {
2660
        return null;
2661
    }
2662
    $deprecatedinfo = $alldeprecatedcaps[$capabilityname];
2663
    $deprecatedinfo['fullmessage'] = "The capability '{$capabilityname}' is deprecated.";
2664
    if (!empty($deprecatedinfo['message'])) {
2665
        $deprecatedinfo['fullmessage'] .= $deprecatedinfo['message'];
2666
    }
2667
    if (!empty($deprecatedinfo['replacement'])) {
2668
        $deprecatedinfo['fullmessage'] .=
2669
            "It will be replaced by '{$deprecatedinfo['replacement']}'.";
2670
    }
2671
    return $deprecatedinfo;
2672
}
2673
 
2674
/**
2675
 * Returns all capabilitiy records, preferably from MUC and not database.
2676
 *
2677
 * @return array All capability records indexed by capability name
2678
 */
2679
function get_all_capabilities() {
2680
    global $DB;
2681
    $cache = cache::make('core', 'capabilities');
2682
    if (!$allcaps = $cache->get('core_capabilities')) {
2683
        $rs = $DB->get_recordset('capabilities');
2684
        $allcaps = array();
2685
        foreach ($rs as $capability) {
2686
            $capability->riskbitmask = (int) $capability->riskbitmask;
2687
            $allcaps[$capability->name] = (array) $capability;
2688
        }
2689
        $rs->close();
2690
        $cache->set('core_capabilities', $allcaps);
2691
    }
2692
    return $allcaps;
2693
}
2694
 
2695
/**
2696
 * Returns the human-readable, translated version of the capability.
2697
 * Basically a big switch statement.
2698
 *
2699
 * @param string $capabilityname e.g. mod/choice:readresponses
2700
 * @return string
2701
 */
2702
function get_capability_string($capabilityname) {
2703
 
2704
    // Typical capability name is 'plugintype/pluginname:capabilityname'
2705
    list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
2706
 
2707
    if ($type === 'moodle') {
2708
        $component = 'core_role';
2709
    } else if ($type === 'quizreport') {
2710
        //ugly hack!!
2711
        $component = 'quiz_'.$name;
2712
    } else {
2713
        $component = $type.'_'.$name;
2714
    }
2715
 
2716
    $stringname = $name.':'.$capname;
2717
 
2718
    if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
2719
        return get_string($stringname, $component);
2720
    }
2721
 
2722
    $dir = core_component::get_component_directory($component);
2723
    if (!isset($dir) || !file_exists($dir)) {
2724
        // plugin broken or does not exist, do not bother with printing of debug message
2725
        return $capabilityname.' ???';
2726
    }
2727
 
2728
    // something is wrong in plugin, better print debug
2729
    return get_string($stringname, $component);
2730
}
2731
 
2732
/**
2733
 * This gets the mod/block/course/core etc strings.
2734
 *
2735
 * @param string $component
2736
 * @param int $contextlevel
2737
 * @return string|bool String is success, false if failed
2738
 */
2739
function get_component_string($component, $contextlevel) {
2740
 
2741
    if ($component === 'moodle' || $component === 'core') {
2742
        return context_helper::get_level_name($contextlevel);
2743
    }
2744
 
2745
    list($type, $name) = core_component::normalize_component($component);
2746
    $dir = core_component::get_plugin_directory($type, $name);
2747
    if (!isset($dir) || !file_exists($dir)) {
2748
        // plugin not installed, bad luck, there is no way to find the name
2749
        return $component . ' ???';
2750
    }
2751
 
2752
    // Some plugin types need an extra prefix to make the name easy to understand.
2753
    switch ($type) {
2754
        case 'quiz':
2755
            $prefix = get_string('quizreport', 'quiz') . ': ';
2756
            break;
2757
        case 'repository':
2758
            $prefix = get_string('repository', 'repository') . ': ';
2759
            break;
2760
        case 'gradeimport':
2761
            $prefix = get_string('gradeimport', 'grades') . ': ';
2762
            break;
2763
        case 'gradeexport':
2764
            $prefix = get_string('gradeexport', 'grades') . ': ';
2765
            break;
2766
        case 'gradereport':
2767
            $prefix = get_string('gradereport', 'grades') . ': ';
2768
            break;
2769
        case 'webservice':
2770
            $prefix = get_string('webservice', 'webservice') . ': ';
2771
            break;
2772
        case 'block':
2773
            $prefix = get_string('block') . ': ';
2774
            break;
2775
        case 'mod':
2776
            $prefix = get_string('activity') . ': ';
2777
            break;
2778
 
2779
        // Default case, just use the plugin name.
2780
        default:
2781
            $prefix = '';
2782
    }
2783
    return $prefix . get_string('pluginname', $component);
2784
}
2785
 
2786
/**
2787
 * Gets the list of roles assigned to this context and up (parents)
2788
 * from the aggregation of:
2789
 * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
2790
 * b) if applicable, those roles that are assigned in the context.
2791
 *
2792
 * @param context $context
2793
 * @return array
2794
 */
2795
function get_profile_roles(context $context) {
2796
    global $CFG, $DB;
2797
    // If the current user can assign roles, then they can see all roles on the profile and participants page,
2798
    // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2799
    if (has_capability('moodle/role:assign', $context)) {
2800
        $rolesinscope = array_keys(get_all_roles($context));
2801
    } else {
2802
        $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2803
    }
2804
 
2805
    if (empty($rolesinscope)) {
2806
        return [];
2807
    }
2808
 
2809
    list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2810
    list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2811
    $params = array_merge($params, $cparams);
2812
 
2813
    if ($coursecontext = $context->get_course_context(false)) {
2814
        $params['coursecontext'] = $coursecontext->id;
2815
    } else {
2816
        $params['coursecontext'] = 0;
2817
    }
2818
 
2819
    $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2820
              FROM {role_assignments} ra, {role} r
2821
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2822
             WHERE r.id = ra.roleid
2823
                   AND ra.contextid $contextlist
2824
                   AND r.id $rallowed
2825
          ORDER BY r.sortorder ASC";
2826
 
2827
    return $DB->get_records_sql($sql, $params);
2828
}
2829
 
2830
/**
2831
 * Gets the list of roles assigned to this context and up (parents)
2832
 *
2833
 * @param context $context
2834
 * @param boolean $includeparents, false means without parents.
2835
 * @return array
2836
 */
2837
function get_roles_used_in_context(context $context, $includeparents = true) {
2838
    global $DB;
2839
 
2840
    if ($includeparents === true) {
2841
        list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
2842
    } else {
2843
        list($contextlist, $params) = $DB->get_in_or_equal($context->id, SQL_PARAMS_NAMED, 'cl');
2844
    }
2845
 
2846
    if ($coursecontext = $context->get_course_context(false)) {
2847
        $params['coursecontext'] = $coursecontext->id;
2848
    } else {
2849
        $params['coursecontext'] = 0;
2850
    }
2851
 
2852
    $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2853
              FROM {role_assignments} ra, {role} r
2854
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2855
             WHERE r.id = ra.roleid
2856
                   AND ra.contextid $contextlist
2857
          ORDER BY r.sortorder ASC";
2858
 
2859
    return $DB->get_records_sql($sql, $params);
2860
}
2861
 
2862
/**
2863
 * This function is used to print roles column in user profile page.
2864
 * It is using the CFG->profileroles to limit the list to only interesting roles.
2865
 * (The permission tab has full details of user role assignments.)
2866
 *
2867
 * @param int $userid
2868
 * @param int $courseid
2869
 * @return string
2870
 */
2871
function get_user_roles_in_course($userid, $courseid) {
2872
    global $CFG, $DB;
2873
    if ($courseid == SITEID) {
2874
        $context = context_system::instance();
2875
    } else {
2876
        $context = context_course::instance($courseid);
2877
    }
2878
    // If the current user can assign roles, then they can see all roles on the profile and participants page,
2879
    // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
2880
    if (has_capability('moodle/role:assign', $context)) {
2881
        $rolesinscope = array_keys(get_all_roles($context));
2882
    } else {
2883
        $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
2884
    }
2885
    if (empty($rolesinscope)) {
2886
        return '';
2887
    }
2888
 
2889
    list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
2890
    list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
2891
    $params = array_merge($params, $cparams);
2892
 
2893
    if ($coursecontext = $context->get_course_context(false)) {
2894
        $params['coursecontext'] = $coursecontext->id;
2895
    } else {
2896
        $params['coursecontext'] = 0;
2897
    }
2898
 
2899
    $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
2900
              FROM {role_assignments} ra, {role} r
2901
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2902
             WHERE r.id = ra.roleid
2903
                   AND ra.contextid $contextlist
2904
                   AND r.id $rallowed
2905
                   AND ra.userid = :userid
2906
          ORDER BY r.sortorder ASC";
2907
    $params['userid'] = $userid;
2908
 
2909
    $rolestring = '';
2910
 
2911
    if ($roles = $DB->get_records_sql($sql, $params)) {
2912
        $viewableroles = get_viewable_roles($context, $userid);
2913
 
2914
        $rolenames = array();
2915
        foreach ($roles as $roleid => $unused) {
2916
            if (isset($viewableroles[$roleid])) {
2917
                $url = new moodle_url('/user/index.php', ['contextid' => $context->id, 'roleid' => $roleid]);
2918
                $rolenames[] = '<a href="' . $url . '">' . $viewableroles[$roleid] . '</a>';
2919
            }
2920
        }
2921
        $rolestring = implode(', ', $rolenames);
2922
    }
2923
 
2924
    return $rolestring;
2925
}
2926
 
2927
/**
2928
 * Checks if a user can assign users to a particular role in this context
2929
 *
2930
 * @param context $context
2931
 * @param int $targetroleid - the id of the role you want to assign users to
2932
 * @return boolean
2933
 */
2934
function user_can_assign(context $context, $targetroleid) {
2935
    global $DB;
2936
 
2937
    // First check to see if the user is a site administrator.
2938
    if (is_siteadmin()) {
2939
        return true;
2940
    }
2941
 
2942
    // Check if user has override capability.
2943
    // If not return false.
2944
    if (!has_capability('moodle/role:assign', $context)) {
2945
        return false;
2946
    }
2947
    // pull out all active roles of this user from this context(or above)
2948
    if ($userroles = get_user_roles($context)) {
2949
        foreach ($userroles as $userrole) {
2950
            // if any in the role_allow_override table, then it's ok
2951
            if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
2952
                return true;
2953
            }
2954
        }
2955
    }
2956
 
2957
    return false;
2958
}
2959
 
2960
/**
2961
 * Returns all site roles in correct sort order.
2962
 *
2963
 * Note: this method does not localise role names or descriptions,
2964
 *       use role_get_names() if you need role names.
2965
 *
2966
 * @param context $context optional context for course role name aliases
2967
 * @return array of role records with optional coursealias property
2968
 */
2969
function get_all_roles(context $context = null) {
2970
    global $DB;
2971
 
2972
    if (!$context or !$coursecontext = $context->get_course_context(false)) {
2973
        $coursecontext = null;
2974
    }
2975
 
2976
    if ($coursecontext) {
2977
        $sql = "SELECT r.*, rn.name AS coursealias
2978
                  FROM {role} r
2979
             LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
2980
              ORDER BY r.sortorder ASC";
2981
        return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
2982
 
2983
    } else {
2984
        return $DB->get_records('role', array(), 'sortorder ASC');
2985
    }
2986
}
2987
 
2988
/**
2989
 * Returns roles of a specified archetype
2990
 *
2991
 * @param string $archetype
2992
 * @return array of full role records
2993
 */
2994
function get_archetype_roles($archetype) {
2995
    global $DB;
2996
    return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
2997
}
2998
 
2999
/**
3000
 * Gets all the user roles assigned in this context, or higher contexts for a list of users.
3001
 *
3002
 * If you try using the combination $userids = [], $checkparentcontexts = true then this is likely
3003
 * to cause an out-of-memory error on large Moodle sites, so this combination is deprecated and
3004
 * outputs a warning, even though it is the default.
3005
 *
3006
 * @param context $context
3007
 * @param array $userids. An empty list means fetch all role assignments for the context.
3008
 * @param bool $checkparentcontexts defaults to true
3009
 * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
3010
 * @return array
3011
 */
3012
function get_users_roles(context $context, $userids = [], $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
3013
    global $DB;
3014
 
3015
    if (!$userids && $checkparentcontexts) {
3016
        debugging('Please do not call get_users_roles() with $checkparentcontexts = true ' .
3017
                'and $userids array not set. This combination causes large Moodle sites ' .
3018
                'with lots of site-wide role assignemnts to run out of memory.', DEBUG_DEVELOPER);
3019
    }
3020
 
3021
    if ($checkparentcontexts) {
3022
        $contextids = $context->get_parent_context_ids();
3023
    } else {
3024
        $contextids = array();
3025
    }
3026
    $contextids[] = $context->id;
3027
 
3028
    list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
3029
 
3030
    // If userids was passed as an empty array, we fetch all role assignments for the course.
3031
    if (empty($userids)) {
3032
        $useridlist = ' IS NOT NULL ';
3033
        $uparams = [];
3034
    } else {
3035
        list($useridlist, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uids');
3036
    }
3037
 
3038
    $sql = "SELECT ra.*, r.name, r.shortname, ra.userid
3039
              FROM {role_assignments} ra, {role} r, {context} c
3040
             WHERE ra.userid $useridlist
3041
                   AND ra.roleid = r.id
3042
                   AND ra.contextid = c.id
3043
                   AND ra.contextid $contextids
3044
          ORDER BY $order";
3045
 
3046
    $all = $DB->get_records_sql($sql , array_merge($params, $uparams));
3047
 
3048
    // Return results grouped by userid.
3049
    $result = [];
3050
    foreach ($all as $id => $record) {
3051
        if (!isset($result[$record->userid])) {
3052
            $result[$record->userid] = [];
3053
        }
3054
        $result[$record->userid][$record->id] = $record;
3055
    }
3056
 
3057
    // Make sure all requested users are included in the result, even if they had no role assignments.
3058
    foreach ($userids as $id) {
3059
        if (!isset($result[$id])) {
3060
            $result[$id] = [];
3061
        }
3062
    }
3063
 
3064
    return $result;
3065
}
3066
 
3067
 
3068
/**
3069
 * Gets all the user roles assigned in this context, or higher contexts
3070
 * this is mainly used when checking if a user can assign a role, or overriding a role
3071
 * i.e. we need to know what this user holds, in order to verify against allow_assign and
3072
 * allow_override tables
3073
 *
3074
 * @param context $context
3075
 * @param int $userid
3076
 * @param bool $checkparentcontexts defaults to true
3077
 * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
3078
 * @return array
3079
 */
3080
function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
3081
    global $USER, $DB;
3082
 
3083
    if (empty($userid)) {
3084
        if (empty($USER->id)) {
3085
            return array();
3086
        }
3087
        $userid = $USER->id;
3088
    }
3089
 
3090
    if ($checkparentcontexts) {
3091
        $contextids = $context->get_parent_context_ids();
3092
    } else {
3093
        $contextids = array();
3094
    }
3095
    $contextids[] = $context->id;
3096
 
3097
    list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
3098
 
3099
    array_unshift($params, $userid);
3100
 
3101
    $sql = "SELECT ra.*, r.name, r.shortname
3102
              FROM {role_assignments} ra, {role} r, {context} c
3103
             WHERE ra.userid = ?
3104
                   AND ra.roleid = r.id
3105
                   AND ra.contextid = c.id
3106
                   AND ra.contextid $contextids
3107
          ORDER BY $order";
3108
 
3109
    return $DB->get_records_sql($sql ,$params);
3110
}
3111
 
3112
/**
3113
 * Like get_user_roles, but adds in the authenticated user role, and the front
3114
 * page roles, if applicable.
3115
 *
3116
 * @param context $context the context.
3117
 * @param int $userid optional. Defaults to $USER->id
3118
 * @return array of objects with fields ->userid, ->contextid and ->roleid.
3119
 */
3120
function get_user_roles_with_special(context $context, $userid = 0) {
3121
    global $CFG, $USER;
3122
 
3123
    if (empty($userid)) {
3124
        if (empty($USER->id)) {
3125
            return array();
3126
        }
3127
        $userid = $USER->id;
3128
    }
3129
 
3130
    $ras = get_user_roles($context, $userid);
3131
 
3132
    // Add front-page role if relevant.
3133
    $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3134
    $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
3135
            is_inside_frontpage($context);
3136
    if ($defaultfrontpageroleid && $isfrontpage) {
3137
        $frontpagecontext = context_course::instance(SITEID);
3138
        $ra = new stdClass();
3139
        $ra->userid = $userid;
3140
        $ra->contextid = $frontpagecontext->id;
3141
        $ra->roleid = $defaultfrontpageroleid;
3142
        $ras[] = $ra;
3143
    }
3144
 
3145
    // Add authenticated user role if relevant.
3146
    $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3147
    if ($defaultuserroleid && !isguestuser($userid)) {
3148
        $systemcontext = context_system::instance();
3149
        $ra = new stdClass();
3150
        $ra->userid = $userid;
3151
        $ra->contextid = $systemcontext->id;
3152
        $ra->roleid = $defaultuserroleid;
3153
        $ras[] = $ra;
3154
    }
3155
 
3156
    return $ras;
3157
}
3158
 
3159
/**
3160
 * Creates a record in the role_allow_override table
3161
 *
3162
 * @param int $fromroleid source roleid
3163
 * @param int $targetroleid target roleid
3164
 * @return void
3165
 */
3166
function core_role_set_override_allowed($fromroleid, $targetroleid) {
3167
    global $DB;
3168
 
3169
    $record = new stdClass();
3170
    $record->roleid        = $fromroleid;
3171
    $record->allowoverride = $targetroleid;
3172
    $DB->insert_record('role_allow_override', $record);
3173
}
3174
 
3175
/**
3176
 * Creates a record in the role_allow_assign table
3177
 *
3178
 * @param int $fromroleid source roleid
3179
 * @param int $targetroleid target roleid
3180
 * @return void
3181
 */
3182
function core_role_set_assign_allowed($fromroleid, $targetroleid) {
3183
    global $DB;
3184
 
3185
    $record = new stdClass();
3186
    $record->roleid      = $fromroleid;
3187
    $record->allowassign = $targetroleid;
3188
    $DB->insert_record('role_allow_assign', $record);
3189
}
3190
 
3191
/**
3192
 * Creates a record in the role_allow_switch table
3193
 *
3194
 * @param int $fromroleid source roleid
3195
 * @param int $targetroleid target roleid
3196
 * @return void
3197
 */
3198
function core_role_set_switch_allowed($fromroleid, $targetroleid) {
3199
    global $DB;
3200
 
3201
    $record = new stdClass();
3202
    $record->roleid      = $fromroleid;
3203
    $record->allowswitch = $targetroleid;
3204
    $DB->insert_record('role_allow_switch', $record);
3205
}
3206
 
3207
/**
3208
 * Creates a record in the role_allow_view table
3209
 *
3210
 * @param int $fromroleid source roleid
3211
 * @param int $targetroleid target roleid
3212
 * @return void
3213
 */
3214
function core_role_set_view_allowed($fromroleid, $targetroleid) {
3215
    global $DB;
3216
 
3217
    $record = new stdClass();
3218
    $record->roleid      = $fromroleid;
3219
    $record->allowview = $targetroleid;
3220
    $DB->insert_record('role_allow_view', $record);
3221
}
3222
 
3223
/**
3224
 * Gets a list of roles that this user can assign in this context
3225
 *
3226
 * @param context $context the context.
3227
 * @param int $rolenamedisplay the type of role name to display. One of the
3228
 *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3229
 * @param bool $withusercounts if true, count the number of users with each role.
3230
 * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
3231
 * @return array if $withusercounts is false, then an array $roleid => $rolename.
3232
 *      if $withusercounts is true, returns a list of three arrays,
3233
 *      $rolenames, $rolecounts, and $nameswithcounts.
3234
 */
3235
function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
3236
    global $USER, $DB;
3237
 
3238
    // make sure there is a real user specified
3239
    if ($user === null) {
3240
        $userid = isset($USER->id) ? $USER->id : 0;
3241
    } else {
3242
        $userid = is_object($user) ? $user->id : $user;
3243
    }
3244
 
3245
    if (!has_capability('moodle/role:assign', $context, $userid)) {
3246
        if ($withusercounts) {
3247
            return array(array(), array(), array());
3248
        } else {
3249
            return array();
3250
        }
3251
    }
3252
 
3253
    $params = array();
3254
    $extrafields = '';
3255
 
3256
    if ($withusercounts) {
3257
        $extrafields = ', (SELECT COUNT(DISTINCT u.id)
3258
                             FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
3259
                            WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
3260
                          ) AS usercount';
3261
        $params['conid'] = $context->id;
3262
    }
3263
 
3264
    if (is_siteadmin($userid)) {
3265
        // show all roles allowed in this context to admins
3266
        $assignrestriction = "";
3267
    } else {
3268
        $parents = $context->get_parent_context_ids(true);
3269
        $contexts = implode(',' , $parents);
3270
        $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
3271
                                      FROM {role_allow_assign} raa
3272
                                      JOIN {role_assignments} ra ON ra.roleid = raa.roleid
3273
                                     WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3274
                                   ) ar ON ar.id = r.id";
3275
        $params['userid'] = $userid;
3276
    }
3277
    $params['contextlevel'] = $context->contextlevel;
3278
 
3279
    if ($coursecontext = $context->get_course_context(false)) {
3280
        $params['coursecontext'] = $coursecontext->id;
3281
    } else {
3282
        $params['coursecontext'] = 0; // no course aliases
3283
        $coursecontext = null;
3284
    }
3285
    $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
3286
              FROM {role} r
3287
              $assignrestriction
3288
              JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
3289
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3290
          ORDER BY r.sortorder ASC";
3291
    $roles = $DB->get_records_sql($sql, $params);
3292
 
3293
    $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
3294
 
3295
    if (!$withusercounts) {
3296
        return $rolenames;
3297
    }
3298
 
3299
    $rolecounts = array();
3300
    $nameswithcounts = array();
3301
    foreach ($roles as $role) {
3302
        $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
3303
        $rolecounts[$role->id] = $roles[$role->id]->usercount;
3304
    }
3305
    return array($rolenames, $rolecounts, $nameswithcounts);
3306
}
3307
 
3308
/**
3309
 * Gets a list of roles that this user can switch to in a context
3310
 *
3311
 * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
3312
 * This function just process the contents of the role_allow_switch table. You also need to
3313
 * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
3314
 *
3315
 * @param context $context a context.
3316
 * @param int $rolenamedisplay the type of role name to display. One of the
3317
 *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3318
 * @return array an array $roleid => $rolename.
3319
 */
3320
function get_switchable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS) {
3321
    global $USER, $DB;
3322
 
3323
    // You can't switch roles without this capability.
3324
    if (!has_capability('moodle/role:switchroles', $context)) {
3325
        return [];
3326
    }
3327
 
3328
    $params = array();
3329
    $extrajoins = '';
3330
    $extrawhere = '';
3331
    if (!is_siteadmin()) {
3332
        // Admins are allowed to switch to any role with.
3333
        // Others are subject to the additional constraint that the switch-to role must be allowed by
3334
        // 'role_allow_switch' for some role they have assigned in this context or any parent.
3335
        $parents = $context->get_parent_context_ids(true);
3336
        $contexts = implode(',' , $parents);
3337
 
3338
        $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
3339
        JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3340
        $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
3341
        $params['userid'] = $USER->id;
3342
    }
3343
 
3344
    if ($coursecontext = $context->get_course_context(false)) {
3345
        $params['coursecontext'] = $coursecontext->id;
3346
    } else {
3347
        $params['coursecontext'] = 0; // no course aliases
3348
        $coursecontext = null;
3349
    }
3350
 
3351
    $query = "
3352
        SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3353
          FROM (SELECT DISTINCT rc.roleid
3354
                  FROM {role_capabilities} rc
3355
 
3356
                  $extrajoins
3357
                  $extrawhere) idlist
3358
          JOIN {role} r ON r.id = idlist.roleid
3359
     LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3360
      ORDER BY r.sortorder";
3361
    $roles = $DB->get_records_sql($query, $params);
3362
 
3363
    return role_fix_names($roles, $context, $rolenamedisplay, true);
3364
}
3365
 
3366
/**
3367
 * Gets a list of roles that this user can view in a context
3368
 *
3369
 * @param context $context a context.
3370
 * @param int $userid id of user.
3371
 * @param int $rolenamedisplay the type of role name to display. One of the
3372
 *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3373
 * @return array an array $roleid => $rolename.
3374
 */
3375
function get_viewable_roles(context $context, $userid = null, $rolenamedisplay = ROLENAME_ALIAS) {
3376
    global $USER, $DB;
3377
 
3378
    if ($userid == null) {
3379
        $userid = $USER->id;
3380
    }
3381
 
3382
    $params = array();
3383
    $extrajoins = '';
3384
    $extrawhere = '';
3385
    if (!is_siteadmin()) {
3386
        // Admins are allowed to view any role.
3387
        // Others are subject to the additional constraint that the view role must be allowed by
3388
        // 'role_allow_view' for some role they have assigned in this context or any parent.
3389
        $contexts = $context->get_parent_context_ids(true);
3390
        list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
3391
 
3392
        $extrajoins = "JOIN {role_allow_view} ras ON ras.allowview = r.id
3393
                       JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
3394
        $extrawhere = "WHERE ra.userid = :userid AND ra.contextid $insql";
3395
 
3396
        $params += $inparams;
3397
        $params['userid'] = $userid;
3398
    }
3399
 
3400
    if ($coursecontext = $context->get_course_context(false)) {
3401
        $params['coursecontext'] = $coursecontext->id;
3402
    } else {
3403
        $params['coursecontext'] = 0; // No course aliases.
3404
        $coursecontext = null;
3405
    }
3406
 
3407
    $query = "
3408
        SELECT r.id, r.name, r.shortname, rn.name AS coursealias, r.sortorder
3409
          FROM {role} r
3410
          $extrajoins
3411
     LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3412
          $extrawhere
3413
      GROUP BY r.id, r.name, r.shortname, rn.name, r.sortorder
3414
      ORDER BY r.sortorder";
3415
    $roles = $DB->get_records_sql($query, $params);
3416
 
3417
    return role_fix_names($roles, $context, $rolenamedisplay, true);
3418
}
3419
 
3420
/**
3421
 * Gets a list of roles that this user can override in this context.
3422
 *
3423
 * @param context $context the context.
3424
 * @param int $rolenamedisplay the type of role name to display. One of the
3425
 *      ROLENAME_X constants. Default ROLENAME_ALIAS.
3426
 * @param bool $withcounts if true, count the number of overrides that are set for each role.
3427
 * @return array if $withcounts is false, then an array $roleid => $rolename.
3428
 *      if $withusercounts is true, returns a list of three arrays,
3429
 *      $rolenames, $rolecounts, and $nameswithcounts.
3430
 */
3431
function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
3432
    global $USER, $DB;
3433
 
3434
    if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
3435
        if ($withcounts) {
3436
            return array(array(), array(), array());
3437
        } else {
3438
            return array();
3439
        }
3440
    }
3441
 
3442
    $parents = $context->get_parent_context_ids(true);
3443
    $contexts = implode(',' , $parents);
3444
 
3445
    $params = array();
3446
    $extrafields = '';
3447
 
3448
    $params['userid'] = $USER->id;
3449
    if ($withcounts) {
3450
        $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
3451
                WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
3452
        $params['conid'] = $context->id;
3453
    }
3454
 
3455
    if ($coursecontext = $context->get_course_context(false)) {
3456
        $params['coursecontext'] = $coursecontext->id;
3457
    } else {
3458
        $params['coursecontext'] = 0; // no course aliases
3459
        $coursecontext = null;
3460
    }
3461
 
3462
    if (is_siteadmin()) {
3463
        // show all roles to admins
3464
        $roles = $DB->get_records_sql("
3465
            SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3466
              FROM {role} ro
3467
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3468
          ORDER BY ro.sortorder ASC", $params);
3469
 
3470
    } else {
3471
        $roles = $DB->get_records_sql("
3472
            SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
3473
              FROM {role} ro
3474
              JOIN (SELECT DISTINCT r.id
3475
                      FROM {role} r
3476
                      JOIN {role_allow_override} rao ON r.id = rao.allowoverride
3477
                      JOIN {role_assignments} ra ON rao.roleid = ra.roleid
3478
                     WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
3479
                   ) inline_view ON ro.id = inline_view.id
3480
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
3481
          ORDER BY ro.sortorder ASC", $params);
3482
    }
3483
 
3484
    $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
3485
 
3486
    if (!$withcounts) {
3487
        return $rolenames;
3488
    }
3489
 
3490
    $rolecounts = array();
3491
    $nameswithcounts = array();
3492
    foreach ($roles as $role) {
3493
        $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
3494
        $rolecounts[$role->id] = $roles[$role->id]->overridecount;
3495
    }
3496
    return array($rolenames, $rolecounts, $nameswithcounts);
3497
}
3498
 
3499
/**
3500
 * Create a role menu suitable for default role selection in enrol plugins.
3501
 *
3502
 * @package    core_enrol
3503
 *
3504
 * @param context $context
3505
 * @param int $addroleid current or default role - always added to list
3506
 * @return array roleid=>localised role name
3507
 */
3508
function get_default_enrol_roles(context $context, $addroleid = null) {
3509
    global $DB;
3510
 
3511
    $params = array('contextlevel'=>CONTEXT_COURSE);
3512
 
3513
    if ($coursecontext = $context->get_course_context(false)) {
3514
        $params['coursecontext'] = $coursecontext->id;
3515
    } else {
3516
        $params['coursecontext'] = 0; // no course names
3517
        $coursecontext = null;
3518
    }
3519
 
3520
    if ($addroleid) {
3521
        $addrole = "OR r.id = :addroleid";
3522
        $params['addroleid'] = $addroleid;
3523
    } else {
3524
        $addrole = "";
3525
    }
3526
 
3527
    $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
3528
              FROM {role} r
3529
         LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
3530
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
3531
             WHERE rcl.id IS NOT NULL $addrole
3532
          ORDER BY sortorder DESC";
3533
 
3534
    $roles = $DB->get_records_sql($sql, $params);
3535
 
3536
    return role_fix_names($roles, $context, ROLENAME_BOTH, true);
3537
}
3538
 
3539
/**
3540
 * Return context levels where this role is assignable.
3541
 *
3542
 * @param integer $roleid the id of a role.
3543
 * @return array list of the context levels at which this role may be assigned.
3544
 */
3545
function get_role_contextlevels($roleid) {
3546
    global $DB;
3547
    return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
3548
            'contextlevel', 'id,contextlevel');
3549
}
3550
 
3551
/**
3552
 * Return roles suitable for assignment at the specified context level.
3553
 *
3554
 * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
3555
 *
3556
 * @param integer $contextlevel a contextlevel.
3557
 * @return array list of role ids that are assignable at this context level.
3558
 */
3559
function get_roles_for_contextlevels($contextlevel) {
3560
    global $DB;
3561
    return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
3562
            '', 'id,roleid');
3563
}
3564
 
3565
/**
3566
 * Returns default context levels where roles can be assigned.
3567
 *
3568
 * @param string $rolearchetype one of the role archetypes - that is, one of the keys
3569
 *      from the array returned by get_role_archetypes();
3570
 * @return array list of the context levels at which this type of role may be assigned by default.
3571
 */
3572
function get_default_contextlevels($rolearchetype) {
3573
    return \context_helper::get_compatible_levels($rolearchetype);
3574
}
3575
 
3576
/**
3577
 * Set the context levels at which a particular role can be assigned.
3578
 * Throws exceptions in case of error.
3579
 *
3580
 * @param integer $roleid the id of a role.
3581
 * @param array $contextlevels the context levels at which this role should be assignable,
3582
 *      duplicate levels are removed.
3583
 * @return void
3584
 */
3585
function set_role_contextlevels($roleid, array $contextlevels) {
3586
    global $DB;
3587
    $DB->delete_records('role_context_levels', array('roleid' => $roleid));
3588
    $rcl = new stdClass();
3589
    $rcl->roleid = $roleid;
3590
    $contextlevels = array_unique($contextlevels);
3591
    foreach ($contextlevels as $level) {
3592
        $rcl->contextlevel = $level;
3593
        $DB->insert_record('role_context_levels', $rcl, false, true);
3594
    }
3595
}
3596
 
3597
/**
3598
 * Gets sql joins for finding users with capability in the given context.
3599
 *
3600
 * @param context $context Context for the join.
3601
 * @param string|array $capability Capability name or array of names.
3602
 *      If an array is provided then this is the equivalent of a logical 'OR',
3603
 *      i.e. the user needs to have one of these capabilities.
3604
 * @param string $useridcolumn e.g. 'u.id'.
3605
 * @return \core\dml\sql_join Contains joins, wheres, params.
3606
 *      This function will set ->cannotmatchanyrows if applicable.
3607
 *      This may let you skip doing a DB query.
3608
 */
3609
function get_with_capability_join(context $context, $capability, $useridcolumn) {
3610
    global $CFG, $DB;
3611
 
3612
    // Add a unique prefix to param names to ensure they are unique.
3613
    static $i = 0;
3614
    $i++;
3615
    $paramprefix = 'eu' . $i . '_';
3616
 
3617
    $defaultuserroleid      = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
3618
    $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
3619
 
3620
    $ctxids = trim($context->path, '/');
3621
    $ctxids = str_replace('/', ',', $ctxids);
3622
 
3623
    // Context is the frontpage
3624
    $isfrontpage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID;
3625
    $isfrontpage = $isfrontpage || is_inside_frontpage($context);
3626
 
3627
    $caps = (array) $capability;
3628
 
3629
    // Construct list of context paths bottom --> top.
3630
    list($contextids, $paths) = get_context_info_list($context);
3631
 
3632
    // We need to find out all roles that have these capabilities either in definition or in overrides.
3633
    $defs = [];
3634
    list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, $paramprefix . 'con');
3635
    list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, $paramprefix . 'cap');
3636
 
3637
    // Check whether context locking is enabled.
3638
    // Filter out any write capability if this is the case.
3639
    $excludelockedcaps = '';
3640
    $excludelockedcapsparams = [];
3641
    if (!empty($CFG->contextlocking) && $context->locked) {
3642
        $excludelockedcaps = 'AND (cap.captype = :capread OR cap.name = :managelockscap)';
3643
        $excludelockedcapsparams['capread'] = 'read';
3644
        $excludelockedcapsparams['managelockscap'] = 'moodle/site:managecontextlocks';
3645
    }
3646
 
3647
    $params = array_merge($params, $params2, $excludelockedcapsparams);
3648
    $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
3649
              FROM {role_capabilities} rc
3650
              JOIN {capabilities} cap ON rc.capability = cap.name
3651
              JOIN {context} ctx on rc.contextid = ctx.id
3652
             WHERE rc.contextid $incontexts AND rc.capability $incaps $excludelockedcaps";
3653
 
3654
    $rcs = $DB->get_records_sql($sql, $params);
3655
    foreach ($rcs as $rc) {
3656
        $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
3657
    }
3658
 
3659
    // Go through the permissions bottom-->top direction to evaluate the current permission,
3660
    // first one wins (prohibit is an exception that always wins).
3661
    $access = [];
3662
    foreach ($caps as $cap) {
3663
        foreach ($paths as $path) {
3664
            if (empty($defs[$cap][$path])) {
3665
                continue;
3666
            }
3667
            foreach ($defs[$cap][$path] as $roleid => $perm) {
3668
                if ($perm == CAP_PROHIBIT) {
3669
                    $access[$cap][$roleid] = CAP_PROHIBIT;
3670
                    continue;
3671
                }
3672
                if (!isset($access[$cap][$roleid])) {
3673
                    $access[$cap][$roleid] = (int)$perm;
3674
                }
3675
            }
3676
        }
3677
    }
3678
 
3679
    // Make lists of roles that are needed and prohibited in this context.
3680
    $needed = []; // One of these is enough.
3681
    $prohibited = []; // Must not have any of these.
3682
    foreach ($caps as $cap) {
3683
        if (empty($access[$cap])) {
3684
            continue;
3685
        }
3686
        foreach ($access[$cap] as $roleid => $perm) {
3687
            if ($perm == CAP_PROHIBIT) {
3688
                unset($needed[$cap][$roleid]);
3689
                $prohibited[$cap][$roleid] = true;
3690
            } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
3691
                $needed[$cap][$roleid] = true;
3692
            }
3693
        }
3694
        if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
3695
            // Easy, nobody has the permission.
3696
            unset($needed[$cap]);
3697
            unset($prohibited[$cap]);
3698
        } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
3699
            // Everybody is disqualified on the frontpage.
3700
            unset($needed[$cap]);
3701
            unset($prohibited[$cap]);
3702
        }
3703
        if (empty($prohibited[$cap])) {
3704
            unset($prohibited[$cap]);
3705
        }
3706
    }
3707
 
3708
    if (empty($needed)) {
3709
        // There can not be anybody if no roles match this request.
3710
        return new \core\dml\sql_join('', '1 = 2', [], true);
3711
    }
3712
 
3713
    if (empty($prohibited)) {
3714
        // We can compact the needed roles.
3715
        $n = [];
3716
        foreach ($needed as $cap) {
3717
            foreach ($cap as $roleid => $unused) {
3718
                $n[$roleid] = true;
3719
            }
3720
        }
3721
        $needed = ['any' => $n];
3722
        unset($n);
3723
    }
3724
 
3725
    // Prepare query clauses.
3726
    $wherecond = [];
3727
    $params    = [];
3728
    $joins     = [];
3729
    $cannotmatchanyrows = false;
3730
 
3731
    // We never return deleted users or guest account.
3732
    // Use a hack to get the deleted user column without an API change.
3733
    $deletedusercolumn = substr($useridcolumn, 0, -2) . 'deleted';
3734
    $wherecond[] = "$deletedusercolumn = 0 AND $useridcolumn <> :{$paramprefix}guestid";
3735
    $params[$paramprefix . 'guestid'] = $CFG->siteguest;
3736
 
3737
    // Now add the needed and prohibited roles conditions as joins.
3738
    if (!empty($needed['any'])) {
3739
        // Simple case - there are no prohibits involved.
3740
        if (!empty($needed['any'][$defaultuserroleid]) ||
3741
                ($isfrontpage && !empty($needed['any'][$defaultfrontpageroleid]))) {
3742
            // Everybody.
3743
        } else {
3744
            $joins[] = "JOIN (SELECT DISTINCT userid
3745
                                FROM {role_assignments}
3746
                               WHERE contextid IN ($ctxids)
3747
                                     AND roleid IN (" . implode(',', array_keys($needed['any'])) . ")
3748
                             ) ra ON ra.userid = $useridcolumn";
3749
        }
3750
    } else {
3751
        $unions = [];
3752
        $everybody = false;
3753
        foreach ($needed as $cap => $unused) {
3754
            if (empty($prohibited[$cap])) {
3755
                if (!empty($needed[$cap][$defaultuserroleid]) ||
3756
                        ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3757
                    $everybody = true;
3758
                    break;
3759
                } else {
3760
                    $unions[] = "SELECT userid
3761
                                   FROM {role_assignments}
3762
                                  WHERE contextid IN ($ctxids)
3763
                                        AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
3764
                }
3765
            } else {
3766
                if (!empty($prohibited[$cap][$defaultuserroleid]) ||
3767
                        ($isfrontpage && !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
3768
                    // Nobody can have this cap because it is prohibited in default roles.
3769
                    continue;
3770
 
3771
                } else if (!empty($needed[$cap][$defaultuserroleid]) ||
3772
                        ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
3773
                    // Everybody except the prohibited - hiding does not matter.
3774
                    $unions[] = "SELECT id AS userid
3775
                                   FROM {user}
3776
                                  WHERE id NOT IN (SELECT userid
3777
                                                     FROM {role_assignments}
3778
                                                    WHERE contextid IN ($ctxids)
3779
                                                          AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3780
 
3781
                } else {
3782
                    $unions[] = "SELECT userid
3783
                                   FROM {role_assignments}
3784
                                  WHERE contextid IN ($ctxids) AND roleid IN (" . implode(',', array_keys($needed[$cap])) . ")
3785
                                        AND userid NOT IN (
3786
                                            SELECT userid
3787
                                              FROM {role_assignments}
3788
                                             WHERE contextid IN ($ctxids)
3789
                                                   AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
3790
                }
3791
            }
3792
        }
3793
 
3794
        if (!$everybody) {
3795
            if ($unions) {
3796
                $joins[] = "JOIN (
3797
                                  SELECT DISTINCT userid
3798
                                    FROM (
3799
                                            " . implode("\n UNION \n", $unions) . "
3800
                                         ) us
3801
                                 ) ra ON ra.userid = $useridcolumn";
3802
            } else {
3803
                // Only prohibits found - nobody can be matched.
3804
                $wherecond[] = "1 = 2";
3805
                $cannotmatchanyrows = true;
3806
            }
3807
        }
3808
    }
3809
 
3810
    return new \core\dml\sql_join(implode("\n", $joins), implode(" AND ", $wherecond), $params, $cannotmatchanyrows);
3811
}
3812
 
3813
/**
3814
 * Who has this capability in this context?
3815
 *
3816
 * This can be a very expensive call - use sparingly and keep
3817
 * the results if you are going to need them again soon.
3818
 *
3819
 * Note if $fields is empty this function attempts to get u.*
3820
 * which can get rather large - and has a serious perf impact
3821
 * on some DBs.
3822
 *
3823
 * @param context $context
3824
 * @param string|array $capability - capability name(s)
3825
 * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
3826
 * @param string $sort - the sort order. Default is lastaccess time.
3827
 * @param mixed $limitfrom - number of records to skip (offset)
3828
 * @param mixed $limitnum - number of records to fetch
3829
 * @param string|array $groups - single group or array of groups - only return
3830
 *               users who are in one of these group(s).
3831
 * @param string|array $exceptions - list of users to exclude, comma separated or array
3832
 * @param bool $notuseddoanything not used any more, admin accounts are never returned
3833
 * @param bool $notusedview - use get_enrolled_sql() instead
3834
 * @param bool $useviewallgroups if $groups is set the return users who
3835
 *               have capability both $capability and moodle/site:accessallgroups
3836
 *               in this context, as well as users who have $capability and who are
3837
 *               in $groups.
3838
 * @return array of user records
3839
 */
3840
function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
3841
        $groups = '', $exceptions = '', $notuseddoanything = null, $notusedview = null, $useviewallgroups = false) {
3842
    global $CFG, $DB;
3843
 
3844
    // Context is a course page other than the frontpage.
3845
    $iscoursepage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid != SITEID;
3846
 
3847
    // Set up default fields list if necessary.
3848
    if (empty($fields)) {
3849
        if ($iscoursepage) {
3850
            $fields = 'u.*, ul.timeaccess AS lastaccess';
3851
        } else {
3852
            $fields = 'u.*';
3853
        }
3854
    } else {
3855
        if ($CFG->debugdeveloper && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
3856
            debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
3857
        }
3858
    }
3859
 
3860
    // Set up default sort if necessary.
3861
    if (empty($sort)) { // default to course lastaccess or just lastaccess
3862
        if ($iscoursepage) {
3863
            $sort = 'ul.timeaccess';
3864
        } else {
3865
            $sort = 'u.lastaccess';
3866
        }
3867
    }
3868
 
3869
    // Get the bits of SQL relating to capabilities.
3870
    $sqljoin = get_with_capability_join($context, $capability, 'u.id');
3871
    if ($sqljoin->cannotmatchanyrows) {
3872
        return [];
3873
    }
3874
 
3875
    // Prepare query clauses.
3876
    $wherecond = [$sqljoin->wheres];
3877
    $params    = $sqljoin->params;
3878
    $joins     = [$sqljoin->joins];
3879
 
3880
    // Add user lastaccess JOIN, if required.
3881
    if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
3882
         // Here user_lastaccess is not required MDL-13810.
3883
    } else {
3884
        if ($iscoursepage) {
3885
            $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
3886
        } else {
3887
            throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
3888
        }
3889
    }
3890
 
3891
    // Groups.
3892
    if ($groups) {
3893
        $groups = (array)$groups;
3894
        list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
3895
        $joins[] = "LEFT OUTER JOIN (SELECT DISTINCT userid
3896
                                       FROM {groups_members}
3897
                                      WHERE groupid $grouptest
3898
                                    ) gm ON gm.userid = u.id";
3899
 
3900
        $params = array_merge($params, $grpparams);
3901
 
3902
        $grouptest = 'gm.userid IS NOT NULL';
3903
        if ($useviewallgroups) {
3904
            $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
3905
            if (!empty($viewallgroupsusers)) {
3906
                $grouptest .= ' OR u.id IN (' . implode(',', array_keys($viewallgroupsusers)) . ')';
3907
            }
3908
        }
3909
        $wherecond[] = "($grouptest)";
3910
    }
3911
 
3912
    // User exceptions.
3913
    if (!empty($exceptions)) {
3914
        $exceptions = (array)$exceptions;
3915
        list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc', false);
3916
        $params = array_merge($params, $exparams);
3917
        $wherecond[] = "u.id $exsql";
3918
    }
3919
 
3920
    // Collect WHERE conditions and needed joins.
3921
    $where = implode(' AND ', $wherecond);
3922
    if ($where !== '') {
3923
        $where = 'WHERE ' . $where;
3924
    }
3925
    $joins = implode("\n", $joins);
3926
 
3927
    // Finally! we have all the bits, run the query.
3928
    $sql = "SELECT $fields
3929
              FROM {user} u
3930
            $joins
3931
            $where
3932
          ORDER BY $sort";
3933
 
3934
    return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
3935
}
3936
 
3937
/**
3938
 * Re-sort a users array based on a sorting policy
3939
 *
3940
 * Will re-sort a $users results array (from get_users_by_capability(), usually)
3941
 * based on a sorting policy. This is to support the odd practice of
3942
 * sorting teachers by 'authority', where authority was "lowest id of the role
3943
 * assignment".
3944
 *
3945
 * Will execute 1 database query. Only suitable for small numbers of users, as it
3946
 * uses an u.id IN() clause.
3947
 *
3948
 * Notes about the sorting criteria.
3949
 *
3950
 * As a default, we cannot rely on role.sortorder because then
3951
 * admins/coursecreators will always win. That is why the sane
3952
 * rule "is locality matters most", with sortorder as 2nd
3953
 * consideration.
3954
 *
3955
 * If you want role.sortorder, use the 'sortorder' policy, and
3956
 * name explicitly what roles you want to cover. It's probably
3957
 * a good idea to see what roles have the capabilities you want
3958
 * (array_diff() them against roiles that have 'can-do-anything'
3959
 * to weed out admin-ish roles. Or fetch a list of roles from
3960
 * variables like $CFG->coursecontact .
3961
 *
3962
 * @param array $users Users array, keyed on userid
3963
 * @param context $context
3964
 * @param array $roles ids of the roles to include, optional
3965
 * @param string $sortpolicy defaults to locality, more about
3966
 * @return array sorted copy of the array
3967
 */
3968
function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') {
3969
    global $DB;
3970
 
3971
    $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
3972
    $contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
3973
    if (empty($roles)) {
3974
        $roleswhere = '';
3975
    } else {
3976
        $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
3977
    }
3978
 
3979
    $sql = "SELECT ra.userid
3980
              FROM {role_assignments} ra
3981
              JOIN {role} r
3982
                   ON ra.roleid=r.id
3983
              JOIN {context} ctx
3984
                   ON ra.contextid=ctx.id
3985
             WHERE $userswhere
3986
                   $contextwhere
3987
                   $roleswhere";
3988
 
3989
    // Default 'locality' policy -- read PHPDoc notes
3990
    // about sort policies...
3991
    $orderby = 'ORDER BY '
3992
                    .'ctx.depth DESC, '  /* locality wins */
3993
                    .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3994
                    .'ra.id';            /* role assignment order tie-breaker */
3995
    if ($sortpolicy === 'sortorder') {
3996
        $orderby = 'ORDER BY '
3997
                        .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
3998
                        .'ra.id';            /* role assignment order tie-breaker */
3999
    }
4000
 
4001
    $sortedids = $DB->get_fieldset_sql($sql . $orderby);
4002
    $sortedusers = array();
4003
    $seen = array();
4004
 
4005
    foreach ($sortedids as $id) {
4006
        // Avoid duplicates
4007
        if (isset($seen[$id])) {
4008
            continue;
4009
        }
4010
        $seen[$id] = true;
4011
 
4012
        // assign
4013
        $sortedusers[$id] = $users[$id];
4014
    }
4015
    return $sortedusers;
4016
}
4017
 
4018
/**
4019
 * Gets all the users assigned this role in this context or higher
4020
 *
4021
 * Note that moodle is based on capabilities and it is usually better
4022
 * to check permissions than to check role ids as the capabilities
4023
 * system is more flexible. If you really need, you can to use this
4024
 * function but consider has_capability() as a possible substitute.
4025
 *
4026
 * All $sort fields are added into $fields if not present there yet.
4027
 *
4028
 * If $roleid is an array or is empty (all roles) you need to set $fields
4029
 * (and $sort by extension) params according to it, as the first field
4030
 * returned by the database should be unique (ra.id is the best candidate).
4031
 *
4032
 * @param int|array $roleid (can also be an array of ints!)
4033
 * @param context $context
4034
 * @param bool $parent if true, get list of users assigned in higher context too
4035
 * @param string $fields fields from user (u.) , role assignment (ra) or role (r.)
4036
 * @param string $sort sort from user (u.) , role assignment (ra.) or role (r.).
4037
 *      null => use default sort from users_order_by_sql.
4038
 * @param bool $all true means all, false means limit to enrolled users
4039
 * @param string $group defaults to ''
4040
 * @param mixed $limitfrom defaults to ''
4041
 * @param mixed $limitnum defaults to ''
4042
 * @param string $extrawheretest defaults to ''
4043
 * @param array $whereorsortparams any paramter values used by $sort or $extrawheretest.
4044
 * @return array
4045
 */
4046
function get_role_users($roleid, context $context, $parent = false, $fields = '',
4047
        $sort = null, $all = true, $group = '',
4048
        $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereorsortparams = array()) {
4049
    global $DB;
4050
 
4051
    if (empty($fields)) {
4052
        $userfieldsapi = \core_user\fields::for_name();
4053
        $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
4054
        $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
4055
                  'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
4056
                  'u.country, u.picture, u.idnumber, u.department, u.institution, '.
4057
                  'u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name AS rolename, r.sortorder, '.
4058
                  'r.shortname AS roleshortname, rn.name AS rolecoursealias';
4059
    }
4060
 
4061
    // Prevent wrong function uses.
4062
    if ((empty($roleid) || is_array($roleid)) && strpos($fields, 'ra.id') !== 0) {
4063
        debugging('get_role_users() without specifying one single roleid needs to be called prefixing ' .
4064
            'role assignments id (ra.id) as unique field, you can use $fields param for it.');
4065
 
4066
        if (!empty($roleid)) {
4067
            // Solving partially the issue when specifying multiple roles.
4068
            $users = array();
4069
            foreach ($roleid as $id) {
4070
                // Ignoring duplicated keys keeping the first user appearance.
4071
                $users = $users + get_role_users($id, $context, $parent, $fields, $sort, $all, $group,
4072
                    $limitfrom, $limitnum, $extrawheretest, $whereorsortparams);
4073
            }
4074
            return $users;
4075
        }
4076
    }
4077
 
4078
    $parentcontexts = '';
4079
    if ($parent) {
4080
        $parentcontexts = substr($context->path, 1); // kill leading slash
4081
        $parentcontexts = str_replace('/', ',', $parentcontexts);
4082
        if ($parentcontexts !== '') {
4083
            $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
4084
        }
4085
    }
4086
 
4087
    if ($roleid) {
4088
        list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'r');
4089
        $roleselect = "AND ra.roleid $rids";
4090
    } else {
4091
        $params = array();
4092
        $roleselect = '';
4093
    }
4094
 
4095
    if ($coursecontext = $context->get_course_context(false)) {
4096
        $params['coursecontext'] = $coursecontext->id;
4097
    } else {
4098
        $params['coursecontext'] = 0;
4099
    }
4100
 
4101
    if ($group) {
4102
        $groupjoin   = "JOIN {groups_members} gm ON gm.userid = u.id";
4103
        $groupselect = " AND gm.groupid = :groupid ";
4104
        $params['groupid'] = $group;
4105
    } else {
4106
        $groupjoin   = '';
4107
        $groupselect = '';
4108
    }
4109
 
4110
    $params['contextid'] = $context->id;
4111
 
4112
    if ($extrawheretest) {
4113
        $extrawheretest = ' AND ' . $extrawheretest;
4114
    }
4115
 
4116
    if ($whereorsortparams) {
4117
        $params = array_merge($params, $whereorsortparams);
4118
    }
4119
 
4120
    if (!$sort) {
4121
        list($sort, $sortparams) = users_order_by_sql('u');
4122
        $params = array_merge($params, $sortparams);
4123
    }
4124
 
4125
    // Adding the fields from $sort that are not present in $fields.
4126
    $sortarray = preg_split('/,\s*/', $sort);
4127
    $fieldsarray = preg_split('/,\s*/', $fields);
4128
 
4129
    // Discarding aliases from the fields.
4130
    $fieldnames = array();
4131
    foreach ($fieldsarray as $key => $field) {
4132
        list($fieldnames[$key]) = explode(' ', $field);
4133
    }
4134
 
4135
    $addedfields = array();
4136
    foreach ($sortarray as $sortfield) {
4137
        // Throw away any additional arguments to the sort (e.g. ASC/DESC).
4138
        list($sortfield) = explode(' ', $sortfield);
4139
        list($tableprefix) = explode('.', $sortfield);
4140
        $fieldpresent = false;
4141
        foreach ($fieldnames as $fieldname) {
4142
            if ($fieldname === $sortfield || $fieldname === $tableprefix.'.*') {
4143
                $fieldpresent = true;
4144
                break;
4145
            }
4146
        }
4147
 
4148
        if (!$fieldpresent) {
4149
            $fieldsarray[] = $sortfield;
4150
            $addedfields[] = $sortfield;
4151
        }
4152
    }
4153
 
4154
    $fields = implode(', ', $fieldsarray);
4155
    if (!empty($addedfields)) {
4156
        $addedfields = implode(', ', $addedfields);
4157
        debugging('get_role_users() adding '.$addedfields.' to the query result because they were required by $sort but missing in $fields');
4158
    }
4159
 
4160
    if ($all === null) {
4161
        // Previously null was used to indicate that parameter was not used.
4162
        $all = true;
4163
    }
4164
    if (!$all and $coursecontext) {
4165
        // Do not use get_enrolled_sql() here for performance reasons.
4166
        $ejoin = "JOIN {user_enrolments} ue ON ue.userid = u.id
4167
                  JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :ecourseid)";
4168
        $params['ecourseid'] = $coursecontext->instanceid;
4169
    } else {
4170
        $ejoin = "";
4171
    }
4172
 
4173
    $sql = "SELECT DISTINCT $fields, ra.roleid
4174
              FROM {role_assignments} ra
4175
              JOIN {user} u ON u.id = ra.userid
4176
              JOIN {role} r ON ra.roleid = r.id
4177
            $ejoin
4178
         LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
4179
        $groupjoin
4180
             WHERE (ra.contextid = :contextid $parentcontexts)
4181
                   $roleselect
4182
                   $groupselect
4183
                   $extrawheretest
4184
          ORDER BY $sort";                  // join now so that we can just use fullname() later
4185
 
4186
    return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
4187
}
4188
 
4189
/**
4190
 * Counts all the users assigned this role in this context or higher
4191
 *
4192
 * @param int|array $roleid either int or an array of ints
4193
 * @param context $context
4194
 * @param bool $parent if true, get list of users assigned in higher context too
4195
 * @return int Returns the result count
4196
 */
4197
function count_role_users($roleid, context $context, $parent = false) {
4198
    global $DB;
4199
 
4200
    if ($parent) {
4201
        if ($contexts = $context->get_parent_context_ids()) {
4202
            $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
4203
        } else {
4204
            $parentcontexts = '';
4205
        }
4206
    } else {
4207
        $parentcontexts = '';
4208
    }
4209
 
4210
    if ($roleid) {
4211
        list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
4212
        $roleselect = "AND r.roleid $rids";
4213
    } else {
4214
        $params = array();
4215
        $roleselect = '';
4216
    }
4217
 
4218
    array_unshift($params, $context->id);
4219
 
4220
    $sql = "SELECT COUNT(DISTINCT u.id)
4221
              FROM {role_assignments} r
4222
              JOIN {user} u ON u.id = r.userid
4223
             WHERE (r.contextid = ? $parentcontexts)
4224
                   $roleselect
4225
                   AND u.deleted = 0";
4226
 
4227
    return $DB->count_records_sql($sql, $params);
4228
}
4229
 
4230
/**
4231
 * This function gets the list of course and course category contexts that this user has a particular capability in.
4232
 *
4233
 * It is now reasonably efficient, but bear in mind that if there are users who have the capability
4234
 * everywhere, it may return an array of all contexts.
4235
 *
4236
 * @param string $capability Capability in question
4237
 * @param int $userid User ID or null for current user
4238
 * @param bool $getcategories Wether to return also course_categories
4239
 * @param bool $doanything True if 'doanything' is permitted (default)
4240
 * @param string $coursefieldsexceptid Leave blank if you only need 'id' in the course records;
4241
 *   otherwise use a comma-separated list of the fields you require, not including id.
4242
 *   Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
4243
 * @param string $categoryfieldsexceptid Leave blank if you only need 'id' in the course records;
4244
 *   otherwise use a comma-separated list of the fields you require, not including id.
4245
 *   Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
4246
 * @param string $courseorderby If set, use a comma-separated list of fields from course
4247
 *   table with sql modifiers (DESC) if needed
4248
 * @param string $categoryorderby If set, use a comma-separated list of fields from course_category
4249
 *   table with sql modifiers (DESC) if needed
4250
 * @param int $limit Limit the number of courses to return on success. Zero equals all entries.
4251
 * @return array Array of categories and courses.
4252
 */
4253
function get_user_capability_contexts(string $capability, bool $getcategories, $userid = null, $doanything = true,
4254
                                      $coursefieldsexceptid = '', $categoryfieldsexceptid = '', $courseorderby = '',
4255
                                      $categoryorderby = '', $limit = 0): array {
4256
    global $DB, $USER;
4257
 
4258
    // Default to current user.
4259
    if (!$userid) {
4260
        $userid = $USER->id;
4261
    }
4262
 
4263
    if (!$capinfo = get_capability_info($capability)) {
4264
        debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
4265
        return [false, false];
4266
    }
4267
 
4268
    if ($doanything && is_siteadmin($userid)) {
4269
        // If the user is a site admin and $doanything is enabled then there is no need to restrict
4270
        // the list of courses.
4271
        $contextlimitsql = '';
4272
        $contextlimitparams = [];
4273
    } else {
4274
        // Gets SQL to limit contexts ('x' table) to those where the user has this capability.
4275
        list ($contextlimitsql, $contextlimitparams) = \core\access\get_user_capability_course_helper::get_sql(
4276
            $userid, $capinfo->name);
4277
        if (!$contextlimitsql) {
4278
            // If the does not have this capability in any context, return false without querying.
4279
            return [false, false];
4280
        }
4281
 
4282
        $contextlimitsql = 'WHERE' . $contextlimitsql;
4283
    }
4284
 
4285
    $categories = [];
4286
    if ($getcategories) {
4287
        $fieldlist = \core\access\get_user_capability_course_helper::map_fieldnames($categoryfieldsexceptid);
4288
        if ($categoryorderby) {
4289
            $fields = explode(',', $categoryorderby);
4290
            $categoryorderby = '';
4291
            foreach ($fields as $field) {
4292
                if ($categoryorderby) {
4293
                    $categoryorderby .= ',';
4294
                }
4295
                $categoryorderby .= 'c.'.$field;
4296
            }
4297
            $categoryorderby = 'ORDER BY '.$categoryorderby;
4298
        }
4299
        $rs = $DB->get_recordset_sql("
4300
            SELECT c.id $fieldlist
4301
              FROM {course_categories} c
4302
               JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ?
4303
            $contextlimitsql
4304
            $categoryorderby", array_merge([CONTEXT_COURSECAT], $contextlimitparams));
4305
        $basedlimit = $limit;
4306
        foreach ($rs as $category) {
4307
            $categories[] = $category;
4308
            $basedlimit--;
4309
            if ($basedlimit == 0) {
4310
                break;
4311
            }
4312
        }
4313
        $rs->close();
4314
    }
4315
 
4316
    $courses = [];
4317
    $fieldlist = \core\access\get_user_capability_course_helper::map_fieldnames($coursefieldsexceptid);
4318
    if ($courseorderby) {
4319
        $fields = explode(',', $courseorderby);
4320
        $courseorderby = '';
4321
        foreach ($fields as $field) {
4322
            if ($courseorderby) {
4323
                $courseorderby .= ',';
4324
            }
4325
            $courseorderby .= 'c.'.$field;
4326
        }
4327
        $courseorderby = 'ORDER BY '.$courseorderby;
4328
    }
4329
    $rs = $DB->get_recordset_sql("
4330
            SELECT c.id $fieldlist
4331
              FROM {course} c
4332
               JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ?
4333
            $contextlimitsql
4334
            $courseorderby", array_merge([CONTEXT_COURSE], $contextlimitparams));
4335
    foreach ($rs as $course) {
4336
        $courses[] = $course;
4337
        $limit--;
4338
        if ($limit == 0) {
4339
            break;
4340
        }
4341
    }
4342
    $rs->close();
4343
    return [$categories, $courses];
4344
}
4345
 
4346
/**
4347
 * This function gets the list of courses that this user has a particular capability in.
4348
 *
4349
 * It is now reasonably efficient, but bear in mind that if there are users who have the capability
4350
 * everywhere, it may return an array of all courses.
4351
 *
4352
 * @param string $capability Capability in question
4353
 * @param int $userid User ID or null for current user
4354
 * @param bool $doanything True if 'doanything' is permitted (default)
4355
 * @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
4356
 *   otherwise use a comma-separated list of the fields you require, not including id.
4357
 *   Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
4358
 * @param string $orderby If set, use a comma-separated list of fields from course
4359
 *   table with sql modifiers (DESC) if needed
4360
 * @param int $limit Limit the number of courses to return on success. Zero equals all entries.
4361
 * @return array|bool Array of courses, if none found false is returned.
4362
 */
4363
function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '',
4364
                                    $orderby = '', $limit = 0) {
4365
    list($categories, $courses) = get_user_capability_contexts(
4366
        $capability,
4367
        false,
4368
        $userid,
4369
        $doanything,
4370
        $fieldsexceptid,
4371
        '',
4372
        $orderby,
4373
        '',
4374
        $limit
4375
    );
4376
    return $courses;
4377
}
4378
 
4379
/**
4380
 * Switches the current user to another role for the current session and only
4381
 * in the given context.
4382
 *
4383
 * The caller *must* check
4384
 * - that this op is allowed
4385
 * - that the requested role can be switched to in this context (use get_switchable_roles)
4386
 * - that the requested role is NOT $CFG->defaultuserroleid
4387
 *
4388
 * To "unswitch" pass 0 as the roleid.
4389
 *
4390
 * This function *will* modify $USER->access - beware
4391
 *
4392
 * @param integer $roleid the role to switch to.
4393
 * @param context $context the context in which to perform the switch.
4394
 * @return bool success or failure.
4395
 */
4396
function role_switch($roleid, context $context) {
4397
    global $USER;
4398
 
4399
    // Add the ghost RA to $USER->access as $USER->access['rsw'][$path] = $roleid.
4400
    // To un-switch just unset($USER->access['rsw'][$path]).
4401
    //
4402
    // Note: it is not possible to switch to roles that do not have course:view
4403
 
4404
    if (!isset($USER->access)) {
4405
        load_all_capabilities();
4406
    }
4407
 
4408
    // Make sure that course index is refreshed.
4409
    if ($coursecontext = $context->get_course_context()) {
4410
        core_courseformat\base::session_cache_reset(get_course($coursecontext->instanceid));
4411
    }
4412
 
4413
    // Add the switch RA
4414
    if ($roleid == 0) {
4415
        unset($USER->access['rsw'][$context->path]);
4416
        return true;
4417
    }
4418
 
4419
    $USER->access['rsw'][$context->path] = $roleid;
4420
 
4421
    return true;
4422
}
4423
 
4424
/**
4425
 * Checks if the user has switched roles within the given course.
4426
 *
4427
 * Note: You can only switch roles within the course, hence it takes a course id
4428
 * rather than a context. On that note Petr volunteered to implement this across
4429
 * all other contexts, all requests for this should be forwarded to him ;)
4430
 *
4431
 * @param int $courseid The id of the course to check
4432
 * @return bool True if the user has switched roles within the course.
4433
 */
4434
function is_role_switched($courseid) {
4435
    global $USER;
4436
    $context = context_course::instance($courseid, MUST_EXIST);
4437
    return (!empty($USER->access['rsw'][$context->path]));
4438
}
4439
 
4440
/**
4441
 * Get any role that has an override on exact context
4442
 *
4443
 * @param context $context The context to check
4444
 * @return array An array of roles
4445
 */
4446
function get_roles_with_override_on_context(context $context) {
4447
    global $DB;
4448
 
4449
    return $DB->get_records_sql("SELECT r.*
4450
                                   FROM {role_capabilities} rc, {role} r
4451
                                  WHERE rc.roleid = r.id AND rc.contextid = ?",
4452
                                array($context->id));
4453
}
4454
 
4455
/**
4456
 * Get all capabilities for this role on this context (overrides)
4457
 *
4458
 * @param stdClass $role
4459
 * @param context $context
4460
 * @return array
4461
 */
4462
function get_capabilities_from_role_on_context($role, context $context) {
4463
    global $DB;
4464
 
4465
    return $DB->get_records_sql("SELECT *
4466
                                   FROM {role_capabilities}
4467
                                  WHERE contextid = ? AND roleid = ?",
4468
                                array($context->id, $role->id));
4469
}
4470
 
4471
/**
4472
 * Find all user assignment of users for this role, on this context
4473
 *
4474
 * @param stdClass $role
4475
 * @param context $context
4476
 * @return array
4477
 */
4478
function get_users_from_role_on_context($role, context $context) {
4479
    global $DB;
4480
 
4481
    return $DB->get_records_sql("SELECT *
4482
                                   FROM {role_assignments}
4483
                                  WHERE contextid = ? AND roleid = ?",
4484
                                array($context->id, $role->id));
4485
}
4486
 
4487
/**
4488
 * Simple function returning a boolean true if user has roles
4489
 * in context or parent contexts, otherwise false.
4490
 *
4491
 * @param int $userid
4492
 * @param int $roleid
4493
 * @param int $contextid empty means any context
4494
 * @return bool
4495
 */
4496
function user_has_role_assignment($userid, $roleid, $contextid = 0) {
4497
    global $DB;
4498
 
4499
    if ($contextid) {
4500
        if (!$context = context::instance_by_id($contextid, IGNORE_MISSING)) {
4501
            return false;
4502
        }
4503
        $parents = $context->get_parent_context_ids(true);
4504
        list($contexts, $params) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED, 'r');
4505
        $params['userid'] = $userid;
4506
        $params['roleid'] = $roleid;
4507
 
4508
        $sql = "SELECT COUNT(ra.id)
4509
                  FROM {role_assignments} ra
4510
                 WHERE ra.userid = :userid AND ra.roleid = :roleid AND ra.contextid $contexts";
4511
 
4512
        $count = $DB->get_field_sql($sql, $params);
4513
        return ($count > 0);
4514
 
4515
    } else {
4516
        return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid));
4517
    }
4518
}
4519
 
4520
/**
4521
 * Get localised role name or alias if exists and format the text.
4522
 *
4523
 * @param stdClass $role role object
4524
 *      - optional 'coursealias' property should be included for performance reasons if course context used
4525
 *      - description property is not required here
4526
 * @param context|bool $context empty means system context
4527
 * @param int $rolenamedisplay type of role name
4528
 * @return string localised role name or course role name alias
4529
 */
4530
function role_get_name(stdClass $role, $context = null, $rolenamedisplay = ROLENAME_ALIAS) {
4531
    global $DB;
4532
 
4533
    if ($rolenamedisplay == ROLENAME_SHORT) {
4534
        return $role->shortname;
4535
    }
4536
 
4537
    if (!$context or !$coursecontext = $context->get_course_context(false)) {
4538
        $coursecontext = null;
4539
    }
4540
 
4541
    if ($coursecontext and !property_exists($role, 'coursealias') and ($rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH or $rolenamedisplay == ROLENAME_ALIAS_RAW)) {
4542
        $role = clone($role); // Do not modify parameters.
4543
        if ($r = $DB->get_record('role_names', array('roleid'=>$role->id, 'contextid'=>$coursecontext->id))) {
4544
            $role->coursealias = $r->name;
4545
        } else {
4546
            $role->coursealias = null;
4547
        }
4548
    }
4549
 
4550
    if ($rolenamedisplay == ROLENAME_ALIAS_RAW) {
4551
        if ($coursecontext) {
4552
            return $role->coursealias;
4553
        } else {
4554
            return null;
4555
        }
4556
    }
4557
 
4558
    if (trim($role->name) !== '') {
4559
        // For filtering always use context where was the thing defined - system for roles here.
4560
        $original = format_string($role->name, true, array('context'=>context_system::instance()));
4561
 
4562
    } else {
4563
        // Empty role->name means we want to see localised role name based on shortname,
4564
        // only default roles are supposed to be localised.
4565
        switch ($role->shortname) {
4566
            case 'manager':         $original = get_string('manager', 'role'); break;
4567
            case 'coursecreator':   $original = get_string('coursecreators'); break;
4568
            case 'editingteacher':  $original = get_string('defaultcourseteacher'); break;
4569
            case 'teacher':         $original = get_string('noneditingteacher'); break;
4570
            case 'student':         $original = get_string('defaultcoursestudent'); break;
4571
            case 'guest':           $original = get_string('guest'); break;
4572
            case 'user':            $original = get_string('authenticateduser'); break;
4573
            case 'frontpage':       $original = get_string('frontpageuser', 'role'); break;
4574
            // We should not get here, the role UI should require the name for custom roles!
4575
            default:                $original = $role->shortname; break;
4576
        }
4577
    }
4578
 
4579
    if ($rolenamedisplay == ROLENAME_ORIGINAL) {
4580
        return $original;
4581
    }
4582
 
4583
    if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
4584
        return "$original ($role->shortname)";
4585
    }
4586
 
4587
    if ($rolenamedisplay == ROLENAME_ALIAS) {
4588
        if ($coursecontext && $role->coursealias && trim($role->coursealias) !== '') {
4589
            return format_string($role->coursealias, true, array('context'=>$coursecontext));
4590
        } else {
4591
            return $original;
4592
        }
4593
    }
4594
 
4595
    if ($rolenamedisplay == ROLENAME_BOTH) {
4596
        if ($coursecontext && $role->coursealias && trim($role->coursealias) !== '') {
4597
            return format_string($role->coursealias, true, array('context'=>$coursecontext)) . " ($original)";
4598
        } else {
4599
            return $original;
4600
        }
4601
    }
4602
 
4603
    throw new coding_exception('Invalid $rolenamedisplay parameter specified in role_get_name()');
4604
}
4605
 
4606
/**
4607
 * Returns localised role description if available.
4608
 * If the name is empty it tries to find the default role name using
4609
 * hardcoded list of default role names or other methods in the future.
4610
 *
4611
 * @param stdClass $role
4612
 * @return string localised role name
4613
 */
4614
function role_get_description(stdClass $role) {
4615
    if (!html_is_blank($role->description)) {
4616
        return format_text($role->description, FORMAT_HTML, array('context'=>context_system::instance()));
4617
    }
4618
 
4619
    switch ($role->shortname) {
4620
        case 'manager':         return get_string('managerdescription', 'role');
4621
        case 'coursecreator':   return get_string('coursecreatorsdescription');
4622
        case 'editingteacher':  return get_string('defaultcourseteacherdescription');
4623
        case 'teacher':         return get_string('noneditingteacherdescription');
4624
        case 'student':         return get_string('defaultcoursestudentdescription');
4625
        case 'guest':           return get_string('guestdescription');
4626
        case 'user':            return get_string('authenticateduserdescription');
4627
        case 'frontpage':       return get_string('frontpageuserdescription', 'role');
4628
        default:                return '';
4629
    }
4630
}
4631
 
4632
/**
4633
 * Get all the localised role names for a context.
4634
 *
4635
 * In new installs default roles have empty names, this function
4636
 * add localised role names using current language pack.
4637
 *
4638
 * @param context $context the context, null means system context
4639
 * @param array of role objects with a ->localname field containing the context-specific role name.
4640
 * @param int $rolenamedisplay
4641
 * @param bool $returnmenu true means id=>localname, false means id=>rolerecord
4642
 * @return array Array of context-specific role names, or role objects with a ->localname field added.
4643
 */
4644
function role_get_names(context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
4645
    return role_fix_names(get_all_roles($context), $context, $rolenamedisplay, $returnmenu);
4646
}
4647
 
4648
/**
4649
 * Prepare list of roles for display, apply aliases and localise default role names.
4650
 *
4651
 * @param array $roleoptions array roleid => roleobject (with optional coursealias), strings are accepted for backwards compatibility only
4652
 * @param context $context the context, null means system context
4653
 * @param int $rolenamedisplay
4654
 * @param bool $returnmenu null means keep the same format as $roleoptions, true means id=>localname, false means id=>rolerecord
4655
 * @return array Array of context-specific role names, or role objects with a ->localname field added.
4656
 */
4657
function role_fix_names($roleoptions, context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
4658
    global $DB;
4659
 
4660
    if (empty($roleoptions)) {
4661
        return array();
4662
    }
4663
 
4664
    if (!$context or !$coursecontext = $context->get_course_context(false)) {
4665
        $coursecontext = null;
4666
    }
4667
 
4668
    // We usually need all role columns...
4669
    $first = reset($roleoptions);
4670
    if ($returnmenu === null) {
4671
        $returnmenu = !is_object($first);
4672
    }
4673
 
4674
    if (!is_object($first) or !property_exists($first, 'shortname')) {
4675
        $allroles = get_all_roles($context);
4676
        foreach ($roleoptions as $rid => $unused) {
4677
            $roleoptions[$rid] = $allroles[$rid];
4678
        }
4679
    }
4680
 
4681
    // Inject coursealias if necessary.
4682
    if ($coursecontext and ($rolenamedisplay == ROLENAME_ALIAS_RAW or $rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH)) {
4683
        $first = reset($roleoptions);
4684
        if (!property_exists($first, 'coursealias')) {
4685
            $aliasnames = $DB->get_records('role_names', array('contextid'=>$coursecontext->id));
4686
            foreach ($aliasnames as $alias) {
4687
                if (isset($roleoptions[$alias->roleid])) {
4688
                    $roleoptions[$alias->roleid]->coursealias = $alias->name;
4689
                }
4690
            }
4691
        }
4692
    }
4693
 
4694
    // Add localname property.
4695
    foreach ($roleoptions as $rid => $role) {
4696
        $roleoptions[$rid]->localname = role_get_name($role, $coursecontext, $rolenamedisplay);
4697
    }
4698
 
4699
    if (!$returnmenu) {
4700
        return $roleoptions;
4701
    }
4702
 
4703
    $menu = array();
4704
    foreach ($roleoptions as $rid => $role) {
4705
        $menu[$rid] = $role->localname;
4706
    }
4707
 
4708
    return $menu;
4709
}
4710
 
4711
/**
4712
 * Aids in detecting if a new line is required when reading a new capability
4713
 *
4714
 * This function helps admin/roles/manage.php etc to detect if a new line should be printed
4715
 * when we read in a new capability.
4716
 * Most of the time, if the 2 components are different we should print a new line, (e.g. course system->rss client)
4717
 * but when we are in grade, all reports/import/export capabilities should be together
4718
 *
4719
 * @param stdClass $cap component string a
4720
 * @param string $comp component string b
4721
 * @param int $contextlevel
4722
 * @return bool whether 2 component are in different "sections"
4723
 */
4724
function component_level_changed($cap, $comp, $contextlevel) {
4725
 
4726
    if (strstr($cap->component, '/') && strstr($comp, '/')) {
4727
        $compsa = explode('/', $cap->component);
4728
        $compsb = explode('/', $comp);
4729
 
4730
        // list of system reports
4731
        if (($compsa[0] == 'report') && ($compsb[0] == 'report')) {
4732
            return false;
4733
        }
4734
 
4735
        // we are in gradebook, still
4736
        if (($compsa[0] == 'gradeexport' || $compsa[0] == 'gradeimport' || $compsa[0] == 'gradereport') &&
4737
            ($compsb[0] == 'gradeexport' || $compsb[0] == 'gradeimport' || $compsb[0] == 'gradereport')) {
4738
            return false;
4739
        }
4740
 
4741
        if (($compsa[0] == 'coursereport') && ($compsb[0] == 'coursereport')) {
4742
            return false;
4743
        }
4744
    }
4745
 
4746
    return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
4747
}
4748
 
4749
/**
4750
 * Fix the roles.sortorder field in the database, so it contains sequential integers,
4751
 * and return an array of roleids in order.
4752
 *
4753
 * @param array $allroles array of roles, as returned by get_all_roles();
4754
 * @return array $role->sortorder =-> $role->id with the keys in ascending order.
4755
 */
4756
function fix_role_sortorder($allroles) {
4757
    global $DB;
4758
 
4759
    $rolesort = array();
4760
    $i = 0;
4761
    foreach ($allroles as $role) {
4762
        $rolesort[$i] = $role->id;
4763
        if ($role->sortorder != $i) {
4764
            $r = new stdClass();
4765
            $r->id = $role->id;
4766
            $r->sortorder = $i;
4767
            $DB->update_record('role', $r);
4768
            $allroles[$role->id]->sortorder = $i;
4769
        }
4770
        $i++;
4771
    }
4772
    return $rolesort;
4773
}
4774
 
4775
/**
4776
 * Switch the sort order of two roles (used in admin/roles/manage.php).
4777
 *
4778
 * @param stdClass $first The first role. Actually, only ->sortorder is used.
4779
 * @param stdClass $second The second role. Actually, only ->sortorder is used.
4780
 * @return boolean success or failure
4781
 */
4782
function switch_roles($first, $second) {
4783
    global $DB;
4784
    $temp = $DB->get_field('role', 'MAX(sortorder) + 1', array());
4785
    $result = $DB->set_field('role', 'sortorder', $temp, array('sortorder' => $first->sortorder));
4786
    $result = $result && $DB->set_field('role', 'sortorder', $first->sortorder, array('sortorder' => $second->sortorder));
4787
    $result = $result && $DB->set_field('role', 'sortorder', $second->sortorder, array('sortorder' => $temp));
4788
    return $result;
4789
}
4790
 
4791
/**
4792
 * Duplicates all the base definitions of a role
4793
 *
4794
 * @param stdClass $sourcerole role to copy from
4795
 * @param int $targetrole id of role to copy to
4796
 */
4797
function role_cap_duplicate($sourcerole, $targetrole) {
4798
    global $DB;
4799
 
4800
    $systemcontext = context_system::instance();
4801
    $caps = $DB->get_records_sql("SELECT *
4802
                                    FROM {role_capabilities}
4803
                                   WHERE roleid = ? AND contextid = ?",
4804
                                 array($sourcerole->id, $systemcontext->id));
4805
    // adding capabilities
4806
    foreach ($caps as $cap) {
4807
        unset($cap->id);
4808
        $cap->roleid = $targetrole;
4809
        $DB->insert_record('role_capabilities', $cap);
4810
    }
4811
 
4812
    // Reset any cache of this role, including MUC.
4813
    accesslib_clear_role_cache($targetrole);
4814
}
4815
 
4816
/**
4817
 * Returns two lists, this can be used to find out if user has capability.
4818
 * Having any needed role and no forbidden role in this context means
4819
 * user has this capability in this context.
4820
 * Use get_role_names_with_cap_in_context() if you need role names to display in the UI
4821
 *
4822
 * @param stdClass $context
4823
 * @param string $capability
4824
 * @return array($neededroles, $forbiddenroles)
4825
 */
4826
function get_roles_with_cap_in_context($context, $capability) {
4827
    global $DB;
4828
 
4829
    $ctxids = trim($context->path, '/'); // kill leading slash
4830
    $ctxids = str_replace('/', ',', $ctxids);
4831
 
4832
    $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
4833
              FROM {role_capabilities} rc
4834
              JOIN {context} ctx ON ctx.id = rc.contextid
4835
              JOIN {capabilities} cap ON rc.capability = cap.name
4836
             WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
4837
          ORDER BY rc.roleid ASC, ctx.depth DESC";
4838
    $params = array('cap'=>$capability);
4839
 
4840
    if (!$capdefs = $DB->get_records_sql($sql, $params)) {
4841
        // no cap definitions --> no capability
4842
        return array(array(), array());
4843
    }
4844
 
4845
    $forbidden = array();
4846
    $needed    = array();
4847
    foreach ($capdefs as $def) {
4848
        if (isset($forbidden[$def->roleid])) {
4849
            continue;
4850
        }
4851
        if ($def->permission == CAP_PROHIBIT) {
4852
            $forbidden[$def->roleid] = $def->roleid;
4853
            unset($needed[$def->roleid]);
4854
            continue;
4855
        }
4856
        if (!isset($needed[$def->roleid])) {
4857
            if ($def->permission == CAP_ALLOW) {
4858
                $needed[$def->roleid] = true;
4859
            } else if ($def->permission == CAP_PREVENT) {
4860
                $needed[$def->roleid] = false;
4861
            }
4862
        }
4863
    }
4864
    unset($capdefs);
4865
 
4866
    // remove all those roles not allowing
4867
    foreach ($needed as $key=>$value) {
4868
        if (!$value) {
4869
            unset($needed[$key]);
4870
        } else {
4871
            $needed[$key] = $key;
4872
        }
4873
    }
4874
 
4875
    return array($needed, $forbidden);
4876
}
4877
 
4878
/**
4879
 * Returns an array of role IDs that have ALL of the the supplied capabilities
4880
 * Uses get_roles_with_cap_in_context(). Returns $allowed minus $forbidden
4881
 *
4882
 * @param stdClass $context
4883
 * @param array $capabilities An array of capabilities
4884
 * @return array of roles with all of the required capabilities
4885
 */
4886
function get_roles_with_caps_in_context($context, $capabilities) {
4887
    $neededarr = array();
4888
    $forbiddenarr = array();
4889
    foreach ($capabilities as $caprequired) {
4890
        list($neededarr[], $forbiddenarr[]) = get_roles_with_cap_in_context($context, $caprequired);
4891
    }
4892
 
4893
    $rolesthatcanrate = array();
4894
    if (!empty($neededarr)) {
4895
        foreach ($neededarr as $needed) {
4896
            if (empty($rolesthatcanrate)) {
4897
                $rolesthatcanrate = $needed;
4898
            } else {
4899
                //only want roles that have all caps
4900
                $rolesthatcanrate = array_intersect_key($rolesthatcanrate,$needed);
4901
            }
4902
        }
4903
    }
4904
    if (!empty($forbiddenarr) && !empty($rolesthatcanrate)) {
4905
        foreach ($forbiddenarr as $forbidden) {
4906
           //remove any roles that are forbidden any of the caps
4907
           $rolesthatcanrate = array_diff($rolesthatcanrate, $forbidden);
4908
        }
4909
    }
4910
    return $rolesthatcanrate;
4911
}
4912
 
4913
/**
4914
 * Returns an array of role names that have ALL of the the supplied capabilities
4915
 * Uses get_roles_with_caps_in_context(). Returns $allowed minus $forbidden
4916
 *
4917
 * @param stdClass $context
4918
 * @param array $capabilities An array of capabilities
4919
 * @return array of roles with all of the required capabilities
4920
 */
4921
function get_role_names_with_caps_in_context($context, $capabilities) {
4922
    global $DB;
4923
 
4924
    $rolesthatcanrate = get_roles_with_caps_in_context($context, $capabilities);
4925
    $allroles = $DB->get_records('role', null, 'sortorder DESC');
4926
 
4927
    $roles = array();
4928
    foreach ($rolesthatcanrate as $r) {
4929
        $roles[$r] = $allroles[$r];
4930
    }
4931
 
4932
    return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
4933
}
4934
 
4935
/**
4936
 * This function verifies the prohibit comes from this context
4937
 * and there are no more prohibits in parent contexts.
4938
 *
4939
 * @param int $roleid
4940
 * @param context $context
4941
 * @param string $capability name
4942
 * @return bool
4943
 */
4944
function prohibit_is_removable($roleid, context $context, $capability) {
4945
    global $DB;
4946
 
4947
    $ctxids = trim($context->path, '/'); // kill leading slash
4948
    $ctxids = str_replace('/', ',', $ctxids);
4949
 
4950
    $params = array('roleid'=>$roleid, 'cap'=>$capability, 'prohibit'=>CAP_PROHIBIT);
4951
 
4952
    $sql = "SELECT ctx.id
4953
              FROM {role_capabilities} rc
4954
              JOIN {context} ctx ON ctx.id = rc.contextid
4955
              JOIN {capabilities} cap ON rc.capability = cap.name
4956
             WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
4957
          ORDER BY ctx.depth DESC";
4958
 
4959
    if (!$prohibits = $DB->get_records_sql($sql, $params)) {
4960
        // no prohibits == nothing to remove
4961
        return true;
4962
    }
4963
 
4964
    if (count($prohibits) > 1) {
4965
        // more prohibits can not be removed
4966
        return false;
4967
    }
4968
 
4969
    return !empty($prohibits[$context->id]);
4970
}
4971
 
4972
/**
4973
 * More user friendly role permission changing,
4974
 * it should produce as few overrides as possible.
4975
 *
4976
 * @param int $roleid
4977
 * @param stdClass|context $context
4978
 * @param string $capname capability name
4979
 * @param int $permission
4980
 * @return void
4981
 */
4982
function role_change_permission($roleid, $context, $capname, $permission) {
4983
    global $DB;
4984
 
4985
    if ($permission == CAP_INHERIT) {
4986
        unassign_capability($capname, $roleid, $context->id);
4987
        return;
4988
    }
4989
 
4990
    $ctxids = trim($context->path, '/'); // kill leading slash
4991
    $ctxids = str_replace('/', ',', $ctxids);
4992
 
4993
    $params = array('roleid'=>$roleid, 'cap'=>$capname);
4994
 
4995
    $sql = "SELECT ctx.id, rc.permission, ctx.depth
4996
              FROM {role_capabilities} rc
4997
              JOIN {context} ctx ON ctx.id = rc.contextid
4998
              JOIN {capabilities} cap ON rc.capability = cap.name
4999
             WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
5000
          ORDER BY ctx.depth DESC";
5001
 
5002
    if ($existing = $DB->get_records_sql($sql, $params)) {
5003
        foreach ($existing as $e) {
5004
            if ($e->permission == CAP_PROHIBIT) {
5005
                // prohibit can not be overridden, no point in changing anything
5006
                return;
5007
            }
5008
        }
5009
        $lowest = array_shift($existing);
5010
        if ($lowest->permission == $permission) {
5011
            // permission already set in this context or parent - nothing to do
5012
            return;
5013
        }
5014
        if ($existing) {
5015
            $parent = array_shift($existing);
5016
            if ($parent->permission == $permission) {
5017
                // permission already set in parent context or parent - just unset in this context
5018
                // we do this because we want as few overrides as possible for performance reasons
5019
                unassign_capability($capname, $roleid, $context->id);
5020
                return;
5021
            }
5022
        }
5023
 
5024
    } else {
5025
        if ($permission == CAP_PREVENT) {
5026
            // nothing means role does not have permission
5027
            return;
5028
        }
5029
    }
5030
 
5031
    // assign the needed capability
5032
    assign_capability($capname, $permission, $roleid, $context->id, true);
5033
}
5034
 
5035
/* ============== DEPRECATED FUNCTIONS ========================================== */
5036
// Old context related functions were deprecated in 2.0, it is recommended
5037
// to use context classes in new code. Old function can be used when
5038
// creating patches that are supposed to be backported to older stable branches.
5039
// These deprecated functions will not be removed in near future,
5040
// before removing devs will be warned with a debugging message first,
5041
// then we will add error message and only after that we can remove the functions
5042
// completely.
5043
 
5044
// Renamed context class do not use lib/db/renamedclasses.php because we cannot
5045
// ask everybody to update all code, so let's keep this here for the next few decades.
5046
// Another benefit is that PHPStorm understands this and stops complaining.
5047
class_alias(core\context_helper::class, 'context_helper', true);
5048
class_alias(core\context::class, 'context', true);
5049
class_alias(core\context\block::class, 'context_block');
5050
class_alias(core\context\course::class, 'context_course', true);
5051
class_alias(core\context\coursecat::class, 'context_coursecat');
5052
class_alias(core\context\module::class, 'context_module', true);
5053
class_alias(core\context\system::class, 'context_system', true);
5054
class_alias(core\context\user::class, 'context_user', true);
5055
 
5056
/**
5057
 * Runs get_records select on context table and returns the result
5058
 * Does get_records_select on the context table, and returns the results ordered
5059
 * by contextlevel, and then the natural sort order within each level.
5060
 * for the purpose of $select, you need to know that the context table has been
5061
 * aliased to ctx, so for example, you can call get_sorted_contexts('ctx.depth = 3');
5062
 *
5063
 * @param string $select the contents of the WHERE clause. Remember to do ctx.fieldname.
5064
 * @param array $params any parameters required by $select.
5065
 * @return array the requested context records.
5066
 */
5067
function get_sorted_contexts($select, $params = array()) {
5068
 
5069
    //TODO: we should probably rewrite all the code that is using this thing, the trouble is we MUST NOT modify the context instances...
5070
 
5071
    global $DB;
5072
    if ($select) {
5073
        $select = 'WHERE ' . $select;
5074
    }
5075
    return $DB->get_records_sql("
5076
            SELECT ctx.*
5077
              FROM {context} ctx
5078
              LEFT JOIN {user} u ON ctx.contextlevel = " . CONTEXT_USER . " AND u.id = ctx.instanceid
5079
              LEFT JOIN {course_categories} cat ON ctx.contextlevel = " . CONTEXT_COURSECAT . " AND cat.id = ctx.instanceid
5080
              LEFT JOIN {course} c ON ctx.contextlevel = " . CONTEXT_COURSE . " AND c.id = ctx.instanceid
5081
              LEFT JOIN {course_modules} cm ON ctx.contextlevel = " . CONTEXT_MODULE . " AND cm.id = ctx.instanceid
5082
              LEFT JOIN {block_instances} bi ON ctx.contextlevel = " . CONTEXT_BLOCK . " AND bi.id = ctx.instanceid
5083
           $select
5084
          ORDER BY ctx.contextlevel, bi.defaultregion, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.defaultweight), u.lastname, u.firstname, cm.id
5085
            ", $params);
5086
}
5087
 
5088
/**
5089
 * Given context and array of users, returns array of users whose enrolment status is suspended,
5090
 * or enrolment has expired or has not started. Also removes those users from the given array
5091
 *
5092
 * @param context $context context in which suspended users should be extracted.
5093
 * @param array $users list of users.
5094
 * @param array $ignoreusers array of user ids to ignore, e.g. guest
5095
 * @return array list of suspended users.
5096
 */
5097
function extract_suspended_users($context, &$users, $ignoreusers=array()) {
5098
    global $DB;
5099
 
5100
    // Get active enrolled users.
5101
    list($sql, $params) = get_enrolled_sql($context, null, null, true);
5102
    $activeusers = $DB->get_records_sql($sql, $params);
5103
 
5104
    // Move suspended users to a separate array & remove from the initial one.
5105
    $susers = array();
5106
    if (sizeof($activeusers)) {
5107
        foreach ($users as $userid => $user) {
5108
            if (!array_key_exists($userid, $activeusers) && !in_array($userid, $ignoreusers)) {
5109
                $susers[$userid] = $user;
5110
                unset($users[$userid]);
5111
            }
5112
        }
5113
    }
5114
    return $susers;
5115
}
5116
 
5117
/**
5118
 * Given context and array of users, returns array of user ids whose enrolment status is suspended,
5119
 * or enrolment has expired or not started.
5120
 *
5121
 * @param context $context context in which user enrolment is checked.
5122
 * @param bool $usecache Enable or disable (default) the request cache
5123
 * @return array list of suspended user id's.
5124
 */
5125
function get_suspended_userids(context $context, $usecache = false) {
5126
    global $DB;
5127
 
5128
    if ($usecache) {
5129
        $cache = cache::make('core', 'suspended_userids');
5130
        $susers = $cache->get($context->id);
5131
        if ($susers !== false) {
5132
            return $susers;
5133
        }
5134
    }
5135
 
5136
    $coursecontext = $context->get_course_context();
5137
    $susers = array();
5138
 
5139
    // Front page users are always enrolled, so suspended list is empty.
5140
    if ($coursecontext->instanceid != SITEID) {
5141
        list($sql, $params) = get_enrolled_sql($context, null, null, false, true);
5142
        $susers = $DB->get_fieldset_sql($sql, $params);
5143
        $susers = array_combine($susers, $susers);
5144
    }
5145
 
5146
    // Cache results for the remainder of this request.
5147
    if ($usecache) {
5148
        $cache->set($context->id, $susers);
5149
    }
5150
 
5151
    return $susers;
5152
}
5153
 
5154
/**
5155
 * Gets sql for finding users with capability in the given context
5156
 *
5157
 * @param context $context
5158
 * @param string|array $capability Capability name or array of names.
5159
 *      If an array is provided then this is the equivalent of a logical 'OR',
5160
 *      i.e. the user needs to have one of these capabilities.
5161
 * @return array($sql, $params)
5162
 */
5163
function get_with_capability_sql(context $context, $capability) {
5164
    static $i = 0;
5165
    $i++;
5166
    $prefix = 'cu' . $i . '_';
5167
 
5168
    $capjoin = get_with_capability_join($context, $capability, $prefix . 'u.id');
5169
 
5170
    $sql = "SELECT DISTINCT {$prefix}u.id
5171
              FROM {user} {$prefix}u
5172
            $capjoin->joins
5173
             WHERE {$prefix}u.deleted = 0 AND $capjoin->wheres";
5174
 
5175
    return array($sql, $capjoin->params);
5176
}