PageRenderTime 53ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/mdeploy.php

https://bitbucket.org/moodle/moodle
PHP | 1531 lines | 780 code | 275 blank | 476 comment | 145 complexity | 87074938548d9afe716d9c08dfca4f7b MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

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

Large files files are truncated, but you can click here to view the full file