Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>.namespace core;use cache;use coding_exception;use core_component;use moodle_exception;use moodle_url;use progress_trace;use stdClass;/*** Defines classes used for plugins management** This library provides a unified interface to various plugin types in* Moodle. It is mainly used by the plugins management admin page and the* plugins check page during the upgrade.** @package core* @copyright 2011 David Mudrak <david@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class plugin_manager {/** the plugin is shipped with standard Moodle distribution */const PLUGIN_SOURCE_STANDARD = 'std';/** the plugin is added extension */const PLUGIN_SOURCE_EXTENSION = 'ext';/** the plugin uses neither database nor capabilities, no versions */const PLUGIN_STATUS_NODB = 'nodb';/** the plugin is up-to-date */const PLUGIN_STATUS_UPTODATE = 'uptodate';/** the plugin is about to be installed */const PLUGIN_STATUS_NEW = 'new';/** the plugin is about to be upgraded */const PLUGIN_STATUS_UPGRADE = 'upgrade';/** the standard plugin is about to be deleted */const PLUGIN_STATUS_DELETE = 'delete';/** the version at the disk is lower than the one already installed */const PLUGIN_STATUS_DOWNGRADE = 'downgrade';/** the plugin is installed but missing from disk */const PLUGIN_STATUS_MISSING = 'missing';/** the given requirement/dependency is fulfilled */const REQUIREMENT_STATUS_OK = 'ok';/** the plugin requires higher core/other plugin version than is currently installed */const REQUIREMENT_STATUS_OUTDATED = 'outdated';/** the required dependency is not installed */const REQUIREMENT_STATUS_MISSING = 'missing';/** the current Moodle version is too high for plugin. */const REQUIREMENT_STATUS_NEWER = 'newer';/** the required dependency is available in the plugins directory */const REQUIREMENT_AVAILABLE = 'available';/** the required dependency is available in the plugins directory */const REQUIREMENT_UNAVAILABLE = 'unavailable';/** the moodle version is explicitly supported */const VERSION_SUPPORTED = 'supported';/** the moodle version is not explicitly supported */const VERSION_NOT_SUPPORTED = 'notsupported';/** the plugin does not specify supports */const VERSION_NO_SUPPORTS = 'nosupports';/** @var plugin_manager holds the singleton instance */protected static $singletoninstance;/** @var stdClass cache of standard plugins */protected static ?stdClass $standardplugincache = null;/** @var array of raw plugins information */protected $pluginsinfo = null;/** @var array of raw subplugins information */protected $subpluginsinfo = null;/** @var array cache information about availability in the plugins directory if requesting "at least" version */protected $remotepluginsinfoatleast = null;/** @var array cache information about availability in the plugins directory if requesting exact version */protected $remotepluginsinfoexact = null;/** @var array list of installed plugins $name=>$version */protected $installedplugins = null;/** @var array list of all enabled plugins $name=>$name */protected $enabledplugins = null;/** @var array list of all enabled plugins $name=>$diskversion */protected $presentplugins = null;/** @var array reordered list of plugin types */protected $plugintypes = null;/** @var \core\update\code_manager code manager to use for plugins code operations */protected $codemanager = null;/** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */protected $updateapiclient = null;/*** Direct initiation not allowed, use the factory method {@link self::instance()}*/protected function __construct() {}/*** Sorry, this is singleton*/protected function __clone() {}/*** Factory method for this class** @return static the singleton instance*/public static function instance() {if (is_null(static::$singletoninstance)) {static::$singletoninstance = new static();}return static::$singletoninstance;}/*** Reset all caches.* @param bool $phpunitreset*/public static function reset_caches($phpunitreset = false) {static::$standardplugincache = null;if ($phpunitreset) {static::$singletoninstance = null;} else {if (static::$singletoninstance) {static::$singletoninstance->pluginsinfo = null;static::$singletoninstance->subpluginsinfo = null;static::$singletoninstance->remotepluginsinfoatleast = null;static::$singletoninstance->remotepluginsinfoexact = null;static::$singletoninstance->installedplugins = null;static::$singletoninstance->enabledplugins = null;static::$singletoninstance->presentplugins = null;static::$singletoninstance->plugintypes = null;static::$singletoninstance->codemanager = null;static::$singletoninstance->updateapiclient = null;}}$cache = cache::make('core', 'plugin_manager');$cache->purge();}/*** Returns the result of {@link core_component::get_plugin_types()} ordered for humans** @see self::reorder_plugin_types()* @return array (string)name => (string)location*/public function get_plugin_types() {if (func_num_args() > 0) {if (!func_get_arg(0)) {throw new coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');}}if ($this->plugintypes) {return $this->plugintypes;}$this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());return $this->plugintypes;}/*** Load list of installed plugins,* always call before using $this->installedplugins.** This method is caching results for all plugins.*/protected function load_installed_plugins() {global $DB, $CFG;if ($this->installedplugins) {return;}if (empty($CFG->version)) {// Nothing installed yet.$this->installedplugins = [];return;}$cache = cache::make('core', 'plugin_manager');$installed = $cache->get('installed');if (is_array($installed)) {$this->installedplugins = $installed;return;}$this->installedplugins = [];$versions = $DB->get_records('config_plugins', ['name' => 'version']);foreach ($versions as $version) {$parts = explode('_', $version->plugin, 2);if (!isset($parts[1])) {// Invalid component, there must be at least one "_".continue;}// Do not verify here if plugin type and name are valid.$this->installedplugins[$parts[0]][$parts[1]] = $version->value;}foreach ($this->installedplugins as $key => $value) {ksort($this->installedplugins[$key]);}$cache->set('installed', $this->installedplugins);}/*** Return list of installed plugins of given type.* @param string $type* @return array $name=>$version*/public function get_installed_plugins($type) {$this->load_installed_plugins();if (isset($this->installedplugins[$type])) {return $this->installedplugins[$type];}return [];}/*** Load list of all enabled plugins,* call before using $this->enabledplugins.** This method is caching results from individual plugin info classes.*/protected function load_enabled_plugins() {global $CFG;if ($this->enabledplugins) {return;}if (empty($CFG->version)) {$this->enabledplugins = [];return;}$cache = cache::make('core', 'plugin_manager');$enabled = $cache->get('enabled');if (is_array($enabled)) {$this->enabledplugins = $enabled;return;}$this->enabledplugins = [];require_once($CFG->libdir . '/adminlib.php');$plugintypes = core_component::get_plugin_types();foreach ($plugintypes as $plugintype => $fulldir) {$plugininfoclass = static::resolve_plugininfo_class($plugintype);if (class_exists($plugininfoclass)) {$enabled = $plugininfoclass::get_enabled_plugins();if (!is_array($enabled)) {continue;}$this->enabledplugins[$plugintype] = $enabled;}}$cache->set('enabled', $this->enabledplugins);}/*** Get list of enabled plugins of given type,* the result may contain missing plugins.** @param string $type* @return array|null list of enabled plugins of this type, null if unknown*/public function get_enabled_plugins($type) {$this->load_enabled_plugins();if (isset($this->enabledplugins[$type])) {return $this->enabledplugins[$type];}return null;}/*** Load list of all present plugins - call before using $this->presentplugins.*/protected function load_present_plugins() {if ($this->presentplugins) {return;}$cache = cache::make('core', 'plugin_manager');$present = $cache->get('present');if (is_array($present)) {$this->presentplugins = $present;return;}$this->presentplugins = [];$plugintypes = core_component::get_plugin_types();foreach ($plugintypes as $type => $typedir) {$plugs = core_component::get_plugin_list($type);foreach ($plugs as $plug => $fullplug) {$module = new stdClass();$plugin = new stdClass();$plugin->version = null;include($fullplug . '/version.php');// Check if the legacy $module syntax is still used.if (!is_object($module) || (count((array)$module) > 0)) {debugging('Unsupported $module syntax detected in version.php of the ' . $type . '_' . $plug . ' plugin.');$skipcache = true;}// Check if the component is properly declared.if (empty($plugin->component) || ($plugin->component !== $type . '_' . $plug)) {debugging('Plugin ' . $type . '_' . $plug . ' does not declare valid $plugin->component in its version.php.');$skipcache = true;}$this->presentplugins[$type][$plug] = $plugin;}}if (empty($skipcache)) {$cache->set('present', $this->presentplugins);}}/*** Load the standard plugin data from the plugins.json file.** @return stdClass*/protected static function load_standard_plugins(): stdClass {if (static::$standardplugincache === null) {$data = file_get_contents(dirname(__DIR__) . '/plugins.json');static::$standardplugincache = json_decode($data, false);}return static::$standardplugincache;}/*** Get list of present plugins of given type.** @param string $type* @return array|null list of presnet plugins $name=>$diskversion, null if unknown*/public function get_present_plugins($type) {$this->load_present_plugins();if (isset($this->presentplugins[$type])) {return $this->presentplugins[$type];}return null;}/*** Returns a tree of known plugins and information about them** @return array 2D array. The first keys are plugin type names (e.g. qtype);* the second keys are the plugin local name (e.g. multichoice); and* the values are the corresponding objects extending {@link \core\plugininfo\base}*/public function get_plugins() {$this->init_pluginsinfo_property();// Make sure all types are initialised.foreach ($this->pluginsinfo as $plugintype => $list) {if ($list === null) {$this->get_plugins_of_type($plugintype);}}return $this->pluginsinfo;}/*** Returns list of known plugins of the given type.** This method returns the subset of the tree returned by {@link self::get_plugins()}.* If the given type is not known, empty array is returned.** @param string $type plugin type, e.g. 'mod' or 'workshopallocation'* @return \core\plugininfo\base[] (string) plugin name => corresponding subclass of {@link \core\plugininfo\base}*/public function get_plugins_of_type($type) {global $CFG;$this->init_pluginsinfo_property();if (!array_key_exists($type, $this->pluginsinfo)) {return [];}if (is_array($this->pluginsinfo[$type])) {return $this->pluginsinfo[$type];}$types = core_component::get_plugin_types();if (!isset($types[$type])) {// Orphaned subplugins!$plugintypeclass = static::resolve_plugininfo_class($type);$this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);return $this->pluginsinfo[$type];}$plugintypeclass = static::resolve_plugininfo_class($type);$plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);$this->pluginsinfo[$type] = $plugins;return $this->pluginsinfo[$type];}/*** Init placeholder array for plugin infos.*/protected function init_pluginsinfo_property() {if (is_array($this->pluginsinfo)) {return;}$this->pluginsinfo = [];$plugintypes = $this->get_plugin_types();foreach ($plugintypes as $plugintype => $plugintyperootdir) {$this->pluginsinfo[$plugintype] = null;}// Add orphaned subplugin types.$this->load_installed_plugins();foreach ($this->installedplugins as $plugintype => $unused) {if (!isset($plugintypes[$plugintype])) {$this->pluginsinfo[$plugintype] = null;}}}/*** Find the plugin info class for given type.** @param string $type* @return string name of pluginfo class for give plugin type*/public static function resolve_plugininfo_class($type) {$plugintypes = core_component::get_plugin_types();if (!isset($plugintypes[$type])) {return '\core\plugininfo\orphaned';}$parent = core_component::get_subtype_parent($type);if ($parent) {$class = '\\' . $parent . '\plugininfo\\' . $type;if (class_exists($class)) {$plugintypeclass = $class;} else {if ($dir = core_component::get_component_directory($parent)) {// BC only - use namespace instead!if (file_exists("$dir/adminlib.php")) {global $CFG;include_once("$dir/adminlib.php");}if (class_exists('plugininfo_' . $type)) {$plugintypeclass = 'plugininfo_' . $type;debugging('Class "' . $plugintypeclass . '" is deprecated, migrate to "' . $class . '"', DEBUG_DEVELOPER);} else {debugging('Subplugin type "' . $type . '" should define class "' . $class . '"', DEBUG_DEVELOPER);$plugintypeclass = '\core\plugininfo\general';}} else {$plugintypeclass = '\core\plugininfo\general';}}} else {$class = '\core\plugininfo\\' . $type;if (class_exists($class)) {$plugintypeclass = $class;} else {debugging('All standard types including "' . $type . '" should have plugininfo class!', DEBUG_DEVELOPER);$plugintypeclass = '\core\plugininfo\general';}}if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');}return $plugintypeclass;}/*** Returns list of all known subplugins of the given plugin.** For plugins that do not provide subplugins (i.e. there is no support for it),* empty array is returned.** @param string $component full component name, e.g. 'mod_workshop'* @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}*/public function get_subplugins_of_plugin($component) {$pluginfo = $this->get_plugin_info($component);if (is_null($pluginfo)) {return [];}$subplugins = $this->get_subplugins();if (!isset($subplugins[$pluginfo->component])) {return [];}$list = [];foreach ($subplugins[$pluginfo->component] as $subdata) {foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {$list[$subpluginfo->component] = $subpluginfo;}}return $list;}/*** Returns list of plugins that define their subplugins and the information* about them from the db/subplugins.json file.** @return array with keys like 'mod_quiz', and values the data from the* corresponding db/subplugins.json file.*/public function get_subplugins() {if (is_array($this->subpluginsinfo)) {return $this->subpluginsinfo;}$plugintypes = core_component::get_plugin_types();$this->subpluginsinfo = [];foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {$component = $type . '_' . $plugin;$subplugins = core_component::get_subplugins($component);if (!$subplugins) {continue;}$this->subpluginsinfo[$component] = [];foreach ($subplugins as $subplugintype => $ignored) {$subplugin = new stdClass();$subplugin->type = $subplugintype;$subplugin->typerootdir = $plugintypes[$subplugintype];$this->subpluginsinfo[$component][$subplugintype] = $subplugin;}}}return $this->subpluginsinfo;}/*** Returns the name of the plugin that defines the given subplugin type** If the given subplugin type is not actually a subplugin, returns false.** @param string $subplugintype the name of subplugin type, eg. workshopform or quiz* @return false|string the name of the parent plugin, eg. mod_workshop*/public function get_parent_of_subplugin($subplugintype) {$parent = core_component::get_subtype_parent($subplugintype);if (!$parent) {return false;}return $parent;}/*** Returns a localized name of a given plugin** @param string $component name of the plugin, eg mod_workshop or auth_ldap* @return string*/public function plugin_name($component) {$pluginfo = $this->get_plugin_info($component);if (is_null($pluginfo)) {throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', ['plugin' => $component]);}return $pluginfo->displayname;}/*** Returns a localized name of a plugin typed in singular form** Most plugin types define their names in core_plugin lang file. In case of subplugins,* we try to ask the parent plugin for the name. In the worst case, we will return* the value of the passed $type parameter.** @param string $type the type of the plugin, e.g. mod or workshopform* @return string*/public function plugintype_name($type) {if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {// For most plugin types, their names are defined in core_plugin lang file.return get_string('type_' . $type, 'core_plugin');} else if ($parent = $this->get_parent_of_subplugin($type)) {// If this is a subplugin, try to ask the parent plugin for the name.return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);} else {return $type;}}/*** Returns a localized name of a plugin type in plural form** Most plugin types define their names in core_plugin lang file. In case of subplugins,* we try to ask the parent plugin for the name. In the worst case, we will return* the value of the passed $type parameter.** @param string $type the type of the plugin, e.g. mod or workshopform* @return string*/public function plugintype_name_plural($type) {if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {// For most plugin types, their names are defined in core_plugin lang file.return get_string('type_' . $type . '_plural', 'core_plugin');} else if ($parent = $this->get_parent_of_subplugin($type)) {// If this is a subplugin, try to ask the parent plugin for the name.return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);} else {return $type;}}/*** Returns information about the known plugin, or null** @param string $component frankenstyle component name.* @return \core\plugininfo\base|null the corresponding plugin information.*/public function get_plugin_info($component) {[$type, $name] = core_component::normalize_component($component);$plugins = $this->get_plugins_of_type($type);if (isset($plugins[$name])) {return $plugins[$name];} else {return null;}}/*** Check to see if the current version of the plugin seems to be a checkout of an external repository.** @param string $component frankenstyle component name* @return false|string*/public function plugin_external_source($component) {$plugininfo = $this->get_plugin_info($component);if (is_null($plugininfo)) {return false;}$pluginroot = $plugininfo->rootdir;if (is_dir($pluginroot . '/.git')) {return 'git';}if (is_file($pluginroot . '/.git')) {return 'git-submodule';}if (is_dir($pluginroot . '/CVS')) {return 'cvs';}if (is_dir($pluginroot . '/.svn')) {return 'svn';}if (is_dir($pluginroot . '/.hg')) {return 'mercurial';}return false;}/*** Get a list of any other plugins that require this one.* @param string $component frankenstyle component name.* @return array of frankensyle component names that require this one.*/public function other_plugins_that_require($component) {$others = [];foreach ($this->get_plugins() as $type => $plugins) {foreach ($plugins as $plugin) {$required = $plugin->get_other_required_plugins();if (isset($required[$component])) {$others[] = $plugin->component;}}}return $others;}/*** Check a dependencies list against the list of installed plugins.* @param array $dependencies compenent name to required version or ANY_VERSION.* @return bool true if all the dependencies are satisfied.*/public function are_dependencies_satisfied($dependencies) {foreach ($dependencies as $component => $requiredversion) {$otherplugin = $this->get_plugin_info($component);if (is_null($otherplugin)) {return false;}if ($requiredversion != ANY_VERSION && $otherplugin->versiondisk < $requiredversion) {return false;}}return true;}/*** Checks all dependencies for all installed plugins** This is used by install and upgrade. The array passed by reference as the second* argument is populated with the list of plugins that have failed dependencies (note that* a single plugin can appear multiple times in the $failedplugins).** @param int $moodleversion the version from version.php.* @param array $failedplugins to return the list of plugins with non-satisfied dependencies* @param int $branch the current moodle branch, null if not provided* @return bool true if all the dependencies are satisfied for all plugins.*/public function all_plugins_ok($moodleversion, &$failedplugins = [], $branch = null) {global $CFG;if (empty($branch)) {$branch = $CFG->branch ?? '';if (empty($branch)) {// During initial install there is no branch set.require($CFG->dirroot . '/version.php');$branch = (int)$branch;// Force CFG->branch to int value during install.$CFG->branch = $branch;}}$return = true;foreach ($this->get_plugins() as $type => $plugins) {foreach ($plugins as $plugin) {if (!$plugin->is_core_dependency_satisfied($moodleversion)) {$return = false;$failedplugins[] = $plugin->component;}if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {$return = false;$failedplugins[] = $plugin->component;}if (!$plugin->is_core_compatible_satisfied($branch)) {$return = false;$failedplugins[] = $plugin->component;}}}return $return;}/*** Resolve requirements and dependencies of a plugin.** Returns an array of objects describing the requirement/dependency,* indexed by the frankenstyle name of the component. The returned array* can be empty. The objects in the array have following properties:** ->(numeric)hasver* ->(numeric)reqver* ->(string)status* ->(string)availability** @param \core\plugininfo\base $plugin the plugin we are checking* @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version* @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch* @return array of objects*/public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion = null, $moodlebranch = null) {global $CFG;if ($plugin->versiondisk === null) {// Missing from disk, we have no version.php to read from.return [];}if ($moodleversion === null) {$moodleversion = $CFG->version;}if ($moodlebranch === null) {$moodlebranch = $CFG->branch;}$reqs = [];$reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch);if (!empty($reqcore)) {$reqs['core'] = $reqcore;}foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {$reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);}return $reqs;}/*** Helper method to resolve plugin's requirements on the moodle core.** @param \core\plugininfo\base $plugin the plugin we are checking* @param string|int|double $moodleversion moodle core branch to check against* @return stdClass*/protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion, $moodlebranch) {$reqs = (object)['hasver' => null,'reqver' => null,'status' => null,'availability' => null,];$reqs->hasver = $moodleversion;if (empty($plugin->versionrequires)) {$reqs->reqver = ANY_VERSION;} else {$reqs->reqver = $plugin->versionrequires;}if ($plugin->is_core_dependency_satisfied($moodleversion)) {$reqs->status = self::REQUIREMENT_STATUS_OK;} else {$reqs->status = self::REQUIREMENT_STATUS_OUTDATED;}// Now check if there is an explicit incompatible, supersedes requires.if (isset($plugin->pluginincompatible) && $plugin->pluginincompatible != null) {if (!$plugin->is_core_compatible_satisfied($moodlebranch)) {$reqs->status = self::REQUIREMENT_STATUS_NEWER;}}return $reqs;}/*** Helper method to resolve plugin's dependecies on other plugins.** @param \core\plugininfo\base $plugin the plugin we are checking* @param string $otherpluginname* @param string|int $requiredversion* @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch* @return stdClass*/protected function resolve_dependency_requirements(\core\plugininfo\base $plugin,$otherpluginname,$requiredversion,$moodlebranch) {$reqs = (object)['hasver' => null,'reqver' => null,'status' => null,'availability' => null,];$otherplugin = $this->get_plugin_info($otherpluginname);if ($otherplugin !== null) {// The required plugin is installed.$reqs->hasver = $otherplugin->versiondisk;$reqs->reqver = $requiredversion;// Check it has sufficient version.if ($requiredversion == ANY_VERSION || $otherplugin->versiondisk >= $requiredversion) {$reqs->status = self::REQUIREMENT_STATUS_OK;} else {$reqs->status = self::REQUIREMENT_STATUS_OUTDATED;}} else {// The required plugin is not installed.$reqs->hasver = null;$reqs->reqver = $requiredversion;$reqs->status = self::REQUIREMENT_STATUS_MISSING;}if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {$reqs->availability = self::REQUIREMENT_AVAILABLE;} else {$reqs->availability = self::REQUIREMENT_UNAVAILABLE;}}return $reqs;}/*** Helper method to determine whether a moodle version is explicitly supported.** @param \core\plugininfo\base $plugin the plugin we are checking* @param int $branch the moodle branch to check support for* @return string*/public function check_explicitly_supported($plugin, $branch): string {// Check for correctly formed supported.if (isset($plugin->pluginsupported)) {// Broken apart for readability.$error = false;if (!is_array($plugin->pluginsupported)) {$error = true;}if (!is_int($plugin->pluginsupported[0]) || !is_int($plugin->pluginsupported[1])) {$error = true;}if (count($plugin->pluginsupported) != 2) {$error = true;}if ($error) {throw new coding_exception(get_string('err_supported_syntax', 'core_plugin'));}}if (isset($plugin->pluginsupported) && $plugin->pluginsupported != null) {if ($plugin->pluginsupported[0] <= $branch && $branch <= $plugin->pluginsupported[1]) {return self::VERSION_SUPPORTED;} else {return self::VERSION_NOT_SUPPORTED;}} else {// If supports aren't specified, but incompatible is, return not supported if not incompatible.if (!isset($plugin->pluginsupported) && isset($plugin->pluginincompatible) && !empty($plugin->pluginincompatible)) {if (!$plugin->is_core_compatible_satisfied($branch)) {return self::VERSION_NOT_SUPPORTED;}}return self::VERSION_NO_SUPPORTS;}}/*** Is the given plugin version available in the plugins directory?** See {@link self::get_remote_plugin_info()} for the full explanation of how the $version* parameter is interpretted.** @param string $component plugin frankenstyle name* @param string|int $version ANY_VERSION or the version number* @param bool $exactmatch false if "given version or higher" is requested* @return boolean*/public function is_remote_plugin_available($component, $version, $exactmatch) {$info = $this->get_remote_plugin_info($component, $version, $exactmatch);if (empty($info)) {// There is no available plugin of that name.return false;}if (empty($info->version)) {// Plugin is known, but no suitable version was found.return false;}return true;}/*** Can the given plugin version be installed via the admin UI?** This check should be used whenever attempting to install a plugin from* the plugins directory (new install, available update, missing dependency).** @param string $component* @param int $version version number* @param string $reason returned code of the reason why it is not* @param bool $checkremote check this version availability on moodle server* @return boolean*/public function is_remote_plugin_installable($component, $version, &$reason = null, $checkremote = true) {global $CFG;// Make sure the feature is not disabled.if (!empty($CFG->disableupdateautodeploy)) {$reason = 'disabled';return false;}// Make sure the version is available.if ($checkremote && !$this->is_remote_plugin_available($component, $version, true)) {$reason = 'remoteunavailable';return false;}// Make sure the plugin type root directory is writable.[$plugintype, $pluginname] = core_component::normalize_component($component);if (!$this->is_plugintype_writable($plugintype)) {$reason = 'notwritableplugintype';return false;}if (!$checkremote) {$remoteversion = $version;} else {$remoteinfo = $this->get_remote_plugin_info($component, $version, true);$remoteversion = $remoteinfo->version->version;}$localinfo = $this->get_plugin_info($component);if ($localinfo) {// If the plugin is already present, prevent downgrade.if ($localinfo->versiondb > $remoteversion) {$reason = 'cannotdowngrade';return false;}// Make sure we have write access to all the existing code.if (is_dir($localinfo->rootdir)) {if (!$this->is_plugin_folder_removable($component)) {$reason = 'notwritableplugin';return false;}}}// Looks like it could work.return true;}/*** Given the list of remote plugin infos, return just those installable.** This is typically used on lists returned by* {@link self::available_updates()} or {@link self::missing_dependencies()}* to perform bulk installation of remote plugins.** @param array $remoteinfos list of {@link \core\update\remote_info}* @return array*/public function filter_installable($remoteinfos) {global $CFG;if (!empty($CFG->disableupdateautodeploy)) {return [];}if (empty($remoteinfos)) {return [];}$installable = [];foreach ($remoteinfos as $index => $remoteinfo) {if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {$installable[$index] = $remoteinfo;}}return $installable;}/*** Returns information about a plugin in the plugins directory.** This is typically used when checking for available dependencies (in* which case the $version represents minimal version we need), or* when installing an available update or a new plugin from the plugins* directory (in which case the $version is exact version we are* interested in). The interpretation of the $version is controlled* by the $exactmatch argument.** If a plugin with the given component name is found, data about the* plugin are returned as an object. The ->version property of the object* contains the information about the particular plugin version that* matches best the given critera. The ->version property is false if no* suitable version of the plugin was found (yet the plugin itself is* known).** See {@link \core\update\api::validate_pluginfo_format()} for the* returned data structure.** @param string $component plugin frankenstyle name* @param string|int $version ANY_VERSION or the version number* @param bool $exactmatch false if "given version or higher" is requested* @return \core\update\remote_info|bool*/public function get_remote_plugin_info($component, $version, $exactmatch) {if ($exactmatch && $version == ANY_VERSION) {throw new coding_exception('Invalid request for exactly any version, it does not make sense.');}$client = $this->get_update_api_client();if ($exactmatch) {// Use client's get_plugin_info() method.if (!isset($this->remotepluginsinfoexact[$component][$version])) {$this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);}return $this->remotepluginsinfoexact[$component][$version];} else {// Use client's find_plugin() method.if (!isset($this->remotepluginsinfoatleast[$component][$version])) {$this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);}return $this->remotepluginsinfoatleast[$component][$version];}}/*** Obtain the plugin ZIP file from the given URL** The caller is supposed to know both downloads URL and the MD5 hash of* the ZIP contents in advance, typically by using the API requests against* the plugins directory.** @param string $url* @param string $md5* @return string|bool full path to the file, false on error*/public function get_remote_plugin_zip($url, $md5) {global $CFG;if (!empty($CFG->disableupdateautodeploy)) {return false;}return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);}/*** Extracts the saved plugin ZIP file.** Returns the list of files found in the ZIP. The format of that list is* array of (string)filerelpath => (bool|string) where the array value is* either true or a string describing the problematic file.** @see zip_packer::extract_to_pathname()* @param string $zipfilepath full path to the saved ZIP file* @param string $targetdir full path to the directory to extract the ZIP file to* @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value* @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}*/public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);}/*** Detects the plugin's name from its ZIP file.** Plugin ZIP packages are expected to contain a single directory and the* directory name would become the plugin name once extracted to the Moodle* dirroot.** @param string $zipfilepath full path to the ZIP files* @return string|bool false on error*/public function get_plugin_zip_root_dir($zipfilepath) {return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);}/*** Return a list of missing dependencies.** This should provide the full list of plugins that should be installed to* fulfill the requirements of all plugins, if possible.** @param bool $availableonly return only available missing dependencies* @return array of \core\update\remote_info|bool indexed by the component name*/public function missing_dependencies($availableonly = false) {$dependencies = [];foreach ($this->get_plugins() as $plugintype => $pluginfos) {foreach ($pluginfos as $pluginname => $pluginfo) {foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {if ($reqname === 'core') {continue;}if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {$remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);if (empty($dependencies[$reqname])) {$dependencies[$reqname] = $remoteinfo;} else {// If resolving requirements has led to two different versions of the same// remote plugin, pick the higher version. This can happen in cases like one// plugin requiring ANY_VERSION and another plugin requiring specific higher// version with lower maturity of a remote plugin.if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {$dependencies[$reqname] = $remoteinfo;}}} else {if (!isset($dependencies[$reqname])) {// Unable to find a plugin fulfilling the requirements.$dependencies[$reqname] = false;}}}}}}if ($availableonly) {foreach ($dependencies as $component => $info) {if (empty($info) || empty($info->version)) {unset($dependencies[$component]);}}}return $dependencies;}/*** Is it possible to uninstall the given plugin?** False is returned if the plugininfo subclass declares the uninstall should* not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the* core vetoes it (e.g. becase the plugin or some of its subplugins is required* by some other installed plugin).** @param string $component full frankenstyle name, e.g. mod_foobar* @return bool*/public function can_uninstall_plugin($component) {$pluginfo = $this->get_plugin_info($component);if (is_null($pluginfo)) {return false;}if (!$this->common_uninstall_check($pluginfo)) {return false;}// Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!$subplugins = $this->get_subplugins_of_plugin($pluginfo->component);foreach ($subplugins as $subpluginfo) {// Check if there are some other plugins requiring this subplugin// (but the parent and siblings).foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {$ismyparent = ($pluginfo->component === $requiresme);$ismysibling = in_array($requiresme, array_keys($subplugins));if (!$ismyparent && !$ismysibling) {return false;}}}// Check if there are some other plugins requiring this plugin// (but its subplugins).foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {$ismysubplugin = in_array($requiresme, array_keys($subplugins));if (!$ismysubplugin) {return false;}}return true;}/*** Perform the installation of plugins.** If used for installation of remote plugins from the Moodle Plugins* directory, the $plugins must be list of {@link \core\update\remote_info}* object that represent installable remote plugins. The caller can use* {@link self::filter_installable()} to prepare the list.** If used for installation of plugins from locally available ZIP files,* the $plugins should be list of objects with properties ->component and* ->zipfilepath.** The method uses {@link mtrace()} to produce direct output and can be* used in both web and cli interfaces.** @param array $plugins list of plugins* @param bool $confirmed should the files be really deployed into the dirroot?* @param bool $silent perform without output* @return bool true on success*/public function install_plugins(array $plugins, $confirmed, $silent) {global $CFG, $OUTPUT;if (!empty($CFG->disableupdateautodeploy)) {return false;}if (empty($plugins)) {return false;}$ok = get_string('statusok', 'core');// Let admins know they can expect more verbose output.$silent || $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);// Download all ZIP packages if we do not have them yet.$zips = [];foreach ($plugins as $plugin) {if ($plugin instanceof \core\update\remote_info) {$zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,$plugin->version->downloadmd5);$silent || $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');$silent || $this->mtrace(PHP_EOL . ' <- ' . $plugin->version->downloadurl, '', DEBUG_DEVELOPER);$silent || $this->mtrace(PHP_EOL . ' -> ' . $zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);if (!$zips[$plugin->component]) {$silent || $this->mtrace(get_string('error'));return false;}$silent || $this->mtrace($ok);} else {if (empty($plugin->zipfilepath)) {throw new coding_exception('Unexpected data structure provided');}$zips[$plugin->component] = $plugin->zipfilepath;$silent || $this->mtrace('ZIP ' . $plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);}}// Validate all downloaded packages.foreach ($plugins as $plugin) {$zipfile = $zips[$plugin->component];$silent || $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');[$plugintype, $pluginname] = core_component::normalize_component($plugin->component);$tmp = make_request_directory();$zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);if (empty($zipcontents)) {$silent || $this->mtrace(get_string('error'));$silent || $this->mtrace('Unable to unzip ' . $zipfile, PHP_EOL, DEBUG_DEVELOPER);return false;}$validator = \core\update\validator::instance($tmp, $zipcontents);$validator->assert_plugin_type($plugintype);$validator->assert_moodle_version($CFG->version);// TODO Check for missing dependencies during validation.$result = $validator->execute();if (!$silent) {$result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));foreach ($validator->get_messages() as $message) {if ($message->level === $validator::INFO) {// Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.$level = DEBUG_NORMAL;} else if ($message->level === $validator::DEBUG) {// Display [Debug] validation messages only if debugging mode is DEBUG_ALL.$level = DEBUG_ALL;} else {// Display [Warning] and [Error] always.$level = null;}if ($message->level === $validator::WARNING && !CLI_SCRIPT) {$this->mtrace(' <strong>[' . $validator->message_level_name($message->level) . ']</strong>', ' ', $level);} else {$this->mtrace(' [' . $validator->message_level_name($message->level) . ']', ' ', $level);}$this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);$info = $validator->message_code_info($message->msgcode, $message->addinfo);if ($info) {$this->mtrace('[' . s($info) . ']', ' ', $level);} else if (is_string($message->addinfo)) {$this->mtrace('[' . s($message->addinfo, true) . ']', ' ', $level);} else {$this->mtrace('[' . s(json_encode($message->addinfo, true)) . ']', ' ', $level);}if ($icon = $validator->message_help_icon($message->msgcode)) {if (CLI_SCRIPT) {$this->mtrace(PHP_EOL . ' ^^^ ' . get_string('help') . ': ' .get_string($icon->identifier . '_help', $icon->component), '', $level);} else {$this->mtrace($OUTPUT->render($icon), ' ', $level);}}$this->mtrace(PHP_EOL, '', $level);}}if (!$result) {$silent || $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));return false;}}$silent || $this->mtrace(PHP_EOL . get_string('packagesvalidatingok', 'core_plugin'));if (!$confirmed) {return true;}// Extract all ZIP packs do the dirroot.foreach ($plugins as $plugin) {$silent || $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');$zipfile = $zips[$plugin->component];[$plugintype, $pluginname] = core_component::normalize_component($plugin->component);$target = $this->get_plugintype_root($plugintype);if (file_exists($target . '/' . $pluginname)) {$this->remove_plugin_folder($this->get_plugin_info($plugin->component));}if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {$silent || $this->mtrace(get_string('error'));$silent || $this->mtrace('Unable to unzip ' . $zipfile, PHP_EOL, DEBUG_DEVELOPER);if (function_exists('opcache_reset')) {opcache_reset();}return false;}$silent || $this->mtrace($ok);}if (function_exists('opcache_reset')) {opcache_reset();}return true;}/*** Outputs the given message via {@link mtrace()}.** If $debug is provided, then the message is displayed only at the given* debugging level (e.g. DEBUG_DEVELOPER to display the message only if the* site has developer debugging level selected).** @param string $msg message* @param string $eol end of line* @param null|int $debug null to display always, int only on given debug level*/protected function mtrace($msg, $eol = PHP_EOL, $debug = null) {global $CFG;if ($debug !== null && !debugging(null, $debug)) {return;}mtrace($msg, $eol);}/*** Returns uninstall URL if exists.** @param string $component* @param string $return either 'overview' or 'manage'* @return null|moodle_url uninstall URL, null if uninstall not supported*/public function get_uninstall_url($component, $return = 'overview') {if (!$this->can_uninstall_plugin($component)) {return null;}$pluginfo = $this->get_plugin_info($component);if (is_null($pluginfo)) {return null;}if (method_exists($pluginfo, 'get_uninstall_url')) {debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.',DEBUG_DEVELOPER);return $pluginfo->get_uninstall_url($return);}return $pluginfo->get_default_uninstall_url($return);}/*** Uninstall the given plugin.** Automatically cleans-up all remaining configuration data, log records, events,* files from the file pool etc.** In the future, the functionality of {@link uninstall_plugin()} function may be moved* into this method and all the code should be refactored to use it. At the moment, we* mimic this future behaviour by wrapping that function call.** @param string $component* @param progress_trace $progress traces the process* @return bool true on success, false on errors/problems*/public function uninstall_plugin($component, progress_trace $progress) {$pluginfo = $this->get_plugin_info($component);if (is_null($pluginfo)) {return false;}// Give the pluginfo class a chance to execute some steps.$result = $pluginfo->uninstall($progress);if (!$result) {return false;}// Call the legacy core function to uninstall the plugin.ob_start();uninstall_plugin($pluginfo->type, $pluginfo->name);$progress->output(ob_get_clean());return true;}/*** Checks if there are some plugins with a known available update** @return bool true if there is at least one available update*/public function some_plugins_updatable() {foreach ($this->get_plugins() as $type => $plugins) {foreach ($plugins as $plugin) {if ($plugin->available_updates()) {return true;}}}return false;}/*** Returns list of available updates for the given component.** This method should be considered as internal API and is supposed to be* called by {@link \core\plugininfo\base::available_updates()} only* to lazy load the data once they are first requested.** @param string $component frankenstyle name of the plugin* @return null|array array of \core\update\info objects or null*/public function load_available_updates_for_plugin($component) {global $CFG;$provider = \core\update\checker::instance();if (!$provider->enabled() || $component === '' || during_initial_install()) {return null;}if (isset($CFG->updateminmaturity)) {$minmaturity = $CFG->updateminmaturity;} else {// This can happen during the very first upgrade to 2.3.$minmaturity = MATURITY_STABLE;}return $provider->get_update_info($component, ['minmaturity' => $minmaturity]);}/*** Returns a list of all available updates to be installed.** This is used when "update all plugins" action is performed at the* administration UI screen.** Returns array of remote info objects indexed by the plugin* component. If there are multiple updates available (typically a mix of* stable and non-stable ones), we pick the most mature most recent one.** Plugins without explicit maturity are considered more mature than* release candidates but less mature than explicit stable (this should be* pretty rare case).** @return array (string)component => (\core\update\remote_info)remoteinfo*/public function available_updates() {$updates = [];foreach ($this->get_plugins() as $type => $plugins) {foreach ($plugins as $plugin) {$availableupdates = $plugin->available_updates();if (empty($availableupdates)) {continue;}foreach ($availableupdates as $update) {if (empty($updates[$plugin->component])) {$updates[$plugin->component] = $update;continue;}$maturitycurrent = $updates[$plugin->component]->maturity;if (empty($maturitycurrent)) {$maturitycurrent = MATURITY_STABLE - 25;}$maturityremote = $update->maturity;if (empty($maturityremote)) {$maturityremote = MATURITY_STABLE - 25;}if ($maturityremote < $maturitycurrent) {continue;}if ($maturityremote > $maturitycurrent) {$updates[$plugin->component] = $update;continue;}if ($update->version > $updates[$plugin->component]->version) {$updates[$plugin->component] = $update;continue;}}}}foreach ($updates as $component => $update) {$remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);if (empty($remoteinfo) || empty($remoteinfo->version)) {unset($updates[$component]);} else {$updates[$component] = $remoteinfo;}}return $updates;}/*** Check to see if the given plugin folder can be removed by the web server process.** @param string $component full frankenstyle component* @return bool*/public function is_plugin_folder_removable($component) {$pluginfo = $this->get_plugin_info($component);if (is_null($pluginfo)) {return false;}// To be able to remove the plugin folder, its parent must be writable, too.if (!isset($pluginfo->rootdir) || !is_writable(dirname($pluginfo->rootdir))) {return false;}// Check that the folder and all its content is writable (thence removable).return $this->is_directory_removable($pluginfo->rootdir);}/*** Is it possible to create a new plugin directory for the given plugin type?** @throws coding_exception for invalid plugin types or non-existing plugin type locations* @param string $plugintype* @return boolean*/public function is_plugintype_writable($plugintype) {$plugintypepath = $this->get_plugintype_root($plugintype);if (is_null($plugintypepath)) {throw new coding_exception('Unknown plugin type: ' . $plugintype);}if ($plugintypepath === false) {throw new coding_exception('Plugin type location does not exist: ' . $plugintype);}return is_writable($plugintypepath);}/*** Returns the full path of the root of the given plugin type** Null is returned if the plugin type is not known. False is returned if* the plugin type root is expected but not found. Otherwise, string is* returned.** @param string $plugintype* @return string|bool|null*/public function get_plugintype_root($plugintype) {$plugintypepath = null;foreach (core_component::get_plugin_types() as $type => $fullpath) {if ($type === $plugintype) {$plugintypepath = $fullpath;break;}}if (is_null($plugintypepath)) {return null;}if (!is_dir($plugintypepath)) {return false;}return $plugintypepath;}/*** Defines a list of all plugins that were originally shipped in the standard Moodle distribution,* but are not anymore and are deleted during upgrades.** The main purpose of this list is to hide missing plugins during upgrade.** @param string $type plugin type* @param string $name plugin name* @return bool*/public static function is_deleted_standard_plugin(string $type,string $name,): bool {// Do not include plugins that were removed during upgrades to versions that are// not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE// branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as// Moodle 2.3 supports upgrades from 2.2.x only.$plugins = static::load_standard_plugins()->deleted;if (property_exists($plugins, $type)) {return in_array($name, $plugins->$type);}return false;}/*** Fetches a list of all plugins shipped in the standard Moodle distribution.** If a type is specified but does not exist, a false value is returned.* Otherwise an array of the plugins of the specified type is returned.** @param null|string $type* @return false|array array of standard plugins or false if the type is unknown*/public static function standard_plugins_list(string $type): array|false {$plugins = static::load_standard_plugins()->standard;if (property_exists($plugins, $type)) {return (array) $plugins->$type;} else {return false;}}/*** Get all standard plugins by their component name.** @return array*/public static function get_standard_plugins(): array {$plugins = static::load_standard_plugins()->standard;$result = [];foreach ($plugins as $type => $list) {foreach ($list as $plugin) {$result[] = "{$type}_{$plugin}";}}return $result;}/*** Get all deleted standard plugins by their component name.** @return array*/public static function get_deleted_plugins(): array {$plugins = static::load_standard_plugins()->deleted;$result = [];foreach ($plugins as $type => $list) {foreach ($list as $plugin) {$result[] = "{$type}_{$plugin}";}}return $result;}/*** Remove the current plugin code from the dirroot.** If removing the currently installed version (which happens during* updates), we archive the code so that the upgrade can be cancelled.** To prevent accidental data-loss, we also archive the existing plugin* code if cancelling installation of it, so that the developer does not* loose the only version of their work-in-progress.** @param \core\plugininfo\base $plugin*/public function remove_plugin_folder(\core\plugininfo\base $plugin) {if (!$this->is_plugin_folder_removable($plugin->component)) {throw new moodle_exception('err_removing_unremovable_folder','core_plugin','',['plugin' => $plugin->component, 'rootdir' => $plugin->rootdir],'plugin root folder is not removable as expected');}if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE || $plugin->get_status() === self::PLUGIN_STATUS_NEW) {$this->archive_plugin_version($plugin);}remove_dir($plugin->rootdir);clearstatcache();if (function_exists('opcache_reset')) {opcache_reset();}}/*** Can the installation of the new plugin be cancelled?** Subplugins can be cancelled only via their parent plugin, not separately* (they are considered as implicit requirements if distributed together* with the main package).** @param \core\plugininfo\base $plugin* @return bool*/public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {global $CFG;if (!empty($CFG->disableupdateautodeploy)) {return false;}if (empty($plugin)|| $plugin->is_standard()|| $plugin->is_subplugin()|| !$this->is_plugin_folder_removable($plugin->component)) {return false;}if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {return true;}return false;}/*** Can the upgrade of the existing plugin be cancelled?** Subplugins can be cancelled only via their parent plugin, not separately* (they are considered as implicit requirements if distributed together* with the main package).** @param \core\plugininfo\base $plugin* @return bool*/public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {global $CFG;if (!empty($CFG->disableupdateautodeploy)) {// Cancelling the plugin upgrade is actually installation of the// previously archived version.return false;}if (empty($plugin)|| $plugin->is_standard()|| $plugin->is_subplugin()|| !$this->is_plugin_folder_removable($plugin->component)) {return false;}if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {return true;}}return false;}/*** Removes the plugin code directory if it is not installed yet.** This is intended for the plugins check screen to give the admin a chance* to cancel the installation of just unzipped plugin before the database* upgrade happens.** @param string $component*/public function cancel_plugin_installation($component) {global $CFG;if (!empty($CFG->disableupdateautodeploy)) {return false;}$plugin = $this->get_plugin_info($component);if ($this->can_cancel_plugin_installation($plugin)) {$this->remove_plugin_folder($plugin);}return false;}/*** Returns plugins, the installation of which can be cancelled.** @return array [(string)component] => (\core\plugininfo\base)plugin*/public function list_cancellable_installations() {global $CFG;if (!empty($CFG->disableupdateautodeploy)) {return [];}$cancellable = [];foreach ($this->get_plugins() as $type => $plugins) {foreach ($plugins as $plugin) {if ($this->can_cancel_plugin_installation($plugin)) {$cancellable[$plugin->component] = $plugin;}}}return $cancellable;}/*** Archive the current on-disk plugin code.** @param \core\plugininfo\base $plugin* @return bool*/public function archive_plugin_version(\core\plugininfo\base $plugin) {return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);}/*** Returns list of all archives that can be installed to cancel the plugin upgrade.** @return array [(string)component] => {(string)->component, (string)->zipfilepath}*/public function list_restorable_archives() {global $CFG;if (!empty($CFG->disableupdateautodeploy)) {return false;}$codeman = $this->get_code_manager();$restorable = [];foreach ($this->get_plugins() as $type => $plugins) {foreach ($plugins as $plugin) {if ($this->can_cancel_plugin_upgrade($plugin)) {$restorable[$plugin->component] = (object)['component' => $plugin->component,'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb),];}}}return $restorable;}/*** Reorders plugin types into a sequence to be displayed** For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are* in a certain order that does not need to fit the expected order for the display.* Particularly, activity modules should be displayed first as they represent the* real heart of Moodle. They should be followed by other plugin types that are* used to build the courses (as that is what one expects from LMS). After that,* other supportive plugin types follow.** @param array $types associative array* @return array same array with altered order of items*/protected function reorder_plugin_types(array $types) {$fix = ['mod' => $types['mod']];foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {if (!$subtypes = core_component::get_subplugins('mod_' . $plugin)) {continue;}foreach ($subtypes as $subtype => $ignored) {$fix[$subtype] = $types[$subtype];}}$fix['mod'] = $types['mod'];$fix['block'] = $types['block'];$fix['qtype'] = $types['qtype'];$fix['qbank'] = $types['qbank'];$fix['qbehaviour'] = $types['qbehaviour'];$fix['qformat'] = $types['qformat'];$fix['filter'] = $types['filter'];$fix['editor'] = $types['editor'];foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {if (!$subtypes = core_component::get_subplugins('editor_' . $plugin)) {continue;}foreach ($subtypes as $subtype => $ignored) {$fix[$subtype] = $types[$subtype];}}$fix['enrol'] = $types['enrol'];$fix['auth'] = $types['auth'];$fix['tool'] = $types['tool'];foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {if (!$subtypes = core_component::get_subplugins('tool_' . $plugin)) {continue;}foreach ($subtypes as $subtype => $ignored) {$fix[$subtype] = $types[$subtype];}}foreach ($types as $type => $path) {if (!isset($fix[$type])) {$fix[$type] = $path;}}return $fix;}/*** Check if the given directory can be removed by the web server process.** This recursively checks that the given directory and all its contents* it writable.** @param string $fullpath* @return boolean*/public function is_directory_removable($fullpath) {if (!is_writable($fullpath)) {return false;}if (is_dir($fullpath)) {$handle = opendir($fullpath);} else {return false;}$result = true;while ($filename = readdir($handle)) {if ($filename === '.' || $filename === '..') {continue;}$subfilepath = $fullpath . '/' . $filename;if (is_dir($subfilepath)) {$result = $result && $this->is_directory_removable($subfilepath);} else {$result = $result && is_writable($subfilepath);}}closedir($handle);return $result;}/*** Helper method that implements common uninstall prerequisites** @param \core\plugininfo\base $pluginfo* @return bool*/protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {global $CFG;// Check if uninstall is allowed from the GUI.if (!empty($CFG->uninstallclionly) && (!CLI_SCRIPT)) {return false;}if (!$pluginfo->is_uninstall_allowed()) {// The plugin's plugininfo class declares it should not be uninstalled.return false;}if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {// The plugin is not installed. It should be either installed or removed from the disk.// Relying on this temporary state may be tricky.return false;}if (method_exists($pluginfo, 'get_uninstall_url') && is_null($pluginfo->get_uninstall_url())) {// Backwards compatibility.debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() ' .'instead of returning null in get_uninstall_url()',DEBUG_DEVELOPER);return false;}return true;}/*** Returns a code_manager instance to be used for the plugins code operations.** @return \core\update\code_manager*/protected function get_code_manager() {if ($this->codemanager === null) {$this->codemanager = new \core\update\code_manager();}return $this->codemanager;}/*** Returns a client for https://download.moodle.org/api/** @return \core\update\api*/protected function get_update_api_client() {if ($this->updateapiclient === null) {$this->updateapiclient = \core\update\api::client();}return $this->updateapiclient;}}class_alias(plugin_manager::class, 'core_plugin_manager');