[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Defines classes used for updates. 19 * 20 * @package core 21 * @copyright 2011 David Mudrak <[email protected]> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace core\update; 25 26 use coding_exception, core_component, moodle_url; 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 /** 31 * Implements a communication bridge to the mdeploy.php utility 32 */ 33 class deployer { 34 35 /** @var \core\update\deployer holds the singleton instance */ 36 protected static $singletoninstance; 37 /** @var moodle_url URL of a page that includes the deployer UI */ 38 protected $callerurl; 39 /** @var moodle_url URL to return after the deployment */ 40 protected $returnurl; 41 42 /** 43 * Direct instantiation not allowed, use the factory method {@link self::instance()} 44 */ 45 protected function __construct() { 46 } 47 48 /** 49 * Sorry, this is singleton 50 */ 51 protected function __clone() { 52 } 53 54 /** 55 * Factory method for this class 56 * 57 * @return \core\update\deployer the singleton instance 58 */ 59 public static function instance() { 60 if (is_null(self::$singletoninstance)) { 61 self::$singletoninstance = new self(); 62 } 63 return self::$singletoninstance; 64 } 65 66 /** 67 * Reset caches used by this script 68 * 69 * @param bool $phpunitreset is this called as a part of PHPUnit reset? 70 */ 71 public static function reset_caches($phpunitreset = false) { 72 if ($phpunitreset) { 73 self::$singletoninstance = null; 74 } 75 } 76 77 /** 78 * Is automatic deployment enabled? 79 * 80 * @return bool 81 */ 82 public function enabled() { 83 global $CFG; 84 85 if (!empty($CFG->disableupdateautodeploy)) { 86 // The feature is prohibited via config.php. 87 return false; 88 } 89 90 return get_config('updateautodeploy'); 91 } 92 93 /** 94 * Sets some base properties of the class to make it usable. 95 * 96 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data 97 * @param moodle_url $returnurl the final URL to return to when the deployment is finished 98 */ 99 public function initialize(moodle_url $callerurl, moodle_url $returnurl) { 100 101 if (!$this->enabled()) { 102 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.'); 103 } 104 105 $this->callerurl = $callerurl; 106 $this->returnurl = $returnurl; 107 } 108 109 /** 110 * Has the deployer been initialized? 111 * 112 * Initialized deployer means that the following properties were set: 113 * callerurl, returnurl 114 * 115 * @return bool 116 */ 117 public function initialized() { 118 119 if (!$this->enabled()) { 120 return false; 121 } 122 123 if (empty($this->callerurl)) { 124 return false; 125 } 126 127 if (empty($this->returnurl)) { 128 return false; 129 } 130 131 return true; 132 } 133 134 /** 135 * Returns a list of reasons why the deployment can not happen 136 * 137 * If the returned array is empty, the deployment seems to be possible. The returned 138 * structure is an associative array with keys representing individual impediments. 139 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable. 140 * 141 * @param \core\update\info $info 142 * @return array 143 */ 144 public function deployment_impediments(info $info) { 145 146 $impediments = array(); 147 148 if (empty($info->download)) { 149 $impediments['missingdownloadurl'] = true; 150 } 151 152 if (empty($info->downloadmd5)) { 153 $impediments['missingdownloadmd5'] = true; 154 } 155 156 if (!empty($info->download) and !$this->update_downloadable($info->download)) { 157 $impediments['notdownloadable'] = true; 158 } 159 160 if (!$this->component_writable($info->component)) { 161 $impediments['notwritable'] = true; 162 } 163 164 return $impediments; 165 } 166 167 /** 168 * Check to see if the current version of the plugin seems to be a checkout of an external repository. 169 * 170 * @see core_plugin_manager::plugin_external_source() 171 * @param \core\update\info $info 172 * @return false|string 173 */ 174 public function plugin_external_source(info $info) { 175 176 $paths = core_component::get_plugin_types(); 177 list($plugintype, $pluginname) = core_component::normalize_component($info->component); 178 $pluginroot = $paths[$plugintype].'/'.$pluginname; 179 180 if (is_dir($pluginroot.'/.git')) { 181 return 'git'; 182 } 183 184 if (is_file($pluginroot.'/.git')) { 185 return 'git-submodule'; 186 } 187 188 if (is_dir($pluginroot.'/CVS')) { 189 return 'cvs'; 190 } 191 192 if (is_dir($pluginroot.'/.svn')) { 193 return 'svn'; 194 } 195 196 if (is_dir($pluginroot.'/.hg')) { 197 return 'mercurial'; 198 } 199 200 return false; 201 } 202 203 /** 204 * Prepares a renderable widget to confirm installation of an available update. 205 * 206 * @param \core\update\info $info component version to deploy 207 * @return \renderable 208 */ 209 public function make_confirm_widget(info $info) { 210 211 if (!$this->initialized()) { 212 throw new coding_exception('Illegal method call - deployer not initialized.'); 213 } 214 215 $params = array( 216 'updateaddon' => $info->component, 217 'version' =>$info->version, 218 'sesskey' => sesskey(), 219 ); 220 221 // Append some our own data. 222 if (!empty($this->callerurl)) { 223 $params['callerurl'] = $this->callerurl->out(false); 224 } 225 if (!empty($this->returnurl)) { 226 $params['returnurl'] = $this->returnurl->out(false); 227 } 228 229 $widget = new \single_button( 230 new moodle_url($this->callerurl, $params), 231 get_string('updateavailableinstall', 'core_admin'), 232 'post' 233 ); 234 235 return $widget; 236 } 237 238 /** 239 * Prepares a renderable widget to execute installation of an available update. 240 * 241 * @param \core\update\info $info component version to deploy 242 * @param moodle_url $returnurl URL to return after the installation execution 243 * @return \renderable 244 */ 245 public function make_execution_widget(info $info, moodle_url $returnurl = null) { 246 global $CFG; 247 248 if (!$this->initialized()) { 249 throw new coding_exception('Illegal method call - deployer not initialized.'); 250 } 251 252 $pluginrootpaths = core_component::get_plugin_types(); 253 254 list($plugintype, $pluginname) = core_component::normalize_component($info->component); 255 256 if (empty($pluginrootpaths[$plugintype])) { 257 throw new coding_exception('Unknown plugin type root location', $plugintype); 258 } 259 260 list($passfile, $password) = $this->prepare_authorization(); 261 262 if (is_null($returnurl)) { 263 $returnurl = new moodle_url('/admin'); 264 } else { 265 $returnurl = $returnurl; 266 } 267 268 $params = array( 269 'upgrade' => true, 270 'type' => $plugintype, 271 'name' => $pluginname, 272 'typeroot' => $pluginrootpaths[$plugintype], 273 'package' => $info->download, 274 'md5' => $info->downloadmd5, 275 'dataroot' => $CFG->dataroot, 276 'dirroot' => $CFG->dirroot, 277 'passfile' => $passfile, 278 'password' => $password, 279 'returnurl' => $returnurl->out(false), 280 ); 281 282 if (!empty($CFG->proxyhost)) { 283 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our 284 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy 285 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is 286 // fixed, the condition should be amended. 287 if (true or !is_proxybypass($info->download)) { 288 if (empty($CFG->proxyport)) { 289 $params['proxy'] = $CFG->proxyhost; 290 } else { 291 $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport; 292 } 293 294 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) { 295 $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword; 296 } 297 298 if (!empty($CFG->proxytype)) { 299 $params['proxytype'] = $CFG->proxytype; 300 } 301 } 302 } 303 304 $widget = new \single_button( 305 new moodle_url('/mdeploy.php', $params), 306 get_string('updateavailableinstall', 'core_admin'), 307 'post' 308 ); 309 310 return $widget; 311 } 312 313 /** 314 * Returns array of data objects passed to this tool. 315 * 316 * @return array 317 */ 318 public function submitted_data() { 319 $component = optional_param('updateaddon', '', PARAM_COMPONENT); 320 $version = optional_param('version', '', PARAM_RAW); 321 if (!$component or !$version) { 322 return false; 323 } 324 325 $plugininfo = \core_plugin_manager::instance()->get_plugin_info($component); 326 if (!$plugininfo) { 327 return false; 328 } 329 330 if ($plugininfo->is_standard()) { 331 return false; 332 } 333 334 if (!$updates = $plugininfo->available_updates()) { 335 return false; 336 } 337 338 $info = null; 339 foreach ($updates as $update) { 340 if ($update->version == $version) { 341 $info = $update; 342 break; 343 } 344 } 345 if (!$info) { 346 return false; 347 } 348 349 $data = array( 350 'updateaddon' => $component, 351 'updateinfo' => $info, 352 'callerurl' => optional_param('callerurl', null, PARAM_URL), 353 'returnurl' => optional_param('returnurl', null, PARAM_URL), 354 ); 355 if ($data['callerurl']) { 356 $data['callerurl'] = new moodle_url($data['callerurl']); 357 } 358 if ($data['callerurl']) { 359 $data['returnurl'] = new moodle_url($data['returnurl']); 360 } 361 362 return $data; 363 } 364 365 /** 366 * Handles magic getters and setters for protected properties. 367 * 368 * @param string $name method name, e.g. set_returnurl() 369 * @param array $arguments arguments to be passed to the array 370 */ 371 public function __call($name, array $arguments = array()) { 372 373 if (substr($name, 0, 4) === 'set_') { 374 $property = substr($name, 4); 375 if (empty($property)) { 376 throw new coding_exception('Invalid property name (empty)'); 377 } 378 if (empty($arguments)) { 379 $arguments = array(true); // Default value for flag-like properties. 380 } 381 // Make sure it is a protected property. 382 $isprotected = false; 383 $reflection = new \ReflectionObject($this); 384 foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) { 385 if ($reflectionproperty->getName() === $property) { 386 $isprotected = true; 387 break; 388 } 389 } 390 if (!$isprotected) { 391 throw new coding_exception('Unable to set property - it does not exist or it is not protected'); 392 } 393 $value = reset($arguments); 394 $this->$property = $value; 395 return; 396 } 397 398 if (substr($name, 0, 4) === 'get_') { 399 $property = substr($name, 4); 400 if (empty($property)) { 401 throw new coding_exception('Invalid property name (empty)'); 402 } 403 if (!empty($arguments)) { 404 throw new coding_exception('No parameter expected'); 405 } 406 // Make sure it is a protected property. 407 $isprotected = false; 408 $reflection = new \ReflectionObject($this); 409 foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) { 410 if ($reflectionproperty->getName() === $property) { 411 $isprotected = true; 412 break; 413 } 414 } 415 if (!$isprotected) { 416 throw new coding_exception('Unable to get property - it does not exist or it is not protected'); 417 } 418 return $this->$property; 419 } 420 } 421 422 /** 423 * Generates a random token and stores it in a file in moodledata directory. 424 * 425 * @return array of the (string)filename and (string)password in this order 426 */ 427 public function prepare_authorization() { 428 global $CFG; 429 430 make_upload_directory('mdeploy/auth/'); 431 432 $attempts = 0; 433 $success = false; 434 435 while (!$success and $attempts < 5) { 436 $attempts++; 437 438 $passfile = $this->generate_passfile(); 439 $password = $this->generate_password(); 440 $now = time(); 441 442 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile; 443 444 if (!file_exists($filepath)) { 445 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX); 446 chmod($filepath, $CFG->filepermissions); 447 } 448 } 449 450 if ($success) { 451 return array($passfile, $password); 452 453 } else { 454 throw new \moodle_exception('unable_prepare_authorization', 'core_plugin'); 455 } 456 } 457 458 /* === End of external API === */ 459 460 /** 461 * Returns a random string to be used as a filename of the password storage. 462 * 463 * @return string 464 */ 465 protected function generate_passfile() { 466 return clean_param(uniqid('mdeploy_', true), PARAM_FILE); 467 } 468 469 /** 470 * Returns a random string to be used as the authorization token 471 * 472 * @return string 473 */ 474 protected function generate_password() { 475 return complex_random_string(); 476 } 477 478 /** 479 * Checks if the given component's directory is writable 480 * 481 * For the purpose of the deployment, the web server process has to have 482 * write access to all files in the component's directory (recursively) and for the 483 * directory itself. 484 * 485 * @see worker::move_directory_source_precheck() 486 * @param string $component normalized component name 487 * @return boolean 488 */ 489 protected function component_writable($component) { 490 491 list($plugintype, $pluginname) = core_component::normalize_component($component); 492 493 $directory = core_component::get_plugin_directory($plugintype, $pluginname); 494 495 if (is_null($directory)) { 496 // Plugin unknown, most probably deleted or missing during upgrade, 497 // look at the parent directory instead because they might want to install it. 498 $plugintypes = core_component::get_plugin_types(); 499 if (!isset($plugintypes[$plugintype])) { 500 throw new coding_exception('Unknown component location', $component); 501 } 502 $directory = $plugintypes[$plugintype]; 503 } 504 505 return $this->directory_writable($directory); 506 } 507 508 /** 509 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL 510 * 511 * This is mainly supposed to check if the transmission over HTTPS would 512 * work. That is, if the CA certificates are present at the server. 513 * 514 * @param string $downloadurl the URL of the ZIP package to download 515 * @return bool 516 */ 517 protected function update_downloadable($downloadurl) { 518 global $CFG; 519 520 $curloptions = array( 521 'CURLOPT_SSL_VERIFYHOST' => 2, // This is the default in {@link curl} class but just in case. 522 'CURLOPT_SSL_VERIFYPEER' => true, 523 ); 524 525 $curl = new \curl(array('proxy' => true)); 526 $result = $curl->head($downloadurl, $curloptions); 527 $errno = $curl->get_errno(); 528 if (empty($errno)) { 529 return true; 530 } else { 531 return false; 532 } 533 } 534 535 /** 536 * Checks if the directory and all its contents (recursively) is writable 537 * 538 * @param string $path full path to a directory 539 * @return boolean 540 */ 541 private function directory_writable($path) { 542 543 if (!is_writable($path)) { 544 return false; 545 } 546 547 if (is_dir($path)) { 548 $handle = opendir($path); 549 } else { 550 return false; 551 } 552 553 $result = true; 554 555 while ($filename = readdir($handle)) { 556 $filepath = $path.'/'.$filename; 557 558 if ($filename === '.' or $filename === '..') { 559 continue; 560 } 561 562 if (is_dir($filepath)) { 563 $result = $result && $this->directory_writable($filepath); 564 565 } else { 566 $result = $result && is_writable($filepath); 567 } 568 } 569 570 closedir($handle); 571 572 return $result; 573 } 574 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |