Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   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\context;
  18  
  19  use core\context;
  20  use stdClass;
  21  use coding_exception, moodle_url;
  22  
  23  /**
  24   * Course module context class
  25   *
  26   * @package   core_access
  27   * @category  access
  28   * @copyright Petr Skoda
  29   * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   * @since     Moodle 4.2
  31   */
  32  class module extends context {
  33      /** @var int numeric context level value matching legacy CONTEXT_MODULE */
  34      public const LEVEL = 70;
  35  
  36      /**
  37       * Please use \core\context\module::instance($cmid) if you need the instance of context.
  38       * Alternatively if you know only the context id use \core\context::instance_by_id($contextid)
  39       *
  40       * @param stdClass $record
  41       */
  42      protected function __construct(stdClass $record) {
  43          parent::__construct($record);
  44          if ($record->contextlevel != self::LEVEL) {
  45              throw new coding_exception('Invalid $record->contextlevel in core\context\module constructor.');
  46          }
  47      }
  48  
  49      /**
  50       * Returns short context name.
  51       *
  52       * @since Moodle 4.2
  53       *
  54       * @return string
  55       */
  56      public static function get_short_name(): string {
  57          return 'module';
  58      }
  59  
  60      /**
  61       * Returns human readable context level name.
  62       *
  63       * @return string the human readable context level name.
  64       */
  65      public static function get_level_name() {
  66          return get_string('activitymodule');
  67      }
  68  
  69      /**
  70       * Returns human readable context identifier.
  71       *
  72       * @param boolean $withprefix whether to prefix the name of the context with the
  73       *      module name, e.g. Forum, Glossary, etc.
  74       * @param boolean $short does not apply to module context
  75       * @param boolean $escape Whether the returned name of the context is to be HTML escaped or not.
  76       * @return string the human readable context name.
  77       */
  78      public function get_context_name($withprefix = true, $short = false, $escape = true) {
  79          global $DB;
  80  
  81          $name = '';
  82          if ($cm = $DB->get_record_sql("SELECT cm.*, md.name AS modname
  83                                           FROM {course_modules} cm
  84                                           JOIN {modules} md ON md.id = cm.module
  85                                          WHERE cm.id = ?", array($this->_instanceid))) {
  86              if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) {
  87                  if ($withprefix) {
  88                      $name = get_string('modulename', $cm->modname).': ';
  89                  }
  90                  if (!$escape) {
  91                      $name .= format_string($mod->name, true, array('context' => $this, 'escape' => false));
  92                  } else {
  93                      $name .= format_string($mod->name, true, array('context' => $this));
  94                  }
  95              }
  96          }
  97          return $name;
  98      }
  99  
 100      /**
 101       * Returns the most relevant URL for this context.
 102       *
 103       * @return moodle_url
 104       */
 105      public function get_url() {
 106          global $DB;
 107  
 108          if ($modname = $DB->get_field_sql("SELECT md.name AS modname
 109                                               FROM {course_modules} cm
 110                                               JOIN {modules} md ON md.id = cm.module
 111                                              WHERE cm.id = ?", array($this->_instanceid))) {
 112              return new moodle_url('/mod/' . $modname . '/view.php', array('id' => $this->_instanceid));
 113          }
 114  
 115          return new moodle_url('/');
 116      }
 117  
 118      /**
 119       * Returns context instance database name.
 120       *
 121       * @return string|null table name for all levels except system.
 122       */
 123      protected static function get_instance_table(): ?string {
 124          return 'course_modules';
 125      }
 126  
 127      /**
 128       * Returns list of columns that can be used from behat
 129       * to look up context by reference.
 130       *
 131       * @return array list of column names from instance table
 132       */
 133      protected static function get_behat_reference_columns(): array {
 134          return ['idnumber'];
 135      }
 136  
 137      /**
 138       * Returns list of all role archetypes that are compatible
 139       * with role assignments in context level.
 140       * @since Moodle 4.2
 141       *
 142       * @return int[]
 143       */
 144      protected static function get_compatible_role_archetypes(): array {
 145          return ['editingteacher', 'teacher', 'student'];
 146      }
 147  
 148      /**
 149       * Returns list of all possible parent context levels.
 150       * @since Moodle 4.2
 151       *
 152       * @return int[]
 153       */
 154      public static function get_possible_parent_levels(): array {
 155          return [course::LEVEL];
 156      }
 157  
 158      /**
 159       * Returns array of relevant context capability records.
 160       *
 161       * @param string $sort
 162       * @return array
 163       */
 164      public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
 165          global $DB, $CFG;
 166  
 167          $cm = $DB->get_record('course_modules', array('id' => $this->_instanceid));
 168          $module = $DB->get_record('modules', array('id' => $cm->module));
 169  
 170          $subcaps = array();
 171  
 172          $modulepath = "{$CFG->dirroot}/mod/{$module->name}";
 173          if (file_exists("{$modulepath}/db/subplugins.json")) {
 174              $subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes;
 175          } else if (file_exists("{$modulepath}/db/subplugins.php")) {
 176              debugging('Use of subplugins.php has been deprecated. ' .
 177                  'Please update your plugin to provide a subplugins.json file instead.',
 178                  DEBUG_DEVELOPER);
 179              $subplugins = array();  // Should be redefined in the file.
 180              include("{$modulepath}/db/subplugins.php");
 181          }
 182  
 183          if (!empty($subplugins)) {
 184              foreach (array_keys($subplugins) as $subplugintype) {
 185                  foreach (array_keys(\core_component::get_plugin_list($subplugintype)) as $subpluginname) {
 186                      $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
 187                  }
 188              }
 189          }
 190  
 191          $modfile = "{$modulepath}/lib.php";
 192          $extracaps = array();
 193          if (file_exists($modfile)) {
 194              include_once($modfile);
 195              $modfunction = $module->name.'_get_extra_capabilities';
 196              if (function_exists($modfunction)) {
 197                  $extracaps = $modfunction();
 198              }
 199          }
 200  
 201          $extracaps = array_merge($subcaps, $extracaps);
 202          $extra = '';
 203          list($extra, $params) = $DB->get_in_or_equal(
 204              $extracaps, SQL_PARAMS_NAMED, 'cap0', true, '');
 205          if (!empty($extra)) {
 206              $extra = "OR name $extra";
 207          }
 208  
 209          // Fetch the list of modules, and remove this one.
 210          $components = \core_component::get_component_list();
 211          $componentnames = $components['mod'];
 212          unset($componentnames["mod_{$module->name}"]);
 213          $componentnames = array_keys($componentnames);
 214  
 215          // Exclude all other modules.
 216          list($notcompsql, $notcompparams) = $DB->get_in_or_equal($componentnames, SQL_PARAMS_NAMED, 'notcomp', false);
 217          $params = array_merge($params, $notcompparams);
 218  
 219          // Exclude other component submodules.
 220          $i = 0;
 221          $ignorecomponents = [];
 222          foreach ($componentnames as $mod) {
 223              if ($subplugins = \core_component::get_subplugins($mod)) {
 224                  foreach (array_keys($subplugins) as $subplugintype) {
 225                      $paramname = "notlike{$i}";
 226                      $ignorecomponents[] = $DB->sql_like('component', ":{$paramname}", true, true, true);
 227                      $params[$paramname] = "{$subplugintype}_%";
 228                      $i++;
 229                  }
 230              }
 231          }
 232          $notlikesql = "(" . implode(' AND ', $ignorecomponents) . ")";
 233  
 234          $sql = "SELECT *
 235                    FROM {capabilities}
 236                   WHERE (contextlevel = ".self::LEVEL."
 237                     AND component {$notcompsql}
 238                     AND {$notlikesql})
 239                         $extra
 240                ORDER BY $sort";
 241  
 242          return $DB->get_records_sql($sql, $params);
 243      }
 244  
 245      /**
 246       * Is this context part of any course? If yes return course context.
 247       *
 248       * @param bool $strict true means throw exception if not found, false means return false if not found
 249       * @return course|false context of the enclosing course, null if not found or exception
 250       */
 251      public function get_course_context($strict = true) {
 252          return $this->get_parent_context();
 253      }
 254  
 255      /**
 256       * Returns module context instance.
 257       *
 258       * @param int $cmid id of the record from {course_modules} table; pass cmid there, NOT id in the instance column
 259       * @param int $strictness
 260       * @return module|false context instance
 261       */
 262      public static function instance($cmid, $strictness = MUST_EXIST) {
 263          global $DB;
 264  
 265          if ($context = context::cache_get(self::LEVEL, $cmid)) {
 266              return $context;
 267          }
 268  
 269          if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $cmid))) {
 270              if ($cm = $DB->get_record('course_modules', array('id' => $cmid), 'id,course', $strictness)) {
 271                  $parentcontext = course::instance($cm->course);
 272                  $record = context::insert_context_record(self::LEVEL, $cm->id, $parentcontext->path);
 273              }
 274          }
 275  
 276          if ($record) {
 277              $context = new module($record);
 278              context::cache_add($context);
 279              return $context;
 280          }
 281  
 282          return false;
 283      }
 284  
 285      /**
 286       * Create missing context instances at module context level
 287       */
 288      protected static function create_level_instances() {
 289          global $DB;
 290  
 291          $sql = "SELECT " . self::LEVEL . ", cm.id
 292                    FROM {course_modules} cm
 293                   WHERE NOT EXISTS (SELECT 'x'
 294                                       FROM {context} cx
 295                                      WHERE cm.id = cx.instanceid AND cx.contextlevel=" . self::LEVEL . ")";
 296          $contextdata = $DB->get_recordset_sql($sql);
 297          foreach ($contextdata as $context) {
 298              context::insert_context_record(self::LEVEL, $context->id, null);
 299          }
 300          $contextdata->close();
 301      }
 302  
 303      /**
 304       * Returns sql necessary for purging of stale context instances.
 305       *
 306       * @return string cleanup SQL
 307       */
 308      protected static function get_cleanup_sql() {
 309          $sql = "
 310                    SELECT c.*
 311                      FROM {context} c
 312           LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id
 313                     WHERE cm.id IS NULL AND c.contextlevel = " . self::LEVEL . "
 314                 ";
 315  
 316          return $sql;
 317      }
 318  
 319      /**
 320       * Rebuild context paths and depths at module context level.
 321       *
 322       * @param bool $force
 323       */
 324      protected static function build_paths($force) {
 325          global $DB;
 326  
 327          if ($force || $DB->record_exists_select('context', "contextlevel = " . self::LEVEL . " AND (depth = 0 OR path IS NULL)")) {
 328              if ($force) {
 329                  $ctxemptyclause = '';
 330              } else {
 331                  $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
 332              }
 333  
 334              $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
 335                      SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
 336                        FROM {context} ctx
 337                        JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = " . self::LEVEL . ")
 338                        JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = " . course::LEVEL . ")
 339                       WHERE pctx.path IS NOT NULL AND pctx.depth > 0
 340                             $ctxemptyclause";
 341              $trans = $DB->start_delegated_transaction();
 342              $DB->delete_records('context_temp');
 343              $DB->execute($sql);
 344              context::merge_context_temp_table();
 345              $DB->delete_records('context_temp');
 346              $trans->allow_commit();
 347          }
 348      }
 349  }