Proyectos de Subversion Moodle

Rev

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

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