Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 * Provides tool_installaddon_installer class.
20
 *
21
 * @package     tool_installaddon
22
 * @subpackage  classes
23
 * @copyright   2013 David Mudrak <david@moodle.com>
24
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Implements main plugin features.
31
 *
32
 * @copyright 2013 David Mudrak <david@moodle.com>
33
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34
 */
35
class tool_installaddon_installer {
36
 
37
    /** @var tool_installaddon_installfromzip_form */
38
    protected $installfromzipform = null;
39
 
40
    /**
41
     * Factory method returning an instance of this class.
42
     *
43
     * @return tool_installaddon_installer
44
     */
45
    public static function instance() {
46
        return new static();
47
    }
48
 
49
    /**
50
     * Returns the URL to the main page of this admin tool
51
     *
52
     * @param array optional parameters
53
     * @return moodle_url
54
     */
55
    public function index_url(array $params = null) {
56
        return new moodle_url('/admin/tool/installaddon/index.php', $params);
57
    }
58
 
59
    /**
60
     * Returns URL to the repository that addons can be searched in and installed from
61
     *
62
     * @return moodle_url
63
     */
64
    public function get_addons_repository_url() {
65
        global $CFG;
66
 
67
        if (!empty($CFG->config_php_settings['alternativeaddonsrepositoryurl'])) {
68
            $url = $CFG->config_php_settings['alternativeaddonsrepositoryurl'];
69
        } else {
70
            $url = 'https://moodle.org/plugins/get.php';
71
        }
72
 
73
        if (!$this->should_send_site_info()) {
74
            return new moodle_url($url);
75
        }
76
 
77
        // Append the basic information about our site.
78
        $site = array(
79
            'fullname' => $this->get_site_fullname(),
80
            'url' => $this->get_site_url(),
81
            'majorversion' => $this->get_site_major_version(),
82
        );
83
 
84
        $site = $this->encode_site_information($site);
85
 
86
        return new moodle_url($url, array('site' => $site));
87
    }
88
 
89
    /**
90
     * @return tool_installaddon_installfromzip_form
91
     */
92
    public function get_installfromzip_form() {
93
        if (!is_null($this->installfromzipform)) {
94
            return $this->installfromzipform;
95
        }
96
 
97
        $action = $this->index_url();
98
        $customdata = array('installer' => $this);
99
 
100
        $this->installfromzipform = new tool_installaddon_installfromzip_form($action, $customdata);
101
 
102
        return $this->installfromzipform;
103
    }
104
 
105
    /**
106
     * Makes a unique writable storage for uploaded ZIP packages.
107
     *
108
     * We need the saved ZIP to survive across multiple requests so that it can
109
     * be used by the plugin manager after the installation is confirmed. In
110
     * other words, we cannot use make_request_directory() here.
111
     *
112
     * @return string full path to the directory
113
     */
114
    public function make_installfromzip_storage() {
115
        return make_unique_writable_directory(make_temp_directory('tool_installaddon'));
116
    }
117
 
118
    /**
119
     * Returns localised list of available plugin types
120
     *
121
     * @return array (string)plugintype => (string)plugin name
122
     */
123
    public function get_plugin_types_menu() {
124
        global $CFG;
125
 
126
        $pluginman = core_plugin_manager::instance();
127
 
128
        $menu = array('' => get_string('choosedots'));
129
        foreach (array_keys($pluginman->get_plugin_types()) as $plugintype) {
130
            $menu[$plugintype] = $pluginman->plugintype_name($plugintype).' ('.$plugintype.')';
131
        }
132
 
133
        return $menu;
134
    }
135
 
136
    /**
137
     * Hook method to handle the remote request to install an add-on
138
     *
139
     * This is used as a callback when the admin picks a plugin version in the
140
     * Moodle Plugins directory and is redirected back to their site to install
141
     * it.
142
     *
143
     * This hook is called early from admin/tool/installaddon/index.php page so that
144
     * it has opportunity to take over the UI and display the first confirmation screen.
145
     *
146
     * @param tool_installaddon_renderer $output
147
     * @param string|null $request
148
     */
149
    public function handle_remote_request(tool_installaddon_renderer $output, $request) {
150
 
151
        if (is_null($request)) {
152
            return;
153
        }
154
 
155
        $data = $this->decode_remote_request($request);
156
 
157
        if ($data === false) {
158
            echo $output->remote_request_invalid_page($this->index_url());
159
            exit();
160
        }
161
 
162
        list($plugintype, $pluginname) = core_component::normalize_component($data->component);
163
        $pluginman = core_plugin_manager::instance();
164
 
165
        $plugintypepath = $pluginman->get_plugintype_root($plugintype);
166
 
167
        if (file_exists($plugintypepath.'/'.$pluginname)) {
168
            echo $output->remote_request_alreadyinstalled_page($data, $this->index_url());
169
            exit();
170
        }
171
 
172
        if (!$pluginman->is_plugintype_writable($plugintype)) {
173
            $continueurl = $this->index_url(array('installaddonrequest' => $request));
174
            echo $output->remote_request_permcheck_page($data, $plugintypepath, $continueurl, $this->index_url());
175
            exit();
176
        }
177
 
178
        if (!$pluginman->is_remote_plugin_installable($data->component, $data->version, $reason)) {
179
            $data->reason = $reason;
180
            echo $output->remote_request_non_installable_page($data, $this->index_url());
181
            exit();
182
        }
183
 
184
        $continueurl = $this->index_url(array(
185
            'installremote' => $data->component,
186
            'installremoteversion' => $data->version
187
        ));
188
 
189
        echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
190
        exit();
191
    }
192
 
193
    /**
194
     * Detect the given plugin's component name
195
     *
196
     * Only plugins that declare valid $plugin->component value in the version.php
197
     * are supported.
198
     *
199
     * @param string $zipfilepath full path to the saved ZIP file
200
     * @return string|bool declared component name or false if unable to detect
201
     */
202
    public function detect_plugin_component($zipfilepath) {
203
 
204
        $workdir = make_request_directory();
205
        $versionphp = $this->extract_versionphp_file($zipfilepath, $workdir);
206
 
207
        if (empty($versionphp)) {
208
            return false;
209
        }
210
 
211
        return $this->detect_plugin_component_from_versionphp(file_get_contents($workdir.'/'.$versionphp));
212
    }
213
 
214
    //// End of external API ///////////////////////////////////////////////////
215
 
216
    /**
217
     * @see self::instance()
218
     */
219
    protected function __construct() {
220
    }
221
 
222
    /**
223
     * @return string this site full name
224
     */
225
    protected function get_site_fullname() {
226
        global $SITE;
227
 
228
        return strip_tags($SITE->fullname);
229
    }
230
 
231
    /**
232
     * @return string this site URL
233
     */
234
    protected function get_site_url() {
235
        global $CFG;
236
 
237
        return $CFG->wwwroot;
238
    }
239
 
240
    /**
241
     * @return string major version like 2.5, 2.6 etc.
242
     */
243
    protected function get_site_major_version() {
244
        return moodle_major_version();
245
    }
246
 
247
    /**
248
     * Encodes the given array in a way that can be safely appended as HTTP GET param
249
     *
250
     * Be ware! The recipient may rely on the exact way how the site information is encoded.
251
     * Do not change anything here unless you know what you are doing and understand all
252
     * consequences! (Don't you love warnings like that, too? :-p)
253
     *
254
     * @param array $info
255
     * @return string
256
     */
257
    protected function encode_site_information(array $info) {
258
        return base64_encode(json_encode($info));
259
    }
260
 
261
    /**
262
     * Decide if the encoded site information should be sent to the add-ons repository site
263
     *
264
     * For now, we just return true. In the future, we may want to implement some
265
     * privacy aware logic (based on site/user preferences for example).
266
     *
267
     * @return bool
268
     */
269
    protected function should_send_site_info() {
270
        return true;
271
    }
272
 
273
    /**
274
     * Decode the request from the Moodle Plugins directory
275
     *
276
     * @param string $request submitted via 'installaddonrequest' HTTP parameter
277
     * @return stdClass|bool false on error, object otherwise
278
     */
279
    protected function decode_remote_request($request) {
280
 
281
        $data = base64_decode($request, true);
282
 
283
        if ($data === false) {
284
            return false;
285
        }
286
 
287
        $data = json_decode($data);
288
 
289
        if (is_null($data)) {
290
            return false;
291
        }
292
 
293
        if (!isset($data->name) or !isset($data->component) or !isset($data->version)) {
294
            return false;
295
        }
296
 
297
        $data->name = s(strip_tags($data->name));
298
 
299
        if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) {
300
            return false;
301
        }
302
 
303
        list($plugintype, $pluginname) = core_component::normalize_component($data->component);
304
 
305
        if ($plugintype === 'core') {
306
            return false;
307
        }
308
 
309
        if ($data->component !== $plugintype.'_'.$pluginname) {
310
            return false;
311
        }
312
 
313
        if (!core_component::is_valid_plugin_name($plugintype, $pluginname)) {
314
            return false;
315
        }
316
 
317
        $plugintypes = core_component::get_plugin_types();
318
        if (!isset($plugintypes[$plugintype])) {
319
            return false;
320
        }
321
 
322
        // Keep this regex in sync with the one used by the download.moodle.org/api/x.y/pluginfo.php
323
        if (!preg_match('/^[0-9]+$/', $data->version)) {
324
            return false;
325
        }
326
 
327
        return $data;
328
    }
329
 
330
    /**
331
     * Extracts the version.php from the given plugin ZIP file into the target directory
332
     *
333
     * @param string $zipfilepath full path to the saved ZIP file
334
     * @param string $targetdir full path to extract the file to
335
     * @return string|bool path to the version.php within the $targetpath; false on error (e.g. not found)
336
     */
337
    protected function extract_versionphp_file($zipfilepath, $targetdir) {
338
        global $CFG;
339
        require_once($CFG->libdir.'/filelib.php');
340
 
341
        $fp = get_file_packer('application/zip');
342
        $files = $fp->list_files($zipfilepath);
343
 
344
        if (empty($files)) {
345
            return false;
346
        }
347
 
348
        $rootdirname = null;
349
        $found = null;
350
 
351
        foreach ($files as $file) {
352
            // Valid plugin ZIP package has just one root directory with all
353
            // files in it.
354
            $pathnameitems = explode('/', $file->pathname);
355
 
356
            if (empty($pathnameitems)) {
357
                return false;
358
            }
359
 
360
            // Set the expected name of the root directory in the first
361
            // iteration of the loop.
362
            if ($rootdirname === null) {
363
                $rootdirname = $pathnameitems[0];
364
            }
365
 
366
            // Require the same root directory for all files in the ZIP
367
            // package.
368
            if ($rootdirname !== $pathnameitems[0]) {
369
                return false;
370
            }
371
 
372
            // If we reached the valid version.php file, remember it.
373
            if ($pathnameitems[1] === 'version.php' and !$file->is_directory and $file->size > 0) {
374
                $found = $file->pathname;
375
            }
376
        }
377
 
378
        if (empty($found)) {
379
            return false;
380
        }
381
 
382
        $extracted = $fp->extract_to_pathname($zipfilepath, $targetdir, array($found));
383
 
384
        if (empty($extracted)) {
385
            return false;
386
        }
387
 
388
        // The following syntax uses function array dereferencing, added in PHP 5.4.0.
389
        return array_keys($extracted)[0];
390
    }
391
 
392
    /**
393
     * Return the plugin component declared in its version.php file
394
     *
395
     * @param string $code the contents of the version.php file
396
     * @return string|bool declared plugin component or false if unable to detect
397
     */
398
    protected function detect_plugin_component_from_versionphp($code) {
399
 
400
        $result = preg_match_all('#^\s*\$plugin\->component\s*=\s*([\'"])(.+?_.+?)\1\s*;#m', $code, $matches);
401
 
402
        // Return if and only if the single match was detected.
403
        if ($result === 1 and !empty($matches[2][0])) {
404
            return $matches[2][0];
405
        }
406
 
407
        return false;
408
    }
409
}