source: moodle/trunk/fuentes/admin/tool/installaddon/classes/installer.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

File size: 12.5 KB
Line 
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
27defined('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 */
35class 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}
Note: See TracBrowser for help on using the repository browser.