[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
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 validation class to check the plugin ZIP contents 20 * 21 * Uses fragments of the local_plugins_archive_validator class copyrighted by 22 * Marina Glancy that is part of the local_plugins plugin. 23 * 24 * @package tool_installaddon 25 * @subpackage classes 26 * @copyright 2013 David Mudrak <[email protected]> 27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 */ 29 30 defined('MOODLE_INTERNAL') || die(); 31 32 if (!defined('T_ML_COMMENT')) { 33 define('T_ML_COMMENT', T_COMMENT); 34 } else { 35 define('T_DOC_COMMENT', T_ML_COMMENT); 36 } 37 38 /** 39 * Validates the contents of extracted plugin ZIP file 40 * 41 * @copyright 2013 David Mudrak <[email protected]> 42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 */ 44 class tool_installaddon_validator { 45 46 /** Critical error message level, causes the validation fail. */ 47 const ERROR = 'error'; 48 49 /** Warning message level, validation does not fail but the admin should be always informed. */ 50 const WARNING = 'warning'; 51 52 /** Information message level that the admin should be aware of. */ 53 const INFO = 'info'; 54 55 /** Debugging message level, should be displayed in debugging mode only. */ 56 const DEBUG = 'debug'; 57 58 /** @var string full path to the extracted ZIP contents */ 59 protected $extractdir = null; 60 61 /** @var array as returned by {@link zip_packer::extract_to_pathname()} */ 62 protected $extractfiles = null; 63 64 /** @var bool overall result of validation */ 65 protected $result = null; 66 67 /** @var string the name of the plugin root directory */ 68 protected $rootdir = null; 69 70 /** @var array explicit list of expected/required characteristics of the ZIP */ 71 protected $assertions = null; 72 73 /** @var array of validation log messages */ 74 protected $messages = array(); 75 76 /** @var array|null array of relevant data obtained from version.php */ 77 protected $versionphp = null; 78 79 /** @var string|null the name of found English language file without the .php extension */ 80 protected $langfilename = null; 81 82 /** @var moodle_url|null URL to continue with the installation of validated add-on */ 83 protected $continueurl = null; 84 85 /** 86 * Factory method returning instance of the validator 87 * 88 * @param string $zipcontentpath full path to the extracted ZIP contents 89 * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error 90 * @return tool_installaddon_validator 91 */ 92 public static function instance($zipcontentpath, array $zipcontentfiles) { 93 return new static($zipcontentpath, $zipcontentfiles); 94 } 95 96 /** 97 * Set the expected plugin type, fail the validation otherwise 98 * 99 * @param string $required plugin type 100 */ 101 public function assert_plugin_type($required) { 102 $this->assertions['plugintype'] = $required; 103 } 104 105 /** 106 * Set the expectation that the plugin can be installed into the given Moodle version 107 * 108 * @param string $required Moodle version we are about to install to 109 */ 110 public function assert_moodle_version($required) { 111 $this->assertions['moodleversion'] = $required; 112 } 113 114 /** 115 * Execute the validation process against all explicit and implicit requirements 116 * 117 * Returns true if the validation passes (all explicit and implicit requirements 118 * pass) and the plugin can be installed. Returns false if the validation fails 119 * (some explicit or implicit requirement fails) and the plugin must not be 120 * installed. 121 * 122 * @return bool 123 */ 124 public function execute() { 125 126 $this->result = ( 127 $this->validate_files_layout() 128 and $this->validate_version_php() 129 and $this->validate_language_pack() 130 and $this->validate_target_location() 131 ); 132 133 return $this->result; 134 } 135 136 /** 137 * Returns overall result of the validation. 138 * 139 * Null is returned if the validation has not been executed yet. Otherwise 140 * this method returns true (the installation can continue) or false (it is not 141 * safe to continue with the installation). 142 * 143 * @return bool|null 144 */ 145 public function get_result() { 146 return $this->result; 147 } 148 149 /** 150 * Return the list of validation log messages 151 * 152 * Each validation message is a plain object with properties level, msgcode 153 * and addinfo. 154 * 155 * @return array of (int)index => (stdClass) validation message 156 */ 157 public function get_messages() { 158 return $this->messages; 159 } 160 161 /** 162 * Return the information provided by the the plugin's version.php 163 * 164 * If version.php was not found in the plugin (which is tolerated for 165 * themes only at the moment), null is returned. Otherwise the array 166 * is returned. It may be empty if no information was parsed (which 167 * should not happen). 168 * 169 * @return null|array 170 */ 171 public function get_versionphp_info() { 172 return $this->versionphp; 173 } 174 175 /** 176 * Returns the name of the English language file without the .php extension 177 * 178 * This can be used as a suggestion for fixing the plugin root directory in the 179 * ZIP file during the upload. If no file was found, or multiple PHP files are 180 * located in lang/en/ folder, then null is returned. 181 * 182 * @return null|string 183 */ 184 public function get_language_file_name() { 185 return $this->langfilename; 186 } 187 188 /** 189 * Returns the rootdir of the extracted package (after eventual renaming) 190 * 191 * @return string|null 192 */ 193 public function get_rootdir() { 194 return $this->rootdir; 195 } 196 197 /** 198 * Sets the URL to continue to after successful validation 199 * 200 * @param moodle_url $url 201 */ 202 public function set_continue_url(moodle_url $url) { 203 $this->continueurl = $url; 204 } 205 206 /** 207 * Get the URL to continue to after successful validation 208 * 209 * Null is returned if the URL has not been explicitly set by the caller. 210 * 211 * @return moodle_url|null 212 */ 213 public function get_continue_url() { 214 return $this->continueurl; 215 } 216 217 // End of external API ///////////////////////////////////////////////////// 218 219 /** 220 * @param string $zipcontentpath full path to the extracted ZIP contents 221 * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error 222 */ 223 protected function __construct($zipcontentpath, array $zipcontentfiles) { 224 $this->extractdir = $zipcontentpath; 225 $this->extractfiles = $zipcontentfiles; 226 } 227 228 // Validation methods ////////////////////////////////////////////////////// 229 230 /** 231 * @return bool false if files in the ZIP do not have required layout 232 */ 233 protected function validate_files_layout() { 234 235 if (!is_array($this->extractfiles) or count($this->extractfiles) < 4) { 236 // We need the English language pack with the name of the plugin at least 237 $this->add_message(self::ERROR, 'filesnumber'); 238 return false; 239 } 240 241 foreach ($this->extractfiles as $filerelname => $filestatus) { 242 if ($filestatus !== true) { 243 $this->add_message(self::ERROR, 'filestatus', array('file' => $filerelname, 'status' => $filestatus)); 244 return false; 245 } 246 } 247 248 foreach (array_keys($this->extractfiles) as $filerelname) { 249 if (!file_exists($this->extractdir.'/'.$filerelname)) { 250 $this->add_message(self::ERROR, 'filenotexists', array('file' => $filerelname)); 251 return false; 252 } 253 } 254 255 foreach (array_keys($this->extractfiles) as $filerelname) { 256 $matches = array(); 257 if (!preg_match("#^([^/]+)/#", $filerelname, $matches) or (!is_null($this->rootdir) and $this->rootdir !== $matches[1])) { 258 $this->add_message(self::ERROR, 'onedir'); 259 return false; 260 } 261 $this->rootdir = $matches[1]; 262 } 263 264 if ($this->rootdir !== clean_param($this->rootdir, PARAM_PLUGIN)) { 265 $this->add_message(self::ERROR, 'rootdirinvalid', $this->rootdir); 266 return false; 267 } else { 268 $this->add_message(self::INFO, 'rootdir', $this->rootdir); 269 } 270 271 return is_dir($this->extractdir.'/'.$this->rootdir); 272 } 273 274 /** 275 * @return bool false if the version.php file does not declare required information 276 */ 277 protected function validate_version_php() { 278 279 if (!isset($this->assertions['plugintype'])) { 280 throw new coding_exception('Required plugin type must be set before calling this'); 281 } 282 283 if (!isset($this->assertions['moodleversion'])) { 284 throw new coding_exception('Required Moodle version must be set before calling this'); 285 } 286 287 $fullpath = $this->extractdir.'/'.$this->rootdir.'/version.php'; 288 289 if (!file_exists($fullpath)) { 290 // This is tolerated for themes only. 291 if ($this->assertions['plugintype'] === 'theme') { 292 $this->add_message(self::DEBUG, 'missingversionphp'); 293 return true; 294 } else { 295 $this->add_message(self::ERROR, 'missingversionphp'); 296 return false; 297 } 298 } 299 300 $this->versionphp = array(); 301 $info = $this->parse_version_php($fullpath); 302 303 if ($this->assertions['plugintype'] === 'mod') { 304 $type = 'module'; 305 } else { 306 $type = 'plugin'; 307 } 308 309 if (!isset($info[$type.'->version'])) { 310 if ($type === 'module' and isset($info['plugin->version'])) { 311 // Expect the activity module using $plugin in version.php instead of $module. 312 $type = 'plugin'; 313 $this->versionphp['version'] = $info[$type.'->version']; 314 $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']); 315 } else { 316 $this->add_message(self::ERROR, 'missingversion'); 317 return false; 318 } 319 } else { 320 $this->versionphp['version'] = $info[$type.'->version']; 321 $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']); 322 } 323 324 if (isset($info[$type.'->requires'])) { 325 $this->versionphp['requires'] = $info[$type.'->requires']; 326 if ($this->versionphp['requires'] > $this->assertions['moodleversion']) { 327 $this->add_message(self::ERROR, 'requiresmoodle', $this->versionphp['requires']); 328 return false; 329 } 330 $this->add_message(self::INFO, 'requiresmoodle', $this->versionphp['requires']); 331 } 332 333 if (isset($info[$type.'->component'])) { 334 $this->versionphp['component'] = $info[$type.'->component']; 335 list($reqtype, $reqname) = core_component::normalize_component($this->versionphp['component']); 336 if ($reqtype !== $this->assertions['plugintype']) { 337 $this->add_message(self::ERROR, 'componentmismatchtype', array( 338 'expected' => $this->assertions['plugintype'], 339 'found' => $reqtype)); 340 return false; 341 } 342 if ($reqname !== $this->rootdir) { 343 $this->add_message(self::ERROR, 'componentmismatchname', $reqname); 344 return false; 345 } 346 $this->add_message(self::INFO, 'componentmatch', $this->versionphp['component']); 347 } 348 349 if (isset($info[$type.'->maturity'])) { 350 $this->versionphp['maturity'] = $info[$type.'->maturity']; 351 if ($this->versionphp['maturity'] === 'MATURITY_STABLE') { 352 $this->add_message(self::INFO, 'maturity', $this->versionphp['maturity']); 353 } else { 354 $this->add_message(self::WARNING, 'maturity', $this->versionphp['maturity']); 355 } 356 } 357 358 if (isset($info[$type.'->release'])) { 359 $this->versionphp['release'] = $info[$type.'->release']; 360 $this->add_message(self::INFO, 'release', $this->versionphp['release']); 361 } 362 363 return true; 364 } 365 366 /** 367 * @return bool false if the English language pack is not provided correctly 368 */ 369 protected function validate_language_pack() { 370 371 if (!isset($this->assertions['plugintype'])) { 372 throw new coding_exception('Required plugin type must be set before calling this'); 373 } 374 375 if (!isset($this->extractfiles[$this->rootdir.'/lang/en/']) 376 or $this->extractfiles[$this->rootdir.'/lang/en/'] !== true 377 or !is_dir($this->extractdir.'/'.$this->rootdir.'/lang/en')) { 378 $this->add_message(self::ERROR, 'missinglangenfolder'); 379 return false; 380 } 381 382 $langfiles = array(); 383 foreach (array_keys($this->extractfiles) as $extractfile) { 384 $matches = array(); 385 if (preg_match('#^'.preg_quote($this->rootdir).'/lang/en/([^/]+).php?$#i', $extractfile, $matches)) { 386 $langfiles[] = $matches[1]; 387 } 388 } 389 390 if (empty($langfiles)) { 391 $this->add_message(self::ERROR, 'missinglangenfile'); 392 return false; 393 } else if (count($langfiles) > 1) { 394 $this->add_message(self::WARNING, 'multiplelangenfiles'); 395 } else { 396 $this->langfilename = $langfiles[0]; 397 $this->add_message(self::DEBUG, 'foundlangfile', $this->langfilename); 398 } 399 400 if ($this->assertions['plugintype'] === 'mod') { 401 $expected = $this->rootdir.'.php'; 402 } else { 403 $expected = $this->assertions['plugintype'].'_'.$this->rootdir.'.php'; 404 } 405 406 if (!isset($this->extractfiles[$this->rootdir.'/lang/en/'.$expected]) 407 or $this->extractfiles[$this->rootdir.'/lang/en/'.$expected] !== true 408 or !is_file($this->extractdir.'/'.$this->rootdir.'/lang/en/'.$expected)) { 409 $this->add_message(self::ERROR, 'missingexpectedlangenfile', $expected); 410 return false; 411 } 412 413 return true; 414 } 415 416 417 /** 418 * @return bool false of the given add-on can't be installed into its location 419 */ 420 public function validate_target_location() { 421 422 if (!isset($this->assertions['plugintype'])) { 423 throw new coding_exception('Required plugin type must be set before calling this'); 424 } 425 426 $plugintypepath = $this->get_plugintype_location($this->assertions['plugintype']); 427 428 if (is_null($plugintypepath)) { 429 $this->add_message(self::ERROR, 'unknowntype', $this->assertions['plugintype']); 430 return false; 431 } 432 433 if (!is_dir($plugintypepath)) { 434 throw new coding_exception('Plugin type location does not exist!'); 435 } 436 437 $target = $plugintypepath.'/'.$this->rootdir; 438 439 if (file_exists($target)) { 440 $this->add_message(self::ERROR, 'targetexists', $target); 441 return false; 442 } 443 444 if (is_writable($plugintypepath)) { 445 $this->add_message(self::INFO, 'pathwritable', $plugintypepath); 446 } else { 447 $this->add_message(self::ERROR, 'pathwritable', $plugintypepath); 448 return false; 449 } 450 451 return true; 452 } 453 454 // Helper methods ////////////////////////////////////////////////////////// 455 456 /** 457 * Get as much information from existing version.php as possible 458 * 459 * @param string full path to the version.php file 460 * @return array of found meta-info declarations 461 */ 462 protected function parse_version_php($fullpath) { 463 464 $content = $this->get_stripped_file_contents($fullpath); 465 466 preg_match_all('#\$((plugin|module)\->(version|maturity|release|requires))=()(\d+(\.\d+)?);#m', $content, $matches1); 467 preg_match_all('#\$((plugin|module)\->(maturity))=()(MATURITY_\w+);#m', $content, $matches2); 468 preg_match_all('#\$((plugin|module)\->(release))=([\'"])(.*?)\4;#m', $content, $matches3); 469 preg_match_all('#\$((plugin|module)\->(component))=([\'"])(.+?_.+?)\4;#m', $content, $matches4); 470 471 if (count($matches1[1]) + count($matches2[1]) + count($matches3[1]) + count($matches4[1])) { 472 $info = array_combine( 473 array_merge($matches1[1], $matches2[1], $matches3[1], $matches4[1]), 474 array_merge($matches1[5], $matches2[5], $matches3[5], $matches4[5]) 475 ); 476 477 } else { 478 $info = array(); 479 } 480 481 return $info; 482 } 483 484 /** 485 * Append the given message to the messages log 486 * 487 * @param string $level e.g. self::ERROR 488 * @param string $msgcode may form a string 489 * @param string|array|object $a optional additional info suitable for {@link get_string()} 490 */ 491 protected function add_message($level, $msgcode, $a = null) { 492 $msg = (object)array( 493 'level' => $level, 494 'msgcode' => $msgcode, 495 'addinfo' => $a, 496 ); 497 $this->messages[] = $msg; 498 } 499 500 /** 501 * Returns bare PHP code from the given file 502 * 503 * Returns contents without PHP opening and closing tags, text outside php code, 504 * comments and extra whitespaces. 505 * 506 * @param string $fullpath full path to the file 507 * @return string 508 */ 509 protected function get_stripped_file_contents($fullpath) { 510 511 $source = file_get_contents($fullpath); 512 $tokens = token_get_all($source); 513 $output = ''; 514 $doprocess = false; 515 foreach ($tokens as $token) { 516 if (is_string($token)) { 517 // Simple one character token. 518 $id = -1; 519 $text = $token; 520 } else { 521 // Token array. 522 list($id, $text) = $token; 523 } 524 switch ($id) { 525 case T_WHITESPACE: 526 case T_COMMENT: 527 case T_ML_COMMENT: 528 case T_DOC_COMMENT: 529 // Ignore whitespaces, inline comments, multiline comments and docblocks. 530 break; 531 case T_OPEN_TAG: 532 // Start processing. 533 $doprocess = true; 534 break; 535 case T_CLOSE_TAG: 536 // Stop processing. 537 $doprocess = false; 538 break; 539 default: 540 // Anything else is within PHP tags, return it as is. 541 if ($doprocess) { 542 $output .= $text; 543 if ($text === 'function') { 544 // Explicitly keep the whitespace that would be ignored. 545 $output .= ' '; 546 } 547 } 548 break; 549 } 550 } 551 552 return $output; 553 } 554 555 556 /** 557 * Returns the full path to the root directory of the given plugin type 558 * 559 * @param string $plugintype 560 * @return string|null 561 */ 562 public function get_plugintype_location($plugintype) { 563 564 $plugintypepath = null; 565 566 foreach (core_component::get_plugin_types() as $type => $fullpath) { 567 if ($type === $plugintype) { 568 $plugintypepath = $fullpath; 569 break; 570 } 571 } 572 573 return $plugintypepath; 574 } 575 }
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 |