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.5/src/Support/RollbackManager.php
<?php

declare(strict_types=1);

namespace VPlugins\WPCloudClient\Support;

/**
 * Manages rollback state for plugins and themes.
 *
 * Before each update the current version is persisted in a WordPress option.
 * If the update causes a regression, the trigger action can be called with
 * { "rollback": true } to restore the previous version from wordpress.org.
 */
final class RollbackManager {

	/** WordPress option key that stores the pre-update versions. */
	private const OPTION_KEY = 'wpcc_pre_update_versions';

	/** Legacy option key used before multi-type support. */
	private const LEGACY_OPTION_KEY = 'wpcc_pre_update_version';

	/**
	 * Persist the current version and download URL before an update begins.
	 *
	 * Call this immediately before invoking the WordPress upgrader so that the
	 * version is available for rollback even if the current request ends
	 * abnormally after the files have been replaced.
	 *
	 * @param string      $type        Update type: 'plugin'|'theme'.
	 * @param string|null $slug        Item identifier (slug for plugins/themes, null for core).
	 * @param string|null $version     Version to record. If null, VPLUGINS_WPCC_VERSION is used.
	 * @param string|null $downloadUrl Download URL for reinstalling this version during rollback.
	 * @return void
	 */
	public function recordPreUpdateVersion( string $type = 'plugin', ?string $slug = null, ?string $version = null, ?string $downloadUrl = null ): void {
		$key = $this->buildVersionKey( $type, $slug );

		if ( null === $version ) {
			$version = VPLUGINS_WPCC_VERSION;
		}

		$versions         = (array) get_option( self::OPTION_KEY, [] );
		$versions[ $key ] = [
			'version'      => $version,
			'download_url' => $downloadUrl ?? '',
		];
		update_option( self::OPTION_KEY, $versions, false );
	}

	/**
	 * Return true if a rollback target version has been recorded.
	 *
	 * @param string      $type Update type: 'plugin'|'theme'.
	 * @param string|null $slug Item identifier (slug for plugins/themes, null for core).
	 * @return bool
	 */
	public function canRollback( string $type = 'plugin', ?string $slug = null ): bool {
		return null !== $this->getPreviousVersion( $type, $slug );
	}

	/**
	 * Return the version string that was in place before the last update.
	 *
	 * @param string      $type Update type: 'plugin'|'theme'.
	 * @param string|null $slug Item identifier (slug for plugins/themes, null for core).
	 * @return string|null Version string, or null if no rollback state exists.
	 */
	public function getPreviousVersion( string $type = 'plugin', ?string $slug = null ): ?string {
		$key      = $this->buildVersionKey( $type, $slug );
		$versions = (array) get_option( self::OPTION_KEY, [] );
		$entry    = $versions[ $key ] ?? null;

		if ( null === $entry ) {
			return null;
		}

		// Handle both old (string) and new (array) formats.
		if ( is_array( $entry ) ) {
			$version = (string) ( $entry['version'] ?? '' );
		} else {
			$version = (string) $entry;
		}

		return '' !== $version ? $version : null;
	}

	/**
	 * Return the download URL recorded for rollback.
	 *
	 * @param string      $type Update type: 'plugin'|'theme'.
	 * @param string|null $slug Item identifier (slug for plugins/themes, null for core).
	 * @return string|null Download URL, or null if not available.
	 */
	public function getDownloadUrl( string $type = 'plugin', ?string $slug = null ): ?string {
		$key      = $this->buildVersionKey( $type, $slug );
		$versions = (array) get_option( self::OPTION_KEY, [] );
		$entry    = $versions[ $key ] ?? null;

		if ( ! is_array( $entry ) ) {
			return null;
		}

		$url = (string) ( $entry['download_url'] ?? '' );
		return '' !== $url ? $url : null;
	}

	/**
	 * Remove the recorded pre-update version.
	 *
	 * Call this after a successful rollback or when the recorded version is no
	 * longer needed (e.g., after confirming a new deployment is stable).
	 *
	 * @param string      $type Update type: 'plugin'|'theme'.
	 * @param string|null $slug Item identifier (slug for plugins/themes, null for core).
	 * @return void
	 */
	public function clearPreviousVersion( string $type = 'plugin', ?string $slug = null ): void {
		$key      = $this->buildVersionKey( $type, $slug );
		$versions = (array) get_option( self::OPTION_KEY, [] );
		unset( $versions[ $key ] );
		if ( empty( $versions ) ) {
			delete_option( self::OPTION_KEY );
		} else {
			update_option( self::OPTION_KEY, $versions, false );
		}
	}

	/**
	 * Migrate data from the legacy single-value option key.
	 *
	 * Early versions stored a single version string under 'wpcc_pre_update_version'.
	 * This method migrates that data to the new multi-type format and removes the old key.
	 *
	 * @return void
	 */
	public function migrateFromLegacyKey(): void {
		$old = get_option( self::LEGACY_OPTION_KEY );
		if ( false === $old ) {
			return;
		}

		$versions = (array) get_option( self::OPTION_KEY, [] );

		if ( is_array( $old ) ) {
			$versions = array_merge( $old, $versions );
		} else {
			$versions['plugin'] = [
				'version'      => (string) $old,
				'download_url' => '',
			];
		}

		update_option( self::OPTION_KEY, $versions, false );
		delete_option( self::LEGACY_OPTION_KEY );
	}

	/**
	 * Build a unique key for storing version state.
	 *
	 * @param string      $type Update type.
	 * @param string|null $slug Item identifier.
	 * @return string Unique key for storage.
	 */
	private function buildVersionKey( string $type, ?string $slug ): string {
		if ( null === $slug || '' === $slug ) {
			return $type;
		}
		return "{$type}:{$slug}";
	}
}