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 )
);
}
}
}
}