26c5caa1970e8956962c1e91a9b450ae54fcaad3
[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 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.
26  *
27  * CLI usage example:
28  *
29  *  $ sudo -u apache php mdeploy.php --install \
30  *                                   --package=https://moodle.org/plugins/download.php/...zip \
31  *                                   --typeroot=/var/www/moodle/htdocs/blocks
32  *                                   --name=loancalc
33  *                                   --md5=...
34  *
35  *  $ sudo -u apache php mdeploy.php --upgrade \
36  *                                   --package=https://moodle.org/plugins/download.php/...zip \
37  *                                   --typeroot=/var/www/moodle/htdocs/blocks
38  *                                   --name=loancalc
39  *                                   --md5=...
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.
49  *
50  * @package     core
51  * @subpackage  mdeploy
52  * @version     1.1
53  * @copyright   2012 David Mudrak <david@moodle.com>
54  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
55  */
57 if (defined('MOODLE_INTERNAL')) {
58     die('This is a standalone utility that should not be included by any other Moodle code.');
59 }
61 // This stops immediately at the beginning of lib/setup.php.
62 define('ABORT_AFTER_CONFIG', true);
63 if (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 }
67 require(__DIR__ . '/config.php');
69 // Exceptions //////////////////////////////////////////////////////////////////
71 class invalid_coding_exception extends Exception {}
72 class missing_option_exception extends Exception {}
73 class invalid_option_exception extends Exception {}
74 class unauthorized_access_exception extends Exception {}
75 class download_file_exception extends Exception {}
76 class backup_folder_exception extends Exception {}
77 class zip_exception extends Exception {}
78 class filesystem_exception extends Exception {}
79 class checksum_exception extends Exception {}
80 class invalid_setting_exception extends Exception {}
83 // Various support classes /////////////////////////////////////////////////////
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  */
91 abstract class singleton_pattern {
93     /** @var array singleton_pattern instances */
94     protected static $singletoninstances = array();
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     }
113     /**
114      * Optional post-instantiation code.
115      */
116     protected function initialize() {
117         // Do nothing in this base class.
118     }
120     /**
121      * Direct instantiation not allowed, use the factory method {@link instance()}
122      */
123     final protected function __construct() {
124     }
126     /**
127      * Sorry, this is singleton.
128      */
129     final protected function __clone() {
130     }
134 // User input handling /////////////////////////////////////////////////////////
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  */
145 class input_manager extends singleton_pattern {
147     const TYPE_FILE         = 'file';   // File name
148     const TYPE_FLAG         = 'flag';   // No value, just a flag (switch)
149     const TYPE_INT          = 'int';    // Integer
150     const TYPE_PATH         = 'path';   // Full path to a file or a directory
151     const TYPE_RAW          = 'raw';    // Raw value, keep as is
152     const TYPE_URL          = 'url';    // URL to a file
153     const TYPE_PLUGIN       = 'plugin'; // Plugin name
154     const TYPE_MD5          = 'md5';    // MD5 hash
156     /** @var input_cli_provider|input_http_provider the provider of the input */
157     protected $inputprovider = null;
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') {
180         $this->validate_option_name($name);
182         $info = $this->get_option_info($name);
184         if ($info->type === input_manager::TYPE_FLAG) {
185             return $this->inputprovider->has_option($name);
186         }
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     }
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) {
203         $supportedoptions = array(
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)'),
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\')'),
209             array('', 'returnurl', input_manager::TYPE_URL, 'Return URL (HTTP access only)'),
210             array('h', 'help', input_manager::TYPE_FLAG, 'Prints usage information'),
211             array('i', 'install', input_manager::TYPE_FLAG, 'Installation mode'),
212             array('m', 'md5', input_manager::TYPE_MD5, 'Expected MD5 hash of the ZIP package to deploy'),
213             array('n', 'name', input_manager::TYPE_PLUGIN, 'Plugin name (the name of its folder)'),
214             array('p', 'package', input_manager::TYPE_URL, 'URL to the ZIP package to deploy'),
215             array('r', 'typeroot', input_manager::TYPE_PATH, 'Full path of the container for this plugin type'),
216             array('u', 'upgrade', input_manager::TYPE_FLAG, 'Upgrade mode'),
217         );
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         }
232         $found = false;
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         }
250         if (!$found) {
251             return false;
252         }
254         $info = new stdClass();
255         $info->shortname = $found[0];
256         $info->longname = $found[1];
257         $info->type = $found[2];
258         $info->desc = $found[3];
260         return $info;
261     }
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) {
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         }
278         switch ($type) {
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;
288             case input_manager::TYPE_FLAG:
289                 return true;
291             case input_manager::TYPE_INT:
292                 return (int)$raw;
294             case input_manager::TYPE_PATH:
295                 if (strpos($raw, '~') !== false) {
296                     throw new invalid_option_exception('Using the tilde (~) character in paths is not supported');
297                 }
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                 }
307                 $raw = str_replace('\\', '/', $raw);
308                 $raw = preg_replace('~[[:cntrl:]]|[&<>"`\|\']~u', '', $raw);
309                 $raw = preg_replace('~\.\.+~', '', $raw);
310                 $raw = preg_replace('~//+~', '/', $raw);
311                 $raw = preg_replace('~/(\./)+~', '/', $raw);
312                 return $raw;
314             case input_manager::TYPE_RAW:
315                 return $raw;
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
325                 if (preg_match('#'.$regex.'#i', $raw)) {
326                     return $raw;
327                 } else {
328                     throw new invalid_option_exception('Not a valid URL');
329                 }
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;
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;
346             default:
347                 throw new invalid_coding_exception('Unknown option type.');
349         }
350     }
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     }
363     // End of external API
365     /**
366      * Validates the parameter name.
367      *
368      * @param string $name
369      * @throws invalid_coding_exception
370      */
371     protected function validate_option_name($name) {
373         if (empty($name)) {
374             throw new invalid_coding_exception('Invalid empty option name.');
375         }
377         $meta = $this->get_option_info($name);
378         if (empty($meta)) {
379             throw new invalid_coding_exception('Invalid option name: '.$name);
380         }
381     }
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      */
390     protected function get_required_option($name) {
391         if ($this->inputprovider->has_option($name)) {
392             return $this->inputprovider->get_option($name);
393         } else {
394             throw new missing_option_exception('Missing required option: '.$name);
395         }
396     }
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      */
406     protected function get_optional_option($name, $default) {
407         if ($this->inputprovider->has_option($name)) {
408             return $this->inputprovider->get_option($name);
409         } else {
410             return $default;
411         }
412     }
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  */
422 abstract class input_provider extends singleton_pattern {
424     /** @var array list of all passed valid options */
425     protected $options = array();
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) {
436         if (!$this->has_option($name)) {
437             throw new invalid_coding_exception('Option not passed: '.$name);
438         }
440         return $this->options[$name];
441     }
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     }
453     /**
454      * Initializes the input provider.
455      */
456     protected function initialize() {
457         $this->populate_options();
458     }
460     // End of external API
462     /**
463      * Parses and validates all supported options passed to the script.
464      */
465     protected function populate_options() {
467         $input = input_manager::instance();
468         $raw = $this->parse_raw_options();
469         $cooked = array();
471         foreach ($raw as $k => $v) {
472             if (is_array($v) or is_object($v)) {
473                 // Not supported.
474             }
476             $info = $input->get_option_info($k);
477             if (!$info) {
478                 continue;
479             }
481             $casted = $input->cast_value($v, $info->type);
483             if (!empty($info->shortname)) {
484                 $cooked[$info->shortname] = $casted;
485             }
487             if (!empty($info->longname)) {
488                 $cooked[$info->longname] = $casted;
489             }
490         }
492         // Store the options.
493         $this->options = $cooked;
494     }
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  */
504 class input_cli_provider extends input_provider {
506     /**
507      * Parses raw options passed to the script.
508      *
509      * @return array as returned by getopt()
510      */
511     protected function parse_raw_options() {
513         $input = input_manager::instance();
515         // Signatures of some in-built PHP functions are just crazy, aren't they.
516         $short = '';
517         $long = array();
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         }
531         return getopt($short, $long);
532     }
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  */
542 class input_http_provider extends input_provider {
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() {
550         return $_POST;
551     }
555 // Output handling /////////////////////////////////////////////////////////////
557 /**
558  * Provides output operations.
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_manager extends singleton_pattern {
565     /** @var output_cli_provider|output_http_provider the provider of the output functionality */
566     protected $outputprovider = null;
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     }
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     }
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  */
597 abstract class output_provider extends singleton_pattern {
600 /**
601  * Provides output to the command line.
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_cli_provider extends output_provider {
608     /**
609      * Prints help information in CLI mode.
610      */
611     public function help() {
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     }
630     // End of external API
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     }
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  */
649 class output_http_provider extends output_provider {
651     /**
652      * Prints help on the script usage.
653      */
654     public function help() {
655         // No help available via HTTP
656     }
658     /**
659      * Display the information about uncaught exception
660      *
661      * @param Exception $e uncaught exception
662      */
663     public function exception(Exception $e) {
665         $docslink = 'http://docs.moodle.org/en/admin/mdeploy/'.get_class($e);
666         $this->start_output();
667         echo('<h1>Oops! It did it again</h1>');
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>');
670         echo('<pre>');
671         echo exception_handlers::format_exception_info($e);
672         echo('</pre>');
673         $this->end_output();
674     }
676     // End of external API
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     }
697     /**
698      * Produce the HTML page footer
699      */
700     protected function end_output() {
701         echo '</div></body></html>';
702     }
705 // The main class providing all the functionality //////////////////////////////
707 /**
708  * The actual worker class implementing the main functionality of the script.
709  *
710  * @copyright 2012 David Mudrak <david@moodle.com>
711  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
712  */
713 class worker extends singleton_pattern {
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.
719     /** @var input_manager */
720     protected $input = null;
722     /** @var output_manager */
723     protected $output = null;
725     /** @var int the most recent cURL error number, zero for no error */
726     private $curlerrno = null;
728     /** @var string the most recent cURL error message, empty string for no error */
729     private $curlerror = null;
731     /** @var array|false the most recent cURL request info, if it was successful */
732     private $curlinfo = null;
734     /** @var string the full path to the log file */
735     private $logfile = null;
737     /** @var array the whitelisted config options which can be queried. */
738     private $validconfigoptions = array(
739         'dirroot'       => true,
740         'dataroot'      => true,
741     );
743     /**
744      * Main - the one that actually does something
745      */
746     public function execute() {
748         $this->log('=== MDEPLOY EXECUTION START ===');
750         // Authorize access. None in CLI. Passphrase in HTTP.
751         $this->authorize();
753         // Asking for help in the CLI mode.
754         if ($this->input->get_option('help')) {
755             $this->output->help();
756             $this->done(self::EXIT_HELP);
757         }
759         if ($this->input->get_option('upgrade')) {
760             $this->log('Plugin upgrade requested');
762             // Fetch the ZIP file into a temporary location.
763             $source = $this->input->get_option('package');
764             $target = $this->target_location($source);
765             $this->log('Downloading package '.$source);
767             if ($this->download_file($source, $target)) {
768                 $this->log('Package downloaded into '.$target);
769             } else {
770                 $this->log('cURL error ' . $this->curlerrno . ' ' . $this->curlerror);
771                 $this->log('Unable to download the file from ' . $source . ' into ' . $target);
772                 throw new download_file_exception('Unable to download the package');
773             }
775             // Compare MD5 checksum of the ZIP file
776             $md5remote = $this->input->get_option('md5');
777             $md5local = md5_file($target);
779             if ($md5local !== $md5remote) {
780                 $this->log('MD5 checksum failed. Expected: '.$md5remote.' Got: '.$md5local);
781                 throw new checksum_exception('MD5 checksum failed');
782             }
783             $this->log('MD5 checksum ok');
785             // Check that the specified typeroot is within the current site's dirroot.
786             $plugintyperoot = $this->input->get_option('typeroot');
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             }
791             // Backup the current version of the plugin
792             $pluginname = $this->input->get_option('name');
793             $sourcelocation = $plugintyperoot.'/'.$pluginname;
794             $backuplocation = $this->backup_location($sourcelocation);
796             $this->log('Current plugin code location: '.$sourcelocation);
797             $this->log('Moving the current code into archive: '.$backuplocation);
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                 }
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                 }
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                 }
818             }
820             // Unzip the plugin package file into the target location.
821             $this->unzip_plugin($target, $plugintyperoot, $sourcelocation, $backuplocation);
822             $this->log('Package successfully extracted');
824             // Redirect to the given URL (in HTTP) or exit (in CLI).
825             $this->done();
827         } else if ($this->input->get_option('install')) {
828             $this->log('Plugin installation requested');
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');
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             }
839             // Check if the plugin location if available for us.
840             $pluginlocation = $plugintyperoot.'/'.$pluginname;
842             $this->log('New plugin code location: '.$pluginlocation);
844             if (file_exists($pluginlocation)) {
845                 throw new filesystem_exception('Unable to prepare the plugin location (directory already exists)');
846             }
848             if (!$this->create_directory_precheck($pluginlocation)) {
849                 throw new filesystem_exception('Unable to prepare the plugin location (cannot create new directory)');
850             }
852             // Fetch the ZIP file into a temporary location.
853             $target = $this->target_location($source);
854             $this->log('Downloading package '.$source);
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             }
864             // Compare MD5 checksum of the ZIP file
865             $md5local = md5_file($target);
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');
873             // Unzip the plugin package file into the plugin location.
874             $this->unzip_plugin($target, $plugintyperoot, $pluginlocation, false);
875             $this->log('Package successfully extracted');
877             // Redirect to the given URL (in HTTP) or exit (in CLI).
878             $this->done();
879         }
881         // Print help in CLI by default.
882         $this->output->help();
883         $this->done(self::EXIT_UNKNOWN_ACTION);
884     }
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     }
895     /**
896      * Initialize the worker class.
897      */
898     protected function initialize() {
899         $this->input = input_manager::instance();
900         $this->output = output_manager::instance();
901     }
903     // End of external API
905     /**
906      * Finish this script execution.
907      *
908      * @param int $exitcode
909      */
910     protected function done($exitcode = self::EXIT_OK) {
912         if (PHP_SAPI === 'cli') {
913             exit($exitcode);
915         } else {
916             $returnurl = $this->input->get_option('returnurl');
917             $this->redirect($returnurl);
918             exit($exitcode);
919         }
920     }
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() {
932         if (PHP_SAPI === 'cli') {
933             $this->log('Successfully authorized using the CLI SAPI');
934             return;
935         }
937         $passfile = $this->input->get_option('passfile');
938         $password = $this->input->get_option('password');
940         $passpath = $this->get_env('dataroot') . '/mdeploy/auth/' . $passfile;
942         if (!is_readable($passpath)) {
943             throw new unauthorized_access_exception('Unable to read the passphrase file.');
944         }
946         $stored = file($passpath, FILE_IGNORE_NEW_LINES);
948         // "This message will self-destruct in five seconds." -- Mission Commander Swanbeck, Mission: Impossible II
949         unlink($passpath);
951         if (is_readable($passpath)) {
952             throw new unauthorized_access_exception('Unable to remove the passphrase file.');
953         }
955         if (count($stored) < 2) {
956             throw new unauthorized_access_exception('Invalid format of the passphrase file.');
957         }
959         if (time() - (int)$stored[1] > 30 * 60) {
960             throw new unauthorized_access_exception('Passphrase timeout.');
961         }
963         if (strlen($stored[0]) < 24) {
964             throw new unauthorized_access_exception('Session passphrase not long enough.');
965         }
967         if ($password !== $stored[0]) {
968             throw new unauthorized_access_exception('Session passphrase does not match the stored one.');
969         }
971         $this->log('Successfully authorized using the passphrase file');
972     }
974     /**
975      * Returns the full path to the log file.
976      *
977      * @return string
978      */
979     protected function log_location() {
980         if (!is_null($this->logfile)) {
981             return $this->logfile;
982         }
984         $dataroot = $this->get_env('dataroot');
986         if (empty($dataroot)) {
987             $this->logfile = false;
988             return $this->logfile;
989         }
991         $myroot = $dataroot.'/mdeploy';
993         if (!is_dir($myroot)) {
994             mkdir($myroot, 02777, true);
995         }
997         $this->logfile = $myroot.'/mdeploy.log';
998         return $this->logfile;
999     }
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) {
1008         $dataroot = $this->get_env('dataroot');
1009         $pool = $dataroot.'/mdeploy/var';
1011         if (!is_dir($pool)) {
1012             mkdir($pool, 02777, true);
1013         }
1015         $target = $pool.'/'.md5($source);
1017         $suffix = 0;
1018         while (file_exists($target.'.'.$suffix.'.zip')) {
1019             $suffix++;
1020         }
1022         return $target.'.'.$suffix.'.zip';
1023     }
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) {
1032         $dataroot = $this->get_env('dataroot');
1033         $pool = $dataroot.'/mdeploy/archive';
1035         if (!is_dir($pool)) {
1036             mkdir($pool, 02777, true);
1037         }
1039         $target = $pool.'/'.basename($path).'_'.time();
1041         $suffix = 0;
1042         while (file_exists($target.'.'.$suffix)) {
1043             $suffix++;
1044         }
1046         return $target.'.'.$suffix;
1047     }
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) {
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)) {
1068             $this->log('Unable to init cURL.');
1069             return false;
1070         }
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);
1079         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Allow redirection, we trust in ssl.
1080         curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
1082         if ($cacertfile = $this->get_cacert()) {
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);
1087         } else {
1088             $this->log('Using operating system CA certificates.');
1089         }
1091         $proxy = $this->input->get_option('proxy', false);
1092         if (!empty($proxy)) {
1093             curl_setopt($ch, CURLOPT_PROXY, $proxy);
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             }
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         }
1112         $targetfile = fopen($target, 'w');
1114         if (!$targetfile) {
1115             throw new download_file_exception('Unable to create local file '.$target);
1116         }
1118         curl_setopt($ch, CURLOPT_FILE, $targetfile);
1120         $result = curl_exec($ch);
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         }
1128         fclose($targetfile);
1130         $this->curlerrno = curl_errno($ch);
1131         $this->curlerror = curl_error($ch);
1132         $this->curlinfo = curl_getinfo($ch);
1134         if (!$result or $this->curlerrno) {
1135             $this->log('Curl Error.');
1136             return false;
1138         } else if (is_array($this->curlinfo) and (empty($this->curlinfo['http_code']) or ($this->curlinfo['http_code'] != 200))) {
1139             $this->log('Curl remote error.');
1140             $this->log(print_r($this->curlinfo,true));
1141             return false;
1142         }
1144         return true;
1145     }
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;
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     }
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() {
1172         $dataroot = $this->get_env('dataroot');
1174         // Bundle in dataroot always wins.
1175         if (is_readable($dataroot.'/moodleorgca.crt')) {
1176             return realpath($dataroot.'/moodleorgca.crt');
1177         }
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         }
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         }
1192         // Use default, this should work fine on all properly configured *nix systems.
1193         return null;
1194     }
1196     /**
1197      * Log a message
1198      *
1199      * @param string $message
1200      */
1201     protected function log($message) {
1203         $logpath = $this->log_location();
1205         if (empty($logpath)) {
1206             // no logging available
1207             return;
1208         }
1210         $f = fopen($logpath, 'ab');
1212         if ($f === false) {
1213             throw new filesystem_exception('Unable to open the log file for appending');
1214         }
1216         $message = $this->format_log_message($message);
1218         fwrite($f, $message);
1220         fclose($f);
1221     }
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) {
1231         $msg = trim($msg);
1232         $timestamp = date("Y-m-d H:i:s");
1234         return $timestamp . ' '. $msg . PHP_EOL;
1235     }
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) {
1245         if (!is_writable($source)) {
1246             return false;
1247         }
1249         if (is_dir($source)) {
1250             $handle = opendir($source);
1251         } else {
1252             return false;
1253         }
1255         $result = true;
1257         while ($filename = readdir($handle)) {
1258             $sourcepath = $source.'/'.$filename;
1260             if ($filename === '.' or $filename === '..') {
1261                 continue;
1262             }
1264             if (is_dir($sourcepath)) {
1265                 $result = $result && $this->move_directory_source_precheck($sourcepath);
1267             } else {
1268                 $result = $result && is_writable($sourcepath);
1269             }
1270         }
1272         closedir($handle);
1274         return $result;
1275     }
1277     /**
1278      * Checks to see if a source folder could be safely moved into the given new location
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) {
1285         // Check if the target folder does not exist yet, can be created
1286         // and removed again.
1287         $result = $this->create_directory_precheck($target);
1289         // At the moment, it seems to be enough to check. We may want to add
1290         // more steps in the future.
1292         return $result;
1293     }
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) {
1303         if (file_exists($path)) {
1304             return false;
1305         }
1307         $result = mkdir($path, 02777) && rmdir($path);
1309         return $result;
1310     }
1312     /**
1313      * Moves the given source into a new location recursively
1314      *
1315      * The target location can not exist.
1316      *
1317      * @param string $source full path to the existing directory
1318      * @param string $destination full path to the new location of the folder
1319      * @param bool $keepsourceroot should the root of the $source be kept or removed at the end
1320      * @return bool
1321      */
1322     protected function move_directory($source, $target, $keepsourceroot = false) {
1324         if (file_exists($target)) {
1325             throw new filesystem_exception('Unable to move the directory - target location already exists');
1326         }
1328         return $this->move_directory_into($source, $target, $keepsourceroot);
1329     }
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) {
1343         if (is_dir($source)) {
1344             $handle = opendir($source);
1345         } else {
1346             throw new filesystem_exception('Source location is not a directory');
1347         }
1349         if (is_dir($target)) {
1350             $result = true;
1351         } else {
1352             $result = mkdir($target, 02777);
1353         }
1355         while ($filename = readdir($handle)) {
1356             $sourcepath = $source.'/'.$filename;
1357             $targetpath = $target.'/'.$filename;
1359             if ($filename === '.' or $filename === '..') {
1360                 continue;
1361             }
1363             if (is_dir($sourcepath)) {
1364                 $result = $result && $this->move_directory($sourcepath, $targetpath, false);
1366             } else {
1367                 $result = $result && rename($sourcepath, $targetpath);
1368             }
1369         }
1371         closedir($handle);
1373         if (!$keepsourceroot) {
1374             $result = $result && rmdir($source);
1375         }
1377         clearstatcache();
1379         return $result;
1380     }
1382     /**
1383      * Deletes the given directory recursively
1384      *
1385      * @param string $path full path to the directory
1386      * @param bool $keeppathroot should the root of the $path be kept (i.e. remove the content only) or removed too
1387      * @return bool
1388      */
1389     protected function remove_directory($path, $keeppathroot = false) {
1391         $result = true;
1393         if (!file_exists($path)) {
1394             return $result;
1395         }
1397         if (is_dir($path)) {
1398             $handle = opendir($path);
1399         } else {
1400             throw new filesystem_exception('Given path is not a directory');
1401         }
1403         while ($filename = readdir($handle)) {
1404             $filepath = $path.'/'.$filename;
1406             if ($filename === '.' or $filename === '..') {
1407                 continue;
1408             }
1410             if (is_dir($filepath)) {
1411                 $result = $result && $this->remove_directory($filepath, false);
1413             } else {
1414                 $result = $result && unlink($filepath);
1415             }
1416         }
1418         closedir($handle);
1420         if (!$keeppathroot) {
1421             $result = $result && rmdir($path);
1422         }
1424         clearstatcache();
1426         return $result;
1427     }
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
1435      * @param string|bool $backuplocation location of the previous version of the plugin or false for no backup
1436      */
1437     protected function unzip_plugin($ziplocation, $plugintyperoot, $expectedlocation, $backuplocation) {
1439         $zip = new ZipArchive();
1440         $result = $zip->open($ziplocation);
1442         if ($result !== true) {
1443             if ($backuplocation !== false) {
1444                 $this->move_directory($backuplocation, $expectedlocation);
1445             }
1446             throw new zip_exception('Unable to open the zip package');
1447         }
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         }
1461         if (!$zip->extractTo($plugintyperoot)) {
1462             $zip->close();
1463             $this->remove_directory($expectedlocation, true); // just in case something was created
1464             if ($backuplocation !== false) {
1465                 $this->move_directory_into($backuplocation, $expectedlocation);
1466             }
1467             throw new zip_exception('Unable to extract the zip package');
1468         }
1470         $zip->close();
1471         unlink($ziplocation);
1472     }
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     }
1486 /**
1487  * Provides exception handlers for this script
1488  */
1489 class exception_handlers {
1491     /**
1492      * Sets the exception handler
1493      *
1494      *
1495      * @param string $handler name
1496      */
1497     public static function set_handler($handler) {
1499         if (PHP_SAPI === 'cli') {
1500             // No custom handler available for CLI mode.
1501             set_exception_handler(null);
1502             return;
1503         }
1505         set_exception_handler('exception_handlers::'.$handler.'_exception_handler');
1506     }
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) {
1520         $mydir = dirname(__FILE__).'/';
1521         $text = $e->__toString();
1522         $text = str_replace($mydir, '', $text);
1523         return $text;
1524     }
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     }
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) {
1549         $worker = worker::instance();
1550         $worker->log_exception($e);
1552         $output = output_manager::instance();
1553         $output->exception($e);
1554     }
1557 ////////////////////////////////////////////////////////////////////////////////
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__'
1562 if (!debug_backtrace()) {
1563     // We are executed by the SAPI.
1564     exception_handlers::set_handler('bootstrap');
1565     // Initialize the worker class to actually make the job.
1566     $worker = worker::instance();
1567     exception_handlers::set_handler('default');
1569     // Lights, Camera, Action!
1570     $worker->execute();
1572 } else {
1573     // We are included - probably by some unit testing framework. Do nothing.