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