3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * This library includes all the necessary stuff to use the one-click
20 * download and install feature of Moodle, used to keep updated some
21 * items like languages, pear, enviroment... i.e, components.
23 * It has been developed harcoding some important limits that are
25 * - It only can check, download and install items under moodledata.
26 * - Every downloadeable item must be one zip file.
27 * - The zip file root content must be 1 directory, i.e, everything
28 * is stored under 1 directory.
29 * - Zip file name and root directory must have the same name (but
30 * the .zip extension, of course).
31 * - Every .zip file must be defined in one .md5 file that will be
32 * stored in the same remote directory than the .zip file.
33 * - The name of such .md5 file is free, although it's recommended
34 * to use the same name than the .zip (that's the default
35 * assumption if no specified).
36 * - Every remote .md5 file will be a comma separated (CVS) file where each
37 * line will follow this format:
38 * - Field 1: name of the zip file (without extension). Mandatory.
39 * - Field 2: md5 of the zip file. Mandatory.
40 * - Field 3: whatever you want (or need). Optional.
41 * -Every local .md5 file will:
42 * - Have the zip file name (without the extension) plus -md5
43 * - Will reside inside the expanded zip file dir
44 * - Will contain the md5 od the latest installed component
45 * With all these details present, the process will perform this tasks:
46 * - Perform security checks. Only admins are allowed to use this for now.
47 * - Read the .md5 file from source (1).
48 * - Extract the correct line for the .zip being requested.
49 * - Compare it with the local .md5 file (2).
51 * - Download the newer .zip file from source.
52 * - Calculate its md5 (3).
53 * - Compare (1) and (3).
55 * - Delete old directory.
56 * - Uunzip the newer .zip file.
57 * - Create the new local .md5 file.
58 * - Delete the .zip file.
60 * - ERROR. Old package won't be modified. We shouldn't
62 * - If component download is not possible, a message text about how to do
63 * the process manually (remotedownloaderror) must be displayed to explain it.
67 * To install one component:
69 * require_once($CFG->libdir.'/componentlib.class.php');
70 * if ($cd = new component_installer('https://download.moodle.org', 'langpack/2.0',
71 * 'es.zip', 'languages.md5', 'lang')) {
72 * $status = $cd->install(); //returns COMPONENT_(ERROR | UPTODATE | INSTALLED)
74 * case COMPONENT_ERROR:
75 * if ($cd->get_error() == 'remotedownloaderror') {
76 * $a = new stdClass();
77 * $a->url = 'https://download.moodle.org/langpack/2.0/es.zip';
78 * $a->dest= $CFG->dataroot.'/lang';
79 * throw new \moodle_exception($cd->get_error(), 'error', '', $a);
81 * throw new \moodle_exception($cd->get_error(), 'error');
84 * case COMPONENT_UPTODATE:
85 * //Print error string or whatever you want to do
87 * case COMPONENT_INSTALLED:
88 * //Print/do whatever you want
91 * //We shouldn't reach this point
94 * //We shouldn't reach this point
98 * To switch of component (maintaining the rest of settings):
100 * $status = $cd->change_zip_file('en.zip'); //returns boolean false on error
103 * To retrieve all the components in one remote md5 file
105 * $components = $cd->get_all_components_md5(); //returns boolean false on error, array instead
108 * To check if current component needs to be updated
110 * $status = $cd->need_upgrade(); //returns COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
113 * To get the 3rd field of the md5 file (optional)
115 * $field = $cd->get_extra_md5_field(); //returns string (empty if not exists)
118 * For all the error situations the $cd->get_error() method should return always the key of the
119 * error to be retrieved by one standard get_string() call against the error.php lang file.
124 * @copyright (C) 2001-3001 Eloy Lafuente (stronk7) {@link http://contiento.com}
125 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
128 defined('MOODLE_INTERNAL') ||
die();
131 * @global object $CFG
135 require_once($CFG->libdir
.'/filelib.php');
137 // Some needed constants
138 define('COMPONENT_ERROR', 0);
139 define('COMPONENT_UPTODATE', 1);
140 define('COMPONENT_NEEDUPDATE', 2);
141 define('COMPONENT_INSTALLED', 3);
144 * This class is used to check, download and install items from
145 * download.moodle.org to the moodledata directory.
147 * It always return true/false in all their public methods to say if
148 * execution has ended succesfuly or not. If there is any problem
149 * its getError() method can be called, returning one error string
150 * to be used with the standard get/print_string() functions.
152 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
153 * @package moodlecore
155 class component_installer
{
159 var $sourcebase; /// Full http URL, base for downloadable items
160 var $zippath; /// Relative path (from sourcebase) where the
161 /// downloadeable item resides.
162 var $zipfilename; /// Name of the .zip file to be downloaded
163 var $md5filename; /// Name of the .md5 file to be read
164 var $componentname;/// Name of the component. Must be the zip name without
165 /// the extension. And it defines a lot of things:
166 /// the md5 line to search for, the default m5 file name
167 /// and the name of the root dir stored inside the zip file
168 var $destpath; /// Relative path (from moodledata) where the .zip
169 /// file will be expanded.
170 var $errorstring; /// Latest error produced. It will contain one lang string key.
171 var $extramd5info; /// Contents of the optional third field in the .md5 file.
172 var $requisitesok; /// Flag to see if requisites check has been passed ok.
176 var $cachedmd5components; /// Array of cached components to avoid to
177 /// download the same md5 file more than once per request.
180 * Standard constructor of the class. It will initialize all attributes.
181 * without performing any check at all.
183 * @param string $sourcebase Full http URL, base for downloadeable items
184 * @param string $zippath Relative path (from sourcebase) where the
185 * downloadeable item resides
186 * @param string $zipfilename Name of the .zip file to be downloaded
187 * @param string $md5filename Name of the .md5 file to be read (default '' = same
189 * @param string $destpath Relative path (from moodledata) where the .zip file will
190 * be expanded (default='' = moodledataitself)
193 public function __construct($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
195 $this->sourcebase
= $sourcebase;
196 $this->zippath
= $zippath;
197 $this->zipfilename
= $zipfilename;
198 $this->md5filename
= $md5filename;
199 $this->componentname
= '';
200 $this->destpath
= $destpath;
201 $this->errorstring
= '';
202 $this->extramd5info
= '';
203 $this->requisitesok
= false;
204 $this->cachedmd5components
= array();
206 $this->check_requisites();
210 * Old syntax of class constructor. Deprecated in PHP7.
212 * @deprecated since Moodle 3.1
214 public function component_installer($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
215 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER
);
216 self
::__construct($sourcebase, $zippath, $zipfilename, $md5filename, $destpath);
220 * This function will check if everything is properly set to begin
221 * one installation. Also, it will check for required settings
222 * and will fill everything as needed.
225 * @return boolean true/false (plus detailed error in errorstring)
227 function check_requisites() {
230 $this->requisitesok
= false;
232 /// Check that everything we need is present
233 if (empty($this->sourcebase
) ||
empty($this->zipfilename
)) {
234 $this->errorstring
='missingrequiredfield';
237 /// Check for correct sourcebase (this will be out in the future)
238 if (!PHPUNIT_TEST
and $this->sourcebase
!= 'https://download.moodle.org') {
239 $this->errorstring
='wrongsourcebase';
242 /// Check the zip file is a correct one (by extension)
243 if (stripos($this->zipfilename
, '.zip') === false) {
244 $this->errorstring
='wrongzipfilename';
247 /// Check that exists under dataroot
248 if (!empty($this->destpath
)) {
249 if (!file_exists($CFG->dataroot
.'/'.$this->destpath
)) {
250 $this->errorstring
='wrongdestpath';
254 /// Calculate the componentname
255 $pos = stripos($this->zipfilename
, '.zip');
256 $this->componentname
= substr($this->zipfilename
, 0, $pos);
257 /// Calculate md5filename if it's empty
258 if (empty($this->md5filename
)) {
259 $this->md5filename
= $this->componentname
.'.md5';
261 /// Set the requisites passed flag
262 $this->requisitesok
= true;
267 * This function will perform the full installation if needed, i.e.
268 * compare md5 values, download, unzip, install and regenerate
271 * @uses COMPONENT_ERROR
272 * @uses COMPONENT_UPTODATE
273 * @uses COMPONENT_ERROR
274 * @uses COMPONENT_INSTALLED
275 * @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)
277 public function install() {
280 /// Check requisites are passed
281 if (!$this->requisitesok
) {
282 return COMPONENT_ERROR
;
284 /// Confirm we need upgrade
285 if ($this->need_upgrade() === COMPONENT_ERROR
) {
286 return COMPONENT_ERROR
;
287 } else if ($this->need_upgrade() === COMPONENT_UPTODATE
) {
288 $this->errorstring
='componentisuptodate';
289 return COMPONENT_UPTODATE
;
291 /// Create temp directory if necesary
292 if (!make_temp_directory('', false)) {
293 $this->errorstring
='cannotcreatetempdir';
294 return COMPONENT_ERROR
;
296 /// Download zip file and save it to temp
297 if ($this->zippath
) {
298 $source = $this->sourcebase
.'/'.$this->zippath
.'/'.$this->zipfilename
;
300 $source = $this->sourcebase
.'/'.$this->zipfilename
;
303 $zipfile= $CFG->tempdir
.'/'.$this->zipfilename
;
305 $contents = download_file_content($source, null, null, true);
306 if ($contents->results
&& (int) $contents->status
=== 200) {
307 if ($file = fopen($zipfile, 'w')) {
308 if (!fwrite($file, $contents->results
)) {
310 $this->errorstring
='cannotsavezipfile';
311 return COMPONENT_ERROR
;
314 $this->errorstring
='cannotsavezipfile';
315 return COMPONENT_ERROR
;
319 $this->errorstring
='cannotdownloadzipfile';
320 return COMPONENT_ERROR
;
322 /// Calculate its md5
323 $new_md5 = md5($contents->results
);
324 /// Compare it with the remote md5 to check if we have the correct zip file
325 if (!$remote_md5 = $this->get_component_md5()) {
326 return COMPONENT_ERROR
;
328 if ($new_md5 != $remote_md5) {
329 $this->errorstring
='downloadedfilecheckfailed';
330 return COMPONENT_ERROR
;
333 // Move current revision to a safe place.
334 $destinationdir = $CFG->dataroot
. '/' . $this->destpath
;
335 $destinationcomponent = $destinationdir . '/' . $this->componentname
;
336 $destinationcomponentold = $destinationcomponent . '_old';
337 @remove_dir
($destinationcomponentold); // Deleting a possible old version.
339 // Moving to a safe place.
340 @rename
($destinationcomponent, $destinationcomponentold);
342 // Unzip new version.
343 $packer = get_file_packer('application/zip');
344 $unzipsuccess = $packer->extract_to_pathname($zipfile, $destinationdir, null, null, true);
345 if (!$unzipsuccess) {
346 @remove_dir
($destinationcomponent);
347 @rename
($destinationcomponentold, $destinationcomponent);
348 $this->errorstring
= 'cannotunzipfile';
349 return COMPONENT_ERROR
;
352 // Delete old component version.
353 @remove_dir
($destinationcomponentold);
356 if ($file = fopen($destinationcomponent.'/'.$this->componentname
.'.md5', 'w')) {
357 if (!fwrite($file, $new_md5)) {
359 $this->errorstring
='cannotsavemd5file';
360 return COMPONENT_ERROR
;
363 $this->errorstring
='cannotsavemd5file';
364 return COMPONENT_ERROR
;
367 /// Delete temp zip file
370 return COMPONENT_INSTALLED
;
374 * This function will detect if remote component needs to be installed
375 * because it's different from the local one
377 * @uses COMPONENT_ERROR
378 * @uses COMPONENT_UPTODATE
379 * @uses COMPONENT_NEEDUPDATE
380 * @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
382 function need_upgrade() {
384 /// Check requisites are passed
385 if (!$this->requisitesok
) {
386 return COMPONENT_ERROR
;
389 $local_md5 = $this->get_local_md5();
391 if (!$remote_md5 = $this->get_component_md5()) {
392 return COMPONENT_ERROR
;
395 if ($local_md5 == $remote_md5) {
396 return COMPONENT_UPTODATE
;
398 return COMPONENT_NEEDUPDATE
;
403 * This function will change the zip file to install on the fly
404 * to allow the class to process different components of the
405 * same md5 file without intantiating more objects.
407 * @param string $newzipfilename New zip filename to process
408 * @return boolean true/false
410 function change_zip_file($newzipfilename) {
412 $this->zipfilename
= $newzipfilename;
413 return $this->check_requisites();
417 * This function will get the local md5 value of the installed
421 * @return bool|string md5 of the local component (false on error)
423 function get_local_md5() {
426 /// Check requisites are passed
427 if (!$this->requisitesok
) {
431 $return_value = 'needtobeinstalled'; /// Fake value to force new installation
433 /// Calculate source to read
434 $source = $CFG->dataroot
.'/'.$this->destpath
.'/'.$this->componentname
.'/'.$this->componentname
.'.md5';
435 /// Read md5 value stored (if exists)
436 if (file_exists($source)) {
437 if ($temp = file_get_contents($source)) {
438 $return_value = $temp;
441 return $return_value;
445 * This function will download the specified md5 file, looking for the
446 * current componentname, returning its md5 field and storing extramd5info
447 * if present. Also it caches results to cachedmd5components for better
448 * performance in the same request.
450 * @return mixed md5 present in server (or false if error)
452 function get_component_md5() {
454 /// Check requisites are passed
455 if (!$this->requisitesok
) {
458 /// Get all components of md5 file
459 if (!$comp_arr = $this->get_all_components_md5()) {
460 if (empty($this->errorstring
)) {
461 $this->errorstring
='cannotdownloadcomponents';
465 /// Search for the componentname component
466 if (empty($comp_arr[$this->componentname
]) ||
!$component = $comp_arr[$this->componentname
]) {
467 $this->errorstring
='cannotfindcomponent';
470 /// Check we have a valid md5
471 if (empty($component[1]) ||
strlen($component[1]) != 32) {
472 $this->errorstring
='invalidmd5';
475 /// Set the extramd5info field
476 if (!empty($component[2])) {
477 $this->extramd5info
= $component[2];
479 return $component[1];
483 * This function allows you to retrieve the complete array of components found in
486 * @return bool|array array of components in md5 file or false if error
488 function get_all_components_md5() {
490 /// Check requisites are passed
491 if (!$this->requisitesok
) {
495 /// Initialize components array
498 /// Define and retrieve the full md5 file
499 if ($this->zippath
) {
500 $source = $this->sourcebase
.'/'.$this->zippath
.'/'.$this->md5filename
;
502 $source = $this->sourcebase
.'/'.$this->md5filename
;
505 /// Check if we have downloaded the md5 file before (per request cache)
506 if (!empty($this->cachedmd5components
[$source])) {
507 $comp_arr = $this->cachedmd5components
[$source];
509 /// Not downloaded, let's do it now
510 $availablecomponents = array();
512 $contents = download_file_content($source, null, null, true);
513 if ($contents->results
&& (int) $contents->status
=== 200) {
514 /// Split text into lines
515 $lines = preg_split('/\r?\n/', $contents->results
);
516 /// Each line will be one component
517 foreach($lines as $line) {
518 $availablecomponents[] = explode(',', $line);
520 /// If no components have been found, return error
521 if (empty($availablecomponents)) {
522 $this->errorstring
='cannotdownloadcomponents';
525 /// Build an associative array of components for easily search
526 /// applying trim to avoid linefeeds and other...
528 foreach ($availablecomponents as $component) {
529 /// Avoid sometimes empty lines
530 if (empty($component[0])) {
533 $component[0]=trim($component[0]);
534 if (!empty($component[1])) {
535 $component[1]=trim($component[1]);
537 if (!empty($component[2])) {
538 $component[2]=trim($component[2]);
540 $comp_arr[$component[0]] = $component;
543 $this->cachedmd5components
[$source] = $comp_arr;
546 $this->errorstring
='remotedownloaderror';
550 /// If there is no commponents or erros found, error
551 if (!empty($this->errorstring
)) {
554 } else if (empty($comp_arr)) {
555 $this->errorstring
='cannotdownloadcomponents';
562 * This function returns the errorstring
564 * @return string the error string
566 function get_error() {
567 return $this->errorstring
;
570 /** This function returns the extramd5 field (optional in md5 file)
572 * @return string the extramd5 field
574 function get_extra_md5_field() {
575 return $this->extramd5info
;
578 } /// End of component_installer class
582 * Language packs installer
584 * This class wraps the functionality provided by {@link component_installer}
585 * and adds support for installing a set of language packs.
587 * Given an array of required language packs, this class fetches them all
588 * and installs them. It detects eventual dependencies and installs
589 * all parent languages, too.
591 * @copyright 2011 David Mudrak <david@moodle.com>
592 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
594 class lang_installer
{
596 /** lang pack was successfully downloaded and deployed */
597 const RESULT_INSTALLED
= 'installed';
598 /** lang pack was up-to-date so no download was needed */
599 const RESULT_UPTODATE
= 'uptodate';
600 /** there was a problem with downloading the lang pack */
601 const RESULT_DOWNLOADERROR
= 'downloaderror';
603 /** @var array of languages to install */
604 protected $queue = array();
605 /** @var string the code of language being currently installed */
607 /** @var array of languages already installed by this instance */
608 protected $done = array();
609 /** @var string this Moodle major version */
613 * Prepare the installer
615 * @param string|array $langcode a code of the language to install
617 public function __construct($langcode = '') {
620 $this->set_queue($langcode);
621 $this->version
= moodle_major_version(true);
623 if (!empty($CFG->langotherroot
) and $CFG->langotherroot
!== $CFG->dataroot
. '/lang') {
624 debugging('The in-built language pack installer does not support alternative location ' .
625 'of languages root directory. You are supposed to install and update your language '.
626 'packs on your own.');
631 * Sets the queue of language packs to be installed
633 * @param string|array $langcodes language code like 'cs' or a list of them
635 public function set_queue($langcodes) {
636 if (is_array($langcodes)) {
637 $this->queue
= $langcodes;
638 } else if (!empty($langcodes)) {
639 $this->queue
= array($langcodes);
646 * This method calls {@link self::install_language_pack} for every language in the
647 * queue. If a dependency is detected, the parent language is added to the queue.
649 * @return array results, array of self::RESULT_xxx constants indexed by language code
651 public function run() {
655 while ($this->current
= array_shift($this->queue
)) {
657 if ($this->was_processed($this->current
)) {
658 // do not repeat yourself
662 if ($this->current
=== 'en') {
663 $this->mark_processed($this->current
);
667 $results[$this->current
] = $this->install_language_pack($this->current
);
669 if (in_array($results[$this->current
], array(self
::RESULT_INSTALLED
, self
::RESULT_UPTODATE
))) {
670 if ($parentlang = $this->get_parent_language($this->current
)) {
671 if (!$this->is_queued($parentlang) and !$this->was_processed($parentlang)) {
672 $this->add_to_queue($parentlang);
677 $this->mark_processed($this->current
);
684 * Returns the URL where a given language pack can be downloaded
686 * Alternatively, if the parameter is empty, returns URL of the page with the
687 * list of all available language packs.
689 * @param string $langcode language code like 'cs' or empty for unknown
692 public function lang_pack_url($langcode = '') {
694 if (empty($langcode)) {
695 return 'https://download.moodle.org/langpack/'.$this->version
.'/';
697 return 'https://download.moodle.org/download.php/langpack/'.$this->version
.'/'.$langcode.'.zip';
702 * Returns the list of available language packs from download.moodle.org
704 * @return array|bool false if can not download
706 public function get_remote_list_of_languages() {
707 $source = 'https://download.moodle.org/langpack/' . $this->version
. '/languages.md5';
708 $availablelangs = array();
710 $contents = download_file_content($source, null, null, true);
711 if ($contents->results
&& (int) $contents->status
=== 200) {
712 $alllines = explode("\n", $contents->results
);
713 foreach($alllines as $line) {
715 $availablelangs[] = explode(',', $line);
718 return $availablelangs;
725 // Internal implementation /////////////////////////////////////////////////
728 * Adds a language pack (or a list of them) to the queue
730 * @param string|array $langcodes code of the language to install or a list of them
732 protected function add_to_queue($langcodes) {
733 if (is_array($langcodes)) {
734 $this->queue
= array_merge($this->queue
, $langcodes);
735 } else if (!empty($langcodes)) {
736 $this->queue
[] = $langcodes;
741 * Checks if the given language is queued or if the queue is empty
743 * @example $installer->is_queued('es'); // is Spanish going to be installed?
744 * @example $installer->is_queued(); // is there a language queued?
746 * @param string $langcode language code or empty string for "any"
749 protected function is_queued($langcode = '') {
751 if (empty($langcode)) {
752 return !empty($this->queue
);
755 return in_array($langcode, $this->queue
);
760 * Checks if the given language has already been processed by this instance
762 * @see self::mark_processed()
763 * @param string $langcode
766 protected function was_processed($langcode) {
767 return isset($this->done
[$langcode]);
771 * Mark the given language pack as processed
773 * @see self::was_processed()
774 * @param string $langcode
776 protected function mark_processed($langcode) {
777 $this->done
[$langcode] = 1;
781 * Returns a parent language of the given installed language
783 * @param string $langcode
784 * @return string parent language's code
786 protected function get_parent_language($langcode) {
787 return get_parent_language($langcode);
791 * Perform the actual language pack installation
793 * @uses component_installer
794 * @param string $langcode
795 * @return int return status
797 protected function install_language_pack($langcode) {
799 // initialise new component installer to process this language
800 $installer = new component_installer('https://download.moodle.org', 'download.php/direct/langpack/' . $this->version
,
801 $langcode . '.zip', 'languages.md5', 'lang');
803 if (!$installer->requisitesok
) {
804 throw new lang_installer_exception('installer_requisites_check_failed');
807 $status = $installer->install();
809 if ($status == COMPONENT_ERROR
) {
810 if ($installer->get_error() === 'remotedownloaderror') {
811 return self
::RESULT_DOWNLOADERROR
;
813 throw new lang_installer_exception($installer->get_error(), $langcode);
816 } else if ($status == COMPONENT_UPTODATE
) {
817 return self
::RESULT_UPTODATE
;
819 } else if ($status == COMPONENT_INSTALLED
) {
820 return self
::RESULT_INSTALLED
;
823 throw new lang_installer_exception('unexpected_installer_result', $status);
830 * Exception thrown by {@link lang_installer}
832 * @copyright 2011 David Mudrak <david@moodle.com>
833 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
835 class lang_installer_exception
extends moodle_exception
{
837 public function __construct($errorcode, $debuginfo = null) {
838 parent
::__construct($errorcode, 'error', '', null, $debuginfo);