--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <david@moodle.com>
+ * @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 <david@moodle.com>
+ * @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;
+ }
+}
--- /dev/null
+Plugin must have more than one file.
--- /dev/null
+<?php
+
+$plugin->component = 'repository_mahara';
+$plugin->version = 2014010100;
--- /dev/null
+<?php
+
+echo 'One, my little hobbit, never installs malicisous add-ons';
--- /dev/null
+<?php
+
+$string['pluginname'] = 'Foo bar!';
--- /dev/null
+<?php
+
+$plugin->version = 2013031900;
+$plugin->component = 'local_greenbar';
--- /dev/null
+<?php
+
+$plugin->component = 'local_one';
--- /dev/null
+Only one dir is allowed
--- /dev/null
+<?php
+
+$string['pluginnname'] = 'Root directory mismatch';
--- /dev/null
+<?php
+
+$module->version = 2014122455;
+$plugin->version = 2014122455;
--- /dev/null
+<?php
+
+echo 'Do not use hardcoded strings, provide the language pack';
--- /dev/null
+<?php
+
+$string['pluginversion'] = 'Activity module with no version.php';
--- /dev/null
+<?php
+
+$string['pluginversion'] = 'A theme with no version.php';
--- /dev/null
+<?php
+
+// index.php
--- /dev/null
+<?php
+
+$string['pluginname'] = 'Foo!';
--- /dev/null
+<?php // $Id$
+
+// I don't miss CVS, do you?
--- /dev/null
+<?php
+
+echo 'One, my little hobbit, never installs malicisous add-ons';
--- /dev/null
+<?php
+
+$string['pluginname'] = 'Foo bar!';
--- /dev/null
+<?php
+
+$module->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';
--- /dev/null
+<h1>Example version.php file</h1>
+
+<p>version.php is required for all plugins but themes.</p>
+
+<h2>Example of values</h2>
+
+<pre>
+ $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);
+</pre>
+
+Replace $plugin with $module for activity modules, as in
+
+<pre>
+ $module->version = 2012122400;
+</pre><?php // $Id$ $module->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;
--- /dev/null
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides the unit tests class and some helper classes
+ *
+ * @package tool_installaddon
+ * @category test
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @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 <david@moodle.com>
+ * @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 <david@moodle.com>
+ * @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;
+ }
+}