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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body