[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/admin/tool/installaddon/classes/ -> validator.php (source)

   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  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1