~ubuntu-branches/ubuntu/trusty/moodle/trusty-proposed

« back to all changes in this revision

Viewing changes to mdeploy.php

  • Committer: Package Import Robot
  • Author(s): Thijs Kinkhorst
  • Date: 2013-07-19 08:52:46 UTC
  • mfrom: (1.1.10)
  • Revision ID: package-import@ubuntu.com-20130719085246-yebwditc2exoap2r
Tags: 2.5.1-1
* New upstream version: 2.5.1.
  - Fixes security issues:
    CVE-2013-2242 CVE-2013-2243 CVE-2013-2244 CVE-2013-2245
    CVE-2013-2246
* Depend on apache2 instead of obsolete apache2-mpm-prefork.
* Use packaged libphp-phpmailer (closes: #429339), adodb,
  HTMLPurifier, PclZip.
* Update debconf translations, thanks Salvatore Merone, Pietro Tollot,
  Joe Hansen, Yuri Kozlov, Holger Wansing, Américo Monteiro,
  Adriano Rafael Gomes, victory, Michał Kułach.
  (closes: #716972, #716986, #717080, #717108, #717278)

Show diffs side-by-side

added added

removed removed

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