[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1