MDL-48493 admin: Make plugin installer able to detect plugin component
authorDavid Mudrák <david@moodle.com>
Fri, 12 Dec 2014 10:19:30 +0000 (11:19 +0100)
committerDavid Mudrák <david@moodle.com>
Thu, 15 Jan 2015 11:58:14 +0000 (12:58 +0100)
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
admin/tool/installaddon/tests/fixtures/zips/bar.zip [new file with mode: 0644]
admin/tool/installaddon/tests/installer_test.php

index be0da1c..d9807f1 100644 (file)
@@ -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 (file)
index 0000000..b190d2e
Binary files /dev/null and b/admin/tool/installaddon/tests/fixtures/zips/bar.zip differ
index 3ebf2f7..7280eb9 100644 (file)
@@ -143,6 +143,21 @@ class tool_installaddon_installer_testcase extends advanced_testcase {
         $this->assertTrue(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);
+    }
 }