<?php

namespace Automattic\WooCommerce\StoreApi\Utilities;

/**
 * JsonWebToken class.
 *
 * Simple Json Web Token generator & verifier static utility class, currently supporting only HS256 signatures.
 */
final class JsonWebToken {

	/**
	 * JWT header type.
	 *
	 * @var string
	 */
	private static $type = 'JWT';

	/**
	 * JWT algorithm to generate signature.
	 *
	 * @var string
	 */
	private static $algorithm = 'HS256';

	/**
	 * Generates a token from provided data and secret.
	 *
	 * @param array  $payload Payload data.
	 * @param string $secret The secret used to generate the signature.
	 *
	 * @return string
	 */
	public static function create( array $payload, string $secret ) {
		$header    = self::to_base_64_url( self::generate_header() );
		$payload   = self::to_base_64_url( self::generate_payload( $payload ) );
		$signature = self::to_base_64_url( self::generate_signature( $header . '.' . $payload, $secret ) );

		return $header . '.' . $payload . '.' . $signature;
	}

	/**
	 * Validates a provided token against the provided secret.
	 * Checks for format, valid header for our class, expiration claim validity and signature.
	 * https://datatracker.ietf.org/doc/html/rfc7519#section-7.2
	 *
	 * @param string $token Full token string.
	 * @param string $secret The secret used to generate the signature.
	 *
	 * @return bool
	 */
	public static function validate( string $token, string $secret ) {
		if ( ! $token ) {
			return false;
		}

		/**
		 * Confirm the structure of a JSON Web Token, it has three parts separated
		 * by dots and complies with Base64URL standards.
		 */
		if ( preg_match( '/^[a-zA-Z\d\-_=]+\.[a-zA-Z\d\-_=]+\.[a-zA-Z\d\-_=]+$/', $token ) !== 1 ) {
			return false;
		}

		$parts = self::get_parts( $token );

		/**
		 * Check if header declares a supported JWT by this class.
		 */
		if (
			! is_object( $parts->header ) ||
			! property_exists( $parts->header, 'typ' ) ||
			! property_exists( $parts->header, 'alg' ) ||
			self::$type !== $parts->header->typ ||
			self::$algorithm !== $parts->header->alg
		) {
			return false;
		}

		/**
		 * Check if token is expired.
		 */
		if ( ! property_exists( $parts->payload, 'exp' ) || time() > (int) $parts->payload->exp ) {
			return false;
		}

		/**
		 * Check if the token is based on our secret.
		 */
		$encoded_regenerated_signature = self::to_base_64_url(
			self::generate_signature( $parts->header_encoded . '.' . $parts->payload_encoded, $secret )
		);

		return hash_equals( $encoded_regenerated_signature, $parts->signature_encoded );
	}

	/**
	 * Returns the decoded/encoded header, payload and signature from a token string.
	 *
	 * @param string $token Full token string.
	 *
	 * @return object
	 */
	public static function get_parts( string $token ) {
		$parts = explode( '.', $token );

		return (object) array(
			'header'            => json_decode( self::from_base_64_url( $parts[0] ) ),
			'header_encoded'    => $parts[0],
			'payload'           => json_decode( self::from_base_64_url( $parts[1] ) ),
			'payload_encoded'   => $parts[1],
			'signature'         => self::from_base_64_url( $parts[2] ),
			'signature_encoded' => $parts[2],

		);
	}

	/**
	 * Generates the json formatted header for our HS256 JWT token.
	 *
	 * @return string|bool
	 */
	private static function generate_header() {
		return wp_json_encode(
			array(
				'alg' => self::$algorithm,
				'typ' => self::$type,
			)
		);
	}

	/**
	 * Generates a sha256 signature for the provided string using the provided secret.
	 *
	 * @param string $string Header + Payload token substring.
	 * @param string $secret The secret used to generate the signature.
	 *
	 * @return false|string
	 */
	private static function generate_signature( string $string, string $secret ) {
		return hash_hmac(
			'sha256',
			$string,
			$secret,
			true
		);
	}

	/**
	 * Generates the payload in json formatted string.
	 *
	 * @param array $payload Payload data.
	 *
	 * @return string|bool
	 */
	private static function generate_payload( array $payload ) {
		return wp_json_encode( array_merge( $payload, [ 'iat' => time() ] ) );
	}

	/**
	 * Encodes a string to url safe base64.
	 *
	 * @param string $string The string to be encoded.
	 *
	 * @return string
	 */
	private static function to_base_64_url( string $string ) {
		return str_replace(
			array( '+', '/', '=' ),
			array( '-', '_', '' ),
			base64_encode( $string ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		);
	}

	/**
	 * Decodes a string encoded using url safe base64, supporting auto padding.
	 *
	 * @param string $string the string to be decoded.
	 *
	 * @return string
	 */
	private static function from_base_64_url( string $string ) {
		/**
		 * Add padding to base64 strings which require it. Some base64 URL strings
		 * which are decoded will have missing padding which is represented by the
		 * equals sign.
		 */
		if ( strlen( $string ) % 4 !== 0 ) {
			return self::from_base_64_url( $string . '=' );
		}

		return base64_decode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
			str_replace(
				array( '-', '_' ),
				array( '+', '/' ),
				$string
			)
		);
	}
}
