<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle 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.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <https://www.gnu.org/licenses/>.

namespace core;

use stdClass, IteratorAggregate, ArrayIterator;
use coding_exception, moodle_url;

/**
 * Basic moodle context abstraction class.
 *
 * Google confirms that no other important framework is using "context" class,
 * we could use something else like mcontext or moodle_context, but we need to type
 * this very often which would be annoying and it would take too much space...
 *
 * This class is derived from stdClass for backwards compatibility with
 * odl $context record that was returned from DML $DB->get_record()
 *
 * @package   core_access
 * @category  access
 * @copyright Petr Skoda
 * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @since     Moodle 4.2
 *
 * @property-read int $id context id
 * @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc.
 * @property-read int $instanceid id of related instance in each context
 * @property-read string $path path to context, starts with system context
 * @property-read int $depth
 * @property-read bool $locked true means write capabilities are ignored in this context or parents
 */
abstract class context extends stdClass implements IteratorAggregate {

    /** @var string Default sorting of capabilities in {@see get_capabilities} */
    protected const DEFAULT_CAPABILITY_SORT = 'contextlevel, component, name';

    /**
     * The context id
     * Can be accessed publicly through $context->id
     * @var int
     */
    protected $_id;

    /**
     * The context level
     * Can be accessed publicly through $context->contextlevel
     * @var int One of CONTEXT_* e.g. CONTEXT_COURSE, CONTEXT_MODULE
     */
    protected $_contextlevel;

    /**
     * Id of the item this context is related to e.g. COURSE_CONTEXT => course.id
     * Can be accessed publicly through $context->instanceid
     * @var int
     */
    protected $_instanceid;

    /**
     * The path to the context always starting from the system context
     * Can be accessed publicly through $context->path
     * @var string
     */
    protected $_path;

    /**
     * The depth of the context in relation to parent contexts
     * Can be accessed publicly through $context->depth
     * @var int
     */
    protected $_depth;

    /**
     * Whether this context is locked or not.
     *
     * Can be accessed publicly through $context->locked.
     *
     * @var int
     */
    protected $_locked;

    /**
     * @var array Context caching info
     */
    private static $cache_contextsbyid = array();

    /**
     * @var array Context caching info
     */
    private static $cache_contexts = array();

    /**
     * Context count
     * Why do we do count contexts? Because count($array) is horribly slow for large arrays
     * @var int
     */
    protected static $cache_count = 0;

    /**
     * @var array Context caching info
     */
    protected static $cache_preloaded = array();

    /**
     * @var context\system The system context once initialised
     */
    protected static $systemcontext = null;

    /**
     * Returns short context name.
     *
     * @since Moodle 4.2
     *
     * @return string
     */
    public static function get_short_name(): string {
        // NOTE: it would be more correct to make this abstract,
        // unfortunately there are tests that attempt to mock context classes.
        throw new \coding_exception('get_short_name() method must be overridden in custom context levels');
    }

    /**
     * Resets the cache to remove all data.
     */
    protected static function reset_caches() {
        self::$cache_contextsbyid = array();
        self::$cache_contexts = array();
        self::$cache_count = 0;
        self::$cache_preloaded = array();

        self::$systemcontext = null;
    }

    /**
     * Adds a context to the cache. If the cache is full, discards a batch of
     * older entries.
     *
     * @param context $context New context to add
     * @return void
     */
    protected static function cache_add(context $context) {
        if (isset(self::$cache_contextsbyid[$context->id])) {
            // Already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
            return;
        }

        if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) {
            $i = 0;
            foreach (self::$cache_contextsbyid as $ctx) {
                $i++;
                if ($i <= 100) {
                    // We want to keep the first contexts to be loaded on this page, hopefully they will be needed again later.
                    continue;
                }
                if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
                    // We remove oldest third of the contexts to make room for more contexts.
                    break;
                }
                unset(self::$cache_contextsbyid[$ctx->id]);
                unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
                self::$cache_count--;
            }
        }

        self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
        self::$cache_contextsbyid[$context->id] = $context;
        self::$cache_count++;
    }

    /**
     * Removes a context from the cache.
     *
     * @param context $context Context object to remove
     * @return void
     */
    protected static function cache_remove(context $context) {
        if (!isset(self::$cache_contextsbyid[$context->id])) {
            // Not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
            return;
        }
        unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
        unset(self::$cache_contextsbyid[$context->id]);

        self::$cache_count--;

        if (self::$cache_count < 0) {
            self::$cache_count = 0;
        }
    }

    /**
     * Gets a context from the cache.
     *
     * @param int $contextlevel Context level
     * @param int $instance Instance ID
     * @return context|bool Context or false if not in cache
     */
    protected static function cache_get($contextlevel, $instance) {
        if (isset(self::$cache_contexts[$contextlevel][$instance])) {
            return self::$cache_contexts[$contextlevel][$instance];
        }
        return false;
    }

    /**
     * Gets a context from the cache based on its id.
     *
     * @param int $id Context ID
     * @return context|bool Context or false if not in cache
     */
    protected static function cache_get_by_id($id) {
        if (isset(self::$cache_contextsbyid[$id])) {
            return self::$cache_contextsbyid[$id];
        }
        return false;
    }

    /**
     * Preloads context information from db record and strips the cached info.
     *
     * @param stdClass $rec
     * @return context|null (modifies $rec)
     */
    protected static function preload_from_record(stdClass $rec) {
        $notenoughdata = false;
        $notenoughdata = $notenoughdata || empty($rec->ctxid);
        $notenoughdata = $notenoughdata || empty($rec->ctxlevel);
        $notenoughdata = $notenoughdata || !isset($rec->ctxinstance);
        $notenoughdata = $notenoughdata || empty($rec->ctxpath);
        $notenoughdata = $notenoughdata || empty($rec->ctxdepth);
        $notenoughdata = $notenoughdata || !isset($rec->ctxlocked);
        if ($notenoughdata) {
            // The record does not have enough data, passed here repeatedly or context does not exist yet.
            if (isset($rec->ctxid) && !isset($rec->ctxlocked)) {
                debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
            }
            return null;
        }

        $record = (object) [
            'id' => $rec->ctxid,
            'contextlevel' => $rec->ctxlevel,
            'instanceid' => $rec->ctxinstance,
            'path' => $rec->ctxpath,
            'depth' => $rec->ctxdepth,
            'locked' => $rec->ctxlocked,
        ];

        unset($rec->ctxid);
        unset($rec->ctxlevel);
        unset($rec->ctxinstance);
        unset($rec->ctxpath);
        unset($rec->ctxdepth);
        unset($rec->ctxlocked);

        return self::create_instance_from_record($record);
    }


    /* ====== magic methods ======= */

    /**
     * Magic setter method, we do not want anybody to modify properties from the outside
     * @param string $name
     * @param mixed $value
     */
    public function __set($name, $value) {
        debugging('Can not change context instance properties!');
    }

    /**
     * Magic method getter, redirects to read only values.
     * @param string $name
     * @return mixed
     */
    public function __get($name) {
        switch ($name) {
            case 'id':
                return $this->_id;
            case 'contextlevel':
                return $this->_contextlevel;
            case 'instanceid':
                return $this->_instanceid;
            case 'path':
                return $this->_path;
            case 'depth':
                return $this->_depth;
            case 'locked':
                return $this->is_locked();

            default:
                debugging('Invalid context property accessed! '.$name);
                return null;
        }
    }

    /**
     * Full support for isset on our magic read only properties.
     * @param string $name
     * @return bool
     */
    public function __isset($name) {
        switch ($name) {
            case 'id':
                return isset($this->_id);
            case 'contextlevel':
                return isset($this->_contextlevel);
            case 'instanceid':
                return isset($this->_instanceid);
            case 'path':
                return isset($this->_path);
            case 'depth':
                return isset($this->_depth);
            case 'locked':
                // Locked is always set.
                return true;
            default:
                return false;
        }
    }

    /**
     * All properties are read only, sorry.
     * @param string $name
     */
    public function __unset($name) {
        debugging('Can not unset context instance properties!');
    }

    /* ====== implementing method from interface IteratorAggregate ====== */

    /**
     * Create an iterator because magic vars can't be seen by 'foreach'.
     *
     * Now we can convert context object to array using convert_to_array(),
     * and feed it properly to json_encode().
     */
    public function getIterator(): \Traversable {
        $ret = array(
            'id' => $this->id,
            'contextlevel' => $this->contextlevel,
            'instanceid' => $this->instanceid,
            'path' => $this->path,
            'depth' => $this->depth,
            'locked' => $this->locked,
        );
        return new ArrayIterator($ret);
    }

    /* ====== general context methods ====== */

    /**
     * Constructor is protected so that devs are forced to
     * use context_xxx::instance() or context::instance_by_id().
     *
     * @param stdClass $record
     */
    protected function __construct(stdClass $record) {
        $this->_id = (int)$record->id;
        $this->_contextlevel = (int)$record->contextlevel;
        $this->_instanceid = $record->instanceid;
        $this->_path = $record->path;
        $this->_depth = $record->depth;

        if (isset($record->locked)) {
            $this->_locked = $record->locked;
        } else if (!during_initial_install() && !moodle_needs_upgrading()) {
            debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
        }
    }

    /**
     * This function is also used to work around 'protected' keyword problems in context_helper.
     *
     * @param stdClass $record
     * @return context instance
     */
    protected static function create_instance_from_record(stdClass $record) {
        $classname = context_helper::get_class_for_level($record->contextlevel);

        if ($context = self::cache_get_by_id($record->id)) {
            return $context;
        }

        $context = new $classname($record);
        self::cache_add($context);

        return $context;
    }

    /**
     * Copy prepared new contexts from temp table to context table,
     * we do this in db specific way for perf reasons only.
     */
    protected static function merge_context_temp_table() {
        global $DB;

        /* MDL-11347:
         *  - mysql does not allow to use FROM in UPDATE statements
         *  - using two tables after UPDATE works in mysql, but might give unexpected
         *    results in pg 8 (depends on configuration)
         *  - using table alias in UPDATE does not work in pg < 8.2
         *
         * Different code for each database - mostly for performance reasons
         */

        $dbfamily = $DB->get_dbfamily();
        if ($dbfamily == 'mysql') {
            $updatesql = "UPDATE {context} ct, {context_temp} temp
                             SET ct.path = temp.path,
                                 ct.depth = temp.depth,
                                 ct.locked = temp.locked
                           WHERE ct.id = temp.id";
        } else if ($dbfamily == 'oracle') {
            $updatesql = "UPDATE {context} ct
                             SET (ct.path, ct.depth, ct.locked) =
                                 (SELECT temp.path, temp.depth, temp.locked
                                    FROM {context_temp} temp
                                   WHERE temp.id=ct.id)
                           WHERE EXISTS (SELECT 'x'
                                           FROM {context_temp} temp
                                           WHERE temp.id = ct.id)";
        } else if ($dbfamily == 'postgres' || $dbfamily == 'mssql') {
            $updatesql = "UPDATE {context}
                             SET path = temp.path,
                                 depth = temp.depth,
                                 locked = temp.locked
                            FROM {context_temp} temp
                           WHERE temp.id={context}.id";
        } else {
            // Sqlite and others.
            $updatesql = "UPDATE {context}
                             SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
                                 depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id),
                                 locked = (SELECT locked FROM {context_temp} WHERE id = {context}.id)
                             WHERE id IN (SELECT id FROM {context_temp})";
        }

        $DB->execute($updatesql);
    }

    /**
     * Get a context instance as an object, from a given context id.
     *
     * @param int $id context id
     * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
     *                        MUST_EXIST means throw exception if no record found
     * @return context|bool the context object or false if not found
     */
    public static function instance_by_id($id, $strictness = MUST_EXIST) {
        global $DB;

        if (get_called_class() !== 'core\context' && get_called_class() !== 'core\context_helper') {
            // Some devs might confuse context->id and instanceid, better prevent these mistakes completely.
            throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
        }

        if ($id == SYSCONTEXTID) {
            return context\system::instance(0, $strictness);
        }

        if (is_array($id) || is_object($id) || empty($id)) {
            throw new coding_exception('Invalid context id specified context::instance_by_id()');
        }

        if ($context = self::cache_get_by_id($id)) {
            return $context;
        }

        if ($record = $DB->get_record('context', array('id' => $id), '*', $strictness)) {
            return self::create_instance_from_record($record);
        }

        return false;
    }

    /**
     * Update context info after moving context in the tree structure.
     *
     * @param context $newparent
     * @return void
     */
    public function update_moved(context $newparent) {
        global $DB;

        $frompath = $this->_path;
        $newpath = $newparent->path . '/' . $this->_id;

        $trans = $DB->start_delegated_transaction();

        $setdepth = '';
        if (($newparent->depth + 1) != $this->_depth) {
            $diff = $newparent->depth - $this->_depth + 1;
            $setdepth = ", depth = depth + $diff";
        }
        $sql = "UPDATE {context}
                   SET path = ?
                       $setdepth
                 WHERE id = ?";
        $params = array($newpath, $this->_id);
        $DB->execute($sql, $params);

        $this->_path = $newpath;
        $this->_depth = $newparent->depth + 1;

        $sql = "UPDATE {context}
                   SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath) + 1))."
                       $setdepth
                 WHERE path LIKE ?";
        $params = array($newpath, "{$frompath}/%");
        $DB->execute($sql, $params);

        $this->mark_dirty();

        self::reset_caches();

        $trans->allow_commit();
    }

    /**
     * Set whether this context has been locked or not.
     *
     * @param   bool    $locked
     * @return  $this
     */
    public function set_locked(bool $locked) {
        global $DB;

        if ($this->_locked == $locked) {
            return $this;
        }

        $this->_locked = $locked;
        $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
        $this->mark_dirty();

        if ($locked) {
            $eventname = '\\core\\event\\context_locked';
        } else {
            $eventname = '\\core\\event\\context_unlocked';
        }
        $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
        $event->trigger();

        self::reset_caches();

        return $this;
    }

    /**
     * Remove all context path info and optionally rebuild it.
     *
     * @param bool $rebuild
     * @return void
     */
    public function reset_paths($rebuild = true) {
        global $DB;

        if ($this->_path) {
            $this->mark_dirty();
        }
        $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
        $DB->set_field_select('context', 'path', null, "path LIKE '%/$this->_id/%'");
        if ($this->_contextlevel != CONTEXT_SYSTEM) {
            $DB->set_field('context', 'depth', 0, array('id' => $this->_id));
            $DB->set_field('context', 'path', null, array('id' => $this->_id));
            $this->_depth = 0;
            $this->_path = null;
        }

        if ($rebuild) {
            context_helper::build_all_paths(false);
        }

        self::reset_caches();
    }

    /**
     * Delete all data linked to content, do not delete the context record itself
     */
    public function delete_content() {
        global $CFG, $DB;

        blocks_delete_all_for_context($this->_id);
        filter_delete_all_for_context($this->_id);

        require_once($CFG->dirroot . '/comment/lib.php');
        \comment::delete_comments(array('contextid' => $this->_id));

        require_once($CFG->dirroot.'/rating/lib.php');
        $delopt = new stdclass();
        $delopt->contextid = $this->_id;
        $rm = new \rating_manager();
        $rm->delete_ratings($delopt);

        // Delete all files attached to this context.
        $fs = get_file_storage();
        $fs->delete_area_files($this->_id);

        // Delete all repository instances attached to this context.
        require_once($CFG->dirroot . '/repository/lib.php');
        \repository::delete_all_for_context($this->_id);

        // Delete all advanced grading data attached to this context.
        require_once($CFG->dirroot.'/grade/grading/lib.php');
        \grading_manager::delete_all_for_context($this->_id);

        // Now delete stuff from role related tables, role_unassign_all
        // and unenrol should be called earlier to do proper cleanup.
        $DB->delete_records('role_assignments', array('contextid' => $this->_id));
        $DB->delete_records('role_names', array('contextid' => $this->_id));
        $this->delete_capabilities();
    }

    /**
     * Unassign all capabilities from a context.
     */
    public function delete_capabilities() {
        global $DB;

        $ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
        if ($ids) {
            $DB->delete_records('role_capabilities', array('contextid' => $this->_id));

            // Reset any cache of these roles, including MUC.
            accesslib_clear_role_cache($ids);
        }
    }

    /**
     * Delete the context content and the context record itself
     */
    public function delete() {
        global $DB;

        if ($this->_contextlevel <= CONTEXT_SYSTEM) {
            throw new coding_exception('Cannot delete system context');
        }

        // Double check the context still exists.
        if (!$DB->record_exists('context', array('id' => $this->_id))) {
            self::cache_remove($this);
            return;
        }

        $this->delete_content();
        $DB->delete_records('context', array('id' => $this->_id));
        // Purge static context cache if entry present.
        self::cache_remove($this);

        // Inform search engine to delete data related to this context.
        \core_search\manager::context_deleted($this);
    }

    /* ====== context level related methods ====== */

    /**
     * Utility method for context creation
     *
     * @param int $contextlevel
     * @param int $instanceid
     * @param string $parentpath
     * @return stdClass context record
     */
    protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
        global $DB;

        $record = new stdClass();
        $record->contextlevel = $contextlevel;
        $record->instanceid = $instanceid;
        $record->depth = 0;
        $record->path = null; // Not known before insert.
        $record->locked = 0;

        $record->id = $DB->insert_record('context', $record);

        // Now add path if known - it can be added later.
        if (!is_null($parentpath)) {
            $record->path = $parentpath.'/'.$record->id;
            $record->depth = substr_count($record->path, '/');
            $DB->update_record('context', $record);
        }

        return $record;
    }

    /**
     * Returns human readable context identifier.
     *
     * @param boolean $withprefix whether to prefix the name of the context with the
     *      type of context, e.g. User, Course, Forum, etc.
     * @param boolean $short whether to use the short name of the thing. Only applies
     *      to course contexts
     * @param boolean $escape Whether the returned name of the thing is to be
     *      HTML escaped or not.
     * @return string the human readable context name.
     */
    public function get_context_name($withprefix = true, $short = false, $escape = true) {
        // Must be implemented in all context levels.
        throw new coding_exception('can not get name of abstract context');
    }

    /**
     * Whether the current context is locked.
     *
     * @return  bool
     */
    public function is_locked() {
        if ($this->_locked) {
            return true;
        }

        if ($parent = $this->get_parent_context()) {
            return $parent->is_locked();
        }

        return false;
    }

    /**
     * Returns the most relevant URL for this context.
     *
     * @return moodle_url
     */
    abstract public function get_url();

    /**
     * Returns context instance database name.
     *
     * @return string|null table name for all levels except system.
     */
    protected static function get_instance_table(): ?string {
        return null;
    }

    /**
     * Returns list of columns that can be used from behat
     * to look up context by reference.
     *
     * @return array list of column names from instance table
     */
    protected static function get_behat_reference_columns(): array {
        return [];
    }

    /**
     * Returns list of all role archetypes that are compatible
     * with role assignments in context level.
     * @since Moodle 4.2
     *
     * @return string[]
     */
    protected static function get_compatible_role_archetypes(): array {
        // Override if archetype roles should be allowed to be assigned in context level.
        return [];
    }

    /**
     * Returns list of all possible parent context levels,
     * it may include itself if nesting is allowed.
     * @since Moodle 4.2
     *
     * @return int[]
     */
    public static function get_possible_parent_levels(): array {
        // Override if other type of parents are expected.
        return [context\system::LEVEL];
    }

    /**
     * Returns array of relevant context capability records.
     *
     * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display
     * @return array
     */
    abstract public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT);

    /**
     * Recursive function which, given a context, find all its children context ids.
     *
     * For course category contexts it will return immediate children and all subcategory contexts.
     * It will NOT recurse into courses or subcategories categories.
     * If you want to do that, call it on the returned courses/categories.
     *
     * When called for a course context, it will return the modules and blocks
     * displayed in the course page and blocks displayed on the module pages.
     *
     * If called on a user/course/module context it _will_ populate the cache with the appropriate
     * contexts ;-)
     *
     * @return array Array of child records
     */
    public function get_child_contexts() {
        global $DB;

        if (empty($this->_path) || empty($this->_depth)) {
            debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
            return array();
        }

        $sql = "SELECT ctx.*
                  FROM {context} ctx
                 WHERE ctx.path LIKE ?";
        $params = array($this->_path.'/%');
        $records = $DB->get_records_sql($sql, $params);

        $result = array();
        foreach ($records as $record) {
            $result[$record->id] = self::create_instance_from_record($record);
        }

        return $result;
    }

    /**
     * Determine if the current context is a parent of the possible child.
     *
     * @param   context $possiblechild
     * @param   bool $includeself Whether to check the current context
     * @return  bool
     */
    public function is_parent_of(context $possiblechild, bool $includeself): bool {
        // A simple substring check is used on the context path.
        // The possible child's path is used as a haystack, with the current context as the needle.
        // The path is prefixed with '+' to ensure that the parent always starts at the top.
        // It is suffixed with '+' to ensure that parents are not included.
        // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
        // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
        // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
        $haystacksuffix = $includeself ? '/+' : '+';

        $strpos = strpos(
            "+{$possiblechild->path}{$haystacksuffix}",
            "+{$this->path}/"
        );
        return $strpos === 0;
    }

    /**
     * Returns parent contexts of this context in reversed order, i.e. parent first,
     * then grand parent, etc.
     *
     * @param bool $includeself true means include self too
     * @return array of context instances
     */
    public function get_parent_contexts($includeself = false) {
        if (!$contextids = $this->get_parent_context_ids($includeself)) {
            return array();
        }

        // Preload the contexts to reduce DB calls.
        context_helper::preload_contexts_by_id($contextids);

        $result = array();
        foreach ($contextids as $contextid) {
            // Do NOT change this to self!
            $parent = context_helper::instance_by_id($contextid, MUST_EXIST);
            $result[$parent->id] = $parent;
        }

        return $result;
    }

    /**
     * Determine if the current context is a child of the possible parent.
     *
     * @param   context $possibleparent
     * @param   bool $includeself Whether to check the current context
     * @return  bool
     */
    public function is_child_of(context $possibleparent, bool $includeself): bool {
        // A simple substring check is used on the context path.
        // The current context is used as a haystack, with the possible parent as the needle.
        // The path is prefixed with '+' to ensure that the parent always starts at the top.
        // It is suffixed with '+' to ensure that children are not included.
        // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
        // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
        // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
        $haystacksuffix = $includeself ? '/+' : '+';

        $strpos = strpos(
            "+{$this->path}{$haystacksuffix}",
            "+{$possibleparent->path}/"
        );
        return $strpos === 0;
    }

    /**
     * Returns parent context ids of this context in reversed order, i.e. parent first,
     * then grand parent, etc.
     *
     * @param bool $includeself true means include self too
     * @return array of context ids
     */
    public function get_parent_context_ids($includeself = false) {
        if (empty($this->_path)) {
            return array();
        }

        $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
        $parentcontexts = explode('/', $parentcontexts);
        if (!$includeself) {
            array_pop($parentcontexts); // And remove its own id.
        }

        return array_reverse($parentcontexts);
    }

    /**
     * Returns parent context paths of this context.
     *
     * @param bool $includeself true means include self too
     * @return array of context paths
     */
    public function get_parent_context_paths($includeself = false) {
        if (empty($this->_path)) {
            return array();
        }

        $contextids = explode('/', $this->_path);

        $path = '';
        $paths = array();
        foreach ($contextids as $contextid) {
            if ($contextid) {
                $path .= '/' . $contextid;
                $paths[$contextid] = $path;
            }
        }

        if (!$includeself) {
            unset($paths[$this->_id]);
        }

        return $paths;
    }

    /**
     * Returns parent context
     *
     * @return context|false
     */
    public function get_parent_context() {
        if (empty($this->_path) || $this->_id == SYSCONTEXTID) {
            return false;
        }

        $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
        $parentcontexts = explode('/', $parentcontexts);
        array_pop($parentcontexts); // Self.
        $contextid = array_pop($parentcontexts); // Immediate parent.

        // Do NOT change this to self!
        return context_helper::instance_by_id($contextid, MUST_EXIST);
    }

    /**
     * Is this context part of any course? If yes return course context.
     *
     * @param bool $strict true means throw exception if not found, false means return false if not found
     * @return context\course|false context of the enclosing course, null if not found or exception
     */
    public function get_course_context($strict = true) {
        if ($strict) {
            throw new coding_exception('Context does not belong to any course.');
        } else {
            return false;
        }
    }

    /**
     * Returns sql necessary for purging of stale context instances.
     *
     * @return string cleanup SQL
     */
    protected static function get_cleanup_sql() {
        throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
    }

    /**
     * Rebuild context paths and depths at context level.
     *
     * @param bool $force
     * @return void
     */
    protected static function build_paths($force) {
        throw new coding_exception('build_paths() method must be implemented in all context levels');
    }

    /**
     * Create missing context instances at given level
     *
     * @return void
     */
    protected static function create_level_instances() {
        throw new coding_exception('create_level_instances() method must be implemented in all context levels');
    }

    /**
     * Reset all cached permissions and definitions if the necessary.
     * @return void
     */
    public function reload_if_dirty() {
        global $ACCESSLIB_PRIVATE, $USER;

        // Load dirty contexts list if needed.
        if (CLI_SCRIPT) {
            if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
                // We do not load dirty flags in CLI and cron.
                $ACCESSLIB_PRIVATE->dirtycontexts = array();
            }
        } else {
            if (!isset($USER->access['time'])) {
                // Nothing has been loaded yet, so we do not need to check dirty flags now.
                return;
            }

            // From skodak: No idea why -2 is there, server cluster time difference maybe...
            $changedsince = $USER->access['time'] - 2;

            if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
                $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
            }

            if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
                $ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
            }
        }

        $dirty = false;

        if (!empty($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
            $dirty = true;
        } else if (!empty($ACCESSLIB_PRIVATE->dirtycontexts)) {
            $paths = $this->get_parent_context_paths(true);

            foreach ($paths as $path) {
                if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
                    $dirty = true;
                    break;
                }
            }
        }

        if ($dirty) {
            // Reload all capabilities of USER and others - preserving loginas, roleswitches, etc.
            // Then cleanup any marks of dirtyness... at least from our short term memory!
            reload_all_capabilities();
        }
    }

    /**
     * Mark a context as dirty (with timestamp) so as to force reloading of the context.
     */
    public function mark_dirty() {
        global $CFG, $USER, $ACCESSLIB_PRIVATE;

        if (during_initial_install()) {
            return;
        }

        // Only if it is a non-empty string.
        if (is_string($this->_path) && $this->_path !== '') {
            set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time() + $CFG->sessiontimeout);
            if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
                $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
            } else {
                if (CLI_SCRIPT) {
                    $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
                } else {
                    if (isset($USER->access['time'])) {
                        $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time'] - 2);
                    } else {
                        $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
                    }
                    // Flags not loaded yet, it will be done later in $context->reload_if_dirty().
                }
            }
        }
    }
}
