diff --git a/admin/tool/installaddon/classes/validator.php b/admin/tool/installaddon/classes/validator.php new file mode 100644 index 0000000000000..557fbc1d66412 --- /dev/null +++ b/admin/tool/installaddon/classes/validator.php @@ -0,0 +1,575 @@ +. + +/** + * Provides validation class to check the plugin ZIP contents + * + * Uses fragments of the local_plugins_archive_validator class copyrighted by + * Marina Glancy that is part of the local_plugins plugin. + * + * @package tool_installaddon + * @subpackage classes + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if (!defined('T_ML_COMMENT')) { + define('T_ML_COMMENT', T_COMMENT); +} else { + define('T_DOC_COMMENT', T_ML_COMMENT); +} + +/** + * Validates the contents of extracted plugin ZIP file + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_installaddon_validator { + + /** Critical error message level, causes the validation fail. */ + const ERROR = 'error'; + + /** Warning message level, validation does not fail but the admin should be always informed. */ + const WARNING = 'warning'; + + /** Information message level that the admin should be aware of. */ + const INFO = 'info'; + + /** Debugging message level, should be displayed in debugging mode only. */ + const DEBUG = 'debug'; + + /** @var string full path to the extracted ZIP contents */ + protected $extractdir = null; + + /** @var array as returned by {@link zip_packer::extract_to_pathname()} */ + protected $extractfiles = null; + + /** @var bool overall result of validation */ + protected $result = null; + + /** @var string the name of the plugin root directory */ + protected $rootdir = null; + + /** @var array explicit list of expected/required characteristics of the ZIP */ + protected $assertions = null; + + /** @var array of validation log messages */ + protected $messages = array(); + + /** @var array|null array of relevant data obtained from version.php */ + protected $versionphp = null; + + /** @var string|null the name of found English language file without the .php extension */ + protected $langfilename = null; + + /** @var moodle_url|null URL to continue with the installation of validated add-on */ + protected $continueurl = null; + + /** + * Factory method returning instance of the validator + * + * @param string $zipcontentpath full path to the extracted ZIP contents + * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error + * @return tool_installaddon_validator + */ + public static function instance($zipcontentpath, array $zipcontentfiles) { + return new static($zipcontentpath, $zipcontentfiles); + } + + /** + * Set the expected plugin type, fail the validation otherwise + * + * @param string $required plugin type + */ + public function assert_plugin_type($required) { + $this->assertions['plugintype'] = $required; + } + + /** + * Set the expectation that the plugin can be installed into the given Moodle version + * + * @param string $required Moodle version we are about to install to + */ + public function assert_moodle_version($required) { + $this->assertions['moodleversion'] = $required; + } + + /** + * Execute the validation process against all explicit and implicit requirements + * + * Returns true if the validation passes (all explicit and implicit requirements + * pass) and the plugin can be installed. Returns false if the validation fails + * (some explicit or implicit requirement fails) and the plugin must not be + * installed. + * + * @return bool + */ + public function execute() { + + $this->result = ( + $this->validate_files_layout() + and $this->validate_version_php() + and $this->validate_language_pack() + and $this->validate_target_location() + ); + + return $this->result; + } + + /** + * Returns overall result of the validation. + * + * Null is returned if the validation has not been executed yet. Otherwise + * this method returns true (the installation can continue) or false (it is not + * safe to continue with the installation). + * + * @return bool|null + */ + public function get_result() { + return $this->result; + } + + /** + * Return the list of validation log messages + * + * Each validation message is a plain object with properties level, msgcode + * and addinfo. + * + * @return array of (int)index => (stdClass) validation message + */ + public function get_messages() { + return $this->messages; + } + + /** + * Return the information provided by the the plugin's version.php + * + * If version.php was not found in the plugin (which is tolerated for + * themes only at the moment), null is returned. Otherwise the array + * is returned. It may be empty if no information was parsed (which + * should not happen). + * + * @return null|array + */ + public function get_versionphp_info() { + return $this->versionphp; + } + + /** + * Returns the name of the English language file without the .php extension + * + * This can be used as a suggestion for fixing the plugin root directory in the + * ZIP file during the upload. If no file was found, or multiple PHP files are + * located in lang/en/ folder, then null is returned. + * + * @return null|string + */ + public function get_language_file_name() { + return $this->langfilename; + } + + /** + * Returns the rootdir of the extracted package (after eventual renaming) + * + * @return string|null + */ + public function get_rootdir() { + return $this->rootdir; + } + + /** + * Sets the URL to continue to after successful validation + * + * @param moodle_url $url + */ + public function set_continue_url(moodle_url $url) { + $this->continueurl = $url; + } + + /** + * Get the URL to continue to after successful validation + * + * Null is returned if the URL has not been explicitly set by the caller. + * + * @return moodle_url|null + */ + public function get_continue_url() { + return $this->continueurl; + } + + // End of external API ///////////////////////////////////////////////////// + + /** + * @param string $zipcontentpath full path to the extracted ZIP contents + * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error + */ + protected function __construct($zipcontentpath, array $zipcontentfiles) { + $this->extractdir = $zipcontentpath; + $this->extractfiles = $zipcontentfiles; + } + + // Validation methods ////////////////////////////////////////////////////// + + /** + * @return bool false if files in the ZIP do not have required layout + */ + protected function validate_files_layout() { + + if (!is_array($this->extractfiles) or count($this->extractfiles) < 4) { + // We need the English language pack with the name of the plugin at least + $this->add_message(self::ERROR, 'filesnumber'); + return false; + } + + foreach ($this->extractfiles as $filerelname => $filestatus) { + if ($filestatus !== true) { + $this->add_message(self::ERROR, 'filestatus', array('file' => $filerelname, 'status' => $filestatus)); + return false; + } + } + + foreach (array_keys($this->extractfiles) as $filerelname) { + if (!file_exists($this->extractdir.'/'.$filerelname)) { + $this->add_message(self::ERROR, 'filenotexists', array('file' => $filerelname)); + return false; + } + } + + foreach (array_keys($this->extractfiles) as $filerelname) { + $matches = array(); + if (!preg_match("#^([^/]+)/#", $filerelname, $matches) or (!is_null($this->rootdir) and $this->rootdir !== $matches[1])) { + $this->add_message(self::ERROR, 'onedir'); + return false; + } + $this->rootdir = $matches[1]; + } + + if ($this->rootdir !== clean_param($this->rootdir, PARAM_PLUGIN)) { + $this->add_message(self::ERROR, 'rootdirinvalid', $this->rootdir); + return false; + } else { + $this->add_message(self::INFO, 'rootdir', $this->rootdir); + } + + return is_dir($this->extractdir.'/'.$this->rootdir); + } + + /** + * @return bool false if the version.php file does not declare required information + */ + protected function validate_version_php() { + + if (!isset($this->assertions['plugintype'])) { + throw new coding_exception('Required plugin type must be set before calling this'); + } + + if (!isset($this->assertions['moodleversion'])) { + throw new coding_exception('Required Moodle version must be set before calling this'); + } + + $fullpath = $this->extractdir.'/'.$this->rootdir.'/version.php'; + + if (!file_exists($fullpath)) { + // This is tolerated for themes only. + if ($this->assertions['plugintype'] === 'theme') { + $this->add_message(self::DEBUG, 'missingversionphp'); + return true; + } else { + $this->add_message(self::ERROR, 'missingversionphp'); + return false; + } + } + + $this->versionphp = array(); + $info = $this->parse_version_php($fullpath); + + if ($this->assertions['plugintype'] === 'mod') { + $type = 'module'; + } else { + $type = 'plugin'; + } + + if (!isset($info[$type.'->version'])) { + if ($type === 'module' and isset($info['plugin->version'])) { + // Expect the activity module using $plugin in version.php instead of $module. + $type = 'plugin'; + $this->versionphp['version'] = $info[$type.'->version']; + $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']); + } else { + $this->add_message(self::ERROR, 'missingversion'); + return false; + } + } else { + $this->versionphp['version'] = $info[$type.'->version']; + $this->add_message(self::INFO, 'pluginversion', $this->versionphp['version']); + } + + if (isset($info[$type.'->requires'])) { + $this->versionphp['requires'] = $info[$type.'->requires']; + if ($this->versionphp['requires'] > $this->assertions['moodleversion']) { + $this->add_message(self::ERROR, 'requiresmoodle', $this->versionphp['requires']); + return false; + } + $this->add_message(self::INFO, 'requiresmoodle', $this->versionphp['requires']); + } + + if (isset($info[$type.'->component'])) { + $this->versionphp['component'] = $info[$type.'->component']; + list($reqtype, $reqname) = normalize_component($this->versionphp['component']); + if ($reqtype !== $this->assertions['plugintype']) { + $this->add_message(self::ERROR, 'componentmismatchtype', array( + 'expected' => $this->assertions['plugintype'], + 'found' => $reqtype)); + return false; + } + if ($reqname !== $this->rootdir) { + $this->add_message(self::ERROR, 'componentmismatchname', $reqname); + return false; + } + $this->add_message(self::INFO, 'componentmatch', $this->versionphp['component']); + } + + if (isset($info[$type.'->maturity'])) { + $this->versionphp['maturity'] = $info[$type.'->maturity']; + if ($this->versionphp['maturity'] === 'MATURITY_STABLE') { + $this->add_message(self::INFO, 'maturity', $this->versionphp['maturity']); + } else { + $this->add_message(self::WARNING, 'maturity', $this->versionphp['maturity']); + } + } + + if (isset($info[$type.'->release'])) { + $this->versionphp['release'] = $info[$type.'->release']; + $this->add_message(self::INFO, 'release', $this->versionphp['release']); + } + + return true; + } + + /** + * @return bool false if the English language pack is not provided correctly + */ + protected function validate_language_pack() { + + if (!isset($this->assertions['plugintype'])) { + throw new coding_exception('Required plugin type must be set before calling this'); + } + + if (!isset($this->extractfiles[$this->rootdir.'/lang/en/']) + or $this->extractfiles[$this->rootdir.'/lang/en/'] !== true + or !is_dir($this->extractdir.'/'.$this->rootdir.'/lang/en')) { + $this->add_message(self::ERROR, 'missinglangenfolder'); + return false; + } + + $langfiles = array(); + foreach (array_keys($this->extractfiles) as $extractfile) { + $matches = array(); + if (preg_match('#^'.preg_quote($this->rootdir).'/lang/en/([^/]+).php?$#i', $extractfile, $matches)) { + $langfiles[] = $matches[1]; + } + } + + if (empty($langfiles)) { + $this->add_message(self::ERROR, 'missinglangenfile'); + return false; + } else if (count($langfiles) > 1) { + $this->add_message(self::WARNING, 'multiplelangenfiles'); + } else { + $this->langfilename = $langfiles[0]; + $this->add_message(self::DEBUG, 'foundlangfile', $this->langfilename); + } + + if ($this->assertions['plugintype'] === 'mod') { + $expected = $this->rootdir.'.php'; + } else { + $expected = $this->assertions['plugintype'].'_'.$this->rootdir.'.php'; + } + + if (!isset($this->extractfiles[$this->rootdir.'/lang/en/'.$expected]) + or $this->extractfiles[$this->rootdir.'/lang/en/'.$expected] !== true + or !is_file($this->extractdir.'/'.$this->rootdir.'/lang/en/'.$expected)) { + $this->add_message(self::ERROR, 'missingexpectedlangenfile', $expected); + return false; + } + + return true; + } + + + /** + * @return bool false of the given add-on can't be installed into its location + */ + public function validate_target_location() { + + if (!isset($this->assertions['plugintype'])) { + throw new coding_exception('Required plugin type must be set before calling this'); + } + + $plugintypepath = $this->get_plugintype_location($this->assertions['plugintype']); + + if (is_null($plugintypepath)) { + $this->add_message(self::ERROR, 'unknowntype', $this->assertions['plugintype']); + return false; + } + + if (!is_dir($plugintypepath)) { + throw new coding_exception('Plugin type location does not exist!'); + } + + $target = $plugintypepath.'/'.$this->rootdir; + + if (file_exists($target)) { + $this->add_message(self::ERROR, 'targetexists', $target); + return false; + } + + if (is_writable($plugintypepath)) { + $this->add_message(self::INFO, 'pathwritable', $plugintypepath); + } else { + $this->add_message(self::ERROR, 'pathwritable', $plugintypepath); + return false; + } + + return true; + } + + // Helper methods ////////////////////////////////////////////////////////// + + /** + * Get as much information from existing version.php as possible + * + * @param string full path to the version.php file + * @return array of found meta-info declarations + */ + protected function parse_version_php($fullpath) { + + $content = $this->get_stripped_file_contents($fullpath); + + preg_match_all('#\$((plugin|module)\->(version|maturity|release|requires))=()(\d+(\.\d+)?);#m', $content, $matches1); + preg_match_all('#\$((plugin|module)\->(maturity))=()(MATURITY_\w+);#m', $content, $matches2); + preg_match_all('#\$((plugin|module)\->(release))=([\'"])(.*?)\4;#m', $content, $matches3); + preg_match_all('#\$((plugin|module)\->(component))=([\'"])(.+?_.+?)\4;#m', $content, $matches4); + + if (count($matches1[1]) + count($matches2[1]) + count($matches3[1]) + count($matches4[1])) { + $info = array_combine( + array_merge($matches1[1], $matches2[1], $matches3[1], $matches4[1]), + array_merge($matches1[5], $matches2[5], $matches3[5], $matches4[5]) + ); + + } else { + $info = array(); + } + + return $info; + } + + /** + * Append the given message to the messages log + * + * @param string $level e.g. self::ERROR + * @param string $msgcode may form a string + * @param string|array|object $a optional additional info suitable for {@link get_string()} + */ + protected function add_message($level, $msgcode, $a = null) { + $msg = (object)array( + 'level' => $level, + 'msgcode' => $msgcode, + 'addinfo' => $a, + ); + $this->messages[] = $msg; + } + + /** + * Returns bare PHP code from the given file + * + * Returns contents without PHP opening and closing tags, text outside php code, + * comments and extra whitespaces. + * + * @param string $fullpath full path to the file + * @return string + */ + protected function get_stripped_file_contents($fullpath) { + + $source = file_get_contents($fullpath); + $tokens = token_get_all($source); + $output = ''; + $doprocess = false; + foreach ($tokens as $token) { + if (is_string($token)) { + // Simple one character token. + $id = -1; + $text = $token; + } else { + // Token array. + list($id, $text) = $token; + } + switch ($id) { + case T_WHITESPACE: + case T_COMMENT: + case T_ML_COMMENT: + case T_DOC_COMMENT: + // Ignore whitespaces, inline comments, multiline comments and docblocks. + break; + case T_OPEN_TAG: + // Start processing. + $doprocess = true; + break; + case T_CLOSE_TAG: + // Stop processing. + $doprocess = false; + break; + default: + // Anything else is within PHP tags, return it as is. + if ($doprocess) { + $output .= $text; + if ($text === 'function') { + // Explicitly keep the whitespace that would be ignored. + $output .= ' '; + } + } + break; + } + } + + return $output; + } + + + /** + * Returns the full path to the root directory of the given plugin type + * + * @param string $plugintype + * @return string|null + */ + public function get_plugintype_location($plugintype) { + + $plugintypepath = null; + + foreach (get_plugin_types() as $type => $fullpath) { + if ($type === $plugintype) { + $plugintypepath = $fullpath; + break; + } + } + + return $plugintypepath; + } +} diff --git a/admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt b/admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt new file mode 100644 index 0000000000000..158d760751bd8 --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt @@ -0,0 +1 @@ +Plugin must have more than one file. diff --git a/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php b/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php new file mode 100644 index 0000000000000..b3d9bbc7f3711 --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php @@ -0,0 +1 @@ +component = 'repository_mahara'; +$plugin->version = 2014010100; diff --git a/admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php b/admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php new file mode 100644 index 0000000000000..c65b6f7652dd1 --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php @@ -0,0 +1,3 @@ +version = 2013031900; +$plugin->component = 'local_greenbar'; diff --git a/admin/tool/installaddon/tests/fixtures/multidir/one/version.php b/admin/tool/installaddon/tests/fixtures/multidir/one/version.php new file mode 100644 index 0000000000000..84af5f26a90d9 --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/multidir/one/version.php @@ -0,0 +1,3 @@ +component = 'local_one'; diff --git a/admin/tool/installaddon/tests/fixtures/multidir/two/README.txt b/admin/tool/installaddon/tests/fixtures/multidir/two/README.txt new file mode 100644 index 0000000000000..84e1459a1305b --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/multidir/two/README.txt @@ -0,0 +1 @@ +Only one dir is allowed diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/index.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/index.php new file mode 100644 index 0000000000000..b3d9bbc7f3711 --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/nolang/bah/index.php @@ -0,0 +1 @@ +version = 2014122455; +$plugin->version = 2014122455; diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/view.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/view.php new file mode 100644 index 0000000000000..654f0709e84be --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/nolang/bah/view.php @@ -0,0 +1,3 @@ +version = 10; // Ignored, this should use $plugin +$plugin->version = 2013031900; +$plugin->component = 'local_foobar'; +$plugin->requires = 2013031200; +$module->release = 'We are not an activity module!'; +$plugin->maturity = MATURITY_ALPHA; +//$plugin->release = 'And this is commented'; diff --git a/admin/tool/installaddon/tests/fixtures/versionphp/version1.php b/admin/tool/installaddon/tests/fixtures/versionphp/version1.php new file mode 100644 index 0000000000000..61646f05b9137 --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/versionphp/version1.php @@ -0,0 +1,52 @@ +

Example version.php file

+ +

version.php is required for all plugins but themes.

+ +

Example of values

+ +
+    $plugin->version = 2011051000;
+    $plugin->requires = 2010112400;
+    $plugin->cron = 0;
+    $plugin->component = 'plugintype_pluginname';
+    $plugin->maturity = MATURITY_STABLE;
+    $plugin->release = '2.x (Build: 2011051000)';
+    $plugin->dependencies = array('mod_forum' => ANY_VERSION, 'mod_data' => 2010020300);
+
+ +Replace $plugin with $module for activity modules, as in + +
+    $module->version = 2012122400;
+
version = 1; + + $plugin->component + = 'old_foobar';//$plugin->component='commented'; + + $plugin->component = + 'block_foobar'; + +$plugin->version = 2013010100; + ////////$plugin->version = 0; + /* for activity + modules use: + $module->version = 2014131300; + + ***/ +$plugin->version = "2010091855"; // Do not use quotes here. +$plugin->version = '2010091856.9'; // Do not use quotes here. + + +$plugin->requires = /* 2012010100 */ 2012122401 ; + +$module->maturity = MATURITY_STABLE; +$module->maturity = 50; // If both present, the constant wins (on contrary to what PHP would do) +$module->maturity = 'MATURITY_BETA'; // Do not use quotes here. + +$plugin->maturity = 10; +$plugin->maturity = MATURITY_ALPHA; + + + +$module->release = 2.3; $plugin->release = 'v2.4'; +$module->release = "v2.3"; $plugin->release = 2.4; diff --git a/admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt b/admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt new file mode 100644 index 0000000000000..a37b9435fb4e6 --- /dev/null +++ b/admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt @@ -0,0 +1 @@ +Existing diff --git a/admin/tool/installaddon/tests/validator_test.php b/admin/tool/installaddon/tests/validator_test.php new file mode 100644 index 0000000000000..23d54a088f98f --- /dev/null +++ b/admin/tool/installaddon/tests/validator_test.php @@ -0,0 +1,322 @@ +. + +/** + * Provides the unit tests class and some helper classes + * + * @package tool_installaddon + * @category test + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/installaddon/classes/validator.php'); + + +/** + * Unit tests for the {@link tool_installaddon_installer} class + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_installaddon_validator_test extends basic_testcase { + + public function test_validate_files_layout() { + $fixtures = dirname(__FILE__).'/fixtures'; + + // Non-existing directory. + $validator = tool_installaddon_validator::instance($fixtures.'/nulldir', array( + 'null/' => true, + 'null/lang/' => true, + 'null/lang/en/' => true, + 'null/lang/en/null.php' => true)); + $this->assertEquals('tool_installaddon_validator', get_class($validator)); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, + 'filenotexists', array('file' => 'null/'))); + + // Missing expected file + $validator = tool_installaddon_validator::instance($fixtures.'/plugindir', array( + 'foobar/' => true, + 'foobar/version.php' => true, + 'foobar/index.php' => true, + 'foobar/lang/' => true, + 'foobar/lang/en/' => true, + 'foobar/lang/en/local_foobar.php' => true, + 'foobar/NOTEXISTS.txt' => true)); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, + 'filenotexists', array('file' => 'foobar/NOTEXISTS.txt'))); + + // Errors during ZIP extraction + $validator = tool_installaddon_validator::instance($fixtures.'/multidir', array( + 'one/' => true, + 'one/version.php' => 'Can not write target file', + 'two/' => true, + 'two/README.txt' => true)); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'filestatus', + array('file' => 'one/version.php', 'status' => 'Can not write target file'))); + + // Insufficient number of extracted files + $validator = tool_installaddon_validator::instance($fixtures.'/emptydir', array( + 'emptydir/' => true, + 'emptydir/README.txt' => true)); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'filesnumber')); + + // No wrapping directory + $validator = tool_installaddon_validator::instance($fixtures.'/nowrapdir', array( + 'version.php' => true, + 'index.php' => true, + 'lang/' => true, + 'lang/en/' => true, + 'lang/en/foo.php' => true)); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir')); + + // Multiple directories + $validator = tool_installaddon_validator::instance($fixtures.'/multidir', array( + 'one/' => true, + 'one/version.php' => true, + 'two/' => true, + 'two/README.txt' => true)); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir')); + + // Invalid root directory name + $validator = tool_installaddon_validator::instance($fixtures.'/github', array( + 'moodle-repository_mahara-master/' => true, + 'moodle-repository_mahara-master/lang/' => true, + 'moodle-repository_mahara-master/lang/en/' => true, + 'moodle-repository_mahara-master/lang/en/repository_mahara.php' => true, + 'moodle-repository_mahara-master/version.php' => true)); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'rootdirinvalid', + 'moodle-repository_mahara-master')); + } + + public function test_validate_version_php() { + $fixtures = dirname(__FILE__).'/fixtures'; + + $validator = tool_installaddon_validator::instance($fixtures.'/noversiontheme', array( + 'noversion/' => true, + 'noversion/lang/' => true, + 'noversion/lang/en/' => true, + 'noversion/lang/en/theme_noversion.php' => true)); + $validator->assert_plugin_type('theme'); + $validator->assert_moodle_version(0); + $this->assertTrue($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'missingversionphp')); + $this->assertTrue(is_null($validator->get_versionphp_info())); + + $validator = tool_installaddon_validator::instance($fixtures.'/noversionmod', array( + 'noversion/' => true, + 'noversion/lang/' => true, + 'noversion/lang/en/' => true, + 'noversion/lang/en/noversion.php' => true)); + $validator->assert_plugin_type('mod'); + $validator->assert_moodle_version(0); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingversionphp')); + + $validator = tool_installaddon_validator::instance($fixtures.'/plugindir', array( + 'foobar/' => true, + 'foobar/version.php' => true, + 'foobar/index.php' => true, + 'foobar/lang/' => true)); + $validator->assert_plugin_type('block'); + $validator->assert_moodle_version('2013031400.00'); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'componentmismatchtype', + array('expected' => 'block', 'found' => 'local'))); + + $validator = tool_installaddon_validator::instance($fixtures.'/plugindir', array( + 'foobar/' => true, + 'foobar/version.php' => true, + 'foobar/index.php' => true, + 'foobar/lang/' => true, + 'foobar/lang/en/' => true, + 'foobar/lang/en/local_foobar.php' => true)); + $validator->assert_plugin_type('local'); + $validator->assert_moodle_version('2013031400.00'); + $this->assertTrue($validator->execute()); + $this->assertTrue($validator->get_result()); + $this->assertEquals('foobar', $validator->get_rootdir()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::INFO, 'rootdir', 'foobar')); + $versionphpinfo = $validator->get_versionphp_info(); + $this->assertEquals('array', gettype($versionphpinfo)); + $this->assertEquals(4, count($versionphpinfo)); + $this->assertEquals(2013031900, $versionphpinfo['version']); + $this->assertEquals(2013031200, $versionphpinfo['requires']); + $this->assertEquals('local_foobar', $versionphpinfo['component']); + $this->assertEquals('MATURITY_ALPHA', $versionphpinfo['maturity']); // Note we get the constant name here. + $this->assertEquals(MATURITY_ALPHA, constant($versionphpinfo['maturity'])); // This is how to get the real value. + $this->assertTrue($this->has_message($validator->get_messages(), $validator::WARNING, 'maturity', 'MATURITY_ALPHA')); + } + + public function test_validate_language_pack() { + $fixtures = dirname(__FILE__).'/fixtures'; + + $validator = tool_installaddon_validator::instance($fixtures.'/nolang', array( + 'bah/' => true, + 'bah/index.php' => true, + 'bah/view.php' => true, + 'bah/version.php' => true)); + $validator->assert_plugin_type('mod'); + $validator->assert_moodle_version(0); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfolder')); + + $validator = tool_installaddon_validator::instance($fixtures.'/nolang', array( + 'bah/' => true, + 'bah/version.php' => true, + 'bah/lang/' => true, + 'bah/lang/en/' => true)); + $validator->assert_plugin_type('mod'); + $validator->assert_moodle_version(0); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfile')); + + $validator = tool_installaddon_validator::instance($fixtures.'/nolang', array( + 'bah/' => true, + 'bah/version.php' => true, + 'bah/lang/' => true, + 'bah/lang/en/' => true, + 'bah/lang/en/bleh.php' => true, + 'bah/lang/en/bah.php' => true)); + $validator->assert_plugin_type('mod'); + $validator->assert_moodle_version(0); + $this->assertTrue($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::WARNING, 'multiplelangenfiles')); + $this->assertTrue(is_null($validator->get_language_file_name())); + + $validator = tool_installaddon_validator::instance($fixtures.'/nolang', array( + 'bah/' => true, + 'bah/version.php' => true, + 'bah/lang/' => true, + 'bah/lang/en/' => true, + 'bah/lang/en/bah.php' => true)); + $validator->assert_plugin_type('block'); + $validator->assert_moodle_version(0); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingexpectedlangenfile', 'block_bah.php')); + $this->assertEquals('bah', $validator->get_language_file_name()); + + $validator = tool_installaddon_validator::instance($fixtures.'/noversiontheme', array( + 'noversion/' => true, + 'noversion/lang/' => true, + 'noversion/lang/en/' => true, + 'noversion/lang/en/theme_noversion.php' => true)); + $validator->assert_plugin_type('theme'); + $validator->assert_moodle_version(0); + $this->assertTrue($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'foundlangfile', 'theme_noversion')); + $this->assertEquals('theme_noversion', $validator->get_language_file_name()); + + $validator = tool_installaddon_validator::instance($fixtures.'/plugindir', array( + 'foobar/' => true, + 'foobar/version.php' => true, + 'foobar/index.php' => true, + 'foobar/lang/' => true, + 'foobar/lang/en/' => true, + 'foobar/lang/en/local_foobar.php' => true)); + $validator->assert_plugin_type('local'); + $validator->assert_moodle_version('2013031400.00'); + $this->assertTrue($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::DEBUG, 'foundlangfile', 'local_foobar')); + $this->assertEquals('local_foobar', $validator->get_language_file_name()); + } + + public function test_validate_target_location() { + $fixtures = dirname(__FILE__).'/fixtures'; + + $validator = testable_tool_installaddon_validator::instance($fixtures.'/installed', array( + 'greenbar/' => true, + 'greenbar/version.php' => true, + 'greenbar/index.php' => true, + 'greenbar/lang/' => true, + 'greenbar/lang/en/' => true, + 'greenbar/lang/en/local_greenbar.php' => true)); + $validator->assert_plugin_type('local'); + $validator->assert_moodle_version('2013031400.00'); + $this->assertFalse($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'targetexists', $fixtures.'/writable/local/greenbar')); + + $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array( + 'foobar/' => true, + 'foobar/version.php' => true, + 'foobar/index.php' => true, + 'foobar/lang/' => true, + 'foobar/lang/en/' => true, + 'foobar/lang/en/local_foobar.php' => true)); + $validator->assert_plugin_type('local'); + $validator->assert_moodle_version('2013031400.00'); + $this->assertTrue($validator->execute()); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::INFO, 'pathwritable', $fixtures.'/writable/local')); + } + + public function test_parse_version_php() { + $fixtures = dirname(__FILE__).'/fixtures/versionphp'; + + $validator = testable_tool_installaddon_validator::instance($fixtures, array()); + $this->assertEquals('testable_tool_installaddon_validator', get_class($validator)); + + $info = $validator->testable_parse_version_php($fixtures.'/version1.php'); + $this->assertEquals('array', gettype($info)); + $this->assertEquals(7, count($info)); + $this->assertEquals('block_foobar', $info['plugin->component']); // Later in the file. + $this->assertEquals('2013010100', $info['plugin->version']); // Numeric wins over strings. + $this->assertEquals('2012122401', $info['plugin->requires']); // Commented. + $this->assertEquals('MATURITY_STABLE', $info['module->maturity']); // Constant wins regardless the order (non-PHP behaviour). + $this->assertEquals('MATURITY_ALPHA', $info['plugin->maturity']); // Constant wins regardless the order (non-PHP behaviour). + $this->assertEquals('v2.3', $info['module->release']); // String wins over numeric (non-PHP behaviour). + $this->assertEquals('v2.4', $info['plugin->release']); // String wins over numeric (non-PHP behaviour). + } + + // Helper methods ////////////////////////////////////////////////////////// + + protected function has_message(array $messages, $level, $msgcode, $addinfo = null) { + foreach ($messages as $message) { + if ($message->level === $level and $message->msgcode === $msgcode and $message->addinfo === $addinfo) { + return true; + } + } + return false; + } +} + + +/** + * Provides access to protected methods we want to explicitly test + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class testable_tool_installaddon_validator extends tool_installaddon_validator { + + public function testable_parse_version_php($fullpath) { + return parent::parse_version_php($fullpath); + } + + public function get_plugintype_location($plugintype) { + return dirname(__FILE__).'/fixtures/writable/'.$plugintype; + } +}