MDL-34762 course Hide system archetypes from mod chooser
[moodle.git] / mdeploy.php
1 <?php
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/>.
18 /**
19  * Moodle deployment utility
20  *
21  * This script looks after deploying available updates to the local Moodle site.
22  *
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  *
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  */
33 if (defined('MOODLE_INTERNAL')) {
34     die('This is a standalone utility that should not be included by any other Moodle code.');
35 }
38 // Exceptions //////////////////////////////////////////////////////////////////
40 class invalid_coding_exception extends Exception {}
41 class missing_option_exception extends Exception {}
42 class invalid_option_exception extends Exception {}
43 class unauthorized_access_exception extends Exception {}
44 class download_file_exception extends Exception {}
45 class backup_folder_exception extends Exception {}
46 class zip_exception extends Exception {}
47 class filesystem_exception extends Exception {}
48 class checksum_exception extends Exception {}
51 // Various support classes /////////////////////////////////////////////////////
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  */
59 abstract class singleton_pattern {
61     /** @var array singleton_pattern instances */
62     protected static $singletoninstances = array();
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     }
81     /**
82      * Optional post-instantiation code.
83      */
84     protected function initialize() {
85         // Do nothing in this base class.
86     }
88     /**
89      * Direct instantiation not allowed, use the factory method {@link instance()}
90      */
91     final protected function __construct() {
92     }
94     /**
95      * Sorry, this is singleton.
96      */
97     final protected function __clone() {
98     }
99 }
102 // User input handling /////////////////////////////////////////////////////////
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  */
113 class input_manager extends singleton_pattern {
115     const TYPE_FILE         = 'file';   // File name
116     const TYPE_FLAG         = 'flag';   // No value, just a flag (switch)
117     const TYPE_INT          = 'int';    // Integer
118     const TYPE_PATH         = 'path';   // Full path to a file or a directory
119     const TYPE_RAW          = 'raw';    // Raw value, keep as is
120     const TYPE_URL          = 'url';    // URL to a file
121     const TYPE_PLUGIN       = 'plugin'; // Plugin name
122     const TYPE_MD5          = 'md5';    // MD5 hash
124     /** @var input_cli_provider|input_http_provider the provider of the input */
125     protected $inputprovider = null;
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') {
148         $this->validate_option_name($name);
150         $info = $this->get_option_info($name);
152         if ($info->type === input_manager::TYPE_FLAG) {
153             return $this->inputprovider->has_option($name);
154         }
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     }
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) {
171         $supportedoptions = array(
172             array('', 'passfile', input_manager::TYPE_FILE, 'File name of the passphrase file (HTTP access only)'),
173             array('', 'password', input_manager::TYPE_RAW, 'Session passphrase (HTTP access only)'),
174             array('', 'returnurl', input_manager::TYPE_URL, 'Return URL (HTTP access only)'),
175             array('d', 'dataroot', input_manager::TYPE_PATH, 'Full path to the dataroot (moodledata) directory'),
176             array('h', 'help', input_manager::TYPE_FLAG, 'Prints usage information'),
177             array('i', 'install', input_manager::TYPE_FLAG, 'Installation mode'),
178             array('m', 'md5', input_manager::TYPE_MD5, 'Expected MD5 hash of the ZIP package to deploy'),
179             array('n', 'name', input_manager::TYPE_PLUGIN, 'Plugin name (the name of its folder)'),
180             array('p', 'package', input_manager::TYPE_URL, 'URL to the ZIP package to deploy'),
181             array('r', 'typeroot', input_manager::TYPE_PATH, 'Full path of the container for this plugin type'),
182             array('u', 'upgrade', input_manager::TYPE_FLAG, 'Upgrade mode'),
183         );
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         }
198         $found = false;
200         foreach ($supportedoptions as $optioninfo) {
201             if (strlen($name) == 1) {
202                 // Search by the short option name
203                 if ($optioninfo[0] === $name) {
204                     $found = $optioninfo;
205                     break;
206                 }
207             } else {
208                 // Search by the long option name
209                 if ($optioninfo[1] === $name) {
210                     $found = $optioninfo;
211                     break;
212                 }
213             }
214         }
216         if (!$found) {
217             return false;
218         }
220         $info = new stdClass();
221         $info->shortname = $found[0];
222         $info->longname = $found[1];
223         $info->type = $found[2];
224         $info->desc = $found[3];
226         return $info;
227     }
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) {
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         }
244         switch ($type) {
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;
254             case input_manager::TYPE_FLAG:
255                 return true;
257             case input_manager::TYPE_INT:
258                 return (int)$raw;
260             case input_manager::TYPE_PATH:
261                 if (strpos($raw, '~') !== false) {
262                     throw new invalid_option_exception('Using the tilde (~) character in paths is not supported');
263                 }
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;
271             case input_manager::TYPE_RAW:
272                 return $raw;
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
282                 if (preg_match('#'.$regex.'#i', $raw)) {
283                     return $raw;
284                 } else {
285                     throw new invalid_option_exception('Not a valid URL');
286                 }
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;
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;
303             default:
304                 throw new invalid_coding_exception('Unknown option type.');
306         }
307     }
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     }
320     // End of external API
322     /**
323      * Validates the parameter name.
324      *
325      * @param string $name
326      * @throws invalid_coding_exception
327      */
328     protected function validate_option_name($name) {
330         if (empty($name)) {
331             throw new invalid_coding_exception('Invalid empty option name.');
332         }
334         $meta = $this->get_option_info($name);
335         if (empty($meta)) {
336             throw new invalid_coding_exception('Invalid option name: '.$name);
337         }
338     }
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      */
347     protected function get_required_option($name) {
348         if ($this->inputprovider->has_option($name)) {
349             return $this->inputprovider->get_option($name);
350         } else {
351             throw new missing_option_exception('Missing required option: '.$name);
352         }
353     }
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      */
363     protected function get_optional_option($name, $default) {
364         if ($this->inputprovider->has_option($name)) {
365             return $this->inputprovider->get_option($name);
366         } else {
367             return $default;
368         }
369     }
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  */
379 abstract class input_provider extends singleton_pattern {
381     /** @var array list of all passed valid options */
382     protected $options = array();
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) {
393         if (!$this->has_option($name)) {
394             throw new invalid_coding_exception('Option not passed: '.$name);
395         }
397         return $this->options[$name];
398     }
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     }
410     /**
411      * Initializes the input provider.
412      */
413     protected function initialize() {
414         $this->populate_options();
415     }
417     // End of external API
419     /**
420      * Parses and validates all supported options passed to the script.
421      */
422     protected function populate_options() {
424         $input = input_manager::instance();
425         $raw = $this->parse_raw_options();
426         $cooked = array();
428         foreach ($raw as $k => $v) {
429             if (is_array($v) or is_object($v)) {
430                 // Not supported.
431             }
433             $info = $input->get_option_info($k);
434             if (!$info) {
435                 continue;
436             }
438             $casted = $input->cast_value($v, $info->type);
440             if (!empty($info->shortname)) {
441                 $cooked[$info->shortname] = $casted;
442             }
444             if (!empty($info->longname)) {
445                 $cooked[$info->longname] = $casted;
446             }
447         }
449         // Store the options.
450         $this->options = $cooked;
451     }
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  */
461 class input_cli_provider extends input_provider {
463     /**
464      * Parses raw options passed to the script.
465      *
466      * @return array as returned by getopt()
467      */
468     protected function parse_raw_options() {
470         $input = input_manager::instance();
472         // Signatures of some in-built PHP functions are just crazy, aren't they.
473         $short = '';
474         $long = array();
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         }
488         return getopt($short, $long);
489     }
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  */
499 class input_http_provider extends input_provider {
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() {
507         return $_POST;
508     }
512 // Output handling /////////////////////////////////////////////////////////////
514 /**
515  * Provides output operations.
516  *
517  * @copyright 2012 David Mudrak <david@moodle.com>
518  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
519  */
520 class output_manager extends singleton_pattern {
522     /** @var output_cli_provider|output_http_provider the provider of the output functionality */
523     protected $outputprovider = null;
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     }
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     }
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  */
554 abstract class output_provider extends singleton_pattern {
557 /**
558  * Provides output to the command line.
559  *
560  * @copyright 2012 David Mudrak <david@moodle.com>
561  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
562  */
563 class output_cli_provider extends output_provider {
565     /**
566      * Prints help information in CLI mode.
567      */
568     public function help() {
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     }
587     // End of external API
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     }
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  */
606 class output_http_provider extends output_provider {
608     /**
609      * Prints help on the script usage.
610      */
611     public function help() {
612         // No help available via HTTP
613     }
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     }
630     // End of external API
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     }
651     /**
652      * Produce the HTML page footer
653      */
654     protected function end_output() {
655         echo '</div></body></html>';
656     }
659 // The main class providing all the functionality //////////////////////////////
661 /**
662  * The actual worker class implementing the main functionality of the script.
663  *
664  * @copyright 2012 David Mudrak <david@moodle.com>
665  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
666  */
667 class worker extends singleton_pattern {
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.
673     /** @var input_manager */
674     protected $input = null;
676     /** @var output_manager */
677     protected $output = null;
679     /** @var int the most recent cURL error number, zero for no error */
680     private $curlerrno = null;
682     /** @var string the most recent cURL error message, empty string for no error */
683     private $curlerror = null;
685     /** @var array|false the most recent cURL request info, if it was successful */
686     private $curlinfo = null;
688     /** @var string the full path to the log file */
689     private $logfile = null;
691     /**
692      * Main - the one that actually does something
693      */
694     public function execute() {
696         $this->log('=== MDEPLOY EXECUTION START ===');
698         // Authorize access. None in CLI. Passphrase in HTTP.
699         $this->authorize();
701         // Asking for help in the CLI mode.
702         if ($this->input->get_option('help')) {
703             $this->output->help();
704             $this->done(self::EXIT_HELP);
705         }
707         if ($this->input->get_option('upgrade')) {
708             $this->log('Plugin upgrade requested');
710             // Fetch the ZIP file into a temporary location.
711             $source = $this->input->get_option('package');
712             $target = $this->target_location($source);
713             $this->log('Downloading package '.$source);
715             if ($this->download_file($source, $target)) {
716                 $this->log('Package downloaded into '.$target);
717             } else {
718                 $this->log('cURL error ' . $this->curlerrno . ' ' . $this->curlerror);
719                 $this->log('Unable to download the file');
720                 throw new download_file_exception('Unable to download the package');
721             }
723             // Compare MD5 checksum of the ZIP file
724             $md5remote = $this->input->get_option('md5');
725             $md5local = md5_file($target);
727             if ($md5local !== $md5remote) {
728                 $this->log('MD5 checksum failed. Expected: '.$md5remote.' Got: '.$md5local);
729                 throw new checksum_exception('MD5 checksum failed');
730             }
731             $this->log('MD5 checksum ok');
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);
739             $this->log('Current plugin code location: '.$sourcelocation);
740             $this->log('Moving the current code into archive: '.$backuplocation);
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             }
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             }
755             // Unzip the plugin package file into the target location.
756             $this->unzip_plugin($target, $plugintyperoot, $sourcelocation, $backuplocation);
757             $this->log('Package successfully extracted');
759             // Redirect to the given URL (in HTTP) or exit (in CLI).
760             $this->done();
762         } else if ($this->input->get_option('install')) {
763             // Installing a new plugin not implemented yet.
764         }
766         // Print help in CLI by default.
767         $this->output->help();
768         $this->done(self::EXIT_UNKNOWN_ACTION);
769     }
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     }
780     /**
781      * Initialize the worker class.
782      */
783     protected function initialize() {
784         $this->input = input_manager::instance();
785         $this->output = output_manager::instance();
786     }
788     // End of external API
790     /**
791      * Finish this script execution.
792      *
793      * @param int $exitcode
794      */
795     protected function done($exitcode = self::EXIT_OK) {
797         if (PHP_SAPI === 'cli') {
798             exit($exitcode);
800         } else {
801             $returnurl = $this->input->get_option('returnurl');
802             $this->redirect($returnurl);
803             exit($exitcode);
804         }
805     }
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() {
818         if (PHP_SAPI === 'cli') {
819             $this->log('Successfully authorized using the CLI SAPI');
820             return;
821         }
823         $dataroot = $this->input->get_option('dataroot');
824         $passfile = $this->input->get_option('passfile');
825         $password = $this->input->get_option('password');
827         $passpath = $dataroot.'/mdeploy/auth/'.$passfile;
829         if (!is_readable($passpath)) {
830             throw new unauthorized_access_exception('Unable to read the passphrase file.');
831         }
833         $stored = file($passpath, FILE_IGNORE_NEW_LINES);
835         // "This message will self-destruct in five seconds." -- Mission Commander Swanbeck, Mission: Impossible II
836         unlink($passpath);
838         if (is_readable($passpath)) {
839             throw new unauthorized_access_exception('Unable to remove the passphrase file.');
840         }
842         if (count($stored) < 2) {
843             throw new unauthorized_access_exception('Invalid format of the passphrase file.');
844         }
846         if (time() - (int)$stored[1] > 30 * 60) {
847             throw new unauthorized_access_exception('Passphrase timeout.');
848         }
850         if (strlen($stored[0]) < 24) {
851             throw new unauthorized_access_exception('Session passphrase not long enough.');
852         }
854         if ($password !== $stored[0]) {
855             throw new unauthorized_access_exception('Session passphrase does not match the stored one.');
856         }
858         $this->log('Successfully authorized using the passphrase file');
859     }
861     /**
862      * Returns the full path to the log file.
863      *
864      * @return string
865      */
866     protected function log_location() {
868         if (!is_null($this->logfile)) {
869             return $this->logfile;
870         }
872         $dataroot = $this->input->get_option('dataroot', '');
874         if (empty($dataroot)) {
875             $this->logfile = false;
876             return $this->logfile;
877         }
879         $myroot = $dataroot.'/mdeploy';
881         if (!is_dir($myroot)) {
882             mkdir($myroot, 02777, true);
883         }
885         $this->logfile = $myroot.'/mdeploy.log';
886         return $this->logfile;
887     }
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) {
897         $dataroot = $this->input->get_option('dataroot');
898         $pool = $dataroot.'/mdeploy/var';
900         if (!is_dir($pool)) {
901             mkdir($pool, 02777, true);
902         }
904         $target = $pool.'/'.md5($source);
906         $suffix = 0;
907         while (file_exists($target.'.'.$suffix.'.zip')) {
908             $suffix++;
909         }
911         return $target.'.'.$suffix.'.zip';
912     }
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) {
922         $dataroot = $this->input->get_option('dataroot');
923         $pool = $dataroot.'/mdeploy/archive';
925         if (!is_dir($pool)) {
926             mkdir($pool, 02777, true);
927         }
929         $target = $pool.'/'.basename($path).'_'.time();
931         $suffix = 0;
932         while (file_exists($target.'.'.$suffix)) {
933             $suffix++;
934         }
936         return $target.'.'.$suffix;
937     }
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) {
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)) {
958             $this->log('Unable to init cURL.');
959             return false;
960         }
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);
970         $targetfile = fopen($target, 'w');
972         if (!$targetfile) {
973             throw new download_file_exception('Unable to create local file '.$target);
974         }
976         curl_setopt($ch, CURLOPT_FILE, $targetfile);
978         $result = curl_exec($ch);
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         }
986         fclose($targetfile);
988         $this->curlerrno = curl_errno($ch);
989         $this->curlerror = curl_error($ch);
990         $this->curlinfo = curl_getinfo($ch);
992         if (!$result or $this->curlerrno) {
993             return false;
995         } else if (is_array($this->curlinfo) and (empty($this->curlinfo['http_code']) or $this->curlinfo['http_code'] != 200)) {
996             return false;
997         }
999         return true;
1000     }
1002     /**
1003      * Log a message
1004      *
1005      * @param string $message
1006      */
1007     protected function log($message) {
1009         $logpath = $this->log_location();
1011         if (empty($logpath)) {
1012             // no logging available
1013             return;
1014         }
1016         $f = fopen($logpath, 'ab');
1018         if ($f === false) {
1019             throw new filesystem_exception('Unable to open the log file for appending');
1020         }
1022         $message = $this->format_log_message($message);
1024         fwrite($f, $message);
1026         fclose($f);
1027     }
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) {
1037         $msg = trim($msg);
1038         $timestamp = date("Y-m-d H:i:s");
1040         return $timestamp . ' '. $msg . PHP_EOL;
1041     }
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) {
1051         if (!is_writable($source)) {
1052             return false;
1053         }
1055         if (is_dir($source)) {
1056             $handle = opendir($source);
1057         } else {
1058             return false;
1059         }
1061         $result = true;
1063         while ($filename = readdir($handle)) {
1064             $sourcepath = $source.'/'.$filename;
1066             if ($filename === '.' or $filename === '..') {
1067                 continue;
1068             }
1070             if (is_dir($sourcepath)) {
1071                 $result = $result && $this->move_directory_source_precheck($sourcepath);
1073             } else {
1074                 $result = $result && is_writable($sourcepath);
1075             }
1076         }
1078         closedir($handle);
1080         return $result;
1081     }
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) {
1091         if (file_exists($target)) {
1092             return false;
1093         }
1095         $result = mkdir($target, 02777) && rmdir($target);
1097         return $result;
1098     }
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) {
1109         if (file_exists($target)) {
1110             throw new filesystem_exception('Unable to move the directory - target location already exists');
1111         }
1113         if (is_dir($source)) {
1114             $handle = opendir($source);
1115         } else {
1116             throw new filesystem_exception('Source location is not a directory');
1117         }
1119         mkdir($target, 02777);
1121         while ($filename = readdir($handle)) {
1122             $sourcepath = $source.'/'.$filename;
1123             $targetpath = $target.'/'.$filename;
1125             if ($filename === '.' or $filename === '..') {
1126                 continue;
1127             }
1129             if (is_dir($sourcepath)) {
1130                 $this->move_directory($sourcepath, $targetpath);
1132             } else {
1133                 rename($sourcepath, $targetpath);
1134             }
1135         }
1137         closedir($handle);
1138         return rmdir($source);
1139     }
1141     /**
1142      * Deletes the given directory recursively
1143      *
1144      * @param string $path full path to the directory
1145      */
1146     protected function remove_directory($path) {
1148         if (!file_exists($path)) {
1149             return;
1150         }
1152         if (is_dir($path)) {
1153             $handle = opendir($path);
1154         } else {
1155             throw new filesystem_exception('Given path is not a directory');
1156         }
1158         while ($filename = readdir($handle)) {
1159             $filepath = $path.'/'.$filename;
1161             if ($filename === '.' or $filename === '..') {
1162                 continue;
1163             }
1165             if (is_dir($filepath)) {
1166                 $this->remove_directory($filepath);
1168             } else {
1169                 unlink($filepath);
1170             }
1171         }
1173         closedir($handle);
1174         return rmdir($path);
1175     }
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) {
1187         $zip = new ZipArchive();
1188         $result = $zip->open($ziplocation);
1190         if ($result !== true) {
1191             $this->move_directory($backuplocation, $expectedlocation);
1192             throw new zip_exception('Unable to open the zip package');
1193         }
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         }
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         }
1214         $zip->close();
1215         unlink($ziplocation);
1216     }
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     }
1230 /**
1231  * Provides exception handlers for this script
1232  */
1233 class exception_handlers {
1235     /**
1236      * Sets the exception handler
1237      *
1238      *
1239      * @param string $handler name
1240      */
1241     public static function set_handler($handler) {
1243         if (PHP_SAPI === 'cli') {
1244             // No custom handler available for CLI mode.
1245             set_exception_handler(null);
1246             return;
1247         }
1249         set_exception_handler('exception_handlers::'.$handler.'_exception_handler');
1250     }
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) {
1264         $mydir = dirname(__FILE__).'/';
1265         $text = $e->__toString();
1266         $text = str_replace($mydir, '', $text);
1267         return $text;
1268     }
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     }
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) {
1293         $worker = worker::instance();
1294         $worker->log_exception($e);
1296         $output = output_manager::instance();
1297         $output->exception($e);
1298     }
1301 ////////////////////////////////////////////////////////////////////////////////
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__'
1306 if (!debug_backtrace()) {
1307     // We are executed by the SAPI.
1308     exception_handlers::set_handler('bootstrap');
1309     // Initialize the worker class to actually make the job.
1310     $worker = worker::instance();
1311     exception_handlers::set_handler('default');
1313     // Lights, Camera, Action!
1314     $worker->execute();
1316 } else {
1317     // We are included - probably by some unit testing framework. Do nothing.