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.32
Disabled: pcntl_fork
Upload Files
File: //wordpress/plugins/wp-cloud-client/1.1.9/src/Multisite/SubsiteSync.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\Multisite;

use VPlugins\WPCloudClient\Settings\SettingsRepository;
use VPlugins\WPCloudClient\Support\Logger;
use WP_Site;

/**
 * Syncs multisite subsite data to the WP Cloud Manager API whenever
 * site lifecycle events occur (create, delete, status changes, option updates).
 *
 * On each event, the full network subsite list is re-fetched and POSTed to:
 *   POST {managerUrl}/wordpress/v1/site/{vendastaSiteId}/update-subsites-info/
 */
final class SubsiteSync {

	/**
	 * Options whose changes should trigger a sync.
	 *
	 * @var string[]
	 */
	private const WATCHED_OPTIONS = [ 'blogname', 'siteurl', 'home' ];

	/**
	 * Construct with WP Cloud Manager credentials and a logger.
	 *
	 * @param SettingsRepository $settings Per-site settings (API URL, site ID, API key).
	 * @param Logger             $logger   Logger instance.
	 */
	public function __construct(
		private readonly SettingsRepository $settings,
		private readonly Logger $logger,
	) {}

	/**
	 * Register WordPress lifecycle hooks.
	 *
	 * @return void
	 */
	public function register(): void {
		// Site creation / hard deletion (WP 5.1+).
		add_action( 'wp_initialize_site', [ $this, 'handleSiteChange' ], 10, 2 );
		add_action( 'wp_delete_site', [ $this, 'handleSiteChange' ], 10, 1 );

		// Option changes that affect site identity (title, URLs).
		add_action( 'updated_option', [ $this, 'handleUpdatedOption' ], 10, 3 );

		// Status changes: soft delete / restore, archive, spam.
		$statusHooks = [
			'make_delete_blog',
			'make_undelete_blog',
			'archive_blog',
			'unarchive_blog',
			'make_spam_blog',
			'make_ham_blog',
		];

		foreach ( $statusHooks as $hook ) {
			add_action( $hook, [ $this, 'handleSiteAction' ], 10, 1 );
		}
	}

	/**
	 * Handle site creation and hard deletion events.
	 *
	 * Accepts either a WP_Site object (wp_initialize_site / wp_delete_site)
	 * or no arguments — variadic to absorb whatever WordPress passes.
	 *
	 * @param mixed ...$args WordPress hook arguments (ignored).
	 * @return void
	 */
	public function handleSiteChange( mixed ...$args ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
		$this->syncSubsites();
	}

	/**
	 * Trigger a sync when a watched site option changes.
	 *
	 * Hooked to: updated_option
	 *
	 * @param string $optionName The name of the updated option.
	 * @param mixed  $oldValue   Previous value (unused).
	 * @param mixed  $newValue   New value (unused).
	 * @return void
	 */
	public function handleUpdatedOption( string $optionName, mixed $oldValue, mixed $newValue ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		if ( in_array( $optionName, self::WATCHED_OPTIONS, true ) ) {
			$this->syncSubsites();
		}
	}

	/**
	 * Handle soft status-change events (delete, archive, spam, etc.).
	 *
	 * Hooked to: make_delete_blog, make_undelete_blog, archive_blog,
	 *            unarchive_blog, make_spam_blog, make_ham_blog
	 *
	 * @param int $blogId The affected blog ID (unused — full sync is always sent).
	 * @return void
	 */
	public function handleSiteAction( int $blogId ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
		$this->syncSubsites();
	}

	// -------------------------------------------------------------------------
	// Sync
	// -------------------------------------------------------------------------

	/**
	 * Fetch the full subsite list and POST it to the WP Cloud Manager API.
	 *
	 * Silently returns early if the manager URL or site ID is not configured.
	 *
	 * @return void
	 */
	private function syncSubsites(): void {
		$managerUrl     = $this->settings->getManagerUrl();
		$siteId         = (string) $this->settings->getSiteId();
		$vendastaSiteId = (string) $this->settings->getVendastaSiteId();

		if ( ! \is_multisite() ) {
			// get_sites() is only available after network_install has run and
			// WordPress is booting in multisite mode. Guard here so bootstrap_site
			// does not fatal when the site type is MULTISITE but network_install
			// has not yet converted the site (e.g. unknown_action soft-fail).
			return;
		}

		if ( empty( $managerUrl ) || empty( $siteId ) || empty( $vendastaSiteId ) ) {
			$this->logger->error( 'SubsiteSync: skipped — managerUrl, siteId, or vendastaSiteId is not configured.' );
			return;
		}

		$allSites = \get_sites( [ 'number' => 0 ] );
		$subsites = [];

		foreach ( $allSites as $site ) {
			$details = \get_blog_details( (int) $site->blog_id );

			if ( ! $details ) {
				continue;
			}

			if ( $details->spam ) {
				$status = 'spam';
			} elseif ( $details->archived ) {
				$status = 'archived';
			} elseif ( $details->deleted ) {
				$status = 'deactivated';
			} else {
				$status = 'active';
			}

			$subsites[] = [
				'blog_id'      => (int) $site->blog_id,
				'blog_name'    => (string) \get_blog_option( (int) $site->blog_id, 'blogname' ),
				'path'         => (string) $site->path,
				'domain'       => (string) $details->domain,
				'status'       => $status,
				'created_at'   => (string) $details->registered,
				'last_updated' => (string) $details->last_updated,
			];
		}

		$payload = [
			'staging'  => $this->settings->isStaging(),
			'subsites' => $subsites,
		];

		$this->sendDataToInternalApi( $siteId, $vendastaSiteId, $managerUrl, $payload );
	}

	/**
	 * POST the payload to the manager API.
	 *
	 * @param string               $siteId         The ATOMIC_SITE_ID used for authentication.
	 * @param string               $vendastaSiteId The Vendasta site ID (WSP_SITE_ID) used in the URL path.
	 * @param string               $managerUrl     Base manager URL.
	 * @param array<string, mixed> $payload        Data to send.
	 * @return void
	 */
	private function sendDataToInternalApi( string $siteId, string $vendastaSiteId, string $managerUrl, array $payload ): void {
		$url = rtrim( $managerUrl, '/' ) . '/wordpress/v1/site/' . rawurlencode( $vendastaSiteId ) . '/update-subsites-info/';

		$response = wp_remote_post(
			$url,
			[
				'body'        => wp_json_encode( $payload ),
				'data_format' => 'body',
				'headers'     => [
					'Content-Type'   => 'application/json',
					'X-WPCC-Site-ID' => $siteId,
					'X-WPCC-API-Key' => (string) $this->settings->getApiKey(),
				],
				'timeout'     => 5,
			]
		);

		if ( is_wp_error( $response ) ) {
			$this->logger->error(
				'SubsiteSync: failed to send subsite data to WP Cloud Manager',
				$response->get_error_message()
			);
		} else {
			$code = (int) wp_remote_retrieve_response_code( $response );
			if ( $code < 200 || $code >= 300 ) {
				$this->logger->error(
					'SubsiteSync: manager rejected subsite update',
					'HTTP ' . $code . ' ' . wp_remote_retrieve_body( $response )
				);
			}
		}
	}
}