<?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;
< if($contents = download_file_content($source)) {
> $contents = download_file_content($source, null, null, true);
> if ($contents->results && (int) $contents->status === 200) {
if ($file = fopen($zipfile, 'w')) {
< if (!fwrite($file, $contents)) {
> 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);
> $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();
< if ($contents = download_file_content($source)) {
> $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);
> $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();
< if ($content = download_file_content($source)) {
< $alllines = explode("\n", $content);
> $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);
}
}