File: /wordpress/plugins/wp-cloud-client/1.1.4/src/Handler/TriggerJetpackBackupHandler.php
<?php
declare(strict_types=1);
namespace VPlugins\WPCloudClient\Handler;
use VPlugins\WPCloudClient\Settings\SettingsRepository;
use VPlugins\WPCloudClient\Support\Logger;
/**
* Triggers an on-demand Jetpack Backup via the wp-cloud-manager relay.
*
* Action name: trigger_jetpack_backup
*
* No required params.
*
* The Jetpack partner bearer token is never stored on the WP install.
* The manager relay holds the partner token server-side and proxies
* the request to Automattic's rewind API on behalf of this site.
*
* Success response example:
* {
* "triggered": true,
* "blog_id": 123456789,
* "backup_id": "abc-123",
* "status": "queued",
* "message": "Backup triggered successfully."
* }
*
* Error conditions (returned as structured 400/500 via ActionController):
* - Jetpack not connected on this site → 400 validation_error
* - WPCC_MANAGER_URL not configured → 400 validation_error
* - Jetpack Backup license not active → 400 validation_error
* - Jetpack partner token not provisioned → 400 validation_error
* - Manager rejected auth (401) → 500 action_failed
* - Transport failure or manager server error → 500 action_failed
*/
final class TriggerJetpackBackupHandler extends AbstractHandler {
/**
* Class constructor.
*
* @param SettingsRepository $settings Plugin settings repository.
* @param Logger $logger Logger instance.
*/
public function __construct(
private readonly SettingsRepository $settings,
private readonly Logger $logger,
) {}
/**
* Return the action name.
*
* @return string
*/
public function action(): string {
return 'trigger_jetpack_backup';
}
/**
* Trigger a Jetpack Backup via the wp-cloud-manager relay.
*
* @param array<string, mixed> $params Action parameters (unused).
* @return array<string, mixed> Backup trigger result.
*
* @throws \InvalidArgumentException When Jetpack is not connected, manager URL is missing,
* the backup license is not active, or the partner token
* is not provisioned.
* @throws \RuntimeException On transport failure, manager auth failure (401),
* or manager server error.
*/
public function execute( array $params ): array {
$blogId = $this->resolveBlogId();
$managerUrl = $this->resolveManagerUrl();
return $this->callManagerRelay( $blogId, $managerUrl );
}
/**
* Read the Jetpack blog ID from wp_options and validate the connection.
*
* @return int Jetpack blog ID.
* @throws \InvalidArgumentException When Jetpack is not connected or options are corrupt.
*/
private function resolveBlogId(): int {
$jetpackOptions = (array) get_option( 'jetpack_options', [] );
if ( ! isset( $jetpackOptions['id'] ) ) {
$this->logger->warning( 'TriggerJetpackBackup: Jetpack is not connected — blog ID missing from jetpack_options' );
throw new \InvalidArgumentException(
'Jetpack is not connected to this site. Connect Jetpack before triggering a backup.'
);
}
if ( ! is_numeric( $jetpackOptions['id'] ) ) {
$this->logger->warning(
'TriggerJetpackBackup: jetpack_options[id] is not numeric — option may be corrupt',
[ 'type' => gettype( $jetpackOptions['id'] ) ]
);
throw new \InvalidArgumentException(
'Jetpack options are corrupt — blog ID is not a valid integer. Check jetpack_options in wp_options.'
);
}
$blogId = (int) $jetpackOptions['id'];
if ( $blogId <= 0 ) {
$this->logger->warning( 'TriggerJetpackBackup: Jetpack is not connected — blog ID is zero or negative' );
throw new \InvalidArgumentException(
'Jetpack is not connected to this site. Connect Jetpack before triggering a backup.'
);
}
return $blogId;
}
/**
* Resolve and validate the wp-cloud-manager base URL.
*
* @return string Manager base URL.
* @throws \InvalidArgumentException When WPCC_MANAGER_URL is not configured.
*/
private function resolveManagerUrl(): string {
$url = $this->settings->getManagerUrl();
if ( '' === $url ) {
throw new \InvalidArgumentException(
'WPCC_MANAGER_URL is not configured. Set the WPCC_MANAGER_URL constant or wpcc_manager_url filter.'
);
}
return $url;
}
/**
* POST to the wp-cloud-manager relay endpoint and handle the response.
*
* @param int $blogId Jetpack blog ID.
* @param string $managerUrl Base URL of wp-cloud-manager.
* @return array<string, mixed> Parsed success payload.
*
* @throws \InvalidArgumentException On 402 (no license) or 424 (partner token unprovisioned).
* @throws \RuntimeException On transport failures, 401 auth errors, 5xx, or unexpected codes.
*/
private function callManagerRelay( int $blogId, string $managerUrl ): array {
$url = rtrim( $managerUrl, '/' ) . '/wordpress/v1/jetpack/backup/trigger';
$response = wp_remote_post(
$url,
[
'body' => wp_json_encode( [ 'blog_id' => $blogId ] ),
'data_format' => 'body',
'headers' => [
'Content-Type' => 'application/json',
'X-WPCC-Site-ID' => (string) $this->settings->getSiteId(),
'X-WPCC-API-Key' => (string) $this->settings->getApiKey(),
],
'timeout' => 15,
]
);
$this->handleTransportError( $response, $blogId );
$statusCode = (int) wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( 202 === $statusCode ) {
return $this->parseSuccessBody( $blogId, $body );
}
$this->handleErrorResponse( $statusCode, $body );
}
/**
* Throw a RuntimeException if the HTTP response is a WP_Error (transport failure).
*
* @param array|\WP_Error $response Response from wp_remote_post.
* @param int $blogId Blog ID for log context.
* @return void
* @throws \RuntimeException On transport failure.
*/
private function handleTransportError( array|\WP_Error $response, int $blogId ): void {
if ( ! is_wp_error( $response ) ) {
return;
}
$this->logger->error(
'TriggerJetpackBackup: transport error',
[
'blog_id' => $blogId,
'error' => $response->get_error_message(),
]
);
throw new \RuntimeException( 'Failed to reach wp-cloud-manager. See server log for details.' );
}
/**
* Parse and validate the 202 response body from the manager relay.
*
* @param int $blogId Blog ID for log context and the response payload.
* @param string $body Raw response body JSON.
* @return array<string, mixed>
* @throws \RuntimeException When the body is not valid JSON, or backup_id is missing.
*/
private function parseSuccessBody( int $blogId, string $body ): array {
$decoded = json_decode( $body, true );
if ( ! is_array( $decoded ) ) {
$this->logger->error(
'TriggerJetpackBackup: manager returned 202 with non-JSON body',
[
'blog_id' => $blogId,
'body_excerpt' => substr( $body, 0, 256 ),
]
);
throw new \RuntimeException( 'wp-cloud-manager returned an invalid response body.' );
}
if ( ! isset( $decoded['backup_id'] ) ) {
$this->logger->error(
'TriggerJetpackBackup: manager 202 response is missing backup_id',
[
'blog_id' => $blogId,
'body_excerpt' => substr( $body, 0, 256 ),
]
);
throw new \RuntimeException( 'wp-cloud-manager response did not include backup_id.' );
}
$backupId = (string) $decoded['backup_id'];
$status = isset( $decoded['status'] ) ? (string) $decoded['status'] : 'queued';
$this->logger->info(
'TriggerJetpackBackup: backup triggered',
[
'blog_id' => $blogId,
'backup_id' => $backupId,
'status' => $status,
]
);
return [
'triggered' => true,
'blog_id' => $blogId,
'backup_id' => $backupId,
'status' => $status,
'message' => 'Backup triggered successfully.',
];
}
/**
* Map non-202 status codes to typed exceptions.
*
* @param int $statusCode HTTP status code.
* @param string $body Raw response body (excerpt logged for diagnostics).
*
* @throws \InvalidArgumentException On 402 (no license) or 424 (partner token unprovisioned).
* @throws \RuntimeException On 401, 5xx, or any other unexpected status code.
*/
private function handleErrorResponse( int $statusCode, string $body ): never {
$this->logger->error(
'TriggerJetpackBackup: manager returned non-202',
[
'status' => $statusCode,
'body_excerpt' => substr( $body, 0, 512 ),
]
);
match ( true ) {
402 === $statusCode => throw new \InvalidArgumentException(
'Jetpack Backup license is not active on this site. Provision a license before triggering a backup.'
),
424 === $statusCode => throw new \InvalidArgumentException(
'Jetpack partner token is not provisioned for this site. Contact WP Cloud support.'
),
401 === $statusCode => throw new \RuntimeException(
'Manager rejected the request — check Site ID and API Key configuration.'
),
$statusCode >= 500 => throw new \RuntimeException(
sprintf( 'wp-cloud-manager returned a server error (HTTP %d). Try again later.', $statusCode )
),
default => throw new \RuntimeException(
sprintf( 'Unexpected response from wp-cloud-manager (HTTP %d).', $statusCode )
),
};
}
}