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 00000000000..a2090259878 Binary files /dev/null and b/admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip differ diff --git a/admin/tool/installaddon/tests/installer_test.php b/admin/tool/installaddon/tests/installer_test.php index a5028ddb800..f32bc193e6d 100644 --- a/admin/tool/installaddon/tests/installer_test.php +++ b/admin/tool/installaddon/tests/installer_test.php @@ -39,7 +39,7 @@ require_once($CFG->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)); + } + } + } }