1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
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.
9//
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.
14//
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/>.
17
18/**
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.
22 *
23 * It has been developed harcoding some important limits that are
24 * explained below:
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).
50 *    - If different:
51 *        - Download the newer .zip file from source.
52 *        - Calculate its md5 (3).
53 *        - Compare (1) and (3).
54 *        - If equal:
55 *            - Delete old directory.
56 *            - Uunzip the newer .zip file.
57 *            - Create the new local .md5 file.
58 *            - Delete the .zip file.
59 *        - If different:
60 *            - ERROR. Old package won't be modified. We shouldn't
61 *              reach here ever.
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.
64 *
65 * General Usage:
66 *
67 * To install one component:
68 * <code>
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)
73 *         switch ($status) {
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 *                     print_error($cd->get_error(), 'error', '', $a);
80 *                 } else {
81 *                     print_error($cd->get_error(), 'error');
82 *                 }
83 *                 break;
84 *             case COMPONENT_UPTODATE:
85 *                 //Print error string or whatever you want to do
86 *                 break;
87 *             case COMPONENT_INSTALLED:
88 *                 //Print/do whatever you want
89 *                 break;
90 *             default:
91 *                 //We shouldn't reach this point
92 *         }
93 *     } else {
94 *         //We shouldn't reach this point
95 *     }
96 * </code>
97 *
98 * To switch of component (maintaining the rest of settings):
99 * <code>
100 *     $status = $cd->change_zip_file('en.zip'); //returns boolean false on error
101 * </code>
102 *
103 * To retrieve all the components in one remote md5 file
104 * <code>
105 *     $components = $cd->get_all_components_md5();  //returns boolean false on error, array instead
106 * </code>
107 *
108 * To check if current component needs to be updated
109 * <code>
110 *     $status = $cd->need_upgrade();  //returns COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
111 * </code>
112 *
113 * To get the 3rd field of the md5 file (optional)
114 * <code>
115 *     $field = $cd->get_extra_md5_field();  //returns string (empty if not exists)
116 * </code>
117 *
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.
120 *
121 * That's all!
122 *
123 * @package   core
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
126 */
127
128defined('MOODLE_INTERNAL') || die();
129
130 /**
131  * @global object $CFG
132  * @name $CFG
133  */
134global $CFG;
135require_once($CFG->libdir.'/filelib.php');
136
137// Some needed constants
138define('COMPONENT_ERROR',           0);
139define('COMPONENT_UPTODATE',        1);
140define('COMPONENT_NEEDUPDATE',      2);
141define('COMPONENT_INSTALLED',       3);
142
143/**
144 * This class is used to check, download and install items from
145 * download.moodle.org to the moodledata directory.
146 *
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.
151 *
152 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
153 * @package moodlecore
154 */
155class component_installer {
156    /**
157     * @var string
158     */
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.
173    /**
174     * @var array
175     */
176    var $cachedmd5components; /// Array of cached components to avoid to
177                              /// download the same md5 file more than once per request.
178
179    /**
180     * Standard constructor of the class. It will initialize all attributes.
181     * without performing any check at all.
182     *
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
188     *               than zipfilename)
189     * @param string $destpath Relative path (from moodledata) where the .zip file will
190     *               be expanded (default='' = moodledataitself)
191     * @return object
192     */
193    public function __construct($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
194
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();
205
206        $this->check_requisites();
207    }
208
209    /**
210     * Old syntax of class constructor. Deprecated in PHP7.
211     *
212     * @deprecated since Moodle 3.1
213     */
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);
217    }
218
219    /**
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.
223     *
224     * @global object
225     * @return boolean true/false (plus detailed error in errorstring)
226     */
227    function check_requisites() {
228        global $CFG;
229
230        $this->requisitesok = false;
231
232    /// Check that everything we need is present
233        if (empty($this->sourcebase) || empty($this->zipfilename)) {
234            $this->errorstring='missingrequiredfield';
235            return false;
236        }
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';
240            return false;
241        }
242    /// Check the zip file is a correct one (by extension)
243        if (stripos($this->zipfilename, '.zip') === false) {
244            $this->errorstring='wrongzipfilename';
245            return false;
246        }
247    /// Check that exists under dataroot
248        if (!empty($this->destpath)) {
249            if (!file_exists($CFG->dataroot.'/'.$this->destpath)) {
250                $this->errorstring='wrongdestpath';
251                return false;
252            }
253        }
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';
260        }
261    /// Set the requisites passed flag
262        $this->requisitesok = true;
263        return true;
264    }
265
266    /**
267     * This function will perform the full installation if needed, i.e.
268     * compare md5 values, download, unzip, install and regenerate
269     * local md5 file
270     *
271     * @uses COMPONENT_ERROR
272     * @uses COMPONENT_UPTODATE
273     * @uses COMPONENT_ERROR
274     * @uses COMPONENT_INSTALLED
275     * @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)
276     */
277    public function install() {
278        global $CFG;
279
280    /// Check requisites are passed
281        if (!$this->requisitesok) {
282            return COMPONENT_ERROR;
283        }
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;
290        }
291    /// Create temp directory if necesary
292        if (!make_temp_directory('', false)) {
293             $this->errorstring='cannotcreatetempdir';
294             return COMPONENT_ERROR;
295        }
296    /// Download zip file and save it to temp
297        if ($this->zippath) {
298            $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;
299        } else {
300            $source = $this->sourcebase.'/'.$this->zipfilename;
301        }
302
303        $zipfile= $CFG->tempdir.'/'.$this->zipfilename;
304
305        if($contents = download_file_content($source)) {
306            if ($file = fopen($zipfile, 'w')) {
307                if (!fwrite($file, $contents)) {
308                    fclose($file);
309                    $this->errorstring='cannotsavezipfile';
310                    return COMPONENT_ERROR;
311                }
312            } else {
313                $this->errorstring='cannotsavezipfile';
314                return COMPONENT_ERROR;
315            }
316            fclose($file);
317        } else {
318            $this->errorstring='cannotdownloadzipfile';
319            return COMPONENT_ERROR;
320        }
321    /// Calculate its md5
322        $new_md5 = md5($contents);
323    /// Compare it with the remote md5 to check if we have the correct zip file
324        if (!$remote_md5 = $this->get_component_md5()) {
325            return COMPONENT_ERROR;
326        }
327        if ($new_md5 != $remote_md5) {
328            $this->errorstring='downloadedfilecheckfailed';
329            return COMPONENT_ERROR;
330        }
331
332        // Move current revision to a safe place.
333        $destinationdir = $CFG->dataroot . '/' . $this->destpath;
334        $destinationcomponent = $destinationdir . '/' . $this->componentname;
335        $destinationcomponentold = $destinationcomponent . '_old';
336        @remove_dir($destinationcomponentold);     // Deleting a possible old version.
337
338        // Moving to a safe place.
339        @rename($destinationcomponent, $destinationcomponentold);
340
341        // Unzip new version.
342        $packer = get_file_packer('application/zip');
343        $unzipsuccess = $packer->extract_to_pathname($zipfile, $destinationdir, null, null, true);
344        if (!$unzipsuccess) {
345            @remove_dir($destinationcomponent);
346            @rename($destinationcomponentold, $destinationcomponent);
347            $this->errorstring = 'cannotunzipfile';
348            return COMPONENT_ERROR;
349        }
350
351        // Delete old component version.
352        @remove_dir($destinationcomponentold);
353
354        // Create local md5.
355        if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {
356            if (!fwrite($file, $new_md5)) {
357                fclose($file);
358                $this->errorstring='cannotsavemd5file';
359                return COMPONENT_ERROR;
360            }
361        } else  {
362            $this->errorstring='cannotsavemd5file';
363            return COMPONENT_ERROR;
364        }
365        fclose($file);
366    /// Delete temp zip file
367        @unlink($zipfile);
368
369        return COMPONENT_INSTALLED;
370    }
371
372    /**
373     * This function will detect if remote component needs to be installed
374     * because it's different from the local one
375     *
376     * @uses COMPONENT_ERROR
377     * @uses COMPONENT_UPTODATE
378     * @uses COMPONENT_NEEDUPDATE
379     * @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
380     */
381    function need_upgrade() {
382
383    /// Check requisites are passed
384        if (!$this->requisitesok) {
385            return COMPONENT_ERROR;
386        }
387    /// Get local md5
388        $local_md5 = $this->get_local_md5();
389    /// Get remote md5
390        if (!$remote_md5 = $this->get_component_md5()) {
391            return COMPONENT_ERROR;
392        }
393    /// Return result
394       if ($local_md5 == $remote_md5) {
395           return COMPONENT_UPTODATE;
396       } else {
397           return COMPONENT_NEEDUPDATE;
398       }
399    }
400
401    /**
402     * This function will change the zip file to install on the fly
403     * to allow the class to process different components of the
404     * same md5 file without intantiating more objects.
405     *
406     * @param string $newzipfilename New zip filename to process
407     * @return boolean true/false
408     */
409    function change_zip_file($newzipfilename) {
410
411        $this->zipfilename = $newzipfilename;
412        return $this->check_requisites();
413    }
414
415    /**
416     * This function will get the local md5 value of the installed
417     * component.
418     *
419     * @global object
420     * @return bool|string md5 of the local component (false on error)
421     */
422    function get_local_md5() {
423        global $CFG;
424
425    /// Check requisites are passed
426        if (!$this->requisitesok) {
427            return false;
428        }
429
430        $return_value = 'needtobeinstalled';   /// Fake value to force new installation
431
432    /// Calculate source to read
433       $source = $CFG->dataroot.'/'.$this->destpath.'/'.$this->componentname.'/'.$this->componentname.'.md5';
434    /// Read md5 value stored (if exists)
435       if (file_exists($source)) {
436           if ($temp = file_get_contents($source)) {
437               $return_value = $temp;
438           }
439        }
440        return $return_value;
441    }
442
443    /**
444     * This function will download the specified md5 file, looking for the
445     * current componentname, returning its md5 field and storing extramd5info
446     * if present. Also it caches results to cachedmd5components for better
447     * performance in the same request.
448     *
449     * @return mixed md5 present in server (or false if error)
450     */
451    function get_component_md5() {
452
453    /// Check requisites are passed
454        if (!$this->requisitesok) {
455            return false;
456        }
457    /// Get all components of md5 file
458        if (!$comp_arr = $this->get_all_components_md5()) {
459            if (empty($this->errorstring)) {
460                $this->errorstring='cannotdownloadcomponents';
461            }
462            return false;
463        }
464    /// Search for the componentname component
465        if (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) {
466             $this->errorstring='cannotfindcomponent';
467             return false;
468        }
469    /// Check we have a valid md5
470        if (empty($component[1]) || strlen($component[1]) != 32) {
471            $this->errorstring='invalidmd5';
472            return false;
473        }
474    /// Set the extramd5info field
475        if (!empty($component[2])) {
476            $this->extramd5info = $component[2];
477        }
478        return $component[1];
479    }
480
481    /**
482     * This function allows you to retrieve the complete array of components found in
483     * the md5filename
484     *
485     * @return bool|array array of components in md5 file or false if error
486     */
487    function get_all_components_md5() {
488
489    /// Check requisites are passed
490        if (!$this->requisitesok) {
491            return false;
492        }
493
494    /// Initialize components array
495        $comp_arr = array();
496
497    /// Define and retrieve the full md5 file
498        if ($this->zippath) {
499            $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
500        } else {
501            $source = $this->sourcebase.'/'.$this->md5filename;
502        }
503
504    /// Check if we have downloaded the md5 file before (per request cache)
505        if (!empty($this->cachedmd5components[$source])) {
506            $comp_arr = $this->cachedmd5components[$source];
507        } else {
508        /// Not downloaded, let's do it now
509            $availablecomponents = array();
510
511            if ($contents = download_file_content($source)) {
512            /// Split text into lines
513                $lines=preg_split('/\r?\n/',$contents);
514            /// Each line will be one component
515                foreach($lines as $line) {
516                    $availablecomponents[] = explode(',', $line);
517                }
518            /// If no components have been found, return error
519                if (empty($availablecomponents)) {
520                    $this->errorstring='cannotdownloadcomponents';
521                    return false;
522                }
523            /// Build an associative array of components for easily search
524            /// applying trim to avoid linefeeds and other...
525                $comp_arr = array();
526                foreach ($availablecomponents as $component) {
527                /// Avoid sometimes empty lines
528                    if (empty($component[0])) {
529                        continue;
530                    }
531                    $component[0]=trim($component[0]);
532                    if (!empty($component[1])) {
533                        $component[1]=trim($component[1]);
534                    }
535                    if (!empty($component[2])) {
536                        $component[2]=trim($component[2]);
537                    }
538                    $comp_arr[$component[0]] = $component;
539                }
540            /// Cache components
541                $this->cachedmd5components[$source] = $comp_arr;
542            } else {
543            /// Return error
544                $this->errorstring='remotedownloaderror';
545                return false;
546            }
547        }
548    /// If there is no commponents or erros found, error
549        if (!empty($this->errorstring)) {
550             return false;
551
552        } else if (empty($comp_arr)) {
553             $this->errorstring='cannotdownloadcomponents';
554             return false;
555        }
556        return $comp_arr;
557    }
558
559    /**
560     * This function returns the errorstring
561     *
562     * @return string the error string
563     */
564    function get_error() {
565        return $this->errorstring;
566    }
567
568    /** This function returns the extramd5 field (optional in md5 file)
569     *
570     * @return string the extramd5 field
571     */
572    function get_extra_md5_field() {
573        return $this->extramd5info;
574    }
575
576} /// End of component_installer class
577
578
579/**
580 * Language packs installer
581 *
582 * This class wraps the functionality provided by {@link component_installer}
583 * and adds support for installing a set of language packs.
584 *
585 * Given an array of required language packs, this class fetches them all
586 * and installs them. It detects eventual dependencies and installs
587 * all parent languages, too.
588 *
589 * @copyright 2011 David Mudrak <david@moodle.com>
590 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
591 */
592class lang_installer {
593
594    /** lang pack was successfully downloaded and deployed */
595    const RESULT_INSTALLED      = 'installed';
596    /** lang pack was up-to-date so no download was needed */
597    const RESULT_UPTODATE       = 'uptodate';
598    /** there was a problem with downloading the lang pack */
599    const RESULT_DOWNLOADERROR  = 'downloaderror';
600
601    /** @var array of languages to install */
602    protected $queue = array();
603    /** @var string the code of language being currently installed */
604    protected $current;
605    /** @var array of languages already installed by this instance */
606    protected $done = array();
607    /** @var string this Moodle major version */
608    protected $version;
609
610    /**
611     * Prepare the installer
612     *
613     * @param string|array $langcode a code of the language to install
614     */
615    public function __construct($langcode = '') {
616        global $CFG;
617
618        $this->set_queue($langcode);
619        $this->version = moodle_major_version(true);
620
621        if (!empty($CFG->langotherroot) and $CFG->langotherroot !== $CFG->dataroot . '/lang') {
622            debugging('The in-built language pack installer does not support alternative location ' .
623                'of languages root directory. You are supposed to install and update your language '.
624                'packs on your own.');
625        }
626    }
627
628    /**
629     * Sets the queue of language packs to be installed
630     *
631     * @param string|array $langcodes language code like 'cs' or a list of them
632     */
633    public function set_queue($langcodes) {
634        if (is_array($langcodes)) {
635            $this->queue = $langcodes;
636        } else if (!empty($langcodes)) {
637            $this->queue = array($langcodes);
638        }
639    }
640
641    /**
642     * Runs the installer
643     *
644     * This method calls {@link self::install_language_pack} for every language in the
645     * queue. If a dependency is detected, the parent language is added to the queue.
646     *
647     * @return array results, array of self::RESULT_xxx constants indexed by language code
648     */
649    public function run() {
650
651        $results = array();
652
653        while ($this->current = array_shift($this->queue)) {
654
655            if ($this->was_processed($this->current)) {
656                // do not repeat yourself
657                continue;
658            }
659
660            if ($this->current === 'en') {
661                $this->mark_processed($this->current);
662                continue;
663            }
664
665            $results[$this->current] = $this->install_language_pack($this->current);
666
667            if (in_array($results[$this->current], array(self::RESULT_INSTALLED, self::RESULT_UPTODATE))) {
668                if ($parentlang = $this->get_parent_language($this->current)) {
669                    if (!$this->is_queued($parentlang) and !$this->was_processed($parentlang)) {
670                        $this->add_to_queue($parentlang);
671                    }
672                }
673            }
674
675            $this->mark_processed($this->current);
676        }
677
678        return $results;
679    }
680
681    /**
682     * Returns the URL where a given language pack can be downloaded
683     *
684     * Alternatively, if the parameter is empty, returns URL of the page with the
685     * list of all available language packs.
686     *
687     * @param string $langcode language code like 'cs' or empty for unknown
688     * @return string URL
689     */
690    public function lang_pack_url($langcode = '') {
691
692        if (empty($langcode)) {
693            return 'https://download.moodle.org/langpack/'.$this->version.'/';
694        } else {
695            return 'https://download.moodle.org/download.php/langpack/'.$this->version.'/'.$langcode.'.zip';
696        }
697    }
698
699    /**
700     * Returns the list of available language packs from download.moodle.org
701     *
702     * @return array|bool false if can not download
703     */
704    public function get_remote_list_of_languages() {
705        $source = 'https://download.moodle.org/langpack/' . $this->version . '/languages.md5';
706        $availablelangs = array();
707
708        if ($content = download_file_content($source)) {
709            $alllines = explode("\n", $content);
710            foreach($alllines as $line) {
711                if (!empty($line)){
712                    $availablelangs[] = explode(',', $line);
713                }
714            }
715            return $availablelangs;
716
717        } else {
718            return false;
719        }
720    }
721
722    // Internal implementation /////////////////////////////////////////////////
723
724    /**
725     * Adds a language pack (or a list of them) to the queue
726     *
727     * @param string|array $langcodes code of the language to install or a list of them
728     */
729    protected function add_to_queue($langcodes) {
730        if (is_array($langcodes)) {
731            $this->queue = array_merge($this->queue, $langcodes);
732        } else if (!empty($langcodes)) {
733            $this->queue[] = $langcodes;
734        }
735    }
736
737    /**
738     * Checks if the given language is queued or if the queue is empty
739     *
740     * @example $installer->is_queued('es');    // is Spanish going to be installed?
741     * @example $installer->is_queued();        // is there a language queued?
742     *
743     * @param string $langcode language code or empty string for "any"
744     * @return boolean
745     */
746    protected function is_queued($langcode = '') {
747
748        if (empty($langcode)) {
749            return !empty($this->queue);
750
751        } else {
752            return in_array($langcode, $this->queue);
753        }
754    }
755
756    /**
757     * Checks if the given language has already been processed by this instance
758     *
759     * @see self::mark_processed()
760     * @param string $langcode
761     * @return boolean
762     */
763    protected function was_processed($langcode) {
764        return isset($this->done[$langcode]);
765    }
766
767    /**
768     * Mark the given language pack as processed
769     *
770     * @see self::was_processed()
771     * @param string $langcode
772     */
773    protected function mark_processed($langcode) {
774        $this->done[$langcode] = 1;
775    }
776
777    /**
778     * Returns a parent language of the given installed language
779     *
780     * @param string $langcode
781     * @return string parent language's code
782     */
783    protected function get_parent_language($langcode) {
784        return get_parent_language($langcode);
785    }
786
787    /**
788     * Perform the actual language pack installation
789     *
790     * @uses component_installer
791     * @param string $langcode
792     * @return int return status
793     */
794    protected function install_language_pack($langcode) {
795
796        // initialise new component installer to process this language
797        $installer = new component_installer('https://download.moodle.org', 'download.php/direct/langpack/' . $this->version,
798            $langcode . '.zip', 'languages.md5', 'lang');
799
800        if (!$installer->requisitesok) {
801            throw new lang_installer_exception('installer_requisites_check_failed');
802        }
803
804        $status = $installer->install();
805
806        if ($status == COMPONENT_ERROR) {
807            if ($installer->get_error() === 'remotedownloaderror') {
808                return self::RESULT_DOWNLOADERROR;
809            } else {
810                throw new lang_installer_exception($installer->get_error(), $langcode);
811            }
812
813        } else if ($status == COMPONENT_UPTODATE) {
814            return self::RESULT_UPTODATE;
815
816        } else if ($status == COMPONENT_INSTALLED) {
817            return self::RESULT_INSTALLED;
818
819        } else {
820            throw new lang_installer_exception('unexpected_installer_result', $status);
821        }
822    }
823}
824
825
826/**
827 * Exception thrown by {@link lang_installer}
828 *
829 * @copyright 2011 David Mudrak <david@moodle.com>
830 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
831 */
832class lang_installer_exception extends moodle_exception {
833
834    public function __construct($errorcode, $debuginfo = null) {
835        parent::__construct($errorcode, 'error', '', null, $debuginfo);
836    }
837}
838