Files
wp-graphql-woocommerce/includes/utils/class-ql-session-handler.php
T

507 lines
14 KiB
PHP
Raw Permalink Normal View History

<?php
/**
* Handles data for the current customers session.
*
2019-10-25 19:13:36 -04:00
* @package WPGraphQL\WooCommerce\Utils
* @since 0.1.2
*/
2019-10-25 19:13:36 -04:00
namespace WPGraphQL\WooCommerce\Utils;
2019-11-18 16:18:13 -05:00
use Firebase\JWT\JWT;
2022-06-24 17:42:05 -04:00
use Firebase\JWT\Key;
use GraphQL\Error\UserError;
use WC_Session_Handler;
2019-08-27 11:57:52 -04:00
/**
* Class - QL_Session_Handler
2023-06-13 23:17:02 +03:00
*
* @property int $_session_expiring
* @property int $_session_expiration
* @property int|string $_customer_id
*/
class QL_Session_Handler extends WC_Session_Handler {
/**
2019-11-18 16:18:13 -05:00
* Stores the name of the HTTP header used to pass the session token.
*
* @var string $_token
2019-06-20 14:14:13 -04:00
*/
protected $_token; // @codingStandardsIgnoreLine
2019-06-20 14:14:13 -04:00
2019-11-18 16:18:13 -05:00
/**
* Stores Timestamp of when the session token was issued.
*
2023-06-13 23:17:02 +03:00
* @var float $_session_issued
2019-11-18 16:18:13 -05:00
*/
protected $_session_issued; // @codingStandardsIgnoreLine
2019-06-20 14:14:13 -04:00
2019-11-18 16:18:13 -05:00
/**
* True when the token exists.
*
* @var bool $_has_token
2019-11-18 16:18:13 -05:00
*/
protected $_has_token = false; // @codingStandardsIgnoreLine
2019-06-20 14:14:13 -04:00
2019-11-18 16:18:13 -05:00
/**
2019-11-25 17:02:50 -05:00
* True when a new session token has been issued.
2019-11-18 16:18:13 -05:00
*
2019-11-25 17:02:50 -05:00
* @var bool $_issuing_new_token
2019-11-18 16:18:13 -05:00
*/
2019-11-25 17:02:50 -05:00
protected $_issuing_new_token = false; // @codingStandardsIgnoreLine
2019-11-18 16:18:13 -05:00
/**
* Constructor for the session class.
*/
public function __construct() {
$this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' );
2019-11-18 16:18:13 -05:00
$this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
2019-06-20 14:14:13 -04:00
}
/**
* Returns formatted $_SERVER index from provided string.
*
2019-11-18 16:18:13 -05:00
* @param string $header String to be formatted.
2019-06-20 14:14:13 -04:00
*
* @return string
*/
2019-11-18 16:18:13 -05:00
private function get_server_key( $header = null ) {
2023-06-13 23:17:02 +03:00
/**
* Server key.
*
* @var string $server_key
*/
$server_key = preg_replace( '#[^A-z0-9]#', '_', ! empty( $header ) ? $header : $this->_token );
return null !== $server_key
? 'HTTP_' . strtoupper( $server_key )
: '';
2019-11-18 16:18:13 -05:00
}
/**
* This returns the secret key, using the defined constant if defined, and passing it through a filter to
* allow for the config to be able to be set via another method other than a defined constant, such as an
* admin UI that allows the key to be updated/changed/revoked at any time without touching server files
*
* @return mixed|null|string
*/
private function get_secret_key() {
// Use the defined secret key, if it exists.
2019-11-18 16:18:13 -05:00
$secret_key = defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) && ! empty( GRAPHQL_WOOCOMMERCE_SECRET_KEY )
? GRAPHQL_WOOCOMMERCE_SECRET_KEY :
'graphql-woo-cart-session';
2019-11-18 16:18:13 -05:00
return apply_filters( 'graphql_woocommerce_secret_key', $secret_key );
}
/**
* Init hooks and session data.
2023-06-13 23:17:02 +03:00
*
* @return void
2019-11-18 16:18:13 -05:00
*/
public function init() {
$this->init_session_token();
2023-06-13 23:17:02 +03:00
Session_Transaction_Manager::get( $this );
2019-11-18 16:18:13 -05:00
2023-06-13 23:17:02 +03:00
/**
* Necessary since Session_Transaction_Manager applies to the reference.
*
* @var self $this
*/
add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 );
add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 );
add_action( 'graphql_after_resolve_field', [ $this, 'save_if_dirty' ], 10, 4 );
add_action( 'shutdown', [ $this, 'save_data' ] );
add_action( 'wp_logout', [ $this, 'destroy_session' ] );
2019-11-18 16:18:13 -05:00
if ( ! is_user_logged_in() ) {
add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 );
2019-11-18 16:18:13 -05:00
}
}
/**
* Setup token and customer ID.
*
* @throws UserError Invalid token.
2023-06-13 23:17:02 +03:00
*
* @return void
2019-11-18 16:18:13 -05:00
*/
public function init_session_token() {
$token = $this->get_session_token();
// Process existing session if not expired or invalid.
2023-06-13 23:17:02 +03:00
if ( $token && is_object( $token ) && ! is_wp_error( $token ) ) {
$this->_customer_id = $token->data->customer_id;
2019-11-18 16:18:13 -05:00
$this->_session_issued = $token->iat;
$this->_session_expiration = $token->exp;
2019-11-24 23:54:50 -05:00
$this->_session_expiring = $token->exp - ( 3600 );
2019-11-18 16:18:13 -05:00
$this->_has_token = true;
$this->_data = $this->get_session_data();
// If the user logs in, update session.
if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) {
$guest_session_id = $this->_customer_id;
$this->_customer_id = strval( get_current_user_id() );
$this->_dirty = true;
2020-10-01 01:45:26 -04:00
// If session empty check for previous data associated with customer and assign that to the session.
if ( empty( $this->_data ) ) {
$this->_data = $this->get_session_data();
}
2023-06-13 23:17:02 +03:00
// @phpstan-ignore-next-line
2019-11-18 16:18:13 -05:00
$this->save_data( $guest_session_id );
2019-11-24 23:54:50 -05:00
$this->set_customer_session_token( true );
2019-11-18 16:18:13 -05:00
}
// Update session expiration on each action.
$this->set_session_expiration();
if ( $token->exp < $this->_session_expiration ) {
2023-06-13 23:17:02 +03:00
$this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration );
2019-11-18 16:18:13 -05:00
}
} else {
2020-10-01 01:45:26 -04:00
// If token invalid throw warning.
if ( is_wp_error( $token ) ) {
2019-11-25 20:01:35 -05:00
add_filter(
'graphql_woocommerce_session_token_errors',
2019-11-25 20:01:35 -05:00
function( $errors ) use ( $token ) {
$errors = $token->get_error_message();
return $errors;
}
);
}
2020-10-01 01:45:26 -04:00
// Start new session.
2019-11-18 16:18:13 -05:00
$this->set_session_expiration();
2020-10-01 01:45:26 -04:00
// Get Customer ID.
$this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
2019-11-18 16:18:13 -05:00
$this->_data = $this->get_session_data();
2020-10-01 01:45:26 -04:00
$this->set_customer_session_token( true );
}//end if
2019-11-18 16:18:13 -05:00
}
/**
* Retrieve and decrypt the session data from session, if set. Otherwise return false.
*
* Session cookies without a customer ID are invalid.
*
* @throws \Exception Invalid token.
2023-06-13 23:17:02 +03:00
* @return false|\WP_Error|object{ iat: int, exp: int, data: object{ customer_id: string } }
2019-11-18 16:18:13 -05:00
*/
public function get_session_token() {
// Get the Auth header.
$session_header = $this->get_session_header();
if ( empty( $session_header ) ) {
return false;
}
2023-06-13 23:17:02 +03:00
// Get the token from the header.
$token_string = sscanf( $session_header, 'Session %s' );
if ( empty( $token_string ) ) {
return false;
}
list( $token ) = $token_string;
2019-11-18 16:18:13 -05:00
/**
* Try to decode the token
*/
try {
JWT::$leeway = 60;
$secret = $this->get_secret_key();
2022-06-24 17:42:05 -04:00
$key = new Key( $secret, 'HS256' );
2023-06-13 23:17:02 +03:00
/**
* Decode the token
*
* @var null|object{ iat: int, exp: int, data: object{ customer_id: string }, iss: string } $token
*/
$token = ! empty( $token ) ? JWT::decode( $token, $key ) : null;
// Check if token was successful decoded.
if ( ! $token ) {
throw new \Exception( __( 'Failed to decode session token', 'wp-graphql-woocommerce' ) );
}
2019-11-18 16:18:13 -05:00
// The Token is decoded now validate the iss.
2019-11-25 20:01:35 -05:00
if ( empty( $token->iss ) || get_bloginfo( 'url' ) !== $token->iss ) {
2019-11-18 16:18:13 -05:00
throw new \Exception( __( 'The iss do not match with this server', 'wp-graphql-woocommerce' ) );
}
// Validate the customer id in the token.
2019-11-25 20:01:35 -05:00
if ( empty( $token->data ) || empty( $token->data->customer_id ) ) {
2019-11-18 16:18:13 -05:00
throw new \Exception( __( 'Customer ID not found in the token', 'wp-graphql-woocommerce' ) );
}
} catch ( \Exception $error ) {
return new \WP_Error( 'invalid_token', $error->getMessage() );
}//end try
2019-11-18 16:18:13 -05:00
return $token;
}
/**
* Get the value of the cart session header from the $_SERVER super global
*
* @return mixed|string
*/
public function get_session_header() {
$session_header_key = $this->get_server_key();
// Looking for the cart session header.
$session_header = isset( $_SERVER[ $session_header_key ] )
? $_SERVER[ $session_header_key ] //@codingStandardsIgnoreLine
2019-11-18 16:18:13 -05:00
: false;
/**
* Return the cart session header, passed through a filter
*
* @param string $session_header The header used to identify a user's cart session token.
*/
return apply_filters( 'graphql_woocommerce_cart_session_header', $session_header );
2019-06-20 14:14:13 -04:00
}
/**
2021-02-02 10:29:37 -05:00
* Creates JSON Web Token for customer session.
*
2023-06-13 23:17:02 +03:00
* @return false|string
2021-02-02 10:29:37 -05:00
*/
public function build_token() {
if ( empty( $this->_session_issued ) ) {
return false;
}
2021-02-02 10:29:37 -05:00
/**
* Determine the "not before" value for use in the token
*
2023-06-13 23:17:02 +03:00
* @param float $issued The timestamp of token was issued.
* @param int|string $customer_id Customer ID.
* @param array $session_data Cart session data.
2021-02-02 10:29:37 -05:00
*/
$not_before = apply_filters(
'graphql_woo_cart_session_not_before',
$this->_session_issued,
$this->_customer_id,
$this->_data
);
// Configure the token array, which will be encoded.
$token = [
2021-02-02 10:29:37 -05:00
'iss' => get_bloginfo( 'url' ),
'iat' => $this->_session_issued,
'nbf' => $not_before,
'exp' => $this->_session_expiration,
'data' => [
2021-02-02 10:29:37 -05:00
'customer_id' => $this->_customer_id,
],
];
2021-02-02 10:29:37 -05:00
/**
* Filter the token, allowing for individual systems to configure the token as needed
*
2023-06-13 23:17:02 +03:00
* @param array $token The token array that will be encoded
* @param int|string $customer_id ID of customer associated with token.
* @param array $session_data Session data associated with token.
2021-02-02 10:29:37 -05:00
*/
$token = apply_filters(
'graphql_woocommerce_cart_session_before_token_sign',
$token,
$this->_customer_id,
$this->_data
);
// Encode the token.
JWT::$leeway = 60;
$token = JWT::encode( $token, $this->get_secret_key(), 'HS256' );
/**
* Filter the token before returning it, allowing for individual systems to override what's returned.
*
* For example, if the user should not be granted a token for whatever reason, a filter could have the token return null.
*
2023-06-13 23:17:02 +03:00
* @param string $token The signed JWT token that will be returned
* @param int|string $customer_id ID of customer associated with token.
* @param array $session_data Session data associated with token.
2021-02-02 10:29:37 -05:00
*/
$token = apply_filters(
'graphql_woocommerce_cart_session_signed_token',
$token,
$this->_customer_id,
$this->_data
);
return $token;
}
/**
* Sets the session header on-demand (usually after adding an item to the cart).
2019-06-20 14:14:13 -04:00
*
* Warning: Headers will only be set if this is called before the headers are sent.
*
* @param bool $set Should the session cookie be set.
2023-06-13 23:17:02 +03:00
*
* @return void
*/
2019-11-18 16:18:13 -05:00
public function set_customer_session_token( $set ) {
if ( ! empty( $this->_session_issued ) && $set ) {
2019-11-18 16:18:13 -05:00
/**
2021-02-02 10:29:37 -05:00
* Set callback session token for use in the HTTP response header and customer/user "sessionToken" field.
2019-11-25 17:02:50 -05:00
*/
add_filter(
'graphql_response_headers_to_send',
2021-02-02 10:29:37 -05:00
function( $headers ) {
$token = $this->build_token();
if ( $token ) {
$headers[ $this->_token ] = $token;
}
2019-11-25 17:02:50 -05:00
return $headers;
},
10
);
2019-11-25 17:02:50 -05:00
$this->_issuing_new_token = true;
2019-11-18 16:18:13 -05:00
}
}
/**
2019-06-20 14:14:13 -04:00
* Return true if the current user has an active session, i.e. a cookie to retrieve values.
*
* @return bool
*/
public function has_session() {
// @codingStandardsIgnoreLine.
2019-11-25 17:02:50 -05:00
return $this->_issuing_new_token || $this->_has_token || is_user_logged_in();
2019-06-20 14:14:13 -04:00
}
/**
2019-11-18 16:18:13 -05:00
* Set session expiration.
2023-06-13 23:17:02 +03:00
*
* @return void
*/
2019-11-18 16:18:13 -05:00
public function set_session_expiration() {
$this->_session_issued = time();
// 14 Days.
2019-11-18 16:18:13 -05:00
$this->_session_expiration = apply_filters(
'graphql_woocommerce_cart_session_expire',
2022-03-15 15:45:04 -04:00
// Seconds * Minutes * Hours * Days.
$this->_session_issued + ( 60 * 60 * 24 * 14 )
2019-11-18 16:18:13 -05:00
);
// 13 Days.
2022-03-15 15:45:04 -04:00
$this->_session_expiring = $this->_session_expiration - ( 60 * 60 * 24 );
}
/**
* Forget all session data without destroying it.
2023-06-13 23:17:02 +03:00
*
* @return void
*/
public function forget_session() {
2019-11-18 16:18:13 -05:00
if ( isset( $this->_token_to_be_sent ) ) {
unset( $this->_token_to_be_sent );
}
wc_empty_cart();
$this->_data = [];
$this->_dirty = false;
// Start new session.
$this->set_session_expiration();
// Get Customer ID.
$this->_customer_id = is_user_logged_in() ? get_current_user_id() : $this->generate_customer_id();
}
2021-02-02 10:29:37 -05:00
/**
* Save any changes to database after a session mutations has been run.
*
2023-06-13 23:17:02 +03:00
* @param mixed $source Operation root object.
* @param array $args Operation arguments.
* @param \WPGraphQL\AppContext $context AppContext instance.
* @param \GraphQL\Type\Definition\ResolveInfo $info Operation ResolveInfo object.
*
* @return void
2021-02-02 10:29:37 -05:00
*/
public function save_if_dirty( $source, $args, $context, $info ) {
// Bail early, if not one of the session mutations.
if ( ! in_array( $info->fieldName, Session_Transaction_Manager::get_session_mutations(), true ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
return;
}
// Update if user recently authenticated.
if ( is_user_logged_in() && get_current_user_id() !== $this->_customer_id ) {
$this->_customer_id = get_current_user_id();
$this->_dirty = true;
2021-02-02 10:29:37 -05:00
}
// Bail if no changes.
if ( ! $this->_dirty ) {
return;
}
$this->save_data();
}
/**
* For refreshing session data mid-request when changes occur in concurrent requests.
2023-06-13 23:17:02 +03:00
*
* @return void
2021-02-02 10:29:37 -05:00
*/
public function reload_data() {
\WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
2023-06-13 23:17:02 +03:00
// Get session data.
$data = $this->get_session( (string) $this->_customer_id );
if ( is_array( $data ) ) {
$this->_data = $data;
}
2021-02-02 10:29:37 -05:00
}
/**
* Noop for \WC_Session_Handler method.
*
* Prevents potential crticial errors when calling this method.
*
* @param bool $set Should the session cookie be set.
2023-06-13 23:17:02 +03:00
*
* @return void
*/
public function set_customer_session_cookie( $set ) {}
/**
* Returns "client_session_id". "client_session_id_expiration" is used
* to keep "client_session_id" as fresh as possible.
*
* For the most strict level of security it's highly recommend these values
* be set client-side using the `updateSession` mutation.
* "client_session_id" in particular should be salted with some
* kind of client identifier like the end-user "IP" or "user-agent"
* then hashed parodying the tokens generated by
* WP's WP_Session_Tokens class.
*
* @return string
*/
public function get_client_session_id() {
// Get client session ID.
$client_session_id = $this->get( 'client_session_id', false );
$client_session_id_expiration = absint( $this->get( 'client_session_id_expiration', 0 ) );
// If client session ID valid return it.
if ( false !== $client_session_id && time() < $client_session_id_expiration ) {
2023-06-13 23:17:02 +03:00
// @phpstan-ignore-next-line
return $client_session_id;
}
// Generate a new client session ID.
$client_session_id = uniqid();
$client_session_id_expiration = time() + 3600;
$this->set( 'client_session_id', $client_session_id );
$this->set( 'client_session_id_expiration', $client_session_id_expiration );
$this->save_data();
// Return new client session ID.
return $client_session_id;
}
}