<?php
/**
 * Plugin rollback — reinstall a previously released Blocksify version from
 * the WordPress.org plugin repository.
 *
 * Blocksify ships from wp.org, so every released version is permanently
 * downloadable from the trusted wp.org CDN at
 * `https://downloads.wordpress.org/plugin/blocksify.{version}.zip`. Rolling
 * back is therefore "install an older, already-published package over the
 * current one" — no custom endpoint, no untrusted source, no bypass of
 * WordPress's own update/install flow.
 *
 * Mechanism (the only reliable way to make core install a *lower* version):
 *   1. The version list comes from `plugins_api()->versions` — the same map
 *      WordPress itself reads — filtered to stable tags strictly below the
 *      running version.
 *   2. On confirm, inject the chosen version as the pending update in the
 *      `update_plugins` site transient, then run `Plugin_Upgrader::upgrade()`.
 *      Core's upgrader downloads from wp.org, unpacks, deactivates/reactivates
 *      and (crucially) renders the WP_Filesystem credentials form itself when
 *      the host isn't direct-write — which is why this runs as a full-page
 *      `admin-post` flow rather than a silent REST call.
 *
 * Surface:
 *   - `Blocksify_Rollback::get_versions()`  → string[] of roll-back-able tags.
 *   - `Blocksify_Rollback::action_url()`    → nonce'd admin-post base URL.
 *   - admin_post_blocksify_rollback         → performs the rollback.
 *
 * NOTE: deliberately carries none of the beta-channel interference guards a
 * beta-updater would need (forced-rollback flags, stale-state cleanup). Beta
 * updates are not shipped; after a rollback WordPress will simply offer the
 * newer stable again on the next update check, which is the correct behavior.
 *
 * @package Blocksify
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Rollback handler. Instance-less; methods are static.
 */
class Blocksify_Rollback {

	const ACTION       = 'blocksify_rollback';
	const SLUG         = 'blocksify';
	const MAX_VERSIONS = 10;

	/**
	 * Wire the admin-post handler. Called once from blocksify.php.
	 */
	public static function boot() {
		add_action( 'admin_post_' . self::ACTION, array( __CLASS__, 'handle_rollback' ) );
	}

	/**
	 * Capability required to perform a rollback. Mirrors the gate WordPress
	 * uses for its own plugin-update screen — `update_plugins` is held by
	 * single-site admins and multisite super-admins only.
	 *
	 * @return string
	 */
	public static function capability() {
		return 'update_plugins';
	}

	/**
	 * Transient key for the cached version list, namespaced by the running
	 * version so a plugin update naturally invalidates the cache.
	 *
	 * @return string
	 */
	private static function versions_transient_key() {
		return 'blocksify_rollback_versions_' . ( defined( 'BLOCKSIFY_VERSION' ) ? BLOCKSIFY_VERSION : '0' );
	}

	/**
	 * Roll-back-able versions: stable tags published on wp.org strictly below
	 * the running version, newest first, capped at MAX_VERSIONS.
	 *
	 * Pulls from `plugins_api()->versions` (cached a week) and drops
	 * pre-release / non-stable tags (trunk, alpha, beta, rc, dev) so the user
	 * only ever rolls back to a real release.
	 *
	 * @return string[] Version strings (may be empty — e.g. when only the
	 *                  current version has ever shipped).
	 */
	public static function get_versions() {
		$cache_key = self::versions_transient_key();
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			return $cached;
		}

		if ( ! function_exists( 'plugins_api' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
		}

		$information = plugins_api(
			'plugin_information',
			array(
				'slug'   => self::SLUG,
				'fields' => array( 'versions' => true ),
			)
		);

		if ( is_wp_error( $information ) || empty( $information->versions ) || ! is_array( $information->versions ) ) {
			// Cache the empty result briefly so a wp.org hiccup doesn't hammer
			// the API on every panel open, but recover within the hour.
			set_transient( $cache_key, array(), HOUR_IN_SECONDS );
			return array();
		}

		$current  = defined( 'BLOCKSIFY_VERSION' ) ? BLOCKSIFY_VERSION : '0';
		$versions = array();

		foreach ( array_keys( $information->versions ) as $version ) {
			$version = (string) $version;

			// Skip non-stable / non-numeric tags (trunk, x.y.z-beta1, rc, …).
			if ( preg_match( '/(trunk|alpha|beta|rc|dev)/i', $version ) ) {
				continue;
			}
			// Only offer versions strictly OLDER than what's installed.
			if ( version_compare( $version, $current, '>=' ) ) {
				continue;
			}
			$versions[] = $version;
		}

		usort(
			$versions,
			static function ( $a, $b ) {
				return version_compare( $b, $a );
			}
		);

		$versions = array_slice( $versions, 0, self::MAX_VERSIONS );

		set_transient( $cache_key, $versions, WEEK_IN_SECONDS );

		return $versions;
	}

	/**
	 * Nonce'd base URL for the rollback admin-post action. The caller appends
	 * `&version={tag}`; the handler re-validates that tag against
	 * get_versions(), so a tampered query string can only ever select a real,
	 * older, wp.org-published release.
	 *
	 * Built with esc_url_raw (NOT wp_nonce_url, which esc_html()s the URL into
	 * `&amp;`): this string is consumed by JS as `window.location.href`, where
	 * an encoded `&amp;` would be parsed as a literal `amp;_wpnonce` param and
	 * break the nonce check. esc_url_raw keeps ampersands raw.
	 *
	 * @return string
	 */
	public static function action_url() {
		return esc_url_raw(
			add_query_arg(
				array(
					'action'   => self::ACTION,
					'_wpnonce' => wp_create_nonce( self::ACTION ),
				),
				admin_url( 'admin-post.php' )
			)
		);
	}

	/**
	 * admin_post_blocksify_rollback handler. Capability + nonce + allow-list
	 * gated, then hands off to core's Plugin_Upgrader (which renders its own
	 * progress UI and, when needed, the WP_Filesystem credentials form).
	 *
	 * @return void
	 */
	public static function handle_rollback() {
		if ( ! current_user_can( self::capability() ) ) {
			wp_die(
				esc_html__( 'You do not have permission to roll back Blocksify.', 'blocksify' ),
				esc_html__( 'Roll back Blocksify', 'blocksify' ),
				array( 'response' => 403 )
			);
		}

		check_admin_referer( self::ACTION );

		$version  = isset( $_GET['version'] ) ? sanitize_text_field( wp_unslash( $_GET['version'] ) ) : '';
		$versions = self::get_versions();

		if ( '' === $version || ! in_array( $version, $versions, true ) ) {
			wp_die(
				esc_html__( 'The selected version is not available to roll back to. Try selecting a different version.', 'blocksify' ),
				esc_html__( 'Roll back Blocksify', 'blocksify' ),
				array( 'response' => 400 )
			);
		}

		self::run( $version );
	}

	/**
	 * Inject the chosen version as the pending update, then run core's
	 * upgrader against it. Ends the request — the upgrader skin owns the
	 * output from here.
	 *
	 * @param string $version Validated, wp.org-published tag below the current version.
	 * @return void
	 */
	private static function run( $version ) {
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

		$basename = plugin_basename( BLOCKSIFY_FILE ); // e.g. blocksify/blocksify.php.
		$package  = sprintf( 'https://downloads.wordpress.org/plugin/%s.%s.zip', self::SLUG, $version );

		// Pre-seed the update transient so Plugin_Upgrader::upgrade() has a
		// package to install. WordPress never lists a LOWER version as an
		// available update, so without this the upgrader has nothing to do.
		$transient = get_site_transient( 'update_plugins' );
		if ( ! is_object( $transient ) ) {
			$transient = new stdClass();
		}
		if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
			$transient->response = array();
		}

		$offer              = new stdClass();
		$offer->slug        = self::SLUG;
		$offer->plugin      = $basename;
		$offer->new_version = $version;
		$offer->package     = $package;
		$offer->url         = 'https://wordpress.org/plugins/' . self::SLUG . '/';

		$transient->response[ $basename ] = $offer;
		set_site_transient( 'update_plugins', $transient );

		$skin = new Plugin_Upgrader_Skin(
			array(
				'title'  => __( 'Roll back Blocksify', 'blocksify' ),
				'plugin' => $basename,
				'nonce'  => 'upgrade-plugin_' . $basename,
				'url'    => 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $basename ),
			)
		);

		$upgrader = new Plugin_Upgrader( $skin );
		$upgrader->upgrade( $basename );

		exit;
	}
}
