Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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