From 203b4321597f3c4507672a32c2ae84728c63473c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Thu, 14 Mar 2013 20:44:35 +0100 Subject: [PATCH 01/22] MDL-38493 Make mdeploy.php able to install new plugin The patch implements the second mode of the mdeploy.php script. If --install option is passed, the given package is downloaded, verified and deployed to the requested location. --- mdeploy.php | 80 +++++++++++++++++++++++++++++++++++++++++++++---- mdeploytest.php | 16 ++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/mdeploy.php b/mdeploy.php index 303d3d1a442..ca9a4ce1b73 100644 --- a/mdeploy.php +++ b/mdeploy.php @@ -775,7 +775,53 @@ class worker extends singleton_pattern { $this->done(); } else if ($this->input->get_option('install')) { - // Installing a new plugin not implemented yet. + $this->log('Plugin installation requested'); + + $plugintyperoot = $this->input->get_option('typeroot'); + $pluginname = $this->input->get_option('name'); + $source = $this->input->get_option('package'); + $md5remote = $this->input->get_option('md5'); + + // Check if the plugin location if available for us. + $pluginlocation = $plugintyperoot.'/'.$pluginname; + + $this->log('New plugin code location: '.$pluginlocation); + + if (file_exists($pluginlocation)) { + throw new filesystem_exception('Unable to prepare the plugin location (directory already exists)'); + } + + if (!$this->create_directory_precheck($pluginlocation)) { + throw new filesystem_exception('Unable to prepare the plugin location (cannot create new directory)'); + } + + // Fetch the ZIP file into a temporary location. + $target = $this->target_location($source); + $this->log('Downloading package '.$source); + + if ($this->download_file($source, $target)) { + $this->log('Package downloaded into '.$target); + } else { + $this->log('cURL error ' . $this->curlerrno . ' ' . $this->curlerror); + $this->log('Unable to download the file'); + throw new download_file_exception('Unable to download the package'); + } + + // Compare MD5 checksum of the ZIP file + $md5local = md5_file($target); + + if ($md5local !== $md5remote) { + $this->log('MD5 checksum failed. Expected: '.$md5remote.' Got: '.$md5local); + throw new checksum_exception('MD5 checksum failed'); + } + $this->log('MD5 checksum ok'); + + // Unzip the plugin package file into the plugin location. + $this->unzip_plugin($target, $plugintyperoot, $pluginlocation, false); + $this->log('Package successfully extracted'); + + // Redirect to the given URL (in HTTP) or exit (in CLI). + $this->done(); } // Print help in CLI by default. @@ -1126,18 +1172,36 @@ class worker extends singleton_pattern { } /** - * Checks to see if a source foldr could be safely moved into the given new location + * Checks to see if a source folder could be safely moved into the given new location * * @param string $destination full path to the new expected location of a folder * @return bool */ protected function move_directory_target_precheck($target) { - if (file_exists($target)) { + // Check if the target folder does not exist yet, can be created + // and removed again. + $result = $this->create_directory_precheck($target); + + // At the moment, it seems to be enough to check. We may want to add + // more steps in the future. + + return $result; + } + + /** + * Make sure the given directory can be created (and removed) + * + * @param string $path full path to the folder + * @return bool + */ + protected function create_directory_precheck($path) { + + if (file_exists($path)) { return false; } - $result = mkdir($target, 02777) && rmdir($target); + $result = mkdir($path, 02777) && rmdir($path); return $result; } @@ -1273,7 +1337,9 @@ class worker extends singleton_pattern { $result = $zip->open($ziplocation); if ($result !== true) { - $this->move_directory($backuplocation, $expectedlocation); + if ($backuplocation !== false) { + $this->move_directory($backuplocation, $expectedlocation); + } throw new zip_exception('Unable to open the zip package'); } @@ -1292,7 +1358,9 @@ class worker extends singleton_pattern { if (!$zip->extractTo($plugintyperoot)) { $zip->close(); $this->remove_directory($expectedlocation, true); // just in case something was created - $this->move_directory_into($backuplocation, $expectedlocation); + if ($backuplocation !== false) { + $this->move_directory_into($backuplocation, $expectedlocation); + } throw new zip_exception('Unable to extract the zip package'); } diff --git a/mdeploytest.php b/mdeploytest.php index 76800ebadb7..7fb81c59231 100644 --- a/mdeploytest.php +++ b/mdeploytest.php @@ -106,6 +106,13 @@ class testable_worker extends worker { public function remove_directory($path, $keeppathroot = false) { return parent::remove_directory($path, $keeppathroot); } + + /** + * Provides access to the protected method. + */ + public function create_directory_precheck($path) { + return parent::create_directory_precheck($path); + } } @@ -284,4 +291,13 @@ class mdeploytest extends PHPUnit_Framework_TestCase { $this->assertTrue($worker->remove_directory($root.'/c')); $this->assertFalse(is_dir($root.'/c')); } + + public function test_create_directory_precheck() { + $worker = testable_worker::instance(); + + $root = sys_get_temp_dir().'/'.uniqid('mdeploytest', true); + $this->assertFalse(file_exists($root)); + $this->assertTrue($worker->create_directory_precheck($root)); + $this->assertFalse(file_exists($root)); // The precheck is supposed to remove it again. + } } From e91670edc054e69dbb0cc0e2e9658b62695df4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Thu, 14 Mar 2013 20:46:28 +0100 Subject: [PATCH 02/22] MDL-38493 Improve documentation of mdeploy.php --- mdeploy.php | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/mdeploy.php b/mdeploy.php index ca9a4ce1b73..6c54d59f934 100644 --- a/mdeploy.php +++ b/mdeploy.php @@ -18,14 +18,40 @@ /** * Moodle deployment utility * - * This script looks after deploying available updates to the local Moodle site. + * This script looks after deploying new add-ons and available updates for them + * to the local Moodle site. It can operate via both HTTP and CLI mode. + * Moodle itself calls this utility via the HTTP mode when the admin is about to + * install or update an add-on. You can use the CLI mode in your custom deployment + * shell scripts. * * CLI usage example: + * + * $ sudo -u apache php mdeploy.php --install \ + * --package=https://moodle.org/plugins/download.php/...zip \ + * --typeroot=/var/www/moodle/htdocs/blocks + * --name=loancalc + * --md5=... + * --dataroot=/var/www/moodle/data + * * $ sudo -u apache php mdeploy.php --upgrade \ * --package=https://moodle.org/plugins/download.php/...zip \ - * --dataroot=/home/mudrd8mz/moodledata/moodle24 + * --typeroot=/var/www/moodle/htdocs/blocks + * --name=loancalc + * --md5=... + * --dataroot=/var/www/moodle/data + * + * When called via HTTP, additional parameters returnurl, passfile and password must be + * provided. Optional proxy configuration can be passed using parameters proxy, proxytype + * and proxyuserpwd. + * + * Changes + * + * 1.1 - Added support to install a new plugin from the Moodle Plugins directory. + * 1.0 - Initial version used in Moodle 2.4 to deploy available updates. * * @package core + * @subpackage mdeploy + * @version 1.1 * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -1329,7 +1355,7 @@ class worker extends singleton_pattern { * @param string $ziplocation full path to the ZIP file * @param string $plugintyperoot full path to the plugin's type location * @param string $expectedlocation expected full path to the plugin after it is extracted - * @param string $backuplocation location of the previous version of the plugin + * @param string|bool $backuplocation location of the previous version of the plugin or false for no backup */ protected function unzip_plugin($ziplocation, $plugintyperoot, $expectedlocation, $backuplocation) { From c2c4f722b5f446bceb12caa20cf858484bd4517a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Sat, 16 Mar 2013 01:59:28 +0100 Subject: [PATCH 03/22] MDL-38509 Document the plugin deployment lockers in config-dist.php --- config-dist.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config-dist.php b/config-dist.php index c9ce7613b08..7653437eb6a 100644 --- a/config-dist.php +++ b/config-dist.php @@ -438,6 +438,16 @@ $CFG->admin = 'admin'; // // $CFG->disableupdatenotifications = true; // +// Use the following flag to completely disable the Automatic updates deployment +// feature and hide it from the server administration UI. +// +// $CFG->disableupdateautodeploy = true; +// +// Use the following flag to completely disable the On-click add-on installation +// feature and hide it from the server administration UI. +// +// $CFG->disableonclickaddoninstall = true; +// // As of version 2.4 Moodle serves icons as SVG images if the users browser appears // to support SVG. // For those wanting to control the serving of SVG images the following setting can From ce1a0d3cd8b1a7fc25f0a660f9117f424454dbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Sat, 16 Mar 2013 02:05:19 +0100 Subject: [PATCH 04/22] MDL-38509 Implement new plugin_manager::get_plugin_types() This new method just takes the result of the core get_plugin_types() and re-orders the plugin types in the same way as they are displayed at the Plugins overview screen. --- lib/pluginlib.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index f5fba435048..f0f93f7d4a2 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -96,6 +96,17 @@ class plugin_manager { } } + /** + * Returns the result of {@link get_plugin_types()} ordered for humans + * + * @see self::reorder_plugin_types() + * @param bool $fullpaths false means relative paths from dirroot + * @return array (string)name => (string)location + */ + public function get_plugin_types($fullpaths = true) { + return $this->reorder_plugin_types(get_plugin_types($fullpaths)); + } + /** * Returns a tree of known plugins and information about them * @@ -119,8 +130,7 @@ class plugin_manager { } } $this->pluginsinfo = array(); - $plugintypes = get_plugin_types(); - $plugintypes = $this->reorder_plugin_types($plugintypes); + $plugintypes = $this->get_plugin_types(); foreach ($plugintypes as $plugintype => $plugintyperootdir) { if (in_array($plugintype, array('base', 'general'))) { throw new coding_exception('Illegal usage of reserved word for plugin type'); From b8efcb9233b5d4527bfc2c2bfaf8d9d5d8e6ebbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Sat, 16 Mar 2013 02:07:34 +0100 Subject: [PATCH 05/22] MDL-38509 Implement new plugin_manager::plugintype_name() This has exactly the same behaviour as the existing plugintype_name_plural() method, but returns the singular form of the plugin type (such as 'Activity module'). --- lib/pluginlib.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index f0f93f7d4a2..dc925792a6e 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -229,6 +229,35 @@ class plugin_manager { return $this->pluginsinfo[$type][$name]->displayname; } + /** + * Returns a localized name of a plugin typed in singular form + * + * Most plugin types define their names in core_plugin lang file. In case of subplugins, + * we try to ask the parent plugin for the name. In the worst case, we will return + * the value of the passed $type parameter. + * + * @param string $type the type of the plugin, e.g. mod or workshopform + * @return string + */ + public function plugintype_name($type) { + + if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) { + // for most plugin types, their names are defined in core_plugin lang file + return get_string('type_' . $type, 'core_plugin'); + + } else if ($parent = $this->get_parent_of_subplugin($type)) { + // if this is a subplugin, try to ask the parent plugin for the name + if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) { + return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent); + } else { + return $this->plugin_name($parent) . ' / ' . $type; + } + + } else { + return $type; + } + } + /** * Returns a localized name of a plugin type in plural form * From 0056f2a37b1880d35d8f3f9b170c271cf25a6c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Sat, 16 Mar 2013 02:15:10 +0100 Subject: [PATCH 06/22] MDL-38509 Initial version of the new admin tool to install add-ons --- admin/tool/installaddon/classes/installer.php | 160 ++++++++++++++++++ .../classes/installfromzip_form.php | 76 +++++++++ admin/tool/installaddon/index.php | 50 ++++++ .../lang/en/tool_installaddon.php | 41 +++++ admin/tool/installaddon/pix/icon.png | Bin 0 -> 266 bytes admin/tool/installaddon/pix/icon.svg | 16 ++ admin/tool/installaddon/renderer.php | 114 +++++++++++++ admin/tool/installaddon/settings.php | 34 ++++ admin/tool/installaddon/styles.css | 13 ++ .../installaddon/tests/installer_test.php | 80 +++++++++ admin/tool/installaddon/version.php | 29 ++++ 11 files changed, 613 insertions(+) create mode 100644 admin/tool/installaddon/classes/installer.php create mode 100644 admin/tool/installaddon/classes/installfromzip_form.php create mode 100644 admin/tool/installaddon/index.php create mode 100644 admin/tool/installaddon/lang/en/tool_installaddon.php create mode 100644 admin/tool/installaddon/pix/icon.png create mode 100644 admin/tool/installaddon/pix/icon.svg create mode 100644 admin/tool/installaddon/renderer.php create mode 100644 admin/tool/installaddon/settings.php create mode 100644 admin/tool/installaddon/styles.css create mode 100644 admin/tool/installaddon/tests/installer_test.php create mode 100644 admin/tool/installaddon/version.php diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php new file mode 100644 index 00000000000..eb052558d5a --- /dev/null +++ b/admin/tool/installaddon/classes/installer.php @@ -0,0 +1,160 @@ +. + +/** + * Provides tool_installaddon_installer class + * + * @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(); + +/** + * Implements main plugin features. + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_installaddon_installer { + + /** + * Factory method returning an instance of this class. + * + * @return tool_installaddon_installer + */ + public static function instance() { + return new self(); + } + + /** + * Returns URL to the repository that addons can be searched in and installed from + * + * @return moodle_url + */ + public function get_addons_repository_url() { + global $CFG; + + if (!empty($CFG->config_php_settings['alternativeaddonsrepositoryurl'])) { + $url = $CFG->config_php_settings['alternativeaddonsrepositoryurl']; + } else { + $url = 'https://moodle.org/plugins/get.php'; + } + + if (!$this->should_send_site_info()) { + return new moodle_url($url); + } + + // Append the basic information about our site. + $site = array( + 'fullname' => $this->get_site_fullname(), + 'url' => $this->get_site_url(), + 'major_version' => $this->get_site_major_version(), + ); + + $site = $this->encode_site_information($site); + + return new moodle_url($url, array('site' => $site)); + } + + /** + * @return tool_installaddon_installfromzip + */ + public function get_installfromzip_form() { + global $CFG; + require_once(dirname(__FILE__).'/installfromzip_form.php'); + + $action = new moodle_url('/admin/tool/installaddon/index.php'); + $customdata = array('installer' => $this); + + return new tool_installaddon_installfromzip($action, $customdata); + } + + /** + * Returns localised list of available plugin types + * + * @return array (string)plugintype => (string)plugin name + */ + public function get_plugin_types_menu() { + global $CFG; + require_once($CFG->libdir.'/pluginlib.php'); + + $pluginman = plugin_manager::instance(); + + $menu = array('' => get_string('choosedots')); + foreach (array_keys($pluginman->get_plugin_types()) as $plugintype) { + $menu[$plugintype] = $pluginman->plugintype_name($plugintype).' ('.$plugintype.')'; + } + + return $menu; + } + + //// End of external API /////////////////////////////////////////////////// + + /** + * @return string this site full name + */ + protected function get_site_fullname() { + global $SITE; + + return $SITE->fullname; + } + + /** + * @return string this site URL + */ + protected function get_site_url() { + global $CFG; + + return $CFG->wwwroot; + } + + /** + * @return string major version like 2.5, 2.6 etc. + */ + protected function get_site_major_version() { + return moodle_major_version(); + } + + /** + * Encodes the given array in a way that can be safely appended as HTTP GET param + * + * Be ware! The recipient may rely on the exact way how the site information is encoded. + * Do not change anything here unless you know what you are doing and understand all + * consequences! (Don't you love warnings like that, too? :-p) + * + * @param array $info + * @return string + */ + protected function encode_site_information(array $info) { + return base64_encode(json_encode($info)); + } + + /** + * Decide if the encoded site information should be sent to the add-ons repository site + * + * For now, we just return true. In the future, we may want to implement some + * privacy aware logic (based on site/user preferences for example). + * + * @return bool + */ + protected function should_send_site_info() { + return true; + } +} diff --git a/admin/tool/installaddon/classes/installfromzip_form.php b/admin/tool/installaddon/classes/installfromzip_form.php new file mode 100644 index 00000000000..56ce0af0ae1 --- /dev/null +++ b/admin/tool/installaddon/classes/installfromzip_form.php @@ -0,0 +1,76 @@ +. + +/** + * @package tool_installaddon + * @subpackage classes + * @category form + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Defines a simple form for uploading the add-on ZIP package + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_installaddon_installfromzip extends moodleform { + + /** + * Defines the form elements + */ + public function definition() { + + $mform = $this->_form; + $installer = $this->_customdata['installer']; + + $options = $installer->get_plugin_types_menu(); + $mform->addElement('select', 'plugintype', get_string('installfromziptype', 'tool_installaddon'), $options); + $mform->addHelpButton('plugintype', 'installfromziptype', 'tool_installaddon'); + $mform->addRule('plugintype', null, 'required', null, 'client'); + + $mform->addElement('filepicker', 'zipfile', get_string('installfromzipfile', 'tool_installaddon'), + null, array('accepted_types' => '.zip')); + $mform->addHelpButton('zipfile', 'installfromzipfile', 'tool_installaddon'); + $mform->addRule('zipfile', null, 'required', null, 'client'); + + $mform->addElement('checkbox', 'acknowledgement', get_string('acknowledgement', 'tool_installaddon'), + ' '.get_string('acknowledgementtext', 'tool_installaddon')); + $mform->addRule('acknowledgement', get_string('acknowledgementmust', 'tool_installaddon'), 'required', null, 'client'); + + $this->add_action_buttons(false, get_string('installfromzipsubmit', 'tool_installaddon')); + } + + /** + * Validate the form fields + * + * @param array $data + * @param array $files + * @return array (string)field name => (string)validation error text + */ + public function validation($data, $files) { + + $errors = parent::validation($data, $files); + + return $errors; + } +} diff --git a/admin/tool/installaddon/index.php b/admin/tool/installaddon/index.php new file mode 100644 index 00000000000..062e05cbc69 --- /dev/null +++ b/admin/tool/installaddon/index.php @@ -0,0 +1,50 @@ +. + +/** + * The main screen of the tool. + * + * @package tool_installaddon + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(dirname(__FILE__) . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once(dirname(__FILE__).'/classes/installer.php'); + +admin_externalpage_setup('tool_installaddon_index'); + +if (!empty($CFG->disableonclickaddoninstall)) { + notice(get_string('featuredisabled', 'tool_installaddon')); +} + +$installer = tool_installaddon_installer::instance(); + +$form = $installer->get_installfromzip_form(); + +if ($form->is_cancelled()) { + redirect($PAGE->url); + +} else if ($data = $form->get_data()) { + // todo $installer->process_installfromzip_form($data); +} + +$output = $PAGE->get_renderer('tool_installaddon'); +$output->set_installer_instance($installer); + +echo $output->index_page(); diff --git a/admin/tool/installaddon/lang/en/tool_installaddon.php b/admin/tool/installaddon/lang/en/tool_installaddon.php new file mode 100644 index 00000000000..a9b9a78401b --- /dev/null +++ b/admin/tool/installaddon/lang/en/tool_installaddon.php @@ -0,0 +1,41 @@ +. + +/** + * Strings for the tool_installaddon component. + * + * @package tool_installaddon + * @category string + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['acknowledgement'] = 'Acknowledgement'; +$string['acknowledgementmust'] = 'You must acknowledge this'; +$string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing add-ons. I accept and understand that add-ons (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.'; +$string['featuredisabled'] = 'Add-on installer is disabled at this site.'; +$string['installaddons'] = 'Install add-ons'; +$string['installfromrepo'] = 'Install add-ons from Moodle plugins directory'; +$string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site fullname, URL and major version will be sent as well, to make the installation process easier for you.'; +$string['installfromzipfile'] = 'ZIP package'; +$string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory with the name of the plugin. The ZIP will be extracted into the appropriate location for the given plugin type. Packages downloaded from the Moodle plugins directory have this format.'; +$string['installfromzipsubmit'] = 'Install add-on from the ZIP file'; +$string['installfromziptype'] = 'Plugin type'; +$string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. The installation procedure may fail badly when incorrect plugin type is provided.'; +$string['pluginname'] = 'Add-on installer'; diff --git a/admin/tool/installaddon/pix/icon.png b/admin/tool/installaddon/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f2d450b5c37f47339c61e03dffb77b03b3bab6ad GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X)_A%&hE&{2N;$xoc5cp1h9$2S-Z}Q=%NMqsS2H9En5$)S z7V#=@1>`U&&)U!0aHUQ`tbt3@;U&ArDW=AUg^b4%9gPd@1f&x-dN6k0Q4`o8rh;v%uj$djkVA!;0O8%q&y>Xan8N N;OXk;vd$@?2>|sCWE=nh literal 0 HcmV?d00001 diff --git a/admin/tool/installaddon/pix/icon.svg b/admin/tool/installaddon/pix/icon.svg new file mode 100644 index 00000000000..19dead1036a --- /dev/null +++ b/admin/tool/installaddon/pix/icon.svg @@ -0,0 +1,16 @@ + + + +]> + + + + + diff --git a/admin/tool/installaddon/renderer.php b/admin/tool/installaddon/renderer.php new file mode 100644 index 00000000000..aedeca41aad --- /dev/null +++ b/admin/tool/installaddon/renderer.php @@ -0,0 +1,114 @@ +. + +/** + * Output rendering for the plugin. + * + * @package tool_installaddon + * @category output + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implements the plugin renderer + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_installaddon_renderer extends plugin_renderer_base { + + /** @var tool_installaddon_installer */ + protected $installer = null; + + /** + * Sets the tool_installaddon_installer instance being used. + * + * @throws coding_exception if the installer has been already set + * @param tool_installaddon_installer $installer + */ + public function set_installer_instance(tool_installaddon_installer $installer) { + if (is_null($this->installer)) { + $this->installer = $installer; + } else { + throw new coding_exception('Attempting to reset the installer instance.'); + } + } + + /** + * Defines the index page layout + * + * @return string + */ + public function index_page() { + + $out = $this->output->header(); + $out .= $this->index_page_heading(); + $out .= $this->index_page_repository(); + $out .= $this->index_page_upload(); + $out .= $this->output->footer(); + + return $out; + } + + /** + * Renders the index page heading + * + * @return string + */ + protected function index_page_heading() { + return $this->output->heading(get_string('pluginname', 'tool_installaddon')); + } + + /** + * Renders the widget for browsing the add-on repository + * + * @return string + */ + protected function index_page_repository() { + + $url = $this->installer->get_addons_repository_url(); + + $out = $this->box( + $this->output->single_button($url, get_string('installfromrepo', 'tool_installaddon'), 'get'). + $this->output->help_icon('installfromrepo', 'tool_installaddon'), + 'generalbox', 'installfromrepobox' + ); + + return $out; + } + + /** + * Renders the widget for uploading the add-on ZIP package + * + * @return string + */ + protected function index_page_upload() { + + $form = $this->installer->get_installfromzip_form(); + + ob_start(); + $form->display(); + $out = ob_get_clean(); + + $out = $this->box($out, 'generalbox', 'installfromzipbox'); + + return $out; + } +} diff --git a/admin/tool/installaddon/settings.php b/admin/tool/installaddon/settings.php new file mode 100644 index 00000000000..326f0309a5a --- /dev/null +++ b/admin/tool/installaddon/settings.php @@ -0,0 +1,34 @@ +. + +/** + * Puts the plugin actions into the admin settings tree. + * + * @package tool_installaddon + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig and empty($CFG->disableonclickaddoninstall)) { + + $ADMIN->add('modules', new admin_externalpage('tool_installaddon_index', + get_string('installaddons', 'tool_installaddon'), + "$CFG->wwwroot/$CFG->admin/tool/installaddon/")); + +} diff --git a/admin/tool/installaddon/styles.css b/admin/tool/installaddon/styles.css new file mode 100644 index 00000000000..3de50f35259 --- /dev/null +++ b/admin/tool/installaddon/styles.css @@ -0,0 +1,13 @@ +#page-admin-tool-installaddon-index #installfromrepobox { + text-align: center; + padding-top: 2em; + padding-bottom: 2em; +} + +#page-admin-tool-installaddon-index #installfromrepobox .singlebutton { + display: inline-block; +} + +#page-admin-tool-installaddon-index #installfromrepobox .singlebutton input[type=submit] { + padding: 1em; +} diff --git a/admin/tool/installaddon/tests/installer_test.php b/admin/tool/installaddon/tests/installer_test.php new file mode 100644 index 00000000000..a5028ddb800 --- /dev/null +++ b/admin/tool/installaddon/tests/installer_test.php @@ -0,0 +1,80 @@ +. + +/** + * 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/installer.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_installer_test extends advanced_testcase { + + public function test_get_addons_repository_url() { + $installer = new testable_tool_installaddon_installer(); + $url = $installer->get_addons_repository_url(); + $query = parse_url($url, PHP_URL_QUERY); + $this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches)); + $site = rawurldecode($matches[1]); + $site = json_decode(base64_decode($site), true); + $this->assertEquals('array', gettype($site)); + $this->assertEquals(3, count($site)); + $this->assertSame($installer->get_site_fullname(), $site['fullname']); + $this->assertSame($installer->get_site_url(), $site['url']); + $this->assertSame($installer->get_site_major_version(), $site['major_version']); + } +} + + +/** + * Testable subclass of the tested class + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class testable_tool_installaddon_installer extends tool_installaddon_installer { + + public function get_site_fullname() { + return '

Nasty site

'; + } + + public function get_site_url() { + return 'file:///etc/passwd'; + } + + public function get_site_major_version() { + return "2.5'; DROP TABLE mdl_user; --"; + } + + protected function should_send_site_info() { + return true; + } +} diff --git a/admin/tool/installaddon/version.php b/admin/tool/installaddon/version.php new file mode 100644 index 00000000000..403deefc6f8 --- /dev/null +++ b/admin/tool/installaddon/version.php @@ -0,0 +1,29 @@ +. + +/** + * @package tool_installaddon + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'tool_installaddon'; +$plugin->version = 2013031400; +$plugin->requires = 2013031400; +$plugin->maturity = MATURITY_ALPHA; From 2459758b3023b2a58eb55bcecb2a97d56987fcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Sat, 16 Mar 2013 02:17:26 +0100 Subject: [PATCH 07/22] MDL-38509 Mark the new installaddon admin tool as the standard plugin --- lib/pluginlib.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index dc925792a6e..aca067c47d0 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -609,9 +609,10 @@ class plugin_manager { ), 'tool' => array( - 'assignmentupgrade', 'behat', 'capability', 'customlang', 'dbtransfer', - 'generator', 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', - 'profiling', 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', + 'assignmentupgrade', 'behat', 'capability', 'customlang', + 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon', + 'langimport', 'multilangupgrade', 'phpunit', 'profiling', + 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest', 'uploaduser', 'unsuproles', 'xmldb' ), From ddab904ba800d0aa7d7ae824c0847aeca394e334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Tue, 19 Mar 2013 00:10:49 +0100 Subject: [PATCH 08/22] MDL-38509 Check for writable plugin type location in install from ZIP form Standard mform validation is implemented as well as progressively enhanced AJAX version. --- admin/tool/installaddon/classes/installer.php | 38 ++++++- .../classes/installfromzip_form.php | 14 ++- .../lang/en/tool_installaddon.php | 5 + admin/tool/installaddon/permcheck.php | 79 +++++++++++++ admin/tool/installaddon/renderer.php | 6 + .../installaddon/yui/permcheck/permcheck.js | 105 ++++++++++++++++++ 6 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 admin/tool/installaddon/permcheck.php create mode 100644 admin/tool/installaddon/yui/permcheck/permcheck.js diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index eb052558d5a..2833254837e 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -34,6 +34,9 @@ defined('MOODLE_INTERNAL') || die(); */ class tool_installaddon_installer { + /** @var tool_installaddon_installfromzip */ + protected $installfromzipform = null; + /** * Factory method returning an instance of this class. * @@ -80,10 +83,16 @@ class tool_installaddon_installer { global $CFG; require_once(dirname(__FILE__).'/installfromzip_form.php'); + if (!is_null($this->installfromzipform)) { + return $this->installfromzipform; + } + $action = new moodle_url('/admin/tool/installaddon/index.php'); $customdata = array('installer' => $this); - return new tool_installaddon_installfromzip($action, $customdata); + $this->installfromzipform = new tool_installaddon_installfromzip($action, $customdata); + + return $this->installfromzipform; } /** @@ -105,6 +114,33 @@ class tool_installaddon_installer { return $menu; } + /** + * Is it possible to create a new plugin directory for the given plugin type? + * + * @throws coding_exception for invalid plugin types or non-existing plugin type locations + * @param string $plugintype + * @return boolean + */ + public function is_plugintype_writable($plugintype) { + + $plugintypepath = null; + foreach (get_plugin_types() as $type => $fullpath) { + if ($type === $plugintype) { + $plugintypepath = $fullpath; + break; + } + } + if (is_null($plugintypepath)) { + throw new coding_exception('Unknown plugin type!'); + } + + if (!is_dir($plugintypepath)) { + throw new coding_exception('Plugin type location does not exist!'); + } + + return is_writable($plugintypepath); + } + //// End of external API /////////////////////////////////////////////////// /** diff --git a/admin/tool/installaddon/classes/installfromzip_form.php b/admin/tool/installaddon/classes/installfromzip_form.php index 56ce0af0ae1..aef05c7fcfb 100644 --- a/admin/tool/installaddon/classes/installfromzip_form.php +++ b/admin/tool/installaddon/classes/installfromzip_form.php @@ -44,10 +44,15 @@ class tool_installaddon_installfromzip extends moodleform { $installer = $this->_customdata['installer']; $options = $installer->get_plugin_types_menu(); - $mform->addElement('select', 'plugintype', get_string('installfromziptype', 'tool_installaddon'), $options); + $mform->addElement('select', 'plugintype', get_string('installfromziptype', 'tool_installaddon'), $options, + array('id' => 'tool_installaddon_installfromzip_plugintype')); $mform->addHelpButton('plugintype', 'installfromziptype', 'tool_installaddon'); $mform->addRule('plugintype', null, 'required', null, 'client'); + $mform->addElement('static', 'permcheck', '', + html_writer::span(get_string('permcheck', 'tool_installaddon'), '', + array('id' => 'tool_installaddon_installfromzip_permcheck'))); + $mform->addElement('filepicker', 'zipfile', get_string('installfromzipfile', 'tool_installaddon'), null, array('accepted_types' => '.zip')); $mform->addHelpButton('zipfile', 'installfromzipfile', 'tool_installaddon'); @@ -69,8 +74,15 @@ class tool_installaddon_installfromzip extends moodleform { */ public function validation($data, $files) { + $installer = $this->_customdata['installer']; $errors = parent::validation($data, $files); + if (!$installer->is_plugintype_writable($data['plugintype'])) { + $paths = get_plugin_types(true); + $path = $paths[$data['plugintype']]; + $errors['plugintype'] = get_string('permcheckresultno', 'tool_installaddon', array('path' => $path)); + } + return $errors; } } diff --git a/admin/tool/installaddon/lang/en/tool_installaddon.php b/admin/tool/installaddon/lang/en/tool_installaddon.php index a9b9a78401b..833a7f16f80 100644 --- a/admin/tool/installaddon/lang/en/tool_installaddon.php +++ b/admin/tool/installaddon/lang/en/tool_installaddon.php @@ -38,4 +38,9 @@ $string['installfromzipfile_help'] = 'The plugin ZIP package must contain just o $string['installfromzipsubmit'] = 'Install add-on from the ZIP file'; $string['installfromziptype'] = 'Plugin type'; $string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. The installation procedure may fail badly when incorrect plugin type is provided.'; +$string['permcheck'] = 'Make sure the plugin type root location is writable by the web server process'; +$string['permcheckerror'] = 'Error while checking for write permission'; +$string['permcheckprogress'] = 'Checking for write permission ...'; +$string['permcheckresultno'] = 'Plugin type location {$a->path} not writable'; +$string['permcheckresultyes'] = 'Plugin type location {$a->path} is writable'; $string['pluginname'] = 'Add-on installer'; diff --git a/admin/tool/installaddon/permcheck.php b/admin/tool/installaddon/permcheck.php new file mode 100644 index 00000000000..d2125bb6a56 --- /dev/null +++ b/admin/tool/installaddon/permcheck.php @@ -0,0 +1,79 @@ +. + +/** + * Checks the write permission for the given plugin type + * + * @package tool_installaddon + * @subpackage ajax + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('AJAX_SCRIPT', true); + +require(dirname(__FILE__) . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once(dirname(__FILE__).'/classes/installer.php'); + +require_login(); + +if (!has_capability('moodle/site:config', context_system::instance())) { + header('HTTP/1.1 403 Forbidden'); + die(); +} + +if (!empty($CFG->disableonclickaddoninstall)) { + header('HTTP/1.1 403 Forbidden'); + die(); +} + +if (!confirm_sesskey()) { + header('HTTP/1.1 403 Forbidden'); + die(); +} + +$plugintype = optional_param('plugintype', null, PARAM_ALPHANUMEXT); +if (is_null($plugintype)) { + header('HTTP/1.1 400 Bad Request'); + die(); +} + +$plugintypepath = null; +foreach (get_plugin_types() as $type => $fullpath) { + if ($type === $plugintype) { + $plugintypepath = $fullpath; + break; + } +} +if (is_null($plugintypepath)) { + header('HTTP/1.1 400 Bad Request'); + die(); +} + +$installer = tool_installaddon_installer::instance(); + +$response = array('path' => $plugintypepath); + +if ($installer->is_plugintype_writable($plugintype)) { + $response['writable'] = 1; +} else { + $response['writable'] = 0; +} + +header('Content-Type: application/json; charset: utf-8'); +echo json_encode($response); diff --git a/admin/tool/installaddon/renderer.php b/admin/tool/installaddon/renderer.php index aedeca41aad..e670c6d5e08 100644 --- a/admin/tool/installaddon/renderer.php +++ b/admin/tool/installaddon/renderer.php @@ -58,6 +58,12 @@ class tool_installaddon_renderer extends plugin_renderer_base { */ public function index_page() { + $permcheckurl = new moodle_url('/admin/tool/installaddon/permcheck.php'); + $this->page->requires->yui_module('moodle-tool_installaddon-permcheck', 'M.tool_installaddon.permcheck.init', + array(array('permcheckurl' => $permcheckurl->out()))); + $this->page->requires->strings_for_js( + array('permcheckprogress', 'permcheckresultno', 'permcheckresultyes', 'permcheckerror'), 'tool_installaddon'); + $out = $this->output->header(); $out .= $this->index_page_heading(); $out .= $this->index_page_repository(); diff --git a/admin/tool/installaddon/yui/permcheck/permcheck.js b/admin/tool/installaddon/yui/permcheck/permcheck.js new file mode 100644 index 00000000000..ad0e4bd60cd --- /dev/null +++ b/admin/tool/installaddon/yui/permcheck/permcheck.js @@ -0,0 +1,105 @@ +/** + * Check for write permission for the selected plugin type + * + * @module moodle-tool_installaddon-permcheck + * @author David Mudrak + */ +YUI.add('moodle-tool_installaddon-permcheck', function(Y) { + + M.tool_installaddon = M.tool_installaddon || {}; + + /** + * @class permcheck + * @static + */ + M.tool_installaddon.permcheck = { + + /** + * @method init + * @param {Object} config Configuration passed from the PHP + */ + init : function(config) { + this.config = config; + var plugintypesel = Y.one('#tool_installaddon_installfromzip_plugintype'); + if (plugintypesel) { + plugintypesel.on('change', this.check_for_permission, this); + } + }, + + /** + * @method check_for_permission + * @param {Event} e + */ + check_for_permission : function(e) { + var plugintype = e.currentTarget.get('value'); + if (plugintype == '') { + return; + } + Y.log('Selected plugin type: ' + plugintype, 'debug', 'moodle-tool_installaddon-permcheck'); + Y.io(this.config.permcheckurl, { + 'method' : 'GET', + 'data' : { + 'sesskey' : M.cfg.sesskey, + 'plugintype' : plugintype + }, + 'arguments' : { + 'plugintypeselector' : e.currentTarget, + 'showresult' : function(msg, status) { + var resultline = Y.one('#tool_installaddon_installfromzip_permcheck'); + if (resultline) { + if (status === 'success') { + resultline.setContent(' ' + + msg + ''); + } else if (status === 'progress') { + resultline.setContent(' ' + + msg + ''); + } else { + resultline.setContent(' ' + + msg + ''); + } + } + } + }, + 'on' : { + 'start' : function(transid, args) { + args.showresult(M.util.get_string('permcheckprogress', 'tool_installaddon'), 'progress'); + }, + 'success': function(transid, outcome, args) { + var response; + try { + response = Y.JSON.parse(outcome.responseText); + if (response.error) { + Y.log(response.error, 'error', 'moodle-tool_installaddon-permcheck'); + args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon', response), 'error'); + } else if (response.path && response.writable == 1) { + args.showresult(M.util.get_string('permcheckresultyes', 'tool_installaddon', response), 'success'); + } else if (response.path && response.writable == 0) { + args.showresult(M.util.get_string('permcheckresultno', 'tool_installaddon', response), 'error'); + } else { + Y.log(response, 'debug', 'moodle-tool_installaddon-permcheck'); + args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon', response), 'error'); + } + + } catch (e) { + Y.log(e, 'error', 'moodle-tool_installaddon-permcheck'); + args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon'), 'error'); + } + }, + 'failure': function(transid, outcome, args) { + Y.log(outcome.statusText, 'error', 'moodle-tool_installaddon-permcheck'); + args.showresult(M.util.get_string('permcheckerror', 'tool_installaddon')); + } + } + }); + }, + + /** + * @property + * @type {Object} + */ + config : null + }; + +}, '@VERSION@', { + requires:['node', 'event', 'io-base'] +}); From a4fcf56f0a5643497ad1521fad304ebb7ba23fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Wed, 20 Mar 2013 17:08:29 +0100 Subject: [PATCH 09/22] MDL-38509 Implement the plugin ZIP package validator The validator code contains (modified) fragments of the local_plugins_archive_validator class copyrighted by Marina Glancy that is part of the local_plugins plugin. It operates over an extracted copy of the ZIP file. --- admin/tool/installaddon/classes/validator.php | 575 ++++++++++++++++++ .../fixtures/emptydir/emptydir/README.txt | 1 + .../lang/en/repository_mahara.php | 1 + .../version.php | 4 + .../fixtures/installed/greenbar/index.php | 3 + .../greenbar/lang/en/local_greenbar.php | 3 + .../fixtures/installed/greenbar/version.php | 4 + .../tests/fixtures/multidir/one/version.php | 3 + .../tests/fixtures/multidir/two/README.txt | 1 + .../tests/fixtures/nolang/bah/index.php | 1 + .../tests/fixtures/nolang/bah/lang/en/bah.php | 1 + .../fixtures/nolang/bah/lang/en/bleh.php | 3 + .../tests/fixtures/nolang/bah/lib.php | 3 + .../tests/fixtures/nolang/bah/version.php | 4 + .../tests/fixtures/nolang/bah/view.php | 3 + .../noversion/lang/en/noversion.php | 3 + .../noversion/lang/en/theme_noversion.php | 3 + .../tests/fixtures/nowrapdir/index.php | 3 + .../tests/fixtures/nowrapdir/lang/en/foo.php | 3 + .../tests/fixtures/nowrapdir/version.php | 3 + .../tests/fixtures/plugindir/foobar/index.php | 3 + .../plugindir/foobar/lang/en/local_foobar.php | 3 + .../fixtures/plugindir/foobar/version.php | 9 + .../tests/fixtures/versionphp/version1.php | 52 ++ .../writable/local/greenbar/README.txt | 1 + .../installaddon/tests/validator_test.php | 322 ++++++++++ 26 files changed, 1015 insertions(+) create mode 100644 admin/tool/installaddon/classes/validator.php create mode 100644 admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt create mode 100644 admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php create mode 100644 admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/version.php create mode 100644 admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php create mode 100644 admin/tool/installaddon/tests/fixtures/installed/greenbar/lang/en/local_greenbar.php create mode 100644 admin/tool/installaddon/tests/fixtures/installed/greenbar/version.php create mode 100644 admin/tool/installaddon/tests/fixtures/multidir/one/version.php create mode 100644 admin/tool/installaddon/tests/fixtures/multidir/two/README.txt create mode 100644 admin/tool/installaddon/tests/fixtures/nolang/bah/index.php create mode 100644 admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bah.php create mode 100644 admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bleh.php create mode 100644 admin/tool/installaddon/tests/fixtures/nolang/bah/lib.php create mode 100644 admin/tool/installaddon/tests/fixtures/nolang/bah/version.php create mode 100644 admin/tool/installaddon/tests/fixtures/nolang/bah/view.php create mode 100644 admin/tool/installaddon/tests/fixtures/noversionmod/noversion/lang/en/noversion.php create mode 100644 admin/tool/installaddon/tests/fixtures/noversiontheme/noversion/lang/en/theme_noversion.php create mode 100644 admin/tool/installaddon/tests/fixtures/nowrapdir/index.php create mode 100644 admin/tool/installaddon/tests/fixtures/nowrapdir/lang/en/foo.php create mode 100644 admin/tool/installaddon/tests/fixtures/nowrapdir/version.php create mode 100644 admin/tool/installaddon/tests/fixtures/plugindir/foobar/index.php create mode 100644 admin/tool/installaddon/tests/fixtures/plugindir/foobar/lang/en/local_foobar.php create mode 100644 admin/tool/installaddon/tests/fixtures/plugindir/foobar/version.php create mode 100644 admin/tool/installaddon/tests/fixtures/versionphp/version1.php create mode 100644 admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt create mode 100644 admin/tool/installaddon/tests/validator_test.php diff --git a/admin/tool/installaddon/classes/validator.php b/admin/tool/installaddon/classes/validator.php new file mode 100644 index 00000000000..557fbc1d664 --- /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 00000000000..158d760751b --- /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 00000000000..b3d9bbc7f37 --- /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 00000000000..c65b6f7652d --- /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 00000000000..84af5f26a90 --- /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 00000000000..84e1459a130 --- /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 00000000000..b3d9bbc7f37 --- /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 00000000000..654f0709e84 --- /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 00000000000..61646f05b91 --- /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 00000000000..a37b9435fb4 --- /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 00000000000..23d54a088f9 --- /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; + } +} From 30bec5ba8dfb1dc8b18aa085e2870700d9b6cf1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Mar 2013 17:38:59 +0100 Subject: [PATCH 10/22] MDL-38509 Add new tool_installaddon_installer::get_plugintype_root() method --- admin/tool/installaddon/classes/installer.php | 34 ++++++++++++++++--- .../classes/installfromzip_form.php | 3 +- admin/tool/installaddon/permcheck.php | 15 +++----- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index 2833254837e..60397cacf9b 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -115,13 +115,15 @@ class tool_installaddon_installer { } /** - * Is it possible to create a new plugin directory for the given plugin type? + * Returns the full path of the root of the given plugin type + * + * Null is returned if the plugin type is not known. False is returned if the plugin type + * root is expected but not found. Otherwise, string is returned. * - * @throws coding_exception for invalid plugin types or non-existing plugin type locations * @param string $plugintype - * @return boolean + * @return string|bool|null */ - public function is_plugintype_writable($plugintype) { + public function get_plugintype_root($plugintype) { $plugintypepath = null; foreach (get_plugin_types() as $type => $fullpath) { @@ -131,10 +133,32 @@ class tool_installaddon_installer { } } if (is_null($plugintypepath)) { - throw new coding_exception('Unknown plugin type!'); + return null; } if (!is_dir($plugintypepath)) { + return false; + } + + return $plugintypepath; + } + + /** + * Is it possible to create a new plugin directory for the given plugin type? + * + * @throws coding_exception for invalid plugin types or non-existing plugin type locations + * @param string $plugintype + * @return boolean + */ + public function is_plugintype_writable($plugintype) { + + $plugintypepath = $this->get_plugintype_root($plugintype); + + if (is_null($plugintypepath)) { + throw new coding_exception('Unknown plugin type!'); + } + + if ($plugintypepath === false) { throw new coding_exception('Plugin type location does not exist!'); } diff --git a/admin/tool/installaddon/classes/installfromzip_form.php b/admin/tool/installaddon/classes/installfromzip_form.php index aef05c7fcfb..9942679cb7d 100644 --- a/admin/tool/installaddon/classes/installfromzip_form.php +++ b/admin/tool/installaddon/classes/installfromzip_form.php @@ -78,8 +78,7 @@ class tool_installaddon_installfromzip extends moodleform { $errors = parent::validation($data, $files); if (!$installer->is_plugintype_writable($data['plugintype'])) { - $paths = get_plugin_types(true); - $path = $paths[$data['plugintype']]; + $path = $installer->get_plugintype_root($data['plugintype']); $errors['plugintype'] = get_string('permcheckresultno', 'tool_installaddon', array('path' => $path)); } diff --git a/admin/tool/installaddon/permcheck.php b/admin/tool/installaddon/permcheck.php index d2125bb6a56..f38d109f59a 100644 --- a/admin/tool/installaddon/permcheck.php +++ b/admin/tool/installaddon/permcheck.php @@ -53,20 +53,15 @@ if (is_null($plugintype)) { die(); } -$plugintypepath = null; -foreach (get_plugin_types() as $type => $fullpath) { - if ($type === $plugintype) { - $plugintypepath = $fullpath; - break; - } -} -if (is_null($plugintypepath)) { +$installer = tool_installaddon_installer::instance(); + +$plugintypepath = $installer->get_plugintype_root($plugintype); + +if (empty($plugintypepath)) { header('HTTP/1.1 400 Bad Request'); die(); } -$installer = tool_installaddon_installer::instance(); - $response = array('path' => $plugintypepath); if ($installer->is_plugintype_writable($plugintype)) { From 585b64a6079a6d63a19ae9537da8b2085966a63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Mar 2013 17:42:31 +0100 Subject: [PATCH 11/22] MDL-38509 Save uploaded ZIP into a temporary location and redirect to validator --- admin/tool/installaddon/classes/installer.php | 18 ++++++++++++++++++ admin/tool/installaddon/index.php | 13 ++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index 60397cacf9b..b7f18bc0a69 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -95,6 +95,24 @@ class tool_installaddon_installer { return $this->installfromzipform; } + /** + * Saves the ZIP file from the {@link tool_installaddon_installfromzip} form + * + * The file is saved into the given temporary location for inspection and eventual + * deployment. The form is expected to be submitted and validated. + * + * @param tool_installaddon_installfromzip $form + * @param string $targetdir full path to the directory where the ZIP should be stored to + * @return string filename of the saved file relative to the given target + */ + public function save_installfromzip_file(tool_installaddon_installfromzip $form, $targetdir) { + + $filename = clean_param($form->get_new_filename('zipfile'), PARAM_FILE); + $form->save_file('zipfile', $targetdir.'/'.$filename); + + return $filename; + } + /** * Returns localised list of available plugin types * diff --git a/admin/tool/installaddon/index.php b/admin/tool/installaddon/index.php index 062e05cbc69..8ca05d187f0 100644 --- a/admin/tool/installaddon/index.php +++ b/admin/tool/installaddon/index.php @@ -41,7 +41,18 @@ if ($form->is_cancelled()) { redirect($PAGE->url); } else if ($data = $form->get_data()) { - // todo $installer->process_installfromzip_form($data); + // Save the ZIP file into a temporary location. + $jobid = md5(rand().uniqid('', true)); + $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source'); + $zipfilename = $installer->save_installfromzip_file($form, $sourcedir); + // Redirect to the validation page. + $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array( + 'sesskey' => sesskey(), + 'jobid' => $jobid, + 'zip' => $zipfilename, + 'type' => $data->plugintype, + 'rootdir' => $data->rootdir)); + redirect($nexturl); } $output = $PAGE->get_renderer('tool_installaddon'); From 07083b230ba4bb007d22f25be374c773196ffd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Mar 2013 17:49:25 +0100 Subject: [PATCH 12/22] MDL-38509 Add new tool_installaddon_installer::extract_installfromzip_file() method --- admin/tool/installaddon/classes/installer.php | 91 +++++++++++++++++- .../tests/fixtures/zips/invalidroot.zip | Bin 0 -> 714 bytes .../installaddon/tests/installer_test.php | 25 ++++- 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index b7f18bc0a69..7353684eee6 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -43,7 +43,7 @@ class tool_installaddon_installer { * @return tool_installaddon_installer */ public static function instance() { - return new self(); + return new static(); } /** @@ -113,6 +113,37 @@ class tool_installaddon_installer { return $filename; } + /** + * Extracts the saved file previously saved by {self::save_installfromzip_file()} + * + * The list of files found in the ZIP is returned via $zipcontentfiles parameter + * by reference. The format of that list is array of (string)filerelpath => (bool|string) + * where the array value is either true or a string describing the problematic file. + * + * @see zip_packer::extract_to_pathname() + * @param string $zipfilepath full path to the saved ZIP file + * @param string $targetdir full path to the directory to extract the ZIP file to + * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value + * @param array list of extracted files as returned by {@link zip_packer::extract_to_pathname()} + */ + public function extract_installfromzip_file($zipfilepath, $targetdir, $rootdir = '') { + global $CFG; + require_once($CFG->libdir.'/filelib.php'); + + $fp = get_file_packer('application/zip'); + $files = $fp->extract_to_pathname($zipfilepath, $targetdir); + + if ($files) { + if (!empty($rootdir)) { + $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files); + } + return $files; + + } else { + return array(); + } + } + /** * Returns localised list of available plugin types * @@ -185,6 +216,12 @@ class tool_installaddon_installer { //// End of external API /////////////////////////////////////////////////// + /** + * @see self::instance() + */ + protected function __construct() { + } + /** * @return string this site full name */ @@ -235,4 +272,56 @@ class tool_installaddon_installer { protected function should_send_site_info() { return true; } + + /** + * Renames the root directory of the extracted ZIP package. + * + * This method does not validate the presence of the single root directory + * (the validator does it later). It just searches for the first directory + * under the given location and renames it. + * + * The method will not rename the root if the requested location already + * exists. + * + * @param string $dirname the location of the extracted ZIP package + * @param string $rootdir the requested name of the root directory + * @param array $files list of extracted files + * @return array eventually amended list of extracted files + */ + protected function rename_extracted_rootdir($dirname, $rootdir, array $files) { + + if (!is_dir($dirname)) { + debugging('Unable to rename rootdir of non-existing content', DEBUG_DEVELOPER); + return $files; + } + + if (file_exists($dirname.'/'.$rootdir)) { + debugging('Unable to rename rootdir to already existing folder', DEBUG_DEVELOPER); + return $files; + } + + $found = null; // The name of the first subdirectory under the $dirname. + foreach (scandir($dirname) as $item) { + if (substr($item, 0, 1) === '.') { + continue; + } + if (is_dir($dirname.'/'.$item)) { + $found = $item; + break; + } + } + + if (!is_null($found)) { + if (rename($dirname.'/'.$found, $dirname.'/'.$rootdir)) { + $newfiles = array(); + foreach ($files as $filepath => $status) { + $newpath = preg_replace('~^'.preg_quote($found.'/').'~', preg_quote($rootdir.'/'), $filepath); + $newfiles[$newpath] = $status; + } + return $newfiles; + } + } + + return $files; + } } diff --git a/admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip b/admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip new file mode 100644 index 0000000000000000000000000000000000000000..a2090259878fcc7ee5076a0e25f2cba016dd97dc GIT binary patch literal 714 zcmWIWW@h1H0D*JSWlmrQl;CBMVaUuYOU%hk(Jji)FVPPT;bdSwY~&NTABamUxEUB( zUNAE-fQbN{#t5MslarX24l(Np&>WC?IL*3+<|I+9W~JsqjJpjq3S=Z+k(v?@^0Zz-MgiF0Aj3fz&2SrgpaQM{Z$>6LW?aE20S!4Gn}-F@qP`+{swY1qU|JXe?oY>|~J9s6h=enh|K| dlEz24j7AT5To$o{qK|dirroot.'/'.$CFG->admin.'/tool/installaddon/classes/installer class tool_installaddon_installer_test extends advanced_testcase { public function test_get_addons_repository_url() { - $installer = new testable_tool_installaddon_installer(); + $installer = testable_tool_installaddon_installer::instance(); $url = $installer->get_addons_repository_url(); $query = parse_url($url, PHP_URL_QUERY); $this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches)); @@ -51,6 +51,29 @@ class tool_installaddon_installer_test extends advanced_testcase { $this->assertSame($installer->get_site_url(), $site['url']); $this->assertSame($installer->get_site_major_version(), $site['major_version']); } + + public function test_extract_installfromzip_file() { + $jobid = md5(rand().uniqid('test_', true)); + $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source'); + $contentsdir = make_temp_directory('tool_installaddon/'.$jobid.'/contents'); + copy(dirname(__FILE__).'/fixtures/zips/invalidroot.zip', $sourcedir.'/testinvalidroot.zip'); + + $installer = tool_installaddon_installer::instance(); + $files = $installer->extract_installfromzip_file($sourcedir.'/testinvalidroot.zip', $contentsdir, 'fixed_root'); + $this->assertEquals('array', gettype($files)); + $this->assertEquals(4, count($files)); + $this->assertSame(true, $files['fixed_root/']); + $this->assertSame(true, $files['fixed_root/lang/']); + $this->assertSame(true, $files['fixed_root/lang/en/']); + $this->assertSame(true, $files['fixed_root/lang/en/fixed_root.php']); + foreach ($files as $file => $status) { + if (substr($file, -1) === '/') { + $this->assertTrue(is_dir($contentsdir.'/'.$file)); + } else { + $this->assertTrue(is_file($contentsdir.'/'.$file)); + } + } + } } From cbd125e85d739df4991e4d006947d66ea7fa4930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Mar 2013 17:53:55 +0100 Subject: [PATCH 13/22] MDL-38509 Implement the ZIP package validation page The script validate.php expects a ZIP file stored in a temporary location. It extracts the ZIP (optionally renaming the root directory) and executes the validator. Then it renders the validator log messages and continue buttons. --- .../classes/installfromzip_form.php | 8 + admin/tool/installaddon/renderer.php | 160 ++++++++++++++++++ admin/tool/installaddon/settings.php | 7 +- admin/tool/installaddon/styles.css | 55 ++++++ admin/tool/installaddon/validate.php | 80 +++++++++ 5 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 admin/tool/installaddon/validate.php diff --git a/admin/tool/installaddon/classes/installfromzip_form.php b/admin/tool/installaddon/classes/installfromzip_form.php index 9942679cb7d..18552faadb7 100644 --- a/admin/tool/installaddon/classes/installfromzip_form.php +++ b/admin/tool/installaddon/classes/installfromzip_form.php @@ -43,6 +43,9 @@ class tool_installaddon_installfromzip extends moodleform { $mform = $this->_form; $installer = $this->_customdata['installer']; + $mform->addElement('header', 'general', get_string('installfromzip', 'tool_installaddon')); + $mform->addHelpButton('general', 'installfromzip', 'tool_installaddon'); + $options = $installer->get_plugin_types_menu(); $mform->addElement('select', 'plugintype', get_string('installfromziptype', 'tool_installaddon'), $options, array('id' => 'tool_installaddon_installfromzip_plugintype')); @@ -58,6 +61,11 @@ class tool_installaddon_installfromzip extends moodleform { $mform->addHelpButton('zipfile', 'installfromzipfile', 'tool_installaddon'); $mform->addRule('zipfile', null, 'required', null, 'client'); + $mform->addElement('text', 'rootdir', get_string('installfromziprootdir', 'tool_installaddon')); + $mform->addHelpButton('rootdir', 'installfromziprootdir', 'tool_installaddon'); + $mform->setType('rootdir', PARAM_PLUGIN); + $mform->setAdvanced('rootdir'); + $mform->addElement('checkbox', 'acknowledgement', get_string('acknowledgement', 'tool_installaddon'), ' '.get_string('acknowledgementtext', 'tool_installaddon')); $mform->addRule('acknowledgement', get_string('acknowledgementmust', 'tool_installaddon'), 'required', null, 'client'); diff --git a/admin/tool/installaddon/renderer.php b/admin/tool/installaddon/renderer.php index e670c6d5e08..0cc273dd8c6 100644 --- a/admin/tool/installaddon/renderer.php +++ b/admin/tool/installaddon/renderer.php @@ -37,6 +37,9 @@ class tool_installaddon_renderer extends plugin_renderer_base { /** @var tool_installaddon_installer */ protected $installer = null; + /** @var tool_installaddon_validator */ + protected $validator = null; + /** * Sets the tool_installaddon_installer instance being used. * @@ -51,6 +54,20 @@ class tool_installaddon_renderer extends plugin_renderer_base { } } + /** + * Sets the tool_installaddon_validator instance being used. + * + * @throws coding_exception if the validator has been already set + * @param tool_installaddon_validator $validator + */ + public function set_validator_instance(tool_installaddon_validator $validator) { + if (is_null($this->validator)) { + $this->validator = $validator; + } else { + throw new coding_exception('Attempting to reset the validator instance.'); + } + } + /** * Defines the index page layout * @@ -58,6 +75,10 @@ class tool_installaddon_renderer extends plugin_renderer_base { */ public function index_page() { + if (is_null($this->installer)) { + throw new coding_exception('Installer instance has not been set.'); + } + $permcheckurl = new moodle_url('/admin/tool/installaddon/permcheck.php'); $this->page->requires->yui_module('moodle-tool_installaddon-permcheck', 'M.tool_installaddon.permcheck.init', array(array('permcheckurl' => $permcheckurl->out()))); @@ -73,6 +94,33 @@ class tool_installaddon_renderer extends plugin_renderer_base { return $out; } + /** + * Defines the validation results page layout + * + * @return string + */ + public function validation_page() { + + if (is_null($this->installer)) { + throw new coding_exception('Installer instance has not been set.'); + } + + if (is_null($this->validator)) { + throw new coding_exception('Validator instance has not been set.'); + } + + $out = $this->output->header(); + $out .= $this->validation_page_heading(); + $out .= $this->validation_page_messages(); + $out .= $this->validation_page_continue(); + $out .= $this->output->footer(); + + return $out; + + } + + // End of the external API ///////////////////////////////////////////////// + /** * Renders the index page heading * @@ -117,4 +165,116 @@ class tool_installaddon_renderer extends plugin_renderer_base { return $out; } + + /** + * Renders the page title and the overall validation verdict + * + * @return string + */ + protected function validation_page_heading() { + + $heading = $this->output->heading(get_string('validation', 'tool_installaddon')); + + if ($this->validator->get_result()) { + $status = $this->output->container( + html_writer::span(get_string('validationresult1', 'tool_installaddon'), 'verdict'). + $this->output->help_icon('validationresult1', 'tool_installaddon'), + array('validationresult', 'success') + ); + } else { + $status = $this->output->container( + html_writer::span(get_string('validationresult0', 'tool_installaddon'), 'verdict'). + $this->output->help_icon('validationresult0', 'tool_installaddon'), + array('validationresult', 'failure') + ); + } + + return $heading . $status; + } + + /** + * Renders validation log messages. + * + * @return string + */ + protected function validation_page_messages() { + + $validator = $this->validator; // We need this to be able to use their constants. + $messages = $validator->get_messages(); + + if (empty($messages)) { + return ''; + } + + $table = new html_table(); + $table->attributes['class'] = 'validationmessages generaltable'; + $table->head = array( + get_string('validationresultstatus', 'tool_installaddon'), + get_string('validationresultmsg', 'tool_installaddon'), + get_string('validationresultinfo', 'tool_installaddon') + ); + $table->colclasses = array('msgstatus', 'msgtext', 'msginfo'); + + $stringman = get_string_manager(); + + foreach ($messages as $message) { + + if ($message->level === $validator::DEBUG and !debugging()) { + continue; + } + + $msgstatus = get_string('validationmsglevel_'.$message->level, 'tool_installaddon'); + $msgtext = $msgtext = s($message->msgcode); + if (is_null($message->addinfo)) { + $msginfo = ''; + } else { + $msginfo = html_writer::tag('pre', s(print_r($message->addinfo, true))); + } + $msghelp = ''; + + // Replace the message code with the string if it is defined. + if ($stringman->string_exists('validationmsg_'.$message->msgcode, 'tool_installaddon')) { + $msgtext = get_string('validationmsg_'.$message->msgcode, 'tool_installaddon'); + // And check for the eventual help, too. + if ($stringman->string_exists('validationmsg_'.$message->msgcode.'_help', 'tool_installaddon')) { + $msghelp = $this->output->help_icon('validationmsg_'.$message->msgcode, 'tool_installaddon'); + } + } + + // Re-format the message info using a string if it is define. + if (!is_null($message->addinfo) and $stringman->string_exists('validationmsg_'.$message->msgcode.'_info', 'tool_installaddon')) { + $msginfo = get_string('validationmsg_'.$message->msgcode.'_info', 'tool_installaddon', $message->addinfo); + } + + $row = new html_table_row(array($msgstatus, $msgtext.$msghelp, $msginfo)); + $row->attributes['class'] = 'level-'.$message->level.' '.$message->msgcode; + + $table->data[] = $row; + } + + return html_writer::table($table); + } + + /** + * Renders widgets to continue from the validation results page + * + * @return string + */ + protected function validation_page_continue() { + + $conturl = $this->validator->get_continue_url(); + if (is_null($conturl)) { + $contbutton = ''; + } else { + $contbutton = $this->output->single_button( + $conturl, get_string('installaddon', 'tool_installaddon'), 'post', + array('class' => 'singlebutton continuebutton')); + } + + $cancelbutton = $this->output->single_button( + new moodle_url('/admin/tool/installaddon/index.php'), get_string('cancel', 'core'), 'get', + array('class' => 'singlebutton cancelbutton')); + + return $this->output->container($cancelbutton.$contbutton, 'postvalidationbuttons'); + } } diff --git a/admin/tool/installaddon/settings.php b/admin/tool/installaddon/settings.php index 326f0309a5a..0c03c736fb1 100644 --- a/admin/tool/installaddon/settings.php +++ b/admin/tool/installaddon/settings.php @@ -29,6 +29,11 @@ if ($hassiteconfig and empty($CFG->disableonclickaddoninstall)) { $ADMIN->add('modules', new admin_externalpage('tool_installaddon_index', get_string('installaddons', 'tool_installaddon'), - "$CFG->wwwroot/$CFG->admin/tool/installaddon/")); + "$CFG->wwwroot/$CFG->admin/tool/installaddon/index.php")); + $ADMIN->add('modules', new admin_externalpage('tool_installaddon_validate', + get_string('validation', 'tool_installaddon'), + "$CFG->wwwroot/$CFG->admin/tool/installaddon/validate.php", + 'moodle/site:config', + true)); } diff --git a/admin/tool/installaddon/styles.css b/admin/tool/installaddon/styles.css index 3de50f35259..05155ca612c 100644 --- a/admin/tool/installaddon/styles.css +++ b/admin/tool/installaddon/styles.css @@ -11,3 +11,58 @@ #page-admin-tool-installaddon-index #installfromrepobox .singlebutton input[type=submit] { padding: 1em; } + +#page-admin-tool-installaddon-validate .validationresult { + margin: 2em auto; + text-align: center; +} + +#page-admin-tool-installaddon-validate .validationresult .verdict { + margin: 0em 0.5em; + padding: 0.5em; + border: 2px solid; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + font-weight: bold; +} + +#page-admin-tool-installaddon-validate .validationresult.success .verdict { + background-color: #e7f1c3; + border-color: #aaeeaa; +} + +#page-admin-tool-installaddon-validate .validationresult.failure .verdict { + background-color: #ffd3d9; + border-color: #eeaaaa; +} + +#page-admin-tool-installaddon-validate .validationmessages { + margin: 0px auto; +} + +#page-admin-tool-installaddon-validate .validationmessages .level-error .msgstatus { + background-color: #ffd3d9; +} + +#page-admin-tool-installaddon-validate .validationmessages .level-warning .msgstatus { + background-color: #f3f2aa; +} + +#page-admin-tool-installaddon-validate .validationmessages .level-info .msgstatus { + background-color: #e7f1c3; +} + +#page-admin-tool-installaddon-validate .validationmessages .level-debug .msgstatus { + background-color: #d2ebff; +} + +#page-admin-tool-installaddon-validate .postvalidationbuttons { + text-align: center; + margin: 1em auto; +} + +#page-admin-tool-installaddon-validate .postvalidationbuttons .singlebutton { + display: inline-block; + margin: 1em 1em; +} diff --git a/admin/tool/installaddon/validate.php b/admin/tool/installaddon/validate.php new file mode 100644 index 00000000000..2fc66cf3bc9 --- /dev/null +++ b/admin/tool/installaddon/validate.php @@ -0,0 +1,80 @@ +. + +/** + * The ZIP package validation. + * + * @package tool_installaddon + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(dirname(__FILE__) . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/filelib.php'); +require_once(dirname(__FILE__).'/classes/installer.php'); +require_once(dirname(__FILE__).'/classes/validator.php'); + +navigation_node::override_active_url(new moodle_url('/admin/tool/installaddon/index.php')); +admin_externalpage_setup('tool_installaddon_validate'); + +if (!empty($CFG->disableonclickaddoninstall)) { + notice(get_string('featuredisabled', 'tool_installaddon')); +} + +require_sesskey(); + +$jobid = required_param('jobid', PARAM_ALPHANUM); +$zipfilename = required_param('zip', PARAM_FILE); +$plugintype = required_param('type', PARAM_ALPHANUMEXT); +$rootdir = optional_param('rootdir', '', PARAM_PLUGIN); + +$zipfilepath = $CFG->tempdir.'/tool_installaddon/'.$jobid.'/source/'.$zipfilename; +if (!file_exists($zipfilepath)) { + redirect(new moodle_url('/admin/tool/installaddon/index.php'), + get_string('invaliddata', 'core_error')); +} + +$installer = tool_installaddon_installer::instance(); + +// Extract the ZIP contents. +fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid.'/contents'); +$zipcontentpath = make_temp_directory('tool_installaddon/'.$jobid.'/contents'); +$zipcontentfiles = $installer->extract_installfromzip_file($zipfilepath, $zipcontentpath, $rootdir); + +// Validate the contents of the plugin ZIP file. +$validator = tool_installaddon_validator::instance($zipcontentpath, $zipcontentfiles); +$validator->assert_plugin_type($plugintype); +$validator->assert_moodle_version($CFG->version); +$result = $validator->execute(); + +if ($result) { + $validator->set_continue_url(new moodle_url('/admin/tool/installaddon/deploy.php', array( + 'sesskey' => sesskey(), + 'jobid' => $jobid, + 'type' => $plugintype, + 'name' => $validator->get_rootdir()))); + +} else { + fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid); +} + +// Display the validation results. +$output = $PAGE->get_renderer('tool_installaddon'); +$output->set_installer_instance($installer); +$output->set_validator_instance($validator); +echo $output->validation_page(); From a143984a36d159a8e6217e477e688651e38a9f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Mar 2013 17:56:40 +0100 Subject: [PATCH 14/22] MDL-38509 Add strings for the new admin tool --- .../lang/en/tool_installaddon.php | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/admin/tool/installaddon/lang/en/tool_installaddon.php b/admin/tool/installaddon/lang/en/tool_installaddon.php index 833a7f16f80..6179238967e 100644 --- a/admin/tool/installaddon/lang/en/tool_installaddon.php +++ b/admin/tool/installaddon/lang/en/tool_installaddon.php @@ -30,11 +30,16 @@ $string['acknowledgement'] = 'Acknowledgement'; $string['acknowledgementmust'] = 'You must acknowledge this'; $string['acknowledgementtext'] = 'I understand that it is my responsibility to have full backups of this site prior to installing add-ons. I accept and understand that add-ons (especially but not only those originating in unofficial sources) may contain security holes, can make the site unavailable, or cause private data leaks or loss.'; $string['featuredisabled'] = 'Add-on installer is disabled at this site.'; +$string['installaddon'] = 'Install add-on!'; $string['installaddons'] = 'Install add-ons'; $string['installfromrepo'] = 'Install add-ons from Moodle plugins directory'; $string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site fullname, URL and major version will be sent as well, to make the installation process easier for you.'; +$string['installfromzip'] = 'Install add-on from the ZIP file'; +$string['installfromzip_help'] = 'Alternatively to installing add-ons directly from the Moodle plugins directory, you can install add-ons from manually uploaded ZIP packages. Such ZIP packages are expected to have same structure as the ones available in the Moodle plugins directory.'; $string['installfromzipfile'] = 'ZIP package'; $string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory with the name of the plugin. The ZIP will be extracted into the appropriate location for the given plugin type. Packages downloaded from the Moodle plugins directory have this format.'; +$string['installfromziprootdir'] = 'Rename the root directory'; +$string['installfromziprootdir_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You can rename the root directory of the extracted package to the correct value defined in this field.'; $string['installfromzipsubmit'] = 'Install add-on from the ZIP file'; $string['installfromziptype'] = 'Plugin type'; $string['installfromziptype_help'] = 'Choose the correct type of plugin you are about to install. The installation procedure may fail badly when incorrect plugin type is provided.'; @@ -44,3 +49,48 @@ $string['permcheckprogress'] = 'Checking for write permission ...'; $string['permcheckresultno'] = 'Plugin type location {$a->path} not writable'; $string['permcheckresultyes'] = 'Plugin type location {$a->path} is writable'; $string['pluginname'] = 'Add-on installer'; +$string['validation'] = 'Add-on package validation'; +$string['validationmsg_componentmatch'] = 'Full component name'; +$string['validationmsg_componentmismatchname'] = 'Add-on name mismatch'; +$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You have to fix the name of the root directory to match the declared add-on name.'; +$string['validationmsg_componentmismatchname_info'] = 'The add-on declares its name is \'{$a}\' but that does not match the name of the root directory.'; +$string['validationmsg_componentmismatchtype'] = 'Add-on type mismatch'; +$string['validationmsg_componentmismatchtype_info'] = 'You have selected the type \'{$a->expected}\' but the add-on declares its type is \'{$a->found}\'.'; +$string['validationmsg_filenotexists'] = 'Extracted file not found'; +$string['validationmsg_filesnumber'] = 'Not enough files found in the package'; +$string['validationmsg_filestatus'] = 'Unable to extract all files'; +$string['validationmsg_filestatus_info'] = 'Attempting to extract file {$a->file} resulted in error \'{$a->status}\'.'; +$string['validationmsg_maturity'] = 'Declared maturity level'; +$string['validationmsg_maturity_help'] = 'The add-on can declare its maturity level. If the maintainer considers the add-on stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.'; +$string['validationmsg_missingexpectedlangenfile'] = 'English language file name mismatch'; +$string['validationmsg_missingexpectedlangenfile_info'] = 'The given add-on type would need to provide the English language file {$a}.'; +$string['validationmsg_missinglangenfile'] = 'No English language file found'; +$string['validationmsg_missinglangenfolder'] = 'Missing English language folder'; +$string['validationmsg_missingversion'] = 'Add-on does not declare its version'; +$string['validationmsg_missingversionphp'] = 'File version.php not found'; +$string['validationmsg_multiplelangenfiles'] = 'Multiple English language files found'; +$string['validationmsg_onedir'] = 'Invalid structure of the ZIP package.'; +$string['validationmsg_onedir_help'] = 'The ZIP package must contain just one root directory that holds the add-on code. The name of that root directory must match the name of the plugin.'; +$string['validationmsg_pathwritable'] = 'Write access check'; +$string['validationmsg_pluginversion'] = 'Add-on version'; +$string['validationmsg_release'] = 'Add-on release'; +$string['validationmsg_requiresmoodle'] = 'Required Moodle version'; +$string['validationmsg_rootdir'] = 'Name of the add-on to be installed'; +$string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the add-on to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the add-on.'; +$string['validationmsg_rootdirinvalid'] = 'Invalid name of the add-on'; +$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain incorrect name of the root directory. You have to fix the name of the root directory to match the add-on name.'; +$string['validationmsg_targetexists'] = 'Target location already exists'; +$string['validationmsg_targetexists_help'] = 'The directory that the add-on is to be installed to, must not exist yet.'; +$string['validationmsg_unknowntype'] = 'Unknown plugin type'; +$string['validationmsglevel_debug'] = 'Debug'; +$string['validationmsglevel_error'] = 'Error'; +$string['validationmsglevel_info'] = 'OK'; +$string['validationmsglevel_warning'] = 'Warning'; +$string['validationresult0'] = 'Validation failed!'; +$string['validationresult0_help'] = 'Some serious problem was detected. It is not safe to install the add-on. See the validation log messages for more details.'; +$string['validationresult1'] = 'Validation passed!'; +$string['validationresult1_help'] = 'No serious problems were detected. You can continue with the add-on installation. See the validation log messages for more details and eventual warnings.'; +$string['validationresult1_help'] = 'The add-on package has been validated and no serious problems were detected.'; +$string['validationresultinfo'] = 'Info'; +$string['validationresultmsg'] = 'Message'; +$string['validationresultstatus'] = 'Status'; From 59a2cc8d3866959343d62a50b460ec253eb84154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Mar 2013 18:05:37 +0100 Subject: [PATCH 15/22] MDL-38509 Add the script that finally deploys the validated ZIP contents --- admin/tool/installaddon/deploy.php | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 admin/tool/installaddon/deploy.php diff --git a/admin/tool/installaddon/deploy.php b/admin/tool/installaddon/deploy.php new file mode 100644 index 00000000000..932e9f2001f --- /dev/null +++ b/admin/tool/installaddon/deploy.php @@ -0,0 +1,76 @@ +. + +/** + * Deploy the validated contents of the ZIP package to the $CFG->dirroot + * + * @package tool_installaddon + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(dirname(__FILE__) . '/../../../config.php'); +require_once($CFG->libdir.'/filelib.php'); +require_once(dirname(__FILE__).'/classes/installer.php'); +require_once(dirname(__FILE__).'/classes/validator.php'); + +require_login(); +require_capability('moodle/site:config', context_system::instance()); + +if (!empty($CFG->disableonclickaddoninstall)) { + notice(get_string('featuredisabled', 'tool_installaddon')); +} + +require_sesskey(); + +$jobid = required_param('jobid', PARAM_ALPHANUM); +$plugintype = required_param('type', PARAM_ALPHANUMEXT); +$pluginname = required_param('name', PARAM_PLUGIN); + +$zipcontentpath = $CFG->tempdir.'/tool_installaddon/'.$jobid.'/contents'; + +if (!is_dir($zipcontentpath)) { + debugging('Invalid location of the extracted ZIP package: '.s($zipcontentpath), DEBUG_DEVELOPER); + redirect(new moodle_url('/admin/tool/installaddon/index.php'), + get_string('invaliddata', 'core_error')); +} + +if (!is_dir($zipcontentpath.'/'.$pluginname)) { + debugging('Invalid location of the plugin root directory: '.$zipcontentpath.'/'.$pluginname, DEBUG_DEVELOPER); + redirect(new moodle_url('/admin/tool/installaddon/index.php'), + get_string('invaliddata', 'core_error')); +} + +$installer = tool_installaddon_installer::instance(); + +if (!$installer->is_plugintype_writable($plugintype)) { + debugging('Plugin type location not writable', DEBUG_DEVELOPER); + redirect(new moodle_url('/admin/tool/installaddon/index.php'), + get_string('invaliddata', 'core_error')); +} + +$plugintypepath = $installer->get_plugintype_root($plugintype); + +if (file_exists($plugintypepath.'/'.$pluginname)) { + debugging('Target location already exists', DEBUG_DEVELOPER); + redirect(new moodle_url('/admin/tool/installaddon/index.php'), + get_string('invaliddata', 'core_error')); +} + +rename($zipcontentpath.'/'.$pluginname, $plugintypepath.'/'.$pluginname); +fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid); +redirect(new moodle_url('/admin')); From b7f6442670ac074fe6edd951ec48f190282ece2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Mar 2013 18:14:25 +0100 Subject: [PATCH 16/22] MDL-38509 Fix the communication protocol with Moodle plugins directory Implements the behaviour currently specified at http://docs.moodle.org/dev/On-click_add-on_installation --- admin/tool/installaddon/classes/installer.php | 4 ++-- admin/tool/installaddon/tests/installer_test.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index 7353684eee6..adac7c1638f 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -68,7 +68,7 @@ class tool_installaddon_installer { $site = array( 'fullname' => $this->get_site_fullname(), 'url' => $this->get_site_url(), - 'major_version' => $this->get_site_major_version(), + 'majorversion' => $this->get_site_major_version(), ); $site = $this->encode_site_information($site); @@ -228,7 +228,7 @@ class tool_installaddon_installer { protected function get_site_fullname() { global $SITE; - return $SITE->fullname; + return strip_tags($SITE->fullname); } /** diff --git a/admin/tool/installaddon/tests/installer_test.php b/admin/tool/installaddon/tests/installer_test.php index f32bc193e6d..7f53cb17c2b 100644 --- a/admin/tool/installaddon/tests/installer_test.php +++ b/admin/tool/installaddon/tests/installer_test.php @@ -47,9 +47,9 @@ class tool_installaddon_installer_test extends advanced_testcase { $site = json_decode(base64_decode($site), true); $this->assertEquals('array', gettype($site)); $this->assertEquals(3, count($site)); - $this->assertSame($installer->get_site_fullname(), $site['fullname']); - $this->assertSame($installer->get_site_url(), $site['url']); - $this->assertSame($installer->get_site_major_version(), $site['major_version']); + $this->assertSame('Nasty site', $site['fullname']); + $this->assertSame('file:///etc/passwd', $site['url']); + $this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']); } public function test_extract_installfromzip_file() { @@ -86,7 +86,7 @@ class tool_installaddon_installer_test extends advanced_testcase { class testable_tool_installaddon_installer extends tool_installaddon_installer { public function get_site_fullname() { - return '

Nasty site

'; + return strip_tags('

Nasty site

'); } public function get_site_url() { From 399ddcc73e26d8f5fd1d7d130a62c4c649559a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Thu, 28 Mar 2013 11:36:48 +0100 Subject: [PATCH 17/22] MDL-38509 Move the tool node in the navigation The "Install add-ons" should appear just below Site administration > Plugins, before the "Activity modules". --- admin/tool/installaddon/settings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/tool/installaddon/settings.php b/admin/tool/installaddon/settings.php index 0c03c736fb1..b533360b233 100644 --- a/admin/tool/installaddon/settings.php +++ b/admin/tool/installaddon/settings.php @@ -29,11 +29,11 @@ if ($hassiteconfig and empty($CFG->disableonclickaddoninstall)) { $ADMIN->add('modules', new admin_externalpage('tool_installaddon_index', get_string('installaddons', 'tool_installaddon'), - "$CFG->wwwroot/$CFG->admin/tool/installaddon/index.php")); + "$CFG->wwwroot/$CFG->admin/tool/installaddon/index.php"), 'modsettings'); $ADMIN->add('modules', new admin_externalpage('tool_installaddon_validate', get_string('validation', 'tool_installaddon'), "$CFG->wwwroot/$CFG->admin/tool/installaddon/validate.php", 'moodle/site:config', - true)); + true), 'modsettings'); } From af96f120e9fa46f12f6acc67d763058265c95f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 29 Mar 2013 00:06:06 +0100 Subject: [PATCH 18/22] MDL-38509 Add ability to install add-ons from the remote repository There is a new hook in the index.php file. If valid HTTP parameter installaddonrequest is detected, the installer asks the administrator to confirm the request. If confirmed, the installer calls download.moodle.org/api/1.2/pluginfo.php service to get information about the given plugin version. The essential data are the URL of the ZIP to download and the MD5 hash of the ZIP. These data must be fetched via HTTPS to protect against MiM attack. If the ZIP is downloaded and the MD5 content hash is correct, the user is redirected to the previously implemented ZIP validation page, as if the ZIP was uploaded manually. The valid format of the installaddonrequest is documented via the test_decode_remote_request() unit test method. --- admin/tool/installaddon/classes/installer.php | 282 +++++++++++++++++- .../installaddon/classes/pluginfo_client.php | 209 +++++++++++++ admin/tool/installaddon/index.php | 12 +- .../lang/en/tool_installaddon.php | 6 + admin/tool/installaddon/renderer.php | 118 ++++++++ .../installaddon/tests/installer_test.php | 55 ++++ 6 files changed, 677 insertions(+), 5 deletions(-) create mode 100644 admin/tool/installaddon/classes/pluginfo_client.php diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index adac7c1638f..c83fa09d5cb 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -16,7 +16,7 @@ // along with Moodle. If not, see . /** - * Provides tool_installaddon_installer class + * Provides tool_installaddon_installer related classes * * @package tool_installaddon * @subpackage classes @@ -46,6 +46,16 @@ class tool_installaddon_installer { return new static(); } + /** + * Returns the URL to the main page of this admin tool + * + * @param array optional parameters + * @return moodle_url + */ + public function index_url(array $params = null) { + return new moodle_url('/admin/tool/installaddon/index.php', $params); + } + /** * Returns URL to the repository that addons can be searched in and installed from * @@ -87,7 +97,7 @@ class tool_installaddon_installer { return $this->installfromzipform; } - $action = new moodle_url('/admin/tool/installaddon/index.php'); + $action = $this->index_url(); $customdata = array('installer' => $this); $this->installfromzipform = new tool_installaddon_installfromzip($action, $customdata); @@ -214,6 +224,208 @@ class tool_installaddon_installer { return is_writable($plugintypepath); } + /** + * Hook method to handle the remote request to install an add-on + * + * This is used as a callback when the admin picks a plugin version in the + * Moodle Plugins directory and is redirected back to their site to install + * it. + * + * This hook is called early from admin/tool/installaddon/index.php page so that + * it has opportunity to take over the UI. + * + * @param tool_installaddon_renderer $output + * @param string|null $request + * @param bool $confirmed + */ + public function handle_remote_request(tool_installaddon_renderer $output, $request, $confirmed = false) { + global $CFG; + require_once(dirname(__FILE__).'/pluginfo_client.php'); + + if (is_null($request)) { + return; + } + + $data = $this->decode_remote_request($request); + + if ($data === false) { + echo $output->remote_request_invalid_page($this->index_url()); + exit(); + } + + list($plugintype, $pluginname) = normalize_component($data->component); + + $plugintypepath = $this->get_plugintype_root($plugintype); + + if (file_exists($plugintypepath.'/'.$pluginname)) { + echo $output->remote_request_alreadyinstalled_page($data, $this->index_url()); + exit(); + } + + if (!$this->is_plugintype_writable($plugintype)) { + $continueurl = $this->index_url(array('installaddonrequest' => $request)); + echo $output->remote_request_permcheck_page($data, $plugintypepath, $continueurl, $this->index_url()); + exit(); + } + + $continueurl = $this->index_url(array( + 'installaddonrequest' => $request, + 'confirm' => 1, + 'sesskey' => sesskey())); + + if (!$confirmed) { + echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url()); + exit(); + } + + // The admin has confirmed their intention to install the add-on. + require_sesskey(); + + // Fetch the plugin info. The essential information is the URL to download the ZIP + // and the MD5 hash of the ZIP, obtained via HTTPS. + $client = tool_installaddon_pluginfo_client::instance(); + + try { + $pluginfo = $client->get_pluginfo($data->component, $data->version); + + } catch (tool_installaddon_pluginfo_exception $e) { + if (debugging()) { + throw $e; + } else { + echo $output->remote_request_pluginfo_exception($data, $e, $this->index_url()); + exit(); + } + } + + // Fetch the ZIP with the plugin version + $jobid = md5(rand().uniqid('', true)); + $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source'); + $zipfilename = 'downloaded.zip'; + + try { + $this->download_file($pluginfo->downloadurl, $sourcedir.'/'.$zipfilename); + + } catch (tool_installaddon_installer_exception $e) { + if (debugging()) { + throw $e; + } else { + echo $output->installer_exception($e, $this->index_url()); + exit(); + } + } + + // Check the MD5 checksum + $md5expected = $pluginfo->downloadmd5; + $md5actual = md5_file($sourcedir.'/'.$zipfilename); + if ($md5expected !== $md5actual) { + $e = new tool_installaddon_installer_exception('err_zip_md5', array('expected' => $md5expected, 'actual' => $md5actual)); + if (debugging()) { + throw $e; + } else { + echo $output->installer_exception($e, $this->index_url()); + exit(); + } + } + + // Redirect to the validation page. + $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array( + 'sesskey' => sesskey(), + 'jobid' => $jobid, + 'zip' => $zipfilename, + 'type' => $plugintype)); + redirect($nexturl); + } + + /** + * Download the given file into the given destination. + * + * This is basically a simplified version of {@link download_file_content()} from + * Moodle itself, tuned for fetching files from moodle.org servers. Same code is used + * in mdeploy.php for fetching available updates. + * + * @param string $source file url starting with http(s):// + * @param string $target store the downloaded content to this file (full path) + * @throws tool_installaddon_installer_exception + */ + public function download_file($source, $target) { + global $CFG; + + $newlines = array("\r", "\n"); + $source = str_replace($newlines, '', $source); + if (!preg_match('|^https?://|i', $source)) { + throw new tool_installaddon_installer_exception('err_download_transport_protocol', $source); + } + if (!$ch = curl_init($source)) { + throw new tool_installaddon_installer_exception('err_curl_init', $source); + } + + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // verify the peer's certificate + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // check the existence of a common name and also verify that it matches the hostname provided + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the transfer as a string + curl_setopt($ch, CURLOPT_HEADER, false); // don't include the header in the output + curl_setopt($ch, CURLOPT_TIMEOUT, 3600); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); // nah, moodle.org is never unavailable! :-p + curl_setopt($ch, CURLOPT_URL, $source); + + $cacertfile = $CFG->dataroot.'/moodleorgca.crt'; + if (is_readable($cacertfile)) { + // Do not use CA certs provided by the operating system. Instead, + // use this CA cert to verify the ZIP provider. + curl_setopt($ch, CURLOPT_CAINFO, $cacertfile); + } + + if (!empty($CFG->proxyhost) and !is_proxybypass($source)) { + if (!empty($CFG->proxytype)) { + if (strtoupper($CFG->proxytype) === 'SOCKS5') { + curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); + } else { + curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, false); + } + } + + if (empty($CFG->proxyport)) { + curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost); + } else { + curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost.':'.$CFG->proxyport); + } + + if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) { + curl_setopt($ch, CURLOPT_PROXYUSERPWD, $CFG->proxyuser.':'.$CFG->proxypassword); + curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM); + } + } + + $targetfile = fopen($target, 'w'); + + if (!$targetfile) { + throw new tool_installaddon_installer_exception('err_download_write_file', $target); + } + + curl_setopt($ch, CURLOPT_FILE, $targetfile); + + $result = curl_exec($ch); + + // try to detect encoding problems + if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) { + curl_setopt($ch, CURLOPT_ENCODING, 'none'); + $result = curl_exec($ch); + } + + fclose($targetfile); + + $curlerrno = curl_errno($ch); + $curlerror = curl_error($ch); + $curlinfo = curl_getinfo($ch); + + if ($result === false or $curlerrno) { + throw new tool_installaddon_installer_exception('err_curl_exec', array('url' => $source, 'errorno' => $curlerrno, 'error' => $curlerror)); + + } else if (is_array($curlinfo) and (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200)) { + throw new tool_installaddon_installer_exception('err_curl_code', array('url' => $source, 'httpcode' => $curlinfo['http_code'])); + } + } + //// End of external API /////////////////////////////////////////////////// /** @@ -324,4 +536,70 @@ class tool_installaddon_installer { return $files; } + + /** + * Decode the request from the Moodle Plugins directory + * + * @param string $request submitted via 'installaddonrequest' HTTP parameter + * @return stdClass|bool false on error, object otherwise + */ + protected function decode_remote_request($request) { + + $data = base64_decode($request, true); + + if ($data === false) { + return false; + } + + $data = json_decode($data); + + if (is_null($data)) { + return false; + } + + if (!isset($data->name) or !isset($data->component) or !isset($data->version)) { + return false; + } + + $data->name = s(strip_tags($data->name)); + + if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) { + return false; + } + + list($plugintype, $pluginname) = normalize_component($data->component); + + if ($plugintype === 'core') { + return false; + } + + if ($data->component !== $plugintype.'_'.$pluginname) { + return false; + } + + // Keep this regex in sync with the one used by the download.moodle.org/api/x.y/pluginfo.php + if (!preg_match('/^[0-9]+$/', $data->version)) { + return false; + } + + return $data; + } +} + + +/** + * General exception thrown by {@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_installer_exception extends moodle_exception { + + /** + * @param string $errorcode exception description identifier + * @param mixed $debuginfo debugging data to display + */ + public function __construct($errorcode, $a=null, $debuginfo=null) { + parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true)); + } } diff --git a/admin/tool/installaddon/classes/pluginfo_client.php b/admin/tool/installaddon/classes/pluginfo_client.php new file mode 100644 index 00000000000..521389911f7 --- /dev/null +++ b/admin/tool/installaddon/classes/pluginfo_client.php @@ -0,0 +1,209 @@ +. + +/** + * Provides tool_installaddon_pluginfo_client and related classes + * + * @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(); + +/** + * Implements a client for https://download.moodle.org/api/x.y/pluginfo.php service + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_installaddon_pluginfo_client { + + /** + * Factory method returning an instance of this class. + * + * @return tool_installaddon_pluginfo_client + */ + public static function instance() { + return new static(); + } + + /** + * Return the information about the plugin + * + * @throws tool_installaddon_pluginfo_exception + * @param string $component + * @param string $version + * @return stdClass the pluginfo structure + */ + public function get_pluginfo($component, $version) { + + $response = $this->call_service($component, $version); + $response = $this->decode_response($response); + $this->validate_response($response); + + return $response->pluginfo; + } + + // End of external API ///////////////////////////////////////////////// + + /** + * @see self::instance() + */ + protected function __construct() { + } + + /** + * Calls the pluginfo.php service and returns the raw response + * + * @param string $component + * @param string $version + * @return string + */ + protected function call_service($component, $version) { + global $CFG; + require_once($CFG->libdir.'/filelib.php'); + + $curl = new curl(array('proxy' => true)); + $response = $curl->get( + $this->service_request_url(), + $this->service_request_params($component, $version), + $this->service_request_options()); + $curlerrno = $curl->get_errno(); + if (!empty($curlerrno)) { + throw new tool_installaddon_pluginfo_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error); + } + $curlinfo = $curl->get_info(); +/* if ($curlinfo['http_code'] == 404) { + throw new tool_installaddon_pluginfo_exception('err_response_http_code_404'); +}*/ + if ($curlinfo['http_code'] != 200) { + throw new tool_installaddon_pluginfo_exception('err_response_http_code', $curlinfo['http_code']); + } + return $response; + } + + /** + * Return URL to the pluginfo.php service + * + * @return moodle_url + */ + protected function service_request_url() { + global $CFG; + + if (!empty($CFG->config_php_settings['alternativepluginfoserviceurl'])) { + $url = $CFG->config_php_settings['alternativepluginfoserviceurl']; + } else { + $url = 'https://download.moodle.org/api/1.2/pluginfo.php'; + } + + return new moodle_url($url); + } + + /** + * Return list of pluginfo service parameters + * + * @param string $component + * @param string $version + * @return array + */ + protected function service_request_params($component, $version) { + + $params = array(); + $params['format'] = 'json'; + $params['plugin'] = $component.'@'.$version; + + return $params; + } + + /** + * Return cURL options for the service request + * + * @return array of (string)param => (string)value + */ + protected function service_request_options() { + global $CFG; + + $options = array( + 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case + 'CURLOPT_SSL_VERIFYPEER' => true, + ); + + $cacertfile = $CFG->dataroot.'/moodleorgca.crt'; + if (is_readable($cacertfile)) { + // Do not use CA certs provided by the operating system. Instead, + // use this CA cert to verify the updates provider. + $options['CURLOPT_CAINFO'] = $cacertfile; + } + + return $options; + } + + /** + * Decode the raw service response + * + * @param string $raw + * @return stdClass + */ + protected function decode_response($raw) { + return json_decode($raw); + } + + /** + * Validate decoded service response + * + * @param stdClass $response + */ + protected function validate_response($response) { + + if (empty($response)) { + throw new tool_installaddon_pluginfo_exception('err_response_empty'); + } + + if (empty($response->status) or $response->status !== 'OK') { + throw new tool_installaddon_pluginfo_exception('err_response_status', $response->status); + } + + if (empty($response->apiver) or $response->apiver !== '1.2') { + throw new tool_installaddon_pluginfo_exception('err_response_api_version', $response->apiver); + } + + if (empty($response->pluginfo->component) or empty($response->pluginfo->downloadurl) + or empty($response->pluginfo->downloadmd5)) { + throw new tool_installaddon_pluginfo_exception('err_response_pluginfo'); + } + } +} + + +/** + * General exception thrown by {@link tool_installaddon_pluginfo_client} class + * + * @copyright 2013 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_installaddon_pluginfo_exception extends moodle_exception { + + /** + * @param string $errorcode exception description identifier + * @param mixed $debuginfo debugging data to display + */ + public function __construct($errorcode, $a=null, $debuginfo=null) { + parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true)); + } +} diff --git a/admin/tool/installaddon/index.php b/admin/tool/installaddon/index.php index 8ca05d187f0..cb0d2a62d44 100644 --- a/admin/tool/installaddon/index.php +++ b/admin/tool/installaddon/index.php @@ -35,6 +35,14 @@ if (!empty($CFG->disableonclickaddoninstall)) { $installer = tool_installaddon_installer::instance(); +$output = $PAGE->get_renderer('tool_installaddon'); +$output->set_installer_instance($installer); + +// Handle the eventual request for installing from remote repository. +$remoterequest = optional_param('installaddonrequest', null, PARAM_RAW); +$confirmed = optional_param('confirm', false, PARAM_BOOL); +$installer->handle_remote_request($output, $remoterequest, $confirmed); + $form = $installer->get_installfromzip_form(); if ($form->is_cancelled()) { @@ -55,7 +63,5 @@ if ($form->is_cancelled()) { redirect($nexturl); } -$output = $PAGE->get_renderer('tool_installaddon'); -$output->set_installer_instance($installer); - +// Output starts here. echo $output->index_page(); diff --git a/admin/tool/installaddon/lang/en/tool_installaddon.php b/admin/tool/installaddon/lang/en/tool_installaddon.php index 6179238967e..bca494f1263 100644 --- a/admin/tool/installaddon/lang/en/tool_installaddon.php +++ b/admin/tool/installaddon/lang/en/tool_installaddon.php @@ -32,6 +32,7 @@ $string['acknowledgementtext'] = 'I understand that it is my responsibility to h $string['featuredisabled'] = 'Add-on installer is disabled at this site.'; $string['installaddon'] = 'Install add-on!'; $string['installaddons'] = 'Install add-ons'; +$string['installexception'] = 'Oops... An error occured while trying to install the add-on. Turn debugging mode on to see more details about the error.'; $string['installfromrepo'] = 'Install add-ons from Moodle plugins directory'; $string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site fullname, URL and major version will be sent as well, to make the installation process easier for you.'; $string['installfromzip'] = 'Install add-on from the ZIP file'; @@ -49,6 +50,11 @@ $string['permcheckprogress'] = 'Checking for write permission ...'; $string['permcheckresultno'] = 'Plugin type location {$a->path} not writable'; $string['permcheckresultyes'] = 'Plugin type location {$a->path} is writable'; $string['pluginname'] = 'Add-on installer'; +$string['remoterequestalreadyinstalled'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. This plugin is already installed at this site.'; +$string['remoterequestconfirm'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. If you continue, the add-on ZIP package will be downloaded for validation. Nothing will be installed yet.'; +$string['remoterequestinvalid'] = 'There is a request to install add-on from the Moodle plugins directory to this site. Unfortunately, the request is not valid. The add-on cannot be installed.'; +$string['remoterequestpermcheck'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. The plugin type location {$a->typepath} is not writable though. You need to give the write access for the web server user to the plugin type location now. Once the write access is granted, press the continue button to repeat the check.'; +$string['remoterequestpluginfoexception'] = 'Oops... An error occured while trying to obtain information about the add-on {$a->name} ({$a->component}) version {$a->version}. The add-on cannot be installed. Turn debugging mode on to see more details about the error.'; $string['validation'] = 'Add-on package validation'; $string['validationmsg_componentmatch'] = 'Full component name'; $string['validationmsg_componentmismatchname'] = 'Add-on name mismatch'; diff --git a/admin/tool/installaddon/renderer.php b/admin/tool/installaddon/renderer.php index 0cc273dd8c6..c4c9706fcc4 100644 --- a/admin/tool/installaddon/renderer.php +++ b/admin/tool/installaddon/renderer.php @@ -116,7 +116,125 @@ class tool_installaddon_renderer extends plugin_renderer_base { $out .= $this->output->footer(); return $out; + } + /** + * Inform the user about invalid remote installation request. + * + * @param moodle_url $continueurl + * @return string + */ + public function remote_request_invalid_page(moodle_url $continueurl) { + + $out = $this->output->header(); + $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon')); + $out .= $this->output->box(get_string('remoterequestinvalid', 'tool_installaddon'), 'generalbox', 'notice'); + $out .= $this->output->continue_button($continueurl, 'get'); + $out .= $this->output->footer(); + + return $out; + } + + /** + * Inform the user that such plugin is already installed + * + * @param stdClass $data decoded request data + * @param moodle_url $continueurl + * @return string + */ + public function remote_request_alreadyinstalled_page(stdClass $data, moodle_url $continueurl) { + + $out = $this->output->header(); + $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon')); + $out .= $this->output->box(get_string('remoterequestalreadyinstalled', 'tool_installaddon', $data), 'generalbox', 'notice'); + $out .= $this->output->continue_button($continueurl, 'get'); + $out .= $this->output->footer(); + + return $out; + } + + /** + * Let the user confirm the remote installation request. + * + * @param stdClass $data decoded request data + * @param moodle_url $continueurl + * @param moodle_url $cancelurl + * @return string + */ + public function remote_request_confirm_page(stdClass $data, moodle_url $continueurl, moodle_url $cancelurl) { + + $out = $this->output->header(); + $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon')); + $out .= $this->output->confirm(get_string('remoterequestconfirm', 'tool_installaddon', $data), $continueurl, $cancelurl); + $out .= $this->output->footer(); + + return $out; + } + + /** + * Inform the user that the target plugin type location is not writable. + * + * @param stdClass $data decoded request data + * @param string $plugintypepath full path to the plugin type location + * @param moodle_url $continueurl to repeat the write permission check + * @param moodle_url $cancelurl to cancel the installation + * @return string + */ + public function remote_request_permcheck_page(stdClass $data, $plugintypepath, moodle_url $continueurl, moodle_url $cancelurl) { + + $data->typepath = $plugintypepath; + + $out = $this->output->header(); + $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon')); + $out .= $this->output->confirm(get_string('remoterequestpermcheck', 'tool_installaddon', $data), $continueurl, $cancelurl); + $out .= $this->output->footer(); + + return $out; + } + + /** + * Inform the user about pluginfo service call exception + * + * This implementation does not actually use the passed exception. Custom renderers might want to + * display additional data obtained via {@link get_exception_info()}. Also note, this method is called + * in non-debugging mode only. If debugging is allowed at the site, default exception handler is triggered. + * + * @param stdClass $data decoded request data + * @param tool_installaddon_pluginfo_exception $e thrown exception + * @param moodle_url $continueurl + * @return string + */ + public function remote_request_pluginfo_exception(stdClass $data, tool_installaddon_pluginfo_exception $e, moodle_url $continueurl) { + + $out = $this->output->header(); + $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon')); + $out .= $this->output->box(get_string('remoterequestpluginfoexception', 'tool_installaddon', $data), 'generalbox', 'notice'); + $out .= $this->output->continue_button($continueurl, 'get'); + $out .= $this->output->footer(); + + return $out; + } + + /** + * Inform the user about the installer exception + * + * This implementation does not actually use the passed exception. Custom renderers might want to + * display additional data obtained via {@link get_exception_info()}. Also note, this method is called + * in non-debugging mode only. If debugging is allowed at the site, default exception handler is triggered. + * + * @param tool_installaddon_installer_exception $e thrown exception + * @param moodle_url $continueurl + * @return string + */ + public function installer_exception(tool_installaddon_installer_exception $e, moodle_url $continueurl) { + + $out = $this->output->header(); + $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon')); + $out .= $this->output->box(get_string('installexception', 'tool_installaddon'), 'generalbox', 'notice'); + $out .= $this->output->continue_button($continueurl, 'get'); + $out .= $this->output->footer(); + + return $out; } // End of the external API ///////////////////////////////////////////////// diff --git a/admin/tool/installaddon/tests/installer_test.php b/admin/tool/installaddon/tests/installer_test.php index 7f53cb17c2b..fd8d1d1c0b9 100644 --- a/admin/tool/installaddon/tests/installer_test.php +++ b/admin/tool/installaddon/tests/installer_test.php @@ -74,6 +74,57 @@ class tool_installaddon_installer_test extends advanced_testcase { } } } + + public function test_decode_remote_request() { + $installer = testable_tool_installaddon_installer::instance(); + + $request = base64_encode(json_encode(array( + 'name' => '

Stamp collection

"; DELETE FROM mdl_users; --', + 'component' => 'mod_stampcoll', + 'version' => 2013032800, + ))); + $request = $installer->testable_decode_remote_request($request); + $this->assertTrue(is_object($request)); + // One, my little hobbit, never trusts the input parameters! + $this->assertEquals('Stamp collection"; DELETE FROM mdl_users; --', $request->name); + $this->assertEquals('mod_stampcoll', $request->component); + $this->assertEquals(2013032800, $request->version); + + $request = base64_encode(json_encode(array( + 'name' => 'Theme with invalid version number', + 'component' => 'theme_invalid', + 'version' => '1.0', + ))); + $this->assertSame(false, $installer->testable_decode_remote_request($request)); + + $request = base64_encode(json_encode(array( + 'name' => 'Invalid activity name', + 'component' => 'mod_invalid_activity', + 'version' => 2013032800, + ))); + $this->assertSame(false, $installer->testable_decode_remote_request($request)); + + $request = base64_encode(json_encode(array( + 'name' => 'Moodle 3.0', + 'component' => 'core', + 'version' => 2022010100, + ))); + $this->assertSame(false, $installer->testable_decode_remote_request($request)); + + $request = base64_encode(json_encode(array( + 'name' => 'Invalid core subsystem', + 'component' => 'core_cache', + 'version' => 2014123400, + ))); + $this->assertSame(false, $installer->testable_decode_remote_request($request)); + + $request = base64_encode(json_encode(array( + 'name' => 'Non-existing plugintype', + 'component' => 'david_mudrak', + 'version' => 2012123199, + ))); + $this->assertSame(false, $installer->testable_decode_remote_request($request)); + } } @@ -97,6 +148,10 @@ class testable_tool_installaddon_installer extends tool_installaddon_installer { return "2.5'; DROP TABLE mdl_user; --"; } + public function testable_decode_remote_request($request) { + return parent::decode_remote_request($request); + } + protected function should_send_site_info() { return true; } From 0a6a344d29c333fb9e498afdc173e112dd14c1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 29 Mar 2013 01:00:16 +0100 Subject: [PATCH 19/22] MDL-38509 Add hook for the installaddonrequest into admin/index.php If the user selects a plugin in Moodle plugins directory to be installed into their site, Moodle plugins directory will redirect them into their site's admin/index.php script, providing the installaddonrequest parameter. This patch makes sure that only if the user is logged-in as the admin and the site is fully installed and up-to-date, the add-on installation request will be dispatched to the tool_installaddon for actual processing. We need to store the installaddonrequest value in the $PAGE's URL so that is is stored in $SESSION->wantsurl in case the user needs to log in at their site. Thanks to this, the request is dispatched after the user logs in. --- admin/index.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/admin/index.php b/admin/index.php index 35cbdaf1e60..def20d29ecd 100644 --- a/admin/index.php +++ b/admin/index.php @@ -59,10 +59,18 @@ $confirmplugins = optional_param('confirmplugincheck', 0, PARAM_BOOL); $showallplugins = optional_param('showallplugins', 0, PARAM_BOOL); $agreelicense = optional_param('agreelicense', 0, PARAM_BOOL); $fetchupdates = optional_param('fetchupdates', 0, PARAM_BOOL); +$newaddonreq = optional_param('installaddonrequest', null, PARAM_RAW); // Check some PHP server settings -$PAGE->set_url('/admin/index.php'); +if (is_null($newaddonreq)) { + $PAGE->set_url('/admin/index.php'); +} else { + // We need to set the eventual add-on installation request in the $PAGE's URL + // so that it is stored in $SESSION->wantsurl and the admin is redirected + // correctly once they are logged-in. + $PAGE->set_url('/admin/index.php', array('installaddonrequest' => $newaddonreq)); +} $PAGE->set_pagelayout('admin'); // Set a default pagelayout $documentationlink = 'Installation docs'; @@ -423,6 +431,17 @@ if (!empty($id) and $id == $CFG->siteidentifier) { set_config('registered', time()); } +// Check if we are returning from an add-on installation request at moodle.org/plugins +if (!is_null($newaddonreq)) { + if (!empty($CFG->disableonclickaddoninstall)) { + // The feature is disabled in config.php, ignore the request. + } else { + redirect(new moodle_url('/admin/tool/installaddon/index.php', array( + 'installaddonrequest' => $newaddonreq, + 'confirm' => 0))); + } +} + // setup critical warnings before printing admin tree block $insecuredataroot = is_dataroot_insecure(true); $SESSION->admin_critical_warning = ($insecuredataroot==INSECURE_DATAROOT_ERROR); From b2a138a20f40d367c594c4649e9fd6e88b087a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 29 Mar 2013 01:30:00 +0100 Subject: [PATCH 20/22] MDL-38509 Escalate the maturity level of the plugin The API is considered stable now and there should not be a need to change it. --- admin/tool/installaddon/version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/tool/installaddon/version.php b/admin/tool/installaddon/version.php index 403deefc6f8..94ab2e5c22f 100644 --- a/admin/tool/installaddon/version.php +++ b/admin/tool/installaddon/version.php @@ -26,4 +26,4 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'tool_installaddon'; $plugin->version = 2013031400; $plugin->requires = 2013031400; -$plugin->maturity = MATURITY_ALPHA; +$plugin->maturity = MATURITY_BETA; From c9c4d0b24e8e82eda65743693a3a0421191699eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Tue, 2 Apr 2013 16:58:12 +0200 Subject: [PATCH 21/22] MDL-38509 Fix false unit test failures in the add-on installer Previously, the testable validator's get_plugintype_location() used to return path to a directory that itself did not need to be writable. This was causing false failures. This patch reimplements the testable validator's get_plugintype_location() method so that it returns a path inside $CFG->tempdir which is always writable. Also, to make all test methods consistent, all of them now use the testable_tool_installaddon_validator class. Before this patch, some test methods used the testable class and some used the parent class. --- .../writable/local/greenbar/README.txt | 1 - .../installaddon/tests/validator_test.php | 55 ++++++++++++------- 2 files changed, 34 insertions(+), 22 deletions(-) delete mode 100644 admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt diff --git a/admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt b/admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt deleted file mode 100644 index a37b9435fb4..00000000000 --- a/admin/tool/installaddon/tests/fixtures/writable/local/greenbar/README.txt +++ /dev/null @@ -1 +0,0 @@ -Existing diff --git a/admin/tool/installaddon/tests/validator_test.php b/admin/tool/installaddon/tests/validator_test.php index 23d54a088f9..c008d9cfd31 100644 --- a/admin/tool/installaddon/tests/validator_test.php +++ b/admin/tool/installaddon/tests/validator_test.php @@ -42,18 +42,18 @@ class tool_installaddon_validator_test extends basic_testcase { $fixtures = dirname(__FILE__).'/fixtures'; // Non-existing directory. - $validator = tool_installaddon_validator::instance($fixtures.'/nulldir', array( + $validator = testable_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->assertEquals('testable_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( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array( 'foobar/' => true, 'foobar/version.php' => true, 'foobar/index.php' => true, @@ -66,7 +66,7 @@ class tool_installaddon_validator_test extends basic_testcase { 'filenotexists', array('file' => 'foobar/NOTEXISTS.txt'))); // Errors during ZIP extraction - $validator = tool_installaddon_validator::instance($fixtures.'/multidir', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/multidir', array( 'one/' => true, 'one/version.php' => 'Can not write target file', 'two/' => true, @@ -76,14 +76,14 @@ class tool_installaddon_validator_test extends basic_testcase { array('file' => 'one/version.php', 'status' => 'Can not write target file'))); // Insufficient number of extracted files - $validator = tool_installaddon_validator::instance($fixtures.'/emptydir', array( + $validator = testable_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( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/nowrapdir', array( 'version.php' => true, 'index.php' => true, 'lang/' => true, @@ -93,7 +93,7 @@ class tool_installaddon_validator_test extends basic_testcase { $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir')); // Multiple directories - $validator = tool_installaddon_validator::instance($fixtures.'/multidir', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/multidir', array( 'one/' => true, 'one/version.php' => true, 'two/' => true, @@ -102,7 +102,7 @@ class tool_installaddon_validator_test extends basic_testcase { $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'onedir')); // Invalid root directory name - $validator = tool_installaddon_validator::instance($fixtures.'/github', array( + $validator = testable_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, @@ -116,7 +116,7 @@ class tool_installaddon_validator_test extends basic_testcase { public function test_validate_version_php() { $fixtures = dirname(__FILE__).'/fixtures'; - $validator = tool_installaddon_validator::instance($fixtures.'/noversiontheme', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/noversiontheme', array( 'noversion/' => true, 'noversion/lang/' => true, 'noversion/lang/en/' => true, @@ -127,7 +127,7 @@ class tool_installaddon_validator_test extends basic_testcase { $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( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/noversionmod', array( 'noversion/' => true, 'noversion/lang/' => true, 'noversion/lang/en/' => true, @@ -137,7 +137,7 @@ class tool_installaddon_validator_test extends basic_testcase { $this->assertFalse($validator->execute()); $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missingversionphp')); - $validator = tool_installaddon_validator::instance($fixtures.'/plugindir', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array( 'foobar/' => true, 'foobar/version.php' => true, 'foobar/index.php' => true, @@ -148,7 +148,7 @@ class tool_installaddon_validator_test extends basic_testcase { $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'componentmismatchtype', array('expected' => 'block', 'found' => 'local'))); - $validator = tool_installaddon_validator::instance($fixtures.'/plugindir', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array( 'foobar/' => true, 'foobar/version.php' => true, 'foobar/index.php' => true, @@ -175,7 +175,7 @@ class tool_installaddon_validator_test extends basic_testcase { public function test_validate_language_pack() { $fixtures = dirname(__FILE__).'/fixtures'; - $validator = tool_installaddon_validator::instance($fixtures.'/nolang', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array( 'bah/' => true, 'bah/index.php' => true, 'bah/view.php' => true, @@ -185,7 +185,7 @@ class tool_installaddon_validator_test extends basic_testcase { $this->assertFalse($validator->execute()); $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfolder')); - $validator = tool_installaddon_validator::instance($fixtures.'/nolang', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array( 'bah/' => true, 'bah/version.php' => true, 'bah/lang/' => true, @@ -195,7 +195,7 @@ class tool_installaddon_validator_test extends basic_testcase { $this->assertFalse($validator->execute()); $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'missinglangenfile')); - $validator = tool_installaddon_validator::instance($fixtures.'/nolang', array( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array( 'bah/' => true, 'bah/version.php' => true, 'bah/lang/' => true, @@ -208,7 +208,7 @@ class tool_installaddon_validator_test extends basic_testcase { $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( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/nolang', array( 'bah/' => true, 'bah/version.php' => true, 'bah/lang/' => true, @@ -220,7 +220,7 @@ class tool_installaddon_validator_test extends basic_testcase { $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( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/noversiontheme', array( 'noversion/' => true, 'noversion/lang/' => true, 'noversion/lang/en/' => true, @@ -231,7 +231,7 @@ class tool_installaddon_validator_test extends basic_testcase { $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( + $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array( 'foobar/' => true, 'foobar/version.php' => true, 'foobar/index.php' => true, @@ -258,7 +258,8 @@ class tool_installaddon_validator_test extends basic_testcase { $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')); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::ERROR, 'targetexists', + $validator->get_plugintype_location('local').'/greenbar')); $validator = testable_tool_installaddon_validator::instance($fixtures.'/plugindir', array( 'foobar/' => true, @@ -270,7 +271,8 @@ class tool_installaddon_validator_test extends basic_testcase { $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')); + $this->assertTrue($this->has_message($validator->get_messages(), $validator::INFO, 'pathwritable', + $validator->get_plugintype_location('local'))); } public function test_parse_version_php() { @@ -317,6 +319,17 @@ class testable_tool_installaddon_validator extends tool_installaddon_validator { } public function get_plugintype_location($plugintype) { - return dirname(__FILE__).'/fixtures/writable/'.$plugintype; + + $testableroot = make_temp_directory('testable_tool_installaddon_validator/plugintypes'); + if (!is_dir($testableroot.'/'.$plugintype)) { + make_temp_directory('testable_tool_installaddon_validator/plugintypes/'.$plugintype); + } + + if ($plugintype === 'local') { + // We need the following for the test_validate_target_location() method + make_temp_directory('testable_tool_installaddon_validator/plugintypes/local/greenbar'); + } + + return $testableroot.'/'.$plugintype; } } From 3e6a8aeb7ca46e26fca7fae9ff3df3765997baad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Tue, 2 Apr 2013 23:11:01 +0200 Subject: [PATCH 22/22] MDL-38509 Unify curl class usage in the add-on installer tool Both classes using cURL features now access it via the core curl wrapper class. Credit goes to Dan Poltawski for spotting the previous discrepancy during the integration review. --- admin/tool/installaddon/classes/installer.php | 89 ++++++------------- .../installaddon/classes/pluginfo_client.php | 23 +++-- 2 files changed, 44 insertions(+), 68 deletions(-) diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index c83fa09d5cb..fa4babb5bed 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -349,52 +349,7 @@ class tool_installaddon_installer { */ public function download_file($source, $target) { global $CFG; - - $newlines = array("\r", "\n"); - $source = str_replace($newlines, '', $source); - if (!preg_match('|^https?://|i', $source)) { - throw new tool_installaddon_installer_exception('err_download_transport_protocol', $source); - } - if (!$ch = curl_init($source)) { - throw new tool_installaddon_installer_exception('err_curl_init', $source); - } - - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // verify the peer's certificate - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // check the existence of a common name and also verify that it matches the hostname provided - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the transfer as a string - curl_setopt($ch, CURLOPT_HEADER, false); // don't include the header in the output - curl_setopt($ch, CURLOPT_TIMEOUT, 3600); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); // nah, moodle.org is never unavailable! :-p - curl_setopt($ch, CURLOPT_URL, $source); - - $cacertfile = $CFG->dataroot.'/moodleorgca.crt'; - if (is_readable($cacertfile)) { - // Do not use CA certs provided by the operating system. Instead, - // use this CA cert to verify the ZIP provider. - curl_setopt($ch, CURLOPT_CAINFO, $cacertfile); - } - - if (!empty($CFG->proxyhost) and !is_proxybypass($source)) { - if (!empty($CFG->proxytype)) { - if (strtoupper($CFG->proxytype) === 'SOCKS5') { - curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); - } else { - curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); - curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, false); - } - } - - if (empty($CFG->proxyport)) { - curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost); - } else { - curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost.':'.$CFG->proxyport); - } - - if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) { - curl_setopt($ch, CURLOPT_PROXYUSERPWD, $CFG->proxyuser.':'.$CFG->proxypassword); - curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM); - } - } + require_once($CFG->libdir.'/filelib.php'); $targetfile = fopen($target, 'w'); @@ -402,27 +357,41 @@ class tool_installaddon_installer { throw new tool_installaddon_installer_exception('err_download_write_file', $target); } - curl_setopt($ch, CURLOPT_FILE, $targetfile); + $options = array( + 'file' => $targetfile, + 'timeout' => 300, + 'followlocation' => true, + 'maxredirs' => 3, + 'ssl_verifypeer' => true, + 'ssl_verifyhost' => 2, + ); - $result = curl_exec($ch); - - // try to detect encoding problems - if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) { - curl_setopt($ch, CURLOPT_ENCODING, 'none'); - $result = curl_exec($ch); + $cacertfile = $CFG->dataroot.'/moodleorgca.crt'; + if (is_readable($cacertfile)) { + // Do not use CA certs provided by the operating system. Instead, + // use this CA cert to verify the ZIP provider. + $options['cainfo'] = $cacertfile; } + $curl = new curl(array('proxy' => true)); + + $result = $curl->download_one($source, null, $options); + + $curlinfo = $curl->get_info(); + fclose($targetfile); - $curlerrno = curl_errno($ch); - $curlerror = curl_error($ch); - $curlinfo = curl_getinfo($ch); + if ($result !== true) { + throw new tool_installaddon_installer_exception('err_curl_exec', array( + 'url' => $source, 'errorno' => $curl->get_errno(), 'error' => $result)); - if ($result === false or $curlerrno) { - throw new tool_installaddon_installer_exception('err_curl_exec', array('url' => $source, 'errorno' => $curlerrno, 'error' => $curlerror)); + } else if (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200) { + throw new tool_installaddon_installer_exception('err_curl_http_code', array( + 'url' => $source, 'http_code' => $curlinfo['http_code'])); - } else if (is_array($curlinfo) and (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200)) { - throw new tool_installaddon_installer_exception('err_curl_code', array('url' => $source, 'httpcode' => $curlinfo['http_code'])); + } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) { + throw new tool_installaddon_installer_exception('err_curl_ssl_verify', array( + 'url' => $source, 'ssl_verify_result' => $curlinfo['ssl_verify_result'])); } } diff --git a/admin/tool/installaddon/classes/pluginfo_client.php b/admin/tool/installaddon/classes/pluginfo_client.php index 521389911f7..49cc3341175 100644 --- a/admin/tool/installaddon/classes/pluginfo_client.php +++ b/admin/tool/installaddon/classes/pluginfo_client.php @@ -80,21 +80,28 @@ class tool_installaddon_pluginfo_client { require_once($CFG->libdir.'/filelib.php'); $curl = new curl(array('proxy' => true)); + $response = $curl->get( $this->service_request_url(), $this->service_request_params($component, $version), $this->service_request_options()); + $curlerrno = $curl->get_errno(); - if (!empty($curlerrno)) { - throw new tool_installaddon_pluginfo_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error); - } $curlinfo = $curl->get_info(); -/* if ($curlinfo['http_code'] == 404) { - throw new tool_installaddon_pluginfo_exception('err_response_http_code_404'); -}*/ - if ($curlinfo['http_code'] != 200) { - throw new tool_installaddon_pluginfo_exception('err_response_http_code', $curlinfo['http_code']); + + if (!empty($curlerrno)) { + throw new tool_installaddon_pluginfo_exception('err_curl_exec', array( + 'url' => $curlinfo['url'], 'errno' => $curlerrno, 'error' => $curl->error)); + + } else if ($curlinfo['http_code'] != 200) { + throw new tool_installaddon_pluginfo_exception('err_curl_http_code', array( + 'url' => $curlinfo['url'], 'http_code' => $curlinfo['http_code'])); + + } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) { + throw new tool_installaddon_pluginfo_exception('err_curl_ssl_verify', array( + 'url' => $curlinfo['url'], 'ssl_verify_result' => $curlinfo['ssl_verify_result'])); } + return $response; }