From bbf3cd4e93cd856ff4159d49403d8169f257023f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Fri, 12 Dec 2014 11:19:30 +0100 Subject: [PATCH] MDL-48493 admin: Make plugin installer able to detect plugin component On contrary to deeper heuristic (read: guessing) we perform in the Plugins directory (such as looking at the names of the language files), here we simply rely on the plugin component being correctly defined in the version.php file. The validator class has more robust processing, to make sure the component declaration is not provided in a commented area of the version.php etc. However, as it is fully acceptable that the auto-detection fails if the version.php uses non-standard syntax, this easier approach is valid here. --- admin/tool/installaddon/classes/installer.php | 101 ++++++++++++++++++ .../installaddon/tests/fixtures/zips/bar.zip | Bin 0 -> 908 bytes .../installaddon/tests/installer_test.php | 19 ++++ 3 files changed, 120 insertions(+) create mode 100644 admin/tool/installaddon/tests/fixtures/zips/bar.zip diff --git a/admin/tool/installaddon/classes/installer.php b/admin/tool/installaddon/classes/installer.php index be0da1c16f3..d9807f1111c 100644 --- a/admin/tool/installaddon/classes/installer.php +++ b/admin/tool/installaddon/classes/installer.php @@ -459,6 +459,27 @@ class tool_installaddon_installer { clearstatcache(); } + /** + * Detect the given plugin's component name + * + * Only plugins that declare valid $plugin->component value in the version.php + * are supported. + * + * @param string $zipfilepath full path to the saved ZIP file + * @param string $workdir full path to the directory we can use for extracting required bits from the archive + * @return string|bool declared component name or false if unable to detect + */ + public function detect_plugin_component($zipfilepath, $workdir) { + + $versionphp = $this->extract_versionphp_file($zipfilepath, $workdir); + + if (empty($versionphp)) { + return false; + } + + return $this->detect_plugin_component_from_versionphp(file_get_contents($workdir.'/'.$versionphp)); + } + //// End of external API /////////////////////////////////////////////////// /** @@ -626,6 +647,86 @@ class tool_installaddon_installer { return $data; } + + /** + * Extracts the version.php from the given plugin ZIP file into the target directory + * + * @param string $zipfilepath full path to the saved ZIP file + * @param string $targetdir full path to extract the file to + * @return string|bool path to the version.php within the $targetpath; false on error (e.g. not found) + */ + protected function extract_versionphp_file($zipfilepath, $targetdir) { + global $CFG; + require_once($CFG->libdir.'/filelib.php'); + + $fp = get_file_packer('application/zip'); + $files = $fp->list_files($zipfilepath); + + if (empty($files)) { + return false; + } + + $rootdirname = null; + $found = null; + + foreach ($files as $file) { + // Valid plugin ZIP package has just one root directory with all + // files in it. + $pathnameitems = explode('/', $file->pathname); + + if (empty($pathnameitems)) { + return false; + } + + // Set the expected name of the root directory in the first + // iteration of the loop. + if ($rootdirname === null) { + $rootdirname = $pathnameitems[0]; + } + + // Require the same root directory for all files in the ZIP + // package. + if ($rootdirname !== $pathnameitems[0]) { + return false; + } + + // If we reached the valid version.php file, remember it. + if ($pathnameitems[1] === 'version.php' and !$file->is_directory and $file->size > 0) { + $found = $file->pathname; + } + } + + if (empty($found)) { + return false; + } + + $extracted = $fp->extract_to_pathname($zipfilepath, $targetdir, array($found)); + + if (empty($extracted)) { + return false; + } + + // The following syntax uses function array dereferencing, added in PHP 5.4.0. + return array_keys($extracted)[0]; + } + + /** + * Return the plugin component declared in its version.php file + * + * @param string $code the contents of the version.php file + * @return string|bool declared plugin component or false if unable to detect + */ + protected function detect_plugin_component_from_versionphp($code) { + + $result = preg_match_all('#^\s*\$plugin\->component\s*=\s*([\'"])(.+?_.+?)\1\s*;#m', $code, $matches); + + // Return if and only if the single match was detected. + if ($result === 1 and !empty($matches[2][0])) { + return $matches[2][0]; + } + + return false; + } } diff --git a/admin/tool/installaddon/tests/fixtures/zips/bar.zip b/admin/tool/installaddon/tests/fixtures/zips/bar.zip new file mode 100644 index 0000000000000000000000000000000000000000..b190d2e6f7c8e6a7d7eb865b0b5a901d2652a28a GIT binary patch literal 908 zcmWIWW@h1H0D*%MJ+5E|lwe_yVMt0W(hm*cWMJmn*A=pHZ&yfZ1vdjD%L`@(1~3r- z*F7CgHz!ngPGVj<#E88>?Yr<9u>#Eq9%Lg@^B|_&2ATje38zCqrqnA;RnrD~1BAuV zOi9bnj|X~7uOOoU>@$#oAdF_9jXh8S7ne$LNl|8AdbD~$PHB2(US48us(P$~t%AB+ ze!c?G7)5n!E>Iwd0Bzy`2jTLMOw)aVyfh%@huU41T2!2wp9i-S6q+E6X6NRO`X@9V zJPOi0q3L`6%;l}x-nv@n&YTb45Ncp#U}9)uWMX8n!OcVc`E#)+&zC-%${@O8`P1je z)m6n-DxVZzDmE=mY^fOYQr(3NOXL{>ycwC~m~lm|1kiOLAi(g}5kx~GniUe!XwE`5 z2{W3JO|pZU1dVi{aaf}Y;V{f7M>ehz**I_l0Gfy;u8assertTrue(is_file($jobroot.'/moved/sub/folder/readme.txt')); $this->assertSame('Hello world!', file_get_contents($jobroot.'/moved/sub/folder/readme.txt')); } + + public function test_detect_plugin_component() { + $jobid = md5(rand().uniqid('test_', true)); + $workdir = make_temp_directory('tool_installaddon/'.$jobid.'/version'); + $zipfile = __DIR__.'/fixtures/zips/bar.zip'; + $installer = tool_installaddon_installer::instance(); + $this->assertEquals('foo_bar', $installer->detect_plugin_component($zipfile, $workdir)); + } + + public function test_detect_plugin_component_from_versionphp() { + $installer = testable_tool_installaddon_installer::instance(); + $this->assertEquals('bar_bar_conan', $installer->detect_plugin_component_from_versionphp(' +$plugin->version = 2014121300; + $plugin->component= "bar_bar_conan" ; // Go Arnie go!')); + } } @@ -173,4 +188,8 @@ class testable_tool_installaddon_installer extends tool_installaddon_installer { protected function should_send_site_info() { return true; } + + public function detect_plugin_component_from_versionphp($code) { + return parent::detect_plugin_component_from_versionphp($code); + } }