File "OnboardingUrl.php"
Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-onboarding/src/Helper/OnboardingUrl.php
File size: 8.2 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Manages an Onboarding Url / Token to preserve /v2/customer/partner-referrals action_url integrity.
*
* @package WooCommerce\PayPalCommerce\Onboarding\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use RuntimeException;
/**
* Class OnboardingUrl
*/
class OnboardingUrl {
/**
* The user ID to associate with the cache key
*
* @var int
*/
private $user_id;
/**
* The cryptographically secure secret
*
* @var ?string
*/
private $secret = null;
/**
* Unix Timestamp when token was generated
*
* @var ?int
*/
private $time = null;
/**
* The "action_url" from /v2/customer/partner-referrals
*
* @var ?string
*/
private $url = null;
/**
* The cache object
*
* @var Cache
*/
private $cache;
/**
* The prefix for the cache key
*
* @var string
*/
private $cache_key_prefix;
/**
* The TTL for the cache.
*
* @var int
*/
private $cache_ttl = MONTH_IN_SECONDS;
/**
* The TTL for the previous token cache.
*
* @var int
*/
private $previous_cache_ttl = 60;
/**
* The constructor
*
* @param Cache $cache The cache object to store the URL.
* @param string $cache_key_prefix The prefix for the cache entry.
* @param int $user_id User ID to associate the link with.
*/
public function __construct(
Cache $cache,
string $cache_key_prefix,
int $user_id
) {
$this->cache = $cache;
$this->cache_key_prefix = $cache_key_prefix;
$this->user_id = $user_id;
}
/**
* Instances the object with a $token.
*
* @param Cache $cache The cache object where the URL is stored.
* @param string $token The token to validate.
* @param int $user_id User ID to associate the link with.
* @return false|self
*/
public static function make_from_token( Cache $cache, string $token, int $user_id ) {
if ( ! $token ) {
return false;
}
$token_data = json_decode( UrlHelper::url_safe_base64_decode( $token ) ?: '', true );
if ( ! $token_data ) {
return false;
}
if ( ! isset( $token_data['u'] ) || ! isset( $token_data['k'] ) ) {
return false;
}
if ( $token_data['u'] !== $user_id ) {
return false;
}
return new self( $cache, $token_data['k'], $token_data['u'] );
}
/**
* Validates the token, if it's valid then delete it.
* If it's invalid don't delete it, to prevent malicious requests from invalidating the token.
*
* @param Cache $cache The cache object where the URL is stored.
* @param string $token The token to validate.
* @param int $user_id User ID to associate the link with.
* @return bool
*/
public static function validate_token_and_delete( Cache $cache, string $token, int $user_id ): bool {
$onboarding_url = self::make_from_token( $cache, $token, $user_id );
if ( $onboarding_url === false ) {
return false;
}
if ( ! $onboarding_url->load() ) {
return false;
}
if ( ( $onboarding_url->token() ?: '' ) !== $token ) {
return false;
}
$onboarding_url->replace_previous_token( $token );
$onboarding_url->delete();
return true;
}
/**
* Validates the token against the previous token.
* Useful to don't throw errors on burst calls to endpoints.
*
* @param Cache $cache The cache object where the URL is stored.
* @param string $token The token to validate.
* @param int $user_id User ID to associate the link with.
* @return bool
*/
public static function validate_previous_token( Cache $cache, string $token, int $user_id ): bool {
$onboarding_url = self::make_from_token( $cache, $token, $user_id );
if ( $onboarding_url === false ) {
return false;
}
return $onboarding_url->check_previous_token( $token );
}
/**
* Load cached data if is valid and initialize object.
*
* @return bool
*/
public function load(): bool {
if ( ! $this->cache->has( $this->cache_key() ) ) {
return false;
}
$cached_data = $this->cache->get( $this->cache_key() );
if ( ! $this->validate_cache_data( $cached_data ) ) {
return false;
}
$this->secret = $cached_data['secret'];
$this->time = $cached_data['time'];
$this->url = $cached_data['url'];
return true;
}
/**
* Initializes the object
*
* @return void
*/
public function init(): void {
try {
$this->secret = bin2hex( random_bytes( 16 ) );
} catch ( \Exception $e ) {
$this->secret = wp_generate_password( 16 );
}
$this->time = time();
$this->url = null;
}
/**
* Validates data from cache
*
* @param array $cache_data The data retrieved from the cache.
* @return bool
*/
private function validate_cache_data( $cache_data ): bool {
if ( ! is_array( $cache_data ) ) {
return false;
}
if (
! ( $cache_data['user_id'] ?? false )
|| ! ( $cache_data['hash_check'] ?? false )
|| ! ( $cache_data['secret'] ?? false )
|| ! ( $cache_data['time'] ?? false )
|| ! ( $cache_data['url'] ?? false )
) {
return false;
}
if ( $cache_data['user_id'] !== $this->user_id ) {
return false;
}
// Detect if salt has changed.
if ( $cache_data['hash_check'] !== wp_hash( '' ) ) {
return false;
}
// If we want we can also validate time for expiration eventually.
return true;
}
/**
* Returns the URL
*
* @return string
* @throws RuntimeException Throws in case the URL isn't initialized.
*/
public function get(): string {
if ( null === $this->url ) {
throw new RuntimeException( 'Object not initialized.' );
}
return $this->url;
}
/**
* Returns the Token
*
* @return string
* @throws RuntimeException Throws in case the object isn't initialized.
*/
public function token(): string {
if (
null === $this->secret
|| null === $this->time
|| null === $this->user_id
) {
throw new RuntimeException( 'Object not initialized.' );
}
// Trim the hash to make sure the token isn't too long.
$hash = substr(
wp_hash(
implode(
'|',
array(
$this->cache_key_prefix,
$this->user_id,
$this->secret,
$this->time,
)
)
),
0,
32
);
$token = wp_json_encode(
array(
'k' => $this->cache_key_prefix,
'u' => $this->user_id,
'h' => $hash,
)
);
if ( ! $token ) {
throw new RuntimeException( 'Unable to generate token.' );
}
return UrlHelper::url_safe_base64_encode( $token );
}
/**
* Sets the URL
*
* @param string $url The URL to store in the cache.
* @return void
*/
public function set( string $url ): void {
$this->url = $url;
}
/**
* Persists the URL and related data in cache
*
* @return void
*/
public function persist(): void {
if (
null === $this->secret
|| null === $this->time
|| null === $this->user_id
|| null === $this->url
) {
return;
}
$this->cache->set(
$this->cache_key(),
array(
'hash_check' => wp_hash( '' ), // To detect if salt has changed.
'secret' => $this->secret,
'time' => $this->time,
'user_id' => $this->user_id,
'url' => $this->url,
),
$this->cache_ttl
);
}
/**
* Deletes the token from cache
*
* @return void
*/
public function delete(): void {
$this->cache->delete( $this->cache_key() );
}
/**
* Returns the compiled cache key
*
* @return string
*/
private function cache_key(): string {
return implode( '_', array( $this->cache_key_prefix, $this->user_id ) );
}
/**
* Returns the compiled cache key of the previous token
*
* @return string
*/
private function previous_cache_key(): string {
return $this->cache_key() . '_previous';
}
/**
* Checks it the previous token matches the token provided.
*
* @param string $previous_token The previous token.
* @return bool
*/
private function check_previous_token( string $previous_token ): bool {
if ( ! $this->cache->has( $this->previous_cache_key() ) ) {
return false;
}
$cached_token = $this->cache->get( $this->previous_cache_key() );
return $cached_token === $previous_token;
}
/**
* Replaces the previous token.
*
* @param string $previous_token The previous token.
* @return void
*/
private function replace_previous_token( string $previous_token ): void {
$this->cache->set(
$this->previous_cache_key(),
$previous_token,
$this->previous_cache_ttl
);
}
}