Proyectos de Subversion Moodle

Rev

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