Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions components/Blueprints/RunnerConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*
Expand Down
19 changes: 18 additions & 1 deletion components/Blueprints/Runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<?php function append_output( $output ) { file_put_contents( getenv("OUTPUT_FILE"), $output, FILE_APPEND ); } $_SERVER["HTTP_HOST"] = "localhost"; ?>';
$code = '<?php '
. 'function append_output( $output ) { file_put_contents( getenv("OUTPUT_FILE"), $output, FILE_APPEND ); } '
. 'if (!isset($_SERVER["HTTP_HOST"])) { $_SERVER["HTTP_HOST"] = "localhost"; } '
. 'if (!isset($_SERVER["SERVER_NAME"])) { $_SERVER["SERVER_NAME"] = $_SERVER["HTTP_HOST"]; } '
. '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["REQUEST_METHOD"])) { $_SERVER["REQUEST_METHOD"] = "GET"; } '
. 'if (!isset($_SERVER["HTTPS"])) { $_SERVER["HTTPS"] = "off"; } '
. 'if (!isset($_SERVER["SERVER_PORT"])) { $_SERVER["SERVER_PORT"] = "80"; } '
. 'if (!isset($_SERVER["SCRIPT_NAME"])) { $_SERVER["SCRIPT_NAME"] = "/index.php"; } '
. '$__docroot = getenv("DOCROOT"); '
. 'if ($__docroot) { '
. ' if (!isset($_SERVER["DOCUMENT_ROOT"])) { $_SERVER["DOCUMENT_ROOT"] = $__docroot; } '
. ' if (!isset($_SERVER["SCRIPT_FILENAME"])) { $_SERVER["SCRIPT_FILENAME"] = $__docroot . "/index.php"; } '
. '} '
. 'unset($__docroot); '
. '?>';
$code .= file_get_contents( $script_path );
file_put_contents( $actual_script_path, $code );

Expand Down
31 changes: 10 additions & 21 deletions components/Blueprints/SiteResolver/NewSiteResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
181 changes: 181 additions & 0 deletions components/Blueprints/SiteResolver/WordPressInstaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

namespace WordPress\Blueprints\SiteResolver;

use WordPress\Blueprints\Progress\Tracker;
use WordPress\Blueprints\Runtime;
use WordPress\Blueprints\Steps\DefineConstantsStep;
use WordPress\Blueprints\Exception\BlueprintExecutionException;

class WordPressInstaller {
/**
* Install WordPress core without relying on WP-CLI.
*
* Supported options (defaults shown):
* - 'site_url' => $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' ) {
// 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 );
}

// 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'
<?php
$docroot = getenv('DOCROOT');
$site_url = getenv('SITE_URL');
$title = getenv('TITLE');
$admin_user = getenv('ADMIN_USER');
$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) {
// 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);
}
if (function_exists('flush_rewrite_rules')) {
flush_rewrite_rules(false);
}
PHP
,
[
'DOCROOT' => $runtime->getConfiguration()->getTargetSiteRoot(),
'SITE_URL' => $siteUrl,
'TITLE' => $title,
'ADMIN_USER' => $adminUser,
'ADMIN_PASS' => $adminPass,
'ADMIN_EMAIL' => $adminEmail,
'SKIP_EMAIL' => $skipEmail ? '1' : '0',
]
);
}
}


23 changes: 23 additions & 0 deletions components/Blueprints/bin/blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'] );
Expand Down
Loading