MDL-49329 admin: Convert install plugins tool to use new APIs
[moodle.git] / admin / tool / installaddon / classes / installer.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * 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  */
27 defined('MOODLE_INTERNAL') || die();
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 {
37     /** @var tool_installaddon_installfromzip_form */
38     protected $installfromzipform = null;
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     }
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     }
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;
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         }
73         if (!$this->should_send_site_info()) {
74             return new moodle_url($url);
75         }
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         );
84         $site = $this->encode_site_information($site);
86         return new moodle_url($url, array('site' => $site));
87     }
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         }
97         $action = $this->index_url();
98         $customdata = array('installer' => $this);
100         $this->installfromzipform = new tool_installaddon_installfromzip_form($action, $customdata);
102         return $this->installfromzipform;
103     }
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     }
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;
126         $pluginman = core_plugin_manager::instance();
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         }
133         return $menu;
134     }
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) {
151         if (is_null($request)) {
152             return;
153         }
155         $data = $this->decode_remote_request($request);
157         if ($data === false) {
158             echo $output->remote_request_invalid_page($this->index_url());
159             exit();
160         }
162         list($plugintype, $pluginname) = core_component::normalize_component($data->component);
163         $pluginman = core_plugin_manager::instance();
165         $plugintypepath = $pluginman->get_plugintype_root($plugintype);
167         if (file_exists($plugintypepath.'/'.$pluginname)) {
168             echo $output->remote_request_alreadyinstalled_page($data, $this->index_url());
169             exit();
170         }
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         }
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         }
184         $continueurl = $this->index_url(array(
185             'installremote' => $data->component,
186             'installremoteversion' => $data->version
187         ));
189         echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
190         exit();
191     }
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) {
204         $workdir = make_request_directory();
205         $versionphp = $this->extract_versionphp_file($zipfilepath, $workdir);
207         if (empty($versionphp)) {
208             return false;
209         }
211         return $this->detect_plugin_component_from_versionphp(file_get_contents($workdir.'/'.$versionphp));
212     }
214     //// End of external API ///////////////////////////////////////////////////
216     /**
217      * @see self::instance()
218      */
219     protected function __construct() {
220     }
222     /**
223      * @return string this site full name
224      */
225     protected function get_site_fullname() {
226         global $SITE;
228         return strip_tags($SITE->fullname);
229     }
231     /**
232      * @return string this site URL
233      */
234     protected function get_site_url() {
235         global $CFG;
237         return $CFG->wwwroot;
238     }
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     }
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     }
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     }
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) {
281         $data = base64_decode($request, true);
283         if ($data === false) {
284             return false;
285         }
287         $data = json_decode($data);
289         if (is_null($data)) {
290             return false;
291         }
293         if (!isset($data->name) or !isset($data->component) or !isset($data->version)) {
294             return false;
295         }
297         $data->name = s(strip_tags($data->name));
299         if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) {
300             return false;
301         }
303         list($plugintype, $pluginname) = core_component::normalize_component($data->component);
305         if ($plugintype === 'core') {
306             return false;
307         }
309         if ($data->component !== $plugintype.'_'.$pluginname) {
310             return false;
311         }
313         if (!core_component::is_valid_plugin_name($plugintype, $pluginname)) {
314             return false;
315         }
317         $plugintypes = core_component::get_plugin_types();
318         if (!isset($plugintypes[$plugintype])) {
319             return false;
320         }
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         }
327         return $data;
328     }
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');
341         $fp = get_file_packer('application/zip');
342         $files = $fp->list_files($zipfilepath);
344         if (empty($files)) {
345             return false;
346         }
348         $rootdirname = null;
349         $found = null;
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);
356             if (empty($pathnameitems)) {
357                 return false;
358             }
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             }
366             // Require the same root directory for all files in the ZIP
367             // package.
368             if ($rootdirname !== $pathnameitems[0]) {
369                 return false;
370             }
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         }
378         if (empty($found)) {
379             return false;
380         }
382         $extracted = $fp->extract_to_pathname($zipfilepath, $targetdir, array($found));
384         if (empty($extracted)) {
385             return false;
386         }
388         // The following syntax uses function array dereferencing, added in PHP 5.4.0.
389         return array_keys($extracted)[0];
390     }
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) {
400         $result = preg_match_all('#^\s*\$plugin\->component\s*=\s*([\'"])(.+?_.+?)\1\s*;#m', $code, $matches);
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         }
407         return false;
408     }