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

Search This Site: