Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 *                     throw new \moodle_exception($cd->get_error(), 'error', '', $a);
80
 *                 } else {
81
 *                     throw new \moodle_exception($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
 
128
defined('MOODLE_INTERNAL') || die();
129
 
130
 /**
131
  * @global object $CFG
132
  * @name $CFG
133
  */
134
global $CFG;
135
require_once($CFG->libdir.'/filelib.php');
136
 
137
// Some needed constants
138
define('COMPONENT_ERROR',           0);
139
define('COMPONENT_UPTODATE',        1);
140
define('COMPONENT_NEEDUPDATE',      2);
141
define('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
 */
155
class 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
        $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)) {
309
                    fclose($file);
310
                    $this->errorstring='cannotsavezipfile';
311
                    return COMPONENT_ERROR;
312
                }
313
            } else {
314
                $this->errorstring='cannotsavezipfile';
315
                return COMPONENT_ERROR;
316
            }
317
            fclose($file);
318
        } else {
319
            $this->errorstring='cannotdownloadzipfile';
320
            return COMPONENT_ERROR;
321
        }
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;
327
        }
328
        if ($new_md5 != $remote_md5) {
329
            $this->errorstring='downloadedfilecheckfailed';
330
            return COMPONENT_ERROR;
331
        }
332
 
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.
338
 
339
        // Moving to a safe place.
340
        @rename($destinationcomponent, $destinationcomponentold);
341
 
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;
350
        }
351
 
352
        // Delete old component version.
353
        @remove_dir($destinationcomponentold);
354
 
355
        // Create local md5.
356
        if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {
357
            if (!fwrite($file, $new_md5)) {
358
                fclose($file);
359
                $this->errorstring='cannotsavemd5file';
360
                return COMPONENT_ERROR;
361
            }
362
        } else  {
363
            $this->errorstring='cannotsavemd5file';
364
            return COMPONENT_ERROR;
365
        }
366
        fclose($file);
367
    /// Delete temp zip file
368
        @unlink($zipfile);
369
 
370
        return COMPONENT_INSTALLED;
371
    }
372
 
373
    /**
374
     * This function will detect if remote component needs to be installed
375
     * because it's different from the local one
376
     *
377
     * @uses COMPONENT_ERROR
378
     * @uses COMPONENT_UPTODATE
379
     * @uses COMPONENT_NEEDUPDATE
380
     * @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
381
     */
382
    function need_upgrade() {
383
 
384
    /// Check requisites are passed
385
        if (!$this->requisitesok) {
386
            return COMPONENT_ERROR;
387
        }
388
    /// Get local md5
389
        $local_md5 = $this->get_local_md5();
390
    /// Get remote md5
391
        if (!$remote_md5 = $this->get_component_md5()) {
392
            return COMPONENT_ERROR;
393
        }
394
    /// Return result
395
       if ($local_md5 == $remote_md5) {
396
           return COMPONENT_UPTODATE;
397
       } else {
398
           return COMPONENT_NEEDUPDATE;
399
       }
400
    }
401
 
402
    /**
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.
406
     *
407
     * @param string $newzipfilename New zip filename to process
408
     * @return boolean true/false
409
     */
410
    function change_zip_file($newzipfilename) {
411
 
412
        $this->zipfilename = $newzipfilename;
413
        return $this->check_requisites();
414
    }
415
 
416
    /**
417
     * This function will get the local md5 value of the installed
418
     * component.
419
     *
420
     * @global object
421
     * @return bool|string md5 of the local component (false on error)
422
     */
423
    function get_local_md5() {
424
        global $CFG;
425
 
426
    /// Check requisites are passed
427
        if (!$this->requisitesok) {
428
            return false;
429
        }
430
 
431
        $return_value = 'needtobeinstalled';   /// Fake value to force new installation
432
 
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;
439
           }
440
        }
441
        return $return_value;
442
    }
443
 
444
    /**
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.
449
     *
450
     * @return mixed md5 present in server (or false if error)
451
     */
452
    function get_component_md5() {
453
 
454
    /// Check requisites are passed
455
        if (!$this->requisitesok) {
456
            return false;
457
        }
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';
462
            }
463
            return false;
464
        }
465
    /// Search for the componentname component
466
        if (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) {
467
             $this->errorstring='cannotfindcomponent';
468
             return false;
469
        }
470
    /// Check we have a valid md5
471
        if (empty($component[1]) || strlen($component[1]) != 32) {
472
            $this->errorstring='invalidmd5';
473
            return false;
474
        }
475
    /// Set the extramd5info field
476
        if (!empty($component[2])) {
477
            $this->extramd5info = $component[2];
478
        }
479
        return $component[1];
480
    }
481
 
482
    /**
483
     * This function allows you to retrieve the complete array of components found in
484
     * the md5filename
485
     *
486
     * @return bool|array array of components in md5 file or false if error
487
     */
488
    function get_all_components_md5() {
489
 
490
    /// Check requisites are passed
491
        if (!$this->requisitesok) {
492
            return false;
493
        }
494
 
495
    /// Initialize components array
496
        $comp_arr = array();
497
 
498
    /// Define and retrieve the full md5 file
499
        if ($this->zippath) {
500
            $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
501
        } else {
502
            $source = $this->sourcebase.'/'.$this->md5filename;
503
        }
504
 
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];
508
        } else {
509
        /// Not downloaded, let's do it now
510
            $availablecomponents = array();
511
 
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);
519
                }
520
            /// If no components have been found, return error
521
                if (empty($availablecomponents)) {
522
                    $this->errorstring='cannotdownloadcomponents';
523
                    return false;
524
                }
525
            /// Build an associative array of components for easily search
526
            /// applying trim to avoid linefeeds and other...
527
                $comp_arr = array();
528
                foreach ($availablecomponents as $component) {
529
                /// Avoid sometimes empty lines
530
                    if (empty($component[0])) {
531
                        continue;
532
                    }
533
                    $component[0]=trim($component[0]);
534
                    if (!empty($component[1])) {
535
                        $component[1]=trim($component[1]);
536
                    }
537
                    if (!empty($component[2])) {
538
                        $component[2]=trim($component[2]);
539
                    }
540
                    $comp_arr[$component[0]] = $component;
541
                }
542
            /// Cache components
543
                $this->cachedmd5components[$source] = $comp_arr;
544
            } else {
545
            /// Return error
546
                $this->errorstring='remotedownloaderror';
547
                return false;
548
            }
549
        }
550
    /// If there is no commponents or erros found, error
551
        if (!empty($this->errorstring)) {
552
             return false;
553
 
554
        } else if (empty($comp_arr)) {
555
             $this->errorstring='cannotdownloadcomponents';
556
             return false;
557
        }
558
        return $comp_arr;
559
    }
560
 
561
    /**
562
     * This function returns the errorstring
563
     *
564
     * @return string the error string
565
     */
566
    function get_error() {
567
        return $this->errorstring;
568
    }
569
 
570
    /** This function returns the extramd5 field (optional in md5 file)
571
     *
572
     * @return string the extramd5 field
573
     */
574
    function get_extra_md5_field() {
575
        return $this->extramd5info;
576
    }
577
 
578
} /// End of component_installer class
579
 
580
 
581
/**
582
 * Language packs installer
583
 *
584
 * This class wraps the functionality provided by {@link component_installer}
585
 * and adds support for installing a set of language packs.
586
 *
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.
590
 *
591
 * @copyright 2011 David Mudrak <david@moodle.com>
592
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
593
 */
594
class lang_installer {
595
 
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';
602
 
603
    /** @var array of languages to install */
604
    protected $queue = array();
605
    /** @var string the code of language being currently installed */
606
    protected $current;
607
    /** @var array of languages already installed by this instance */
608
    protected $done = array();
609
    /** @var string this Moodle major version */
610
    protected $version;
611
 
612
    /**
613
     * Prepare the installer
614
     *
615
     * @param string|array $langcode a code of the language to install
616
     */
617
    public function __construct($langcode = '') {
618
        global $CFG;
619
 
620
        $this->set_queue($langcode);
621
        $this->version = moodle_major_version(true);
622
 
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.');
627
        }
628
    }
629
 
630
    /**
631
     * Sets the queue of language packs to be installed
632
     *
633
     * @param string|array $langcodes language code like 'cs' or a list of them
634
     */
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);
640
        }
641
    }
642
 
643
    /**
644
     * Runs the installer
645
     *
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.
648
     *
649
     * @return array results, array of self::RESULT_xxx constants indexed by language code
650
     */
651
    public function run() {
652
 
653
        $results = array();
654
 
655
        while ($this->current = array_shift($this->queue)) {
656
 
657
            if ($this->was_processed($this->current)) {
658
                // do not repeat yourself
659
                continue;
660
            }
661
 
662
            if ($this->current === 'en') {
663
                $this->mark_processed($this->current);
664
                continue;
665
            }
666
 
667
            $results[$this->current] = $this->install_language_pack($this->current);
668
 
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);
673
                    }
674
                }
675
            }
676
 
677
            $this->mark_processed($this->current);
678
        }
679
 
680
        return $results;
681
    }
682
 
683
    /**
684
     * Returns the URL where a given language pack can be downloaded
685
     *
686
     * Alternatively, if the parameter is empty, returns URL of the page with the
687
     * list of all available language packs.
688
     *
689
     * @param string $langcode language code like 'cs' or empty for unknown
690
     * @return string URL
691
     */
692
    public function lang_pack_url($langcode = '') {
693
 
694
        if (empty($langcode)) {
695
            return 'https://download.moodle.org/langpack/'.$this->version.'/';
696
        } else {
697
            return 'https://download.moodle.org/download.php/langpack/'.$this->version.'/'.$langcode.'.zip';
698
        }
699
    }
700
 
701
    /**
702
     * Returns the list of available language packs from download.moodle.org
703
     *
704
     * @return array|bool false if can not download
705
     */
706
    public function get_remote_list_of_languages() {
707
        $source = 'https://download.moodle.org/langpack/' . $this->version . '/languages.md5';
708
        $availablelangs = array();
709
 
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) {
714
                if (!empty($line)){
715
                    $availablelangs[] = explode(',', $line);
716
                }
717
            }
718
            return $availablelangs;
719
 
720
        } else {
721
            return false;
722
        }
723
    }
724
 
725
    // Internal implementation /////////////////////////////////////////////////
726
 
727
    /**
728
     * Adds a language pack (or a list of them) to the queue
729
     *
730
     * @param string|array $langcodes code of the language to install or a list of them
731
     */
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;
737
        }
738
    }
739
 
740
    /**
741
     * Checks if the given language is queued or if the queue is empty
742
     *
743
     * @example $installer->is_queued('es');    // is Spanish going to be installed?
744
     * @example $installer->is_queued();        // is there a language queued?
745
     *
746
     * @param string $langcode language code or empty string for "any"
747
     * @return boolean
748
     */
749
    protected function is_queued($langcode = '') {
750
 
751
        if (empty($langcode)) {
752
            return !empty($this->queue);
753
 
754
        } else {
755
            return in_array($langcode, $this->queue);
756
        }
757
    }
758
 
759
    /**
760
     * Checks if the given language has already been processed by this instance
761
     *
762
     * @see self::mark_processed()
763
     * @param string $langcode
764
     * @return boolean
765
     */
766
    protected function was_processed($langcode) {
767
        return isset($this->done[$langcode]);
768
    }
769
 
770
    /**
771
     * Mark the given language pack as processed
772
     *
773
     * @see self::was_processed()
774
     * @param string $langcode
775
     */
776
    protected function mark_processed($langcode) {
777
        $this->done[$langcode] = 1;
778
    }
779
 
780
    /**
781
     * Returns a parent language of the given installed language
782
     *
783
     * @param string $langcode
784
     * @return string parent language's code
785
     */
786
    protected function get_parent_language($langcode) {
787
        return get_parent_language($langcode);
788
    }
789
 
790
    /**
791
     * Perform the actual language pack installation
792
     *
793
     * @uses component_installer
794
     * @param string $langcode
795
     * @return string return status
796
     */
797
    protected function install_language_pack($langcode) {
798
 
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');
802
 
803
        if (!$installer->requisitesok) {
804
            throw new lang_installer_exception('installer_requisites_check_failed');
805
        }
806
 
807
        $status = $installer->install();
808
 
809
        if ($status == COMPONENT_ERROR) {
810
            if ($installer->get_error() === 'remotedownloaderror') {
811
                return self::RESULT_DOWNLOADERROR;
812
            } else {
813
                throw new lang_installer_exception($installer->get_error(), $langcode);
814
            }
815
 
816
        } else if ($status == COMPONENT_UPTODATE) {
817
            return self::RESULT_UPTODATE;
818
 
819
        } else if ($status == COMPONENT_INSTALLED) {
820
            return self::RESULT_INSTALLED;
821
 
822
        } else {
823
            throw new lang_installer_exception('unexpected_installer_result', $status);
824
        }
825
    }
826
}
827
 
828
 
829
/**
830
 * Exception thrown by {@link lang_installer}
831
 *
832
 * @copyright 2011 David Mudrak <david@moodle.com>
833
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
834
 */
835
class lang_installer_exception extends moodle_exception {
836
 
837
    public function __construct($errorcode, $debuginfo = null) {
838
        parent::__construct($errorcode, 'error', '', null, $debuginfo);
839
    }
840
}