accesslib.php 210 KB
Newer Older
stronk7's avatar
stronk7 committed
1
2
3
4
5
6
7
8
9
<?php // $Id$

///////////////////////////////////////////////////////////////////////////
//                                                                       //
// NOTICE OF COPYRIGHT                                                   //
//                                                                       //
// Moodle - Modular Object-Oriented Dynamic Learning Environment         //
//          http://moodle.org                                            //
//                                                                       //
10
// Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com     //
stronk7's avatar
stronk7 committed
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//                                                                       //
// This program is free software; you can redistribute it and/or modify  //
// it under the terms of the GNU General Public License as published by  //
// the Free Software Foundation; either version 2 of the License, or     //
// (at your option) any later version.                                   //
//                                                                       //
// This program is distributed in the hope that it will be useful,       //
// but WITHOUT ANY WARRANTY; without even the implied warranty of        //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         //
// GNU General Public License for more details:                          //
//                                                                       //
//          http://www.gnu.org/copyleft/gpl.html                         //
//                                                                       //
///////////////////////////////////////////////////////////////////////////

26
/**
27
 * Public API vs internals
28
 * -----------------------
29
 *
30
31
 * General users probably only care about
 *
32
33
34
35
36
 * Context handling
 * - get_context_instance()
 * - get_context_instance_by_id()
 * - get_parent_contexts()
 * - get_child_contexts()
37
 *
38
 * Whether the user can do something...
39
 * - has_capability()
40
41
 * - has_any_capability()
 * - has_all_capabilities()
42
 * - require_capability()
43
44
45
 * - require_login() (from moodlelib)
 *
 * What courses has this user access to?
46
 * - get_user_courses_bycap()
47
 *
48
49
50
 * What users can do X in this context?
 * - get_users_by_capability()
 *
51
 * Enrol/unenrol
52
53
 * - enrol_into_course()
 * - role_assign()/role_unassign()
54
 *
55
56
 *
 * Advanced use
57
58
 * - load_all_capabilities()
 * - reload_all_capabilities()
59
 * - $ACCESS global
60
 * - has_capability_in_accessdata()
61
62
 * - is_siteadmin()
 * - get_user_access_sitewide()
63
 * - load_subcontext()
64
65
66
67
 * - get_role_access_bycontext()
 *
 * Name conventions
 * ----------------
68
 *
69
 * - "ctx" means context
70
71
72
73
74
75
 *
 * accessdata
 * ----------
 *
 * Access control data is held in the "accessdata" array
 * which - for the logged-in user, will be in $USER->access
76
 *
77
78
79
 * For other users can be generated and passed around (but see
 * the $ACCESS global).
 *
80
 * $accessdata is a multidimensional array, holding
81
 * role assignments (RAs), role-capabilities-perm sets
82
 * (role defs) and a list of courses we have loaded
83
84
 * data for.
 *
85
 * Things are keyed on "contextpaths" (the path field of
86
 * the context table) for fast walking up/down the tree.
87
 *
88
89
 * $accessdata[ra][$contextpath]= array($roleid)
 *                [$contextpath]= array($roleid)
90
 *                [$contextpath]= array($roleid)
91
92
93
94
 *
 * Role definitions are stored like this
 * (no cap merge is done - so it's compact)
 *
95
96
97
 * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
 *                                        [mod/forum:editallpost] = -1
 *                                        [mod/forum:startdiscussion] = -1000
98
 *
99
 * See how has_capability_in_accessdata() walks up/down the tree.
100
101
102
103
104
 *
 * Normally - specially for the logged-in user, we only load
 * rdef and ra down to the course level, but not below. This
 * keeps accessdata small and compact. Below-the-course ra/rdef
 * are loaded as needed. We keep track of which courses we
105
 * have loaded ra/rdef in
106
 *
107
 * $accessdata[loaded] = array($contextpath, $contextpath)
108
109
110
111
112
113
114
115
116
 *
 * Stale accessdata
 * ----------------
 *
 * For the logged-in user, accessdata is long-lived.
 *
 * On each pageload we load $DIRTYPATHS which lists
 * context paths affected by changes. Any check at-or-below
 * a dirty context will trigger a transparent reload of accessdata.
117
 *
118
119
120
121
 * Changes at the sytem level will force the reload for everyone.
 *
 * Default role caps
 * -----------------
122
123
 * The default role assignment is not in the DB, so we
 * add it manually to accessdata.
124
125
126
 *
 * This means that functions that work directly off the
 * DB need to ensure that the default role caps
127
 * are dealt with appropriately.
128
 *
129
130
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 * @package roles
131
 */
132

vyshane's avatar
vyshane committed
133
134
require_once $CFG->dirroot.'/lib/blocklib.php';

135
// permission definitions
moodler's avatar
moodler committed
136
define('CAP_INHERIT', 0);
137
138
139
140
141
142
define('CAP_ALLOW', 1);
define('CAP_PREVENT', -1);
define('CAP_PROHIBIT', -1000);

// context definitions
define('CONTEXT_SYSTEM', 10);
143
define('CONTEXT_USER', 30);
144
145
146
147
148
149
define('CONTEXT_COURSECAT', 40);
define('CONTEXT_COURSE', 50);
define('CONTEXT_GROUP', 60);
define('CONTEXT_MODULE', 70);
define('CONTEXT_BLOCK', 80);

150
// capability risks - see http://docs.moodle.org/en/Development:Hardening_new_Roles_system
151
define('RISK_MANAGETRUST', 0x0001);
skodak's avatar
skodak committed
152
define('RISK_CONFIG',      0x0002);
153
154
155
define('RISK_XSS',         0x0004);
define('RISK_PERSONAL',    0x0008);
define('RISK_SPAM',        0x0010);
156
define('RISK_DATALOSS',    0x0020);
157

158
159
// rolename displays
define('ROLENAME_ORIGINAL', 0);// the name as defined in the role definition
160
define('ROLENAME_ALIAS', 1);   // the name as defined by a role alias
161
define('ROLENAME_BOTH', 2);    // Both, like this:  Role alias (Original)
162
163
define('ROLENAME_ORIGINALANDSHORT', 3); // the name as defined in the role definition and the shortname in brackets
define('ROLENAME_ALIAS_RAW', 4);   // the name as defined by a role alias, in raw form suitable for editing
164

moodler's avatar
moodler committed
165
166
$context_cache    = array();    // Cache of all used context objects for performance (by level and instance)
$context_cache_id = array();    // Index to above cache by id
167

skodak's avatar
skodak committed
168
169
$DIRTYCONTEXTS = null; // dirty contexts cache
$ACCESS = array(); // cache of caps for cron user switching and has_capability for other users (==not $USER)
170
$RDEFS = array(); // role definitions cache - helps a lot with mem usage in cron
171

172
function get_role_context_caps($roleid, $context) {
173
174
    global $DB;

175
176
177
    //this is really slow!!!! - do not use above course context level!
    $result = array();
    $result[$context->id] = array();
178

179
180
181
182
    // first emulate the parent context capabilities merging into context
    $searchcontexts = array_reverse(get_parent_contexts($context));
    array_push($searchcontexts, $context->id);
    foreach ($searchcontexts as $cid) {
183
        if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
184
185
186
187
188
189
190
191
            foreach ($capabilities as $cap) {
                if (!array_key_exists($cap->capability, $result[$context->id])) {
                    $result[$context->id][$cap->capability] = 0;
                }
                $result[$context->id][$cap->capability] += $cap->permission;
            }
        }
    }
192

193
    // now go through the contexts bellow given context
194
    $searchcontexts = array_keys(get_child_contexts($context));
195
    foreach ($searchcontexts as $cid) {
196
        if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
197
198
199
200
201
202
203
            foreach ($capabilities as $cap) {
                if (!array_key_exists($cap->contextid, $result)) {
                    $result[$cap->contextid] = array();
                }
                $result[$cap->contextid][$cap->capability] = $cap->permission;
            }
        }
204
205
    }

206
207
208
209
    return $result;
}

/**
210
 * Gets the accessdata for role "sitewide"
211
212
 * (system down to course)
 *
213
 * @return array
214
 */
215
function get_role_access($roleid, $accessdata=NULL) {
vyshane's avatar
vyshane committed
216

217
    global $CFG, $DB;
218

219
220
221
222
    /* Get it in 1 cheap DB query...
     * - relevant role caps at the root and down
     *   to the course level - but not below
     */
223
224
225
226
227
    if (is_null($accessdata)) {
        $accessdata           = array(); // named list
        $accessdata['ra']     = array();
        $accessdata['rdef']   = array();
        $accessdata['loaded'] = array();
228
229
    }

230
231
232
233
234
235
    //
    // Overrides for the role IN ANY CONTEXTS
    // down to COURSE - not below -
    //
    $sql = "SELECT ctx.path,
                   rc.capability, rc.permission
236
237
238
239
240
241
242
              FROM {context} ctx
              JOIN {role_capabilities} rc
                   ON rc.contextid=ctx.id
             WHERE rc.roleid = ?
                   AND ctx.contextlevel <= ".CONTEXT_COURSE."
          ORDER BY ctx.depth, ctx.path";
    $params = array($roleid);
243

244
245
    // we need extra caching in CLI scripts and cron
    if (CLI_SCRIPT) {
246
247
248
249
        static $cron_cache = array();

        if (!isset($cron_cache[$roleid])) {
            $cron_cache[$roleid] = array();
250
251
            if ($rs = $DB->get_recordset_sql($sql, $params)) {
                foreach ($rs as $rd) {
252
253
                    $cron_cache[$roleid][] = $rd;
                }
254
                $rs->close();
255
256
257
258
            }
        }

        foreach ($cron_cache[$roleid] as $rd) {
259
260
            $k = "{$rd->path}:{$roleid}";
            $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
261
        }
262

263
    } else {
264
265
        if ($rs = $DB->get_recordset_sql($sql, $params)) {
            foreach ($rs as $rd) {
266
267
268
269
                $k = "{$rd->path}:{$roleid}";
                $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
            }
            unset($rd);
270
            $rs->close();
271
        }
272
    }
273

274
    return $accessdata;
275
276
}

277
/**
278
 * Gets the accessdata for role "sitewide"
279
280
281
282
283
284
 * (system down to course)
 *
 * @return array
 */
function get_default_frontpage_role_access($roleid, $accessdata=NULL) {

285
    global $CFG, $DB;
286

287
288
    $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
    $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
289

290
291
292
293
294
    //
    // Overrides for the role in any contexts related to the course
    //
    $sql = "SELECT ctx.path,
                   rc.capability, rc.permission
295
296
297
298
299
300
301
302
              FROM {context} ctx
              JOIN {role_capabilities} rc
                   ON rc.contextid=ctx.id
             WHERE rc.roleid = ?
                   AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?)
                   AND ctx.contextlevel <= ".CONTEXT_COURSE."
          ORDER BY ctx.depth, ctx.path";
    $params = array($roleid, "$base/%");
303

304
305
    if ($rs = $DB->get_recordset_sql($sql, $params)) {
        foreach ($rs as $rd) {
306
307
            $k = "{$rd->path}:{$roleid}";
            $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
308
        }
309
        unset($rd);
310
        $rs->close();
311
312
313
314
315
316
    }

    return $accessdata;
}


317
318
319
320
321
/**
 * Get the default guest role
 * @return object role
 */
function get_guest_role() {
322
    global $CFG, $DB;
323
324
325
326
327
328
329
330
331
332

    if (empty($CFG->guestroleid)) {
        if ($roles = get_roles_with_capability('moodle/legacy:guest', CAP_ALLOW)) {
            $guestrole = array_shift($roles);   // Pick the first one
            set_config('guestroleid', $guestrole->id);
            return $guestrole;
        } else {
            debugging('Can not find any guest role!');
            return false;
        }
333
    } else {
334
        if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
335
336
337
338
339
340
            return $guestrole;
        } else {
            //somebody is messing with guest roles, remove incorrect setting and try to find a new one
            set_config('guestroleid', '');
            return get_guest_role();
        }
341
342
343
    }
}

skodak's avatar
skodak committed
344
345
346
347
348
349
350
351
352
/**
 * This function returns whether the current user has the capability of performing a function
 * For example, we can do has_capability('mod/forum:replypost',$context) in forum
 * @param string $capability - name of the capability (or debugcache or clearcache)
 * @param object $context - a context object (record from context table)
 * @param integer $userid - a userid number, empty if current $USER
 * @param bool $doanything - if false, ignore do anything
 * @return bool
 */
353
function has_capability($capability, $context, $userid=NULL, $doanything=true) {
354
355
356
357
358
359
360
361
362
363
    global $USER, $ACCESS, $CFG, $DIRTYCONTEXTS, $DB, $SCRIPT;

    if (empty($CFG->rolesactive)) {
        if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") {
            // we are in an installer - roles can not work yet
            return true;
        } else {
            return false;
        }
    }
364

365
    // the original $CONTEXT here was hiding serious errors
skodak's avatar
skodak committed
366
    // for security reasons do not reuse previous context
367
368
369
    if (empty($context)) {
        debugging('Incorrect context specified');
        return false;
370
    }
371

372
373
/// Some sanity checks
    if (debugging('',DEBUG_DEVELOPER)) {
374
375
        if (!is_valid_capability($capability)) {
            debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
376
        }
377
        if (!is_bool($doanything)) {
378
379
380
381
            debugging('Capability parameter "doanything" is wierd ("'.$doanything.'"). This should be fixed in code.');
        }
    }

skodak's avatar
skodak committed
382
    if (empty($userid)) { // we must accept null, 0, '0', '' etc. in $userid
383
384
385
        $userid = $USER->id;
    }

skodak's avatar
skodak committed
386
387
388
389
390
391
    if (is_null($context->path) or $context->depth == 0) {
        //this should not happen
        $contexts = array(SYSCONTEXTID, $context->id);
        $context->path = '/'.SYSCONTEXTID.'/'.$context->id;
        debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);

392
393
394
395
396
    } else {
        $contexts = explode('/', $context->path);
        array_shift($contexts);
    }

397
    if (CLI_SCRIPT && !isset($USER->access)) {
398
399
        // In cron, some modules setup a 'fake' $USER,
        // ensure we load the appropriate accessdata.
skodak's avatar
skodak committed
400
401
402
        if (isset($ACCESS[$userid])) {
            $DIRTYCONTEXTS = NULL; //load fresh dirty contexts
        } else {
403
            load_user_accessdata($userid);
skodak's avatar
skodak committed
404
            $DIRTYCONTEXTS = array();
405
406
        }
        $USER->access = $ACCESS[$userid];
407

skodak's avatar
skodak committed
408
    } else if ($USER->id == $userid && !isset($USER->access)) {
409
        // caps not loaded yet - better to load them to keep BC with 1.8
skodak's avatar
skodak committed
410
        // not-logged-in user or $USER object set up manually first time here
411
        load_all_capabilities();
skodak's avatar
skodak committed
412
        $ACCESS = array(); // reset the cache for other users too, the dirty contexts are empty now
413
        $RDEFS = array();
414
415
    }

skodak's avatar
skodak committed
416
    // Load dirty contexts list if needed
417
    if (!isset($DIRTYCONTEXTS)) {
418
419
420
421
422
423
        if (isset($USER->access['time'])) {
            $DIRTYCONTEXTS = get_dirty_contexts($USER->access['time']);
        }
        else {
            $DIRTYCONTEXTS = array();
        }
424
    }
skodak's avatar
skodak committed
425
426
427

    // Careful check for staleness...
    if (count($DIRTYCONTEXTS) !== 0 and is_contextpath_dirty($contexts, $DIRTYCONTEXTS)) {
428
429
430
        // reload all capabilities - preserving loginas, roleswitches, etc
        // and then cleanup any marks of dirtyness... at least from our short
        // term memory! :-)
skodak's avatar
skodak committed
431
        $ACCESS = array();
432
        $RDEFS = array();
skodak's avatar
skodak committed
433

434
        if (CLI_SCRIPT) {
skodak's avatar
skodak committed
435
436
437
438
439
440
441
            load_user_accessdata($userid);
            $USER->access = $ACCESS[$userid];
            $DIRTYCONTEXTS = array();

        } else {
            reload_all_capabilities();
        }
442
    }
skodak's avatar
skodak committed
443

444
445
446
    // divulge how many times we are called
    //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");

skodak's avatar
skodak committed
447
    if ($USER->id == $userid) { // we must accept strings and integers in $userid
448
449
450
451
452
453
454
455
456
        //
        // For the logged in user, we have $USER->access
        // which will have all RAs and caps preloaded for
        // course and above contexts.
        //
        // Contexts below courses && contexts that do not
        // hang from courses are loaded into $USER->access
        // on demand, and listed in $USER->access[loaded]
        //
457
458
        if ($context->contextlevel <= CONTEXT_COURSE) {
            // Course and above are always preloaded
459
            return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
460
        }
461
        // Load accessdata for below-the-course contexts
462
        if (!path_inaccessdata($context->path,$USER->access)) {
463
            // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
464
465
            // $bt = debug_backtrace();
            // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
466
            load_subcontext($USER->id, $context, $USER->access);
467
        }
468
        return has_capability_in_accessdata($capability, $context, $USER->access, $doanything);
469
    }
470

471
472
473
    if (!isset($ACCESS[$userid])) {
        load_user_accessdata($userid);
    }
474
475
    if ($context->contextlevel <= CONTEXT_COURSE) {
        // Course and above are always preloaded
476
        return has_capability_in_accessdata($capability, $context, $ACCESS[$userid], $doanything);
477
478
    }
    // Load accessdata for below-the-course contexts as needed
skodak's avatar
skodak committed
479
    if (!path_inaccessdata($context->path, $ACCESS[$userid])) {
480
        // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
481
482
        // $bt = debug_backtrace();
        // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
483
        load_subcontext($userid, $context, $ACCESS[$userid]);
484
    }
485
    return has_capability_in_accessdata($capability, $context, $ACCESS[$userid], $doanything);
486
487
}

488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
/**
 * This function returns whether the current user has any of the capabilities in the
 * $capabilities array. This is a simple wrapper around has_capability for convinience.
 *
 * There are probably tricks that could be done to improve the performance here, for example,
 * check the capabilities that are already cached first.
 *
 * @param array $capabilities - an array of capability names.
 * @param object $context - a context object (record from context table)
 * @param integer $userid - a userid number, empty if current $USER
 * @param bool $doanything - if false, ignore do anything
 * @return bool
 */
function has_any_capability($capabilities, $context, $userid=NULL, $doanything=true) {
    foreach ($capabilities as $capability) {
503
        if (has_capability($capability, $context, $userid, $doanything)) {
504
505
506
507
508
509
            return true;
        }
    }
    return false;
}

510
511
512
513
514
515
516
517
518
519
520
521
522
523
/**
 * This function returns whether the current user has all of the capabilities in the
 * $capabilities array. This is a simple wrapper around has_capability for convinience.
 *
 * There are probably tricks that could be done to improve the performance here, for example,
 * check the capabilities that are already cached first.
 *
 * @param array $capabilities - an array of capability names.
 * @param object $context - a context object (record from context table)
 * @param integer $userid - a userid number, empty if current $USER
 * @param bool $doanything - if false, ignore do anything
 * @return bool
 */
function has_all_capabilities($capabilities, $context, $userid=NULL, $doanything=true) {
524
525
526
527
    if (!is_array($capabilities)) {
        debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
        return false;
    }
528
529
530
531
532
533
534
535
    foreach ($capabilities as $capability) {
        if (!has_capability($capability, $context, $userid, $doanything)) {
            return false;
        }
    }
    return true;
}

skodak's avatar
skodak committed
536
/**
537
538
539
540
541
542
543
544
545
546
 * Uses 1 DB query to answer whether a user is an admin at the sitelevel.
 * It depends on DB schema >=1.7 but does not depend on the new datastructures
 * in v1.9 (context.path, or $USER->access)
 *
 * Will return true if the userid has any of
 *  - moodle/site:config
 *  - moodle/legacy:admin
 *  - moodle/site:doanything
 *
 * @param   int  $userid
skodak's avatar
skodak committed
547
 * @returns bool true is user can administer server settings
548
549
 */
function is_siteadmin($userid) {
550
    global $CFG, $DB;
551

552
    $sql = "SELECT SUM(rc.permission)
553
              FROM {role_capabilities} rc
554
              JOIN {context} ctx
555
556
557
558
559
                   ON ctx.id=rc.contextid
              JOIN {role_assignments} ra
                   ON ra.roleid=rc.roleid AND ra.contextid=ctx.id
             WHERE ctx.contextlevel=10
                   AND ra.userid=?
560
                   AND rc.capability IN (?, ?, ?)
561
          GROUP BY rc.capability
562
            HAVING SUM(rc.permission) > 0";
563
    $params = array($userid, 'moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');
564

skodak's avatar
skodak committed
565
    return $DB->record_exists_sql($sql, $params);
566
567
}

568
569
570
571
572
/**
 * @param integer $roleid a role id.
 * @return boolean, whether this role is an admin role.
 */
function is_admin_role($roleid) {
573
    global $DB;
574
575
576
577
578
579
580
581
582
583
584
585
586
587

    $sql = "SELECT 1
              FROM {role_capabilities} rc
              JOIN {context} ctx ON ctx.id = rc.contextid
             WHERE ctx.contextlevel = 10
                   AND rc.roleid = ?
                   AND rc.capability IN (?, ?, ?)
          GROUP BY rc.capability
            HAVING SUM(rc.permission) > 0";
    $params = array($roleid, 'moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');

    return $DB->record_exists_sql($sql, $params);
}

588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
/**
 * @return all the roles for which is_admin_role($role->id) is true.
 */
function get_admin_roles() {
    global $DB;

    $sql = "SELECT *
              FROM {role} r
             WHERE EXISTS (
                    SELECT 1
                      FROM {role_capabilities} rc
                      JOIN {context} ctx ON ctx.id = rc.contextid
                     WHERE ctx.contextlevel = 10
                           AND rc.roleid = r.id
                           AND rc.capability IN (?, ?, ?)
                  GROUP BY rc.capability
                    HAVING SUM(rc.permission) > 0
             )
          ORDER BY r.sortorder";
    $params = array('moodle/site:config', 'moodle/legacy:admin', 'moodle/site:doanything');

    return $DB->get_records_sql($sql, $params);
}

612
613
614
615
616
617
618
619
function get_course_from_path ($path) {
    // assume that nothing is more than 1 course deep
    if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
        return $matches[1];
    }
    return false;
}

620
function path_inaccessdata($path, $accessdata) {
621
622
623

    // assume that contexts hang from sys or from a course
    // this will only work well with stuff that hangs from a course
624
    if (in_array($path, $accessdata['loaded'], true)) {
stronk7's avatar
stronk7 committed
625
            // error_log("found it!");
626
627
628
629
630
631
632
633
        return true;
    }
    $base = '/' . SYSCONTEXTID;
    while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
        $path = $matches[1];
        if ($path === $base) {
            return false;
        }
634
        if (in_array($path, $accessdata['loaded'], true)) {
635
636
637
638
639
640
            return true;
        }
    }
    return false;
}

skodak's avatar
skodak committed
641
/**
642
 * Walk the accessdata array and return true/false.
643
644
645
646
 * Deals with prohibits, roleswitching, aggregating
 * capabilities, etc.
 *
 * The main feature of here is being FAST and with no
647
 * side effects.
648
 *
649
650
651
652
653
654
655
656
 * Notes:
 *
 * Switch Roles exits early
 * -----------------------
 * cap checks within a switchrole need to exit early
 * in our bottom up processing so they don't "see" that
 * there are real RAs that can do all sorts of things.
 *
657
658
659
660
661
662
663
 * Switch Role merges with default role
 * ------------------------------------
 * If you are a teacher in course X, you have at least
 * teacher-in-X + defaultloggedinuser-sitewide. So in the
 * course you'll have techer+defaultloggedinuser.
 * We try to mimic that in switchrole.
 *
664
665
666
667
668
669
670
671
672
 * Local-most role definition and role-assignment wins
 * ---------------------------------------------------
 * So if the local context has said 'allow', it wins
 * over a high-level context that says 'deny'.
 * This is applied when walking rdefs, and RAs.
 * Only at the same context the values are SUM()med.
 *
 * The exception is CAP_PROHIBIT.
 *
673
674
675
676
677
678
679
680
681
682
683
684
685
686
 * "Guest default role" exception
 * ------------------------------
 *
 * See MDL-7513 and $ignoreguest below for details.
 *
 * The rule is that
 *
 *    IF we are being asked about moodle/legacy:guest
 *                             OR moodle/course:view
 *    FOR a real, logged-in user
 *    AND we reached the top of the path in ra and rdef
 *    AND that role has moodle/legacy:guest === 1...
 *    THEN we act as if we hadn't seen it.
 *
687
 * Note that this function must be kept in synch with has_capability_in_accessdata.
688
689
 *
 * To Do:
690
691
692
693
694
 *
 * - Document how it works
 * - Rewrite in ASM :-)
 *
 */
695
function has_capability_in_accessdata($capability, $context, $accessdata, $doanything) {
696

697
698
    global $CFG;

699
700
701
702
703
704
705
706
707
    $path = $context->path;

    // build $contexts as a list of "paths" of the current
    // contexts and parents with the order top-to-bottom
    $contexts = array($path);
    while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
        $path = $matches[1];
        array_unshift($contexts, $path);
    }
708
709

    $ignoreguest = false;
710
    if (isset($accessdata['dr'])
711
712
713
714
        && ($capability    == 'moodle/course:view'
            || $capability == 'moodle/legacy:guest')) {
        // At the base, ignore rdefs where moodle/legacy:guest
        // is set
715
        $ignoreguest = $accessdata['dr'];
716
717
    }

718
719
    // Coerce it to an int
    $CAP_PROHIBIT = (int)CAP_PROHIBIT;
720

721
722
    $cc = count($contexts);

723
    $can = 0;
724
    $capdepth = 0;
725

726
727
728
    //
    // role-switches loop
    //
729
    if (isset($accessdata['rsw'])) {
730
        // check for isset() is fast
731
        // empty() is slow...
732
733
        if (empty($accessdata['rsw'])) {
            unset($accessdata['rsw']); // keep things fast and unambiguous
734
735
736
737
738
            break;
        }
        // From the bottom up...
        for ($n=$cc-1;$n>=0;$n--) {
            $ctxp = $contexts[$n];
739
            if (isset($accessdata['rsw'][$ctxp])) {
740
                // Found a switchrole assignment
741
                // check for that role _plus_ the default user role
742
                $ras = array($accessdata['rsw'][$ctxp],$CFG->defaultuserroleid);
743
                for ($rn=0;$rn<2;$rn++) {
744
                    $roleid = (int)$ras[$rn];
745
746
747
748
                    // Walk the path for capabilities
                    // from the bottom up...
                    for ($m=$cc-1;$m>=0;$m--) {
                        $capctxp = $contexts[$m];
749
                        if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
750
751
                            $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];

752
753
754
755
                            // The most local permission (first to set) wins
                            // the only exception is CAP_PROHIBIT
                            if ($can === 0) {
                                $can = $perm;
756
                            } elseif ($perm === $CAP_PROHIBIT) {
757
758
                                $can = $perm;
                                break;
759
                            }
760
761
762
763
                        }
                    }
                }
                // As we are dealing with a switchrole,
764
                // we return _here_, do _not_ walk up
765
766
767
768
                // the hierarchy any further
                if ($can < 1) {
                    if ($doanything) {
                        // didn't find it as an explicit cap,
769
                        // but maybe the user can doanything in this context...
770
                        return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
771
772
773
774
775
776
                    } else {
                        return false;
                    }
                } else {
                    return true;
                }
777

778
779
780
781
            }
        }
    }

782
783
784
785
    //
    // Main loop for normal RAs
    // From the bottom up...
    //
786
787
    for ($n=$cc-1;$n>=0;$n--) {
        $ctxp = $contexts[$n];
788
        if (isset($accessdata['ra'][$ctxp])) {
789
            // Found role assignments on this leaf
790
            $ras = $accessdata['ra'][$ctxp];
791
792
793
794

            $rc          = count($ras);
            $ctxcan      = 0;
            $ctxcapdepth = 0;
795
            for ($rn=0;$rn<$rc;$rn++) {
796
                $roleid  = (int)$ras[$rn];
797
                $rolecan = 0;
798
                $rolecapdepth = 0;
799
800
801
802
                // Walk the path for capabilities
                // from the bottom up...
                for ($m=$cc-1;$m>=0;$m--) {
                    $capctxp = $contexts[$m];
803
804
805
806
807
                    // ignore some guest caps
                    // at base ra and rdef
                    if ($ignoreguest == $roleid
                        && $n === 0
                        && $m === 0
808
809
                        && isset($accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'])
                        && $accessdata['rdef']["{$capctxp}:$roleid"]['moodle/legacy:guest'] > 0) {
810
811
                            continue;
                    }
812
                    if (isset($accessdata['rdef']["{$capctxp}:$roleid"][$capability])) {
813
                        $perm = (int)$accessdata['rdef']["{$capctxp}:$roleid"][$capability];
814
815
816
                        // The most local permission (first to set) wins
                        // the only exception is CAP_PROHIBIT
                        if ($rolecan === 0) {
817
818
819
820
821
                            $rolecan      = $perm;
                            $rolecapdepth = $m;
                        } elseif ($perm === $CAP_PROHIBIT) {
                            $rolecan      = $perm;
                            $rolecapdepth = $m;
822
                            break;
823
                        }
824
825
                    }
                }
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
                // Rules for RAs at the same context...
                // - prohibits always wins
                // - permissions at the same ctxlevel & capdepth are added together
                // - deeper capdepth wins
                if ($ctxcan === $CAP_PROHIBIT || $rolecan === $CAP_PROHIBIT) {
                    $ctxcan      = $CAP_PROHIBIT;
                    $ctxcapdepth = 0;
                } elseif ($ctxcapdepth === $rolecapdepth) {
                    $ctxcan += $rolecan;
                } elseif ($ctxcapdepth < $rolecapdepth) {
                    $ctxcan      = $rolecan;
                    $ctxcapdepth = $rolecapdepth;
                } else { // ctxcaptdepth is deeper
                    // rolecap ignored
                }
841
842
843
844
            }
            // The most local RAs with a defined
            // permission ($ctxcan) win, except
            // for CAP_PROHIBIT
845
846
847
848
849
            // NOTE: If we want the deepest RDEF to
            // win regardless of the depth of the RA,
            // change the elseif below to read
            // ($can === 0 || $capdepth < $ctxcapdepth) {
            if ($ctxcan === $CAP_PROHIBIT) {
850
851
                $can = $ctxcan;
                break;
852
853
854
            } elseif ($can === 0) { // see note above
                $can      = $ctxcan;
                $capdepth = $ctxcapdepth;
855
856
857
858
859
860
861
            }
        }
    }

    if ($can < 1) {
        if ($doanything) {
            // didn't find it as an explicit cap,
862
            // but maybe the user can doanything in this context...
863
            return has_capability_in_accessdata('moodle/site:doanything', $context, $accessdata, false);
864
865
866
867
868
869
870
871
        } else {
            return false;
        }
    } else {
        return true;
    }

}
872

873
function aggregate_roles_from_accessdata($context, $accessdata) {
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890

    $path = $context->path;

    // build $contexts as a list of "paths" of the current
    // contexts and parents with the order top-to-bottom
    $contexts = array($path);
    while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
        $path = $matches[1];
        array_unshift($contexts, $path);
    }

    $cc = count($contexts);

    $roles = array();
    // From the bottom up...
    for ($n=$cc-1;$n>=0;$n--) {
        $ctxp = $contexts[$n];
891
        if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
892
            // Found assignments on this leaf
893
            $addroles = $accessdata['ra'][$ctxp];
894
            $roles    = array_merge($roles, $addroles);
895
896
897
898
899
900
        }
    }

    return array_unique($roles);
}

moodler's avatar
moodler committed
901
/**
902
903
 * This is an easy to use function, combining has_capability() with require_course_login().
 * And will call those where needed.
904
 *
905
906
907
908
909
910
 * It checks for a capability assertion being true.  If it isn't
 * then the page is terminated neatly with a standard error message.
 *
 * If the user is not logged in, or is using 'guest' access or other special "users,
 * it provides a logon prompt.
 *
moodler's avatar
moodler committed
911
912
913
 * @param string $capability - name of the capability
 * @param object $context - a context object (record from context table)
 * @param integer $userid - a userid number
914
 * @param bool $doanything - if false, ignore do anything
moodler's avatar
moodler committed
915
 * @param string $errorstring - an errorstring
916
 * @param string $stringfile - which stringfile to get it from
moodler's avatar
moodler committed
917
 */
skodak's avatar
skodak committed
918
function require_capability($capability, $context, $userid=NULL, $doanything=true,
919
                            $errormessage='nopermissions', $stringfile='') {
moodler's avatar
moodler committed
920

921
    global $USER, $CFG, $DB;
moodler's avatar
moodler committed
922

skodak's avatar
skodak committed
923
924
925
926
927
    /* Empty $userid means current user, if the current user is not logged in,
     * then make sure they are (if needed).
     * Originally there was a check for loaded permissions - it is not needed here.
     * Context is now required parameter, the cached $CONTEXT was only hiding errors.
     */
928
929
    $errorlink = '';

skodak's avatar
skodak committed
930
931
    if (empty($userid)) {
        if ($context->contextlevel == CONTEXT_COURSE) {
moodler's avatar
moodler committed
932
            require_login($context->instanceid);
933

skodak's avatar
skodak committed
934
        } else if ($context->contextlevel == CONTEXT_MODULE) {
935
            if (!$cm = $DB->get_record('course_modules', array('id'=>$context->instanceid))) {
dongsheng's avatar
dongsheng committed
936
                print_error('invalidmodule');
937
            }
938
            if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
dongsheng's avatar
dongsheng committed
939
                print_error('invalidcourseid');
skodak's avatar
skodak committed
940
941
            }
            require_course_login($course, true, $cm);
942
            $errorlink = $CFG->wwwroot.'/course/view.php?id='.$cm->course;
skodak's avatar
skodak committed
943
944

        } else if ($context->contextlevel == CONTEXT_SYSTEM) {
945
946
947
948
            if (!empty($CFG->forcelogin)) {
                require_login();
            }

moodler's avatar
moodler committed
949
950
951
952
        } else {
            require_login();
        }
    }
953

moodler's avatar
moodler committed
954
955
/// OK, if they still don't have the capability then print a nice error message

956
    if (!has_capability($capability, $context, $userid, $doanything)) {
moodler's avatar
moodler committed
957
        $capabilityname = get_capability_string($capability);
dongsheng's avatar
dongsheng committed
958
        print_error('nopermissions', '', $errorlink, $capabilityname);
moodler's avatar
moodler committed
959
960
961
    }
}

skodak's avatar
skodak committed
962
/**
963
 * Get an array of courses (with magic extra bits)
964
 * where the accessdata and in DB enrolments show
965
 * that the cap requested is available.
966
967
968
969
970
971
972
973
974
975
976
977
 *
 * The main use is for get_my_courses().
 *
 * Notes
 *
 * - $fields is an array of fieldnames to ADD
 *   so name the fields you really need, which will
 *   be added and uniq'd
 *
 * - the course records have $c->context which is a fully
 *   valid context object. Saves you a query per course!
 *
978
979
980
 * - the course records have $c->categorypath to make
 *   category lookups cheap
 *
981
982
983
984
 * - current implementation is split in -
 *
 *   - if the user has the cap systemwide, stupidly
 *     grab *every* course for a capcheck. This eats
985
 *     a TON of bandwidth, specially on large sites
986
987
988
989
990
991
992
993
994
995
996
997
 *     with separate DBs...
 *
 *   - otherwise, fetch "likely" courses with a wide net
 *     that should get us _cheaply_ at least the courses we need, and some
 *     we won't - we get courses that...
 *      - are in a category where user has the cap
 *      - or where use has a role-assignment (any kind)
 *      - or where the course has an override on for this cap
 *
 *   - walk the courses recordset checking the caps oneach one
 *     the checks are all in memory and quite fast
 *     (though we could implement a specialised variant of the
998
 *     has_capability_in_accessdata() code to speed it up)
999
1000
 *
 * @param string $capability - name of the capability