Ignore:
Timestamp:
May 2, 2016, 12:09:23 PM (3 years ago)
Author:
jrpelegrina
Message:

Updated to moodle 3.0.3

File:
1 edited

Legend:

Unmodified
Added
Removed
  • moodle/trunk/fuentes/lib/classes/plugin_manager.php

    r136 r1331  
    5454    const PLUGIN_STATUS_MISSING     = 'missing';
    5555
     56    /** the given requirement/dependency is fulfilled */
     57    const REQUIREMENT_STATUS_OK = 'ok';
     58    /** the plugin requires higher core/other plugin version than is currently installed */
     59    const REQUIREMENT_STATUS_OUTDATED = 'outdated';
     60    /** the required dependency is not installed */
     61    const REQUIREMENT_STATUS_MISSING = 'missing';
     62
     63    /** the required dependency is available in the plugins directory */
     64    const REQUIREMENT_AVAILABLE = 'available';
     65    /** the required dependency is available in the plugins directory */
     66    const REQUIREMENT_UNAVAILABLE = 'unavailable';
     67
    5668    /** @var core_plugin_manager holds the singleton instance */
    5769    protected static $singletoninstance;
     
    6072    /** @var array of raw subplugins information */
    6173    protected $subpluginsinfo = null;
     74    /** @var array cache information about availability in the plugins directory if requesting "at least" version */
     75    protected $remotepluginsinfoatleast = null;
     76    /** @var array cache information about availability in the plugins directory if requesting exact version */
     77    protected $remotepluginsinfoexact = null;
    6278    /** @var array list of installed plugins $name=>$version */
    6379    protected $installedplugins = null;
     
    6884    /** @var array reordered list of plugin types */
    6985    protected $plugintypes = null;
     86    /** @var \core\update\code_manager code manager to use for plugins code operations */
     87    protected $codemanager = null;
     88    /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */
     89    protected $updateapiclient = null;
    7090
    7191    /**
     
    87107     */
    88108    public static function instance() {
    89         if (is_null(self::$singletoninstance)) {
    90             self::$singletoninstance = new self();
    91         }
    92         return self::$singletoninstance;
     109        if (is_null(static::$singletoninstance)) {
     110            static::$singletoninstance = new static();
     111        }
     112        return static::$singletoninstance;
    93113    }
    94114
     
    99119    public static function reset_caches($phpunitreset = false) {
    100120        if ($phpunitreset) {
    101             self::$singletoninstance = null;
     121            static::$singletoninstance = null;
    102122        } else {
    103             if (self::$singletoninstance) {
    104                 self::$singletoninstance->pluginsinfo = null;
    105                 self::$singletoninstance->subpluginsinfo = null;
    106                 self::$singletoninstance->installedplugins = null;
    107                 self::$singletoninstance->enabledplugins = null;
    108                 self::$singletoninstance->presentplugins = null;
    109                 self::$singletoninstance->plugintypes = null;
     123            if (static::$singletoninstance) {
     124                static::$singletoninstance->pluginsinfo = null;
     125                static::$singletoninstance->subpluginsinfo = null;
     126                static::$singletoninstance->remotepluginsinfoatleast = null;
     127                static::$singletoninstance->remotepluginsinfoexact = null;
     128                static::$singletoninstance->installedplugins = null;
     129                static::$singletoninstance->enabledplugins = null;
     130                static::$singletoninstance->presentplugins = null;
     131                static::$singletoninstance->plugintypes = null;
     132                static::$singletoninstance->codemanager = null;
     133                static::$singletoninstance->updateapiclient = null;
    110134            }
    111135        }
     
    239263        $plugintypes = core_component::get_plugin_types();
    240264        foreach ($plugintypes as $plugintype => $fulldir) {
    241             $plugininfoclass = self::resolve_plugininfo_class($plugintype);
     265            $plugininfoclass = static::resolve_plugininfo_class($plugintype);
    242266            if (class_exists($plugininfoclass)) {
    243267                $enabled = $plugininfoclass::get_enabled_plugins();
     
    289313            $plugs = core_component::get_plugin_list($type);
    290314            foreach ($plugs as $plug => $fullplug) {
     315                $module = new stdClass();
    291316                $plugin = new stdClass();
    292317                $plugin->version = null;
    293                 $module = $plugin;
    294                 @include($fullplug.'/version.php');
     318                include($fullplug.'/version.php');
     319
     320                // Check if the legacy $module syntax is still used.
     321                if (!is_object($module) or (count((array)$module) > 0)) {
     322                    debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
     323                    $skipcache = true;
     324                }
     325
     326                // Check if the component is properly declared.
     327                if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
     328                    debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
     329                    $skipcache = true;
     330                }
     331
    295332                $this->presentplugins[$type][$plug] = $plugin;
    296333            }
    297334        }
    298335
    299         $cache->set('present', $this->presentplugins);
     336        if (empty($skipcache)) {
     337            $cache->set('present', $this->presentplugins);
     338        }
    300339    }
    301340
     
    360399        if (!isset($types[$type])) {
    361400            // Orphaned subplugins!
    362             $plugintypeclass = self::resolve_plugininfo_class($type);
    363             $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass);
     401            $plugintypeclass = static::resolve_plugininfo_class($type);
     402            $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
    364403            return $this->pluginsinfo[$type];
    365404        }
    366405
    367406        /** @var \core\plugininfo\base $plugintypeclass */
    368         $plugintypeclass = self::resolve_plugininfo_class($type);
    369         $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass);
     407        $plugintypeclass = static::resolve_plugininfo_class($type);
     408        $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
    370409        $this->pluginsinfo[$type] = $plugins;
    371 
    372         if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
    373             // Append the information about available updates provided by {@link \core\update\checker()}.
    374             $provider = \core\update\checker::instance();
    375             foreach ($plugins as $plugininfoholder) {
    376                 $plugininfoholder->check_available_updates($provider);
    377             }
    378         }
    379410
    380411        return $this->pluginsinfo[$type];
     
    637668     * Check to see if the current version of the plugin seems to be a checkout of an external repository.
    638669     *
    639      * @see \core\update\deployer::plugin_external_source()
    640670     * @param string $component frankenstyle component name
    641671     * @return false|string
     
    742772
    743773        return $return;
     774    }
     775
     776    /**
     777     * Resolve requirements and dependencies of a plugin.
     778     *
     779     * Returns an array of objects describing the requirement/dependency,
     780     * indexed by the frankenstyle name of the component. The returned array
     781     * can be empty. The objects in the array have following properties:
     782     *
     783     *  ->(numeric)hasver
     784     *  ->(numeric)reqver
     785     *  ->(string)status
     786     *  ->(string)availability
     787     *
     788     * @param \core\plugininfo\base $plugin the plugin we are checking
     789     * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
     790     * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
     791     * @return array of objects
     792     */
     793    public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
     794        global $CFG;
     795
     796        if ($plugin->versiondisk === null) {
     797            // Missing from disk, we have no version.php to read from.
     798            return array();
     799        }
     800
     801        if ($moodleversion === null) {
     802            $moodleversion = $CFG->version;
     803        }
     804
     805        if ($moodlebranch === null) {
     806            $moodlebranch = $CFG->branch;
     807        }
     808
     809        $reqs = array();
     810        $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
     811
     812        if (!empty($reqcore)) {
     813            $reqs['core'] = $reqcore;
     814        }
     815
     816        foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
     817            $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
     818        }
     819
     820        return $reqs;
     821    }
     822
     823    /**
     824     * Helper method to resolve plugin's requirements on the moodle core.
     825     *
     826     * @param \core\plugininfo\base $plugin the plugin we are checking
     827     * @param string|int|double $moodleversion moodle core branch to check against
     828     * @return stdObject
     829     */
     830    protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
     831
     832        $reqs = (object)array(
     833            'hasver' => null,
     834            'reqver' => null,
     835            'status' => null,
     836            'availability' => null,
     837        );
     838
     839        $reqs->hasver = $moodleversion;
     840
     841        if (empty($plugin->versionrequires)) {
     842            $reqs->reqver = ANY_VERSION;
     843        } else {
     844            $reqs->reqver = $plugin->versionrequires;
     845        }
     846
     847        if ($plugin->is_core_dependency_satisfied($moodleversion)) {
     848            $reqs->status = self::REQUIREMENT_STATUS_OK;
     849        } else {
     850            $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
     851        }
     852
     853        return $reqs;
     854    }
     855
     856    /**
     857     * Helper method to resolve plugin's dependecies on other plugins.
     858     *
     859     * @param \core\plugininfo\base $plugin the plugin we are checking
     860     * @param string $otherpluginname
     861     * @param string|int $requiredversion
     862     * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
     863     * @return stdClass
     864     */
     865    protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
     866            $requiredversion, $moodlebranch) {
     867
     868        $reqs = (object)array(
     869            'hasver' => null,
     870            'reqver' => null,
     871            'status' => null,
     872            'availability' => null,
     873        );
     874
     875        $otherplugin = $this->get_plugin_info($otherpluginname);
     876
     877        if ($otherplugin !== null) {
     878            // The required plugin is installed.
     879            $reqs->hasver = $otherplugin->versiondisk;
     880            $reqs->reqver = $requiredversion;
     881            // Check it has sufficient version.
     882            if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
     883                $reqs->status = self::REQUIREMENT_STATUS_OK;
     884            } else {
     885                $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
     886            }
     887
     888        } else {
     889            // The required plugin is not installed.
     890            $reqs->hasver = null;
     891            $reqs->reqver = $requiredversion;
     892            $reqs->status = self::REQUIREMENT_STATUS_MISSING;
     893        }
     894
     895        if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
     896            if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
     897                $reqs->availability = self::REQUIREMENT_AVAILABLE;
     898            } else {
     899                $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
     900            }
     901        }
     902
     903        return $reqs;
     904    }
     905
     906    /**
     907     * Is the given plugin version available in the plugins directory?
     908     *
     909     * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
     910     * parameter is interpretted.
     911     *
     912     * @param string $component plugin frankenstyle name
     913     * @param string|int $version ANY_VERSION or the version number
     914     * @param bool $exactmatch false if "given version or higher" is requested
     915     * @return boolean
     916     */
     917    public function is_remote_plugin_available($component, $version, $exactmatch) {
     918
     919        $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
     920
     921        if (empty($info)) {
     922            // There is no available plugin of that name.
     923            return false;
     924        }
     925
     926        if (empty($info->version)) {
     927            // Plugin is known, but no suitable version was found.
     928            return false;
     929        }
     930
     931        return true;
     932    }
     933
     934    /**
     935     * Can the given plugin version be installed via the admin UI?
     936     *
     937     * This check should be used whenever attempting to install a plugin from
     938     * the plugins directory (new install, available update, missing dependency).
     939     *
     940     * @param string $component
     941     * @param int $version version number
     942     * @param string $reason returned code of the reason why it is not
     943     * @return boolean
     944     */
     945    public function is_remote_plugin_installable($component, $version, &$reason=null) {
     946        global $CFG;
     947
     948        // Make sure the feature is not disabled.
     949        if (!empty($CFG->disableupdateautodeploy)) {
     950            $reason = 'disabled';
     951            return false;
     952        }
     953
     954        // Make sure the version is available.
     955        if (!$this->is_remote_plugin_available($component, $version, true)) {
     956            $reason = 'remoteunavailable';
     957            return false;
     958        }
     959
     960        // Make sure the plugin type root directory is writable.
     961        list($plugintype, $pluginname) = core_component::normalize_component($component);
     962        if (!$this->is_plugintype_writable($plugintype)) {
     963            $reason = 'notwritableplugintype';
     964            return false;
     965        }
     966
     967        $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
     968        $localinfo = $this->get_plugin_info($component);
     969
     970        if ($localinfo) {
     971            // If the plugin is already present, prevent downgrade.
     972            if ($localinfo->versiondb > $remoteinfo->version->version) {
     973                $reason = 'cannotdowngrade';
     974                return false;
     975            }
     976
     977            // Make sure we have write access to all the existing code.
     978            if (is_dir($localinfo->rootdir)) {
     979                if (!$this->is_plugin_folder_removable($component)) {
     980                    $reason = 'notwritableplugin';
     981                    return false;
     982                }
     983            }
     984        }
     985
     986        // Looks like it could work.
     987        return true;
     988    }
     989
     990    /**
     991     * Given the list of remote plugin infos, return just those installable.
     992     *
     993     * This is typically used on lists returned by
     994     * {@link self::available_updates()} or {@link self::missing_dependencies()}
     995     * to perform bulk installation of remote plugins.
     996     *
     997     * @param array $remoteinfos list of {@link \core\update\remote_info}
     998     * @return array
     999     */
     1000    public function filter_installable($remoteinfos) {
     1001        global $CFG;
     1002
     1003        if (!empty($CFG->disableupdateautodeploy)) {
     1004            return array();
     1005        }
     1006        if (empty($remoteinfos)) {
     1007            return array();
     1008        }
     1009        $installable = array();
     1010        foreach ($remoteinfos as $index => $remoteinfo) {
     1011            if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
     1012                $installable[$index] = $remoteinfo;
     1013            }
     1014        }
     1015        return $installable;
     1016    }
     1017
     1018    /**
     1019     * Returns information about a plugin in the plugins directory.
     1020     *
     1021     * This is typically used when checking for available dependencies (in
     1022     * which case the $version represents minimal version we need), or
     1023     * when installing an available update or a new plugin from the plugins
     1024     * directory (in which case the $version is exact version we are
     1025     * interested in). The interpretation of the $version is controlled
     1026     * by the $exactmatch argument.
     1027     *
     1028     * If a plugin with the given component name is found, data about the
     1029     * plugin are returned as an object. The ->version property of the object
     1030     * contains the information about the particular plugin version that
     1031     * matches best the given critera. The ->version property is false if no
     1032     * suitable version of the plugin was found (yet the plugin itself is
     1033     * known).
     1034     *
     1035     * See {@link \core\update\api::validate_pluginfo_format()} for the
     1036     * returned data structure.
     1037     *
     1038     * @param string $component plugin frankenstyle name
     1039     * @param string|int $version ANY_VERSION or the version number
     1040     * @param bool $exactmatch false if "given version or higher" is requested
     1041     * @return \core\update\remote_info|bool
     1042     */
     1043    public function get_remote_plugin_info($component, $version, $exactmatch) {
     1044
     1045        if ($exactmatch and $version == ANY_VERSION) {
     1046            throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
     1047        }
     1048
     1049        $client = $this->get_update_api_client();
     1050
     1051        if ($exactmatch) {
     1052            // Use client's get_plugin_info() method.
     1053            if (!isset($this->remotepluginsinfoexact[$component][$version])) {
     1054                $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
     1055            }
     1056            return $this->remotepluginsinfoexact[$component][$version];
     1057
     1058        } else {
     1059            // Use client's find_plugin() method.
     1060            if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
     1061                $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
     1062            }
     1063            return $this->remotepluginsinfoatleast[$component][$version];
     1064        }
     1065    }
     1066
     1067    /**
     1068     * Obtain the plugin ZIP file from the given URL
     1069     *
     1070     * The caller is supposed to know both downloads URL and the MD5 hash of
     1071     * the ZIP contents in advance, typically by using the API requests against
     1072     * the plugins directory.
     1073     *
     1074     * @param string $url
     1075     * @param string $md5
     1076     * @return string|bool full path to the file, false on error
     1077     */
     1078    public function get_remote_plugin_zip($url, $md5) {
     1079        global $CFG;
     1080
     1081        if (!empty($CFG->disableupdateautodeploy)) {
     1082            return false;
     1083        }
     1084        return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
     1085    }
     1086
     1087    /**
     1088     * Extracts the saved plugin ZIP file.
     1089     *
     1090     * Returns the list of files found in the ZIP. The format of that list is
     1091     * array of (string)filerelpath => (bool|string) where the array value is
     1092     * either true or a string describing the problematic file.
     1093     *
     1094     * @see zip_packer::extract_to_pathname()
     1095     * @param string $zipfilepath full path to the saved ZIP file
     1096     * @param string $targetdir full path to the directory to extract the ZIP file to
     1097     * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
     1098     * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
     1099     */
     1100    public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
     1101        return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
     1102    }
     1103
     1104    /**
     1105     * Detects the plugin's name from its ZIP file.
     1106     *
     1107     * Plugin ZIP packages are expected to contain a single directory and the
     1108     * directory name would become the plugin name once extracted to the Moodle
     1109     * dirroot.
     1110     *
     1111     * @param string $zipfilepath full path to the ZIP files
     1112     * @return string|bool false on error
     1113     */
     1114    public function get_plugin_zip_root_dir($zipfilepath) {
     1115        return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
     1116    }
     1117
     1118    /**
     1119     * Return a list of missing dependencies.
     1120     *
     1121     * This should provide the full list of plugins that should be installed to
     1122     * fulfill the requirements of all plugins, if possible.
     1123     *
     1124     * @param bool $availableonly return only available missing dependencies
     1125     * @return array of \core\update\remote_info|bool indexed by the component name
     1126     */
     1127    public function missing_dependencies($availableonly=false) {
     1128
     1129        $dependencies = array();
     1130
     1131        foreach ($this->get_plugins() as $plugintype => $pluginfos) {
     1132            foreach ($pluginfos as $pluginname => $pluginfo) {
     1133                foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
     1134                    if ($reqname === 'core') {
     1135                        continue;
     1136                    }
     1137                    if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
     1138                        if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
     1139                            $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
     1140
     1141                            if (empty($dependencies[$reqname])) {
     1142                                $dependencies[$reqname] = $remoteinfo;
     1143                            } else {
     1144                                // If resolving requirements has led to two different versions of the same
     1145                                // remote plugin, pick the higher version. This can happen in cases like one
     1146                                // plugin requiring ANY_VERSION and another plugin requiring specific higher
     1147                                // version with lower maturity of a remote plugin.
     1148                                if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
     1149                                    $dependencies[$reqname] = $remoteinfo;
     1150                                }
     1151                            }
     1152
     1153                        } else {
     1154                            if (!isset($dependencies[$reqname])) {
     1155                                // Unable to find a plugin fulfilling the requirements.
     1156                                $dependencies[$reqname] = false;
     1157                            }
     1158                        }
     1159                    }
     1160                }
     1161            }
     1162        }
     1163
     1164        if ($availableonly) {
     1165            foreach ($dependencies as $component => $info) {
     1166                if (empty($info) or empty($info->version)) {
     1167                    unset($dependencies[$component]);
     1168                }
     1169            }
     1170        }
     1171
     1172        return $dependencies;
    7441173    }
    7451174
     
    7941223
    7951224    /**
     1225     * Perform the installation of plugins.
     1226     *
     1227     * If used for installation of remote plugins from the Moodle Plugins
     1228     * directory, the $plugins must be list of {@link \core\update\remote_info}
     1229     * object that represent installable remote plugins. The caller can use
     1230     * {@link self::filter_installable()} to prepare the list.
     1231     *
     1232     * If used for installation of plugins from locally available ZIP files,
     1233     * the $plugins should be list of objects with properties ->component and
     1234     * ->zipfilepath.
     1235     *
     1236     * The method uses {@link mtrace()} to produce direct output and can be
     1237     * used in both web and cli interfaces.
     1238     *
     1239     * @param array $plugins list of plugins
     1240     * @param bool $confirmed should the files be really deployed into the dirroot?
     1241     * @param bool $silent perform without output
     1242     * @return bool true on success
     1243     */
     1244    public function install_plugins(array $plugins, $confirmed, $silent) {
     1245        global $CFG, $OUTPUT;
     1246
     1247        if (!empty($CFG->disableupdateautodeploy)) {
     1248            return false;
     1249        }
     1250
     1251        if (empty($plugins)) {
     1252            return false;
     1253        }
     1254
     1255        $ok = get_string('ok', 'core');
     1256
     1257        // Let admins know they can expect more verbose output.
     1258        $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);
     1259
     1260        // Download all ZIP packages if we do not have them yet.
     1261        $zips = array();
     1262        foreach ($plugins as $plugin) {
     1263            if ($plugin instanceof \core\update\remote_info) {
     1264                $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
     1265                    $plugin->version->downloadmd5);
     1266                $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
     1267                $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
     1268                $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
     1269                if (!$zips[$plugin->component]) {
     1270                    $silent or $this->mtrace(get_string('error'));
     1271                    return false;
     1272                }
     1273                $silent or $this->mtrace($ok);
     1274            } else {
     1275                if (empty($plugin->zipfilepath)) {
     1276                    throw new coding_exception('Unexpected data structure provided');
     1277                }
     1278                $zips[$plugin->component] = $plugin->zipfilepath;
     1279                $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
     1280            }
     1281        }
     1282
     1283        // Validate all downloaded packages.
     1284        foreach ($plugins as $plugin) {
     1285            $zipfile = $zips[$plugin->component];
     1286            $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');
     1287            list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
     1288            $tmp = make_request_directory();
     1289            $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
     1290            if (empty($zipcontents)) {
     1291                $silent or $this->mtrace(get_string('error'));
     1292                $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
     1293                return false;
     1294            }
     1295
     1296            $validator = \core\update\validator::instance($tmp, $zipcontents);
     1297            $validator->assert_plugin_type($plugintype);
     1298            $validator->assert_moodle_version($CFG->version);
     1299            // TODO Check for missing dependencies during validation.
     1300            $result = $validator->execute();
     1301            if (!$silent) {
     1302                $result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
     1303                foreach ($validator->get_messages() as $message) {
     1304                    if ($message->level === $validator::INFO) {
     1305                        // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
     1306                        $level = DEBUG_NORMAL;
     1307                    } else if ($message->level === $validator::DEBUG) {
     1308                        // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
     1309                        $level = DEBUG_ALL;
     1310                    } else {
     1311                        // Display [Warning] and [Error] always.
     1312                        $level = null;
     1313                    }
     1314                    if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
     1315                        $this->mtrace('  <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
     1316                    } else {
     1317                        $this->mtrace('  ['.$validator->message_level_name($message->level).']', ' ', $level);
     1318                    }
     1319                    $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
     1320                    $info = $validator->message_code_info($message->msgcode, $message->addinfo);
     1321                    if ($info) {
     1322                        $this->mtrace('['.s($info).']', ' ', $level);
     1323                    } else if (is_string($message->addinfo)) {
     1324                        $this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
     1325                    } else {
     1326                        $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
     1327                    }
     1328                    if ($icon = $validator->message_help_icon($message->msgcode)) {
     1329                        if (CLI_SCRIPT) {
     1330                            $this->mtrace(PHP_EOL.'  ^^^ '.get_string('help').': '.
     1331                                get_string($icon->identifier.'_help', $icon->component), '', $level);
     1332                        } else {
     1333                            $this->mtrace($OUTPUT->render($icon), ' ', $level);
     1334                        }
     1335                    }
     1336                    $this->mtrace(PHP_EOL, '', $level);
     1337                }
     1338            }
     1339            if (!$result) {
     1340                $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
     1341                return false;
     1342            }
     1343        }
     1344        $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));
     1345
     1346        if (!$confirmed) {
     1347            return true;
     1348        }
     1349
     1350        // Extract all ZIP packs do the dirroot.
     1351        foreach ($plugins as $plugin) {
     1352            $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');
     1353            $zipfile = $zips[$plugin->component];
     1354            list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
     1355            $target = $this->get_plugintype_root($plugintype);
     1356            if (file_exists($target.'/'.$pluginname)) {
     1357                $this->remove_plugin_folder($this->get_plugin_info($plugin->component));
     1358            }
     1359            if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
     1360                $silent or $this->mtrace(get_string('error'));
     1361                $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
     1362                if (function_exists('opcache_reset')) {
     1363                    opcache_reset();
     1364                }
     1365                return false;
     1366            }
     1367            $silent or $this->mtrace($ok);
     1368        }
     1369        if (function_exists('opcache_reset')) {
     1370            opcache_reset();
     1371        }
     1372
     1373        return true;
     1374    }
     1375
     1376    /**
     1377     * Outputs the given message via {@link mtrace()}.
     1378     *
     1379     * If $debug is provided, then the message is displayed only at the given
     1380     * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
     1381     * site has developer debugging level selected).
     1382     *
     1383     * @param string $msg message
     1384     * @param string $eol end of line
     1385     * @param null|int $debug null to display always, int only on given debug level
     1386     */
     1387    protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
     1388        global $CFG;
     1389
     1390        if ($debug !== null and !debugging(null, $debug)) {
     1391            return;
     1392        }
     1393
     1394        mtrace($msg, $eol);
     1395    }
     1396
     1397    /**
    7961398     * Returns uninstall URL if exists.
    7971399     *
     
    8731475
    8741476    /**
     1477     * Returns list of available updates for the given component.
     1478     *
     1479     * This method should be considered as internal API and is supposed to be
     1480     * called by {@link \core\plugininfo\base::available_updates()} only
     1481     * to lazy load the data once they are first requested.
     1482     *
     1483     * @param string $component frankenstyle name of the plugin
     1484     * @return null|array array of \core\update\info objects or null
     1485     */
     1486    public function load_available_updates_for_plugin($component) {
     1487        global $CFG;
     1488
     1489        $provider = \core\update\checker::instance();
     1490
     1491        if (!$provider->enabled() or during_initial_install()) {
     1492            return null;
     1493        }
     1494
     1495        if (isset($CFG->updateminmaturity)) {
     1496            $minmaturity = $CFG->updateminmaturity;
     1497        } else {
     1498            // This can happen during the very first upgrade to 2.3.
     1499            $minmaturity = MATURITY_STABLE;
     1500        }
     1501
     1502        return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
     1503    }
     1504
     1505    /**
     1506     * Returns a list of all available updates to be installed.
     1507     *
     1508     * This is used when "update all plugins" action is performed at the
     1509     * administration UI screen.
     1510     *
     1511     * Returns array of remote info objects indexed by the plugin
     1512     * component. If there are multiple updates available (typically a mix of
     1513     * stable and non-stable ones), we pick the most mature most recent one.
     1514     *
     1515     * Plugins without explicit maturity are considered more mature than
     1516     * release candidates but less mature than explicit stable (this should be
     1517     * pretty rare case).
     1518     *
     1519     * @return array (string)component => (\core\update\remote_info)remoteinfo
     1520     */
     1521    public function available_updates() {
     1522
     1523        $updates = array();
     1524
     1525        foreach ($this->get_plugins() as $type => $plugins) {
     1526            foreach ($plugins as $plugin) {
     1527                $availableupdates = $plugin->available_updates();
     1528                if (empty($availableupdates)) {
     1529                    continue;
     1530                }
     1531                foreach ($availableupdates as $update) {
     1532                    if (empty($updates[$plugin->component])) {
     1533                        $updates[$plugin->component] = $update;
     1534                        continue;
     1535                    }
     1536                    $maturitycurrent = $updates[$plugin->component]->maturity;
     1537                    if (empty($maturitycurrent)) {
     1538                        $maturitycurrent = MATURITY_STABLE - 25;
     1539                    }
     1540                    $maturityremote = $update->maturity;
     1541                    if (empty($maturityremote)) {
     1542                        $maturityremote = MATURITY_STABLE - 25;
     1543                    }
     1544                    if ($maturityremote < $maturitycurrent) {
     1545                        continue;
     1546                    }
     1547                    if ($maturityremote > $maturitycurrent) {
     1548                        $updates[$plugin->component] = $update;
     1549                        continue;
     1550                    }
     1551                    if ($update->version > $updates[$plugin->component]->version) {
     1552                        $updates[$plugin->component] = $update;
     1553                        continue;
     1554                    }
     1555                }
     1556            }
     1557        }
     1558
     1559        foreach ($updates as $component => $update) {
     1560            $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
     1561            if (empty($remoteinfo) or empty($remoteinfo->version)) {
     1562                unset($updates[$component]);
     1563            } else {
     1564                $updates[$component] = $remoteinfo;
     1565            }
     1566        }
     1567
     1568        return $updates;
     1569    }
     1570
     1571    /**
    8751572     * Check to see if the given plugin folder can be removed by the web server process.
    8761573     *
     
    8931590        // Check that the folder and all its content is writable (thence removable).
    8941591        return $this->is_directory_removable($pluginfo->rootdir);
     1592    }
     1593
     1594    /**
     1595     * Is it possible to create a new plugin directory for the given plugin type?
     1596     *
     1597     * @throws coding_exception for invalid plugin types or non-existing plugin type locations
     1598     * @param string $plugintype
     1599     * @return boolean
     1600     */
     1601    public function is_plugintype_writable($plugintype) {
     1602
     1603        $plugintypepath = $this->get_plugintype_root($plugintype);
     1604
     1605        if (is_null($plugintypepath)) {
     1606            throw new coding_exception('Unknown plugin type: '.$plugintype);
     1607        }
     1608
     1609        if ($plugintypepath === false) {
     1610            throw new coding_exception('Plugin type location does not exist: '.$plugintype);
     1611        }
     1612
     1613        return is_writable($plugintypepath);
     1614    }
     1615
     1616    /**
     1617     * Returns the full path of the root of the given plugin type
     1618     *
     1619     * Null is returned if the plugin type is not known. False is returned if
     1620     * the plugin type root is expected but not found. Otherwise, string is
     1621     * returned.
     1622     *
     1623     * @param string $plugintype
     1624     * @return string|bool|null
     1625     */
     1626    public function get_plugintype_root($plugintype) {
     1627
     1628        $plugintypepath = null;
     1629        foreach (core_component::get_plugin_types() as $type => $fullpath) {
     1630            if ($type === $plugintype) {
     1631                $plugintypepath = $fullpath;
     1632                break;
     1633            }
     1634        }
     1635        if (is_null($plugintypepath)) {
     1636            return null;
     1637        }
     1638        if (!is_dir($plugintypepath)) {
     1639            return false;
     1640        }
     1641
     1642        return $plugintypepath;
    8951643    }
    8961644
     
    9141662            'enrol' => array('authorize'),
    9151663            'tinymce' => array('dragmath'),
    916             'tool' => array('bloglevelupgrade', 'qeupgradehelper'),
     1664            'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport'),
    9171665            'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
    9181666                'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
     
    9681716
    9691717            'block' => array(
    970                 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
    971                 'blog_recent', 'blog_tags', 'calendar_month',
     1718                'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
     1719                'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
    9721720                'calendar_upcoming', 'comments', 'community',
    9731721                'completionstatus', 'course_list', 'course_overview',
     
    10541802
    10551803            'ltiservice' => array(
    1056                 'profile', 'toolproxy', 'toolsettings'
     1804                'memberships', 'profile', 'toolproxy', 'toolsettings'
    10571805            ),
    10581806
     
    10971845            'qtype' => array(
    10981846                'calculated', 'calculatedmulti', 'calculatedsimple',
    1099                 'description', 'essay', 'match', 'missingtype', 'multianswer',
     1847                'ddimageortext', 'ddmarker', 'ddwtos', 'description',
     1848                'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
    11001849                'multichoice', 'numerical', 'random', 'randomsamatch',
    11011850                'shortanswer', 'truefalse'
     
    11131862            'report' => array(
    11141863                'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
    1115                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
     1864                'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance',
     1865                'usersessions',
    11161866            ),
    11171867
     
    11411891            'tool' => array(
    11421892                'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
    1143                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
     1893                'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
    11441894                'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
    1145                 'replace', 'spamcleaner', 'task', 'timezoneimport',
     1895                'replace', 'spamcleaner', 'task', 'templatelibrary',
    11461896                'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
    11471897            ),
     
    11691919            return false;
    11701920        }
     1921    }
     1922
     1923    /**
     1924     * Remove the current plugin code from the dirroot.
     1925     *
     1926     * If removing the currently installed version (which happens during
     1927     * updates), we archive the code so that the upgrade can be cancelled.
     1928     *
     1929     * To prevent accidental data-loss, we also archive the existing plugin
     1930     * code if cancelling installation of it, so that the developer does not
     1931     * loose the only version of their work-in-progress.
     1932     *
     1933     * @param \core\plugininfo\base $plugin
     1934     */
     1935    public function remove_plugin_folder(\core\plugininfo\base $plugin) {
     1936
     1937        if (!$this->is_plugin_folder_removable($plugin->component)) {
     1938            throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
     1939                array('plugin' => $pluginfo->component, 'rootdir' => $pluginfo->rootdir),
     1940                'plugin root folder is not removable as expected');
     1941        }
     1942
     1943        if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
     1944            $this->archive_plugin_version($plugin);
     1945        }
     1946
     1947        remove_dir($plugin->rootdir);
     1948        clearstatcache();
     1949        if (function_exists('opcache_reset')) {
     1950            opcache_reset();
     1951        }
     1952    }
     1953
     1954    /**
     1955     * Can the installation of the new plugin be cancelled?
     1956     *
     1957     * Subplugins can be cancelled only via their parent plugin, not separately
     1958     * (they are considered as implicit requirements if distributed together
     1959     * with the main package).
     1960     *
     1961     * @param \core\plugininfo\base $plugin
     1962     * @return bool
     1963     */
     1964    public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
     1965        global $CFG;
     1966
     1967        if (!empty($CFG->disableupdateautodeploy)) {
     1968            return false;
     1969        }
     1970
     1971        if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
     1972                or !$this->is_plugin_folder_removable($plugin->component)) {
     1973            return false;
     1974        }
     1975
     1976        if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
     1977            return true;
     1978        }
     1979
     1980        return false;
     1981    }
     1982
     1983    /**
     1984     * Can the upgrade of the existing plugin be cancelled?
     1985     *
     1986     * Subplugins can be cancelled only via their parent plugin, not separately
     1987     * (they are considered as implicit requirements if distributed together
     1988     * with the main package).
     1989     *
     1990     * @param \core\plugininfo\base $plugin
     1991     * @return bool
     1992     */
     1993    public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
     1994        global $CFG;
     1995
     1996        if (!empty($CFG->disableupdateautodeploy)) {
     1997            // Cancelling the plugin upgrade is actually installation of the
     1998            // previously archived version.
     1999            return false;
     2000        }
     2001
     2002        if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
     2003                or !$this->is_plugin_folder_removable($plugin->component)) {
     2004            return false;
     2005        }
     2006
     2007        if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
     2008            if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
     2009                return true;
     2010            }
     2011        }
     2012
     2013        return false;
     2014    }
     2015
     2016    /**
     2017     * Removes the plugin code directory if it is not installed yet.
     2018     *
     2019     * This is intended for the plugins check screen to give the admin a chance
     2020     * to cancel the installation of just unzipped plugin before the database
     2021     * upgrade happens.
     2022     *
     2023     * @param string $component
     2024     */
     2025    public function cancel_plugin_installation($component) {
     2026        global $CFG;
     2027
     2028        if (!empty($CFG->disableupdateautodeploy)) {
     2029            return false;
     2030        }
     2031
     2032        $plugin = $this->get_plugin_info($component);
     2033
     2034        if ($this->can_cancel_plugin_installation($plugin)) {
     2035            $this->remove_plugin_folder($plugin);
     2036        }
     2037
     2038        return false;
     2039    }
     2040
     2041    /**
     2042     * Returns plugins, the installation of which can be cancelled.
     2043     *
     2044     * @return array [(string)component] => (\core\plugininfo\base)plugin
     2045     */
     2046    public function list_cancellable_installations() {
     2047        global $CFG;
     2048
     2049        if (!empty($CFG->disableupdateautodeploy)) {
     2050            return array();
     2051        }
     2052
     2053        $cancellable = array();
     2054        foreach ($this->get_plugins() as $type => $plugins) {
     2055            foreach ($plugins as $plugin) {
     2056                if ($this->can_cancel_plugin_installation($plugin)) {
     2057                    $cancellable[$plugin->component] = $plugin;
     2058                }
     2059            }
     2060        }
     2061
     2062        return $cancellable;
     2063    }
     2064
     2065    /**
     2066     * Archive the current on-disk plugin code.
     2067     *
     2068     * @param \core\plugiinfo\base $plugin
     2069     * @return bool
     2070     */
     2071    public function archive_plugin_version(\core\plugininfo\base $plugin) {
     2072        return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
     2073    }
     2074
     2075    /**
     2076     * Returns list of all archives that can be installed to cancel the plugin upgrade.
     2077     *
     2078     * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
     2079     */
     2080    public function list_restorable_archives() {
     2081        global $CFG;
     2082
     2083        if (!empty($CFG->disableupdateautodeploy)) {
     2084            return false;
     2085        }
     2086
     2087        $codeman = $this->get_code_manager();
     2088        $restorable = array();
     2089        foreach ($this->get_plugins() as $type => $plugins) {
     2090            foreach ($plugins as $plugin) {
     2091                if ($this->can_cancel_plugin_upgrade($plugin)) {
     2092                    $restorable[$plugin->component] = (object)array(
     2093                        'component' => $plugin->component,
     2094                        'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
     2095                    );
     2096                }
     2097            }
     2098        }
     2099
     2100        return $restorable;
    11712101    }
    11722102
     
    12412171     * @return boolean
    12422172     */
    1243     protected function is_directory_removable($fullpath) {
     2173    public function is_directory_removable($fullpath) {
    12442174
    12452175        if (!is_writable($fullpath)) {
     
    12892219        }
    12902220
    1291         if ($pluginfo->get_status() === self::PLUGIN_STATUS_NEW) {
     2221        if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
    12922222            // The plugin is not installed. It should be either installed or removed from the disk.
    12932223            // Relying on this temporary state may be tricky.
     
    13042234        return true;
    13052235    }
     2236
     2237    /**
     2238     * Returns a code_manager instance to be used for the plugins code operations.
     2239     *
     2240     * @return \core\update\code_manager
     2241     */
     2242    protected function get_code_manager() {
     2243
     2244        if ($this->codemanager === null) {
     2245            $this->codemanager = new \core\update\code_manager();
     2246        }
     2247
     2248        return $this->codemanager;
     2249    }
     2250
     2251    /**
     2252     * Returns a client for https://download.moodle.org/api/
     2253     *
     2254     * @return \core\update\api
     2255     */
     2256    protected function get_update_api_client() {
     2257
     2258        if ($this->updateapiclient === null) {
     2259            $this->updateapiclient = \core\update\api::client();
     2260        }
     2261
     2262        return $this->updateapiclient;
     2263    }
    13062264}
Note: See TracChangeset for help on using the changeset viewer.