Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [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              'contenttype' => array(
1829                  'h5p'
1830              ),
1831  
1832              'customfield' => array(
1833                  'checkbox', 'date', 'select', 'text', 'textarea'
1834              ),
1835  
1836              'coursereport' => array(
1837                  // Deprecated!
1838              ),
1839  
1840              'datafield' => array(
1841                  'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1842                  'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1843              ),
1844  
1845              'dataformat' => array(
1846                  'html', 'csv', 'json', 'excel', 'ods', 'pdf',
1847              ),
1848  
1849              'datapreset' => array(
1850                  'imagegallery',
1851                  'journal',
1852                  'proposals',
1853                  'resources',
1854              ),
1855  
1856              'fileconverter' => array(
1857                  'unoconv', 'googledrive'
1858              ),
1859  
1860              'editor' => array(
1861                  'atto', 'textarea', 'tiny',
1862              ),
1863  
1864              'enrol' => array(
1865                  'category', 'cohort', 'database', 'flatfile',
1866                  'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet',
1867                  'paypal', 'self', 'fee',
1868              ),
1869  
1870              'filter' => array(
1871                  'activitynames', 'algebra', 'emailprotect',
1872                  'emoticon', 'displayh5p', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1873                  'urltolink', 'data', 'glossary'
1874              ),
1875  
1876              'format' => array(
1877                  'singleactivity', 'social', 'topics', 'weeks'
1878              ),
1879  
1880              'forumreport' => array(
1881                  'summary',
1882              ),
1883  
1884              'gradeexport' => array(
1885                  'ods', 'txt', 'xls', 'xml'
1886              ),
1887  
1888              'gradeimport' => array(
1889                  'csv', 'direct', 'xml'
1890              ),
1891  
1892              'gradereport' => array(
1893                  'grader', 'history', 'outcomes', 'overview', 'user', 'singleview', 'summary'
1894              ),
1895  
1896              'gradingform' => array(
1897                  'rubric', 'guide'
1898              ),
1899  
1900              'h5plib' => array(
1901                  'v124'
1902              ),
1903  
1904              'local' => array(
1905              ),
1906  
1907              'logstore' => array(
1908                  'database', 'standard',
1909              ),
1910  
1911              'ltiservice' => array(
1912                  'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings', 'basicoutcomes'
1913              ),
1914  
1915              'mlbackend' => array(
1916                  'php', 'python'
1917              ),
1918  
1919              'media' => array(
1920                  'html5audio', 'html5video', 'videojs', 'vimeo', 'youtube'
1921              ),
1922  
1923              'message' => array(
1924                  'airnotifier', 'email', 'popup'
1925              ),
1926  
1927              'mnetservice' => array(
1928                  'enrol'
1929              ),
1930  
1931              'mod' => array(
1932                  'assign', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1933                  'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page',
1934                  'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1935              ),
1936  
1937              'paygw' => [
1938                  'paypal',
1939              ],
1940  
1941              'plagiarism' => array(
1942              ),
1943  
1944              'portfolio' => array(
1945                  'download', 'flickr', 'googledocs', 'mahara'
1946              ),
1947  
1948              'profilefield' => array(
1949                  'checkbox', 'datetime', 'menu', 'social', 'text', 'textarea'
1950              ),
1951  
1952              'qbank' => [
1953                  'bulkmove',
1954                  'columnsortorder',
1955                  'comment',
1956                  'customfields',
1957                  'deletequestion',
1958                  'editquestion',
1959                  'exporttoxml',
1960                  'exportquestions',
1961                  'history',
1962                  'importquestions',
1963                  'managecategories',
1964                  'previewquestion',
1965                  'statistics',
1966                  'tagquestion',
1967                  'usage',
1968                  'viewcreator',
1969                  'viewquestionname',
1970                  'viewquestiontext',
1971                  'viewquestiontype',
1972              ],
1973  
1974              'qbehaviour' => array(
1975                  'adaptive', 'adaptivenopenalty', 'deferredcbm',
1976                  'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1977                  'informationitem', 'interactive', 'interactivecountback',
1978                  'manualgraded', 'missing'
1979              ),
1980  
1981              'qformat' => array(
1982                  'aiken', 'blackboard_six', 'gift',
1983                  'missingword', 'multianswer',
1984                  'xhtml', 'xml'
1985              ),
1986  
1987              'qtype' => array(
1988                  'calculated', 'calculatedmulti', 'calculatedsimple',
1989                  'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1990                  'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
1991                  'multichoice', 'numerical', 'random', 'randomsamatch',
1992                  'shortanswer', 'truefalse'
1993              ),
1994  
1995              'quiz' => array(
1996                  'grading', 'overview', 'responses', 'statistics'
1997              ),
1998  
1999              'quizaccess' => array(
2000                  'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
2001                  'password', 'seb', 'securewindow', 'timelimit'
2002              ),
2003  
2004              'report' => array(
2005                  'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
2006                  'infectedfiles', 'insights', 'log', 'loglive', 'outline', 'participation', 'progress',
2007                  'questioninstances', 'security', 'stats', 'status', 'performance', 'usersessions'
2008              ),
2009  
2010              'repository' => array(
2011                  'areafiles', 'contentbank', 'coursefiles', 'dropbox', 'equella', 'filesystem',
2012                  'flickr', 'flickr_public', 'googledocs', 'local', 'merlot', 'nextcloud',
2013                  'onedrive', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
2014                  'wikimedia', 'youtube'
2015              ),
2016  
2017              'search' => array(
2018                  'simpledb', 'solr'
2019              ),
2020  
2021              'scormreport' => array(
2022                  'basic',
2023                  'interactions',
2024                  'graphs',
2025                  'objectives'
2026              ),
2027  
2028              'tiny' => [
2029                  'accessibilitychecker',
2030                  'autosave',
2031                  'equation',
2032                  'h5p',
2033                  'media',
2034                  'recordrtc',
2035                  'link'
2036              ],
2037  
2038              'theme' => array(
2039                  'boost', 'classic'
2040              ),
2041  
2042              'tool' => array(
2043                  'admin_presets', 'analytics', 'availabilityconditions', 'behat', 'brickfield', 'capability', 'cohortroles',
2044                  'componentlibrary', 'customlang', 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'httpsreplace', 'innodb',
2045                  'installaddon', 'langimport', 'licensemanager', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound',
2046                  'mobile', 'moodlenet', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
2047                  'replace', 'spamcleaner', 'task', 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles',
2048                  'usertours', 'xmldb'
2049              ),
2050  
2051              'webservice' => array(
2052                  'rest', 'soap'
2053              ),
2054  
2055              'workshopallocation' => array(
2056                  'manual', 'random', 'scheduled'
2057              ),
2058  
2059              'workshopeval' => array(
2060                  'best'
2061              ),
2062  
2063              'workshopform' => array(
2064                  'accumulative', 'comments', 'numerrors', 'rubric'
2065              )
2066          );
2067  
2068          if (isset($standard_plugins[$type])) {
2069              return $standard_plugins[$type];
2070          } else {
2071              return false;
2072          }
2073      }
2074  
2075      /**
2076       * Remove the current plugin code from the dirroot.
2077       *
2078       * If removing the currently installed version (which happens during
2079       * updates), we archive the code so that the upgrade can be cancelled.
2080       *
2081       * To prevent accidental data-loss, we also archive the existing plugin
2082       * code if cancelling installation of it, so that the developer does not
2083       * loose the only version of their work-in-progress.
2084       *
2085       * @param \core\plugininfo\base $plugin
2086       */
2087      public function remove_plugin_folder(\core\plugininfo\base $plugin) {
2088  
2089          if (!$this->is_plugin_folder_removable($plugin->component)) {
2090              throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
2091                  array('plugin' => $plugin->component, 'rootdir' => $plugin->rootdir),
2092                  'plugin root folder is not removable as expected');
2093          }
2094  
2095          if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2096              $this->archive_plugin_version($plugin);
2097          }
2098  
2099          remove_dir($plugin->rootdir);
2100          clearstatcache();
2101          if (function_exists('opcache_reset')) {
2102              opcache_reset();
2103          }
2104      }
2105  
2106      /**
2107       * Can the installation of the new plugin be cancelled?
2108       *
2109       * Subplugins can be cancelled only via their parent plugin, not separately
2110       * (they are considered as implicit requirements if distributed together
2111       * with the main package).
2112       *
2113       * @param \core\plugininfo\base $plugin
2114       * @return bool
2115       */
2116      public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
2117          global $CFG;
2118  
2119          if (!empty($CFG->disableupdateautodeploy)) {
2120              return false;
2121          }
2122  
2123          if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2124                  or !$this->is_plugin_folder_removable($plugin->component)) {
2125              return false;
2126          }
2127  
2128          if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2129              return true;
2130          }
2131  
2132          return false;
2133      }
2134  
2135      /**
2136       * Can the upgrade of the existing plugin be cancelled?
2137       *
2138       * Subplugins can be cancelled only via their parent plugin, not separately
2139       * (they are considered as implicit requirements if distributed together
2140       * with the main package).
2141       *
2142       * @param \core\plugininfo\base $plugin
2143       * @return bool
2144       */
2145      public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
2146          global $CFG;
2147  
2148          if (!empty($CFG->disableupdateautodeploy)) {
2149              // Cancelling the plugin upgrade is actually installation of the
2150              // previously archived version.
2151              return false;
2152          }
2153  
2154          if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2155                  or !$this->is_plugin_folder_removable($plugin->component)) {
2156              return false;
2157          }
2158  
2159          if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
2160              if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
2161                  return true;
2162              }
2163          }
2164  
2165          return false;
2166      }
2167  
2168      /**
2169       * Removes the plugin code directory if it is not installed yet.
2170       *
2171       * This is intended for the plugins check screen to give the admin a chance
2172       * to cancel the installation of just unzipped plugin before the database
2173       * upgrade happens.
2174       *
2175       * @param string $component
2176       */
2177      public function cancel_plugin_installation($component) {
2178          global $CFG;
2179  
2180          if (!empty($CFG->disableupdateautodeploy)) {
2181              return false;
2182          }
2183  
2184          $plugin = $this->get_plugin_info($component);
2185  
2186          if ($this->can_cancel_plugin_installation($plugin)) {
2187              $this->remove_plugin_folder($plugin);
2188          }
2189  
2190          return false;
2191      }
2192  
2193      /**
2194       * Returns plugins, the installation of which can be cancelled.
2195       *
2196       * @return array [(string)component] => (\core\plugininfo\base)plugin
2197       */
2198      public function list_cancellable_installations() {
2199          global $CFG;
2200  
2201          if (!empty($CFG->disableupdateautodeploy)) {
2202              return array();
2203          }
2204  
2205          $cancellable = array();
2206          foreach ($this->get_plugins() as $type => $plugins) {
2207              foreach ($plugins as $plugin) {
2208                  if ($this->can_cancel_plugin_installation($plugin)) {
2209                      $cancellable[$plugin->component] = $plugin;
2210                  }
2211              }
2212          }
2213  
2214          return $cancellable;
2215      }
2216  
2217      /**
2218       * Archive the current on-disk plugin code.
2219       *
2220       * @param \core\plugiinfo\base $plugin
2221       * @return bool
2222       */
2223      public function archive_plugin_version(\core\plugininfo\base $plugin) {
2224          return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
2225      }
2226  
2227      /**
2228       * Returns list of all archives that can be installed to cancel the plugin upgrade.
2229       *
2230       * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
2231       */
2232      public function list_restorable_archives() {
2233          global $CFG;
2234  
2235          if (!empty($CFG->disableupdateautodeploy)) {
2236              return false;
2237          }
2238  
2239          $codeman = $this->get_code_manager();
2240          $restorable = array();
2241          foreach ($this->get_plugins() as $type => $plugins) {
2242              foreach ($plugins as $plugin) {
2243                  if ($this->can_cancel_plugin_upgrade($plugin)) {
2244                      $restorable[$plugin->component] = (object)array(
2245                          'component' => $plugin->component,
2246                          'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
2247                      );
2248                  }
2249              }
2250          }
2251  
2252          return $restorable;
2253      }
2254  
2255      /**
2256       * Reorders plugin types into a sequence to be displayed
2257       *
2258       * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2259       * in a certain order that does not need to fit the expected order for the display.
2260       * Particularly, activity modules should be displayed first as they represent the
2261       * real heart of Moodle. They should be followed by other plugin types that are
2262       * used to build the courses (as that is what one expects from LMS). After that,
2263       * other supportive plugin types follow.
2264       *
2265       * @param array $types associative array
2266       * @return array same array with altered order of items
2267       */
2268      protected function reorder_plugin_types(array $types) {
2269          $fix = array('mod' => $types['mod']);
2270          foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
2271              if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
2272                  continue;
2273              }
2274              foreach ($subtypes as $subtype => $ignored) {
2275                  $fix[$subtype] = $types[$subtype];
2276              }
2277          }
2278  
2279          $fix['mod']        = $types['mod'];
2280          $fix['block']      = $types['block'];
2281          $fix['qtype']      = $types['qtype'];
2282          $fix['qbank']      = $types['qbank'];
2283          $fix['qbehaviour'] = $types['qbehaviour'];
2284          $fix['qformat']    = $types['qformat'];
2285          $fix['filter']     = $types['filter'];
2286  
2287          $fix['editor']     = $types['editor'];
2288          foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2289              if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2290                  continue;
2291              }
2292              foreach ($subtypes as $subtype => $ignored) {
2293                  $fix[$subtype] = $types[$subtype];
2294              }
2295          }
2296  
2297          $fix['enrol'] = $types['enrol'];
2298          $fix['auth']  = $types['auth'];
2299          $fix['tool']  = $types['tool'];
2300          foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2301              if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2302                  continue;
2303              }
2304              foreach ($subtypes as $subtype => $ignored) {
2305                  $fix[$subtype] = $types[$subtype];
2306              }
2307          }
2308  
2309          foreach ($types as $type => $path) {
2310              if (!isset($fix[$type])) {
2311                  $fix[$type] = $path;
2312              }
2313          }
2314          return $fix;
2315      }
2316  
2317      /**
2318       * Check if the given directory can be removed by the web server process.
2319       *
2320       * This recursively checks that the given directory and all its contents
2321       * it writable.
2322       *
2323       * @param string $fullpath
2324       * @return boolean
2325       */
2326      public function is_directory_removable($fullpath) {
2327  
2328          if (!is_writable($fullpath)) {
2329              return false;
2330          }
2331  
2332          if (is_dir($fullpath)) {
2333              $handle = opendir($fullpath);
2334          } else {
2335              return false;
2336          }
2337  
2338          $result = true;
2339  
2340          while ($filename = readdir($handle)) {
2341  
2342              if ($filename === '.' or $filename === '..') {
2343                  continue;
2344              }
2345  
2346              $subfilepath = $fullpath.'/'.$filename;
2347  
2348              if (is_dir($subfilepath)) {
2349                  $result = $result && $this->is_directory_removable($subfilepath);
2350  
2351              } else {
2352                  $result = $result && is_writable($subfilepath);
2353              }
2354          }
2355  
2356          closedir($handle);
2357  
2358          return $result;
2359      }
2360  
2361      /**
2362       * Helper method that implements common uninstall prerequisites
2363       *
2364       * @param \core\plugininfo\base $pluginfo
2365       * @return bool
2366       */
2367      protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
2368          global $CFG;
2369          // Check if uninstall is allowed from the GUI.
2370          if (!empty($CFG->uninstallclionly) && (!CLI_SCRIPT)) {
2371              return false;
2372          }
2373  
2374          if (!$pluginfo->is_uninstall_allowed()) {
2375              // The plugin's plugininfo class declares it should not be uninstalled.
2376              return false;
2377          }
2378  
2379          if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
2380              // The plugin is not installed. It should be either installed or removed from the disk.
2381              // Relying on this temporary state may be tricky.
2382              return false;
2383          }
2384  
2385          if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2386              // Backwards compatibility.
2387              debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2388                  DEBUG_DEVELOPER);
2389              return false;
2390          }
2391  
2392          return true;
2393      }
2394  
2395      /**
2396       * Returns a code_manager instance to be used for the plugins code operations.
2397       *
2398       * @return \core\update\code_manager
2399       */
2400      protected function get_code_manager() {
2401  
2402          if ($this->codemanager === null) {
2403              $this->codemanager = new \core\update\code_manager();
2404          }
2405  
2406          return $this->codemanager;
2407      }
2408  
2409      /**
2410       * Returns a client for https://download.moodle.org/api/
2411       *
2412       * @return \core\update\api
2413       */
2414      protected function get_update_api_client() {
2415  
2416          if ($this->updateapiclient === null) {
2417              $this->updateapiclient = \core\update\api::client();
2418          }
2419  
2420          return $this->updateapiclient;
2421      }
2422  }