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