MDL-35238 Compare the ZIP package content hash with the expected value
[moodle.git] / mdeploy.php
blob69b7b06d215cf84bdfa3753f512b06a7442e8085
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.
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
21 * This script looks after deploying available updates to the local Moodle site.
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
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
33 if (defined('MOODLE_INTERNAL')) {
34 die('This is a standalone utility that should not be included by any other Moodle code.');
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.
56 * @copyright 2012 David Mudrak <david@moodle.com>
57 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
59 abstract class singleton_pattern {
61 /** @var array singleton_pattern instances */
62 protected static $singletoninstances = array();
64 /**
65 * Factory method returning the singleton instance.
67 * Subclasses may want to override the {@link self::initialize()} method that is
68 * called right after their instantiation.
70 * @return mixed the singleton instance
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();
78 return static::$singletoninstances[$class];
81 /**
82 * Optional post-instantiation code.
84 protected function initialize() {
85 // Do nothing in this base class.
88 /**
89 * Direct instantiation not allowed, use the factory method {@link instance()}
91 final protected function __construct() {
94 /**
95 * Sorry, this is singleton.
97 final protected function __clone() {
102 // User input handling /////////////////////////////////////////////////////////
105 * Provides access to the script options.
107 * Implements the delegate pattern by dispatching the calls to appropriate
108 * helper class (CLI or HTTP).
110 * @copyright 2012 David Mudrak <david@moodle.com>
111 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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;
128 * Returns the value of an option passed to the script.
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.
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.
138 * The array options are not supported.
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
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);
156 if (func_num_args() == 1) {
157 return $this->get_required_option($name);
158 } else {
159 return $this->get_optional_option($name, $default);
164 * Returns the meta-information about the given option.
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
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'),
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;
195 return $all;
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;
207 } else {
208 // Search by the long option name
209 if ($optioninfo[1] === $name) {
210 $found = $optioninfo;
211 break;
216 if (!$found) {
217 return false;
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;
230 * Casts the value to the given type.
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
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.');
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 = '';
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 $raw = str_replace('\\', '/', $raw);
262 $raw = preg_replace('~[[:cntrl:]]|[&<>"`\|\':]~u', '', $raw);
263 $raw = preg_replace('~\.\.+~', '', $raw);
264 $raw = preg_replace('~//+~', '/', $raw);
265 $raw = preg_replace('~/(\./)+~', '/', $raw);
266 return $raw;
268 case input_manager::TYPE_RAW:
269 return $raw;
271 case input_manager::TYPE_URL:
272 $regex = '^(https?|ftp)\:\/\/'; // protocol
273 $regex .= '([a-z0-9+!*(),;?&=\$_.-]+(\:[a-z0-9+!*(),;?&=\$_.-]+)?@)?'; // optional user and password
274 $regex .= '[a-z0-9+\$_-]+(\.[a-z0-9+\$_-]+)*'; // hostname or IP (one word like http://localhost/ allowed)
275 $regex .= '(\:[0-9]{2,5})?'; // port (optional)
276 $regex .= '(\/([a-z0-9+\$_-]\.?)+)*\/?'; // path to the file
277 $regex .= '(\?[a-z+&\$_.-][a-z0-9;:@/&%=+\$_.-]*)?'; // HTTP params
279 if (preg_match('#'.$regex.'#i', $raw)) {
280 return $raw;
281 } else {
282 throw new invalid_option_exception('Not a valid URL');
285 case input_manager::TYPE_PLUGIN:
286 if (!preg_match('/^[a-z][a-z0-9_]*[a-z0-9]$/', $raw)) {
287 throw new invalid_option_exception('Invalid plugin name');
289 if (strpos($raw, '__') !== false) {
290 throw new invalid_option_exception('Invalid plugin name');
292 return $raw;
294 case input_manager::TYPE_MD5:
295 if (!preg_match('/^[a-f0-9]{32}$/', $raw)) {
296 throw new invalid_option_exception('Invalid MD5 hash format');
298 return $raw;
300 default:
301 throw new invalid_coding_exception('Unknown option type.');
307 * Picks the appropriate helper class to delegate calls to.
309 protected function initialize() {
310 if (PHP_SAPI === 'cli') {
311 $this->inputprovider = input_cli_provider::instance();
312 } else {
313 $this->inputprovider = input_http_provider::instance();
317 // End of external API
320 * Validates the parameter name.
322 * @param string $name
323 * @throws invalid_coding_exception
325 protected function validate_option_name($name) {
327 if (empty($name)) {
328 throw new invalid_coding_exception('Invalid empty option name.');
331 $meta = $this->get_option_info($name);
332 if (empty($meta)) {
333 throw new invalid_coding_exception('Invalid option name: '.$name);
338 * Returns cleaned option value or throws exception.
340 * @param string $name the name of the parameter
341 * @param string $type the parameter type, e.g. {@link input_manager::TYPE_INT}
342 * @return mixed
344 protected function get_required_option($name) {
345 if ($this->inputprovider->has_option($name)) {
346 return $this->inputprovider->get_option($name);
347 } else {
348 throw new missing_option_exception('Missing required option: '.$name);
353 * Returns cleaned option value or the default value
355 * @param string $name the name of the parameter
356 * @param string $type the parameter type, e.g. {@link input_manager::TYPE_INT}
357 * @param mixed $default the default value.
358 * @return mixed
360 protected function get_optional_option($name, $default) {
361 if ($this->inputprovider->has_option($name)) {
362 return $this->inputprovider->get_option($name);
363 } else {
364 return $default;
371 * Base class for input providers.
373 * @copyright 2012 David Mudrak <david@moodle.com>
374 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
376 abstract class input_provider extends singleton_pattern {
378 /** @var array list of all passed valid options */
379 protected $options = array();
382 * Returns the casted value of the option.
384 * @param string $name option name
385 * @throws invalid_coding_exception if the option has not been passed
386 * @return mixed casted value of the option
388 public function get_option($name) {
390 if (!$this->has_option($name)) {
391 throw new invalid_coding_exception('Option not passed: '.$name);
394 return $this->options[$name];
398 * Was the given option passed?
400 * @param string $name optionname
401 * @return bool
403 public function has_option($name) {
404 return array_key_exists($name, $this->options);
408 * Initializes the input provider.
410 protected function initialize() {
411 $this->populate_options();
414 // End of external API
417 * Parses and validates all supported options passed to the script.
419 protected function populate_options() {
421 $input = input_manager::instance();
422 $raw = $this->parse_raw_options();
423 $cooked = array();
425 foreach ($raw as $k => $v) {
426 if (is_array($v) or is_object($v)) {
427 // Not supported.
430 $info = $input->get_option_info($k);
431 if (!$info) {
432 continue;
435 $casted = $input->cast_value($v, $info->type);
437 if (!empty($info->shortname)) {
438 $cooked[$info->shortname] = $casted;
441 if (!empty($info->longname)) {
442 $cooked[$info->longname] = $casted;
446 // Store the options.
447 $this->options = $cooked;
453 * Provides access to the script options passed via CLI.
455 * @copyright 2012 David Mudrak <david@moodle.com>
456 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
458 class input_cli_provider extends input_provider {
461 * Parses raw options passed to the script.
463 * @return array as returned by getopt()
465 protected function parse_raw_options() {
467 $input = input_manager::instance();
469 // Signatures of some in-built PHP functions are just crazy, aren't they.
470 $short = '';
471 $long = array();
473 foreach ($input->get_option_info() as $option) {
474 if ($option->type === input_manager::TYPE_FLAG) {
475 // No value expected for this option.
476 $short .= $option->shortname;
477 $long[] = $option->longname;
478 } else {
479 // A value expected for the option, all considered as optional.
480 $short .= empty($option->shortname) ? '' : $option->shortname.'::';
481 $long[] = empty($option->longname) ? '' : $option->longname.'::';
485 return getopt($short, $long);
491 * Provides access to the script options passed via HTTP request.
493 * @copyright 2012 David Mudrak <david@moodle.com>
494 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
496 class input_http_provider extends input_provider {
499 * Parses raw options passed to the script.
501 * @return array of raw values passed via HTTP request
503 protected function parse_raw_options() {
504 return $_POST;
509 // Output handling /////////////////////////////////////////////////////////////
512 * Provides output operations.
514 * @copyright 2012 David Mudrak <david@moodle.com>
515 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
517 class output_manager extends singleton_pattern {
519 /** @var output_cli_provider|output_http_provider the provider of the output functionality */
520 protected $outputprovider = null;
523 * Magic method triggered when invoking an inaccessible method.
525 * @param string $name method name
526 * @param array $arguments method arguments
528 public function __call($name, array $arguments = array()) {
529 call_user_func_array(array($this->outputprovider, $name), $arguments);
533 * Picks the appropriate helper class to delegate calls to.
535 protected function initialize() {
536 if (PHP_SAPI === 'cli') {
537 $this->outputprovider = output_cli_provider::instance();
538 } else {
539 $this->outputprovider = output_http_provider::instance();
546 * Base class for all output providers.
548 * @copyright 2012 David Mudrak <david@moodle.com>
549 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
551 abstract class output_provider extends singleton_pattern {
555 * Provides output to the command line.
557 * @copyright 2012 David Mudrak <david@moodle.com>
558 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
560 class output_cli_provider extends output_provider {
563 * Prints help information in CLI mode.
565 public function help() {
567 $this->outln('mdeploy.php - Moodle (http://moodle.org) deployment utility');
568 $this->outln();
569 $this->outln('Usage: $ sudo -u apache php mdeploy.php [options]');
570 $this->outln();
571 $input = input_manager::instance();
572 foreach($input->get_option_info() as $info) {
573 $option = array();
574 if (!empty($info->shortname)) {
575 $option[] = '-'.$info->shortname;
577 if (!empty($info->longname)) {
578 $option[] = '--'.$info->longname;
580 $this->outln(sprintf('%-20s %s', implode(', ', $option), $info->desc));
584 // End of external API
587 * Writes a text to the STDOUT followed by a new line character.
589 * @param string $text text to print
591 protected function outln($text='') {
592 fputs(STDOUT, $text.PHP_EOL);
598 * Provides HTML output as a part of HTTP response.
600 * @copyright 2012 David Mudrak <david@moodle.com>
601 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
603 class output_http_provider extends output_provider {
606 * Prints help on the script usage.
608 public function help() {
609 // No help available via HTTP
613 // The main class providing all the functionality //////////////////////////////
616 * The actual worker class implementing the main functionality of the script.
618 * @copyright 2012 David Mudrak <david@moodle.com>
619 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
621 class worker extends singleton_pattern {
623 const EXIT_OK = 0; // Success exit code.
624 const EXIT_HELP = 1; // Explicit help required.
625 const EXIT_UNKNOWN_ACTION = 127; // Neither -i nor -u provided.
627 /** @var input_manager */
628 protected $input = null;
630 /** @var output_manager */
631 protected $output = null;
633 /** @var int the most recent cURL error number, zero for no error */
634 private $curlerrno = null;
636 /** @var string the most recent cURL error message, empty string for no error */
637 private $curlerror = null;
639 /** @var array|false the most recent cURL request info, if it was successful */
640 private $curlinfo = null;
643 * Main - the one that actually does something
645 public function execute() {
647 // Authorize access. None in CLI. Passphrase in HTTP.
648 $this->authorize();
650 // Asking for help in the CLI mode.
651 if ($this->input->get_option('help')) {
652 $this->output->help();
653 $this->done(self::EXIT_HELP);
656 if ($this->input->get_option('upgrade')) {
657 // Fetch the ZIP file into a temporary location.
658 $source = $this->input->get_option('package');
659 $target = $this->target_location($source);
661 if ($this->download_file($source, $target)) {
662 $this->log('ZIP fetched into '.$target);
663 } else {
664 $this->log('cURL error ' . $this->curlerrno . ' ' . $this->curlerror);
665 $this->log('Unable to download the file');
666 throw new download_file_exception('Unable to download the ZIP package');
669 // Compare MD5 checksum of the ZIP file
670 $md5remote = $this->input->get_option('md5');
671 $md5local = md5_file($target);
673 if ($md5local !== $md5remote) {
674 $this->log('MD5 checksum failed. Expected: '.$md5remote.' Got: '.$md5local);
675 throw new checksum_exception('MD5 checksum failed');
678 // Backup the current version of the plugin
679 $plugintyperoot = $this->input->get_option('typeroot');
680 $pluginname = $this->input->get_option('name');
681 $sourcelocation = $plugintyperoot.'/'.$pluginname;
682 $backuplocation = $this->backup_location($sourcelocation);
684 // We don't want to touch files unless we are pretty sure it would be all ok.
685 if (!$this->move_directory_source_precheck($sourcelocation)) {
686 throw new backup_folder_exception('Unable to backup the current version of the plugin (source precheck failed)');
688 if (!$this->move_directory_target_precheck($backuplocation)) {
689 throw new backup_folder_exception('Unable to backup the current version of the plugin (backup precheck failed)');
692 // Looking good, let's try it.
693 if (!$this->move_directory($sourcelocation, $backuplocation)) {
694 throw new backup_folder_exception('Unable to backup the current version of the plugin (moving failed)');
697 // Unzip the plugin package file into the target location.
698 $this->unzip_plugin($target, $plugintyperoot, $sourcelocation, $backuplocation);
700 // Redirect to the given URL (in HTTP) or exit (in CLI).
701 $this->done();
703 } else if ($this->input->get_option('install')) {
704 // Installing a new plugin not implemented yet.
707 // Print help in CLI by default.
708 $this->output->help();
709 $this->done(self::EXIT_UNKNOWN_ACTION);
713 * Initialize the worker class.
715 protected function initialize() {
716 $this->input = input_manager::instance();
717 $this->output = output_manager::instance();
720 // End of external API
723 * Finish this script execution.
725 * @param int $exitcode
727 protected function done($exitcode = self::EXIT_OK) {
729 if (PHP_SAPI === 'cli') {
730 exit($exitcode);
732 } else {
733 $returnurl = $this->input->get_option('returnurl');
734 $this->redirect($returnurl);
735 exit($exitcode);
740 * Authorize access to the script.
742 * In CLI mode, the access is automatically authorized. In HTTP mode, the
743 * passphrase submitted via the request params must match the contents of the
744 * file, the name of which is passed in another parameter.
746 * @throws unauthorized_access_exception
748 protected function authorize() {
750 if (PHP_SAPI === 'cli') {
751 return;
754 $dataroot = $this->input->get_option('dataroot');
755 $passfile = $this->input->get_option('passfile');
756 $password = $this->input->get_option('password');
758 $passpath = $dataroot.'/mdeploy/auth/'.$passfile;
760 if (!is_readable($passpath)) {
761 throw new unauthorized_access_exception('Unable to read the passphrase file.');
764 $stored = file($passpath, FILE_IGNORE_NEW_LINES);
766 // "This message will self-destruct in five seconds." -- Mission Commander Swanbeck, Mission: Impossible II
767 unlink($passpath);
769 if (is_readable($passpath)) {
770 throw new unauthorized_access_exception('Unable to remove the passphrase file.');
773 if (count($stored) < 2) {
774 throw new unauthorized_access_exception('Invalid format of the passphrase file.');
777 if (time() - (int)$stored[1] > 30 * 60) {
778 throw new unauthorized_access_exception('Passphrase timeout.');
781 if (strlen($stored[0]) < 24) {
782 throw new unauthorized_access_exception('Session passphrase not long enough.');
785 if ($password !== $stored[0]) {
786 throw new unauthorized_access_exception('Session passphrase does not match the stored one.');
791 * Choose the target location for the given ZIP's URL.
793 * @param string $source URL
794 * @return string
796 protected function target_location($source) {
798 $dataroot = $this->input->get_option('dataroot');
799 $pool = $dataroot.'/mdeploy/var';
801 if (!is_dir($pool)) {
802 mkdir($pool, 02777, true);
805 $target = $pool.'/'.md5($source);
807 $suffix = 0;
808 while (file_exists($target.'.'.$suffix.'.zip')) {
809 $suffix++;
812 return $target.'.'.$suffix.'.zip';
816 * Choose the location of the current plugin folder backup
818 * @param string $path full path to the current folder
819 * @return string
821 protected function backup_location($path) {
823 $dataroot = $this->input->get_option('dataroot');
824 $pool = $dataroot.'/mdeploy/archive';
826 if (!is_dir($pool)) {
827 mkdir($pool, 02777, true);
830 $target = $pool.'/'.basename($path).'_'.time();
832 $suffix = 0;
833 while (file_exists($target.'.'.$suffix)) {
834 $suffix++;
837 return $target.'.'.$suffix;
841 * Downloads the given file into the given destination.
843 * This is basically a simplified version of {@link download_file_content()} from
844 * Moodle itself, tuned for fetching files from moodle.org servers.
846 * @param string $source file url starting with http(s)://
847 * @param string $target store the downloaded content to this file (full path)
848 * @return bool true on success, false otherwise
849 * @throws download_file_exception
851 protected function download_file($source, $target) {
853 $newlines = array("\r", "\n");
854 $source = str_replace($newlines, '', $source);
855 if (!preg_match('|^https?://|i', $source)) {
856 throw new download_file_exception('Unsupported transport protocol.');
858 if (!$ch = curl_init($source)) {
859 // $this->log('Unable to init cURL.');
860 return false;
863 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // verify the peer's certificate
864 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // check the existence of a common name and also verify that it matches the hostname provided
865 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the transfer as a string
866 curl_setopt($ch, CURLOPT_HEADER, false); // don't include the header in the output
867 curl_setopt($ch, CURLOPT_TIMEOUT, 3600);
868 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); // nah, moodle.org is never unavailable! :-p
869 curl_setopt($ch, CURLOPT_URL, $source);
871 $targetfile = fopen($target, 'w');
873 if (!$targetfile) {
874 throw new download_file_exception('Unable to create local file '.$target);
877 curl_setopt($ch, CURLOPT_FILE, $targetfile);
879 $result = curl_exec($ch);
881 // try to detect encoding problems
882 if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) {
883 curl_setopt($ch, CURLOPT_ENCODING, 'none');
884 $result = curl_exec($ch);
887 fclose($targetfile);
889 $this->curlerrno = curl_errno($ch);
890 $this->curlerror = curl_error($ch);
891 $this->curlinfo = curl_getinfo($ch);
893 if (!$result or $this->curlerrno) {
894 return false;
896 } else if (is_array($this->curlinfo) and (empty($this->curlinfo['http_code']) or $this->curlinfo['http_code'] != 200)) {
897 return false;
900 return true;
904 * Log a message
906 * @param string $message
908 protected function log($message) {
909 // TODO
913 * Checks to see if the given source could be safely moved into a new location
915 * @param string $source full path to the existing directory
916 * @return bool
918 protected function move_directory_source_precheck($source) {
920 if (is_dir($source)) {
921 $handle = opendir($source);
922 } else {
923 return false;
926 $result = true;
928 while ($filename = readdir($handle)) {
929 $sourcepath = $source.'/'.$filename;
931 if ($filename === '.' or $filename === '..') {
932 continue;
935 if (is_dir($sourcepath)) {
936 $result = $result && $this->move_directory_source_precheck($sourcepath);
938 } else {
939 $result = $result && is_writable($sourcepath);
943 closedir($handle);
944 return $result && is_writable($source);
948 * Checks to see if a source foldr could be safely moved into the given new location
950 * @param string $destination full path to the new expected location of a folder
951 * @return bool
953 protected function move_directory_target_precheck($target) {
955 if (file_exists($target)) {
956 return false;
959 $result = mkdir($target, 02777) && rmdir($target);
961 return $result;
965 * Moves the given source into a new location recursively
967 * @param string $source full path to the existing directory
968 * @param string $destination full path to the new location of the folder
969 * @return bool
971 protected function move_directory($source, $target) {
973 if (file_exists($target)) {
974 throw new filesystem_exception('Unable to move the directory - target location already exists');
977 if (is_dir($source)) {
978 $handle = opendir($source);
979 } else {
980 throw new filesystem_exception('Source location is not a directory');
983 mkdir($target, 02777);
985 while ($filename = readdir($handle)) {
986 $sourcepath = $source.'/'.$filename;
987 $targetpath = $target.'/'.$filename;
989 if ($filename === '.' or $filename === '..') {
990 continue;
993 if (is_dir($sourcepath)) {
994 $this->move_directory($sourcepath, $targetpath);
996 } else {
997 rename($sourcepath, $targetpath);
1001 closedir($handle);
1002 return rmdir($source);
1006 * Deletes the given directory recursively
1008 * @param string $path full path to the directory
1010 protected function remove_directory($path) {
1012 if (!file_exists($path)) {
1013 return;
1016 if (is_dir($path)) {
1017 $handle = opendir($path);
1018 } else {
1019 throw new filesystem_exception('Given path is not a directory');
1022 while ($filename = readdir($handle)) {
1023 $filepath = $path.'/'.$filename;
1025 if ($filename === '.' or $filename === '..') {
1026 continue;
1029 if (is_dir($filepath)) {
1030 $this->remove_directory($filepath);
1032 } else {
1033 unlink($filepath);
1037 closedir($handle);
1038 return rmdir($path);
1042 * Unzip the file obtained from the Plugins directory to this site
1044 * @param string $ziplocation full path to the ZIP file
1045 * @param string $plugintyperoot full path to the plugin's type location
1046 * @param string $expectedlocation expected full path to the plugin after it is extracted
1047 * @param string $backuplocation location of the previous version of the plugin
1049 protected function unzip_plugin($ziplocation, $plugintyperoot, $expectedlocation, $backuplocation) {
1051 $zip = new ZipArchive();
1052 $result = $zip->open($ziplocation);
1054 if ($result !== true) {
1055 $this->move_directory($backuplocation, $expectedlocation);
1056 throw new zip_exception('Unable to open the zip package');
1059 // Make sure that the ZIP has expected structure
1060 $pluginname = basename($expectedlocation);
1061 for ($i = 0; $i < $zip->numFiles; $i++) {
1062 $stat = $zip->statIndex($i);
1063 $filename = $stat['name'];
1064 $filename = explode('/', $filename);
1065 if ($filename[0] !== $pluginname) {
1066 $zip->close();
1067 throw new zip_exception('Invalid structure of the zip package');
1071 if (!$zip->extractTo($plugintyperoot)) {
1072 $zip->close();
1073 $this->remove_directory($expectedlocation); // just in case something was created
1074 $this->move_directory($backuplocation, $expectedlocation);
1075 throw new zip_exception('Unable to extract the zip package');
1078 $zip->close();
1082 * Redirect the browser
1084 * @todo check if there has been some output yet
1085 * @param string $url
1087 protected function redirect($url) {
1088 header('Location: '.$url);
1093 ////////////////////////////////////////////////////////////////////////////////
1095 // Check if the script is actually executed or if it was just included by someone
1096 // else - typically by the PHPUnit. This is a PHP alternative to the Python's
1097 // if __name__ == '__main__'
1098 if (!debug_backtrace()) {
1099 // We are executed by the SAPI.
1100 // Initialize the worker class to actually make the job.
1101 $worker = worker::instance();
1103 // Lights, Camera, Action!
1104 $worker->execute();
1106 } else {
1107 // We are included - probably by some unit testing framework. Do nothing.