From f9ae5fc5fec05d4905943ad1a6f4dc700733098b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 9 Jun 2025 10:35:15 +0200 Subject: [PATCH 01/17] index on trunk: b18dbb24 [Blueprints] Import WXRs via the DataLiberation importer (#127) From 21cb378686ef7420cefe0bb4e3fe515eeeaa8340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 9 Jun 2025 19:03:35 +0200 Subject: [PATCH 02/17] Consistent output handling for Playground CLI --- components/Blueprints/bin/blueprint.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 0517b4cd..6a11eef9 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -352,7 +352,7 @@ function handleExecCommand( array $positionalArgs, array $options, array $comman $permission = $ex->getPermission(); $flag = RunnerConfiguration::getPermissionCliFlag( $permission ); - $progressReporter->reportError(sprintf("Permission Error: %s", $ex->getMessage())); + $progressReporter->reportError(sprintf("Permission Error: %s", $ex->getMessage()), $ex); $progressReporter->reportError(sprintf("Tip: Run with --allow=%s to grant this permission.", $flag)); exit( 1 ); } @@ -609,7 +609,7 @@ function showCommandHelpMessage( string $command, array $commandConfig ): void { exit( 1 ); } - $progressReporter->reportError($ex->getMessage() . " See the validation errors below:"); + $progressReporter->reportError($ex->getMessage() . ' See the validation errors below:'); $lastPrettyPath = ''; $currentError = $ex->schemaError; while ( $currentError ) { @@ -623,7 +623,6 @@ function showCommandHelpMessage( string $command, array $commandConfig ): void { } exit( 1 ); } catch ( Exception $ex ) { - $progressReporter->reportError($ex->getMessage()); - $progressReporter->reportError("Try 'help' for usage."); + $progressReporter->reportError($ex->getMessage(), $ex); exit( 1 ); } From 31e26b02842550a9029e400bc1a6720155409df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 9 Jun 2025 19:44:27 +0200 Subject: [PATCH 03/17] Do not assume the execution context root is a local directory --- components/Blueprints/Runner.php | 11 ++++++++--- components/Blueprints/Runtime.php | 4 ++-- .../Blueprints/Steps/scripts/import-content.php | 11 +++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/components/Blueprints/Runner.php b/components/Blueprints/Runner.php index 6952d16e..a5b9c9ba 100644 --- a/components/Blueprints/Runner.php +++ b/components/Blueprints/Runner.php @@ -227,8 +227,13 @@ public function run(): void { $targetSiteFs = LocalFilesystem::create( $this->configuration->getTargetSiteRoot() ); $wpCliReference = DataReference::create( 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar' ); - $execution_context_root = $this->blueprintExecutionContext->get_meta()['root']; - assert(is_string($execution_context_root) && strlen($execution_context_root) > 0, 'Assertion failed: Execution context root was empty.'); + $execution_context = $this->blueprintExecutionContext->get_meta(); + if( + isset($execution_context['root']) && + ( !is_string($execution_context['root']) || strlen($execution_context['root']) === 0) + ) { + throw new BlueprintExecutionException('Execution context was a local directory, but the Runner could not determine the root directory. This should never happen. Please report this as a bug.'); + } $this->runtime = new Runtime( $targetSiteFs, @@ -238,7 +243,7 @@ public function run(): void { $this->blueprintArray, $tempRoot, $wpCliReference, - $execution_context_root + $execution_context['root'] ); $this->progressObserver->setRuntime( $this->runtime ); $progress['wpCli']->setCaption( 'Downloading WP-CLI' ); diff --git a/components/Blueprints/Runtime.php b/components/Blueprints/Runtime.php index 17c88a69..4083df64 100644 --- a/components/Blueprints/Runtime.php +++ b/components/Blueprints/Runtime.php @@ -75,7 +75,7 @@ public function __construct( array $blueprint, string $tempRoot, DataReference $wpCliReference, - string $executionContextRoot + ?string $executionContextRoot=null ) { $this->targetFs = $targetFs; $this->configuration = $configuration; @@ -87,7 +87,7 @@ public function __construct( $this->executionContextRoot = $executionContextRoot; } - public function getExecutionContextRoot(): string { + public function getExecutionContextRoot(): ?string { return $this->executionContextRoot; } diff --git a/components/Blueprints/Steps/scripts/import-content.php b/components/Blueprints/Steps/scripts/import-content.php index bdbe1f32..ae6383bc 100644 --- a/components/Blueprints/Steps/scripts/import-content.php +++ b/components/Blueprints/Steps/scripts/import-content.php @@ -244,10 +244,6 @@ function($event) use ($reporter) { bail_out( 'The "source" option is required.' ); } - if(!isset($options['execution_context_root'])) { - bail_out( 'The "execution_context_root" option is required.' ); - } - // Set up progress stages $mainTracker->split([ 'setup' => ['ratio' => 10, 'caption' => 'Setting up import'], @@ -263,9 +259,12 @@ function($event) use ($reporter) { $content_source = DataReference::create($options['source'], [ ExecutionContextPath::class, ]); - $execution_context = LocalFilesystem::create($options['execution_context_root']); $resolver = new DataReferenceResolver($httpClient); - $resolver->setExecutionContext($execution_context); + if(isset($options['execution_context_root']) && $options['execution_context_root'] !== '') { + $resolver->setExecutionContext( + LocalFilesystem::create($options['execution_context_root']) + ); + } $resolved_source = $resolver->resolve_uncached($content_source); $setupTracker->set(30, 'Configuring import mode'); From f926e6d19fe3a698600257290e41fa258f19e049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 9 Jun 2025 19:53:01 +0200 Subject: [PATCH 04/17] Document error messages to reuse --- components/Blueprints/Runner.php | 2 ++ components/Blueprints/bin/blueprint.php | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/components/Blueprints/Runner.php b/components/Blueprints/Runner.php index a5b9c9ba..e0dc7366 100644 --- a/components/Blueprints/Runner.php +++ b/components/Blueprints/Runner.php @@ -476,6 +476,8 @@ private function validateBlueprint(): void { $this->recommendedWpVersion = $wp_version['max']; $max = WordPressVersion::fromString( $wp_version['max'] ); if ( ! $max ) { + // @TODO: Reuse this error message + // 'Unrecognized WordPress version. Please use "latest", a URL, or a numeric version such as "6.2", "6.0.1", "6.2-beta1", or "6.2-RC1"' throw new BlueprintExecutionException( 'Invalid WordPress version string in wordpressVersion.max: ' . $wp_version['max'] ); } } diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 6a11eef9..f5c82454 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -262,6 +262,18 @@ function createProgressReporter(): ProgressReporter { 'db-name' => [ null, true, 'wordpress', 'MySQL database' ], 'db-path' => [ 'p', true, 'wp.db', 'SQLite file path' ], 'truncate-new-site-directory' => [ 't', false, false, 'Delete target directory if it exists before execution' ], + /** + * @TODO: Reuse this error message removed from the Playground repo: + * + * if (!blueprintMayReadAdjacentFiles) { + * throw new ReportableError( + * `Error: Blueprint contained tried to read a local file at path "${path}" (via a resource of type "bundled"). ` + + * `Playground restricts access to local resources by default as a security measure. \n\n` + + * `You can allow this Blueprint to read files from the same parent directory by explicitly adding the ` + + * `--blueprint-may-read-adjacent-files option to your command.` + * ); + * } + */ 'allow' => [ null, true, null, 'Allowed permissions. One of: ' . implode( ', ', $supportedPermissions ) ], ] ), 'examples' => [ From c932c6d9bf8c805e2329627baf61a7a76a7ad635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 11 Jun 2025 01:55:29 +0200 Subject: [PATCH 05/17] More Playground compat --- components/Blueprints/Runner.php | 3 +- components/Blueprints/Runtime.php | 3 +- .../SiteResolver/NewSiteResolver.php | 63 +++++++++++++------ components/Blueprints/bin/blueprint.php | 1 + .../ReadStream/BaseByteReadStream.php | 2 +- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/components/Blueprints/Runner.php b/components/Blueprints/Runner.php index e0dc7366..b57fe6fa 100644 --- a/components/Blueprints/Runner.php +++ b/components/Blueprints/Runner.php @@ -52,6 +52,7 @@ use WordPress\Filesystem\LocalFilesystem; use WordPress\HttpClient\ByteStream\RequestReadStream; use WordPress\HttpClient\Client; +use WordPress\HttpClient\Request; use WordPress\Zip\ZipFilesystem; use function WordPress\Encoding\utf8_is_valid_byte_stream; @@ -243,7 +244,7 @@ public function run(): void { $this->blueprintArray, $tempRoot, $wpCliReference, - $execution_context['root'] + isset($execution_context['root']) ? $execution_context['root'] : null ); $this->progressObserver->setRuntime( $this->runtime ); $progress['wpCli']->setCaption( 'Downloading WP-CLI' ); diff --git a/components/Blueprints/Runtime.php b/components/Blueprints/Runtime.php index 4083df64..cbf49ae6 100644 --- a/components/Blueprints/Runtime.php +++ b/components/Blueprints/Runtime.php @@ -231,8 +231,9 @@ public function evalPhpCodeInSubProcess( $process = $this->createPhpSubProcess( $code, $env, $input, $timeout ); $process->mustRun(); + $output = $process->getOutputStream(Process::OUTPUT_FILE)->consume_all(); return new EvalResult( - $process->getOutputStream(Process::OUTPUT_FILE)->consume_all(), + $output, $process ); } diff --git a/components/Blueprints/SiteResolver/NewSiteResolver.php b/components/Blueprints/SiteResolver/NewSiteResolver.php index dd4f87a0..17643ea4 100644 --- a/components/Blueprints/SiteResolver/NewSiteResolver.php +++ b/components/Blueprints/SiteResolver/NewSiteResolver.php @@ -9,6 +9,7 @@ use WordPress\Blueprints\Runtime; use WordPress\Blueprints\VersionStrings\VersionConstraint; use WordPress\HttpClient\Client; +use WordPress\HttpClient\Request; use WordPress\Zip\ZipFilesystem; use function WordPress\Filesystem\copy_between_filesystems; @@ -96,22 +97,7 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon // Technically, this is a "new site" resolver, but it's entirely possible // the developer-provided WordPress zip already has a sqlite database with the // a WordPress site installed.. - $installCheck = $runtime->evalPhpCodeInSubProcess( - <<<'PHP' -outputFileContent; - - if ( trim( $installCheck ) !== '1' ) { + if ( ! self::isWordPressInstalled( $runtime, $progress ) ) { if ( ! $targetFs->exists( '/wp-config.php' ) ) { if ( $targetFs->exists( 'wp-config-sample.php' ) ) { $targetFs->copy( 'wp-config-sample.php', 'wp-config.php' ); @@ -129,6 +115,8 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon $wp_cli_path, 'core', 'install', + // @TODO: DOCROOT is empty in Playground CLI + // '--path=' . getenv('DOCROOT'), // For Docker compatibility. If we got this far, Blueprint runner was already // allowed to run as root. '--allow-root', @@ -140,10 +128,38 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon '--skip-email', ] ); $process->mustRun(); + + if ( ! self::isWordPressInstalled( $runtime, $progress ) ) { + // @TODO: This breaks in Playground CLI + throw new BlueprintExecutionException( 'WordPress installation failed' ); + } } $progress->finish(); } + static private function isWordPressInstalled( Runtime $runtime, Tracker $progress ) { + $installCheck = $runtime->evalPhpCodeInSubProcess( + <<<'PHP' + getenv('DOCROOT'), + ], + null, + 5 + )->outputFileContent; + + return trim( $installCheck ) === '1'; + } + static private function resolveWordPressZipUrl( Client $client, string $version_string ): string { if ( $version_string === 'latest' ) { return 'https://wordpress.org/latest.zip'; @@ -160,13 +176,19 @@ static private function resolveWordPressZipUrl( Client $client, string $version_ return 'https://wordpress.org/nightly-builds/wordpress-latest.zip'; } - $latestVersions = $client->fetch( 'https://api.wordpress.org/core/version-check/1.7/?channel=beta' )->json(); + $latestVersions = $client->fetch( new Request( 'https://api.wordpress.org/core/version-check/1.7/?channel=beta' ) )->json(); $latestVersions = array_filter( $latestVersions['offers'], function ( $v ) { return $v['response'] === 'autoupdate'; } ); - + $latestNonBeta = null; + foreach ( $latestVersions as $apiVersion ) { + // Keep track of the first non-beta version (which is the latest) + if ( $latestNonBeta === null && strpos( $apiVersion['version'], 'beta' ) === false ) { + $latestNonBeta = $apiVersion; + } + if ( $version_string === 'beta' && strpos( $apiVersion['version'], 'beta' ) !== false ) { return $apiVersion['download']; } elseif ( @@ -189,6 +211,11 @@ static private function resolveWordPressZipUrl( Client $client, string $version_ return $apiVersion['download']; } } + + // If we're looking for beta but no beta was found, return latest + if ( $version_string === 'beta' && $latestNonBeta !== null ) { + return $latestNonBeta['download']; + } /** * If we didn't get a useful match in the API response, it could be version that's not diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index f5c82454..b3b40c27 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -230,6 +230,7 @@ function createProgressReporter(): ProgressReporter { return new TerminalProgressReporter(); } + $progressReporter = createProgressReporter(); // ----------------------------------------------------------------------------- diff --git a/components/ByteStream/ReadStream/BaseByteReadStream.php b/components/ByteStream/ReadStream/BaseByteReadStream.php index fa440802..7fac202a 100644 --- a/components/ByteStream/ReadStream/BaseByteReadStream.php +++ b/components/ByteStream/ReadStream/BaseByteReadStream.php @@ -7,7 +7,7 @@ abstract class BaseByteReadStream implements ByteReadStream { - const CHUNK_SIZE = 100;// 64 * 1024; + const CHUNK_SIZE = 64 * 1024; protected $buffer_size = 2048; From a86db8d61c85f995ad9633f8d4481261150a87c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 16 Jun 2025 19:43:23 +0200 Subject: [PATCH 06/17] Cosmetic changes to succeed in Playground and get more debug info on failure --- components/Blueprints/Runner.php | 8 ++++---- components/Blueprints/Steps/ImportContentStep.php | 7 +++++-- components/Blueprints/Steps/WPCLIStep.php | 1 + components/Filesystem/LocalFilesystem.php | 2 +- components/HttpClient/Client.php | 6 +++++- phar-blueprints.json | 3 +++ 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/components/Blueprints/Runner.php b/components/Blueprints/Runner.php index b57fe6fa..16a52bbf 100644 --- a/components/Blueprints/Runner.php +++ b/components/Blueprints/Runner.php @@ -640,9 +640,9 @@ private function createExecutionPlan(): array { // @TODO: Make sure this doesn't get included twice in the execution plan, // e.g. if the Blueprint specified this step manually. if ( $step instanceof ImportContentStep ) { - if($this->configuration->isRunningAsPhar()) { - throw new InvalidArgumentException( '@TODO: Importing content is not supported when running as phar.' ); - } else { + // if($this->configuration->isRunningAsPhar()) { + // throw new InvalidArgumentException( '@TODO: Importing content is not supported when running as phar.' ); + // } else { $libraries_phar_path = __DIR__ . '/../../dist/php-toolkit.phar'; if(!file_exists($libraries_phar_path)) { throw new InvalidArgumentException( @@ -655,7 +655,7 @@ private function createExecutionPlan(): array { 'filename' => 'php-toolkit.phar', 'content' => file_get_contents( $libraries_phar_path ) ] ) ); - } + // } array_unshift( $plan, $this->createStepObject( 'writeFiles', [ 'files' => [ 'php-toolkit.phar' => $source, diff --git a/components/Blueprints/Steps/ImportContentStep.php b/components/Blueprints/Steps/ImportContentStep.php index 40ff4432..4f4da363 100644 --- a/components/Blueprints/Steps/ImportContentStep.php +++ b/components/Blueprints/Steps/ImportContentStep.php @@ -68,7 +68,7 @@ private function importWxr( Runtime $runtime, array $content_definition, Tracker 'wxr', - 'execution_context_root' => getenv('EXECUTION_CONTEXT'), + 'execution_context_root' => getenv('EXECUTION_CONTEXT') ? getenv('EXECUTION_CONTEXT') : null, 'source' => json_decode(getenv('DATA_SOURCE_DEFINITION'), true), // @TODO: Support arbitrary media URLs to enable fetching assets during import. // 'media_url' => 'https://pd.w.org/' @@ -99,8 +99,11 @@ private function importWxr( Runtime $runtime, array $content_definition, Tracker break; case 'error': throw new BlueprintExecutionException( $data['message'] ); + case 'completion': + $progress->finish(); + break; default: - throw new BlueprintExecutionException( 'Unknown messagetype: ' . $data['type'] ); + throw new BlueprintExecutionException( 'Unknown message type: ' . $data['type'] ); } } diff --git a/components/Blueprints/Steps/WPCLIStep.php b/components/Blueprints/Steps/WPCLIStep.php index e53674bb..dd105ae9 100644 --- a/components/Blueprints/Steps/WPCLIStep.php +++ b/components/Blueprints/Steps/WPCLIStep.php @@ -43,6 +43,7 @@ public function run( Runtime $runtime, Tracker $tracker ) { // For Docker compatibility. If we got this far, the Blueprint runner was already // allowed to run as root. '--allow-root', + '--path=/wordpress', // . (getenv('DOCROOT') ?? '/wordpress'), substr($command, 3), ]); $process = $runtime->startShellCommand( $command ); diff --git a/components/Filesystem/LocalFilesystem.php b/components/Filesystem/LocalFilesystem.php index e7270524..58bb90d0 100644 --- a/components/Filesystem/LocalFilesystem.php +++ b/components/Filesystem/LocalFilesystem.php @@ -39,7 +39,7 @@ public static function create( $root = null ) { if ( ! is_dir( $root ) ) { if ( false === mkdir( $root, 0755, true ) ) { - throw new FilesystemException( sprintf( 'Root directory did not exist and could not be created: %s', $root ) ); + throw new FilesystemException( sprintf( 'Root directory did not exist and could not be created: %s', var_export($root, true) ) ); } } diff --git a/components/HttpClient/Client.php b/components/HttpClient/Client.php index 5ee7af08..ccdc6030 100644 --- a/components/HttpClient/Client.php +++ b/components/HttpClient/Client.php @@ -39,6 +39,7 @@ public function __construct( $options = array() ) { if(empty($options['transport']) || $options['transport'] === 'auto') { $options['transport'] = extension_loaded( 'curl' ) ? 'curl' : 'socket'; } + $options['transport'] = 'socket'; switch ( $options['transport'] ) { case 'curl': @@ -74,7 +75,10 @@ public function __construct( $options = array() ) { * * @return RequestReadStream */ - public function fetch( Request $request, array $options = [] ) { + public function fetch( $request, array $options = [] ) { + if(is_string($request)) { + $request = new Request($request); + } return new RequestReadStream( $request, array_merge( [ 'client' => $this ], diff --git a/phar-blueprints.json b/phar-blueprints.json index ba9fbd99..4a38e9ff 100644 --- a/phar-blueprints.json +++ b/phar-blueprints.json @@ -27,5 +27,8 @@ ], "directories": [ "vendor/composer" + ], + "files": [ + "dist/php-toolkit.phar" ] } From b186ee46d05af81309c7c6380ddbcb29d63c80c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 17 Jun 2025 13:43:56 +0200 Subject: [PATCH 07/17] RequestReadStream: Ignore content-length when the response stream is compressed --- .../ByteStream/RequestReadStream.php | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/components/HttpClient/ByteStream/RequestReadStream.php b/components/HttpClient/ByteStream/RequestReadStream.php index 9f3ee684..f0c691fe 100644 --- a/components/HttpClient/ByteStream/RequestReadStream.php +++ b/components/HttpClient/ByteStream/RequestReadStream.php @@ -118,13 +118,29 @@ private function pull_until_event( $options = array() ) { $content_length = $response->get_header( 'Content-Length' ); if ( null !== $content_length ) { /** - * Set the content-length based on the header, but make sure it stays null - * when the Content-Length header is not set. + * Best-effort attempt to guess the content-length of the response. * - * Important: Don't set the content-length to 0 if the header is missing! This - * would tell the streaming machinery there's no body to consume. + * Web servers often respond with a combination of Content-Length + * and Content-Encoding. For example, a 16kb text file may be compressed + * to 4kb with gzip and served with a Content-Encoding of `gzip` and a + * Content-Length of 4KB. + * + * If we just use that value, we'd truncate a 16KB body stream with at a + * Content-Length of 4KB. + * + * To correct that behavior, we're discarding the Content-Length header when + * it's used alongside a compressed response stream. */ - $this->remote_file_length = (int) $content_length; + if ( ! $response->get_header( 'Content-Encoding' ) ) { + /** + * Set the content-length based on the header, but make sure it stays null + * when the Content-Length header is not set. + * + * Important: Don't set the content-length to 0 if the header is missing! This + * would tell the streaming machinery there's no body to consume. + */ + $this->remote_file_length = (int) $content_length; + } } if ( $stop_at_event === Client::EVENT_GOT_HEADERS ) { return true; From ab87429a0b4936655b16c39ad889aec4ca16699b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 17 Jun 2025 13:44:23 +0200 Subject: [PATCH 08/17] Test importing the stylish press Blueprint --- .../Importer/StreamImporter.php | 6 +- .../Tests/StreamImporterTest.php | 83 +++++++++++++++- .../DataLiberation/Tests/WXRReaderTest.php | 96 +++++++++++++++++++ components/XML/XMLProcessor.php | 2 +- 4 files changed, 183 insertions(+), 4 deletions(-) diff --git a/components/DataLiberation/Importer/StreamImporter.php b/components/DataLiberation/Importer/StreamImporter.php index cbf86b62..f7a3cf98 100644 --- a/components/DataLiberation/Importer/StreamImporter.php +++ b/components/DataLiberation/Importer/StreamImporter.php @@ -321,7 +321,9 @@ protected function __construct( ) { $this->entity_reader_factory = $entity_reader_factory; $this->options = $options; - $this->set_source_site_url( $options['source_site_url'] ); + if ( ! empty( $options['source_site_url'] ) ) { + $this->set_source_site_url( $options['source_site_url'] ); + } if ( isset( $options['source_media_root_urls'] ) ) { foreach ( $options['source_media_root_urls'] as $source_media_root_url ) { @@ -714,7 +716,7 @@ protected function frontload_next_entity() { return true; } - protected function get_current_entity() { + public function get_current_entity() { $entity = $this->entity_iterator->current(); $entity = apply_filters( 'data_liberation.stream_importer.preprocess_entity', diff --git a/components/DataLiberation/Tests/StreamImporterTest.php b/components/DataLiberation/Tests/StreamImporterTest.php index aa93823e..12fd1b2e 100644 --- a/components/DataLiberation/Tests/StreamImporterTest.php +++ b/components/DataLiberation/Tests/StreamImporterTest.php @@ -1,7 +1,12 @@ markTestSkipped( 'Test only runs in Playground' ); + // $this->markTestSkipped( 'Test only runs in Playground' ); } } @@ -40,6 +45,82 @@ public function test_import_simple_wxr() { $this->assertTrue( $import ); } + /** + * + */ + public function test_stylish_press_local_file() { + $uploads_path = sys_get_temp_dir() . '/uploads'; + @mkdir( $uploads_path, 0777, true ); + + $sink = new class() { + public $imported_entities = []; + public $imported_attachments = []; + + public function import_entity( $entity ) { + $this->imported_entities[] = $entity; + return true; + } + public function import_attachment( $filepath, $post_id = null ) { + $this->imported_attachments[] = $filepath; + return true; + } + }; + + $importer = StreamImporter::create_for_wxr_file( __DIR__ . '/wxr/stylish-press.xml', [ + 'new_site_content_root_url' => 'http://127.0.0.1:9400', + 'new_media_root_url' => 'http://127.0.0.1:9400/wp-content/uploads', + 'uploads_path' => $uploads_path, + 'entity_sink' => $sink + ] ); + while ( $importer->next_step() || $importer->advance_to_next_stage() ) { + // noop + } + $this->assertCount( 10, $sink->imported_entities ); + } + + /** + * + */ + public function test_stylish_press_remote_stream() { + $uploads_path = sys_get_temp_dir() . '/uploads'; + @mkdir( $uploads_path, 0777, true ); + + $sink = new class() { + public $imported_entities = []; + public $imported_attachments = []; + + public function import_entity( $entity ) { + $this->imported_entities[] = $entity; + return true; + } + public function import_attachment( $filepath, $post_id = null ) { + $this->imported_attachments[] = $filepath; + return true; + } + }; + + $entity_reader_factory = function ( $cursor ) { + $stream = new RequestReadStream(new Request( + 'https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/stylish-press/site-content.wxr' + )); + return WXREntityReader::create( + $stream, + $cursor + ); + }; + + $importer = StreamImporter::create( $entity_reader_factory, [ + 'new_site_content_root_url' => 'http://127.0.0.1:9400', + 'new_media_root_url' => 'http://127.0.0.1:9400/wp-content/uploads', + 'uploads_path' => $uploads_path, + 'entity_sink' => $sink + ] ); + while ( $importer->next_step() || $importer->advance_to_next_stage() ) { + // noop + } + $this->assertCount( 10, $sink->imported_entities ); + } + public function test_frontloading() { $wxr_path = __DIR__ . '/wxr/frontloading-1-attachment.xml'; $importer = StreamImporter::create_for_wxr_file( $wxr_path ); diff --git a/components/DataLiberation/Tests/WXRReaderTest.php b/components/DataLiberation/Tests/WXRReaderTest.php index df3e982c..96544be8 100644 --- a/components/DataLiberation/Tests/WXRReaderTest.php +++ b/components/DataLiberation/Tests/WXRReaderTest.php @@ -3,6 +3,9 @@ use PHPUnit\Framework\TestCase; use WordPress\ByteStream\ReadStream\FileReadStream; use WordPress\DataLiberation\EntityReader\WXREntityReader; +use WordPress\DataLiberation\ImportEntity; +use WordPress\HttpClient\ByteStream\RequestReadStream; +use WordPress\HttpClient\Request; class WXRReaderTest extends TestCase { @@ -274,6 +277,99 @@ public function test_attachments() { ); } + public function test_stylish_press_wxr_local() { + $importer = WXREntityReader::create(); + $importer->append_bytes( file_get_contents( __DIR__ . '/wxr/stylish-press.xml' ) ); + $importer->input_finished(); + $this->assert_stylish_press_wxr( $importer ); + } + + public function test_stylish_press_wxr_remote() { + $importer = WXREntityReader::create(); + $importer->connect_upstream( + new RequestReadStream(new Request( + 'https://raw.githubusercontent.com/wordpress/blueprints/trunk/blueprints/stylish-press/site-content.wxr' + )) + ); + $this->assert_stylish_press_wxr( $importer ); + } + + private function assert_stylish_press_wxr( $importer ) { + $this->assertTrue( $importer->next_entity() ); + $this->assert_entity_equals( + new ImportEntity( 'site_option', array( + 'option_name' => 'blogname', + 'option_value' => 'Stylish Press', + ) ), + $importer->get_entity() + ); + + $this->assertTrue( $importer->next_entity() ); + $this->assert_entity_equals( + new ImportEntity( 'site_option', array( + 'option_name' => 'siteurl', + 'option_value' => 'http://www.stylishpress.wordpress.org' + ) ), + $importer->get_entity() + ); + + $this->assertTrue( $importer->next_entity() ); + $this->assert_entity_equals( + new ImportEntity( 'site_option', array( + 'option_name' => 'home', + 'option_value' => 'http://www.stylishpress.wordpress.org' + ) ), + $importer->get_entity() + ); + + $this->assertTrue( $importer->next_entity() ); + $this->assert_entity_equals( + new ImportEntity( 'user', array( + 'ID' => 1, + 'user_login' => 'admin', + 'user_email' => 'admin@stylishpress.wordpress.org', + 'display_name' => 'Admin', + 'first_name' => 'John', + 'last_name' => 'Doe', + ) ), + $importer->get_entity() + ); + + $this->assertTrue( $importer->next_entity() ); + $entity = $importer->get_entity(); + $this->assertEquals( 'post', $entity->get_type() ); + $this->assertEquals( 'page', $entity->get_data()['post_type'] ); + $this->assertEquals( 'home', $entity->get_data()['post_name'] ); + $this->assertEquals( 'publish', $entity->get_data()['post_status'] ); + $this->assertEquals( '1', $entity->get_data()['post_id'] ); + $this->assertEquals( 'Homepage', $entity->get_data()['post_title'] ); + + $this->assertTrue( $importer->next_entity() ); + $this->assert_entity_equals( + new ImportEntity( 'post_meta', array( + 'meta_key' => '_edit_last', + 'meta_value' => '1', + 'post_id' => 1 + ) ), + $importer->get_entity() + ); + + $this->assertTrue( $importer->next_entity() ); + + $entity = $importer->get_entity(); + $this->assertEquals( 'post', $entity->get_type() ); + $this->assertEquals( 'page', $entity->get_data()['post_type'] ); + $this->assertEquals( 'the-stylish-story', $entity->get_data()['post_name'] ); + $this->assertEquals( 'publish', $entity->get_data()['post_status'] ); + $this->assertEquals( '2', $entity->get_data()['post_id'] ); + $this->assertEquals( 'The Stylish Story', $entity->get_data()['post_title'] ); + } + + private function assert_entity_equals( $expected, $actual ) { + $this->assertEquals( $expected->get_type(), $actual->get_type() ); + $this->assertEquals( $expected->get_data(), $actual->get_data() ); + } + public function test_terms() { $importer = WXREntityReader::create(); $importer->append_bytes( diff --git a/components/XML/XMLProcessor.php b/components/XML/XMLProcessor.php index 3f70f707..c3cf1f16 100644 --- a/components/XML/XMLProcessor.php +++ b/components/XML/XMLProcessor.php @@ -878,7 +878,7 @@ protected function __construct( $xml, $document_namespaces=[], $use_the_static_c '6.4.0' ); } - $this->xml = $xml; + $this->xml = $xml ?? ''; $this->document_namespaces = array_merge( $document_namespaces, // These initial namespaces cannot be overridden. From 098ec78143b47ceae292243bdb748a9fee78b70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 17 Jun 2025 13:44:33 +0200 Subject: [PATCH 09/17] Report import errors to stderr --- components/Blueprints/Steps/scripts/import-content.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/Blueprints/Steps/scripts/import-content.php b/components/Blueprints/Steps/scripts/import-content.php index ae6383bc..d08092ce 100644 --- a/components/Blueprints/Steps/scripts/import-content.php +++ b/components/Blueprints/Steps/scripts/import-content.php @@ -10,6 +10,7 @@ use WordPress\DataLiberation\EntityReader\EPubEntityReader; use WordPress\DataLiberation\EntityReader\FilesystemEntityReader; use WordPress\DataLiberation\EntityReader\WXREntityReader; +use WordPress\DataLiberation\Importer\EntityImporter; use WordPress\DataLiberation\Importer\ImportSession; use WordPress\DataLiberation\Importer\ImportUtils; use WordPress\DataLiberation\Importer\RetryFrontloadingIterator; @@ -569,6 +570,7 @@ function ( $processor ) { $importer = StreamImporter::create( $entity_reader_factory, array( + 'entity_sink' => new EntityImporter(), 'source_site_url' => $source_site_url, 'new_site_content_root_url' => NEW_SITE_CONTENT_ROOT, 'source_media_root_urls' => $options['media_url'] ?? array( $source_site_url ), From 17deb91aa3c56553f1ad4b006d918ac6f5db56b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 17 Jun 2025 13:44:52 +0200 Subject: [PATCH 10/17] Ignore site URL in EntityImporter --- .../Importer/EntityImporter.php | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/components/DataLiberation/Importer/EntityImporter.php b/components/DataLiberation/Importer/EntityImporter.php index 21d94b2d..879179e9 100644 --- a/components/DataLiberation/Importer/EntityImporter.php +++ b/components/DataLiberation/Importer/EntityImporter.php @@ -144,6 +144,20 @@ public function import_entity( ImportEntity $entity ) { } public function import_site_option( $data ) { + /** + * Ignore site URL options. There doesn't seem to be a compelling use-case for + * overwriting the site URL during a content import. For example, WXR files may + * specify a site URL (typically of the source site) and it is emitted as a + * siteurl option. Is that a good reason to change the target site URL, trigger + * a redirect, and very likely break the entire site? No. + * + * We may need to revisit this approach if this class is ever used to import + * from data sources different than static content files, e.g. a database dump. + */ + if($data['option_name'] === 'siteurl' || $data['option_name'] === 'home') { + return; + } + $this->logger->info( sprintf( /* translators: %s: option name */ @@ -603,7 +617,7 @@ public function import_post( $data ) { * @param array $comments Raw comment data, already processed by {@see process_comments}. * @param array $terms Raw term data, already processed. */ - do_action( 'wxr_importer_process_failed_post', $post_id, $data, $meta, $comments, $terms ); + do_action( 'wxr_importer_process_failed_post', $post_id, $data, $meta ); return false; } @@ -737,7 +751,7 @@ protected function process_menu_item_meta( $post_id, $data, $meta ) { default: // associated object is missing or not imported yet, we'll retry later - $this->missing_menu_items[] = $item; + // $this->missing_menu_items[] = $item; $this->logger->debug( 'Unknown menu item type' ); break; } @@ -1203,7 +1217,7 @@ class Logger { * @param string $message Message to log */ public function debug( $message ) { - // echo( '[DEBUG] ' . $message ); + echo( '[DEBUG] ' . $message . "\n" ); } /** @@ -1212,7 +1226,7 @@ public function debug( $message ) { * @param string $message Message to log */ public function info( $message ) { - // echo( '[INFO] ' . $message ); + echo( '[INFO] ' . $message . "\n" ); } /** @@ -1221,7 +1235,7 @@ public function info( $message ) { * @param string $message Message to log */ public function warning( $message ) { - echo( '[WARNING] ' . $message ); + echo( '[WARNING] ' . $message . "\n" ); } /** @@ -1230,7 +1244,7 @@ public function warning( $message ) { * @param string $message Message to log */ public function error( $message ) { - echo( '[ERROR] ' . $message ); + echo( '[ERROR] ' . $message . "\n" ); } /** @@ -1239,6 +1253,6 @@ public function error( $message ) { * @param string $message Message to log */ public function notice( $message ) { - // echo( '[NOTICE] ' . $message ); + echo( '[NOTICE] ' . $message . "\n" ); } } From 9b72f07dc7772396912106b605754aa77f41b6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 17 Jun 2025 13:45:17 +0200 Subject: [PATCH 11/17] Cosmetic changes to improve subprocess spawning and error reporting --- components/Blueprints/Runtime.php | 2 +- .../SiteResolver/NewSiteResolver.php | 4 +-- .../Blueprints/Steps/InstallPluginStep.php | 35 ++++++++----------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/components/Blueprints/Runtime.php b/components/Blueprints/Runtime.php index cbf49ae6..cdfa1419 100644 --- a/components/Blueprints/Runtime.php +++ b/components/Blueprints/Runtime.php @@ -263,7 +263,7 @@ public function createPhpSubProcess( $phpBinary = null; if ( getenv('PHP_BINARY') ) { $phpBinary = getenv('PHP_BINARY'); - } elseif ( PHP_SAPI === 'cli' && isset($_SERVER['argv'][0]) ) { + } elseif ( PHP_BINARY ) { $phpBinary = PHP_BINARY; } else { $phpBinary = 'php'; diff --git a/components/Blueprints/SiteResolver/NewSiteResolver.php b/components/Blueprints/SiteResolver/NewSiteResolver.php index 17643ea4..2021ad6a 100644 --- a/components/Blueprints/SiteResolver/NewSiteResolver.php +++ b/components/Blueprints/SiteResolver/NewSiteResolver.php @@ -115,8 +115,8 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon $wp_cli_path, 'core', 'install', - // @TODO: DOCROOT is empty in Playground CLI - // '--path=' . getenv('DOCROOT'), + '--path=' . getenv('DOCROOT'), + // For Docker compatibility. If we got this far, Blueprint runner was already // allowed to run as root. '--allow-root', diff --git a/components/Blueprints/Steps/InstallPluginStep.php b/components/Blueprints/Steps/InstallPluginStep.php index dc0c2d80..b76a5c2e 100644 --- a/components/Blueprints/Steps/InstallPluginStep.php +++ b/components/Blueprints/Steps/InstallPluginStep.php @@ -154,7 +154,7 @@ public function error( $errors ) { public function feedback( $string, ...$args ) { // For debugging - error_log( sprintf( $string, ...$args ) ); + fwrite( STDERR, sprintf( $string, ...$args ) . "\n" ); } public function header() { @@ -191,41 +191,34 @@ public function after( $title = '' ) { if ( ! empty( $admins ) ) { wp_set_current_user( $admins[0]->ID ); } else { - error_log( "No admin user found to perform plugin installation." ); + fwrite( STDERR, "No admin user found to perform plugin installation." . "\n" ); exit( 1 ); } $plugin_zip_path = getenv( 'PLUGIN_ZIP_PATH' ); if ( ! $plugin_zip_path ) { - error_log( "PLUGIN_ZIP_PATH environment variable not set." ); + fwrite( STDERR, "PLUGIN_ZIP_PATH environment variable not set." . "\n" ); exit( 1 ); } if ( ! file_exists( $plugin_zip_path ) ) { - error_log( "Plugin zip file not found at " . $plugin_zip_path ); + fwrite( STDERR, "Plugin zip file not found at " . $plugin_zip_path . "\n" ); exit( 1 ); } // List files from the plugin zip $zip = new ZipArchive(); if ( $zip->open( $plugin_zip_path ) !== true ) { - error_log( "Failed to open plugin zip file: " . $plugin_zip_path ); + fwrite( STDERR, "Failed to open plugin zip file: " . $plugin_zip_path . "\n" ); exit( 1 ); } -error_log( "Plugin zip contents:" ); +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 ) === '/'; - - error_log( sprintf( - "%s%s (%s bytes)", - $filename, - $is_dir ? " [directory]" : "", - $size - ) ); } // Extract plugin slug from the zip file @@ -244,7 +237,7 @@ public function after( $title = '' ) { // Make sure the destination directory is writable $wp_plugin_dir = WP_PLUGIN_DIR; if ( ! is_writable( $wp_plugin_dir ) ) { - error_log( "Plugin directory is not writable: " . $wp_plugin_dir ); + fwrite( STDERR, "Plugin directory is not writable: " . $wp_plugin_dir . "\n" ); // Try to fix permissions @chmod( $wp_plugin_dir, 0755 ); if ( ! is_writable( $wp_plugin_dir ) ) { @@ -269,7 +262,7 @@ public function after( $title = '' ) { // Create the directory $GLOBALS['wp_filesystem']->mkdir( $target_directory ); - error_log( "Created target directory: " . $target_directory ); + fwrite( STDERR, "Created target directory: " . $target_directory . "\n" ); } // Install the plugin @@ -281,22 +274,22 @@ public function after( $title = '' ) { // Check for filesystem errors if ( $GLOBALS['wp_filesystem']->errors->has_errors() ) { foreach ( $GLOBALS['wp_filesystem']->errors->get_error_messages() as $message ) { - error_log( "Filesystem error: " . $message ); + fwrite( STDERR, "Filesystem error: " . $message . "\n" ); } exit( 1 ); } if ( is_wp_error( $result ) ) { - error_log( "Failed to install plugin (1): " . $result->get_error_message() ); + fwrite( STDERR, "Failed to install plugin (1): " . $result->get_error_message() . "\n" ); exit( 1 ); } if ( $result === false || $result === null ) { // Check skin for errors if $result is not specific. if ( isset( $skin->result ) && is_wp_error( $skin->result ) ) { - error_log( "Failed to install plugin (2): " . $skin->result->get_error_message() ); + fwrite( STDERR, "Failed to install plugin (2): " . $skin->result->get_error_message() . "\n" ); } else { - error_log( "Failed to install plugin for an unknown reason." ); + fwrite( STDERR, "Failed to install plugin for an unknown reason." . "\n" ); } exit( 1 ); } @@ -304,14 +297,14 @@ public function after( $title = '' ) { // Installation successful, find the main plugin file. $plugin_folder_name = ! empty( $plugin_slug ) ? $plugin_slug : ( $upgrader->result['destination_name'] ?? null ); if ( ! $plugin_folder_name ) { - error_log( "Could not determine plugin folder name after installation." ); + fwrite( STDERR, "Could not determine plugin folder name after installation." . "\n" ); exit( 1 ); } // Get all plugins within the newly installed folder. $plugins_in_folder = get_plugins( '/' . $plugin_folder_name ); if ( empty( $plugins_in_folder ) ) { - error_log( "Could not find any plugin files in the installed folder: " . $plugin_folder_name ); + fwrite( STDERR, "Could not find any plugin files in the installed folder: " . $plugin_folder_name . "\n" ); exit( 1 ); } // The key of the first plugin entry is the relative path needed for activation. From b10661c581069ba346c9f434671a0747255f8408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 18 Jun 2025 12:36:48 +0200 Subject: [PATCH 12/17] More adjustments for Playground CLI, e.g. blueprint.resolve filter --- .../DataReference/DataReferenceResolver.php | 2 +- components/Blueprints/Runner.php | 27 +- .../VersionStrings/WordPressVersion.php | 2 +- components/Blueprints/bin/blueprint.php | 11 +- .../Tests/wxr/stylish-press.xml | 279 ++++++++++++++++++ 5 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 components/DataLiberation/Tests/wxr/stylish-press.xml diff --git a/components/Blueprints/DataReference/DataReferenceResolver.php b/components/Blueprints/DataReference/DataReferenceResolver.php index 2976b63c..7b45b79a 100644 --- a/components/Blueprints/DataReference/DataReferenceResolver.php +++ b/components/Blueprints/DataReference/DataReferenceResolver.php @@ -52,7 +52,7 @@ public function __construct( Client $client, ?string $tmpRoot = null ) { $this->tmpRoot = $tmpRoot ?: wp_unix_sys_get_temp_dir(); } - public function setExecutionContext( Filesystem $executionContext ) { + public function setExecutionContext( ?Filesystem $executionContext ) { $this->executionContext = $executionContext; } diff --git a/components/Blueprints/Runner.php b/components/Blueprints/Runner.php index 16a52bbf..e4b57bec 100644 --- a/components/Blueprints/Runner.php +++ b/components/Blueprints/Runner.php @@ -300,7 +300,14 @@ private function loadBlueprint() { $blueprintString = $resolved->getStream()->consume_all(); $this->blueprintExecutionContext = LocalFilesystem::create( dirname( $reference->get_path() ) ); } else { + // For the purposes of Blueprint resolution, the execution context is the + // current working directory. This way, a path such as ./blueprint.json + // will mean "a blueprint.json file in the current working directory" and not + // "a ./blueprint.json path without a point of reference". + $this->assets->setExecutionContext( LocalFilesystem::create( getcwd() ) ); $resolved = $this->assets->resolve( $reference ); + $this->assets->setExecutionContext( null ); + if ( $resolved instanceof File ) { $stream = $resolved->getStream(); @@ -388,6 +395,8 @@ private function validateBlueprint(): void { $this->configuration->getLogger()->debug( 'Final resolved Blueprint: ' . json_encode( $this->blueprintArray, JSON_PRETTY_PRINT ) ); + $this->blueprintArray = apply_filters( 'blueprint.resolved', $this->blueprintArray ); + // Assert the Blueprint conforms to the latest JSON schema. $v = new HumanFriendlySchemaValidator( json_decode( file_get_contents( __DIR__ . '/Versions/Version2/json-schema/schema-v2.json' ), true ) @@ -450,7 +459,7 @@ private function validateBlueprint(): void { // WordPress Version Constraint if ( isset( $this->blueprintArray['wordpressVersion'] ) ) { $wp_version = $this->blueprintArray['wordpressVersion']; - $recommended = null; + $min = $max = $recommended = null; if ( is_string( $wp_version ) ) { $this->recommendedWpVersion = $wp_version; $recommended = WordPressVersion::fromString( $wp_version ); @@ -500,6 +509,14 @@ private function validateBlueprint(): void { // correctly. The actual version check for WordPress is done in // NewSiteResolver and ExistingSiteResolver. } + + // Validate the override constraint if it was set + if ( $this->wpVersionConstraint ) { + $wpConstraintErrors = $this->wpVersionConstraint->validate(); + if ( ! empty( $wpConstraintErrors ) ) { + throw new BlueprintExecutionException( 'Invalid WordPress version constraint from CLI override: ' . implode( '; ', $wpConstraintErrors ) ); + } + } } private function createExecutionPlan(): array { @@ -934,14 +951,6 @@ static function () { return new WriteFilesStep( $files ); - case 'runPHP': - return new RunPHPStep( - $this->createDataReference( [ - 'filename' => 'run-php.php', - 'content' => $data['code'], - ] ), - $data['env'] ?? [] - ); case 'unzip': $zipFile = $this->createDataReference( $data['zipFile'], [ ExecutionContextPath::class ] ); diff --git a/components/Blueprints/VersionStrings/WordPressVersion.php b/components/Blueprints/VersionStrings/WordPressVersion.php index 35b3e04e..3e1c2032 100644 --- a/components/Blueprints/VersionStrings/WordPressVersion.php +++ b/components/Blueprints/VersionStrings/WordPressVersion.php @@ -46,7 +46,7 @@ class WordPressVersion implements Version { * @return $this|false|null */ static public function fromString( string $raw ) { - if(in_array($raw, ['beta','trunk'], true)) { + if(in_array($raw, ['beta','trunk','latest'], true)) { return null; } if(substr($raw, 0, 8) === 'https://') { diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index b3b40c27..5efe6e34 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -36,6 +36,7 @@ use WordPress\CLI\CLI; use WordPress\Blueprints\DataReference\AbsoluteLocalPath; use WordPress\Blueprints\DataReference\DataReference; +use WordPress\Blueprints\DataReference\ExecutionContextPath; use WordPress\Blueprints\Exception\BlueprintExecutionException; use WordPress\Blueprints\Exception\PermissionsException; use WordPress\Blueprints\Logger\CLILogger; @@ -255,7 +256,7 @@ function createProgressReporter(): ProgressReporter { 'site-url' => [ 'u', true, null, 'Public site URL (https://example.com)' ], 'site-path' => [ null, true, null, 'Target directory with WordPress install context)' ], 'execution-context' => [ 'x', true, null, 'Source directory with Blueprint context files' ], - 'mode' => [ 'm', true, 'create-new-site', 'Execution mode (create|apply)' ], + 'mode' => [ 'm', true, 'create-new-site', 'Execution mode (create-new-site|apply-to-existing-site)' ], 'db-engine' => [ 'd', true, 'mysql', 'Database engine (mysql|sqlite)' ], 'db-host' => [ null, true, '127.0.0.1', 'MySQL host' ], 'db-user' => [ null, true, 'root', 'MySQL user' ], @@ -279,7 +280,7 @@ function createProgressReporter(): ProgressReporter { ] ), 'examples' => [ 'php blueprint.php exec my-blueprint.json --site-url https://mysite.test --site-path /var/www/mysite.com', - 'php blueprint.php exec my-blueprint.json --execution-context /var/www --site-url https://mysite.test --mode apply --site-path ./site', + 'php blueprint.php exec my-blueprint.json --execution-context /var/www --site-url https://mysite.test --mode apply-to-existing-site --site-path ./site', 'php blueprint.php exec my-blueprint.json --site-url https://mysite.test --site-path ./mysite --truncate-new-site-directory', ], 'aliases' => [ 'run' ], @@ -397,9 +398,10 @@ function cliArgsToRunnerConfiguration( array $positionalArgs, array $options ): $blueprint_reference = $positionalArgs[0]; $config->setBlueprint( DataReference::create( $blueprint_reference, [ AbsoluteLocalPath::class, + ExecutionContextPath::class, ] ) ); } catch ( InvalidArgumentException $e ) { - throw new InvalidArgumentException( "Invalid Blueprint reference: " . $positionalArgs[0] ); + throw new InvalidArgumentException( sprintf( "Invalid Blueprint reference: %s. Hint: paths must start with ./ or /. URLs must start with http:// or https://.", $positionalArgs[0] ) ); } if ( ! empty( $options['mode'] ) ) { @@ -409,6 +411,9 @@ function cliArgsToRunnerConfiguration( array $positionalArgs, array $options ): $config->setExecutionMode( 'create-new-site' ); } elseif ( $mode === 'apply-to-existing-site' ) { $config->setExecutionMode( 'apply-to-existing-site' ); + if(!empty($options['wp'])) { + throw new InvalidArgumentException( "The --wp option cannot be used with --mode=apply-to-existing-site. The WordPress version is whatever the existing site has." ); + } } else { throw new InvalidArgumentException( "Invalid execution mode: {$mode}. Supported modes are: create-new-site, apply-to-existing-site" ); } diff --git a/components/DataLiberation/Tests/wxr/stylish-press.xml b/components/DataLiberation/Tests/wxr/stylish-press.xml new file mode 100644 index 00000000..f1358247 --- /dev/null +++ b/components/DataLiberation/Tests/wxr/stylish-press.xml @@ -0,0 +1,279 @@ + + + + Stylish Press + http://www.stylishpress.wordpress.org + Fashion-forward apparel for everyone! + Tue, 05 Jun 2024 12:00:00 +0000 + en-US + 1.2 + http://www.stylishpress.wordpress.org + http://www.stylishpress.wordpress.org + + + 1 + admin + admin@stylishpress.wordpress.org + + + + + + + + Homepage + http://www.stylishpress.wordpress.org/home + Tue, 05 Jun 2024 12:00:00 +0000 + + + http://www.stylishpress.wordpress.org/?p=1 + + +
+ +
+ +

Welcome to Stylish Press

+ + +

Discover the latest in fashion and style.

+ +
+
+ + + +
+ +
+ +
Trendy Outfit
+ +
+ + +
+ +

At Stylish Press, we're dedicated to bringing you the latest trends in fashion. Our collection is carefully curated to ensure you look your best, whether you're hitting the streets or the runway. Explore our site to find out more about our latest collections, fashion tips, and exclusive offers.

+ + +

Browse through our diverse range of apparel, featuring everything from chic tops to stylish bottoms, and accessories that complete your look. Our pieces are designed to make you feel confident and fashionable, day or night.

+ +
+ +
+ + + + + + + +

Our Mission

+ + +

Stylish Press was founded with a firm belief that fashion should be accessible, inclusive, and always evolving. We aim to empower our customers to express themselves through bold and contemporary designs that speak volumes about their unique style.

+ + ]]>
+ + 1 + 2024-06-05 12:00:00 + 2024-06-05 12:00:00 + closed + closed + home + publish + page + 0 + 1 + + _edit_last + 1 + +
+ + + + The Stylish Story + http://www.stylishpress.wordpress.org/the-stylish-story + Tue, 05 Jun 2024 12:00:00 +0000 + + + http://www.stylishpress.wordpress.org/?p=2 + + +
+ +
+ +

The Stylish Story

+ +
+
+ + + +

Our Beginnings

+ + +

Stylish Press was founded in 2020 by a group of fashion enthusiasts who wanted to bring a fresh perspective to the fashion industry. We began as a small online store, focusing on minimalist designs with a bold twist. Our unique approach quickly gained a loyal following, and we have been expanding our collection ever since.

+ + + + + + + + + + + +

Our Commitment

+ + +

We believe in sustainable fashion practices and ethical sourcing. Every piece in our collection is made with care, ensuring minimal impact on the environment. Our commitment to quality craftsmanship means that each item is built to last, keeping you stylish and sustainable.

+ + + +
+

"Fashion is not just about looking good, it's about feeling good and doing good." - Founders of Stylish Press

+
+ + + +

Join us on our journey to revolutionize the fashion industry, one stylish piece at a time. Together, we can make fashion more inclusive, innovative, and inspiring.

+ + ]]>
+ + 2 + 2024-06-05 12:00:00 + 2024-06-05 12:00:00 + closed + closed + the-stylish-story + publish + page + 0 + 2 + + _edit_last + 1 + +
+ + + + Connect + http://www.stylishpress.wordpress.org/contact-us + Tue, 05 Jun 2024 12:00:00 +0000 + + + http://www.stylishpress.wordpress.org/?p=5 + + +
+ +
+ +

Connect with Us

+ +
+
+ + + +

We love hearing from our customers! Whether you have questions about our products, need styling advice, or just want to share your own fashion journey, we're here for you. Reach out to us through our various contact points listed below, and let’s stay connected.

+ + + +
+ +
+ +
+ + +
+ +

Find Us Here

+ + +

Email: support@stylishpress.wordpress.org

+ + +

Phone: +1 (234) 567-890

+ + +

Address: 123 Fashion Street, Trend City, ST 56789

+ + +

Follow Us

+ + + + +
+ +
+ + + +
+

"Fashion is instant language." - Miuccia Prada

+
+ + + +

Connect with Stylish Press today and become a part of our vibrant community where fashion meets passion.

+ + ]]>
+ + 5 + 2024-06-05 12:00:00 + 2024-06-05 12:00:00 + closed + closed + contact-us + publish + page + 0 + 5 + + _edit_last + 1 + +
+ +
+
\ No newline at end of file From 413901368193c7c141942fb1ed5e0575d31d2a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 18 Jun 2025 13:58:09 +0200 Subject: [PATCH 13/17] Support customizing http client via filters --- components/Blueprints/Runner.php | 2 +- components/HttpClient/Client.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/components/Blueprints/Runner.php b/components/Blueprints/Runner.php index e4b57bec..3b9b6fe3 100644 --- a/components/Blueprints/Runner.php +++ b/components/Blueprints/Runner.php @@ -114,7 +114,7 @@ public function __construct( RunnerConfiguration $configuration ) { $this->configuration = $configuration; $this->validateConfiguration( $configuration ); - $this->client = new Client(); + $this->client = apply_filters('blueprint.http_client', new Client()); $this->mainTracker = new Tracker(); // Set up progress logging diff --git a/components/HttpClient/Client.php b/components/HttpClient/Client.php index ccdc6030..4b8d2735 100644 --- a/components/HttpClient/Client.php +++ b/components/HttpClient/Client.php @@ -37,15 +37,14 @@ class Client { public function __construct( $options = array() ) { $this->state = new ClientState( $options ); if(empty($options['transport']) || $options['transport'] === 'auto') { - $options['transport'] = extension_loaded( 'curl' ) ? 'curl' : 'socket'; + $options['transport'] = extension_loaded( 'curl' ) ? 'curl' : 'sockets'; } - $options['transport'] = 'socket'; switch ( $options['transport'] ) { case 'curl': $transport = new CurlTransport( $this->state ); break; - case 'socket': + case 'sockets': $transport = new SocketTransport( $this->state ); break; default: From 27d34f2c38b6bff90db5c8bad190b251eddb16c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 21 Jun 2025 22:48:32 +0200 Subject: [PATCH 14/17] HERDOC PHP 7.2 compat --- components/Blueprints/Runner.php | 18 ++++++++++----- components/Blueprints/RunnerConfiguration.php | 2 +- .../SiteResolver/NewSiteResolver.php | 9 ++++++-- components/Blueprints/Steps/WPCLIStep.php | 2 +- components/Blueprints/bin/blueprint.php | 23 +++++++++---------- 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/components/Blueprints/Runner.php b/components/Blueprints/Runner.php index 3b9b6fe3..fead0a1a 100644 --- a/components/Blueprints/Runner.php +++ b/components/Blueprints/Runner.php @@ -52,7 +52,6 @@ use WordPress\Filesystem\LocalFilesystem; use WordPress\HttpClient\ByteStream\RequestReadStream; use WordPress\HttpClient\Client; -use WordPress\HttpClient\Request; use WordPress\Zip\ZipFilesystem; use function WordPress\Encoding\utf8_is_valid_byte_stream; @@ -60,6 +59,13 @@ use function WordPress\Zip\is_zip_file_stream; class Runner { + const EXECUTION_MODE_CREATE_NEW_SITE = 'create-new-site'; + const EXECUTION_MODE_APPLY_TO_EXISTING_SITE = 'apply-to-existing-site'; + const EXECUTION_MODES = [ + self::EXECUTION_MODE_CREATE_NEW_SITE, + self::EXECUTION_MODE_APPLY_TO_EXISTING_SITE, + ]; + /** * @var RunnerConfiguration */ @@ -135,17 +141,17 @@ private function validateConfiguration( RunnerConfiguration $config ): void { // Validate execution mode $mode = $config->getExecutionMode(); - if ( ! in_array( $mode, [ 'create-new-site', 'apply-to-existing-site' ], true ) ) { - throw new BlueprintExecutionException( "Execution mode must be either 'create-new-site' or 'apply-to-existing-site'." ); + if ( ! in_array( $mode, self::EXECUTION_MODES, true ) ) { + throw new BlueprintExecutionException( "Execution mode must be one of: " . implode( ', ', self::EXECUTION_MODES ) ); } // Validate site URL // Note: $options is not defined in this context, so we skip this block. // If you want to validate the site URL, you should use $config->getTargetSiteUrl(). $siteUrl = $config->getTargetSiteUrl(); - if ( $mode === 'create-new-site' ) { + if ( $mode === self::EXECUTION_MODE_CREATE_NEW_SITE ) { if ( empty( $siteUrl ) ) { - throw new BlueprintExecutionException( "Site URL is required when the execution mode is 'create-new-site'." ); + throw new BlueprintExecutionException( sprintf( "Site URL is required when the execution mode is '%s'.", self::EXECUTION_MODE_CREATE_NEW_SITE ) ); } } if ( ! empty( $siteUrl ) && ! filter_var( $siteUrl, FILTER_VALIDATE_URL ) ) { @@ -253,7 +259,7 @@ public function run(): void { ], $progress['wpCli'] ); $progress['targetResolution']->setCaption( 'Resolving target site' ); - if ( $this->configuration->getExecutionMode() === 'apply-to-existing-site' ) { + if ( $this->configuration->getExecutionMode() === self::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ) { ExistingSiteResolver::resolve( $this->runtime, $progress['targetResolution'], $this->wpVersionConstraint ); } else { NewSiteResolver::resolve( $this->runtime, $progress['targetResolution'], $this->wpVersionConstraint, $this->recommendedWpVersion ); diff --git a/components/Blueprints/RunnerConfiguration.php b/components/Blueprints/RunnerConfiguration.php index 5f532501..f02a04ab 100644 --- a/components/Blueprints/RunnerConfiguration.php +++ b/components/Blueprints/RunnerConfiguration.php @@ -23,7 +23,7 @@ class RunnerConfiguration { /** * @var string */ - private $mode = 'create-new-site'; // or apply-to-existing-site + private $mode = Runner::EXECUTION_MODE_CREATE_NEW_SITE; /** * @var string */ diff --git a/components/Blueprints/SiteResolver/NewSiteResolver.php b/components/Blueprints/SiteResolver/NewSiteResolver.php index 2021ad6a..b33a981d 100644 --- a/components/Blueprints/SiteResolver/NewSiteResolver.php +++ b/components/Blueprints/SiteResolver/NewSiteResolver.php @@ -67,6 +67,10 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon // If SQLite integration zip provided, unzip into appropriate folder if ( $runtime->getConfiguration()->getDatabaseEngine() === 'sqlite' ) { + /* + * @TODO: Ensure DB_NAME gets defined in wp-config.php before installing the SQLite plugin. + */ + $progress['resolve_assets']->setCaption( 'Downloading SQLite integration plugin' ); $resolved = $runtime->resolve( $assets['sqlite-integration'] ); if ( ! $resolved instanceof File ) { @@ -115,7 +119,7 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon $wp_cli_path, 'core', 'install', - '--path=' . getenv('DOCROOT'), + '--path=' . $runtime->getConfiguration()->getTargetSiteRoot(), // For Docker compatibility. If we got this far, Blueprint runner was already // allowed to run as root. @@ -149,7 +153,8 @@ static private function isWordPressInstalled( Runtime $runtime, Tracker $progres require $wp_load; append_output( function_exists('is_blog_installed') && is_blog_installed() ? '1' : '0' ); -PHP, +PHP + , [ 'DOCROOT' => getenv('DOCROOT'), ], diff --git a/components/Blueprints/Steps/WPCLIStep.php b/components/Blueprints/Steps/WPCLIStep.php index dd105ae9..bfed9802 100644 --- a/components/Blueprints/Steps/WPCLIStep.php +++ b/components/Blueprints/Steps/WPCLIStep.php @@ -43,7 +43,7 @@ public function run( Runtime $runtime, Tracker $tracker ) { // For Docker compatibility. If we got this far, the Blueprint runner was already // allowed to run as root. '--allow-root', - '--path=/wordpress', // . (getenv('DOCROOT') ?? '/wordpress'), + '--path=' . $runtime->getConfiguration()->getTargetSiteRoot(), substr($command, 3), ]); $process = $runtime->startShellCommand( $command ); diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 5efe6e34..7f156f53 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -256,7 +256,7 @@ function createProgressReporter(): ProgressReporter { 'site-url' => [ 'u', true, null, 'Public site URL (https://example.com)' ], 'site-path' => [ null, true, null, 'Target directory with WordPress install context)' ], 'execution-context' => [ 'x', true, null, 'Source directory with Blueprint context files' ], - 'mode' => [ 'm', true, 'create-new-site', 'Execution mode (create-new-site|apply-to-existing-site)' ], + 'mode' => [ 'm', true, Runner::EXECUTION_MODE_CREATE_NEW_SITE, sprintf( 'Execution mode (%s|%s)', Runner::EXECUTION_MODE_CREATE_NEW_SITE, Runner::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ) ], 'db-engine' => [ 'd', true, 'mysql', 'Database engine (mysql|sqlite)' ], 'db-host' => [ null, true, '127.0.0.1', 'MySQL host' ], 'db-user' => [ null, true, 'root', 'MySQL user' ], @@ -280,7 +280,7 @@ function createProgressReporter(): ProgressReporter { ] ), 'examples' => [ 'php blueprint.php exec my-blueprint.json --site-url https://mysite.test --site-path /var/www/mysite.com', - 'php blueprint.php exec my-blueprint.json --execution-context /var/www --site-url https://mysite.test --mode apply-to-existing-site --site-path ./site', + sprintf( 'php blueprint.php exec my-blueprint.json --execution-context /var/www --site-url https://mysite.test --mode %s --site-path ./site', Runner::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ), 'php blueprint.php exec my-blueprint.json --site-url https://mysite.test --site-path ./mysite --truncate-new-site-directory', ], 'aliases' => [ 'run' ], @@ -350,7 +350,7 @@ function handleExecCommand( array $positionalArgs, array $options, array $comman $runner = new Runner( $config ); // Execute the Blueprint - if ( $config->getExecutionMode() === 'create-new-site' ) { + if ( $config->getExecutionMode() === Runner::EXECUTION_MODE_CREATE_NEW_SITE ) { $progressReporter->reportProgress(0, 'Creating a new site'); } else { $progressReporter->reportProgress(0, 'Updating an existing site'); @@ -405,24 +405,23 @@ function cliArgsToRunnerConfiguration( array $positionalArgs, array $options ): } if ( ! empty( $options['mode'] ) ) { - // Accept 'create-new-site' or 'apply-to-existing-site' as CLI values, map to internal values $mode = $options['mode']; - if ( $mode === 'create-new-site' ) { - $config->setExecutionMode( 'create-new-site' ); - } elseif ( $mode === 'apply-to-existing-site' ) { - $config->setExecutionMode( 'apply-to-existing-site' ); + if ( $mode === Runner::EXECUTION_MODE_CREATE_NEW_SITE ) { + $config->setExecutionMode( Runner::EXECUTION_MODE_CREATE_NEW_SITE ); + } elseif ( $mode === Runner::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ) { + $config->setExecutionMode( Runner::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ); if(!empty($options['wp'])) { - throw new InvalidArgumentException( "The --wp option cannot be used with --mode=apply-to-existing-site. The WordPress version is whatever the existing site has." ); + throw new InvalidArgumentException( sprintf( "The --wp option cannot be used with --mode=%s. The WordPress version is whatever the existing site has.", Runner::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ) ); } } else { - throw new InvalidArgumentException( "Invalid execution mode: {$mode}. Supported modes are: create-new-site, apply-to-existing-site" ); + throw new InvalidArgumentException( sprintf( "Invalid execution mode: '{$mode}'. Supported modes are: %s", implode( ', ', Runner::EXECUTION_MODES ) ) ); } } $targetSiteRoot = $options['site-path']; if ( $options['truncate-new-site-directory'] ) { - if ( $options['mode'] !== 'create-new-site' ) { - throw new InvalidArgumentException( "--truncate-new-site-directory can only be used with --mode=create-new-site" ); + if ( $options['mode'] !== Runner::EXECUTION_MODE_CREATE_NEW_SITE ) { + throw new InvalidArgumentException( sprintf( "--truncate-new-site-directory can only be used with --mode=%s", Runner::EXECUTION_MODE_CREATE_NEW_SITE ) ); } $absoluteTargetSiteRoot = realpath( $targetSiteRoot ); if ( false === $absoluteTargetSiteRoot) { From 7df16a4bd4d77c392e9dbb820e2bb8d2129cd11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 30 Jun 2025 16:04:58 +0200 Subject: [PATCH 15/17] Preserve the original root directory when truncating --- components/Blueprints/bin/blueprint.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 7f156f53..98a0925a 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -428,8 +428,19 @@ function cliArgsToRunnerConfiguration( array $positionalArgs, array $options ): mkdir( $targetSiteRoot, 0755, true ); } else if( is_dir( $absoluteTargetSiteRoot ) ) { $fs = LocalFilesystem::create( $absoluteTargetSiteRoot ); - $fs->rmdir( '/', [ 'recursive' => true ] ); - $fs->mkdir( '/', [ 'chmod' => 0755 ] ); + // Delete all the files and directories in the target site root, but preserve the + // target directory itself. Why? In Playground CLI, `/wordpress` is likely to be a + // mount removing a mount root throws an Exception. + foreach ( $fs->ls('/') as $file ) { + if( $fs->is_dir( $file ) ) { + $fs->rmdir( $file, [ 'recursive' => true ] ); + } else { + $fs->rm( $file ); + } + } + if ( ! $fs->is_dir( '/' ) ) { + $fs->mkdir( '/', [ 'chmod' => 0755 ] ); + } } } From a193360aefff82e515a41db4ca5ce3101e8c8a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 2 Jul 2025 00:43:31 +0200 Subject: [PATCH 16/17] Add 'type' to JSON Schema for Blueprints v1 (phpExtensionBundles), remove support for the 'importWordPressFiles' in its v1 shape, use the correct DOCROOT in the NewSiteResolver --- .../Blueprints/SiteResolver/NewSiteResolver.php | 2 +- .../Versions/Version1/V1ToV2Transpiler.php | 13 +------------ .../Blueprints/Versions/Version1/schema-v1.json | 4 ++++ 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/components/Blueprints/SiteResolver/NewSiteResolver.php b/components/Blueprints/SiteResolver/NewSiteResolver.php index b33a981d..d874ddce 100644 --- a/components/Blueprints/SiteResolver/NewSiteResolver.php +++ b/components/Blueprints/SiteResolver/NewSiteResolver.php @@ -156,7 +156,7 @@ static private function isWordPressInstalled( Runtime $runtime, Tracker $progres PHP , [ - 'DOCROOT' => getenv('DOCROOT'), + 'DOCROOT' => $runtime->getConfiguration()->getTargetSiteRoot(), ], null, 5 diff --git a/components/Blueprints/Versions/Version1/V1ToV2Transpiler.php b/components/Blueprints/Versions/Version1/V1ToV2Transpiler.php index ba2bd9f6..9724f4fa 100644 --- a/components/Blueprints/Versions/Version1/V1ToV2Transpiler.php +++ b/components/Blueprints/Versions/Version1/V1ToV2Transpiler.php @@ -215,18 +215,7 @@ public function upgrade( array $validated_v1_blueprint ): array { $this->logger->warning( 'The `enableMultisite` step is not supported by the Blueprint v2 schema and will be ignored.' ); break; case 'importWordPressFiles': - if ( isset( $v1step['progress'] ) ) { - $this->logger->warning( 'The `progress` option is not supported on importWordPressFiles step and will be ignored: %s. Use the runtime configuration to set the progress bar instead.' ); - } - if ( isset( $v1step['pathInZip'] ) ) { - $this->logger->warning( 'The `pathInZip` option is not supported on importWordPressFiles step. The entire step will be ignored.' ); - } else { - /** - * wordPressFilesZip refers to a zip file with a full WordPress installation. - * It's relevant for the target site resolution stage, not as a step. - */ - $v2['wordpressVersion'] = self::convertV1ResourceToV2Reference( $v1step['wordPressFilesZip'] ); - } + $this->logger->warning( 'The `importWordPressFiles` step is not supported by the Blueprint v2 schema. The entire step will be ignored.' ); break; case 'runWpInstallationWizard': $this->logger->warning( 'The `runWpInstallationWizard` step is not supported by the Blueprint v2 schema. Provide your WordPress export URL in the top-level "wordpressVersion" key and the runner will handle the installation automatically.' ); diff --git a/components/Blueprints/Versions/Version1/schema-v1.json b/components/Blueprints/Versions/Version1/schema-v1.json index c47a2d6e..ae6bff1e 100644 --- a/components/Blueprints/Versions/Version1/schema-v1.json +++ b/components/Blueprints/Versions/Version1/schema-v1.json @@ -144,6 +144,10 @@ "description": "User to log in as. If true, logs the user in as admin/password." }, "phpExtensionBundles": { + "type": "array", + "items": { + "type": "string" + }, "deprecated": "No longer used. Feel free to remove it from your Blueprint." }, "steps": { From ef9bf96d8089fd7bde2a0873567f149c842e828f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 2 Jul 2025 01:26:25 +0200 Subject: [PATCH 17/17] Tolerate response code 100 for Git requests --- components/Git/GitRemote.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/Git/GitRemote.php b/components/Git/GitRemote.php index e5677534..091c4eb0 100644 --- a/components/Git/GitRemote.php +++ b/components/Git/GitRemote.php @@ -533,8 +533,11 @@ private function http_request( $path, $postData = null, $headers = array() ) { $response = $reader->await_response(); if ( $response->status_code > 299 || $response->status_code < 200 ) { - $reader->pull( 100 ); - throw new GitRemoteException( 'HTTP request failed with status code ' . $response->status_code . '. First 100 body bytes: ' . $reader->peek( 100 ) ); + // GitHub sometimes responds with 100 status code when the request is successful. + if ( $response->status_code !== 100 ) { + $reader->pull( 100 ); + throw new GitRemoteException( 'HTTP request failed with status code ' . $response->status_code . '. First 100 body bytes: ' . $reader->peek( 100 ) ); + } } return $reader;