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