Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16
 
17
namespace core;
18
 
19
use stdClass, IteratorAggregate, ArrayIterator;
20
use coding_exception, moodle_url;
21
 
22
/**
23
 * Basic moodle context abstraction class.
24
 *
25
 * Google confirms that no other important framework is using "context" class,
26
 * we could use something else like mcontext or moodle_context, but we need to type
27
 * this very often which would be annoying and it would take too much space...
28
 *
29
 * This class is derived from stdClass for backwards compatibility with
30
 * odl $context record that was returned from DML $DB->get_record()
31
 *
32
 * @package   core_access
33
 * @category  access
34
 * @copyright Petr Skoda
35
 * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 * @since     Moodle 4.2
37
 *
38
 * @property-read int $id context id
39
 * @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc.
40
 * @property-read int $instanceid id of related instance in each context
41
 * @property-read string $path path to context, starts with system context
42
 * @property-read int $depth
43
 * @property-read bool $locked true means write capabilities are ignored in this context or parents
44
 */
45
abstract class context extends stdClass implements IteratorAggregate {
46
 
47
    /** @var string Default sorting of capabilities in {@see get_capabilities} */
48
    protected const DEFAULT_CAPABILITY_SORT = 'contextlevel, component, name';
49
 
50
    /**
51
     * The context id
52
     * Can be accessed publicly through $context->id
53
     * @var int
54
     */
55
    protected $_id;
56
 
57
    /**
58
     * The context level
59
     * Can be accessed publicly through $context->contextlevel
60
     * @var int One of CONTEXT_* e.g. CONTEXT_COURSE, CONTEXT_MODULE
61
     */
62
    protected $_contextlevel;
63
 
64
    /**
65
     * Id of the item this context is related to e.g. COURSE_CONTEXT => course.id
66
     * Can be accessed publicly through $context->instanceid
67
     * @var int
68
     */
69
    protected $_instanceid;
70
 
71
    /**
72
     * The path to the context always starting from the system context
73
     * Can be accessed publicly through $context->path
74
     * @var string
75
     */
76
    protected $_path;
77
 
78
    /**
79
     * The depth of the context in relation to parent contexts
80
     * Can be accessed publicly through $context->depth
81
     * @var int
82
     */
83
    protected $_depth;
84
 
85
    /**
86
     * Whether this context is locked or not.
87
     *
88
     * Can be accessed publicly through $context->locked.
89
     *
90
     * @var int
91
     */
92
    protected $_locked;
93
 
94
    /**
95
     * @var array Context caching info
96
     */
97
    private static $cache_contextsbyid = array();
98
 
99
    /**
100
     * @var array Context caching info
101
     */
102
    private static $cache_contexts = array();
103
 
104
    /**
105
     * Context count
106
     * Why do we do count contexts? Because count($array) is horribly slow for large arrays
107
     * @var int
108
     */
109
    protected static $cache_count = 0;
110
 
111
    /**
112
     * @var array Context caching info
113
     */
114
    protected static $cache_preloaded = array();
115
 
116
    /**
117
     * @var context\system The system context once initialised
118
     */
119
    protected static $systemcontext = null;
120
 
121
    /**
122
     * Returns short context name.
123
     *
124
     * @since Moodle 4.2
125
     *
126
     * @return string
127
     */
128
    public static function get_short_name(): string {
129
        // NOTE: it would be more correct to make this abstract,
130
        // unfortunately there are tests that attempt to mock context classes.
131
        throw new \coding_exception('get_short_name() method must be overridden in custom context levels');
132
    }
133
 
134
    /**
135
     * Resets the cache to remove all data.
136
     */
137
    protected static function reset_caches() {
138
        self::$cache_contextsbyid = array();
139
        self::$cache_contexts = array();
140
        self::$cache_count = 0;
141
        self::$cache_preloaded = array();
142
 
143
        self::$systemcontext = null;
144
    }
145
 
146
    /**
147
     * Adds a context to the cache. If the cache is full, discards a batch of
148
     * older entries.
149
     *
150
     * @param context $context New context to add
151
     * @return void
152
     */
153
    protected static function cache_add(context $context) {
154
        if (isset(self::$cache_contextsbyid[$context->id])) {
155
            // Already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
156
            return;
157
        }
158
 
159
        if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) {
160
            $i = 0;
161
            foreach (self::$cache_contextsbyid as $ctx) {
162
                $i++;
163
                if ($i <= 100) {
164
                    // We want to keep the first contexts to be loaded on this page, hopefully they will be needed again later.
165
                    continue;
166
                }
167
                if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
168
                    // We remove oldest third of the contexts to make room for more contexts.
169
                    break;
170
                }
171
                unset(self::$cache_contextsbyid[$ctx->id]);
172
                unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
173
                self::$cache_count--;
174
            }
175
        }
176
 
177
        self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
178
        self::$cache_contextsbyid[$context->id] = $context;
179
        self::$cache_count++;
180
    }
181
 
182
    /**
183
     * Removes a context from the cache.
184
     *
185
     * @param context $context Context object to remove
186
     * @return void
187
     */
188
    protected static function cache_remove(context $context) {
189
        if (!isset(self::$cache_contextsbyid[$context->id])) {
190
            // Not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
191
            return;
192
        }
193
        unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
194
        unset(self::$cache_contextsbyid[$context->id]);
195
 
196
        self::$cache_count--;
197
 
198
        if (self::$cache_count < 0) {
199
            self::$cache_count = 0;
200
        }
201
    }
202
 
203
    /**
204
     * Gets a context from the cache.
205
     *
206
     * @param int $contextlevel Context level
207
     * @param int $instance Instance ID
208
     * @return context|bool Context or false if not in cache
209
     */
210
    protected static function cache_get($contextlevel, $instance) {
211
        if (isset(self::$cache_contexts[$contextlevel][$instance])) {
212
            return self::$cache_contexts[$contextlevel][$instance];
213
        }
214
        return false;
215
    }
216
 
217
    /**
218
     * Gets a context from the cache based on its id.
219
     *
220
     * @param int $id Context ID
221
     * @return context|bool Context or false if not in cache
222
     */
223
    protected static function cache_get_by_id($id) {
224
        if (isset(self::$cache_contextsbyid[$id])) {
225
            return self::$cache_contextsbyid[$id];
226
        }
227
        return false;
228
    }
229
 
230
    /**
231
     * Preloads context information from db record and strips the cached info.
232
     *
233
     * @param stdClass $rec
234
     * @return context|null (modifies $rec)
235
     */
236
    protected static function preload_from_record(stdClass $rec) {
237
        $notenoughdata = false;
238
        $notenoughdata = $notenoughdata || empty($rec->ctxid);
239
        $notenoughdata = $notenoughdata || empty($rec->ctxlevel);
240
        $notenoughdata = $notenoughdata || !isset($rec->ctxinstance);
241
        $notenoughdata = $notenoughdata || empty($rec->ctxpath);
242
        $notenoughdata = $notenoughdata || empty($rec->ctxdepth);
243
        $notenoughdata = $notenoughdata || !isset($rec->ctxlocked);
244
        if ($notenoughdata) {
245
            // The record does not have enough data, passed here repeatedly or context does not exist yet.
246
            if (isset($rec->ctxid) && !isset($rec->ctxlocked)) {
247
                debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
248
            }
249
            return null;
250
        }
251
 
252
        $record = (object) [
253
            'id' => $rec->ctxid,
254
            'contextlevel' => $rec->ctxlevel,
255
            'instanceid' => $rec->ctxinstance,
256
            'path' => $rec->ctxpath,
257
            'depth' => $rec->ctxdepth,
258
            'locked' => $rec->ctxlocked,
259
        ];
260
 
261
        unset($rec->ctxid);
262
        unset($rec->ctxlevel);
263
        unset($rec->ctxinstance);
264
        unset($rec->ctxpath);
265
        unset($rec->ctxdepth);
266
        unset($rec->ctxlocked);
267
 
268
        return self::create_instance_from_record($record);
269
    }
270
 
271
 
272
    /* ====== magic methods ======= */
273
 
274
    /**
275
     * Magic setter method, we do not want anybody to modify properties from the outside
276
     * @param string $name
277
     * @param mixed $value
278
     */
279
    public function __set($name, $value) {
280
        debugging('Can not change context instance properties!');
281
    }
282
 
283
    /**
284
     * Magic method getter, redirects to read only values.
285
     * @param string $name
286
     * @return mixed
287
     */
288
    public function __get($name) {
289
        switch ($name) {
290
            case 'id':
291
                return $this->_id;
292
            case 'contextlevel':
293
                return $this->_contextlevel;
294
            case 'instanceid':
295
                return $this->_instanceid;
296
            case 'path':
297
                return $this->_path;
298
            case 'depth':
299
                return $this->_depth;
300
            case 'locked':
301
                return $this->is_locked();
302
 
303
            default:
304
                debugging('Invalid context property accessed! '.$name);
305
                return null;
306
        }
307
    }
308
 
309
    /**
310
     * Full support for isset on our magic read only properties.
311
     * @param string $name
312
     * @return bool
313
     */
314
    public function __isset($name) {
315
        switch ($name) {
316
            case 'id':
317
                return isset($this->_id);
318
            case 'contextlevel':
319
                return isset($this->_contextlevel);
320
            case 'instanceid':
321
                return isset($this->_instanceid);
322
            case 'path':
323
                return isset($this->_path);
324
            case 'depth':
325
                return isset($this->_depth);
326
            case 'locked':
327
                // Locked is always set.
328
                return true;
329
            default:
330
                return false;
331
        }
332
    }
333
 
334
    /**
335
     * All properties are read only, sorry.
336
     * @param string $name
337
     */
338
    public function __unset($name) {
339
        debugging('Can not unset context instance properties!');
340
    }
341
 
342
    /* ====== implementing method from interface IteratorAggregate ====== */
343
 
344
    /**
345
     * Create an iterator because magic vars can't be seen by 'foreach'.
346
     *
347
     * Now we can convert context object to array using convert_to_array(),
348
     * and feed it properly to json_encode().
349
     */
350
    public function getIterator(): \Traversable {
351
        $ret = array(
352
            'id' => $this->id,
353
            'contextlevel' => $this->contextlevel,
354
            'instanceid' => $this->instanceid,
355
            'path' => $this->path,
356
            'depth' => $this->depth,
357
            'locked' => $this->locked,
358
        );
359
        return new ArrayIterator($ret);
360
    }
361
 
362
    /* ====== general context methods ====== */
363
 
364
    /**
365
     * Constructor is protected so that devs are forced to
366
     * use context_xxx::instance() or context::instance_by_id().
367
     *
368
     * @param stdClass $record
369
     */
370
    protected function __construct(stdClass $record) {
371
        $this->_id = (int)$record->id;
372
        $this->_contextlevel = (int)$record->contextlevel;
373
        $this->_instanceid = $record->instanceid;
374
        $this->_path = $record->path;
375
        $this->_depth = $record->depth;
376
 
377
        if (isset($record->locked)) {
378
            $this->_locked = $record->locked;
379
        } else if (!during_initial_install() && !moodle_needs_upgrading()) {
380
            debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
381
        }
382
    }
383
 
384
    /**
385
     * This function is also used to work around 'protected' keyword problems in context_helper.
386
     *
387
     * @param stdClass $record
388
     * @return context instance
389
     */
390
    protected static function create_instance_from_record(stdClass $record) {
391
        $classname = context_helper::get_class_for_level($record->contextlevel);
392
 
393
        if ($context = self::cache_get_by_id($record->id)) {
394
            return $context;
395
        }
396
 
397
        $context = new $classname($record);
398
        self::cache_add($context);
399
 
400
        return $context;
401
    }
402
 
403
    /**
404
     * Copy prepared new contexts from temp table to context table,
405
     * we do this in db specific way for perf reasons only.
406
     */
407
    protected static function merge_context_temp_table() {
408
        global $DB;
409
 
410
        /* MDL-11347:
411
         *  - mysql does not allow to use FROM in UPDATE statements
412
         *  - using two tables after UPDATE works in mysql, but might give unexpected
413
         *    results in pg 8 (depends on configuration)
414
         *  - using table alias in UPDATE does not work in pg < 8.2
415
         *
416
         * Different code for each database - mostly for performance reasons
417
         */
418
 
419
        $dbfamily = $DB->get_dbfamily();
420
        if ($dbfamily == 'mysql') {
421
            $updatesql = "UPDATE {context} ct, {context_temp} temp
422
                             SET ct.path = temp.path,
423
                                 ct.depth = temp.depth,
424
                                 ct.locked = temp.locked
425
                           WHERE ct.id = temp.id";
426
        } else if ($dbfamily == 'oracle') {
427
            $updatesql = "UPDATE {context} ct
428
                             SET (ct.path, ct.depth, ct.locked) =
429
                                 (SELECT temp.path, temp.depth, temp.locked
430
                                    FROM {context_temp} temp
431
                                   WHERE temp.id=ct.id)
432
                           WHERE EXISTS (SELECT 'x'
433
                                           FROM {context_temp} temp
434
                                           WHERE temp.id = ct.id)";
435
        } else if ($dbfamily == 'postgres' || $dbfamily == 'mssql') {
436
            $updatesql = "UPDATE {context}
437
                             SET path = temp.path,
438
                                 depth = temp.depth,
439
                                 locked = temp.locked
440
                            FROM {context_temp} temp
441
                           WHERE temp.id={context}.id";
442
        } else {
443
            // Sqlite and others.
444
            $updatesql = "UPDATE {context}
445
                             SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
446
                                 depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id),
447
                                 locked = (SELECT locked FROM {context_temp} WHERE id = {context}.id)
448
                             WHERE id IN (SELECT id FROM {context_temp})";
449
        }
450
 
451
        $DB->execute($updatesql);
452
    }
453
 
454
    /**
455
     * Get a context instance as an object, from a given context id.
456
     *
457
     * @param int $id context id
458
     * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
459
     *                        MUST_EXIST means throw exception if no record found
460
     * @return context|bool the context object or false if not found
461
     */
462
    public static function instance_by_id($id, $strictness = MUST_EXIST) {
463
        global $DB;
464
 
465
        if (get_called_class() !== 'core\context' && get_called_class() !== 'core\context_helper') {
466
            // Some devs might confuse context->id and instanceid, better prevent these mistakes completely.
467
            throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
468
        }
469
 
470
        if ($id == SYSCONTEXTID) {
471
            return context\system::instance(0, $strictness);
472
        }
473
 
474
        if (is_array($id) || is_object($id) || empty($id)) {
475
            throw new coding_exception('Invalid context id specified context::instance_by_id()');
476
        }
477
 
478
        if ($context = self::cache_get_by_id($id)) {
479
            return $context;
480
        }
481
 
482
        if ($record = $DB->get_record('context', array('id' => $id), '*', $strictness)) {
483
            return self::create_instance_from_record($record);
484
        }
485
 
486
        return false;
487
    }
488
 
489
    /**
490
     * Update context info after moving context in the tree structure.
491
     *
492
     * @param context $newparent
493
     * @return void
494
     */
495
    public function update_moved(context $newparent) {
496
        global $DB;
497
 
498
        $frompath = $this->_path;
499
        $newpath = $newparent->path . '/' . $this->_id;
500
 
501
        $trans = $DB->start_delegated_transaction();
502
 
503
        $setdepth = '';
504
        if (($newparent->depth + 1) != $this->_depth) {
505
            $diff = $newparent->depth - $this->_depth + 1;
506
            $setdepth = ", depth = depth + $diff";
507
        }
508
        $sql = "UPDATE {context}
509
                   SET path = ?
510
                       $setdepth
511
                 WHERE id = ?";
512
        $params = array($newpath, $this->_id);
513
        $DB->execute($sql, $params);
514
 
515
        $this->_path = $newpath;
516
        $this->_depth = $newparent->depth + 1;
517
 
518
        $sql = "UPDATE {context}
519
                   SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath) + 1))."
520
                       $setdepth
521
                 WHERE path LIKE ?";
522
        $params = array($newpath, "{$frompath}/%");
523
        $DB->execute($sql, $params);
524
 
525
        $this->mark_dirty();
526
 
527
        self::reset_caches();
528
 
529
        $trans->allow_commit();
530
    }
531
 
532
    /**
533
     * Set whether this context has been locked or not.
534
     *
535
     * @param   bool    $locked
536
     * @return  $this
537
     */
538
    public function set_locked(bool $locked) {
539
        global $DB;
540
 
541
        if ($this->_locked == $locked) {
542
            return $this;
543
        }
544
 
545
        $this->_locked = $locked;
546
        $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
547
        $this->mark_dirty();
548
 
549
        if ($locked) {
550
            $eventname = '\\core\\event\\context_locked';
551
        } else {
552
            $eventname = '\\core\\event\\context_unlocked';
553
        }
554
        $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
555
        $event->trigger();
556
 
557
        self::reset_caches();
558
 
559
        return $this;
560
    }
561
 
562
    /**
563
     * Remove all context path info and optionally rebuild it.
564
     *
565
     * @param bool $rebuild
566
     * @return void
567
     */
568
    public function reset_paths($rebuild = true) {
569
        global $DB;
570
 
571
        if ($this->_path) {
572
            $this->mark_dirty();
573
        }
574
        $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
575
        $DB->set_field_select('context', 'path', null, "path LIKE '%/$this->_id/%'");
576
        if ($this->_contextlevel != CONTEXT_SYSTEM) {
577
            $DB->set_field('context', 'depth', 0, array('id' => $this->_id));
578
            $DB->set_field('context', 'path', null, array('id' => $this->_id));
579
            $this->_depth = 0;
580
            $this->_path = null;
581
        }
582
 
583
        if ($rebuild) {
584
            context_helper::build_all_paths(false);
585
        }
586
 
587
        self::reset_caches();
588
    }
589
 
590
    /**
591
     * Delete all data linked to content, do not delete the context record itself
592
     */
593
    public function delete_content() {
594
        global $CFG, $DB;
595
 
596
        blocks_delete_all_for_context($this->_id);
597
        filter_delete_all_for_context($this->_id);
598
 
599
        require_once($CFG->dirroot . '/comment/lib.php');
600
        \comment::delete_comments(array('contextid' => $this->_id));
601
 
602
        require_once($CFG->dirroot.'/rating/lib.php');
603
        $delopt = new stdclass();
604
        $delopt->contextid = $this->_id;
605
        $rm = new \rating_manager();
606
        $rm->delete_ratings($delopt);
607
 
608
        // Delete all files attached to this context.
609
        $fs = get_file_storage();
610
        $fs->delete_area_files($this->_id);
611
 
612
        // Delete all repository instances attached to this context.
613
        require_once($CFG->dirroot . '/repository/lib.php');
614
        \repository::delete_all_for_context($this->_id);
615
 
616
        // Delete all advanced grading data attached to this context.
617
        require_once($CFG->dirroot.'/grade/grading/lib.php');
618
        \grading_manager::delete_all_for_context($this->_id);
619
 
620
        // Now delete stuff from role related tables, role_unassign_all
621
        // and unenrol should be called earlier to do proper cleanup.
622
        $DB->delete_records('role_assignments', array('contextid' => $this->_id));
623
        $DB->delete_records('role_names', array('contextid' => $this->_id));
624
        $this->delete_capabilities();
625
    }
626
 
627
    /**
628
     * Unassign all capabilities from a context.
629
     */
630
    public function delete_capabilities() {
631
        global $DB;
632
 
633
        $ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
634
        if ($ids) {
635
            $DB->delete_records('role_capabilities', array('contextid' => $this->_id));
636
 
637
            // Reset any cache of these roles, including MUC.
638
            accesslib_clear_role_cache($ids);
639
        }
640
    }
641
 
642
    /**
643
     * Delete the context content and the context record itself
644
     */
645
    public function delete() {
646
        global $DB;
647
 
648
        if ($this->_contextlevel <= CONTEXT_SYSTEM) {
649
            throw new coding_exception('Cannot delete system context');
650
        }
651
 
652
        // Double check the context still exists.
653
        if (!$DB->record_exists('context', array('id' => $this->_id))) {
654
            self::cache_remove($this);
655
            return;
656
        }
657
 
658
        $this->delete_content();
659
        $DB->delete_records('context', array('id' => $this->_id));
660
        // Purge static context cache if entry present.
661
        self::cache_remove($this);
662
 
663
        // Inform search engine to delete data related to this context.
664
        \core_search\manager::context_deleted($this);
665
    }
666
 
667
    /* ====== context level related methods ====== */
668
 
669
    /**
670
     * Utility method for context creation
671
     *
672
     * @param int $contextlevel
673
     * @param int $instanceid
674
     * @param string $parentpath
675
     * @return stdClass context record
676
     */
677
    protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
678
        global $DB;
679
 
680
        $record = new stdClass();
681
        $record->contextlevel = $contextlevel;
682
        $record->instanceid = $instanceid;
683
        $record->depth = 0;
684
        $record->path = null; // Not known before insert.
685
        $record->locked = 0;
686
 
687
        $record->id = $DB->insert_record('context', $record);
688
 
689
        // Now add path if known - it can be added later.
690
        if (!is_null($parentpath)) {
691
            $record->path = $parentpath.'/'.$record->id;
692
            $record->depth = substr_count($record->path, '/');
693
            $DB->update_record('context', $record);
694
        }
695
 
696
        return $record;
697
    }
698
 
699
    /**
700
     * Returns human readable context identifier.
701
     *
702
     * @param boolean $withprefix whether to prefix the name of the context with the
703
     *      type of context, e.g. User, Course, Forum, etc.
704
     * @param boolean $short whether to use the short name of the thing. Only applies
705
     *      to course contexts
706
     * @param boolean $escape Whether the returned name of the thing is to be
707
     *      HTML escaped or not.
708
     * @return string the human readable context name.
709
     */
710
    public function get_context_name($withprefix = true, $short = false, $escape = true) {
711
        // Must be implemented in all context levels.
712
        throw new coding_exception('can not get name of abstract context');
713
    }
714
 
715
    /**
716
     * Whether the current context is locked.
717
     *
718
     * @return  bool
719
     */
720
    public function is_locked() {
721
        if ($this->_locked) {
722
            return true;
723
        }
724
 
725
        if ($parent = $this->get_parent_context()) {
726
            return $parent->is_locked();
727
        }
728
 
729
        return false;
730
    }
731
 
732
    /**
733
     * Returns the most relevant URL for this context.
734
     *
735
     * @return moodle_url
736
     */
737
    abstract public function get_url();
738
 
739
    /**
740
     * Returns context instance database name.
741
     *
742
     * @return string|null table name for all levels except system.
743
     */
744
    protected static function get_instance_table(): ?string {
745
        return null;
746
    }
747
 
748
    /**
749
     * Returns list of columns that can be used from behat
750
     * to look up context by reference.
751
     *
752
     * @return array list of column names from instance table
753
     */
754
    protected static function get_behat_reference_columns(): array {
755
        return [];
756
    }
757
 
758
    /**
759
     * Returns list of all role archetypes that are compatible
760
     * with role assignments in context level.
761
     * @since Moodle 4.2
762
     *
763
     * @return string[]
764
     */
765
    protected static function get_compatible_role_archetypes(): array {
766
        // Override if archetype roles should be allowed to be assigned in context level.
767
        return [];
768
    }
769
 
770
    /**
771
     * Returns list of all possible parent context levels,
772
     * it may include itself if nesting is allowed.
773
     * @since Moodle 4.2
774
     *
775
     * @return int[]
776
     */
777
    public static function get_possible_parent_levels(): array {
778
        // Override if other type of parents are expected.
779
        return [context\system::LEVEL];
780
    }
781
 
782
    /**
783
     * Returns array of relevant context capability records.
784
     *
785
     * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display
786
     * @return array
787
     */
788
    abstract public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT);
789
 
790
    /**
791
     * Recursive function which, given a context, find all its children context ids.
792
     *
793
     * For course category contexts it will return immediate children and all subcategory contexts.
794
     * It will NOT recurse into courses or subcategories categories.
795
     * If you want to do that, call it on the returned courses/categories.
796
     *
797
     * When called for a course context, it will return the modules and blocks
798
     * displayed in the course page and blocks displayed on the module pages.
799
     *
800
     * If called on a user/course/module context it _will_ populate the cache with the appropriate
801
     * contexts ;-)
802
     *
803
     * @return array Array of child records
804
     */
805
    public function get_child_contexts() {
806
        global $DB;
807
 
808
        if (empty($this->_path) || empty($this->_depth)) {
809
            debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
810
            return array();
811
        }
812
 
813
        $sql = "SELECT ctx.*
814
                  FROM {context} ctx
815
                 WHERE ctx.path LIKE ?";
816
        $params = array($this->_path.'/%');
817
        $records = $DB->get_records_sql($sql, $params);
818
 
819
        $result = array();
820
        foreach ($records as $record) {
821
            $result[$record->id] = self::create_instance_from_record($record);
822
        }
823
 
824
        return $result;
825
    }
826
 
827
    /**
828
     * Determine if the current context is a parent of the possible child.
829
     *
830
     * @param   context $possiblechild
831
     * @param   bool $includeself Whether to check the current context
832
     * @return  bool
833
     */
834
    public function is_parent_of(context $possiblechild, bool $includeself): bool {
835
        // A simple substring check is used on the context path.
836
        // The possible child's path is used as a haystack, with the current context as the needle.
837
        // The path is prefixed with '+' to ensure that the parent always starts at the top.
838
        // It is suffixed with '+' to ensure that parents are not included.
839
        // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
840
        // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
841
        // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
842
        $haystacksuffix = $includeself ? '/+' : '+';
843
 
844
        $strpos = strpos(
845
            "+{$possiblechild->path}{$haystacksuffix}",
846
            "+{$this->path}/"
847
        );
848
        return $strpos === 0;
849
    }
850
 
851
    /**
852
     * Returns parent contexts of this context in reversed order, i.e. parent first,
853
     * then grand parent, etc.
854
     *
855
     * @param bool $includeself true means include self too
856
     * @return array of context instances
857
     */
858
    public function get_parent_contexts($includeself = false) {
859
        if (!$contextids = $this->get_parent_context_ids($includeself)) {
860
            return array();
861
        }
862
 
863
        // Preload the contexts to reduce DB calls.
864
        context_helper::preload_contexts_by_id($contextids);
865
 
866
        $result = array();
867
        foreach ($contextids as $contextid) {
868
            // Do NOT change this to self!
869
            $parent = context_helper::instance_by_id($contextid, MUST_EXIST);
870
            $result[$parent->id] = $parent;
871
        }
872
 
873
        return $result;
874
    }
875
 
876
    /**
877
     * Determine if the current context is a child of the possible parent.
878
     *
879
     * @param   context $possibleparent
880
     * @param   bool $includeself Whether to check the current context
881
     * @return  bool
882
     */
883
    public function is_child_of(context $possibleparent, bool $includeself): bool {
884
        // A simple substring check is used on the context path.
885
        // The current context is used as a haystack, with the possible parent as the needle.
886
        // The path is prefixed with '+' to ensure that the parent always starts at the top.
887
        // It is suffixed with '+' to ensure that children are not included.
888
        // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
889
        // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
890
        // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
891
        $haystacksuffix = $includeself ? '/+' : '+';
892
 
893
        $strpos = strpos(
894
            "+{$this->path}{$haystacksuffix}",
895
            "+{$possibleparent->path}/"
896
        );
897
        return $strpos === 0;
898
    }
899
 
900
    /**
901
     * Returns parent context ids of this context in reversed order, i.e. parent first,
902
     * then grand parent, etc.
903
     *
904
     * @param bool $includeself true means include self too
905
     * @return array of context ids
906
     */
907
    public function get_parent_context_ids($includeself = false) {
908
        if (empty($this->_path)) {
909
            return array();
910
        }
911
 
912
        $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
913
        $parentcontexts = explode('/', $parentcontexts);
914
        if (!$includeself) {
915
            array_pop($parentcontexts); // And remove its own id.
916
        }
917
 
918
        return array_reverse($parentcontexts);
919
    }
920
 
921
    /**
922
     * Returns parent context paths of this context.
923
     *
924
     * @param bool $includeself true means include self too
925
     * @return array of context paths
926
     */
927
    public function get_parent_context_paths($includeself = false) {
928
        if (empty($this->_path)) {
929
            return array();
930
        }
931
 
932
        $contextids = explode('/', $this->_path);
933
 
934
        $path = '';
935
        $paths = array();
936
        foreach ($contextids as $contextid) {
937
            if ($contextid) {
938
                $path .= '/' . $contextid;
939
                $paths[$contextid] = $path;
940
            }
941
        }
942
 
943
        if (!$includeself) {
944
            unset($paths[$this->_id]);
945
        }
946
 
947
        return $paths;
948
    }
949
 
950
    /**
951
     * Returns parent context
952
     *
953
     * @return context|false
954
     */
955
    public function get_parent_context() {
956
        if (empty($this->_path) || $this->_id == SYSCONTEXTID) {
957
            return false;
958
        }
959
 
960
        $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
961
        $parentcontexts = explode('/', $parentcontexts);
962
        array_pop($parentcontexts); // Self.
963
        $contextid = array_pop($parentcontexts); // Immediate parent.
964
 
965
        // Do NOT change this to self!
966
        return context_helper::instance_by_id($contextid, MUST_EXIST);
967
    }
968
 
969
    /**
970
     * Is this context part of any course? If yes return course context.
971
     *
972
     * @param bool $strict true means throw exception if not found, false means return false if not found
973
     * @return context\course|false context of the enclosing course, null if not found or exception
974
     */
975
    public function get_course_context($strict = true) {
976
        if ($strict) {
977
            throw new coding_exception('Context does not belong to any course.');
978
        } else {
979
            return false;
980
        }
981
    }
982
 
983
    /**
984
     * Returns sql necessary for purging of stale context instances.
985
     *
986
     * @return string cleanup SQL
987
     */
988
    protected static function get_cleanup_sql() {
989
        throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
990
    }
991
 
992
    /**
993
     * Rebuild context paths and depths at context level.
994
     *
995
     * @param bool $force
996
     * @return void
997
     */
998
    protected static function build_paths($force) {
999
        throw new coding_exception('build_paths() method must be implemented in all context levels');
1000
    }
1001
 
1002
    /**
1003
     * Create missing context instances at given level
1004
     *
1005
     * @return void
1006
     */
1007
    protected static function create_level_instances() {
1008
        throw new coding_exception('create_level_instances() method must be implemented in all context levels');
1009
    }
1010
 
1011
    /**
1012
     * Reset all cached permissions and definitions if the necessary.
1013
     * @return void
1014
     */
1015
    public function reload_if_dirty() {
1016
        global $ACCESSLIB_PRIVATE, $USER;
1017
 
1018
        // Load dirty contexts list if needed.
1019
        if (CLI_SCRIPT) {
1020
            if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1021
                // We do not load dirty flags in CLI and cron.
1022
                $ACCESSLIB_PRIVATE->dirtycontexts = array();
1023
            }
1024
        } else {
1025
            if (!isset($USER->access['time'])) {
1026
                // Nothing has been loaded yet, so we do not need to check dirty flags now.
1027
                return;
1028
            }
1029
 
1030
            // From skodak: No idea why -2 is there, server cluster time difference maybe...
1031
            $changedsince = $USER->access['time'] - 2;
1032
 
1033
            if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1034
                $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
1035
            }
1036
 
1037
            if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
1038
                $ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
1039
            }
1040
        }
1041
 
1042
        $dirty = false;
1043
 
1044
        if (!empty($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
1045
            $dirty = true;
1046
        } else if (!empty($ACCESSLIB_PRIVATE->dirtycontexts)) {
1047
            $paths = $this->get_parent_context_paths(true);
1048
 
1049
            foreach ($paths as $path) {
1050
                if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
1051
                    $dirty = true;
1052
                    break;
1053
                }
1054
            }
1055
        }
1056
 
1057
        if ($dirty) {
1058
            // Reload all capabilities of USER and others - preserving loginas, roleswitches, etc.
1059
            // Then cleanup any marks of dirtyness... at least from our short term memory!
1060
            reload_all_capabilities();
1061
        }
1062
    }
1063
 
1064
    /**
1065
     * Mark a context as dirty (with timestamp) so as to force reloading of the context.
1066
     */
1067
    public function mark_dirty() {
1068
        global $CFG, $USER, $ACCESSLIB_PRIVATE;
1069
 
1070
        if (during_initial_install()) {
1071
            return;
1072
        }
1073
 
1074
        // Only if it is a non-empty string.
1075
        if (is_string($this->_path) && $this->_path !== '') {
1076
            set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time() + $CFG->sessiontimeout);
1077
            if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1078
                $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
1079
            } else {
1080
                if (CLI_SCRIPT) {
1081
                    $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
1082
                } else {
1083
                    if (isset($USER->access['time'])) {
1084
                        $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time'] - 2);
1085
                    } else {
1086
                        $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
1087
                    }
1088
                    // Flags not loaded yet, it will be done later in $context->reload_if_dirty().
1089
                }
1090
            }
1091
        }
1092
    }
1093
}