Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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  
 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          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   */
 592  class 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   */
 832  class lang_installer_exception extends moodle_exception {
 833  
 834      public function __construct($errorcode, $debuginfo = null) {
 835          parent::__construct($errorcode, 'error', '', null, $debuginfo);
 836      }
 837  }