Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.9.x ended 9 May 2016 (12 months).
  • Bug fixes for security issues in 2.9.x ended 14 November 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • / -> mdeploy.php (source)
       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   * Moodle deployment utility
      20   *
      21   * This script looks after deploying new add-ons and available updates for them
      22   * to the local Moodle site. It can operate via both HTTP and CLI mode.
      23   * Moodle itself calls this utility via the HTTP mode when the admin is about to
      24   * install or update an add-on. You can use the CLI mode in your custom deployment
      25   * shell scripts.
      26   *
      27   * CLI usage example:
      28   *
      29   *  $ sudo -u apache php mdeploy.php --install \
      30   *                                   --package=https://moodle.org/plugins/download.php/...zip \
      31   *                                   --typeroot=/var/www/moodle/htdocs/blocks
      32   *                                   --name=loancalc
      33   *                                   --md5=...
      34   *
      35   *  $ sudo -u apache php mdeploy.php --upgrade \
      36   *                                   --package=https://moodle.org/plugins/download.php/...zip \
      37   *                                   --typeroot=/var/www/moodle/htdocs/blocks
      38   *                                   --name=loancalc
      39   *                                   --md5=...
      40   *
      41   * When called via HTTP, additional parameters returnurl, passfile and password must be
      42   * provided. Optional proxy configuration can be passed using parameters proxy, proxytype
      43   * and proxyuserpwd.
      44   *
      45   * Changes
      46   *
      47   * 1.1 - Added support to install a new plugin from the Moodle Plugins directory.
      48   * 1.0 - Initial version used in Moodle 2.4 to deploy available updates.
      49   *
      50   * @package     core
      51   * @subpackage  mdeploy
      52   * @version     1.1
      53   * @copyright   2012 David Mudrak <david@moodle.com>
      54   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      55   */
      56  
      57  if (defined('MOODLE_INTERNAL')) {
      58      die('This is a standalone utility that should not be included by any other Moodle code.');
      59  }
      60  
      61  // This stops immediately at the beginning of lib/setup.php.
      62  define('ABORT_AFTER_CONFIG', true);
      63  if (PHP_SAPI === 'cli') {
      64      // Called from the CLI - we need to set CLI_SCRIPT to ensure that appropriate CLI checks are made in setup.php.
      65      define('CLI_SCRIPT', true);
      66  }
      67  require(__DIR__ . '/config.php');
      68  
      69  // Exceptions //////////////////////////////////////////////////////////////////
      70  
      71  class invalid_coding_exception extends Exception {}
      72  class missing_option_exception extends Exception {}
      73  class invalid_option_exception extends Exception {}
      74  class unauthorized_access_exception extends Exception {}
      75  class download_file_exception extends Exception {}
      76  class backup_folder_exception extends Exception {}
      77  class zip_exception extends Exception {}
      78  class filesystem_exception extends Exception {}
      79  class checksum_exception extends Exception {}
      80  class invalid_setting_exception extends Exception {}
      81  
      82  
      83  // Various support classes /////////////////////////////////////////////////////
      84  
      85  /**
      86   * Base class implementing the singleton pattern using late static binding feature.
      87   *
      88   * @copyright 2012 David Mudrak <david@moodle.com>
      89   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      90   */
      91  abstract class singleton_pattern {
      92  
      93      /** @var array singleton_pattern instances */
      94      protected static $singletoninstances = array();
      95  
      96      /**
      97       * Factory method returning the singleton instance.
      98       *
      99       * Subclasses may want to override the {@link self::initialize()} method that is
     100       * called right after their instantiation.
     101       *
     102       * @return mixed the singleton instance
     103       */
     104      final public static function instance() {
     105          $class = get_called_class();
     106          if (!isset(static::$singletoninstances[$class])) {
     107              static::$singletoninstances[$class] = new static();
     108              static::$singletoninstances[$class]->initialize();
     109          }
     110          return static::$singletoninstances[$class];
     111      }
     112  
     113      /**
     114       * Optional post-instantiation code.
     115       */
     116      protected function initialize() {
     117          // Do nothing in this base class.
     118      }
     119  
     120      /**
     121       * Direct instantiation not allowed, use the factory method {@link instance()}
     122       */
     123      final protected function __construct() {
     124      }
     125  
     126      /**
     127       * Sorry, this is singleton.
     128       */
     129      final protected function __clone() {
     130      }
     131  }
     132  
     133  
     134  // User input handling /////////////////////////////////////////////////////////
     135  
     136  /**
     137   * Provides access to the script options.
     138   *
     139   * Implements the delegate pattern by dispatching the calls to appropriate
     140   * helper class (CLI or HTTP).
     141   *
     142   * @copyright 2012 David Mudrak <david@moodle.com>
     143   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     144   */
     145  class input_manager extends singleton_pattern {
     146  
     147      const TYPE_FILE         = 'file';   // File name
     148      const TYPE_FLAG         = 'flag';   // No value, just a flag (switch)
     149      const TYPE_INT          = 'int';    // Integer
     150      const TYPE_PATH         = 'path';   // Full path to a file or a directory
     151      const TYPE_RAW          = 'raw';    // Raw value, keep as is
     152      const TYPE_URL          = 'url';    // URL to a file
     153      const TYPE_PLUGIN       = 'plugin'; // Plugin name
     154      const TYPE_MD5          = 'md5';    // MD5 hash
     155  
     156      /** @var input_cli_provider|input_http_provider the provider of the input */
     157      protected $inputprovider = null;
     158  
     159      /**
     160       * Returns the value of an option passed to the script.
     161       *
     162       * If the caller passes just the $name, the requested argument is considered
     163       * required. The caller may specify the second argument which then
     164       * makes the argument optional with the given default value.
     165       *
     166       * If the type of the $name option is TYPE_FLAG (switch), this method returns
     167       * true if the flag has been passed or false if it was not. Specifying the
     168       * default value makes no sense in this case and leads to invalid coding exception.
     169       *
     170       * The array options are not supported.
     171       *
     172       * @example $filename = $input->get_option('f');
     173       * @example $filename = $input->get_option('filename');
     174       * @example if ($input->get_option('verbose')) { ... }
     175       * @param string $name
     176       * @return mixed
     177       */
     178      public function get_option($name, $default = 'provide_default_value_explicitly') {
     179  
     180          $this->validate_option_name($name);
     181  
     182          $info = $this->get_option_info($name);
     183  
     184          if ($info->type === input_manager::TYPE_FLAG) {
     185              return $this->inputprovider->has_option($name);
     186          }
     187  
     188          if (func_num_args() == 1) {
     189              return $this->get_required_option($name);
     190          } else {
     191              return $this->get_optional_option($name, $default);
     192          }
     193      }
     194  
     195      /**
     196       * Returns the meta-information about the given option.
     197       *
     198       * @param string|null $name short or long option name, defaults to returning the list of all
     199       * @return array|object|false array with all, object with the specific option meta-information or false of no such an option
     200       */
     201      public function get_option_info($name=null) {
     202  
     203          $supportedoptions = array(
     204              array('', 'passfile', input_manager::TYPE_FILE, 'File name of the passphrase file (HTTP access only)'),
     205              array('', 'password', input_manager::TYPE_RAW, 'Session passphrase (HTTP access only)'),
     206              array('', 'proxy', input_manager::TYPE_RAW, 'HTTP proxy host and port (e.g. \'our.proxy.edu:8888\')'),
     207              array('', 'proxytype', input_manager::TYPE_RAW, 'Proxy type (HTTP or SOCKS5)'),
     208              array('', 'proxyuserpwd', input_manager::TYPE_RAW, 'Proxy username and password (e.g. \'username:password\')'),
     209              array('', 'returnurl', input_manager::TYPE_URL, 'Return URL (HTTP access only)'),
     210              array('h', 'help', input_manager::TYPE_FLAG, 'Prints usage information'),
     211              array('i', 'install', input_manager::TYPE_FLAG, 'Installation mode'),
     212              array('m', 'md5', input_manager::TYPE_MD5, 'Expected MD5 hash of the ZIP package to deploy'),
     213              array('n', 'name', input_manager::TYPE_PLUGIN, 'Plugin name (the name of its folder)'),
     214              array('p', 'package', input_manager::TYPE_URL, 'URL to the ZIP package to deploy'),
     215              array('r', 'typeroot', input_manager::TYPE_PATH, 'Full path of the container for this plugin type'),
     216              array('u', 'upgrade', input_manager::TYPE_FLAG, 'Upgrade mode'),
     217          );
     218  
     219          if (is_null($name)) {
     220              $all = array();
     221              foreach ($supportedoptions as $optioninfo) {
     222                  $info = new stdClass();
     223                  $info->shortname = $optioninfo[0];
     224                  $info->longname = $optioninfo[1];
     225                  $info->type = $optioninfo[2];
     226                  $info->desc = $optioninfo[3];
     227                  $all[] = $info;
     228              }
     229              return $all;
     230          }
     231  
     232          $found = false;
     233  
     234          foreach ($supportedoptions as $optioninfo) {
     235              if (strlen($name) == 1) {
     236                  // Search by the short option name
     237                  if ($optioninfo[0] === $name) {
     238                      $found = $optioninfo;
     239                      break;
     240                  }
     241              } else {
     242                  // Search by the long option name
     243                  if ($optioninfo[1] === $name) {
     244                      $found = $optioninfo;
     245                      break;
     246                  }
     247              }
     248          }
     249  
     250          if (!$found) {
     251              return false;
     252          }
     253  
     254          $info = new stdClass();
     255          $info->shortname = $found[0];
     256          $info->longname = $found[1];
     257          $info->type = $found[2];
     258          $info->desc = $found[3];
     259  
     260          return $info;
     261      }
     262  
     263      /**
     264       * Casts the value to the given type.
     265       *
     266       * @param mixed $raw the raw value
     267       * @param string $type the expected value type, e.g. {@link input_manager::TYPE_INT}
     268       * @return mixed
     269       */
     270      public function cast_value($raw, $type) {
     271  
     272          if (is_array($raw)) {
     273              throw new invalid_coding_exception('Unsupported array option.');
     274          } else if (is_object($raw)) {
     275              throw new invalid_coding_exception('Unsupported object option.');
     276          }
     277  
     278          switch ($type) {
     279  
     280              case input_manager::TYPE_FILE:
     281                  $raw = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $raw);
     282                  $raw = preg_replace('~\.\.+~', '', $raw);
     283                  if ($raw === '.') {
     284                      $raw = '';
     285                  }
     286                  return $raw;
     287  
     288              case input_manager::TYPE_FLAG:
     289                  return true;
     290  
     291              case input_manager::TYPE_INT:
     292                  return (int)$raw;
     293  
     294              case input_manager::TYPE_PATH:
     295                  if (strpos($raw, '~') !== false) {
     296                      throw new invalid_option_exception('Using the tilde (~) character in paths is not supported');
     297                  }
     298                  $colonpos = strpos($raw, ':');
     299                  if ($colonpos !== false) {
     300                      if ($colonpos !== 1 or strrpos($raw, ':') !== 1) {
     301                          throw new invalid_option_exception('Using the colon (:) character in paths is supported for Windows drive labels only.');
     302                      }
     303                      if (preg_match('/^[a-zA-Z]:/', $raw) !== 1) {
     304                          throw new invalid_option_exception('Using the colon (:) character in paths is supported for Windows drive labels only.');
     305                      }
     306                  }
     307                  $raw = str_replace('\\', '/', $raw);
     308                  $raw = preg_replace('~[[:cntrl:]]|[&<>"`\|\']~u', '', $raw);
     309                  $raw = preg_replace('~\.\.+~', '', $raw);
     310                  $raw = preg_replace('~//+~', '/', $raw);
     311                  $raw = preg_replace('~/(\./)+~', '/', $raw);
     312                  return $raw;
     313  
     314              case input_manager::TYPE_RAW:
     315                  return $raw;
     316  
     317              case input_manager::TYPE_URL:
     318                  $regex  = '^(https?|ftp)\:\/\/'; // protocol
     319                  $regex .= '([a-z0-9+!*(),;?&=\$_.-]+(\:[a-z0-9+!*(),;?&=\$_.-]+)?@)?'; // optional user and password
     320                  $regex .= '[a-z0-9+\$_-]+(\.[a-z0-9+\$_-]+)*'; // hostname or IP (one word like http://localhost/ allowed)
     321                  $regex .= '(\:[0-9]{2,5})?'; // port (optional)
     322                  $regex .= '(\/([a-z0-9+\$_-]\.?)+)*\/?'; // path to the file
     323                  $regex .= '(\?[a-z+&\$_.-][a-z0-9;:@/&%=+\$_.-]*)?'; // HTTP params
     324  
     325                  if (preg_match('#'.$regex.'#i', $raw)) {
     326                      return $raw;
     327                  } else {
     328                      throw new invalid_option_exception('Not a valid URL');
     329                  }
     330  
     331              case input_manager::TYPE_PLUGIN:
     332                  if (!preg_match('/^[a-z][a-z0-9_]*[a-z0-9]$/', $raw)) {
     333                      throw new invalid_option_exception('Invalid plugin name');
     334                  }
     335                  if (strpos($raw, '__') !== false) {
     336                      throw new invalid_option_exception('Invalid plugin name');
     337                  }
     338                  return $raw;
     339  
     340              case input_manager::TYPE_MD5:
     341                  if (!preg_match('/^[a-f0-9]{32}$/', $raw)) {
     342                      throw new invalid_option_exception('Invalid MD5 hash format');
     343                  }
     344                  return $raw;
     345  
     346              default:
     347                  throw new invalid_coding_exception('Unknown option type.');
     348  
     349          }
     350      }
     351  
     352      /**
     353       * Picks the appropriate helper class to delegate calls to.
     354       */
     355      protected function initialize() {
     356          if (PHP_SAPI === 'cli') {
     357              $this->inputprovider = input_cli_provider::instance();
     358          } else {
     359              $this->inputprovider = input_http_provider::instance();
     360          }
     361      }
     362  
     363      // End of external API
     364  
     365      /**
     366       * Validates the parameter name.
     367       *
     368       * @param string $name
     369       * @throws invalid_coding_exception
     370       */
     371      protected function validate_option_name($name) {
     372  
     373          if (empty($name)) {
     374              throw new invalid_coding_exception('Invalid empty option name.');
     375          }
     376  
     377          $meta = $this->get_option_info($name);
     378          if (empty($meta)) {
     379              throw new invalid_coding_exception('Invalid option name: '.$name);
     380          }
     381      }
     382  
     383      /**
     384       * Returns cleaned option value or throws exception.
     385       *
     386       * @param string $name the name of the parameter
     387       * @param string $type the parameter type, e.g. {@link input_manager::TYPE_INT}
     388       * @return mixed
     389       */
     390      protected function get_required_option($name) {
     391          if ($this->inputprovider->has_option($name)) {
     392              return $this->inputprovider->get_option($name);
     393          } else {
     394              throw new missing_option_exception('Missing required option: '.$name);
     395          }
     396      }
     397  
     398      /**
     399       * Returns cleaned option value or the default value
     400       *
     401       * @param string $name the name of the parameter
     402       * @param string $type the parameter type, e.g. {@link input_manager::TYPE_INT}
     403       * @param mixed $default the default value.
     404       * @return mixed
     405       */
     406      protected function get_optional_option($name, $default) {
     407          if ($this->inputprovider->has_option($name)) {
     408              return $this->inputprovider->get_option($name);
     409          } else {
     410              return $default;
     411          }
     412      }
     413  }
     414  
     415  
     416  /**
     417   * Base class for input providers.
     418   *
     419   * @copyright 2012 David Mudrak <david@moodle.com>
     420   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     421   */
     422  abstract class input_provider extends singleton_pattern {
     423  
     424      /** @var array list of all passed valid options */
     425      protected $options = array();
     426  
     427      /**
     428       * Returns the casted value of the option.
     429       *
     430       * @param string $name option name
     431       * @throws invalid_coding_exception if the option has not been passed
     432       * @return mixed casted value of the option
     433       */
     434      public function get_option($name) {
     435  
     436          if (!$this->has_option($name)) {
     437              throw new invalid_coding_exception('Option not passed: '.$name);
     438          }
     439  
     440          return $this->options[$name];
     441      }
     442  
     443      /**
     444       * Was the given option passed?
     445       *
     446       * @param string $name optionname
     447       * @return bool
     448       */
     449      public function has_option($name) {
     450          return array_key_exists($name, $this->options);
     451      }
     452  
     453      /**
     454       * Initializes the input provider.
     455       */
     456      protected function initialize() {
     457          $this->populate_options();
     458      }
     459  
     460      // End of external API
     461  
     462      /**
     463       * Parses and validates all supported options passed to the script.
     464       */
     465      protected function populate_options() {
     466  
     467          $input = input_manager::instance();
     468          $raw = $this->parse_raw_options();
     469          $cooked = array();
     470  
     471          foreach ($raw as $k => $v) {
     472              if (is_array($v) or is_object($v)) {
     473                  // Not supported.
     474              }
     475  
     476              $info = $input->get_option_info($k);
     477              if (!$info) {
     478                  continue;
     479              }
     480  
     481              $casted = $input->cast_value($v, $info->type);
     482  
     483              if (!empty($info->shortname)) {
     484                  $cooked[$info->shortname] = $casted;
     485              }
     486  
     487              if (!empty($info->longname)) {
     488                  $cooked[$info->longname] = $casted;
     489              }
     490          }
     491  
     492          // Store the options.
     493          $this->options = $cooked;
     494      }
     495  }
     496  
     497  
     498  /**
     499   * Provides access to the script options passed via CLI.
     500   *
     501   * @copyright 2012 David Mudrak <david@moodle.com>
     502   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     503   */
     504  class input_cli_provider extends input_provider {
     505  
     506      /**
     507       * Parses raw options passed to the script.
     508       *
     509       * @return array as returned by getopt()
     510       */
     511      protected function parse_raw_options() {
     512  
     513          $input = input_manager::instance();
     514  
     515          // Signatures of some in-built PHP functions are just crazy, aren't they.
     516          $short = '';
     517          $long = array();
     518  
     519          foreach ($input->get_option_info() as $option) {
     520              if ($option->type === input_manager::TYPE_FLAG) {
     521                  // No value expected for this option.
     522                  $short .= $option->shortname;
     523                  $long[] = $option->longname;
     524              } else {
     525                  // A value expected for the option, all considered as optional.
     526                  $short .= empty($option->shortname) ? '' : $option->shortname.'::';
     527                  $long[] = empty($option->longname) ? '' : $option->longname.'::';
     528              }
     529          }
     530  
     531          return getopt($short, $long);
     532      }
     533  }
     534  
     535  
     536  /**
     537   * Provides access to the script options passed via HTTP request.
     538   *
     539   * @copyright 2012 David Mudrak <david@moodle.com>
     540   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     541   */
     542  class input_http_provider extends input_provider {
     543  
     544      /**
     545       * Parses raw options passed to the script.
     546       *
     547       * @return array of raw values passed via HTTP request
     548       */
     549      protected function parse_raw_options() {
     550          return $_POST;
     551      }
     552  }
     553  
     554  
     555  // Output handling /////////////////////////////////////////////////////////////
     556  
     557  /**
     558   * Provides output operations.
     559   *
     560   * @copyright 2012 David Mudrak <david@moodle.com>
     561   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     562   */
     563  class output_manager extends singleton_pattern {
     564  
     565      /** @var output_cli_provider|output_http_provider the provider of the output functionality */
     566      protected $outputprovider = null;
     567  
     568      /**
     569       * Magic method triggered when invoking an inaccessible method.
     570       *
     571       * @param string $name method name
     572       * @param array $arguments method arguments
     573       */
     574      public function __call($name, array $arguments = array()) {
     575          call_user_func_array(array($this->outputprovider, $name), $arguments);
     576      }
     577  
     578      /**
     579       * Picks the appropriate helper class to delegate calls to.
     580       */
     581      protected function initialize() {
     582          if (PHP_SAPI === 'cli') {
     583              $this->outputprovider = output_cli_provider::instance();
     584          } else {
     585              $this->outputprovider = output_http_provider::instance();
     586          }
     587      }
     588  }
     589  
     590  
     591  /**
     592   * Base class for all output providers.
     593   *
     594   * @copyright 2012 David Mudrak <david@moodle.com>
     595   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     596   */
     597  abstract class output_provider extends singleton_pattern {
     598  }
     599  
     600  /**
     601   * Provides output to the command line.
     602   *
     603   * @copyright 2012 David Mudrak <david@moodle.com>
     604   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     605   */
     606  class output_cli_provider extends output_provider {
     607  
     608      /**
     609       * Prints help information in CLI mode.
     610       */
     611      public function help() {
     612  
     613          $this->outln('mdeploy.php - Moodle (http://moodle.org) deployment utility');
     614          $this->outln();
     615          $this->outln('Usage: $ sudo -u apache php mdeploy.php [options]');
     616          $this->outln();
     617          $input = input_manager::instance();
     618          foreach($input->get_option_info() as $info) {
     619              $option = array();
     620              if (!empty($info->shortname)) {
     621                  $option[] = '-'.$info->shortname;
     622              }
     623              if (!empty($info->longname)) {
     624                  $option[] = '--'.$info->longname;
     625              }
     626              $this->outln(sprintf('%-20s %s', implode(', ', $option), $info->desc));
     627          }
     628      }
     629  
     630      // End of external API
     631  
     632      /**
     633       * Writes a text to the STDOUT followed by a new line character.
     634       *
     635       * @param string $text text to print
     636       */
     637      protected function outln($text='') {
     638          fputs(STDOUT, $text.PHP_EOL);
     639      }
     640  }
     641  
     642  
     643  /**
     644   * Provides HTML output as a part of HTTP response.
     645   *
     646   * @copyright 2012 David Mudrak <david@moodle.com>
     647   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     648   */
     649  class output_http_provider extends output_provider {
     650  
     651      /**
     652       * Prints help on the script usage.
     653       */
     654      public function help() {
     655          // No help available via HTTP
     656      }
     657  
     658      /**
     659       * Display the information about uncaught exception
     660       *
     661       * @param Exception $e uncaught exception
     662       */
     663      public function exception(Exception $e) {
     664  
     665          $docslink = 'http://docs.moodle.org/en/admin/mdeploy/'.get_class($e);
     666          $this->start_output();
     667          echo('<h1>Oops! It did it again</h1>');
     668          echo('<p><strong>Moodle deployment utility had a trouble with your request.
     669              See <a href="'.$docslink.'">the docs page</a> and the debugging information for more details.</strong></p>');
     670          echo('<pre>');
     671          echo exception_handlers::format_exception_info($e);
     672          echo('</pre>');
     673          $this->end_output();
     674      }
     675  
     676      // End of external API
     677  
     678      /**
     679       * Produce the HTML page header
     680       */
     681      protected function start_output() {
     682          echo '<!doctype html>
     683  <html lang="en">
     684  <head>
     685    <meta charset="utf-8">
     686    <style type="text/css">
     687      body {background-color:#666;font-family:"DejaVu Sans","Liberation Sans",Freesans,sans-serif;}
     688      h1 {text-align:center;}
     689      pre {white-space: pre-wrap;}
     690      #page {background-color:#eee;width:1024px;margin:5em auto;border:3px solid #333;border-radius: 15px;padding:1em;}
     691    </style>
     692  </head>
     693  <body>
     694  <div id="page">';
     695      }
     696  
     697      /**
     698       * Produce the HTML page footer
     699       */
     700      protected function end_output() {
     701          echo '</div></body></html>';
     702      }
     703  }
     704  
     705  // The main class providing all the functionality //////////////////////////////
     706  
     707  /**
     708   * The actual worker class implementing the main functionality of the script.
     709   *
     710   * @copyright 2012 David Mudrak <david@moodle.com>
     711   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     712   */
     713  class worker extends singleton_pattern {
     714  
     715      const EXIT_OK                       = 0;    // Success exit code.
     716      const EXIT_HELP                     = 1;    // Explicit help required.
     717      const EXIT_UNKNOWN_ACTION           = 127;  // Neither -i nor -u provided.
     718  
     719      /** @var input_manager */
     720      protected $input = null;
     721  
     722      /** @var output_manager */
     723      protected $output = null;
     724  
     725      /** @var int the most recent cURL error number, zero for no error */
     726      private $curlerrno = null;
     727  
     728      /** @var string the most recent cURL error message, empty string for no error */
     729      private $curlerror = null;
     730  
     731      /** @var array|false the most recent cURL request info, if it was successful */
     732      private $curlinfo = null;
     733  
     734      /** @var string the full path to the log file */
     735      private $logfile = null;
     736  
     737      /** @var array the whitelisted config options which can be queried. */
     738      private $validconfigoptions = array(
     739          'dirroot'       => true,
     740          'dataroot'      => true,
     741      );
     742  
     743      /**
     744       * Main - the one that actually does something
     745       */
     746      public function execute() {
     747  
     748          $this->log('=== MDEPLOY EXECUTION START ===');
     749  
     750          // Authorize access. None in CLI. Passphrase in HTTP.
     751          $this->authorize();
     752  
     753          // Asking for help in the CLI mode.
     754          if ($this->input->get_option('help')) {
     755              $this->output->help();
     756              $this->done(self::EXIT_HELP);
     757          }
     758  
     759          if ($this->input->get_option('upgrade')) {
     760              $this->log('Plugin upgrade requested');
     761  
     762              // Fetch the ZIP file into a temporary location.
     763              $source = $this->input->get_option('package');
     764              $target = $this->target_location($source);
     765              $this->log('Downloading package '.$source);
     766  
     767              if ($this->download_file($source, $target)) {
     768                  $this->log('Package downloaded into '.$target);
     769              } else {
     770                  $this->log('cURL error ' . $this->curlerrno . ' ' . $this->curlerror);
     771                  $this->log('Unable to download the file from ' . $source . ' into ' . $target);
     772                  throw new download_file_exception('Unable to download the package');
     773              }
     774  
     775              // Compare MD5 checksum of the ZIP file
     776              $md5remote = $this->input->get_option('md5');
     777              $md5local = md5_file($target);
     778  
     779              if ($md5local !== $md5remote) {
     780                  $this->log('MD5 checksum failed. Expected: '.$md5remote.' Got: '.$md5local);
     781                  throw new checksum_exception('MD5 checksum failed');
     782              }
     783              $this->log('MD5 checksum ok');
     784  
     785              // Check that the specified typeroot is within the current site's dirroot.
     786              $plugintyperoot = $this->input->get_option('typeroot');
     787              if (strpos(realpath($plugintyperoot), realpath($this->get_env('dirroot'))) !== 0) {
     788                  throw new backup_folder_exception('Unable to backup the current version of the plugin (typeroot is invalid)');
     789              }
     790  
     791              // Backup the current version of the plugin
     792              $pluginname = $this->input->get_option('name');
     793              $sourcelocation = $plugintyperoot.'/'.$pluginname;
     794              $backuplocation = $this->backup_location($sourcelocation);
     795  
     796              $this->log('Current plugin code location: '.$sourcelocation);
     797              $this->log('Moving the current code into archive: '.$backuplocation);
     798  
     799              if (file_exists($sourcelocation)) {
     800                  // We don't want to touch files unless we are pretty sure it would be all ok.
     801                  if (!$this->move_directory_source_precheck($sourcelocation)) {
     802                      throw new backup_folder_exception('Unable to backup the current version of the plugin (source precheck failed)');
     803                  }
     804                  if (!$this->move_directory_target_precheck($backuplocation)) {
     805                      throw new backup_folder_exception('Unable to backup the current version of the plugin (backup precheck failed)');
     806                  }
     807  
     808                  // Looking good, let's try it.
     809                  if (!$this->move_directory($sourcelocation, $backuplocation, true)) {
     810                      throw new backup_folder_exception('Unable to backup the current version of the plugin (moving failed)');
     811                  }
     812  
     813              } else {
     814                  // Upgrading missing plugin - this happens often during upgrades.
     815                  if (!$this->create_directory_precheck($sourcelocation)) {
     816                      throw new filesystem_exception('Unable to prepare the plugin location (cannot create new directory)');
     817                  }
     818              }
     819  
     820              // Unzip the plugin package file into the target location.
     821              $this->unzip_plugin($target, $plugintyperoot, $sourcelocation, $backuplocation);
     822              $this->log('Package successfully extracted');
     823  
     824              // Redirect to the given URL (in HTTP) or exit (in CLI).
     825              $this->done();
     826  
     827          } else if ($this->input->get_option('install')) {
     828              $this->log('Plugin installation requested');
     829  
     830              $plugintyperoot = $this->input->get_option('typeroot');
     831              $pluginname     = $this->input->get_option('name');
     832              $source         = $this->input->get_option('package');
     833              $md5remote      = $this->input->get_option('md5');
     834  
     835              if (strpos(realpath($plugintyperoot), realpath($this->get_env('dirroot'))) !== 0) {
     836                  throw new backup_folder_exception('Unable to prepare the plugin location (typeroot is invalid)');
     837              }
     838  
     839              // Check if the plugin location if available for us.
     840              $pluginlocation = $plugintyperoot.'/'.$pluginname;
     841  
     842              $this->log('New plugin code location: '.$pluginlocation);
     843  
     844              if (file_exists($pluginlocation)) {
     845                  throw new filesystem_exception('Unable to prepare the plugin location (directory already exists)');
     846              }
     847  
     848              if (!$this->create_directory_precheck($pluginlocation)) {
     849                  throw new filesystem_exception('Unable to prepare the plugin location (cannot create new directory)');
     850              }
     851  
     852              // Fetch the ZIP file into a temporary location.
     853              $target = $this->target_location($source);
     854              $this->log('Downloading package '.$source);
     855  
     856              if ($this->download_file($source, $target)) {
     857                  $this->log('Package downloaded into '.$target);
     858              } else {
     859                  $this->log('cURL error ' . $this->curlerrno . ' ' . $this->curlerror);
     860                  $this->log('Unable to download the file');
     861                  throw new download_file_exception('Unable to download the package');
     862              }
     863  
     864              // Compare MD5 checksum of the ZIP file
     865              $md5local = md5_file($target);
     866  
     867              if ($md5local !== $md5remote) {
     868                  $this->log('MD5 checksum failed. Expected: '.$md5remote.' Got: '.$md5local);
     869                  throw new checksum_exception('MD5 checksum failed');
     870              }
     871              $this->log('MD5 checksum ok');
     872  
     873              // Unzip the plugin package file into the plugin location.
     874              $this->unzip_plugin($target, $plugintyperoot, $pluginlocation, false);
     875              $this->log('Package successfully extracted');
     876  
     877              // Redirect to the given URL (in HTTP) or exit (in CLI).
     878              $this->done();
     879          }
     880  
     881          // Print help in CLI by default.
     882          $this->output->help();
     883          $this->done(self::EXIT_UNKNOWN_ACTION);
     884      }
     885  
     886      /**
     887       * Attempts to log a thrown exception
     888       *
     889       * @param Exception $e uncaught exception
     890       */
     891      public function log_exception(Exception $e) {
     892          $this->log($e->__toString());
     893      }
     894  
     895      /**
     896       * Initialize the worker class.
     897       */
     898      protected function initialize() {
     899          $this->input = input_manager::instance();
     900          $this->output = output_manager::instance();
     901      }
     902  
     903      // End of external API
     904  
     905      /**
     906       * Finish this script execution.
     907       *
     908       * @param int $exitcode
     909       */
     910      protected function done($exitcode = self::EXIT_OK) {
     911  
     912          if (PHP_SAPI === 'cli') {
     913              exit($exitcode);
     914  
     915          } else {
     916              $returnurl = $this->input->get_option('returnurl');
     917              $this->redirect($returnurl);
     918              exit($exitcode);
     919          }
     920      }
     921  
     922      /**
     923       * Authorize access to the script.
     924       *
     925       * In CLI mode, the access is automatically authorized. In HTTP mode, the
     926       * passphrase submitted via the request params must match the contents of the
     927       * file, the name of which is passed in another parameter.
     928       *
     929       * @throws unauthorized_access_exception
     930       */
     931      protected function authorize() {
     932          if (PHP_SAPI === 'cli') {
     933              $this->log('Successfully authorized using the CLI SAPI');
     934              return;
     935          }
     936  
     937          $passfile = $this->input->get_option('passfile');
     938          $password = $this->input->get_option('password');
     939  
     940          $passpath = $this->get_env('dataroot') . '/mdeploy/auth/' . $passfile;
     941  
     942          if (!is_readable($passpath)) {
     943              throw new unauthorized_access_exception('Unable to read the passphrase file.');
     944          }
     945  
     946          $stored = file($passpath, FILE_IGNORE_NEW_LINES);
     947  
     948          // "This message will self-destruct in five seconds." -- Mission Commander Swanbeck, Mission: Impossible II
     949          unlink($passpath);
     950  
     951          if (is_readable($passpath)) {
     952              throw new unauthorized_access_exception('Unable to remove the passphrase file.');
     953          }
     954  
     955          if (count($stored) < 2) {
     956              throw new unauthorized_access_exception('Invalid format of the passphrase file.');
     957          }
     958  
     959          if (time() - (int)$stored[1] > 30 * 60) {
     960              throw new unauthorized_access_exception('Passphrase timeout.');
     961          }
     962  
     963          if (strlen($stored[0]) < 24) {
     964              throw new unauthorized_access_exception('Session passphrase not long enough.');
     965          }
     966  
     967          if ($password !== $stored[0]) {
     968              throw new unauthorized_access_exception('Session passphrase does not match the stored one.');
     969          }
     970  
     971          $this->log('Successfully authorized using the passphrase file');
     972      }
     973  
     974      /**
     975       * Returns the full path to the log file.
     976       *
     977       * @return string
     978       */
     979      protected function log_location() {
     980          if (!is_null($this->logfile)) {
     981              return $this->logfile;
     982          }
     983  
     984          $dataroot = $this->get_env('dataroot');
     985  
     986          if (empty($dataroot)) {
     987              $this->logfile = false;
     988              return $this->logfile;
     989          }
     990  
     991          $myroot = $dataroot.'/mdeploy';
     992  
     993          if (!is_dir($myroot)) {
     994              mkdir($myroot, 02777, true);
     995          }
     996  
     997          $this->logfile = $myroot.'/mdeploy.log';
     998          return $this->logfile;
     999      }
    1000  
    1001      /**
    1002       * Choose the target location for the given ZIP's URL.
    1003       *
    1004       * @param string $source URL
    1005       * @return string
    1006       */
    1007      protected function target_location($source) {
    1008          $dataroot = $this->get_env('dataroot');
    1009          $pool = $dataroot.'/mdeploy/var';
    1010  
    1011          if (!is_dir($pool)) {
    1012              mkdir($pool, 02777, true);
    1013          }
    1014  
    1015          $target = $pool.'/'.md5($source);
    1016  
    1017          $suffix = 0;
    1018          while (file_exists($target.'.'.$suffix.'.zip')) {
    1019              $suffix++;
    1020          }
    1021  
    1022          return $target.'.'.$suffix.'.zip';
    1023      }
    1024  
    1025      /**
    1026       * Choose the location of the current plugin folder backup
    1027       *
    1028       * @param string $path full path to the current folder
    1029       * @return string
    1030       */
    1031      protected function backup_location($path) {
    1032          $dataroot = $this->get_env('dataroot');
    1033          $pool = $dataroot.'/mdeploy/archive';
    1034  
    1035          if (!is_dir($pool)) {
    1036              mkdir($pool, 02777, true);
    1037          }
    1038  
    1039          $target = $pool.'/'.basename($path).'_'.time();
    1040  
    1041          $suffix = 0;
    1042          while (file_exists($target.'.'.$suffix)) {
    1043              $suffix++;
    1044          }
    1045  
    1046          return $target.'.'.$suffix;
    1047      }
    1048  
    1049      /**
    1050       * Downloads the given file into the given destination.
    1051       *
    1052       * This is basically a simplified version of {@link download_file_content()} from
    1053       * Moodle itself, tuned for fetching files from moodle.org servers.
    1054       *
    1055       * @param string $source file url starting with http(s)://
    1056       * @param string $target store the downloaded content to this file (full path)
    1057       * @return bool true on success, false otherwise
    1058       * @throws download_file_exception
    1059       */
    1060      protected function download_file($source, $target) {
    1061  
    1062          $newlines = array("\r", "\n");
    1063          $source = str_replace($newlines, '', $source);
    1064          if (!preg_match('|^https?://|i', $source)) {
    1065              throw new download_file_exception('Unsupported transport protocol.');
    1066          }
    1067          if (!$ch = curl_init($source)) {
    1068              $this->log('Unable to init cURL.');
    1069              return false;
    1070          }
    1071  
    1072          curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // verify the peer's certificate
    1073          curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // check the existence of a common name and also verify that it matches the hostname provided
    1074          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the transfer as a string
    1075          curl_setopt($ch, CURLOPT_HEADER, false); // don't include the header in the output
    1076          curl_setopt($ch, CURLOPT_TIMEOUT, 3600);
    1077          curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); // nah, moodle.org is never unavailable! :-p
    1078          curl_setopt($ch, CURLOPT_URL, $source);
    1079          curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Allow redirection, we trust in ssl.
    1080          curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
    1081  
    1082          if ($cacertfile = $this->get_cacert()) {
    1083              // Do not use CA certs provided by the operating system. Instead,
    1084              // use this CA cert to verify the ZIP provider.
    1085              $this->log('Using custom CA certificate '.$cacertfile);
    1086              curl_setopt($ch, CURLOPT_CAINFO, $cacertfile);
    1087          } else {
    1088              $this->log('Using operating system CA certificates.');
    1089          }
    1090  
    1091          $proxy = $this->input->get_option('proxy', false);
    1092          if (!empty($proxy)) {
    1093              curl_setopt($ch, CURLOPT_PROXY, $proxy);
    1094  
    1095              $proxytype = $this->input->get_option('proxytype', false);
    1096              if (strtoupper($proxytype) === 'SOCKS5') {
    1097                  $this->log('Using SOCKS5 proxy');
    1098                  curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
    1099              } else if (!empty($proxytype)) {
    1100                  $this->log('Using HTTP proxy');
    1101                  curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
    1102                  curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, false);
    1103              }
    1104  
    1105              $proxyuserpwd = $this->input->get_option('proxyuserpwd', false);
    1106              if (!empty($proxyuserpwd)) {
    1107                  curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuserpwd);
    1108                  curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM);
    1109              }
    1110          }
    1111  
    1112          $targetfile = fopen($target, 'w');
    1113  
    1114          if (!$targetfile) {
    1115              throw new download_file_exception('Unable to create local file '.$target);
    1116          }
    1117  
    1118          curl_setopt($ch, CURLOPT_FILE, $targetfile);
    1119  
    1120          $result = curl_exec($ch);
    1121  
    1122          // try to detect encoding problems
    1123          if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) {
    1124              curl_setopt($ch, CURLOPT_ENCODING, 'none');
    1125              $result = curl_exec($ch);
    1126          }
    1127  
    1128          fclose($targetfile);
    1129  
    1130          $this->curlerrno = curl_errno($ch);
    1131          $this->curlerror = curl_error($ch);
    1132          $this->curlinfo = curl_getinfo($ch);
    1133  
    1134          if (!$result or $this->curlerrno) {
    1135              $this->log('Curl Error.');
    1136              return false;
    1137  
    1138          } else if (is_array($this->curlinfo) and (empty($this->curlinfo['http_code']) or ($this->curlinfo['http_code'] != 200))) {
    1139              $this->log('Curl remote error.');
    1140              $this->log(print_r($this->curlinfo,true));
    1141              return false;
    1142          }
    1143  
    1144          return true;
    1145      }
    1146  
    1147      /**
    1148       * Fetch environment settings.
    1149       *
    1150       * @param string $key The key to fetch
    1151       * @return mixed The value of the key if found.
    1152       * @throws invalid_setting_exception if the option is not set, or is invalid.
    1153       */
    1154      protected function get_env($key) {
    1155          global $CFG;
    1156  
    1157          if (array_key_exists($key, $this->validconfigoptions)) {
    1158              if (isset($CFG->$key)) {
    1159                  return $CFG->$key;
    1160              }
    1161              throw new invalid_setting_exception("Requested environment setting '{$key}' is not currently set.");
    1162          } else {
    1163              throw new invalid_setting_exception("Requested environment setting '{$key}' is invalid.");
    1164          }
    1165      }
    1166  
    1167      /**
    1168       * Get the location of ca certificates.
    1169       * @return string absolute file path or empty if default used
    1170       */
    1171      protected function get_cacert() {
    1172          $dataroot = $this->get_env('dataroot');
    1173  
    1174          // Bundle in dataroot always wins.
    1175          if (is_readable($dataroot.'/moodleorgca.crt')) {
    1176              return realpath($dataroot.'/moodleorgca.crt');
    1177          }
    1178  
    1179          // Next comes the default from php.ini
    1180          $cacert = ini_get('curl.cainfo');
    1181          if (!empty($cacert) and is_readable($cacert)) {
    1182              return realpath($cacert);
    1183          }
    1184  
    1185          // Windows PHP does not have any certs, we need to use something.
    1186          if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) {
    1187              if (is_readable(__DIR__.'/lib/cacert.pem')) {
    1188                  return realpath(__DIR__.'/lib/cacert.pem');
    1189              }
    1190          }
    1191  
    1192          // Use default, this should work fine on all properly configured *nix systems.
    1193          return null;
    1194      }
    1195  
    1196      /**
    1197       * Log a message
    1198       *
    1199       * @param string $message
    1200       */
    1201      protected function log($message) {
    1202  
    1203          $logpath = $this->log_location();
    1204  
    1205          if (empty($logpath)) {
    1206              // no logging available
    1207              return;
    1208          }
    1209  
    1210          $f = fopen($logpath, 'ab');
    1211  
    1212          if ($f === false) {
    1213              throw new filesystem_exception('Unable to open the log file for appending');
    1214          }
    1215  
    1216          $message = $this->format_log_message($message);
    1217  
    1218          fwrite($f, $message);
    1219  
    1220          fclose($f);
    1221      }
    1222  
    1223      /**
    1224       * Prepares the log message for writing into the file
    1225       *
    1226       * @param string $msg
    1227       * @return string
    1228       */
    1229      protected function format_log_message($msg) {
    1230  
    1231          $msg = trim($msg);
    1232          $timestamp = date("Y-m-d H:i:s");
    1233  
    1234          return $timestamp . ' '. $msg . PHP_EOL;
    1235      }
    1236  
    1237      /**
    1238       * Checks to see if the given source could be safely moved into a new location
    1239       *
    1240       * @param string $source full path to the existing directory
    1241       * @return bool
    1242       */
    1243      protected function move_directory_source_precheck($source) {
    1244  
    1245          if (!is_writable($source)) {
    1246              return false;
    1247          }
    1248  
    1249          if (is_dir($source)) {
    1250              $handle = opendir($source);
    1251          } else {
    1252              return false;
    1253          }
    1254  
    1255          $result = true;
    1256  
    1257          while ($filename = readdir($handle)) {
    1258              $sourcepath = $source.'/'.$filename;
    1259  
    1260              if ($filename === '.' or $filename === '..') {
    1261                  continue;
    1262              }
    1263  
    1264              if (is_dir($sourcepath)) {
    1265                  $result = $result && $this->move_directory_source_precheck($sourcepath);
    1266  
    1267              } else {
    1268                  $result = $result && is_writable($sourcepath);
    1269              }
    1270          }
    1271  
    1272          closedir($handle);
    1273  
    1274          return $result;
    1275      }
    1276  
    1277      /**
    1278       * Checks to see if a source folder could be safely moved into the given new location
    1279       *
    1280       * @param string $destination full path to the new expected location of a folder
    1281       * @return bool
    1282       */
    1283      protected function move_directory_target_precheck($target) {
    1284  
    1285          // Check if the target folder does not exist yet, can be created
    1286          // and removed again.
    1287          $result = $this->create_directory_precheck($target);
    1288  
    1289          // At the moment, it seems to be enough to check. We may want to add
    1290          // more steps in the future.
    1291  
    1292          return $result;
    1293      }
    1294  
    1295      /**
    1296       * Make sure the given directory can be created (and removed)
    1297       *
    1298       * @param string $path full path to the folder
    1299       * @return bool
    1300       */
    1301      protected function create_directory_precheck($path) {
    1302  
    1303          if (file_exists($path)) {
    1304              return false;
    1305          }
    1306  
    1307          $result = mkdir($path, 02777) && rmdir($path);
    1308  
    1309          return $result;
    1310      }
    1311  
    1312      /**
    1313       * Moves the given source into a new location recursively
    1314       *
    1315       * The target location can not exist.
    1316       *
    1317       * @param string $source full path to the existing directory
    1318       * @param string $destination full path to the new location of the folder
    1319       * @param bool $keepsourceroot should the root of the $source be kept or removed at the end
    1320       * @return bool
    1321       */
    1322      protected function move_directory($source, $target, $keepsourceroot = false) {
    1323  
    1324          if (file_exists($target)) {
    1325              throw new filesystem_exception('Unable to move the directory - target location already exists');
    1326          }
    1327  
    1328          return $this->move_directory_into($source, $target, $keepsourceroot);
    1329      }
    1330  
    1331      /**
    1332       * Moves the given source into a new location recursively
    1333       *
    1334       * If the target already exists, files are moved into it. The target is created otherwise.
    1335       *
    1336       * @param string $source full path to the existing directory
    1337       * @param string $destination full path to the new location of the folder
    1338       * @param bool $keepsourceroot should the root of the $source be kept or removed at the end
    1339       * @return bool
    1340       */
    1341      protected function move_directory_into($source, $target, $keepsourceroot = false) {
    1342  
    1343          if (is_dir($source)) {
    1344              $handle = opendir($source);
    1345          } else {
    1346              throw new filesystem_exception('Source location is not a directory');
    1347          }
    1348  
    1349          if (is_dir($target)) {
    1350              $result = true;
    1351          } else {
    1352              $result = mkdir($target, 02777);
    1353          }
    1354  
    1355          while ($filename = readdir($handle)) {
    1356              $sourcepath = $source.'/'.$filename;
    1357              $targetpath = $target.'/'.$filename;
    1358  
    1359              if ($filename === '.' or $filename === '..') {
    1360                  continue;
    1361              }
    1362  
    1363              if (is_dir($sourcepath)) {
    1364                  $result = $result && $this->move_directory($sourcepath, $targetpath, false);
    1365  
    1366              } else {
    1367                  $result = $result && rename($sourcepath, $targetpath);
    1368              }
    1369          }
    1370  
    1371          closedir($handle);
    1372  
    1373          if (!$keepsourceroot) {
    1374              $result = $result && rmdir($source);
    1375          }
    1376  
    1377          clearstatcache();
    1378  
    1379          return $result;
    1380      }
    1381  
    1382      /**
    1383       * Deletes the given directory recursively
    1384       *
    1385       * @param string $path full path to the directory
    1386       * @param bool $keeppathroot should the root of the $path be kept (i.e. remove the content only) or removed too
    1387       * @return bool
    1388       */
    1389      protected function remove_directory($path, $keeppathroot = false) {
    1390  
    1391          $result = true;
    1392  
    1393          if (!file_exists($path)) {
    1394              return $result;
    1395          }
    1396  
    1397          if (is_dir($path)) {
    1398              $handle = opendir($path);
    1399          } else {
    1400              throw new filesystem_exception('Given path is not a directory');
    1401          }
    1402  
    1403          while ($filename = readdir($handle)) {
    1404              $filepath = $path.'/'.$filename;
    1405  
    1406              if ($filename === '.' or $filename === '..') {
    1407                  continue;
    1408              }
    1409  
    1410              if (is_dir($filepath)) {
    1411                  $result = $result && $this->remove_directory($filepath, false);
    1412  
    1413              } else {
    1414                  $result = $result && unlink($filepath);
    1415              }
    1416          }
    1417  
    1418          closedir($handle);
    1419  
    1420          if (!$keeppathroot) {
    1421              $result = $result && rmdir($path);
    1422          }
    1423  
    1424          clearstatcache();
    1425  
    1426          return $result;
    1427      }
    1428  
    1429      /**
    1430       * Unzip the file obtained from the Plugins directory to this site
    1431       *
    1432       * @param string $ziplocation full path to the ZIP file
    1433       * @param string $plugintyperoot full path to the plugin's type location
    1434       * @param string $expectedlocation expected full path to the plugin after it is extracted
    1435       * @param string|bool $backuplocation location of the previous version of the plugin or false for no backup
    1436       */
    1437      protected function unzip_plugin($ziplocation, $plugintyperoot, $expectedlocation, $backuplocation) {
    1438  
    1439          $zip = new ZipArchive();
    1440          $result = $zip->open($ziplocation);
    1441  
    1442          if ($result !== true) {
    1443              if ($backuplocation !== false) {
    1444                  $this->move_directory($backuplocation, $expectedlocation);
    1445              }
    1446              throw new zip_exception('Unable to open the zip package');
    1447          }
    1448  
    1449          // Make sure that the ZIP has expected structure
    1450          $pluginname = basename($expectedlocation);
    1451          for ($i = 0; $i < $zip->numFiles; $i++) {
    1452              $stat = $zip->statIndex($i);
    1453              $filename = $stat['name'];
    1454              $filename = explode('/', $filename);
    1455              if ($filename[0] !== $pluginname) {
    1456                  $zip->close();
    1457                  throw new zip_exception('Invalid structure of the zip package');
    1458              }
    1459          }
    1460  
    1461          if (!$zip->extractTo($plugintyperoot)) {
    1462              $zip->close();
    1463              $this->remove_directory($expectedlocation, true); // just in case something was created
    1464              if ($backuplocation !== false) {
    1465                  $this->move_directory_into($backuplocation, $expectedlocation);
    1466              }
    1467              throw new zip_exception('Unable to extract the zip package');
    1468          }
    1469  
    1470          $zip->close();
    1471          unlink($ziplocation);
    1472      }
    1473  
    1474      /**
    1475       * Redirect the browser
    1476       *
    1477       * @todo check if there has been some output yet
    1478       * @param string $url
    1479       */
    1480      protected function redirect($url) {
    1481          header('Location: '.$url);
    1482      }
    1483  }
    1484  
    1485  
    1486  /**
    1487   * Provides exception handlers for this script
    1488   */
    1489  class exception_handlers {
    1490  
    1491      /**
    1492       * Sets the exception handler
    1493       *
    1494       *
    1495       * @param string $handler name
    1496       */
    1497      public static function set_handler($handler) {
    1498  
    1499          if (PHP_SAPI === 'cli') {
    1500              // No custom handler available for CLI mode.
    1501              set_exception_handler(null);
    1502              return;
    1503          }
    1504  
    1505          set_exception_handler('exception_handlers::'.$handler.'_exception_handler');
    1506      }
    1507  
    1508      /**
    1509       * Returns the text describing the thrown exception
    1510       *
    1511       * By default, PHP displays full path to scripts when the exception is thrown. In order to prevent
    1512       * sensitive information leak (and yes, the path to scripts at a web server _is_ sensitive information)
    1513       * the path to scripts is removed from the message.
    1514       *
    1515       * @param Exception $e thrown exception
    1516       * @return string
    1517       */
    1518      public static function format_exception_info(Exception $e) {
    1519  
    1520          $mydir = dirname(__FILE__).'/';
    1521          $text = $e->__toString();
    1522          $text = str_replace($mydir, '', $text);
    1523          return $text;
    1524      }
    1525  
    1526      /**
    1527       * Very basic exception handler
    1528       *
    1529       * @param Exception $e uncaught exception
    1530       */
    1531      public static function bootstrap_exception_handler(Exception $e) {
    1532          echo('<h1>Oops! It did it again</h1>');
    1533          echo('<p><strong>Moodle deployment utility had a trouble with your request. See the debugging information for more details.</strong></p>');
    1534          echo('<pre>');
    1535          echo self::format_exception_info($e);
    1536          echo('</pre>');
    1537      }
    1538  
    1539      /**
    1540       * Default exception handler
    1541       *
    1542       * When this handler is used, input_manager and output_manager singleton instances already
    1543       * exist in the memory and can be used.
    1544       *
    1545       * @param Exception $e uncaught exception
    1546       */
    1547      public static function default_exception_handler(Exception $e) {
    1548  
    1549          $worker = worker::instance();
    1550          $worker->log_exception($e);
    1551  
    1552          $output = output_manager::instance();
    1553          $output->exception($e);
    1554      }
    1555  }
    1556  
    1557  ////////////////////////////////////////////////////////////////////////////////
    1558  
    1559  // Check if the script is actually executed or if it was just included by someone
    1560  // else - typically by the PHPUnit. This is a PHP alternative to the Python's
    1561  // if __name__ == '__main__'
    1562  if (!debug_backtrace()) {
    1563      // We are executed by the SAPI.
    1564      exception_handlers::set_handler('bootstrap');
    1565      // Initialize the worker class to actually make the job.
    1566      $worker = worker::instance();
    1567      exception_handlers::set_handler('default');
    1568  
    1569      // Lights, Camera, Action!
    1570      $worker->execute();
    1571  
    1572  } else {
    1573      // We are included - probably by some unit testing framework. Do nothing.
    1574  }
    

    Search This Site: