Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
<?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';
< * print_error($cd->get_error(), 'error', '', $a);
> * throw new \moodle_exception($cd->get_error(), 'error', '', $a);
* } else {
< * print_error($cd->get_error(), 'error');
> * 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 constants define('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 items var $zippath; /// Relative path (from sourcebase) where the /// downloadeable item resides. var $zipfilename; /// Name of the .zip file to be downloaded var $md5filename; /// Name of the .md5 file to be read var $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 file var $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 present if (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 dataroot if (!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 empty if (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 passed if (!$this->requisitesok) { return COMPONENT_ERROR; } /// Confirm we need upgrade if ($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 necesary if (!make_temp_directory('', false)) { $this->errorstring='cannotcreatetempdir'; return COMPONENT_ERROR; } /// Download zip file and save it to temp if ($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 file if (!$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 passed if (!$this->requisitesok) { return COMPONENT_ERROR; } /// Get local md5 $local_md5 = $this->get_local_md5(); /// Get remote md5 if (!$remote_md5 = $this->get_component_md5()) { return COMPONENT_ERROR; } /// Return result if ($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 passed if (!$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 passed if (!$this->requisitesok) { return false; } /// Get all components of md5 file if (!$comp_arr = $this->get_all_components_md5()) { if (empty($this->errorstring)) { $this->errorstring='cannotdownloadcomponents'; } return false; } /// Search for the componentname component if (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) { $this->errorstring='cannotfindcomponent'; return false; } /// Check we have a valid md5 if (empty($component[1]) || strlen($component[1]) != 32) { $this->errorstring='invalidmd5'; return false; } /// Set the extramd5info field if (!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 passed if (!$this->requisitesok) { return false; } /// Initialize components array $comp_arr = array(); /// Define and retrieve the full md5 file if ($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 component foreach($lines as $line) { $availablecomponents[] = explode(',', $line); } /// If no components have been found, return error if (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 lines if (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, error if (!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 yourself continue; } 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 int 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); } }