From 635da8f42ed4193a7315c776e1801f1e6006e3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 3 Sep 2025 00:43:04 +0200 Subject: [PATCH 1/3] [Blueprints] Install WordPress without WP-CLI --- components/Blueprints/RunnerConfiguration.php | 70 +++++++++++ .../SiteResolver/NewSiteResolver.php | 31 ++--- .../SiteResolver/WordPressInstaller.php | 109 ++++++++++++++++++ components/Blueprints/bin/blueprint.php | 23 ++++ 4 files changed, 212 insertions(+), 21 deletions(-) create mode 100644 components/Blueprints/SiteResolver/WordPressInstaller.php diff --git a/components/Blueprints/RunnerConfiguration.php b/components/Blueprints/RunnerConfiguration.php index f02a04ab3..4818834e0 100644 --- a/components/Blueprints/RunnerConfiguration.php +++ b/components/Blueprints/RunnerConfiguration.php @@ -32,6 +32,26 @@ class RunnerConfiguration { * @var string */ private $siteUrl = ''; + /** + * @var string + */ + private $installSiteTitle = 'WordPress Site'; + /** + * @var string + */ + private $installAdminUser = 'admin'; + /** + * @var string + */ + private $installAdminPassword = 'password'; + /** + * @var string + */ + private $installAdminEmail = 'admin@example.com'; + /** + * @var bool + */ + private $installSkipEmail = false; /** * @var string */ @@ -120,6 +140,56 @@ public function getTargetSiteUrl(): string { return $this->siteUrl; } + public function setInstallSiteTitle( string $t ): self { + $this->installSiteTitle = $t; + + return $this; + } + + public function getInstallSiteTitle(): string { + return $this->installSiteTitle; + } + + public function setInstallAdminUser( string $u ): self { + $this->installAdminUser = $u; + + return $this; + } + + public function getInstallAdminUser(): string { + return $this->installAdminUser; + } + + public function setInstallAdminPassword( string $p ): self { + $this->installAdminPassword = $p; + + return $this; + } + + public function getInstallAdminPassword(): string { + return $this->installAdminPassword; + } + + public function setInstallAdminEmail( string $e ): self { + $this->installAdminEmail = $e; + + return $this; + } + + public function getInstallAdminEmail(): string { + return $this->installAdminEmail; + } + + public function setInstallSkipEmail( bool $skip ): self { + $this->installSkipEmail = $skip; + + return $this; + } + + public function getInstallSkipEmail(): bool { + return $this->installSkipEmail; + } + /** * Sets the database engine. * diff --git a/components/Blueprints/SiteResolver/NewSiteResolver.php b/components/Blueprints/SiteResolver/NewSiteResolver.php index d874ddcef..418bea8c2 100644 --- a/components/Blueprints/SiteResolver/NewSiteResolver.php +++ b/components/Blueprints/SiteResolver/NewSiteResolver.php @@ -11,6 +11,7 @@ use WordPress\HttpClient\Client; use WordPress\HttpClient\Request; use WordPress\Zip\ZipFilesystem; +use WordPress\Blueprints\SiteResolver\WordPressInstaller; use function WordPress\Filesystem\copy_between_filesystems; use function WordPress\Filesystem\wp_join_unix_paths; @@ -110,28 +111,16 @@ static public function resolve( Runtime $runtime, Tracker $progress, ?VersionCon } } - // Perform installation using WP-CLI - // @TODO (low priority): Remove the WP-CLI dependency to lower the download size for blueprints.phar. - $progress['install_wordpress']->set( 0.7, 'Installing WordPress' ); - $wp_cli_path = $runtime->getWpCliPath(); - $process = $runtime->startShellCommand( [ - 'php', - $wp_cli_path, - 'core', - 'install', - '--path=' . $runtime->getConfiguration()->getTargetSiteRoot(), - - // For Docker compatibility. If we got this far, Blueprint runner was already - // allowed to run as root. - '--allow-root', - '--url=' . $runtime->getConfiguration()->getTargetSiteUrl(), - '--title=WordPress Site', - '--admin_user=admin', - '--admin_password=password', - '--admin_email=admin@example.com', - '--skip-email', + // Perform core installation without WP-CLI by invoking WordPressInternals + $installer = new WordPressInstaller(); + $installer->install( $runtime, $progress['install_wordpress'], [ + 'site_url' => $runtime->getConfiguration()->getTargetSiteUrl(), + 'title' => $runtime->getConfiguration()->getInstallSiteTitle(), + 'admin_user' => $runtime->getConfiguration()->getInstallAdminUser(), + 'admin_password' => $runtime->getConfiguration()->getInstallAdminPassword(), + 'admin_email' => $runtime->getConfiguration()->getInstallAdminEmail(), + 'skip_email' => $runtime->getConfiguration()->getInstallSkipEmail(), ] ); - $process->mustRun(); if ( ! self::isWordPressInstalled( $runtime, $progress ) ) { // @TODO: This breaks in Playground CLI diff --git a/components/Blueprints/SiteResolver/WordPressInstaller.php b/components/Blueprints/SiteResolver/WordPressInstaller.php new file mode 100644 index 000000000..9e725642b --- /dev/null +++ b/components/Blueprints/SiteResolver/WordPressInstaller.php @@ -0,0 +1,109 @@ + $runtime->getConfiguration()->getTargetSiteUrl() + * - 'title' => 'WordPress Site' + * - 'admin_user' => 'admin' + * - 'admin_password'=> 'password' + * - 'admin_email' => 'admin@example.com' + * - 'skip_email' => true + */ + public function install( Runtime $runtime, Tracker $tracker, array $options = [] ): void { + $targetFs = $runtime->getTargetFilesystem(); + $tracker->set( 0.65, 'Preparing WordPress installation' ); + + // Ensure wp-config.php exists + if ( ! $targetFs->exists( '/wp-config.php' ) ) { + if ( $targetFs->exists( 'wp-config-sample.php' ) ) { + $targetFs->copy( 'wp-config-sample.php', 'wp-config.php' ); + } else { + throw new BlueprintExecutionException( 'Neither wp-config.php, nor wp-config-sample.php was found in the WordPress archive.' ); + } + } + + // Define DB constants according to configuration + $dbEngine = $runtime->getConfiguration()->getDatabaseEngine(); + $dbCreds = $runtime->getConfiguration()->getDatabaseCredentials(); + $constants = []; + if ( $dbEngine === 'mysql' ) { + $constants = [ + 'DB_NAME' => $dbCreds['databaseName'] ?? 'wordpress', + 'DB_USER' => $dbCreds['username'] ?? 'root', + 'DB_PASSWORD' => $dbCreds['password'] ?? '', + 'DB_HOST' => $dbCreds['host'] ?? '127.0.0.1', + ]; + } elseif ( $dbEngine === 'sqlite' ) { + $constants = [ + 'DB_NAME' => $dbCreds['path'] ?? 'wp.db', + ]; + } + if ( ! empty( $constants ) ) { + (new DefineConstantsStep( $constants ))->run( $runtime, $tracker ); + } + + // Prepare installation options + $siteUrl = $options['site_url'] ?? $runtime->getConfiguration()->getTargetSiteUrl(); + $title = $options['title'] ?? 'WordPress Site'; + $adminUser = $options['admin_user'] ?? 'admin'; + $adminPass = $options['admin_password'] ?? 'password'; + $adminEmail = $options['admin_email'] ?? 'admin@example.com'; + $skipEmail = (bool) ( $options['skip_email'] ?? true ); + + $tracker->set( 0.7, 'Installing WordPress' ); + $runtime->evalPhpCodeInSubProcess( + <<<'PHP' + $runtime->getConfiguration()->getTargetSiteRoot(), + 'SITE_URL' => $siteUrl, + 'TITLE' => $title, + 'ADMIN_USER' => $adminUser, + 'ADMIN_PASS' => $adminPass, + 'ADMIN_EMAIL' => $adminEmail, + 'SKIP_EMAIL' => $skipEmail ? '1' : '0', + ] + ); + } +} + + diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 98a0925a2..d03fe9885 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -263,6 +263,12 @@ function createProgressReporter(): ProgressReporter { 'db-pass' => [ null, true, '', 'MySQL password' ], 'db-name' => [ null, true, 'wordpress', 'MySQL database' ], 'db-path' => [ 'p', true, 'wp.db', 'SQLite file path' ], + // WordPress install options (used when creating a new site) + 'wp-title' => [ null, true, 'WordPress Site', 'Site title used during installation' ], + 'wp-admin-user' => [ null, true, 'admin', 'Administrator username used during installation' ], + 'wp-admin-pass' => [ null, true, 'password', 'Administrator password used during installation' ], + 'wp-admin-email' => [ null, true, 'admin@example.com', 'Administrator email used during installation' ], + 'wp-skip-email' => [ null, false, false, 'Skip sending emails during installation' ], '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: @@ -451,6 +457,23 @@ function cliArgsToRunnerConfiguration( array $positionalArgs, array $options ): $config->setTargetSiteRoot( $absoluteTargetSiteRoot ); $config->setTargetSiteUrl( $options['site-url'] ); + // Install options + if ( ! empty( $options['wp-title'] ) ) { + $config->setInstallSiteTitle( $options['wp-title'] ); + } + if ( ! empty( $options['wp-admin-user'] ) ) { + $config->setInstallAdminUser( $options['wp-admin-user'] ); + } + if ( ! empty( $options['wp-admin-pass'] ) ) { + $config->setInstallAdminPassword( $options['wp-admin-pass'] ); + } + if ( ! empty( $options['wp-admin-email'] ) ) { + $config->setInstallAdminEmail( $options['wp-admin-email'] ); + } + if ( ! empty( $options['wp-skip-email'] ) ) { + $config->setInstallSkipEmail( true ); + } + // Set database engine if ( ! empty( $options['db-engine'] ) ) { $config->setDatabaseEngine( $options['db-engine'] ); From 09352992a13d834d9726d5481cbd3dc1cbedb0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 3 Sep 2025 01:29:18 +0200 Subject: [PATCH 2/3] Adjust tests for Win --- components/Blueprints/Runtime.php | 19 ++++- .../SiteResolver/WordPressInstaller.php | 82 +++++++++++++++++-- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/components/Blueprints/Runtime.php b/components/Blueprints/Runtime.php index cdfa1419c..617b5ada0 100644 --- a/components/Blueprints/Runtime.php +++ b/components/Blueprints/Runtime.php @@ -253,7 +253,24 @@ public function createPhpSubProcess( // Still put the script in a temporary file as the path may be refering // to a file inside the currently executed .phar archive. $actual_script_path = wp_join_unix_paths( $tempDir, 'script.php' ); - $code = ''; + $code = ''; $code .= file_get_contents( $script_path ); file_put_contents( $actual_script_path, $code ); diff --git a/components/Blueprints/SiteResolver/WordPressInstaller.php b/components/Blueprints/SiteResolver/WordPressInstaller.php index 9e725642b..c82a6ad6b 100644 --- a/components/Blueprints/SiteResolver/WordPressInstaller.php +++ b/components/Blueprints/SiteResolver/WordPressInstaller.php @@ -44,9 +44,32 @@ public function install( Runtime $runtime, Tracker $tracker, array $options = [] 'DB_HOST' => $dbCreds['host'] ?? '127.0.0.1', ]; } elseif ( $dbEngine === 'sqlite' ) { - $constants = [ - 'DB_NAME' => $dbCreds['path'] ?? 'wp.db', - ]; + // Prefer canonical default used elsewhere in the runner + $dbPath = $dbCreds['path'] ?? 'wp-content/.ht.sqlite'; + if ($dbPath === '' || $dbPath === null) { + $dbPath = 'wp-content/.ht.sqlite'; + } + $constants = [ 'DB_NAME' => $dbPath ]; + + // Pre-create the database directory to avoid cross‑platform path issues + $targetFs = $runtime->getTargetFilesystem(); + $relativeDbPath = '/' . ltrim( $dbPath, '/' ); + $dbDir = dirname( $relativeDbPath ); + if ( ! $targetFs->is_dir( $dbDir ) ) { + $targetFs->mkdir( $dbDir, 0755, true ); + } + // Best effort to ensure file exists and is writable (SQLite will create as needed) + if ( ! $targetFs->exists( $relativeDbPath ) ) { + try { $targetFs->put_contents( $relativeDbPath, '' ); } catch ( \Throwable $e ) { /* ignore */ } + } + + // Ensure SQLite extension availability for clearer errors on macOS/Windows + if ( ! extension_loaded('sqlite3') && ! extension_loaded('pdo_sqlite') ) { + throw new BlueprintExecutionException( + 'SQLite database engine selected, but neither sqlite3 nor pdo_sqlite PHP extension is loaded. ' + . 'Enable one of these extensions or switch --db-engine to mysql.' + ); + } } if ( ! empty( $constants ) ) { (new DefineConstantsStep( $constants ))->run( $runtime, $tracker ); @@ -71,20 +94,69 @@ public function install( Runtime $runtime, Tracker $tracker, array $options = [] $admin_pass = getenv('ADMIN_PASS'); $admin_email = getenv('ADMIN_EMAIL'); $skip_email = getenv('SKIP_EMAIL') === '1'; + $host = null; $port = null; $scheme = null; + if ($site_url) { + $parts = @parse_url($site_url); + $host = $parts['host'] ?? null; + $port = $parts['port'] ?? null; + $scheme = $parts['scheme'] ?? null; + } if (!file_exists($docroot . '/wp-load.php')) { fwrite(STDERR, "Blueprint Error: wp-load.php not found in DOCROOT\n"); exit(1); } + // Ensure WordPress runs in installing context and suppress emails reliably + if (!defined('WP_INSTALLING')) { + define('WP_INSTALLING', true); + } + if ($site_url && !defined('WP_HOME')) { + define('WP_HOME', rtrim($site_url, '/')); + } + if ($site_url && !defined('WP_SITEURL')) { + define('WP_SITEURL', rtrim($site_url, '/')); + } + // Normalize web server globals to avoid platform-specific behavior + if ($host) { + $_SERVER['HTTP_HOST'] = $host . ($port ? ":$port" : ''); + $_SERVER['SERVER_NAME'] = $host; + } + if ($scheme) { + $_SERVER['HTTPS'] = ($scheme === 'https') ? 'on' : 'off'; + $_SERVER['SERVER_PORT'] = ($scheme === 'https') ? '443' : '80'; + $_SERVER['REQUEST_SCHEME'] = $scheme; + } + if (!isset($_SERVER['REQUEST_URI'])) { + $_SERVER['REQUEST_URI'] = '/'; + } + if (!isset($_SERVER['SERVER_PROTOCOL'])) { + $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; + } + if (!isset($_SERVER['REMOTE_ADDR'])) { + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + } + if (!isset($_SERVER['SCRIPT_NAME'])) { + $_SERVER['SCRIPT_NAME'] = '/index.php'; + } + if (!isset($_SERVER['DOCUMENT_ROOT'])) { + $_SERVER['DOCUMENT_ROOT'] = $docroot; + } + if (!isset($_SERVER['SCRIPT_FILENAME'])) { + $_SERVER['SCRIPT_FILENAME'] = $docroot . '/index.php'; + } require $docroot . '/wp-load.php'; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; if ($skip_email) { - add_filter('wp_mail', '__return_false'); - add_filter('send_password_change_email', '__return_false'); + // Short-circuit wp_mail completely (introduced in WP 5.5) + if (function_exists('add_filter')) { + add_filter('pre_wp_mail', '__return_false'); + add_filter('send_password_change_email', '__return_false'); + } } wp_install($title, $admin_user, $admin_email, /*public*/ true, '', $admin_pass); if ($site_url) { + $site_url = rtrim($site_url, '/'); update_option('siteurl', $site_url); update_option('home', $site_url); } From a8c0f4d7cd26f644ed216086896c675f030b6a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 3 Sep 2025 01:41:43 +0200 Subject: [PATCH 3/3] Adjust NOWDOC syntax for PHP 7.2 --- .../SiteResolver/WordPressInstaller.php | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/components/Blueprints/SiteResolver/WordPressInstaller.php b/components/Blueprints/SiteResolver/WordPressInstaller.php index c82a6ad6b..b490c049a 100644 --- a/components/Blueprints/SiteResolver/WordPressInstaller.php +++ b/components/Blueprints/SiteResolver/WordPressInstaller.php @@ -86,84 +86,84 @@ public function install( Runtime $runtime, Tracker $tracker, array $options = [] $tracker->set( 0.7, 'Installing WordPress' ); $runtime->evalPhpCodeInSubProcess( <<<'PHP' - $runtime->getConfiguration()->getTargetSiteRoot(),