AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** This library includes all the necessary stuff to use the one-click* download and install feature of Moodle, used to keep updated some* items like languages, pear, enviroment... i.e, components.** It has been developed harcoding some important limits that are* explained below:* - It only can check, download and install items under moodledata.* - Every downloadeable item must be one zip file.* - The zip file root content must be 1 directory, i.e, everything* is stored under 1 directory.* - Zip file name and root directory must have the same name (but* the .zip extension, of course).* - Every .zip file must be defined in one .md5 file that will be* stored in the same remote directory than the .zip file.* - The name of such .md5 file is free, although it's recommended* to use the same name than the .zip (that's the default* assumption if no specified).* - Every remote .md5 file will be a comma separated (CVS) file where each* line will follow this format:* - Field 1: name of the zip file (without extension). Mandatory.* - Field 2: md5 of the zip file. Mandatory.* - Field 3: whatever you want (or need). Optional.* -Every local .md5 file will:* - Have the zip file name (without the extension) plus -md5* - Will reside inside the expanded zip file dir* - Will contain the md5 od the latest installed component* With all these details present, the process will perform this tasks:* - Perform security checks. Only admins are allowed to use this for now.* - Read the .md5 file from source (1).* - Extract the correct line for the .zip being requested.* - Compare it with the local .md5 file (2).* - If different:* - Download the newer .zip file from source.* - Calculate its md5 (3).* - Compare (1) and (3).* - If equal:* - Delete old directory.* - Uunzip the newer .zip file.* - Create the new local .md5 file.* - Delete the .zip file.* - If different:* - ERROR. Old package won't be modified. We shouldn't* reach here ever.* - If component download is not possible, a message text about how to do* the process manually (remotedownloaderror) must be displayed to explain it.** General Usage:** To install one component:* <code>* require_once($CFG->libdir.'/componentlib.class.php');* if ($cd = new component_installer('https://download.moodle.org', 'langpack/2.0',* 'es.zip', 'languages.md5', 'lang')) {* $status = $cd->install(); //returns COMPONENT_(ERROR | UPTODATE | INSTALLED)* switch ($status) {* case COMPONENT_ERROR:* if ($cd->get_error() == 'remotedownloaderror') {* $a = new stdClass();* $a->url = 'https://download.moodle.org/langpack/2.0/es.zip';* $a->dest= $CFG->dataroot.'/lang';* throw new \moodle_exception($cd->get_error(), 'error', '', $a);* } else {* throw new \moodle_exception($cd->get_error(), 'error');* }* break;* case COMPONENT_UPTODATE:* //Print error string or whatever you want to do* break;* case COMPONENT_INSTALLED:* //Print/do whatever you want* break;* default:* //We shouldn't reach this point* }* } else {* //We shouldn't reach this point* }* </code>** To switch of component (maintaining the rest of settings):* <code>* $status = $cd->change_zip_file('en.zip'); //returns boolean false on error* </code>** To retrieve all the components in one remote md5 file* <code>* $components = $cd->get_all_components_md5(); //returns boolean false on error, array instead* </code>** To check if current component needs to be updated* <code>* $status = $cd->need_upgrade(); //returns COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)* </code>** To get the 3rd field of the md5 file (optional)* <code>* $field = $cd->get_extra_md5_field(); //returns string (empty if not exists)* </code>** For all the error situations the $cd->get_error() method should return always the key of the* error to be retrieved by one standard get_string() call against the error.php lang file.** That's all!** @package core* @copyright (C) 2001-3001 Eloy Lafuente (stronk7) {@link http://contiento.com}* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();/*** @global object $CFG* @name $CFG*/global $CFG;require_once($CFG->libdir.'/filelib.php');// Some needed constantsdefine('COMPONENT_ERROR', 0);define('COMPONENT_UPTODATE', 1);define('COMPONENT_NEEDUPDATE', 2);define('COMPONENT_INSTALLED', 3);/*** This class is used to check, download and install items from* download.moodle.org to the moodledata directory.** It always return true/false in all their public methods to say if* execution has ended succesfuly or not. If there is any problem* its getError() method can be called, returning one error string* to be used with the standard get/print_string() functions.** @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @package moodlecore*/class component_installer {/*** @var string*/var $sourcebase; /// Full http URL, base for downloadable itemsvar $zippath; /// Relative path (from sourcebase) where the/// downloadeable item resides.var $zipfilename; /// Name of the .zip file to be downloadedvar $md5filename; /// Name of the .md5 file to be readvar $componentname;/// Name of the component. Must be the zip name without/// the extension. And it defines a lot of things:/// the md5 line to search for, the default m5 file name/// and the name of the root dir stored inside the zip filevar $destpath; /// Relative path (from moodledata) where the .zip/// file will be expanded.var $errorstring; /// Latest error produced. It will contain one lang string key.var $extramd5info; /// Contents of the optional third field in the .md5 file.var $requisitesok; /// Flag to see if requisites check has been passed ok./*** @var array*/var $cachedmd5components; /// Array of cached components to avoid to/// download the same md5 file more than once per request./*** Standard constructor of the class. It will initialize all attributes.* without performing any check at all.** @param string $sourcebase Full http URL, base for downloadeable items* @param string $zippath Relative path (from sourcebase) where the* downloadeable item resides* @param string $zipfilename Name of the .zip file to be downloaded* @param string $md5filename Name of the .md5 file to be read (default '' = same* than zipfilename)* @param string $destpath Relative path (from moodledata) where the .zip file will* be expanded (default='' = moodledataitself)* @return object*/public function __construct($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {$this->sourcebase = $sourcebase;$this->zippath = $zippath;$this->zipfilename = $zipfilename;$this->md5filename = $md5filename;$this->componentname= '';$this->destpath = $destpath;$this->errorstring = '';$this->extramd5info = '';$this->requisitesok = false;$this->cachedmd5components = array();$this->check_requisites();}/*** Old syntax of class constructor. Deprecated in PHP7.** @deprecated since Moodle 3.1*/public function component_installer($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);self::__construct($sourcebase, $zippath, $zipfilename, $md5filename, $destpath);}/*** This function will check if everything is properly set to begin* one installation. Also, it will check for required settings* and will fill everything as needed.** @global object* @return boolean true/false (plus detailed error in errorstring)*/function check_requisites() {global $CFG;$this->requisitesok = false;/// Check that everything we need is presentif (empty($this->sourcebase) || empty($this->zipfilename)) {$this->errorstring='missingrequiredfield';return false;}/// Check for correct sourcebase (this will be out in the future)if (!PHPUNIT_TEST and $this->sourcebase != 'https://download.moodle.org') {$this->errorstring='wrongsourcebase';return false;}/// Check the zip file is a correct one (by extension)if (stripos($this->zipfilename, '.zip') === false) {$this->errorstring='wrongzipfilename';return false;}/// Check that exists under datarootif (!empty($this->destpath)) {if (!file_exists($CFG->dataroot.'/'.$this->destpath)) {$this->errorstring='wrongdestpath';return false;}}/// Calculate the componentname$pos = stripos($this->zipfilename, '.zip');$this->componentname = substr($this->zipfilename, 0, $pos);/// Calculate md5filename if it's emptyif (empty($this->md5filename)) {$this->md5filename = $this->componentname.'.md5';}/// Set the requisites passed flag$this->requisitesok = true;return true;}/*** This function will perform the full installation if needed, i.e.* compare md5 values, download, unzip, install and regenerate* local md5 file** @uses COMPONENT_ERROR* @uses COMPONENT_UPTODATE* @uses COMPONENT_ERROR* @uses COMPONENT_INSTALLED* @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)*/public function install() {global $CFG;/// Check requisites are passedif (!$this->requisitesok) {return COMPONENT_ERROR;}/// Confirm we need upgradeif ($this->need_upgrade() === COMPONENT_ERROR) {return COMPONENT_ERROR;} else if ($this->need_upgrade() === COMPONENT_UPTODATE) {$this->errorstring='componentisuptodate';return COMPONENT_UPTODATE;}/// Create temp directory if necesaryif (!make_temp_directory('', false)) {$this->errorstring='cannotcreatetempdir';return COMPONENT_ERROR;}/// Download zip file and save it to tempif ($this->zippath) {$source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;} else {$source = $this->sourcebase.'/'.$this->zipfilename;}$zipfile= $CFG->tempdir.'/'.$this->zipfilename;$contents = download_file_content($source, null, null, true);if ($contents->results && (int) $contents->status === 200) {if ($file = fopen($zipfile, 'w')) {if (!fwrite($file, $contents->results)) {fclose($file);$this->errorstring='cannotsavezipfile';return COMPONENT_ERROR;}} else {$this->errorstring='cannotsavezipfile';return COMPONENT_ERROR;}fclose($file);} else {$this->errorstring='cannotdownloadzipfile';return COMPONENT_ERROR;}/// Calculate its md5$new_md5 = md5($contents->results);/// Compare it with the remote md5 to check if we have the correct zip fileif (!$remote_md5 = $this->get_component_md5()) {return COMPONENT_ERROR;}if ($new_md5 != $remote_md5) {$this->errorstring='downloadedfilecheckfailed';return COMPONENT_ERROR;}// Move current revision to a safe place.$destinationdir = $CFG->dataroot . '/' . $this->destpath;$destinationcomponent = $destinationdir . '/' . $this->componentname;$destinationcomponentold = $destinationcomponent . '_old';@remove_dir($destinationcomponentold); // Deleting a possible old version.// Moving to a safe place.@rename($destinationcomponent, $destinationcomponentold);// Unzip new version.$packer = get_file_packer('application/zip');$unzipsuccess = $packer->extract_to_pathname($zipfile, $destinationdir, null, null, true);if (!$unzipsuccess) {@remove_dir($destinationcomponent);@rename($destinationcomponentold, $destinationcomponent);$this->errorstring = 'cannotunzipfile';return COMPONENT_ERROR;}// Delete old component version.@remove_dir($destinationcomponentold);// Create local md5.if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {if (!fwrite($file, $new_md5)) {fclose($file);$this->errorstring='cannotsavemd5file';return COMPONENT_ERROR;}} else {$this->errorstring='cannotsavemd5file';return COMPONENT_ERROR;}fclose($file);/// Delete temp zip file@unlink($zipfile);return COMPONENT_INSTALLED;}/*** This function will detect if remote component needs to be installed* because it's different from the local one** @uses COMPONENT_ERROR* @uses COMPONENT_UPTODATE* @uses COMPONENT_NEEDUPDATE* @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)*/function need_upgrade() {/// Check requisites are passedif (!$this->requisitesok) {return COMPONENT_ERROR;}/// Get local md5$local_md5 = $this->get_local_md5();/// Get remote md5if (!$remote_md5 = $this->get_component_md5()) {return COMPONENT_ERROR;}/// Return resultif ($local_md5 == $remote_md5) {return COMPONENT_UPTODATE;} else {return COMPONENT_NEEDUPDATE;}}/*** This function will change the zip file to install on the fly* to allow the class to process different components of the* same md5 file without intantiating more objects.** @param string $newzipfilename New zip filename to process* @return boolean true/false*/function change_zip_file($newzipfilename) {$this->zipfilename = $newzipfilename;return $this->check_requisites();}/*** This function will get the local md5 value of the installed* component.** @global object* @return bool|string md5 of the local component (false on error)*/function get_local_md5() {global $CFG;/// Check requisites are passedif (!$this->requisitesok) {return false;}$return_value = 'needtobeinstalled'; /// Fake value to force new installation/// Calculate source to read$source = $CFG->dataroot.'/'.$this->destpath.'/'.$this->componentname.'/'.$this->componentname.'.md5';/// Read md5 value stored (if exists)if (file_exists($source)) {if ($temp = file_get_contents($source)) {$return_value = $temp;}}return $return_value;}/*** This function will download the specified md5 file, looking for the* current componentname, returning its md5 field and storing extramd5info* if present. Also it caches results to cachedmd5components for better* performance in the same request.** @return mixed md5 present in server (or false if error)*/function get_component_md5() {/// Check requisites are passedif (!$this->requisitesok) {return false;}/// Get all components of md5 fileif (!$comp_arr = $this->get_all_components_md5()) {if (empty($this->errorstring)) {$this->errorstring='cannotdownloadcomponents';}return false;}/// Search for the componentname componentif (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) {$this->errorstring='cannotfindcomponent';return false;}/// Check we have a valid md5if (empty($component[1]) || strlen($component[1]) != 32) {$this->errorstring='invalidmd5';return false;}/// Set the extramd5info fieldif (!empty($component[2])) {$this->extramd5info = $component[2];}return $component[1];}/*** This function allows you to retrieve the complete array of components found in* the md5filename** @return bool|array array of components in md5 file or false if error*/function get_all_components_md5() {/// Check requisites are passedif (!$this->requisitesok) {return false;}/// Initialize components array$comp_arr = array();/// Define and retrieve the full md5 fileif ($this->zippath) {$source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;} else {$source = $this->sourcebase.'/'.$this->md5filename;}/// Check if we have downloaded the md5 file before (per request cache)if (!empty($this->cachedmd5components[$source])) {$comp_arr = $this->cachedmd5components[$source];} else {/// Not downloaded, let's do it now$availablecomponents = array();$contents = download_file_content($source, null, null, true);if ($contents->results && (int) $contents->status === 200) {/// Split text into lines$lines = preg_split('/\r?\n/', $contents->results);/// Each line will be one componentforeach($lines as $line) {$availablecomponents[] = explode(',', $line);}/// If no components have been found, return errorif (empty($availablecomponents)) {$this->errorstring='cannotdownloadcomponents';return false;}/// Build an associative array of components for easily search/// applying trim to avoid linefeeds and other...$comp_arr = array();foreach ($availablecomponents as $component) {/// Avoid sometimes empty linesif (empty($component[0])) {continue;}$component[0]=trim($component[0]);if (!empty($component[1])) {$component[1]=trim($component[1]);}if (!empty($component[2])) {$component[2]=trim($component[2]);}$comp_arr[$component[0]] = $component;}/// Cache components$this->cachedmd5components[$source] = $comp_arr;} else {/// Return error$this->errorstring='remotedownloaderror';return false;}}/// If there is no commponents or erros found, errorif (!empty($this->errorstring)) {return false;} else if (empty($comp_arr)) {$this->errorstring='cannotdownloadcomponents';return false;}return $comp_arr;}/*** This function returns the errorstring** @return string the error string*/function get_error() {return $this->errorstring;}/** This function returns the extramd5 field (optional in md5 file)** @return string the extramd5 field*/function get_extra_md5_field() {return $this->extramd5info;}} /// End of component_installer class/*** Language packs installer** This class wraps the functionality provided by {@link component_installer}* and adds support for installing a set of language packs.** Given an array of required language packs, this class fetches them all* and installs them. It detects eventual dependencies and installs* all parent languages, too.** @copyright 2011 David Mudrak <david@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class lang_installer {/** lang pack was successfully downloaded and deployed */const RESULT_INSTALLED = 'installed';/** lang pack was up-to-date so no download was needed */const RESULT_UPTODATE = 'uptodate';/** there was a problem with downloading the lang pack */const RESULT_DOWNLOADERROR = 'downloaderror';/** @var array of languages to install */protected $queue = array();/** @var string the code of language being currently installed */protected $current;/** @var array of languages already installed by this instance */protected $done = array();/** @var string this Moodle major version */protected $version;/*** Prepare the installer** @param string|array $langcode a code of the language to install*/public function __construct($langcode = '') {global $CFG;$this->set_queue($langcode);$this->version = moodle_major_version(true);if (!empty($CFG->langotherroot) and $CFG->langotherroot !== $CFG->dataroot . '/lang') {debugging('The in-built language pack installer does not support alternative location ' .'of languages root directory. You are supposed to install and update your language '.'packs on your own.');}}/*** Sets the queue of language packs to be installed** @param string|array $langcodes language code like 'cs' or a list of them*/public function set_queue($langcodes) {if (is_array($langcodes)) {$this->queue = $langcodes;} else if (!empty($langcodes)) {$this->queue = array($langcodes);}}/*** Runs the installer** This method calls {@link self::install_language_pack} for every language in the* queue. If a dependency is detected, the parent language is added to the queue.** @return array results, array of self::RESULT_xxx constants indexed by language code*/public function run() {$results = array();while ($this->current = array_shift($this->queue)) {if ($this->was_processed($this->current)) {// do not repeat yourselfcontinue;}if ($this->current === 'en') {$this->mark_processed($this->current);continue;}$results[$this->current] = $this->install_language_pack($this->current);if (in_array($results[$this->current], array(self::RESULT_INSTALLED, self::RESULT_UPTODATE))) {if ($parentlang = $this->get_parent_language($this->current)) {if (!$this->is_queued($parentlang) and !$this->was_processed($parentlang)) {$this->add_to_queue($parentlang);}}}$this->mark_processed($this->current);}return $results;}/*** Returns the URL where a given language pack can be downloaded** Alternatively, if the parameter is empty, returns URL of the page with the* list of all available language packs.** @param string $langcode language code like 'cs' or empty for unknown* @return string URL*/public function lang_pack_url($langcode = '') {if (empty($langcode)) {return 'https://download.moodle.org/langpack/'.$this->version.'/';} else {return 'https://download.moodle.org/download.php/langpack/'.$this->version.'/'.$langcode.'.zip';}}/*** Returns the list of available language packs from download.moodle.org** @return array|bool false if can not download*/public function get_remote_list_of_languages() {$source = 'https://download.moodle.org/langpack/' . $this->version . '/languages.md5';$availablelangs = array();$contents = download_file_content($source, null, null, true);if ($contents->results && (int) $contents->status === 200) {$alllines = explode("\n", $contents->results);foreach($alllines as $line) {if (!empty($line)){$availablelangs[] = explode(',', $line);}}return $availablelangs;} else {return false;}}// Internal implementation //////////////////////////////////////////////////*** Adds a language pack (or a list of them) to the queue** @param string|array $langcodes code of the language to install or a list of them*/protected function add_to_queue($langcodes) {if (is_array($langcodes)) {$this->queue = array_merge($this->queue, $langcodes);} else if (!empty($langcodes)) {$this->queue[] = $langcodes;}}/*** Checks if the given language is queued or if the queue is empty** @example $installer->is_queued('es'); // is Spanish going to be installed?* @example $installer->is_queued(); // is there a language queued?** @param string $langcode language code or empty string for "any"* @return boolean*/protected function is_queued($langcode = '') {if (empty($langcode)) {return !empty($this->queue);} else {return in_array($langcode, $this->queue);}}/*** Checks if the given language has already been processed by this instance** @see self::mark_processed()* @param string $langcode* @return boolean*/protected function was_processed($langcode) {return isset($this->done[$langcode]);}/*** Mark the given language pack as processed** @see self::was_processed()* @param string $langcode*/protected function mark_processed($langcode) {$this->done[$langcode] = 1;}/*** Returns a parent language of the given installed language** @param string $langcode* @return string parent language's code*/protected function get_parent_language($langcode) {return get_parent_language($langcode);}/*** Perform the actual language pack installation** @uses component_installer* @param string $langcode* @return string return status*/protected function install_language_pack($langcode) {// initialise new component installer to process this language$installer = new component_installer('https://download.moodle.org', 'download.php/direct/langpack/' . $this->version,$langcode . '.zip', 'languages.md5', 'lang');if (!$installer->requisitesok) {throw new lang_installer_exception('installer_requisites_check_failed');}$status = $installer->install();if ($status == COMPONENT_ERROR) {if ($installer->get_error() === 'remotedownloaderror') {return self::RESULT_DOWNLOADERROR;} else {throw new lang_installer_exception($installer->get_error(), $langcode);}} else if ($status == COMPONENT_UPTODATE) {return self::RESULT_UPTODATE;} else if ($status == COMPONENT_INSTALLED) {return self::RESULT_INSTALLED;} else {throw new lang_installer_exception('unexpected_installer_result', $status);}}}/*** Exception thrown by {@link lang_installer}** @copyright 2011 David Mudrak <david@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class lang_installer_exception extends moodle_exception {public function __construct($errorcode, $debuginfo = null) {parent::__construct($errorcode, 'error', '', null, $debuginfo);}}