<?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 = [];

        $allplugintypes = core_component::get_all_plugin_types();
        foreach ($allplugintypes as $type => $typedir) {
            $plugs = core_component::get_all_plugins_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
     *
     * @param bool $includeindeprecation whether to include plugins which are in deprecation (deprecated or deleted status).
     * @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(bool $includeindeprecation = false) {
        $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, $includeindeprecation);
            }
        }

        if ($includeindeprecation) {
            return $this->pluginsinfo;
        }
        return array_filter($this->pluginsinfo, function($key) {
            return !core_component::is_plugintype_in_deprecation($key);
        }, ARRAY_FILTER_USE_KEY);
    }

    /**
     * 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'
     * @param bool $includeindeprecation whether to include plugins which are in deprecation (deprecated or deleted status).
     * @return \core\plugininfo\base[] (string) plugin name => corresponding subclass of {@link \core\plugininfo\base}
     */
    public function get_plugins_of_type($type, bool $includeindeprecation = false) {
        global $CFG;

        $this->init_pluginsinfo_property();

        $exclude = !$includeindeprecation && core_component::is_plugintype_in_deprecation($type);
        if (!array_key_exists($type, $this->pluginsinfo) || $exclude) {
            return [];
        }

        if (is_array($this->pluginsinfo[$type])) {
            return $this->pluginsinfo[$type];
        }

        $allplugintypes = core_component::get_all_plugin_types();

        if (!isset($allplugintypes[$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);
        if (isset($allplugintypes[$type])) {
            $plugins = $plugintypeclass::get_plugins($type, $allplugintypes[$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 = [];

        // The pluginsinfo instance var contains keys for all plugin types, including those currently in deprecation.
        // Other methods should filter their returns as needed, based on key checks, or by checking either
        // $plugininfo->is_deprecated() or $plugininfo->is_deleted().
        $plugintypes = array_merge(
            $this->get_plugin_types(),
            \core_component::get_deprecated_plugin_types(),
            \core_component::get_deleted_plugin_types()
        );
        foreach ($plugintypes as $plugintype => $plugintyperootdir) {
            $this->pluginsinfo[$plugintype] = null;
        }

        // Add orphaned plugins.
        $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) {
        $allplugintypes = core_component::get_all_plugin_types();

        if (!isset($allplugintypes[$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
     * @param bool $includedeprecated whether to check deprecated subplugin types.
     * @return false|string the name of the parent plugin, eg. mod_workshop
     */
    public function get_parent_of_subplugin($subplugintype, bool $includedeprecated = false) {
        if (!$includedeprecated && core_component::is_plugintype_in_deprecation($subplugintype)) {
            return false;
        }
        $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, true);
        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;
        $allplugintypes = core_component::get_all_plugin_types();
        foreach ($allplugintypes 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');
