[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/classes/ -> plugin_manager.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1