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/previous/src/Handler/TriggerThemeUpdateHandler.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\Handler;

use VPlugins\WPCloudClient\Support\Logger;
use VPlugins\WPCloudClient\Support\RollbackManager;

/**
 * Triggers an immediate theme update or rollback via the REST actions endpoint.
 *
 * Action name: trigger_theme_update
 *
 * Required params:
 *   - slug (string) - Theme stylesheet to update (e.g., "twentytwentyfour").
 *
 * Optional params:
 *   - rollback (bool) – Revert to the version recorded before the last update.
 *
 * Update response example:
 * {
 *   "updated": true,
 *   "slug": "twentytwentyfive",
 *   "previous_version": "1.0",
 *   "new_version": "1.1",
 *   "message": "Theme updated from 1.0 to 1.1."
 * }
 *
 * Rollback response example:
 * {
 *   "rolled_back": true,
 *   "slug": "twentytwentyfive",
 *   "previous_version": "1.1",
 *   "restored_version": "1.0",
 *   "message": "Theme rolled back from 1.1 to 1.0."
 * }
 */
final class TriggerThemeUpdateHandler extends AbstractHandler {

	/**
	 * Class constructor.
	 *
	 * @param RollbackManager $rollbackManager Rollback manager instance.
	 * @param Logger          $logger          Logger instance.
	 */
	public function __construct(
		private readonly RollbackManager $rollbackManager,
		private readonly Logger $logger,
	) {}

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

	/**
	 * Trigger a theme update or rollback.
	 *
	 * @param array<string, mixed> $params Action parameters.
	 * @return array<string, mixed> Operation result.
	 *
	 * @throws \InvalidArgumentException When required params are missing.
	 * @throws \RuntimeException On failure.
	 */
	public function execute( array $params ): array {
		$this->requireParams( $params, 'slug' );

		$slug     = (string) $params['slug'];
		$rollback = ! empty( $params['rollback'] );

		$this->validateThemeExists( $slug );
		$this->loadUpgraderDependencies();

		if ( $rollback ) {
			return $this->performRollback( $slug );
		}

		return $this->performUpdate( $slug );
	}

	/**
	 * Validate that the theme exists.
	 *
	 * @param string $slug Theme slug.
	 * @throws \InvalidArgumentException If theme not found.
	 */
	private function validateThemeExists( string $slug ): void {
		$theme = wp_get_theme( $slug );
		if ( ! $theme->exists() ) {
			throw new \InvalidArgumentException( sprintf( 'Theme "%s" not found.', $slug ) );
		}
	}

	/**
	 * Download and install the latest version of a theme.
	 *
	 * @param string $slug Theme slug.
	 * @return array<string, mixed>
	 * @throws \RuntimeException When the upgrade fails.
	 */
	private function performUpdate( string $slug ): array {
		$theme          = wp_get_theme( $slug );
		$currentVersion = $theme->get( 'Version' );

		// Check if update is available.
		$transient = get_site_transient( 'update_themes' );
		if ( ! is_object( $transient ) || ! isset( $transient->response[ $slug ] ) ) {
			return [
				'updated' => false,
				'slug'    => $slug,
				'message' => 'No update available for this theme.',
			];
		}

		$updateData = $transient->response[ $slug ];
		$newVersion = $updateData['new_version'] ?? 'unknown';

		// Construct download URL for current version (for rollback).
		$downloadUrl = sprintf(
			'https://downloads.wordpress.org/theme/%s.%s.zip',
			$slug,
			$currentVersion
		);

		// Store current version + download URL for rollback.
		$this->rollbackManager->recordPreUpdateVersion( 'theme', $slug, $currentVersion, $downloadUrl );

		// Perform the update.
		$upgrader = new \Theme_Upgrader( new \Automatic_Upgrader_Skin() );
		$result   = $upgrader->upgrade( $slug );

		if ( is_wp_error( $result ) ) {
			$this->logger->error( 'Theme update failed', $result->get_error_message() );
			throw new \RuntimeException(
				sprintf( 'Update failed for theme "%s": %s', $slug, $result->get_error_message() )
			);
		}

		$this->logger->info(
			'Theme updated',
			[
				'slug' => $slug,
				'from' => $currentVersion,
				'to'   => $newVersion,
			]
		);

		return [
			'updated'          => true,
			'slug'             => $slug,
			'previous_version' => $currentVersion,
			'new_version'      => $newVersion,
			'message'          => sprintf(
				'Theme updated from %s to %s.',
				$currentVersion,
				$newVersion
			),
		];
	}

	/**
	 * Re-install the version that was active before the last update.
	 *
	 * @param string $slug Theme slug.
	 * @return array<string, mixed>
	 * @throws \RuntimeException When no rollback target exists or the install fails.
	 */
	private function performRollback( string $slug ): array {
		if ( ! $this->rollbackManager->canRollback( 'theme', $slug ) ) {
			throw new \RuntimeException(
				sprintf( 'No rollback target is recorded for theme "%s". Trigger an update first.', $slug )
			);
		}

		$currentVersion = $this->getCurrentVersion( $slug );
		$targetVersion  = $this->rollbackManager->getPreviousVersion( 'theme', $slug );
		$downloadUrl    = $this->rollbackManager->getDownloadUrl( 'theme', $slug );

		if ( null === $downloadUrl || '' === $downloadUrl ) {
			throw new \RuntimeException(
				sprintf( 'No rollback download URL recorded for theme "%s". Trigger an update first.', $slug )
			);
		}

		$upgrader = new \Theme_Upgrader( new \Automatic_Upgrader_Skin() );
		$result   = $upgrader->install( $downloadUrl, [ 'overwrite_package' => true ] );

		if ( is_wp_error( $result ) ) {
			$this->logger->error( 'Theme rollback failed', $result->get_error_message() );
			throw new \RuntimeException(
				sprintf( 'Rollback failed for theme "%s": %s', $slug, $result->get_error_message() )
			);
		}

		$this->rollbackManager->clearPreviousVersion( 'theme', $slug );

		$this->logger->info(
			'Theme rolled back',
			[
				'slug' => $slug,
				'from' => $currentVersion,
				'to'   => $targetVersion,
			]
		);

		return [
			'rolled_back'      => true,
			'slug'             => $slug,
			'previous_version' => $currentVersion,
			'restored_version' => $targetVersion,
			'message'          => sprintf(
				'Theme rolled back from %s to %s.',
				$currentVersion,
				$targetVersion
			),
		];
	}

	/**
	 * Get the current version of a theme.
	 *
	 * @param string $slug Theme slug.
	 * @return string Current version.
	 */
	private function getCurrentVersion( string $slug ): string {
		$theme = wp_get_theme( $slug );
		return $theme->get( 'Version' );
	}

	/**
	 * Include required WordPress upgrader classes if not already loaded.
	 *
	 * @return void
	 */
	private function loadUpgraderDependencies(): void {
		if ( ! function_exists( 'request_filesystem_credentials' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		if ( ! class_exists( \WP_Upgrader::class ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		if ( ! class_exists( \Automatic_Upgrader_Skin::class ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skins.php';
		}
	}
}