PageRenderTime 54ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/mdeploy.php

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

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