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