Search moodle.org's
Developer Documentation


/ -> 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: