Proyectos de Subversion Moodle

Rev

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

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - 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 == 'postgres' || $dbfamily == 'mssql') {
427
            $updatesql = "UPDATE {context}
428
                             SET path = temp.path,
429
                                 depth = temp.depth,
430
                                 locked = temp.locked
431
                            FROM {context_temp} temp
432
                           WHERE temp.id={context}.id";
433
        } else {
1441 ariadna 434
            throw new \core\exception\coding_exception("Unsupported database family: {$dbfamily}");
1 efrain 435
        }
436
 
437
        $DB->execute($updatesql);
438
    }
439
 
440
    /**
441
     * Get a context instance as an object, from a given context id.
442
     *
443
     * @param int $id context id
444
     * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
445
     *                        MUST_EXIST means throw exception if no record found
446
     * @return context|bool the context object or false if not found
447
     */
448
    public static function instance_by_id($id, $strictness = MUST_EXIST) {
449
        global $DB;
450
 
451
        if (get_called_class() !== 'core\context' && get_called_class() !== 'core\context_helper') {
452
            // Some devs might confuse context->id and instanceid, better prevent these mistakes completely.
453
            throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
454
        }
455
 
456
        if ($id == SYSCONTEXTID) {
457
            return context\system::instance(0, $strictness);
458
        }
459
 
460
        if (is_array($id) || is_object($id) || empty($id)) {
461
            throw new coding_exception('Invalid context id specified context::instance_by_id()');
462
        }
463
 
464
        if ($context = self::cache_get_by_id($id)) {
465
            return $context;
466
        }
467
 
468
        if ($record = $DB->get_record('context', array('id' => $id), '*', $strictness)) {
469
            return self::create_instance_from_record($record);
470
        }
471
 
472
        return false;
473
    }
474
 
475
    /**
476
     * Update context info after moving context in the tree structure.
477
     *
478
     * @param context $newparent
479
     * @return void
480
     */
481
    public function update_moved(context $newparent) {
482
        global $DB;
483
 
484
        $frompath = $this->_path;
485
        $newpath = $newparent->path . '/' . $this->_id;
486
 
487
        $trans = $DB->start_delegated_transaction();
488
 
489
        $setdepth = '';
490
        if (($newparent->depth + 1) != $this->_depth) {
491
            $diff = $newparent->depth - $this->_depth + 1;
492
            $setdepth = ", depth = depth + $diff";
493
        }
494
        $sql = "UPDATE {context}
495
                   SET path = ?
496
                       $setdepth
497
                 WHERE id = ?";
498
        $params = array($newpath, $this->_id);
499
        $DB->execute($sql, $params);
500
 
501
        $this->_path = $newpath;
502
        $this->_depth = $newparent->depth + 1;
503
 
504
        $sql = "UPDATE {context}
505
                   SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath) + 1))."
506
                       $setdepth
507
                 WHERE path LIKE ?";
508
        $params = array($newpath, "{$frompath}/%");
509
        $DB->execute($sql, $params);
510
 
511
        $this->mark_dirty();
512
 
513
        self::reset_caches();
514
 
515
        $trans->allow_commit();
516
    }
517
 
518
    /**
519
     * Set whether this context has been locked or not.
520
     *
521
     * @param   bool    $locked
522
     * @return  $this
523
     */
524
    public function set_locked(bool $locked) {
525
        global $DB;
526
 
527
        if ($this->_locked == $locked) {
528
            return $this;
529
        }
530
 
531
        $this->_locked = $locked;
532
        $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
533
        $this->mark_dirty();
534
 
535
        if ($locked) {
536
            $eventname = '\\core\\event\\context_locked';
537
        } else {
538
            $eventname = '\\core\\event\\context_unlocked';
539
        }
540
        $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
541
        $event->trigger();
542
 
543
        self::reset_caches();
544
 
545
        return $this;
546
    }
547
 
548
    /**
549
     * Remove all context path info and optionally rebuild it.
550
     *
551
     * @param bool $rebuild
552
     * @return void
553
     */
554
    public function reset_paths($rebuild = true) {
555
        global $DB;
556
 
557
        if ($this->_path) {
558
            $this->mark_dirty();
559
        }
560
        $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
561
        $DB->set_field_select('context', 'path', null, "path LIKE '%/$this->_id/%'");
562
        if ($this->_contextlevel != CONTEXT_SYSTEM) {
563
            $DB->set_field('context', 'depth', 0, array('id' => $this->_id));
564
            $DB->set_field('context', 'path', null, array('id' => $this->_id));
565
            $this->_depth = 0;
566
            $this->_path = null;
567
        }
568
 
569
        if ($rebuild) {
570
            context_helper::build_all_paths(false);
571
        }
572
 
573
        self::reset_caches();
574
    }
575
 
576
    /**
577
     * Delete all data linked to content, do not delete the context record itself
578
     */
579
    public function delete_content() {
580
        global $CFG, $DB;
581
 
582
        blocks_delete_all_for_context($this->_id);
583
        filter_delete_all_for_context($this->_id);
584
 
585
        require_once($CFG->dirroot . '/comment/lib.php');
586
        \comment::delete_comments(array('contextid' => $this->_id));
587
 
588
        require_once($CFG->dirroot.'/rating/lib.php');
589
        $delopt = new stdclass();
590
        $delopt->contextid = $this->_id;
591
        $rm = new \rating_manager();
592
        $rm->delete_ratings($delopt);
593
 
594
        // Delete all files attached to this context.
595
        $fs = get_file_storage();
596
        $fs->delete_area_files($this->_id);
597
 
598
        // Delete all repository instances attached to this context.
599
        require_once($CFG->dirroot . '/repository/lib.php');
600
        \repository::delete_all_for_context($this->_id);
601
 
602
        // Delete all advanced grading data attached to this context.
603
        require_once($CFG->dirroot.'/grade/grading/lib.php');
604
        \grading_manager::delete_all_for_context($this->_id);
605
 
606
        // Now delete stuff from role related tables, role_unassign_all
607
        // and unenrol should be called earlier to do proper cleanup.
608
        $DB->delete_records('role_assignments', array('contextid' => $this->_id));
609
        $DB->delete_records('role_names', array('contextid' => $this->_id));
610
        $this->delete_capabilities();
611
    }
612
 
613
    /**
614
     * Unassign all capabilities from a context.
615
     */
616
    public function delete_capabilities() {
617
        global $DB;
618
 
619
        $ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
620
        if ($ids) {
621
            $DB->delete_records('role_capabilities', array('contextid' => $this->_id));
622
 
623
            // Reset any cache of these roles, including MUC.
624
            accesslib_clear_role_cache($ids);
625
        }
626
    }
627
 
628
    /**
629
     * Delete the context content and the context record itself
630
     */
631
    public function delete() {
632
        global $DB;
633
 
634
        if ($this->_contextlevel <= CONTEXT_SYSTEM) {
635
            throw new coding_exception('Cannot delete system context');
636
        }
637
 
638
        // Double check the context still exists.
639
        if (!$DB->record_exists('context', array('id' => $this->_id))) {
640
            self::cache_remove($this);
641
            return;
642
        }
643
 
644
        $this->delete_content();
645
        $DB->delete_records('context', array('id' => $this->_id));
646
        // Purge static context cache if entry present.
647
        self::cache_remove($this);
648
 
649
        // Inform search engine to delete data related to this context.
650
        \core_search\manager::context_deleted($this);
651
    }
652
 
653
    /* ====== context level related methods ====== */
654
 
655
    /**
656
     * Utility method for context creation
657
     *
658
     * @param int $contextlevel
659
     * @param int $instanceid
660
     * @param string $parentpath
661
     * @return stdClass context record
662
     */
663
    protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
664
        global $DB;
665
 
666
        $record = new stdClass();
667
        $record->contextlevel = $contextlevel;
668
        $record->instanceid = $instanceid;
669
        $record->depth = 0;
670
        $record->path = null; // Not known before insert.
671
        $record->locked = 0;
672
 
673
        $record->id = $DB->insert_record('context', $record);
674
 
675
        // Now add path if known - it can be added later.
676
        if (!is_null($parentpath)) {
677
            $record->path = $parentpath.'/'.$record->id;
678
            $record->depth = substr_count($record->path, '/');
679
            $DB->update_record('context', $record);
680
        }
681
 
682
        return $record;
683
    }
684
 
685
    /**
686
     * Returns human readable context identifier.
687
     *
688
     * @param boolean $withprefix whether to prefix the name of the context with the
689
     *      type of context, e.g. User, Course, Forum, etc.
690
     * @param boolean $short whether to use the short name of the thing. Only applies
691
     *      to course contexts
692
     * @param boolean $escape Whether the returned name of the thing is to be
693
     *      HTML escaped or not.
694
     * @return string the human readable context name.
695
     */
696
    public function get_context_name($withprefix = true, $short = false, $escape = true) {
697
        // Must be implemented in all context levels.
698
        throw new coding_exception('can not get name of abstract context');
699
    }
700
 
701
    /**
702
     * Whether the current context is locked.
703
     *
704
     * @return  bool
705
     */
706
    public function is_locked() {
707
        if ($this->_locked) {
708
            return true;
709
        }
710
 
711
        if ($parent = $this->get_parent_context()) {
712
            return $parent->is_locked();
713
        }
714
 
715
        return false;
716
    }
717
 
718
    /**
719
     * Returns the most relevant URL for this context.
720
     *
721
     * @return moodle_url
722
     */
723
    abstract public function get_url();
724
 
725
    /**
726
     * Returns context instance database name.
727
     *
728
     * @return string|null table name for all levels except system.
729
     */
730
    protected static function get_instance_table(): ?string {
731
        return null;
732
    }
733
 
734
    /**
735
     * Returns list of columns that can be used from behat
736
     * to look up context by reference.
737
     *
738
     * @return array list of column names from instance table
739
     */
740
    protected static function get_behat_reference_columns(): array {
741
        return [];
742
    }
743
 
744
    /**
745
     * Returns list of all role archetypes that are compatible
746
     * with role assignments in context level.
747
     * @since Moodle 4.2
748
     *
749
     * @return string[]
750
     */
751
    protected static function get_compatible_role_archetypes(): array {
752
        // Override if archetype roles should be allowed to be assigned in context level.
753
        return [];
754
    }
755
 
756
    /**
757
     * Returns list of all possible parent context levels,
758
     * it may include itself if nesting is allowed.
759
     * @since Moodle 4.2
760
     *
761
     * @return int[]
762
     */
763
    public static function get_possible_parent_levels(): array {
764
        // Override if other type of parents are expected.
765
        return [context\system::LEVEL];
766
    }
767
 
768
    /**
769
     * Returns array of relevant context capability records.
770
     *
771
     * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display
772
     * @return array
773
     */
774
    abstract public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT);
775
 
776
    /**
777
     * Recursive function which, given a context, find all its children context ids.
778
     *
779
     * For course category contexts it will return immediate children and all subcategory contexts.
780
     * It will NOT recurse into courses or subcategories categories.
781
     * If you want to do that, call it on the returned courses/categories.
782
     *
783
     * When called for a course context, it will return the modules and blocks
784
     * displayed in the course page and blocks displayed on the module pages.
785
     *
786
     * If called on a user/course/module context it _will_ populate the cache with the appropriate
787
     * contexts ;-)
788
     *
789
     * @return array Array of child records
790
     */
791
    public function get_child_contexts() {
792
        global $DB;
793
 
794
        if (empty($this->_path) || empty($this->_depth)) {
795
            debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
796
            return array();
797
        }
798
 
799
        $sql = "SELECT ctx.*
800
                  FROM {context} ctx
801
                 WHERE ctx.path LIKE ?";
802
        $params = array($this->_path.'/%');
803
        $records = $DB->get_records_sql($sql, $params);
804
 
805
        $result = array();
806
        foreach ($records as $record) {
807
            $result[$record->id] = self::create_instance_from_record($record);
808
        }
809
 
810
        return $result;
811
    }
812
 
813
    /**
814
     * Determine if the current context is a parent of the possible child.
815
     *
816
     * @param   context $possiblechild
817
     * @param   bool $includeself Whether to check the current context
818
     * @return  bool
819
     */
820
    public function is_parent_of(context $possiblechild, bool $includeself): bool {
821
        // A simple substring check is used on the context path.
822
        // The possible child's path is used as a haystack, with the current context as the needle.
823
        // The path is prefixed with '+' to ensure that the parent always starts at the top.
824
        // It is suffixed with '+' to ensure that parents are not included.
825
        // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
826
        // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
827
        // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
828
        $haystacksuffix = $includeself ? '/+' : '+';
829
 
830
        $strpos = strpos(
831
            "+{$possiblechild->path}{$haystacksuffix}",
832
            "+{$this->path}/"
833
        );
834
        return $strpos === 0;
835
    }
836
 
837
    /**
838
     * Returns parent contexts of this context in reversed order, i.e. parent first,
839
     * then grand parent, etc.
840
     *
841
     * @param bool $includeself true means include self too
842
     * @return array of context instances
843
     */
844
    public function get_parent_contexts($includeself = false) {
845
        if (!$contextids = $this->get_parent_context_ids($includeself)) {
846
            return array();
847
        }
848
 
849
        // Preload the contexts to reduce DB calls.
850
        context_helper::preload_contexts_by_id($contextids);
851
 
852
        $result = array();
853
        foreach ($contextids as $contextid) {
854
            // Do NOT change this to self!
855
            $parent = context_helper::instance_by_id($contextid, MUST_EXIST);
856
            $result[$parent->id] = $parent;
857
        }
858
 
859
        return $result;
860
    }
861
 
862
    /**
863
     * Determine if the current context is a child of the possible parent.
864
     *
865
     * @param   context $possibleparent
866
     * @param   bool $includeself Whether to check the current context
867
     * @return  bool
868
     */
869
    public function is_child_of(context $possibleparent, bool $includeself): bool {
870
        // A simple substring check is used on the context path.
871
        // The current context is used as a haystack, with the possible parent as the needle.
872
        // The path is prefixed with '+' to ensure that the parent always starts at the top.
873
        // It is suffixed with '+' to ensure that children are not included.
874
        // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
875
        // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
876
        // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
877
        $haystacksuffix = $includeself ? '/+' : '+';
878
 
879
        $strpos = strpos(
880
            "+{$this->path}{$haystacksuffix}",
881
            "+{$possibleparent->path}/"
882
        );
883
        return $strpos === 0;
884
    }
885
 
886
    /**
887
     * Returns parent context ids of this context in reversed order, i.e. parent first,
888
     * then grand parent, etc.
889
     *
890
     * @param bool $includeself true means include self too
891
     * @return array of context ids
892
     */
893
    public function get_parent_context_ids($includeself = false) {
894
        if (empty($this->_path)) {
895
            return array();
896
        }
897
 
898
        $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
899
        $parentcontexts = explode('/', $parentcontexts);
900
        if (!$includeself) {
901
            array_pop($parentcontexts); // And remove its own id.
902
        }
903
 
904
        return array_reverse($parentcontexts);
905
    }
906
 
907
    /**
908
     * Returns parent context paths of this context.
909
     *
910
     * @param bool $includeself true means include self too
911
     * @return array of context paths
912
     */
913
    public function get_parent_context_paths($includeself = false) {
914
        if (empty($this->_path)) {
915
            return array();
916
        }
917
 
918
        $contextids = explode('/', $this->_path);
919
 
920
        $path = '';
921
        $paths = array();
922
        foreach ($contextids as $contextid) {
923
            if ($contextid) {
924
                $path .= '/' . $contextid;
925
                $paths[$contextid] = $path;
926
            }
927
        }
928
 
929
        if (!$includeself) {
930
            unset($paths[$this->_id]);
931
        }
932
 
933
        return $paths;
934
    }
935
 
936
    /**
937
     * Returns parent context
938
     *
939
     * @return context|false
940
     */
941
    public function get_parent_context() {
942
        if (empty($this->_path) || $this->_id == SYSCONTEXTID) {
943
            return false;
944
        }
945
 
946
        $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
947
        $parentcontexts = explode('/', $parentcontexts);
948
        array_pop($parentcontexts); // Self.
949
        $contextid = array_pop($parentcontexts); // Immediate parent.
950
 
951
        // Do NOT change this to self!
952
        return context_helper::instance_by_id($contextid, MUST_EXIST);
953
    }
954
 
955
    /**
956
     * Is this context part of any course? If yes return course context.
957
     *
958
     * @param bool $strict true means throw exception if not found, false means return false if not found
959
     * @return context\course|false context of the enclosing course, null if not found or exception
960
     */
961
    public function get_course_context($strict = true) {
962
        if ($strict) {
963
            throw new coding_exception('Context does not belong to any course.');
964
        } else {
965
            return false;
966
        }
967
    }
968
 
969
    /**
970
     * Returns sql necessary for purging of stale context instances.
971
     *
972
     * @return string cleanup SQL
973
     */
974
    protected static function get_cleanup_sql() {
975
        throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
976
    }
977
 
978
    /**
979
     * Rebuild context paths and depths at context level.
980
     *
981
     * @param bool $force
982
     * @return void
983
     */
984
    protected static function build_paths($force) {
985
        throw new coding_exception('build_paths() method must be implemented in all context levels');
986
    }
987
 
988
    /**
989
     * Create missing context instances at given level
990
     *
991
     * @return void
992
     */
993
    protected static function create_level_instances() {
994
        throw new coding_exception('create_level_instances() method must be implemented in all context levels');
995
    }
996
 
997
    /**
998
     * Reset all cached permissions and definitions if the necessary.
999
     * @return void
1000
     */
1001
    public function reload_if_dirty() {
1002
        global $ACCESSLIB_PRIVATE, $USER;
1003
 
1004
        // Load dirty contexts list if needed.
1005
        if (CLI_SCRIPT) {
1006
            if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1007
                // We do not load dirty flags in CLI and cron.
1008
                $ACCESSLIB_PRIVATE->dirtycontexts = array();
1009
            }
1010
        } else {
1011
            if (!isset($USER->access['time'])) {
1012
                // Nothing has been loaded yet, so we do not need to check dirty flags now.
1013
                return;
1014
            }
1015
 
1016
            // From skodak: No idea why -2 is there, server cluster time difference maybe...
1017
            $changedsince = $USER->access['time'] - 2;
1018
 
1019
            if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1020
                $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
1021
            }
1022
 
1023
            if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
1024
                $ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
1025
            }
1026
        }
1027
 
1028
        $dirty = false;
1029
 
1030
        if (!empty($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
1031
            $dirty = true;
1032
        } else if (!empty($ACCESSLIB_PRIVATE->dirtycontexts)) {
1033
            $paths = $this->get_parent_context_paths(true);
1034
 
1035
            foreach ($paths as $path) {
1036
                if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
1037
                    $dirty = true;
1038
                    break;
1039
                }
1040
            }
1041
        }
1042
 
1043
        if ($dirty) {
1044
            // Reload all capabilities of USER and others - preserving loginas, roleswitches, etc.
1045
            // Then cleanup any marks of dirtyness... at least from our short term memory!
1046
            reload_all_capabilities();
1047
        }
1048
    }
1049
 
1050
    /**
1051
     * Mark a context as dirty (with timestamp) so as to force reloading of the context.
1052
     */
1053
    public function mark_dirty() {
1054
        global $CFG, $USER, $ACCESSLIB_PRIVATE;
1055
 
1056
        if (during_initial_install()) {
1057
            return;
1058
        }
1059
 
1060
        // Only if it is a non-empty string.
1061
        if (is_string($this->_path) && $this->_path !== '') {
1062
            set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time() + $CFG->sessiontimeout);
1063
            if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1064
                $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
1065
            } else {
1066
                if (CLI_SCRIPT) {
1067
                    $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
1068
                } else {
1069
                    if (isset($USER->access['time'])) {
1070
                        $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time'] - 2);
1071
                    } else {
1072
                        $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
1073
                    }
1074
                    // Flags not loaded yet, it will be done later in $context->reload_if_dirty().
1075
                }
1076
            }
1077
        }
1078
    }
1079
}