<?php
/**
 * Application-level package authenticity for WP PowerSuite updates.
 *
 * Server payload (on update metadata object `data`):
 * - package_sha256: hex SHA-256 of the release ZIP bytes
 * - package_signature: base64 RSA-SHA256 signature of the lowercase hex digest string
 *
 * Plugin ships only the public key. Private key stays on the license/update server
 * (see docs/UPDATE-PACKAGE-SIGNING.md — not shipped in the customer ZIP).
 *
 * @package WP_PowerSuite
 */

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

final class WPPS_Package_Integrity {

	/**
	 * Default RSA public key (PEM). Override with WP_POWERSUITE_UPDATE_PUBLIC_KEY.
	 */
	private const DEFAULT_PUBLIC_KEY = <<<'PEM'
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArBdMGeaQ7vAuMrmmkPxM
pG7TG3P0LprUaIoDeFyRih51oLp2aaFOcCv6jnXNbwITBzmPv6rpzNfahfdpEMHg
r8DgLXjngxneuBGFpF2fjUZ062FfxywHabRa8UMKjSBzzhsmyVyqB2HSHx9a+Dja
rLXsID2XKYF7BPjYyVTdpcjN5JnhKr7r6FHLX+osxuZjHazbQXejdCXZ4WcDvOP/
HEwKwILaeHYyvP1SRZt0Sa+UQ3K7UpQjU+IttMtc2sC48bHtS7KVwcCe/YF135N3
gMlp4jDANGi1mABE8/WQJEcfqAanKw9wnRKN4PZfc1TnYw4LGAMXhDI/G0avQDW0
2wIDAQAB
-----END PUBLIC KEY-----
PEM;

	/**
	 * When true, installs fail closed unless both SHA-256 and signature verify.
	 */
	public static function signatures_required(): bool {
		if ( defined( 'WP_POWERSUITE_REQUIRE_PACKAGE_SIGNATURE' ) ) {
			return (bool) WP_POWERSUITE_REQUIRE_PACKAGE_SIGNATURE;
		}

		/**
		 * @param bool $required Whether signatures are mandatory for every install.
		 */
		return (bool) apply_filters( 'wp_powersuite_require_package_signature', false );
	}

	public static function get_public_key(): string {
		if ( defined( 'WP_POWERSUITE_UPDATE_PUBLIC_KEY' ) && is_string( WP_POWERSUITE_UPDATE_PUBLIC_KEY ) && WP_POWERSUITE_UPDATE_PUBLIC_KEY !== '' ) {
			return trim( WP_POWERSUITE_UPDATE_PUBLIC_KEY );
		}

		/**
		 * @param string $pem Public key PEM.
		 */
		$filtered = apply_filters( 'wp_powersuite_update_public_key', self::DEFAULT_PUBLIC_KEY );

		return is_string( $filtered ) ? trim( $filtered ) : '';
	}

	/**
	 * Attach integrity fields from update API payload onto the update object.
	 *
	 * @param object $update Update metadata.
	 * @param object $data   Raw API data object.
	 */
	public static function attach_from_update_data( object $update, object $data ): object {
		if ( ! empty( $data->package_sha256 ) ) {
			$update->package_sha256 = strtolower( (string) preg_replace( '/[^a-f0-9]/i', '', (string) $data->package_sha256 ) );
		}
		if ( ! empty( $data->package_signature ) ) {
			$update->package_signature = (string) $data->package_signature;
		}

		return $update;
	}

	/**
	 * Verify a downloaded package file against expected digest + signature.
	 *
	 * @return true|\WP_Error
	 */
	public static function verify_package_file( string $file, string $expected_sha256, string $signature_b64 ) {
		if ( ! is_readable( $file ) ) {
			return new \WP_Error( 'wpps_package_unreadable', __( 'Update package could not be read for integrity verification.', 'wp-powersuite' ) );
		}

		$expected_sha256 = strtolower( (string) preg_replace( '/[^a-f0-9]/i', '', $expected_sha256 ) );
		$required        = self::signatures_required();

		if ( strlen( $expected_sha256 ) !== 64 ) {
			if ( $required ) {
				return new \WP_Error( 'wpps_package_hash_missing', __( 'Update package SHA-256 is missing or invalid.', 'wp-powersuite' ) );
			}

			return true;
		}

		$hash = hash_file( 'sha256', $file );
		if ( ! is_string( $hash ) || ! hash_equals( $expected_sha256, strtolower( $hash ) ) ) {
			return new \WP_Error( 'wpps_package_hash_mismatch', __( 'Update package failed SHA-256 verification.', 'wp-powersuite' ) );
		}

		if ( $signature_b64 === '' ) {
			if ( $required ) {
				return new \WP_Error( 'wpps_package_signature_missing', __( 'Update package signature is missing.', 'wp-powersuite' ) );
			}

			return true;
		}

		return self::verify_signature( $expected_sha256, $signature_b64 );
	}

	/**
	 * @return true|\WP_Error
	 */
	public static function verify_signature( string $sha256_hex, string $signature_b64 ) {
		$public = self::get_public_key();
		if ( $public === '' ) {
			return new \WP_Error( 'wpps_package_pubkey_missing', __( 'Update public key is not configured.', 'wp-powersuite' ) );
		}
		if ( ! function_exists( 'openssl_verify' ) ) {
			return new \WP_Error( 'wpps_package_openssl_missing', __( 'OpenSSL is required to verify update signatures.', 'wp-powersuite' ) );
		}

		$sig = base64_decode( $signature_b64, true );
		if ( ! is_string( $sig ) || $sig === '' ) {
			return new \WP_Error( 'wpps_package_signature_invalid', __( 'Update package signature encoding is invalid.', 'wp-powersuite' ) );
		}

		$key = openssl_pkey_get_public( $public );
		if ( $key === false ) {
			return new \WP_Error( 'wpps_package_pubkey_invalid', __( 'Update public key is invalid.', 'wp-powersuite' ) );
		}

		$ok = openssl_verify( strtolower( $sha256_hex ), $sig, $key, OPENSSL_ALGO_SHA256 );
		if ( $ok !== 1 ) {
			return new \WP_Error( 'wpps_package_signature_mismatch', __( 'Update package signature verification failed.', 'wp-powersuite' ) );
		}

		return true;
	}

	/**
	 * Download a trusted package with TLS verification forced, then verify integrity.
	 *
	 * @param bool|\WP_Error $reply
	 * @param string         $package
	 * @param \WP_Upgrader   $upgrader
	 * @return bool|\WP_Error|string
	 */
	public static function download_and_verify( $reply, string $package, $upgrader ) {
		if ( $reply !== false ) {
			return $reply;
		}
		if ( ! class_exists( 'WPPS_License_Updater', false ) ) {
			return $reply;
		}
		if ( ! WPPS_License_Updater::is_trusted_package_url( $package ) ) {
			return $reply;
		}
		if ( ! class_exists( 'WPPS_License_Updates', false ) || ! WPPS_License_Updates::is_wpps_update_package( $package, $upgrader ) ) {
			return $reply;
		}

		$plugin_file = defined( 'WP_POWERSUITE_PLUGIN_FILE' ) ? plugin_basename( WP_POWERSUITE_PLUGIN_FILE ) : '';
		$meta        = self::get_pending_integrity_meta( $plugin_file );
		$sha         = (string) ( $meta['package_sha256'] ?? '' );
		$sig         = (string) ( $meta['package_signature'] ?? '' );

		if ( $sha === '' && $sig === '' && ! self::signatures_required() ) {
			return $reply;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';

		// Force TLS verification — never honor WP_POWERSUITE_LICENSE_ALLOW_INSECURE_SSL here.
		add_filter( 'https_local_ssl_verify', '__return_true', 999 );
		add_filter( 'https_ssl_verify', '__return_true', 999 );
		$tmp = download_url( $package, 300 );
		remove_filter( 'https_local_ssl_verify', '__return_true', 999 );
		remove_filter( 'https_ssl_verify', '__return_true', 999 );

		if ( is_wp_error( $tmp ) ) {
			return $tmp;
		}

		$verified = self::verify_package_file( (string) $tmp, $sha, $sig );
		if ( is_wp_error( $verified ) ) {
			// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			@unlink( (string) $tmp );

			return $verified;
		}

		return (string) $tmp;
	}

	/**
	 * @return array{package_sha256?:string,package_signature?:string}
	 */
	public static function get_pending_integrity_meta( string $plugin_file ): array {
		$transient = get_site_transient( 'update_plugins' );
		if ( ! is_object( $transient ) || empty( $transient->response[ $plugin_file ] ) || ! is_object( $transient->response[ $plugin_file ] ) ) {
			return [];
		}
		$update = $transient->response[ $plugin_file ];

		return [
			'package_sha256'    => ! empty( $update->package_sha256 ) ? (string) $update->package_sha256 : '',
			'package_signature' => ! empty( $update->package_signature ) ? (string) $update->package_signature : '',
		];
	}
}
