HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.31
Disabled: pcntl_fork
Upload Files
File: /wordpress/plugins/wp-cloud-client/beta/src/Handler/ConfigureMultisiteHandler.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\Handler;

use VPlugins\WPCloudClient\Debug\WpConfigEditor;
use VPlugins\WPCloudClient\Support\Logger;

/**
 * Writes WordPress multisite constants to wp-config.php in two phases.
 *
 * Action name: configure_multisite
 *
 * This action must be called twice by wp-cloud-manager for each multisite site:
 *
 * Phase "pre" — before network_install:
 *   Writes WP_ALLOW_MULTISITE and SUBDOMAIN_INSTALL only. Writing MULTISITE=true
 *   before the network tables exist causes every subsequent WP request to fatal
 *   (ms-settings.php queries wp_blogs which does not exist yet).
 *
 * Phase "post" — after network_install succeeds:
 *   Writes MULTISITE=true and the *_CURRENT_SITE constants. At this point the
 *   network tables exist and WP can safely load ms-settings.php.
 *
 * Required params (both phases):
 *   - phase (string) — "pre" or "post".
 *
 * DOMAIN_CURRENT_SITE is always written as $_SERVER['HTTP_HOST'] so the domain
 * resolves dynamically at runtime — domain changes on the atomic require no
 * wp-config.php edits.
 *
 * Optional params (pre phase):
 *   - subdomain_install (bool) — true for subdomain network (default: false).
 *
 * Optional params (post phase):
 *   - path    (string) — Network base path (default: "/").
 *   - site_id (int)   — SITE_ID_CURRENT_SITE (default: 1).
 *   - blog_id (int)   — BLOG_ID_CURRENT_SITE (default: 1).
 *
 * Pre-phase response example:
 * {
 *   "phase": "pre",
 *   "subdomain_install": false,
 *   "message": "WP_ALLOW_MULTISITE enabled. Call network_install next, then configure_multisite with phase=post."
 * }
 *
 * Post-phase response example:
 * {
 *   "phase": "post",
 *   "domain": "$_SERVER['HTTP_HOST']",
 *   "path": "/",
 *   "site_id": 1,
 *   "blog_id": 1,
 *   "message": "Multisite constants configured successfully."
 * }
 */
final class ConfigureMultisiteHandler extends AbstractHandler {

	/**
	 * Class constructor.
	 *
	 * @param WpConfigEditor $editor wp-config.php editor service.
	 * @param Logger         $logger Logger instance.
	 */
	public function __construct(
		private readonly WpConfigEditor $editor,
		private readonly Logger $logger,
	) {}

	/**
	 * Return the action name.
	 *
	 * @return string Action name.
	 */
	public function action(): string {
		return 'configure_multisite';
	}

	/**
	 * Write multisite constants to wp-config.php for the requested phase.
	 *
	 * @param array<string, mixed> $params Action parameters.
	 * @return array<string, mixed> Configuration result.
	 *
	 * @throws \InvalidArgumentException When phase is missing or invalid.
	 */
	public function execute( array $params ): array {
		$this->requireParams( $params, 'phase' );

		$phase = strtolower( trim( (string) $params['phase'] ) );

		if ( ! in_array( $phase, [ 'pre', 'post' ], true ) ) {
			throw new \InvalidArgumentException( "Invalid phase '{$phase}': must be 'pre' or 'post'." );
		}

		return 'pre' === $phase
			? $this->executePre( $params )
			: $this->executePost( $params );
	}

	/**
	 * Write WP_ALLOW_MULTISITE and SUBDOMAIN_INSTALL only.
	 *
	 * Must be called before network_install. Writing MULTISITE=true here would
	 * cause wp-settings.php to load ms-settings.php and query the not-yet-created
	 * multisite DB tables on every subsequent request, fatalling before any plugin
	 * handler can run — including network_install itself.
	 *
	 * @param array<string, mixed> $params Action parameters.
	 * @return array<string, mixed>
	 *
	 * @throws \InvalidArgumentException When subdomain_install is not a boolean.
	 * @throws \RuntimeException         When wp-config.php cannot be written.
	 */
	private function executePre( array $params ): array {
		if ( isset( $params['subdomain_install'] ) && ! is_bool( $params['subdomain_install'] ) ) {
			throw new \InvalidArgumentException( "'subdomain_install' must be a boolean." );
		}
		$subdomain_install = (bool) ( $params['subdomain_install'] ?? false );

		$this->logger->info(
			'ConfigureMultisite pre: enabling WP_ALLOW_MULTISITE',
			[ 'subdomain_install' => $subdomain_install ]
		);

		try {
			$this->editor->applyConstants(
				[
					'WP_ALLOW_MULTISITE' => true,
					'SUBDOMAIN_INSTALL'  => $subdomain_install,
					// Explicitly set MULTISITE=false so that a previous failed run which
					// already wrote MULTISITE=true does not cause WordPress to load
					// ms-settings.php (and query the not-yet-created wp_site table) on
					// every subsequent request until network_install has been re-run.
					'MULTISITE'          => false,
				]
			);
		} catch ( \RuntimeException $e ) {
			$this->logger->error( 'ConfigureMultisite pre: failed to write wp-config.php', $e );
			throw $e;
		}

		$this->logger->info( 'ConfigureMultisite pre: complete' );

		return [
			'phase'             => 'pre',
			'subdomain_install' => $subdomain_install,
			'message'           => 'WP_ALLOW_MULTISITE enabled. Call network_install next, then configure_multisite with phase=post.',
		];
	}

	/**
	 * Write MULTISITE and the *_CURRENT_SITE constants.
	 *
	 * Must be called after network_install succeeds. At that point the multisite
	 * DB tables exist and subsequent requests can safely load ms-settings.php.
	 *
	 * @param array<string, mixed> $params Action parameters.
	 * @return array<string, mixed>
	 *
	 * @throws \InvalidArgumentException When path/id values are invalid.
	 * @throws \RuntimeException         When wp-config.php cannot be written.
	 */
	private function executePost( array $params ): array {
		$path    = isset( $params['path'] ) ? (string) $params['path'] : '/';
		$site_id = isset( $params['site_id'] ) ? (int) $params['site_id'] : 1;
		$blog_id = isset( $params['blog_id'] ) ? (int) $params['blog_id'] : 1;

		// Always write DOMAIN_CURRENT_SITE as a raw PHP expression so it resolves
		// dynamically at runtime — domain changes require no wp-config.php edits.
		$domain = '$_SERVER[\'HTTP_HOST\']';
		if ( '' === $path || '/' !== $path[0] ) {
			throw new \InvalidArgumentException( "'path' must begin with a forward slash." );
		}
		if ( $site_id < 1 ) {
			throw new \InvalidArgumentException( "'site_id' must be a positive integer." );
		}
		if ( $blog_id < 1 ) {
			throw new \InvalidArgumentException( "'blog_id' must be a positive integer." );
		}

		$this->logger->info(
			'ConfigureMultisite post: writing multisite constants',
			[
				'domain'  => $domain,
				'path'    => $path,
				'site_id' => $site_id,
				'blog_id' => $blog_id,
			]
		);

		try {
			$this->editor->applyConstants(
				[
					'MULTISITE'            => true,
					'DOMAIN_CURRENT_SITE'  => $domain,
					'PATH_CURRENT_SITE'    => $path,
					'SITE_ID_CURRENT_SITE' => $site_id,
					'BLOG_ID_CURRENT_SITE' => $blog_id,
				]
			);
		} catch ( \RuntimeException $e ) {
			$this->logger->error( 'ConfigureMultisite post: failed to write wp-config.php', $e );
			throw $e;
		}

		$this->logger->info( 'ConfigureMultisite post: complete', [ 'domain' => $domain ] );

		return [
			'phase'   => 'post',
			'domain'  => $domain,
			'path'    => $path,
			'site_id' => $site_id,
			'blog_id' => $blog_id,
			'message' => 'Multisite constants configured successfully.',
		];
	}
}