Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   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 int 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  }