Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://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 <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Defines classes used for plugins management
  19   *
  20   * This library provides a unified interface to various plugin types in
  21   * Moodle. It is mainly used by the plugins management admin page and the
  22   * plugins check page during the upgrade.
  23   *
  24   * @package    core
  25   * @copyright  2011 David Mudrak <david@moodle.com>
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  class core_plugin_manager {
  29  
  30      /** the plugin is shipped with standard Moodle distribution */
  31      const PLUGIN_SOURCE_STANDARD    = 'std';
  32      /** the plugin is added extension */
  33      const PLUGIN_SOURCE_EXTENSION   = 'ext';
  34  
  35      /** the plugin uses neither database nor capabilities, no versions */
  36      const PLUGIN_STATUS_NODB        = 'nodb';
  37      /** the plugin is up-to-date */
  38      const PLUGIN_STATUS_UPTODATE    = 'uptodate';
  39      /** the plugin is about to be installed */
  40      const PLUGIN_STATUS_NEW         = 'new';
  41      /** the plugin is about to be upgraded */
  42      const PLUGIN_STATUS_UPGRADE     = 'upgrade';
  43      /** the standard plugin is about to be deleted */
  44      const PLUGIN_STATUS_DELETE     = 'delete';
  45      /** the version at the disk is lower than the one already installed */
  46      const PLUGIN_STATUS_DOWNGRADE   = 'downgrade';
  47      /** the plugin is installed but missing from disk */
  48      const PLUGIN_STATUS_MISSING     = 'missing';
  49  
  50      /** the given requirement/dependency is fulfilled */
  51      const REQUIREMENT_STATUS_OK = 'ok';
  52      /** the plugin requires higher core/other plugin version than is currently installed */
  53      const REQUIREMENT_STATUS_OUTDATED = 'outdated';
  54      /** the required dependency is not installed */
  55      const REQUIREMENT_STATUS_MISSING = 'missing';
  56      /** the current Moodle version is too high for plugin. */
  57      const REQUIREMENT_STATUS_NEWER = 'newer';
  58  
  59      /** the required dependency is available in the plugins directory */
  60      const REQUIREMENT_AVAILABLE = 'available';
  61      /** the required dependency is available in the plugins directory */
  62      const REQUIREMENT_UNAVAILABLE = 'unavailable';
  63  
  64      /** the moodle version is explicitly supported */
  65      const VERSION_SUPPORTED = 'supported';
  66      /** the moodle version is not explicitly supported */
  67      const VERSION_NOT_SUPPORTED = 'notsupported';
  68      /** the plugin does not specify supports */
  69      const VERSION_NO_SUPPORTS = 'nosupports';
  70  
  71      /** @var core_plugin_manager holds the singleton instance */
  72      protected static $singletoninstance;
  73      /** @var array of raw plugins information */
  74      protected $pluginsinfo = null;
  75      /** @var array of raw subplugins information */
  76      protected $subpluginsinfo = null;
  77      /** @var array cache information about availability in the plugins directory if requesting "at least" version */
  78      protected $remotepluginsinfoatleast = null;
  79      /** @var array cache information about availability in the plugins directory if requesting exact version */
  80      protected $remotepluginsinfoexact = null;
  81      /** @var array list of installed plugins $name=>$version */
  82      protected $installedplugins = null;
  83      /** @var array list of all enabled plugins $name=>$name */
  84      protected $enabledplugins = null;
  85      /** @var array list of all enabled plugins $name=>$diskversion */
  86      protected $presentplugins = null;
  87      /** @var array reordered list of plugin types */
  88      protected $plugintypes = null;
  89      /** @var \core\update\code_manager code manager to use for plugins code operations */
  90      protected $codemanager = null;
  91      /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */
  92      protected $updateapiclient = null;
  93  
  94      /**
  95       * Direct initiation not allowed, use the factory method {@link self::instance()}
  96       */
  97      protected function __construct() {
  98      }
  99  
 100      /**
 101       * Sorry, this is singleton
 102       */
 103      protected function __clone() {
 104      }
 105  
 106      /**
 107       * Factory method for this class
 108       *
 109       * @return static the singleton instance
 110       */
 111      public static function instance() {
 112          if (is_null(static::$singletoninstance)) {
 113              static::$singletoninstance = new static();
 114          }
 115          return static::$singletoninstance;
 116      }
 117  
 118      /**
 119       * Reset all caches.
 120       * @param bool $phpunitreset
 121       */
 122      public static function reset_caches($phpunitreset = false) {
 123          if ($phpunitreset) {
 124              static::$singletoninstance = null;
 125          } else {
 126              if (static::$singletoninstance) {
 127                  static::$singletoninstance->pluginsinfo = null;
 128                  static::$singletoninstance->subpluginsinfo = null;
 129                  static::$singletoninstance->remotepluginsinfoatleast = null;
 130                  static::$singletoninstance->remotepluginsinfoexact = null;
 131                  static::$singletoninstance->installedplugins = null;
 132                  static::$singletoninstance->enabledplugins = null;
 133                  static::$singletoninstance->presentplugins = null;
 134                  static::$singletoninstance->plugintypes = null;
 135                  static::$singletoninstance->codemanager = null;
 136                  static::$singletoninstance->updateapiclient = null;
 137              }
 138          }
 139          $cache = cache::make('core', 'plugin_manager');
 140          $cache->purge();
 141      }
 142  
 143      /**
 144       * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
 145       *
 146       * @see self::reorder_plugin_types()
 147       * @return array (string)name => (string)location
 148       */
 149      public function get_plugin_types() {
 150          if (func_num_args() > 0) {
 151              if (!func_get_arg(0)) {
 152                  throw new coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
 153              }
 154          }
 155          if ($this->plugintypes) {
 156              return $this->plugintypes;
 157          }
 158  
 159          $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
 160          return $this->plugintypes;
 161      }
 162  
 163      /**
 164       * Load list of installed plugins,
 165       * always call before using $this->installedplugins.
 166       *
 167       * This method is caching results for all plugins.
 168       */
 169      protected function load_installed_plugins() {
 170          global $DB, $CFG;
 171  
 172          if ($this->installedplugins) {
 173              return;
 174          }
 175  
 176          if (empty($CFG->version)) {
 177              // Nothing installed yet.
 178              $this->installedplugins = array();
 179              return;
 180          }
 181  
 182          $cache = cache::make('core', 'plugin_manager');
 183          $installed = $cache->get('installed');
 184  
 185          if (is_array($installed)) {
 186              $this->installedplugins = $installed;
 187              return;
 188          }
 189  
 190          $this->installedplugins = array();
 191  
 192          $versions = $DB->get_records('config_plugins', array('name'=>'version'));
 193          foreach ($versions as $version) {
 194              $parts = explode('_', $version->plugin, 2);
 195              if (!isset($parts[1])) {
 196                  // Invalid component, there must be at least one "_".
 197                  continue;
 198              }
 199              // Do not verify here if plugin type and name are valid.
 200              $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
 201          }
 202  
 203          foreach ($this->installedplugins as $key => $value) {
 204              ksort($this->installedplugins[$key]);
 205          }
 206  
 207          $cache->set('installed', $this->installedplugins);
 208      }
 209  
 210      /**
 211       * Return list of installed plugins of given type.
 212       * @param string $type
 213       * @return array $name=>$version
 214       */
 215      public function get_installed_plugins($type) {
 216          $this->load_installed_plugins();
 217          if (isset($this->installedplugins[$type])) {
 218              return $this->installedplugins[$type];
 219          }
 220          return array();
 221      }
 222  
 223      /**
 224       * Load list of all enabled plugins,
 225       * call before using $this->enabledplugins.
 226       *
 227       * This method is caching results from individual plugin info classes.
 228       */
 229      protected function load_enabled_plugins() {
 230          global $CFG;
 231  
 232          if ($this->enabledplugins) {
 233              return;
 234          }
 235  
 236          if (empty($CFG->version)) {
 237              $this->enabledplugins = array();
 238              return;
 239          }
 240  
 241          $cache = cache::make('core', 'plugin_manager');
 242          $enabled = $cache->get('enabled');
 243  
 244          if (is_array($enabled)) {
 245              $this->enabledplugins = $enabled;
 246              return;
 247          }
 248  
 249          $this->enabledplugins = array();
 250  
 251          require_once($CFG->libdir.'/adminlib.php');
 252  
 253          $plugintypes = core_component::get_plugin_types();
 254          foreach ($plugintypes as $plugintype => $fulldir) {
 255              $plugininfoclass = static::resolve_plugininfo_class($plugintype);
 256              if (class_exists($plugininfoclass)) {
 257                  $enabled = $plugininfoclass::get_enabled_plugins();
 258                  if (!is_array($enabled)) {
 259                      continue;
 260                  }
 261                  $this->enabledplugins[$plugintype] = $enabled;
 262              }
 263          }
 264  
 265          $cache->set('enabled', $this->enabledplugins);
 266      }
 267  
 268      /**
 269       * Get list of enabled plugins of given type,
 270       * the result may contain missing plugins.
 271       *
 272       * @param string $type
 273       * @return array|null  list of enabled plugins of this type, null if unknown
 274       */
 275      public function get_enabled_plugins($type) {
 276          $this->load_enabled_plugins();
 277          if (isset($this->enabledplugins[$type])) {
 278              return $this->enabledplugins[$type];
 279          }
 280          return null;
 281      }
 282  
 283      /**
 284       * Load list of all present plugins - call before using $this->presentplugins.
 285       */
 286      protected function load_present_plugins() {
 287          if ($this->presentplugins) {
 288              return;
 289          }
 290  
 291          $cache = cache::make('core', 'plugin_manager');
 292          $present = $cache->get('present');
 293  
 294          if (is_array($present)) {
 295              $this->presentplugins = $present;
 296              return;
 297          }
 298  
 299          $this->presentplugins = array();
 300  
 301          $plugintypes = core_component::get_plugin_types();
 302          foreach ($plugintypes as $type => $typedir) {
 303              $plugs = core_component::get_plugin_list($type);
 304              foreach ($plugs as $plug => $fullplug) {
 305                  $module = new stdClass();
 306                  $plugin = new stdClass();
 307                  $plugin->version = null;
 308                  include ($fullplug.'/version.php');
 309  
 310                  // Check if the legacy $module syntax is still used.
 311                  if (!is_object($module) or (count((array)$module) > 0)) {
 312                      debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
 313                      $skipcache = true;
 314                  }
 315  
 316                  // Check if the component is properly declared.
 317                  if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
 318                      debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
 319                      $skipcache = true;
 320                  }
 321  
 322                  $this->presentplugins[$type][$plug] = $plugin;
 323              }
 324          }
 325  
 326          if (empty($skipcache)) {
 327              $cache->set('present', $this->presentplugins);
 328          }
 329      }
 330  
 331      /**
 332       * Get list of present plugins of given type.
 333       *
 334       * @param string $type
 335       * @return array|null  list of presnet plugins $name=>$diskversion, null if unknown
 336       */
 337      public function get_present_plugins($type) {
 338          $this->load_present_plugins();
 339          if (isset($this->presentplugins[$type])) {
 340              return $this->presentplugins[$type];
 341          }
 342          return null;
 343      }
 344  
 345      /**
 346       * Returns a tree of known plugins and information about them
 347       *
 348       * @return array 2D array. The first keys are plugin type names (e.g. qtype);
 349       *      the second keys are the plugin local name (e.g. multichoice); and
 350       *      the values are the corresponding objects extending {@link \core\plugininfo\base}
 351       */
 352      public function get_plugins() {
 353          $this->init_pluginsinfo_property();
 354  
 355          // Make sure all types are initialised.
 356          foreach ($this->pluginsinfo as $plugintype => $list) {
 357              if ($list === null) {
 358                  $this->get_plugins_of_type($plugintype);
 359              }
 360          }
 361  
 362          return $this->pluginsinfo;
 363      }
 364  
 365      /**
 366       * Returns list of known plugins of the given type.
 367       *
 368       * This method returns the subset of the tree returned by {@link self::get_plugins()}.
 369       * If the given type is not known, empty array is returned.
 370       *
 371       * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
 372       * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
 373       */
 374      public function get_plugins_of_type($type) {
 375          global $CFG;
 376  
 377          $this->init_pluginsinfo_property();
 378  
 379          if (!array_key_exists($type, $this->pluginsinfo)) {
 380              return array();
 381          }
 382  
 383          if (is_array($this->pluginsinfo[$type])) {
 384              return $this->pluginsinfo[$type];
 385          }
 386  
 387          $types = core_component::get_plugin_types();
 388  
 389          if (!isset($types[$type])) {
 390              // Orphaned subplugins!
 391              $plugintypeclass = static::resolve_plugininfo_class($type);
 392              $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
 393              return $this->pluginsinfo[$type];
 394          }
 395  
 396          /** @var \core\plugininfo\base $plugintypeclass */
 397          $plugintypeclass = static::resolve_plugininfo_class($type);
 398          $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
 399          $this->pluginsinfo[$type] = $plugins;
 400  
 401          return $this->pluginsinfo[$type];
 402      }
 403  
 404      /**
 405       * Init placeholder array for plugin infos.
 406       */
 407      protected function init_pluginsinfo_property() {
 408          if (is_array($this->pluginsinfo)) {
 409              return;
 410          }
 411          $this->pluginsinfo = array();
 412  
 413          $plugintypes = $this->get_plugin_types();
 414  
 415          foreach ($plugintypes as $plugintype => $plugintyperootdir) {
 416              $this->pluginsinfo[$plugintype] = null;
 417          }
 418  
 419          // Add orphaned subplugin types.
 420          $this->load_installed_plugins();
 421          foreach ($this->installedplugins as $plugintype => $unused) {
 422              if (!isset($plugintypes[$plugintype])) {
 423                  $this->pluginsinfo[$plugintype] = null;
 424              }
 425          }
 426      }
 427  
 428      /**
 429       * Find the plugin info class for given type.
 430       *
 431       * @param string $type
 432       * @return string name of pluginfo class for give plugin type
 433       */
 434      public static function resolve_plugininfo_class($type) {
 435          $plugintypes = core_component::get_plugin_types();
 436          if (!isset($plugintypes[$type])) {
 437              return '\core\plugininfo\orphaned';
 438          }
 439  
 440          $parent = core_component::get_subtype_parent($type);
 441  
 442          if ($parent) {
 443              $class = '\\'.$parent.'\plugininfo\\' . $type;
 444              if (class_exists($class)) {
 445                  $plugintypeclass = $class;
 446              } else {
 447                  if ($dir = core_component::get_component_directory($parent)) {
 448                      // BC only - use namespace instead!
 449                      if (file_exists("$dir/adminlib.php")) {
 450                          global $CFG;
 451                          include_once("$dir/adminlib.php");
 452                      }
 453                      if (class_exists('plugininfo_' . $type)) {
 454                          $plugintypeclass = 'plugininfo_' . $type;
 455                          debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
 456                      } else {
 457                          debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
 458                          $plugintypeclass = '\core\plugininfo\general';
 459                      }
 460                  } else {
 461                      $plugintypeclass = '\core\plugininfo\general';
 462                  }
 463              }
 464          } else {
 465              $class = '\core\plugininfo\\' . $type;
 466              if (class_exists($class)) {
 467                  $plugintypeclass = $class;
 468              } else {
 469                  debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
 470                  $plugintypeclass = '\core\plugininfo\general';
 471              }
 472          }
 473  
 474          if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
 475              throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
 476          }
 477  
 478          return $plugintypeclass;
 479      }
 480  
 481      /**
 482       * Returns list of all known subplugins of the given plugin.
 483       *
 484       * For plugins that do not provide subplugins (i.e. there is no support for it),
 485       * empty array is returned.
 486       *
 487       * @param string $component full component name, e.g. 'mod_workshop'
 488       * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
 489       */
 490      public function get_subplugins_of_plugin($component) {
 491  
 492          $pluginfo = $this->get_plugin_info($component);
 493  
 494          if (is_null($pluginfo)) {
 495              return array();
 496          }
 497  
 498          $subplugins = $this->get_subplugins();
 499  
 500          if (!isset($subplugins[$pluginfo->component])) {
 501              return array();
 502          }
 503  
 504          $list = array();
 505  
 506          foreach ($subplugins[$pluginfo->component] as $subdata) {
 507              foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
 508                  $list[$subpluginfo->component] = $subpluginfo;
 509              }
 510          }
 511  
 512          return $list;
 513      }
 514  
 515      /**
 516       * Returns list of plugins that define their subplugins and the information
 517       * about them from the db/subplugins.json file.
 518       *
 519       * @return array with keys like 'mod_quiz', and values the data from the
 520       *      corresponding db/subplugins.json file.
 521       */
 522      public function get_subplugins() {
 523  
 524          if (is_array($this->subpluginsinfo)) {
 525              return $this->subpluginsinfo;
 526          }
 527  
 528          $plugintypes = core_component::get_plugin_types();
 529  
 530          $this->subpluginsinfo = array();
 531          foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
 532              foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
 533                  $component = $type.'_'.$plugin;
 534                  $subplugins = core_component::get_subplugins($component);
 535                  if (!$subplugins) {
 536                      continue;
 537                  }
 538                  $this->subpluginsinfo[$component] = array();
 539                  foreach ($subplugins as $subplugintype => $ignored) {
 540                      $subplugin = new stdClass();
 541                      $subplugin->type = $subplugintype;
 542                      $subplugin->typerootdir = $plugintypes[$subplugintype];
 543                      $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
 544                  }
 545              }
 546          }
 547          return $this->subpluginsinfo;
 548      }
 549  
 550      /**
 551       * Returns the name of the plugin that defines the given subplugin type
 552       *
 553       * If the given subplugin type is not actually a subplugin, returns false.
 554       *
 555       * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
 556       * @return false|string the name of the parent plugin, eg. mod_workshop
 557       */
 558      public function get_parent_of_subplugin($subplugintype) {
 559          $parent = core_component::get_subtype_parent($subplugintype);
 560          if (!$parent) {
 561              return false;
 562          }
 563          return $parent;
 564      }
 565  
 566      /**
 567       * Returns a localized name of a given plugin
 568       *
 569       * @param string $component name of the plugin, eg mod_workshop or auth_ldap
 570       * @return string
 571       */
 572      public function plugin_name($component) {
 573  
 574          $pluginfo = $this->get_plugin_info($component);
 575  
 576          if (is_null($pluginfo)) {
 577              throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
 578          }
 579  
 580          return $pluginfo->displayname;
 581      }
 582  
 583      /**
 584       * Returns a localized name of a plugin typed in singular form
 585       *
 586       * Most plugin types define their names in core_plugin lang file. In case of subplugins,
 587       * we try to ask the parent plugin for the name. In the worst case, we will return
 588       * the value of the passed $type parameter.
 589       *
 590       * @param string $type the type of the plugin, e.g. mod or workshopform
 591       * @return string
 592       */
 593      public function plugintype_name($type) {
 594  
 595          if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
 596              // For most plugin types, their names are defined in core_plugin lang file.
 597              return get_string('type_' . $type, 'core_plugin');
 598  
 599          } else if ($parent = $this->get_parent_of_subplugin($type)) {
 600              // If this is a subplugin, try to ask the parent plugin for the name.
 601              if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
 602                  return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
 603              } else {
 604                  return $this->plugin_name($parent) . ' / ' . $type;
 605              }
 606  
 607          } else {
 608              return $type;
 609          }
 610      }
 611  
 612      /**
 613       * Returns a localized name of a plugin type in plural form
 614       *
 615       * Most plugin types define their names in core_plugin lang file. In case of subplugins,
 616       * we try to ask the parent plugin for the name. In the worst case, we will return
 617       * the value of the passed $type parameter.
 618       *
 619       * @param string $type the type of the plugin, e.g. mod or workshopform
 620       * @return string
 621       */
 622      public function plugintype_name_plural($type) {
 623  
 624          if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
 625              // For most plugin types, their names are defined in core_plugin lang file.
 626              return get_string('type_' . $type . '_plural', 'core_plugin');
 627  
 628          } else if ($parent = $this->get_parent_of_subplugin($type)) {
 629              // If this is a subplugin, try to ask the parent plugin for the name.
 630              if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
 631                  return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
 632              } else {
 633                  return $this->plugin_name($parent) . ' / ' . $type;
 634              }
 635  
 636          } else {
 637              return $type;
 638          }
 639      }
 640  
 641      /**
 642       * Returns information about the known plugin, or null
 643       *
 644       * @param string $component frankenstyle component name.
 645       * @return \core\plugininfo\base|null the corresponding plugin information.
 646       */
 647      public function get_plugin_info($component) {
 648          list($type, $name) = core_component::normalize_component($component);
 649          $plugins = $this->get_plugins_of_type($type);
 650          if (isset($plugins[$name])) {
 651              return $plugins[$name];
 652          } else {
 653              return null;
 654          }
 655      }
 656  
 657      /**
 658       * Check to see if the current version of the plugin seems to be a checkout of an external repository.
 659       *
 660       * @param string $component frankenstyle component name
 661       * @return false|string
 662       */
 663      public function plugin_external_source($component) {
 664  
 665          $plugininfo = $this->get_plugin_info($component);
 666  
 667          if (is_null($plugininfo)) {
 668              return false;
 669          }
 670  
 671          $pluginroot = $plugininfo->rootdir;
 672  
 673          if (is_dir($pluginroot.'/.git')) {
 674              return 'git';
 675          }
 676  
 677          if (is_file($pluginroot.'/.git')) {
 678              return 'git-submodule';
 679          }
 680  
 681          if (is_dir($pluginroot.'/CVS')) {
 682              return 'cvs';
 683          }
 684  
 685          if (is_dir($pluginroot.'/.svn')) {
 686              return 'svn';
 687          }
 688  
 689          if (is_dir($pluginroot.'/.hg')) {
 690              return 'mercurial';
 691          }
 692  
 693          return false;
 694      }
 695  
 696      /**
 697       * Get a list of any other plugins that require this one.
 698       * @param string $component frankenstyle component name.
 699       * @return array of frankensyle component names that require this one.
 700       */
 701      public function other_plugins_that_require($component) {
 702          $others = array();
 703          foreach ($this->get_plugins() as $type => $plugins) {
 704              foreach ($plugins as $plugin) {
 705                  $required = $plugin->get_other_required_plugins();
 706                  if (isset($required[$component])) {
 707                      $others[] = $plugin->component;
 708                  }
 709              }
 710          }
 711          return $others;
 712      }
 713  
 714      /**
 715       * Check a dependencies list against the list of installed plugins.
 716       * @param array $dependencies compenent name to required version or ANY_VERSION.
 717       * @return bool true if all the dependencies are satisfied.
 718       */
 719      public function are_dependencies_satisfied($dependencies) {
 720          foreach ($dependencies as $component => $requiredversion) {
 721              $otherplugin = $this->get_plugin_info($component);
 722              if (is_null($otherplugin)) {
 723                  return false;
 724              }
 725  
 726              if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
 727                  return false;
 728              }
 729          }
 730  
 731          return true;
 732      }
 733  
 734      /**
 735       * Checks all dependencies for all installed plugins
 736       *
 737       * This is used by install and upgrade. The array passed by reference as the second
 738       * argument is populated with the list of plugins that have failed dependencies (note that
 739       * a single plugin can appear multiple times in the $failedplugins).
 740       *
 741       * @param int $moodleversion the version from version.php.
 742       * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
 743       * @param int $branch the current moodle branch, null if not provided
 744       * @return bool true if all the dependencies are satisfied for all plugins.
 745       */
 746      public function all_plugins_ok($moodleversion, &$failedplugins = array(), $branch = null) {
 747          global $CFG;
 748          if (empty($branch)) {
 749              $branch = $CFG->branch ?? '';
 750              if (empty($branch)) {
 751                  // During initial install there is no branch set.
 752                  require($CFG->dirroot . '/version.php');
 753                  $branch = (int)$branch;
 754                  // Force CFG->branch to int value during install.
 755                  $CFG->branch = $branch;
 756              }
 757          }
 758          $return = true;
 759          foreach ($this->get_plugins() as $type => $plugins) {
 760              foreach ($plugins as $plugin) {
 761  
 762                  if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
 763                      $return = false;
 764                      $failedplugins[] = $plugin->component;
 765                  }
 766  
 767                  if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
 768                      $return = false;
 769                      $failedplugins[] = $plugin->component;
 770                  }
 771  
 772                  if (!$plugin->is_core_compatible_satisfied($branch)) {
 773                      $return = false;
 774                      $failedplugins[] = $plugin->component;
 775                  }
 776              }
 777          }
 778  
 779          return $return;
 780      }
 781  
 782      /**
 783       * Resolve requirements and dependencies of a plugin.
 784       *
 785       * Returns an array of objects describing the requirement/dependency,
 786       * indexed by the frankenstyle name of the component. The returned array
 787       * can be empty. The objects in the array have following properties:
 788       *
 789       *  ->(numeric)hasver
 790       *  ->(numeric)reqver
 791       *  ->(string)status
 792       *  ->(string)availability
 793       *
 794       * @param \core\plugininfo\base $plugin the plugin we are checking
 795       * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
 796       * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
 797       * @return array of objects
 798       */
 799      public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
 800          global $CFG;
 801  
 802          if ($plugin->versiondisk === null) {
 803              // Missing from disk, we have no version.php to read from.
 804              return array();
 805          }
 806  
 807          if ($moodleversion === null) {
 808              $moodleversion = $CFG->version;
 809          }
 810  
 811          if ($moodlebranch === null) {
 812              $moodlebranch = $CFG->branch;
 813          }
 814  
 815          $reqs = array();
 816          $reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch);
 817  
 818          if (!empty($reqcore)) {
 819              $reqs['core'] = $reqcore;
 820          }
 821  
 822          foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
 823              $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
 824          }
 825  
 826          return $reqs;
 827      }
 828  
 829      /**
 830       * Helper method to resolve plugin's requirements on the moodle core.
 831       *
 832       * @param \core\plugininfo\base $plugin the plugin we are checking
 833       * @param string|int|double $moodleversion moodle core branch to check against
 834       * @return stdObject
 835       */
 836      protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion, $moodlebranch) {
 837  
 838          $reqs = (object)array(
 839              'hasver' => null,
 840              'reqver' => null,
 841              'status' => null,
 842              'availability' => null,
 843          );
 844          $reqs->hasver = $moodleversion;
 845  
 846          if (empty($plugin->versionrequires)) {
 847              $reqs->reqver = ANY_VERSION;
 848          } else {
 849              $reqs->reqver = $plugin->versionrequires;
 850          }
 851  
 852          if ($plugin->is_core_dependency_satisfied($moodleversion)) {
 853              $reqs->status = self::REQUIREMENT_STATUS_OK;
 854          } else {
 855              $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
 856          }
 857  
 858          // Now check if there is an explicit incompatible, supersedes requires.
 859          if (isset($plugin->pluginincompatible) && $plugin->pluginincompatible != null) {
 860              if (!$plugin->is_core_compatible_satisfied($moodlebranch)) {
 861  
 862                  $reqs->status = self::REQUIREMENT_STATUS_NEWER;
 863              }
 864          }
 865  
 866          return $reqs;
 867      }
 868  
 869      /**
 870       * Helper method to resolve plugin's dependecies on other plugins.
 871       *
 872       * @param \core\plugininfo\base $plugin the plugin we are checking
 873       * @param string $otherpluginname
 874       * @param string|int $requiredversion
 875       * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
 876       * @return stdClass
 877       */
 878      protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
 879              $requiredversion, $moodlebranch) {
 880  
 881          $reqs = (object)array(
 882              'hasver' => null,
 883              'reqver' => null,
 884              'status' => null,
 885              'availability' => null,
 886          );
 887  
 888          $otherplugin = $this->get_plugin_info($otherpluginname);
 889  
 890          if ($otherplugin !== null) {
 891              // The required plugin is installed.
 892              $reqs->hasver = $otherplugin->versiondisk;
 893              $reqs->reqver = $requiredversion;
 894              // Check it has sufficient version.
 895              if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
 896                  $reqs->status = self::REQUIREMENT_STATUS_OK;
 897              } else {
 898                  $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
 899              }
 900  
 901          } else {
 902              // The required plugin is not installed.
 903              $reqs->hasver = null;
 904              $reqs->reqver = $requiredversion;
 905              $reqs->status = self::REQUIREMENT_STATUS_MISSING;
 906          }
 907  
 908          if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
 909              if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
 910                  $reqs->availability = self::REQUIREMENT_AVAILABLE;
 911              } else {
 912                  $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
 913              }
 914          }
 915  
 916          return $reqs;
 917      }
 918  
 919      /**
 920       * Helper method to determine whether a moodle version is explicitly supported.
 921       *
 922       * @param \core\plugininfo\base $plugin the plugin we are checking
 923       * @param int $branch the moodle branch to check support for
 924       * @return string
 925       */
 926      public function check_explicitly_supported($plugin, $branch) : string {
 927          // Check for correctly formed supported.
 928          if (isset($plugin->pluginsupported)) {
 929              // Broken apart for readability.
 930              $error = false;
 931              if (!is_array($plugin->pluginsupported)) {
 932                  $error = true;
 933              }
 934              if (!is_int($plugin->pluginsupported[0]) || !is_int($plugin->pluginsupported[1])) {
 935                  $error = true;
 936              }
 937              if (count($plugin->pluginsupported) != 2) {
 938                  $error = true;
 939              }
 940              if ($error) {
 941                  throw new coding_exception(get_string('err_supported_syntax', 'core_plugin'));
 942              }
 943          }
 944  
 945          if (isset($plugin->pluginsupported) && $plugin->pluginsupported != null) {
 946              if ($plugin->pluginsupported[0] <= $branch && $branch <= $plugin->pluginsupported[1]) {
 947                  return self::VERSION_SUPPORTED;
 948              } else {
 949                  return self::VERSION_NOT_SUPPORTED;
 950              }
 951          } else {
 952              // If supports aren't specified, but incompatible is, return not supported if not incompatible.
 953              if (!isset($plugin->pluginsupported) && isset($plugin->pluginincompatible) && !empty($plugin->pluginincompatible)) {
 954                  if (!$plugin->is_core_compatible_satisfied($branch)) {
 955                      return self::VERSION_NOT_SUPPORTED;
 956                  }
 957              }
 958              return self::VERSION_NO_SUPPORTS;
 959          }
 960      }
 961  
 962      /**
 963       * Is the given plugin version available in the plugins directory?
 964       *
 965       * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
 966       * parameter is interpretted.
 967       *
 968       * @param string $component plugin frankenstyle name
 969       * @param string|int $version ANY_VERSION or the version number
 970       * @param bool $exactmatch false if "given version or higher" is requested
 971       * @return boolean
 972       */
 973      public function is_remote_plugin_available($component, $version, $exactmatch) {
 974  
 975          $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
 976  
 977          if (empty($info)) {
 978              // There is no available plugin of that name.
 979              return false;
 980          }
 981  
 982          if (empty($info->version)) {
 983              // Plugin is known, but no suitable version was found.
 984              return false;
 985          }
 986  
 987          return true;
 988      }
 989  
 990      /**
 991       * Can the given plugin version be installed via the admin UI?
 992       *
 993       * This check should be used whenever attempting to install a plugin from
 994       * the plugins directory (new install, available update, missing dependency).
 995       *
 996       * @param string $component
 997       * @param int $version version number
 998       * @param string $reason returned code of the reason why it is not
 999       * @param bool $checkremote check this version availability on moodle server
1000       * @return boolean
1001       */
1002      public function is_remote_plugin_installable($component, $version, &$reason = null, $checkremote = true) {
1003          global $CFG;
1004  
1005          // Make sure the feature is not disabled.
1006          if (!empty($CFG->disableupdateautodeploy)) {
1007              $reason = 'disabled';
1008              return false;
1009          }
1010  
1011          // Make sure the version is available.
1012          if ($checkremote && !$this->is_remote_plugin_available($component, $version, true)) {
1013              $reason = 'remoteunavailable';
1014              return false;
1015          }
1016  
1017          // Make sure the plugin type root directory is writable.
1018          list($plugintype, $pluginname) = core_component::normalize_component($component);
1019          if (!$this->is_plugintype_writable($plugintype)) {
1020              $reason = 'notwritableplugintype';
1021              return false;
1022          }
1023  
1024          if (!$checkremote) {
1025              $remoteversion = $version;
1026          } else {
1027              $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
1028              $remoteversion = $remoteinfo->version->version;
1029          }
1030          $localinfo = $this->get_plugin_info($component);
1031  
1032          if ($localinfo) {
1033              // If the plugin is already present, prevent downgrade.
1034              if ($localinfo->versiondb > $remoteversion) {
1035                  $reason = 'cannotdowngrade';
1036                  return false;
1037              }
1038  
1039              // Make sure we have write access to all the existing code.
1040              if (is_dir($localinfo->rootdir)) {
1041                  if (!$this->is_plugin_folder_removable($component)) {
1042                      $reason = 'notwritableplugin';
1043                      return false;
1044                  }
1045              }
1046          }
1047  
1048          // Looks like it could work.
1049          return true;
1050      }
1051  
1052      /**
1053       * Given the list of remote plugin infos, return just those installable.
1054       *
1055       * This is typically used on lists returned by
1056       * {@link self::available_updates()} or {@link self::missing_dependencies()}
1057       * to perform bulk installation of remote plugins.
1058       *
1059       * @param array $remoteinfos list of {@link \core\update\remote_info}
1060       * @return array
1061       */
1062      public function filter_installable($remoteinfos) {
1063          global $CFG;
1064  
1065          if (!empty($CFG->disableupdateautodeploy)) {
1066              return array();
1067          }
1068          if (empty($remoteinfos)) {
1069              return array();
1070          }
1071          $installable = array();
1072          foreach ($remoteinfos as $index => $remoteinfo) {
1073              if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
1074                  $installable[$index] = $remoteinfo;
1075              }
1076          }
1077          return $installable;
1078      }
1079  
1080      /**
1081       * Returns information about a plugin in the plugins directory.
1082       *
1083       * This is typically used when checking for available dependencies (in
1084       * which case the $version represents minimal version we need), or
1085       * when installing an available update or a new plugin from the plugins
1086       * directory (in which case the $version is exact version we are
1087       * interested in). The interpretation of the $version is controlled
1088       * by the $exactmatch argument.
1089       *
1090       * If a plugin with the given component name is found, data about the
1091       * plugin are returned as an object. The ->version property of the object
1092       * contains the information about the particular plugin version that
1093       * matches best the given critera. The ->version property is false if no
1094       * suitable version of the plugin was found (yet the plugin itself is
1095       * known).
1096       *
1097       * See {@link \core\update\api::validate_pluginfo_format()} for the
1098       * returned data structure.
1099       *
1100       * @param string $component plugin frankenstyle name
1101       * @param string|int $version ANY_VERSION or the version number
1102       * @param bool $exactmatch false if "given version or higher" is requested
1103       * @return \core\update\remote_info|bool
1104       */
1105      public function get_remote_plugin_info($component, $version, $exactmatch) {
1106  
1107          if ($exactmatch and $version == ANY_VERSION) {
1108              throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
1109          }
1110  
1111          $client = $this->get_update_api_client();
1112  
1113          if ($exactmatch) {
1114              // Use client's get_plugin_info() method.
1115              if (!isset($this->remotepluginsinfoexact[$component][$version])) {
1116                  $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
1117              }
1118              return $this->remotepluginsinfoexact[$component][$version];
1119  
1120          } else {
1121              // Use client's find_plugin() method.
1122              if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
1123                  $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
1124              }
1125              return $this->remotepluginsinfoatleast[$component][$version];
1126          }
1127      }
1128  
1129      /**
1130       * Obtain the plugin ZIP file from the given URL
1131       *
1132       * The caller is supposed to know both downloads URL and the MD5 hash of
1133       * the ZIP contents in advance, typically by using the API requests against
1134       * the plugins directory.
1135       *
1136       * @param string $url
1137       * @param string $md5
1138       * @return string|bool full path to the file, false on error
1139       */
1140      public function get_remote_plugin_zip($url, $md5) {
1141          global $CFG;
1142  
1143          if (!empty($CFG->disableupdateautodeploy)) {
1144              return false;
1145          }
1146          return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1147      }
1148  
1149      /**
1150       * Extracts the saved plugin ZIP file.
1151       *
1152       * Returns the list of files found in the ZIP. The format of that list is
1153       * array of (string)filerelpath => (bool|string) where the array value is
1154       * either true or a string describing the problematic file.
1155       *
1156       * @see zip_packer::extract_to_pathname()
1157       * @param string $zipfilepath full path to the saved ZIP file
1158       * @param string $targetdir full path to the directory to extract the ZIP file to
1159       * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
1160       * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
1161       */
1162      public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1163          return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1164      }
1165  
1166      /**
1167       * Detects the plugin's name from its ZIP file.
1168       *
1169       * Plugin ZIP packages are expected to contain a single directory and the
1170       * directory name would become the plugin name once extracted to the Moodle
1171       * dirroot.
1172       *
1173       * @param string $zipfilepath full path to the ZIP files
1174       * @return string|bool false on error
1175       */
1176      public function get_plugin_zip_root_dir($zipfilepath) {
1177          return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1178      }
1179  
1180      /**
1181       * Return a list of missing dependencies.
1182       *
1183       * This should provide the full list of plugins that should be installed to
1184       * fulfill the requirements of all plugins, if possible.
1185       *
1186       * @param bool $availableonly return only available missing dependencies
1187       * @return array of \core\update\remote_info|bool indexed by the component name
1188       */
1189      public function missing_dependencies($availableonly=false) {
1190  
1191          $dependencies = array();
1192  
1193          foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1194              foreach ($pluginfos as $pluginname => $pluginfo) {
1195                  foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1196                      if ($reqname === 'core') {
1197                          continue;
1198                      }
1199                      if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
1200                          if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
1201                              $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
1202  
1203                              if (empty($dependencies[$reqname])) {
1204                                  $dependencies[$reqname] = $remoteinfo;
1205                              } else {
1206                                  // If resolving requirements has led to two different versions of the same
1207                                  // remote plugin, pick the higher version. This can happen in cases like one
1208                                  // plugin requiring ANY_VERSION and another plugin requiring specific higher
1209                                  // version with lower maturity of a remote plugin.
1210                                  if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
1211                                      $dependencies[$reqname] = $remoteinfo;
1212                                  }
1213                              }
1214  
1215                          } else {
1216                              if (!isset($dependencies[$reqname])) {
1217                                  // Unable to find a plugin fulfilling the requirements.
1218                                  $dependencies[$reqname] = false;
1219                              }
1220                          }
1221                      }
1222                  }
1223              }
1224          }
1225  
1226          if ($availableonly) {
1227              foreach ($dependencies as $component => $info) {
1228                  if (empty($info) or empty($info->version)) {
1229                      unset($dependencies[$component]);
1230                  }
1231              }
1232          }
1233  
1234          return $dependencies;
1235      }
1236  
1237      /**
1238       * Is it possible to uninstall the given plugin?
1239       *
1240       * False is returned if the plugininfo subclass declares the uninstall should
1241       * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1242       * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1243       * by some other installed plugin).
1244       *
1245       * @param string $component full frankenstyle name, e.g. mod_foobar
1246       * @return bool
1247       */
1248      public function can_uninstall_plugin($component) {
1249  
1250          $pluginfo = $this->get_plugin_info($component);
1251  
1252          if (is_null($pluginfo)) {
1253              return false;
1254          }
1255  
1256          if (!$this->common_uninstall_check($pluginfo)) {
1257              return false;
1258          }
1259  
1260          // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1261          $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
1262          foreach ($subplugins as $subpluginfo) {
1263              // Check if there are some other plugins requiring this subplugin
1264              // (but the parent and siblings).
1265              foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
1266                  $ismyparent = ($pluginfo->component === $requiresme);
1267                  $ismysibling = in_array($requiresme, array_keys($subplugins));
1268                  if (!$ismyparent and !$ismysibling) {
1269                      return false;
1270                  }
1271              }
1272          }
1273  
1274          // Check if there are some other plugins requiring this plugin
1275          // (but its subplugins).
1276          foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
1277              $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1278              if (!$ismysubplugin) {
1279                  return false;
1280              }
1281          }
1282  
1283          return true;
1284      }
1285  
1286      /**
1287       * Perform the installation of plugins.
1288       *
1289       * If used for installation of remote plugins from the Moodle Plugins
1290       * directory, the $plugins must be list of {@link \core\update\remote_info}
1291       * object that represent installable remote plugins. The caller can use
1292       * {@link self::filter_installable()} to prepare the list.
1293       *
1294       * If used for installation of plugins from locally available ZIP files,
1295       * the $plugins should be list of objects with properties ->component and
1296       * ->zipfilepath.
1297       *
1298       * The method uses {@link mtrace()} to produce direct output and can be
1299       * used in both web and cli interfaces.
1300       *
1301       * @param array $plugins list of plugins
1302       * @param bool $confirmed should the files be really deployed into the dirroot?
1303       * @param bool $silent perform without output
1304       * @return bool true on success
1305       */
1306      public function install_plugins(array $plugins, $confirmed, $silent) {
1307          global $CFG, $OUTPUT;
1308  
1309          if (!empty($CFG->disableupdateautodeploy)) {
1310              return false;
1311          }
1312  
1313          if (empty($plugins)) {
1314              return false;
1315          }
1316  
1317          $ok = get_string('statusok', 'core');
1318  
1319          // Let admins know they can expect more verbose output.
1320          $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);
1321  
1322          // Download all ZIP packages if we do not have them yet.
1323          $zips = array();
1324          foreach ($plugins as $plugin) {
1325              if ($plugin instanceof \core\update\remote_info) {
1326                  $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
1327                      $plugin->version->downloadmd5);
1328                  $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
1329                  $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
1330                  $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
1331                  if (!$zips[$plugin->component]) {
1332                      $silent or $this->mtrace(get_string('error'));
1333                      return false;
1334                  }
1335                  $silent or $this->mtrace($ok);
1336              } else {
1337                  if (empty($plugin->zipfilepath)) {
1338                      throw new coding_exception('Unexpected data structure provided');
1339                  }
1340                  $zips[$plugin->component] = $plugin->zipfilepath;
1341                  $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
1342              }
1343          }
1344  
1345          // Validate all downloaded packages.
1346          foreach ($plugins as $plugin) {
1347              $zipfile = $zips[$plugin->component];
1348              $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');
1349              list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1350              $tmp = make_request_directory();
1351              $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
1352              if (empty($zipcontents)) {
1353                  $silent or $this->mtrace(get_string('error'));
1354                  $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1355                  return false;
1356              }
1357  
1358              $validator = \core\update\validator::instance($tmp, $zipcontents);
1359              $validator->assert_plugin_type($plugintype);
1360              $validator->assert_moodle_version($CFG->version);
1361              // TODO Check for missing dependencies during validation.
1362              $result = $validator->execute();
1363              if (!$silent) {
1364                  $result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
1365                  foreach ($validator->get_messages() as $message) {
1366                      if ($message->level === $validator::INFO) {
1367                          // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
1368                          $level = DEBUG_NORMAL;
1369                      } else if ($message->level === $validator::DEBUG) {
1370                          // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
1371                          $level = DEBUG_ALL;
1372                      } else {
1373                          // Display [Warning] and [Error] always.
1374                          $level = null;
1375                      }
1376                      if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
1377                          $this->mtrace('  <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
1378                      } else {
1379                          $this->mtrace('  ['.$validator->message_level_name($message->level).']', ' ', $level);
1380                      }
1381                      $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
1382                      $info = $validator->message_code_info($message->msgcode, $message->addinfo);
1383                      if ($info) {
1384                          $this->mtrace('['.s($info).']', ' ', $level);
1385                      } else if (is_string($message->addinfo)) {
1386                          $this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
1387                      } else {
1388                          $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
1389                      }
1390                      if ($icon = $validator->message_help_icon($message->msgcode)) {
1391                          if (CLI_SCRIPT) {
1392                              $this->mtrace(PHP_EOL.'  ^^^ '.get_string('help').': '.
1393                                  get_string($icon->identifier.'_help', $icon->component), '', $level);
1394                          } else {
1395                              $this->mtrace($OUTPUT->render($icon), ' ', $level);
1396                          }
1397                      }
1398                      $this->mtrace(PHP_EOL, '', $level);
1399                  }
1400              }
1401              if (!$result) {
1402                  $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
1403                  return false;
1404              }
1405          }
1406          $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));
1407  
1408          if (!$confirmed) {
1409              return true;
1410          }
1411  
1412          // Extract all ZIP packs do the dirroot.
1413          foreach ($plugins as $plugin) {
1414              $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');
1415              $zipfile = $zips[$plugin->component];
1416              list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1417              $target = $this->get_plugintype_root($plugintype);
1418              if (file_exists($target.'/'.$pluginname)) {
1419                  $this->remove_plugin_folder($this->get_plugin_info($plugin->component));
1420              }
1421              if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
1422                  $silent or $this->mtrace(get_string('error'));
1423                  $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1424                  if (function_exists('opcache_reset')) {
1425                      opcache_reset();
1426                  }
1427                  return false;
1428              }
1429              $silent or $this->mtrace($ok);
1430          }
1431          if (function_exists('opcache_reset')) {
1432              opcache_reset();
1433          }
1434  
1435          return true;
1436      }
1437  
1438      /**
1439       * Outputs the given message via {@link mtrace()}.
1440       *
1441       * If $debug is provided, then the message is displayed only at the given
1442       * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
1443       * site has developer debugging level selected).
1444       *
1445       * @param string $msg message
1446       * @param string $eol end of line
1447       * @param null|int $debug null to display always, int only on given debug level
1448       */
1449      protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
1450          global $CFG;
1451  
1452          if ($debug !== null and !debugging(null, $debug)) {
1453              return;
1454          }
1455  
1456          mtrace($msg, $eol);
1457      }
1458  
1459      /**
1460       * Returns uninstall URL if exists.
1461       *
1462       * @param string $component
1463       * @param string $return either 'overview' or 'manage'
1464       * @return moodle_url uninstall URL, null if uninstall not supported
1465       */
1466      public function get_uninstall_url($component, $return = 'overview') {
1467          if (!$this->can_uninstall_plugin($component)) {
1468              return null;
1469          }
1470  
1471          $pluginfo = $this->get_plugin_info($component);
1472  
1473          if (is_null($pluginfo)) {
1474              return null;
1475          }
1476  
1477          if (method_exists($pluginfo, 'get_uninstall_url')) {
1478              debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
1479              return $pluginfo->get_uninstall_url($return);
1480          }
1481  
1482          return $pluginfo->get_default_uninstall_url($return);
1483      }
1484  
1485      /**
1486       * Uninstall the given plugin.
1487       *
1488       * Automatically cleans-up all remaining configuration data, log records, events,
1489       * files from the file pool etc.
1490       *
1491       * In the future, the functionality of {@link uninstall_plugin()} function may be moved
1492       * into this method and all the code should be refactored to use it. At the moment, we
1493       * mimic this future behaviour by wrapping that function call.
1494       *
1495       * @param string $component
1496       * @param progress_trace $progress traces the process
1497       * @return bool true on success, false on errors/problems
1498       */
1499      public function uninstall_plugin($component, progress_trace $progress) {
1500  
1501          $pluginfo = $this->get_plugin_info($component);
1502  
1503          if (is_null($pluginfo)) {
1504              return false;
1505          }
1506  
1507          // Give the pluginfo class a chance to execute some steps.
1508          $result = $pluginfo->uninstall($progress);
1509          if (!$result) {
1510              return false;
1511          }
1512  
1513          // Call the legacy core function to uninstall the plugin.
1514          ob_start();
1515          uninstall_plugin($pluginfo->type, $pluginfo->name);
1516          $progress->output(ob_get_clean());
1517  
1518          return true;
1519      }
1520  
1521      /**
1522       * Checks if there are some plugins with a known available update
1523       *
1524       * @return bool true if there is at least one available update
1525       */
1526      public function some_plugins_updatable() {
1527          foreach ($this->get_plugins() as $type => $plugins) {
1528              foreach ($plugins as $plugin) {
1529                  if ($plugin->available_updates()) {
1530                      return true;
1531                  }
1532              }
1533          }
1534  
1535          return false;
1536      }
1537  
1538      /**
1539       * Returns list of available updates for the given component.
1540       *
1541       * This method should be considered as internal API and is supposed to be
1542       * called by {@link \core\plugininfo\base::available_updates()} only
1543       * to lazy load the data once they are first requested.
1544       *
1545       * @param string $component frankenstyle name of the plugin
1546       * @return null|array array of \core\update\info objects or null
1547       */
1548      public function load_available_updates_for_plugin($component) {
1549          global $CFG;
1550  
1551          $provider = \core\update\checker::instance();
1552  
1553          if (!$provider->enabled() or during_initial_install()) {
1554              return null;
1555          }
1556  
1557          if (isset($CFG->updateminmaturity)) {
1558              $minmaturity = $CFG->updateminmaturity;
1559          } else {
1560              // This can happen during the very first upgrade to 2.3.
1561              $minmaturity = MATURITY_STABLE;
1562          }
1563  
1564          return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
1565      }
1566  
1567      /**
1568       * Returns a list of all available updates to be installed.
1569       *
1570       * This is used when "update all plugins" action is performed at the
1571       * administration UI screen.
1572       *
1573       * Returns array of remote info objects indexed by the plugin
1574       * component. If there are multiple updates available (typically a mix of
1575       * stable and non-stable ones), we pick the most mature most recent one.
1576       *
1577       * Plugins without explicit maturity are considered more mature than
1578       * release candidates but less mature than explicit stable (this should be
1579       * pretty rare case).
1580       *
1581       * @return array (string)component => (\core\update\remote_info)remoteinfo
1582       */
1583      public function available_updates() {
1584  
1585          $updates = array();
1586  
1587          foreach ($this->get_plugins() as $type => $plugins) {
1588              foreach ($plugins as $plugin) {
1589                  $availableupdates = $plugin->available_updates();
1590                  if (empty($availableupdates)) {
1591                      continue;
1592                  }
1593                  foreach ($availableupdates as $update) {
1594                      if (empty($updates[$plugin->component])) {
1595                          $updates[$plugin->component] = $update;
1596                          continue;
1597                      }
1598                      $maturitycurrent = $updates[$plugin->component]->maturity;
1599                      if (empty($maturitycurrent)) {
1600                          $maturitycurrent = MATURITY_STABLE - 25;
1601                      }
1602                      $maturityremote = $update->maturity;
1603                      if (empty($maturityremote)) {
1604                          $maturityremote = MATURITY_STABLE - 25;
1605                      }
1606                      if ($maturityremote < $maturitycurrent) {
1607                          continue;
1608                      }
1609                      if ($maturityremote > $maturitycurrent) {
1610                          $updates[$plugin->component] = $update;
1611                          continue;
1612                      }
1613                      if ($update->version > $updates[$plugin->component]->version) {
1614                          $updates[$plugin->component] = $update;
1615                          continue;
1616                      }
1617                  }
1618              }
1619          }
1620  
1621          foreach ($updates as $component => $update) {
1622              $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
1623              if (empty($remoteinfo) or empty($remoteinfo->version)) {
1624                  unset($updates[$component]);
1625              } else {
1626                  $updates[$component] = $remoteinfo;
1627              }
1628          }
1629  
1630          return $updates;
1631      }
1632  
1633      /**
1634       * Check to see if the given plugin folder can be removed by the web server process.
1635       *
1636       * @param string $component full frankenstyle component
1637       * @return bool
1638       */
1639      public function is_plugin_folder_removable($component) {
1640  
1641          $pluginfo = $this->get_plugin_info($component);
1642  
1643          if (is_null($pluginfo)) {
1644              return false;
1645          }
1646  
1647          // To be able to remove the plugin folder, its parent must be writable, too.
1648          if (!isset($pluginfo->rootdir) || !is_writable(dirname($pluginfo->rootdir))) {
1649              return false;
1650          }
1651  
1652          // Check that the folder and all its content is writable (thence removable).
1653          return $this->is_directory_removable($pluginfo->rootdir);
1654      }
1655  
1656      /**
1657       * Is it possible to create a new plugin directory for the given plugin type?
1658       *
1659       * @throws coding_exception for invalid plugin types or non-existing plugin type locations
1660       * @param string $plugintype
1661       * @return boolean
1662       */
1663      public function is_plugintype_writable($plugintype) {
1664  
1665          $plugintypepath = $this->get_plugintype_root($plugintype);
1666  
1667          if (is_null($plugintypepath)) {
1668              throw new coding_exception('Unknown plugin type: '.$plugintype);
1669          }
1670  
1671          if ($plugintypepath === false) {
1672              throw new coding_exception('Plugin type location does not exist: '.$plugintype);
1673          }
1674  
1675          return is_writable($plugintypepath);
1676      }
1677  
1678      /**
1679       * Returns the full path of the root of the given plugin type
1680       *
1681       * Null is returned if the plugin type is not known. False is returned if
1682       * the plugin type root is expected but not found. Otherwise, string is
1683       * returned.
1684       *
1685       * @param string $plugintype
1686       * @return string|bool|null
1687       */
1688      public function get_plugintype_root($plugintype) {
1689  
1690          $plugintypepath = null;
1691          foreach (core_component::get_plugin_types() as $type => $fullpath) {
1692              if ($type === $plugintype) {
1693                  $plugintypepath = $fullpath;
1694                  break;
1695              }
1696          }
1697          if (is_null($plugintypepath)) {
1698              return null;
1699          }
1700          if (!is_dir($plugintypepath)) {
1701              return false;
1702          }
1703  
1704          return $plugintypepath;
1705      }
1706  
1707      /**
1708       * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
1709       * but are not anymore and are deleted during upgrades.
1710       *
1711       * The main purpose of this list is to hide missing plugins during upgrade.
1712       *
1713       * @param string $type plugin type
1714       * @param string $name plugin name
1715       * @return bool
1716       */
1717      public static function is_deleted_standard_plugin($type, $name) {
1718          // Do not include plugins that were removed during upgrades to versions that are
1719          // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
1720          // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
1721          // Moodle 2.3 supports upgrades from 2.2.x only.
1722          $plugins = array(
1723              'qformat' => array('blackboard', 'learnwise', 'examview'),
1724              'assignment' => array('offline', 'online', 'upload', 'uploadsingle'),
1725              'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
1726              'block' => array('course_overview', 'messages', 'community', 'participants', 'quiz_results'),
1727              'cachestore' => array('memcache', 'memcached', 'mongodb'),
1728              'editor' => array('tinymce'),
1729              'enrol' => array('authorize'),
1730              'filter' => array('censor'),
1731              'media' => array('swf'),
1732              'portfolio' => array('picasa', 'boxnet'),
1733              'qformat' => array('webct'),
1734              'message' => array('jabber'),
1735              'mod' => array('assignment'),
1736              'quizaccess' => array('safebrowser'),
1737              'report' => array('search'),
1738              'repository' => array('alfresco', 'picasa', 'skydrive', 'boxnet'),
1739              'tinymce' => array('dragmath', 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
1740                  'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1741              ),
1742  
1743              'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport', 'assignmentupgrade', 'health'),
1744              'theme' => array('bootstrapbase', 'clean', 'more', 'afterburner', 'anomaly', 'arialist', 'base',
1745                  'binarius', 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor', 'fusion', 'leatherbound',
1746                  'magazine', 'mymobile', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
1747                  'standard', 'standardold'),
1748              'logstore' => ['legacy'],
1749              'webservice' => array('amf', 'xmlrpc'),
1750          );
1751  
1752          if (!isset($plugins[$type])) {
1753              return false;
1754          }
1755          return in_array($name, $plugins[$type]);
1756      }
1757  
1758      /**
1759       * Defines a white list of all plugins shipped in the standard Moodle distribution
1760       *
1761       * @param string $type
1762       * @return false|array array of standard plugins or false if the type is unknown
1763       */
1764      public static function standard_plugins_list($type) {
1765  
1766          $standard_plugins = array(
1767  
1768              'antivirus' => array(
1769                  'clamav'
1770              ),
1771  
1772              'atto' => array(
1773                  'accessibilitychecker', 'accessibilityhelper', 'align',
1774                  'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1775                  'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1776                  'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
1777                  'recordrtc', 'rtl', 'strike', 'subscript', 'superscript', 'table',
1778                  'title', 'underline', 'undo', 'unorderedlist', 'h5p', 'emojipicker',
1779              ),
1780  
1781              'assignsubmission' => array(
1782                  'comments', 'file', 'onlinetext'
1783              ),
1784  
1785              'assignfeedback' => array(
1786                  'comments', 'file', 'offline', 'editpdf'
1787              ),
1788  
1789              'auth' => array(
1790                  'cas', 'db', 'email', 'ldap', 'lti', 'manual', 'mnet',
1791                  'nologin', 'none', 'oauth2', 'shibboleth', 'webservice'
1792              ),
1793  
1794              'availability' => array(
1795                  'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1796              ),
1797  
1798              'block' => array(
1799                  'accessreview', 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1800                  'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
1801                  'calendar_upcoming', 'comments',
1802                  'completionstatus', 'course_list', 'course_summary',
1803                  'feedback', 'globalsearch', 'glossary_random', 'html',
1804                  'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile',
1805                  'navigation', 'news_items', 'online_users',
1806                  'private_files', 'recent_activity', 'recentlyaccesseditems',
1807                  'recentlyaccessedcourses', 'rss_client', 'search_forums', 'section_links',
1808                  'selfcompletion', 'settings', 'site_main_menu',
1809                  'social_activities', 'starredcourses', 'tag_flickr', 'tag_youtube', 'tags', 'timeline'
1810              ),
1811  
1812              'booktool' => array(
1813                  'exportimscp', 'importhtml', 'print'
1814              ),
1815  
1816              'cachelock' => array(
1817                  'file'
1818              ),
1819  
1820              'cachestore' => array(
1821                  'file', 'session', 'static', 'apcu', 'redis'
1822              ),
1823  
1824              'calendartype' => array(
1825                  'gregorian'
1826              ),
1827  
1828              'communication' => [
1829                  'customlink',
1830                  'matrix',
1831              ],
1832  
1833              'contenttype' => array(
1834                  'h5p'
1835              ),
1836  
1837              'customfield' => array(
1838                  'checkbox', 'date', 'select', 'text', 'textarea'
1839              ),
1840  
1841              'coursereport' => array(
1842                  // Deprecated!
1843              ),
1844  
1845              'datafield' => array(
1846                  'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1847                  'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1848              ),
1849  
1850              'dataformat' => array(
1851                  'html', 'csv', 'json', 'excel', 'ods', 'pdf',
1852              ),
1853  
1854              'datapreset' => array(
1855                  'imagegallery',
1856                  'journal',
1857                  'proposals',
1858                  'resources',
1859              ),
1860  
1861              'fileconverter' => array(
1862                  'unoconv', 'googledrive'
1863              ),
1864  
1865              'editor' => array(
1866                  'atto', 'textarea', 'tiny',
1867              ),
1868  
1869              'enrol' => array(
1870                  'category', 'cohort', 'database', 'flatfile',
1871                  'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet',
1872                  'paypal', 'self', 'fee',
1873              ),
1874  
1875              'factor' => [
1876                  'admin', 'auth', 'capability', 'cohort',  'email', 'grace', 'iprange', 'nosetup', 'role',
1877                  'token', 'totp', 'webauthn',
1878              ],
1879  
1880              'filter' => array(
1881                  'activitynames', 'algebra', 'emailprotect',
1882                  'emoticon', 'displayh5p', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1883                  'urltolink', 'data', 'glossary', 'codehighlighter'
1884              ),
1885  
1886              'format' => array(
1887                  'singleactivity', 'social', 'topics', 'weeks'
1888              ),
1889  
1890              'forumreport' => array(
1891                  'summary',
1892              ),
1893  
1894              'gradeexport' => array(
1895                  'ods', 'txt', 'xls', 'xml'
1896              ),
1897  
1898              'gradeimport' => array(
1899                  'csv', 'direct', 'xml'
1900              ),
1901  
1902              'gradereport' => array(
1903                  'grader', 'history', 'outcomes', 'overview', 'user', 'singleview', 'summary'
1904              ),
1905  
1906              'gradingform' => array(
1907                  'rubric', 'guide'
1908              ),
1909  
1910              'h5plib' => array(
1911                  'v124'
1912              ),
1913  
1914              'local' => array(
1915              ),
1916  
1917              'logstore' => array(
1918                  'database', 'standard',
1919              ),
1920  
1921              'ltiservice' => array(
1922                  'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings', 'basicoutcomes'
1923              ),
1924  
1925              'mlbackend' => array(
1926                  'php', 'python'
1927              ),
1928  
1929              'media' => array(
1930                  'html5audio', 'html5video', 'videojs', 'vimeo', 'youtube'
1931              ),
1932  
1933              'message' => array(
1934                  'airnotifier', 'email', 'popup'
1935              ),
1936  
1937              'mnetservice' => array(
1938                  'enrol'
1939              ),
1940  
1941              'mod' => array(
1942                  'assign', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1943                  'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page',
1944                  'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1945              ),
1946  
1947              'paygw' => [
1948                  'paypal',
1949              ],
1950  
1951              'plagiarism' => array(
1952              ),
1953  
1954              'portfolio' => array(
1955                  'download', 'flickr', 'googledocs', 'mahara'
1956              ),
1957  
1958              'profilefield' => array(
1959                  'checkbox', 'datetime', 'menu', 'social', 'text', 'textarea'
1960              ),
1961  
1962              'qbank' => [
1963                  'bulkmove',
1964                  'columnsortorder',
1965                  'comment',
1966                  'customfields',
1967                  'deletequestion',
1968                  'editquestion',
1969                  'exporttoxml',
1970                  'exportquestions',
1971                  'history',
1972                  'importquestions',
1973                  'managecategories',
1974                  'previewquestion',
1975                  'statistics',
1976                  'tagquestion',
1977                  'usage',
1978                  'viewcreator',
1979                  'viewquestionname',
1980                  'viewquestiontext',
1981                  'viewquestiontype',
1982              ],
1983  
1984              'qbehaviour' => array(
1985                  'adaptive', 'adaptivenopenalty', 'deferredcbm',
1986                  'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1987                  'informationitem', 'interactive', 'interactivecountback',
1988                  'manualgraded', 'missing'
1989              ),
1990  
1991              'qformat' => array(
1992                  'aiken', 'blackboard_six', 'gift',
1993                  'missingword', 'multianswer',
1994                  'xhtml', 'xml'
1995              ),
1996  
1997              'qtype' => array(
1998                  'calculated', 'calculatedmulti', 'calculatedsimple',
1999                  'ddimageortext', 'ddmarker', 'ddwtos', 'description',
2000                  'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
2001                  'multichoice', 'numerical', 'random', 'randomsamatch',
2002                  'shortanswer', 'truefalse'
2003              ),
2004  
2005              'quiz' => array(
2006                  'grading', 'overview', 'responses', 'statistics'
2007              ),
2008  
2009              'quizaccess' => array(
2010                  'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
2011                  'password', 'seb', 'securewindow', 'timelimit'
2012              ),
2013  
2014              'report' => array(
2015                  'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
2016                  'infectedfiles', 'insights', 'log', 'loglive', 'outline', 'participation', 'progress',
2017                  'questioninstances', 'security', 'stats', 'status', 'performance', 'usersessions'
2018              ),
2019  
2020              'repository' => array(
2021                  'areafiles', 'contentbank', 'coursefiles', 'dropbox', 'equella', 'filesystem',
2022                  'flickr', 'flickr_public', 'googledocs', 'local', 'merlot', 'nextcloud',
2023                  'onedrive', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
2024                  'wikimedia', 'youtube'
2025              ),
2026  
2027              'search' => array(
2028                  'simpledb', 'solr'
2029              ),
2030  
2031              'scormreport' => array(
2032                  'basic',
2033                  'interactions',
2034                  'graphs',
2035                  'objectives'
2036              ),
2037  
2038              'tiny' => [
2039                  'accessibilitychecker',
2040                  'autosave',
2041                  'equation',
2042                  'h5p',
2043                  'media',
2044                  'recordrtc',
2045                  'link',
2046                  'html',
2047                  'noautolink',
2048                  'premium',
2049              ],
2050  
2051              'theme' => array(
2052                  'boost', 'classic'
2053              ),
2054  
2055              'tool' => array(
2056                  'admin_presets', 'analytics', 'availabilityconditions', 'behat', 'brickfield', 'capability', 'cohortroles',
2057                  'componentlibrary', 'customlang', 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'httpsreplace', 'innodb',
2058                  'installaddon', 'langimport', 'licensemanager', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound',
2059                  'mobile', 'moodlenet', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
2060                  'replace', 'spamcleaner', 'task', 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles',
2061                  'usertours', 'xmldb', 'mfa'
2062              ),
2063  
2064              'webservice' => array(
2065                  'rest', 'soap'
2066              ),
2067  
2068              'workshopallocation' => array(
2069                  'manual', 'random', 'scheduled'
2070              ),
2071  
2072              'workshopeval' => array(
2073                  'best'
2074              ),
2075  
2076              'workshopform' => array(
2077                  'accumulative', 'comments', 'numerrors', 'rubric'
2078              )
2079          );
2080  
2081          if (isset($standard_plugins[$type])) {
2082              return $standard_plugins[$type];
2083          } else {
2084              return false;
2085          }
2086      }
2087  
2088      /**
2089       * Remove the current plugin code from the dirroot.
2090       *
2091       * If removing the currently installed version (which happens during
2092       * updates), we archive the code so that the upgrade can be cancelled.
2093       *
2094       * To prevent accidental data-loss, we also archive the existing plugin
2095       * code if cancelling installation of it, so that the developer does not
2096       * loose the only version of their work-in-progress.
2097       *
2098       * @param \core\plugininfo\base $plugin
2099       */
2100      public function remove_plugin_folder(\core\plugininfo\base $plugin) {
2101  
2102          if (!$this->is_plugin_folder_removable($plugin->component)) {
2103              throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
2104                  array('plugin' => $plugin->component, 'rootdir' => $plugin->rootdir),
2105                  'plugin root folder is not removable as expected');
2106          }
2107  
2108          if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2109              $this->archive_plugin_version($plugin);
2110          }
2111  
2112          remove_dir($plugin->rootdir);
2113          clearstatcache();
2114          if (function_exists('opcache_reset')) {
2115              opcache_reset();
2116          }
2117      }
2118  
2119      /**
2120       * Can the installation of the new plugin be cancelled?
2121       *
2122       * Subplugins can be cancelled only via their parent plugin, not separately
2123       * (they are considered as implicit requirements if distributed together
2124       * with the main package).
2125       *
2126       * @param \core\plugininfo\base $plugin
2127       * @return bool
2128       */
2129      public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
2130          global $CFG;
2131  
2132          if (!empty($CFG->disableupdateautodeploy)) {
2133              return false;
2134          }
2135  
2136          if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2137                  or !$this->is_plugin_folder_removable($plugin->component)) {
2138              return false;
2139          }
2140  
2141          if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2142              return true;
2143          }
2144  
2145          return false;
2146      }
2147  
2148      /**
2149       * Can the upgrade of the existing plugin be cancelled?
2150       *
2151       * Subplugins can be cancelled only via their parent plugin, not separately
2152       * (they are considered as implicit requirements if distributed together
2153       * with the main package).
2154       *
2155       * @param \core\plugininfo\base $plugin
2156       * @return bool
2157       */
2158      public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
2159          global $CFG;
2160  
2161          if (!empty($CFG->disableupdateautodeploy)) {
2162              // Cancelling the plugin upgrade is actually installation of the
2163              // previously archived version.
2164              return false;
2165          }
2166  
2167          if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2168                  or !$this->is_plugin_folder_removable($plugin->component)) {
2169              return false;
2170          }
2171  
2172          if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
2173              if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
2174                  return true;
2175              }
2176          }
2177  
2178          return false;
2179      }
2180  
2181      /**
2182       * Removes the plugin code directory if it is not installed yet.
2183       *
2184       * This is intended for the plugins check screen to give the admin a chance
2185       * to cancel the installation of just unzipped plugin before the database
2186       * upgrade happens.
2187       *
2188       * @param string $component
2189       */
2190      public function cancel_plugin_installation($component) {
2191          global $CFG;
2192  
2193          if (!empty($CFG->disableupdateautodeploy)) {
2194              return false;
2195          }
2196  
2197          $plugin = $this->get_plugin_info($component);
2198  
2199          if ($this->can_cancel_plugin_installation($plugin)) {
2200              $this->remove_plugin_folder($plugin);
2201          }
2202  
2203          return false;
2204      }
2205  
2206      /**
2207       * Returns plugins, the installation of which can be cancelled.
2208       *
2209       * @return array [(string)component] => (\core\plugininfo\base)plugin
2210       */
2211      public function list_cancellable_installations() {
2212          global $CFG;
2213  
2214          if (!empty($CFG->disableupdateautodeploy)) {
2215              return array();
2216          }
2217  
2218          $cancellable = array();
2219          foreach ($this->get_plugins() as $type => $plugins) {
2220              foreach ($plugins as $plugin) {
2221                  if ($this->can_cancel_plugin_installation($plugin)) {
2222                      $cancellable[$plugin->component] = $plugin;
2223                  }
2224              }
2225          }
2226  
2227          return $cancellable;
2228      }
2229  
2230      /**
2231       * Archive the current on-disk plugin code.
2232       *
2233       * @param \core\plugiinfo\base $plugin
2234       * @return bool
2235       */
2236      public function archive_plugin_version(\core\plugininfo\base $plugin) {
2237          return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
2238      }
2239  
2240      /**
2241       * Returns list of all archives that can be installed to cancel the plugin upgrade.
2242       *
2243       * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
2244       */
2245      public function list_restorable_archives() {
2246          global $CFG;
2247  
2248          if (!empty($CFG->disableupdateautodeploy)) {
2249              return false;
2250          }
2251  
2252          $codeman = $this->get_code_manager();
2253          $restorable = array();
2254          foreach ($this->get_plugins() as $type => $plugins) {
2255              foreach ($plugins as $plugin) {
2256                  if ($this->can_cancel_plugin_upgrade($plugin)) {
2257                      $restorable[$plugin->component] = (object)array(
2258                          'component' => $plugin->component,
2259                          'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
2260                      );
2261                  }
2262              }
2263          }
2264  
2265          return $restorable;
2266      }
2267  
2268      /**
2269       * Reorders plugin types into a sequence to be displayed
2270       *
2271       * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2272       * in a certain order that does not need to fit the expected order for the display.
2273       * Particularly, activity modules should be displayed first as they represent the
2274       * real heart of Moodle. They should be followed by other plugin types that are
2275       * used to build the courses (as that is what one expects from LMS). After that,
2276       * other supportive plugin types follow.
2277       *
2278       * @param array $types associative array
2279       * @return array same array with altered order of items
2280       */
2281      protected function reorder_plugin_types(array $types) {
2282          $fix = array('mod' => $types['mod']);
2283          foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
2284              if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
2285                  continue;
2286              }
2287              foreach ($subtypes as $subtype => $ignored) {
2288                  $fix[$subtype] = $types[$subtype];
2289              }
2290          }
2291  
2292          $fix['mod']        = $types['mod'];
2293          $fix['block']      = $types['block'];
2294          $fix['qtype']      = $types['qtype'];
2295          $fix['qbank']      = $types['qbank'];
2296          $fix['qbehaviour'] = $types['qbehaviour'];
2297          $fix['qformat']    = $types['qformat'];
2298          $fix['filter']     = $types['filter'];
2299  
2300          $fix['editor']     = $types['editor'];
2301          foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2302              if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2303                  continue;
2304              }
2305              foreach ($subtypes as $subtype => $ignored) {
2306                  $fix[$subtype] = $types[$subtype];
2307              }
2308          }
2309  
2310          $fix['enrol'] = $types['enrol'];
2311          $fix['auth']  = $types['auth'];
2312          $fix['tool']  = $types['tool'];
2313          foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2314              if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2315                  continue;
2316              }
2317              foreach ($subtypes as $subtype => $ignored) {
2318                  $fix[$subtype] = $types[$subtype];
2319              }
2320          }
2321  
2322          foreach ($types as $type => $path) {
2323              if (!isset($fix[$type])) {
2324                  $fix[$type] = $path;
2325              }
2326          }
2327          return $fix;
2328      }
2329  
2330      /**
2331       * Check if the given directory can be removed by the web server process.
2332       *
2333       * This recursively checks that the given directory and all its contents
2334       * it writable.
2335       *
2336       * @param string $fullpath
2337       * @return boolean
2338       */
2339      public function is_directory_removable($fullpath) {
2340  
2341          if (!is_writable($fullpath)) {
2342              return false;
2343          }
2344  
2345          if (is_dir($fullpath)) {
2346              $handle = opendir($fullpath);
2347          } else {
2348              return false;
2349          }
2350  
2351          $result = true;
2352  
2353          while ($filename = readdir($handle)) {
2354  
2355              if ($filename === '.' or $filename === '..') {
2356                  continue;
2357              }
2358  
2359              $subfilepath = $fullpath.'/'.$filename;
2360  
2361              if (is_dir($subfilepath)) {
2362                  $result = $result && $this->is_directory_removable($subfilepath);
2363  
2364              } else {
2365                  $result = $result && is_writable($subfilepath);
2366              }
2367          }
2368  
2369          closedir($handle);
2370  
2371          return $result;
2372      }
2373  
2374      /**
2375       * Helper method that implements common uninstall prerequisites
2376       *
2377       * @param \core\plugininfo\base $pluginfo
2378       * @return bool
2379       */
2380      protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
2381          global $CFG;
2382          // Check if uninstall is allowed from the GUI.
2383          if (!empty($CFG->uninstallclionly) && (!CLI_SCRIPT)) {
2384              return false;
2385          }
2386  
2387          if (!$pluginfo->is_uninstall_allowed()) {
2388              // The plugin's plugininfo class declares it should not be uninstalled.
2389              return false;
2390          }
2391  
2392          if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
2393              // The plugin is not installed. It should be either installed or removed from the disk.
2394              // Relying on this temporary state may be tricky.
2395              return false;
2396          }
2397  
2398          if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2399              // Backwards compatibility.
2400              debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2401                  DEBUG_DEVELOPER);
2402              return false;
2403          }
2404  
2405          return true;
2406      }
2407  
2408      /**
2409       * Returns a code_manager instance to be used for the plugins code operations.
2410       *
2411       * @return \core\update\code_manager
2412       */
2413      protected function get_code_manager() {
2414  
2415          if ($this->codemanager === null) {
2416              $this->codemanager = new \core\update\code_manager();
2417          }
2418  
2419          return $this->codemanager;
2420      }
2421  
2422      /**
2423       * Returns a client for https://download.moodle.org/api/
2424       *
2425       * @return \core\update\api
2426       */
2427      protected function get_update_api_client() {
2428  
2429          if ($this->updateapiclient === null) {
2430              $this->updateapiclient = \core\update\api::client();
2431          }
2432  
2433          return $this->updateapiclient;
2434      }
2435  }