diff --git a/components/Blueprints/Steps/class-installpluginstep.php b/components/Blueprints/Steps/class-installpluginstep.php index 6f9113c5..ee9b0f3c 100644 --- a/components/Blueprints/Steps/class-installpluginstep.php +++ b/components/Blueprints/Steps/class-installpluginstep.php @@ -215,34 +215,6 @@ public function after( $title = '' ) { exit( 1 ); } -// List files from the plugin zip -$zip = new ZipArchive(); -if ( $zip->open( $plugin_zip_path ) !== true ) { - fwrite( STDERR, "Failed to open plugin zip file: " . $plugin_zip_path . "\n" ); - exit( 1 ); -} - -fwrite( STDERR, "Plugin zip contents:" . "\n" ); -for ( $i = 0; $i < $zip->numFiles; $i ++ ) { - $filename = $zip->getNameIndex( $i ); - $stats = $zip->statIndex( $i ); - $size = $stats['size']; - $is_dir = substr( $filename, - 1 ) === '/'; -} - -// Extract plugin slug from the zip file -$plugin_slug = ''; -// Check the first directory in the zip file -if ( $zip->numFiles > 0 ) { - $first_entry = $zip->getNameIndex( 0 ); - // Most plugin zips have a top-level directory that is the plugin slug - if ( strpos( $first_entry, '/' ) !== false ) { - $plugin_slug = explode( '/', $first_entry )[0]; - } -} - -$zip->close(); - // Make sure the destination directory is writable $wp_plugin_dir = WP_PLUGIN_DIR; if ( ! is_writable( $wp_plugin_dir ) ) { @@ -258,26 +230,9 @@ public function after( $title = '' ) { $skin = new Blueprint_WP_Upgrader_Skin(); $upgrader = new Plugin_Upgrader( $skin ); -// If we have a plugin slug from the zip, create the target directory first -$target_directory = null; -if ( ! empty( $plugin_slug ) ) { - $target_directory = WP_PLUGIN_DIR . '/' . $plugin_slug; - - // Remove existing directory if it exists - if ( is_dir( $target_directory ) ) { - $GLOBALS['wp_filesystem']->delete( $target_directory, true ); - } - - // Create the directory - $GLOBALS['wp_filesystem']->mkdir( $target_directory ); - - fwrite( STDERR, "Created target directory: " . $target_directory . "\n" ); -} - // Install the plugin $result = $upgrader->install( $plugin_zip_path, array( 'overwrite_package' => true, - 'destination' => $target_directory, ) ); // Check for filesystem errors @@ -304,7 +259,7 @@ public function after( $title = '' ) { } // Installation successful, find the main plugin file. -$plugin_folder_name = ! empty( $plugin_slug ) ? $plugin_slug : ( $upgrader->result['destination_name'] ?? null ); +$plugin_folder_name = $upgrader->result['destination_name'] ?? null; if ( ! $plugin_folder_name ) { fwrite( STDERR, "Could not determine plugin folder name after installation." . "\n" ); exit( 1 ); diff --git a/components/Blueprints/Steps/class-installthemestep.php b/components/Blueprints/Steps/class-installthemestep.php index ee9bd305..6050c39e 100644 --- a/components/Blueprints/Steps/class-installthemestep.php +++ b/components/Blueprints/Steps/class-installthemestep.php @@ -160,40 +160,10 @@ function show_message( $message ) { } } -// Extract theme slug from the zip file -$theme_slug = ''; -$zip = new ZipArchive(); -if ( $zip->open( $theme_zip_path ) === true ) { - // Check the first directory in the zip file - if ( $zip->numFiles > 0 ) { - $first_entry = $zip->getNameIndex( 0 ); - // Most theme zips have a top-level directory that is the theme slug - if ( strpos( $first_entry, '/' ) !== false ) { - $theme_slug = explode( '/', $first_entry )[0]; - } - } - $zip->close(); -} - -// Target directory for the theme -$target_directory = null; -if ( ! empty( $theme_slug ) ) { - $target_directory = $wp_theme_dir . '/' . $theme_slug; - - // Remove existing directory if it exists - if ( is_dir( $target_directory ) ) { - $GLOBALS['wp_filesystem']->delete( $target_directory, true ); - } - - // Create the directory - $GLOBALS['wp_filesystem']->mkdir( $target_directory ); -} - // Use the Theme_Upgrader class to install the theme $upgrader = new Theme_Upgrader(); $result = $upgrader->install( $theme_zip_path, array( 'overwrite_package' => true, - 'destination' => $target_directory, ) ); // Check for filesystem errors @@ -217,7 +187,7 @@ function show_message( $message ) { } // Installation successful, get the theme folder name (stylesheet) from the result array -$theme_folder_name = ! empty( $theme_slug ) ? $theme_slug : ( $upgrader->result['destination_name'] ?? null ); +$theme_folder_name = $upgrader->result['destination_name'] ?? null; if ( ! $theme_folder_name ) { error_log( "Blueprint Error: Could not determine theme folder name after installation." ); exit( 1 ); diff --git a/components/Blueprints/Tests/Unit/Steps/InstallPluginStepTest.php b/components/Blueprints/Tests/Unit/Steps/InstallPluginStepTest.php index 2fb2a759..7b7df705 100644 --- a/components/Blueprints/Tests/Unit/Steps/InstallPluginStepTest.php +++ b/components/Blueprints/Tests/Unit/Steps/InstallPluginStepTest.php @@ -198,6 +198,35 @@ public function testInstallPluginFromZipWithSubfolder() { $this->assertContains( 'subfolder-name/test-plugin.php', $active_plugins ); } + public function testInstallPluginFromZipWithDotRootPreservesExistingPlugins() { + $target_filesystem = $this->runtime->get_target_filesystem(); + $target_filesystem->put_contents( + 'wp-content/plugins/existing-plugin.php', + self::PLUGIN_FILE_CONTENT + ); + + $zip_file = wp_join_unix_paths( $this->execution_context_path, 'dot-root-plugin.zip' ); + $zip = new ZipArchive(); + if ( $zip->open( $zip_file, ZipArchive::CREATE ) === true ) { + $zip->addEmptyDir( './' ); + $zip->addFromString( './test-plugin.php', self::PLUGIN_FILE_CONTENT ); + $zip->close(); + } + + $step = new InstallPluginStep( + DataReference::create( './dot-root-plugin.zip', [ + ExecutionContextPath::class + ] ), + false + ); + + $tracker = new Tracker(); + $step->run( $this->runtime, $tracker ); + + $this->assertTrue( $target_filesystem->exists( 'wp-content/plugins/existing-plugin.php' ) ); + $this->assertTrue( $target_filesystem->exists( 'wp-content/plugins/dot-root-plugin/test-plugin.php' ) ); + } + public function testInstallPluginFromADirectory() { $this->execution_context->mkdir( 'plugin-directory', [ 'recursive' => true ] diff --git a/components/Blueprints/Tests/Unit/Steps/InstallThemeStepTest.php b/components/Blueprints/Tests/Unit/Steps/InstallThemeStepTest.php index 742350df..fc9c7469 100644 --- a/components/Blueprints/Tests/Unit/Steps/InstallThemeStepTest.php +++ b/components/Blueprints/Tests/Unit/Steps/InstallThemeStepTest.php @@ -124,17 +124,27 @@ public function testInstallThemeWithoutActivation() { $this->assertNotEquals( 'test-theme', trim( $active_theme ) ); } - public function testInstallThemeFromZip() { - $zip_file = wp_join_unix_paths( $this->execution_context_path, 'zipped-test-theme.zip' ); + public function testInstallThemeFromZipWithDotRootPreservesExistingThemes() { + $target_filesystem = $this->runtime->get_target_filesystem(); + $target_filesystem->mkdir( + 'wp-content/themes/existing-theme', [ 'recursive' => true ] + ); + $target_filesystem->put_contents( + 'wp-content/themes/existing-theme/style.css', + self::THEME_STYLE_CSS_CONTENT + ); + + $zip_file = wp_join_unix_paths( $this->execution_context_path, 'dot-root-theme.zip' ); $zip = new ZipArchive(); if ( $zip->open( $zip_file, ZipArchive::CREATE ) === true ) { - $zip->addFromString( 'test-theme/style.css', self::THEME_STYLE_CSS_CONTENT ); - $zip->addFromString( 'test-theme/index.php', self::THEME_INDEX_PHP_CONTENT ); + $zip->addEmptyDir( './' ); + $zip->addFromString( './style.css', self::THEME_STYLE_CSS_CONTENT ); + $zip->addFromString( './index.php', self::THEME_INDEX_PHP_CONTENT ); $zip->close(); } $step = new InstallThemeStep( - DataReference::create( './zipped-test-theme.zip', [ + DataReference::create( './dot-root-theme.zip', [ ExecutionContextPath::class ] ), true @@ -143,10 +153,10 @@ public function testInstallThemeFromZip() { $tracker = new Tracker(); $step->run( $this->runtime, $tracker ); - $fs = $this->runtime->get_target_filesystem(); - $this->assertTrue( $fs->exists( 'wp-content/themes/test-theme' ) ); - $this->assertTrue( $fs->exists( 'wp-content/themes/test-theme/style.css' ) ); - $this->assertTrue( $fs->exists( 'wp-content/themes/test-theme/index.php' ) ); + $this->assertTrue( $target_filesystem->exists( 'wp-content/themes/existing-theme/style.css' ) ); + $this->assertTrue( $target_filesystem->exists( 'wp-content/themes/dot-root-theme' ) ); + $this->assertTrue( $target_filesystem->exists( 'wp-content/themes/dot-root-theme/style.css' ) ); + $this->assertTrue( $target_filesystem->exists( 'wp-content/themes/dot-root-theme/index.php' ) ); $active_theme = $this->runtime->eval_php_code_in_subprocess( <<<'PHP' @@ -157,6 +167,6 @@ public function testInstallThemeFromZip() { )->output_file_content; - $this->assertEquals( 'test-theme', trim( $active_theme ) ); + $this->assertEquals( 'dot-root-theme', trim( $active_theme ) ); } } diff --git a/components/Blueprints/Tests/Unit/Steps/StepTestCase.php b/components/Blueprints/Tests/Unit/Steps/StepTestCase.php index a9d7ed97..75822b0a 100644 --- a/components/Blueprints/Tests/Unit/Steps/StepTestCase.php +++ b/components/Blueprints/Tests/Unit/Steps/StepTestCase.php @@ -101,10 +101,8 @@ public function setUp(): void { * @after */ public function tearDown(): void { - // Don't clean up on Windows – it adds ~20s to each test in GitHub CI! - if (PHP_OS_FAMILY === 'Windows') { - return; - } + // Cleanup is slow on Windows, but retaining every copied WordPress site + // exhausts the temporary drive during the full test suite. // Clean up temp directory if ( is_dir( $this->document_root ) ) { $this->removeDirectory( $this->document_root );