mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
* feat: Authorizing URLs implemented and tested. * feat: More woographql_*_nonce functions implemented. * chore: linting changes made. * chore: linting changes made. * fix: woographql_*_ functions tested. * chore: WPCS compliance met. * devops: lint-code script updated to PHP v8.0 * chore: WPCS compliance met * devops: TransferSessionHandlerTest & QLSessionHandlerTest updated * devops: codeclimate.yml added. * chore: Linter compliance met * devops: Harmonizing WordPress doc written and Settings doc updated. * chore: Typo fixed in docs. * fix: General bugfixes and improvements related to Auth URLs * devops: More docs. * chore: Linter compliance met * chore: small change made to docs.
465 lines
14 KiB
PHP
465 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Handles data for the current customers session.
|
|
*
|
|
* @package WPGraphQL\WooCommerce\Utils
|
|
* @since 0.1.2
|
|
*/
|
|
|
|
namespace WPGraphQL\WooCommerce\Utils;
|
|
|
|
use Firebase\JWT\JWT;
|
|
use Firebase\JWT\Key;
|
|
use GraphQL\Error\UserError;
|
|
use WC_Session_Handler;
|
|
|
|
/**
|
|
* Class - QL_Session_Handler
|
|
*/
|
|
class QL_Session_Handler extends WC_Session_Handler {
|
|
|
|
/**
|
|
* Stores the name of the HTTP header used to pass the session token.
|
|
*
|
|
* @var string $_token
|
|
*/
|
|
protected $_token; // @codingStandardsIgnoreLine
|
|
|
|
/**
|
|
* Stores Timestamp of when the session token was issued.
|
|
*
|
|
* @var string $_session_issued
|
|
*/
|
|
protected $_session_issued; // @codingStandardsIgnoreLine
|
|
|
|
/**
|
|
* True when the token exists.
|
|
*
|
|
* @var bool $_has_token
|
|
*/
|
|
protected $_has_token = false; // @codingStandardsIgnoreLine
|
|
|
|
/**
|
|
* True when a new session token has been issued.
|
|
*
|
|
* @var bool $_issuing_new_token
|
|
*/
|
|
protected $_issuing_new_token = false; // @codingStandardsIgnoreLine
|
|
|
|
/**
|
|
* Manages connection to the session transaction queue.
|
|
*
|
|
* @var Session_Transaction_Manager
|
|
*/
|
|
private $transaction_manager = null;
|
|
|
|
/**
|
|
* Constructor for the session class.
|
|
*/
|
|
public function __construct() {
|
|
$this->_token = apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' );
|
|
$this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions';
|
|
}
|
|
|
|
/**
|
|
* Returns formatted $_SERVER index from provided string.
|
|
*
|
|
* @param string $header String to be formatted.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_server_key( $header = null ) {
|
|
return ! empty( $header )
|
|
? 'HTTP_' . strtoupper( preg_replace( '#[^A-z0-9]#', '_', $header ) )
|
|
: 'HTTP_' . strtoupper( preg_replace( '#[^A-z0-9]#', '_', $this->_token ) );
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
|
|
$secret_key = defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) && ! empty( GRAPHQL_WOOCOMMERCE_SECRET_KEY )
|
|
? GRAPHQL_WOOCOMMERCE_SECRET_KEY :
|
|
'graphql-woo-cart-session';
|
|
return apply_filters( 'graphql_woocommerce_secret_key', $secret_key );
|
|
}
|
|
|
|
/**
|
|
* Init hooks and session data.
|
|
*/
|
|
public function init() {
|
|
$this->init_session_token();
|
|
$this->transaction_manager = Session_Transaction_Manager::get( $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' ] );
|
|
|
|
if ( ! is_user_logged_in() ) {
|
|
add_filter( 'nonce_user_logged_out', [ $this, 'maybe_update_nonce_user_logged_out' ], 10, 2 );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup token and customer ID.
|
|
*
|
|
* @throws UserError Invalid token.
|
|
*/
|
|
public function init_session_token() {
|
|
$token = $this->get_session_token();
|
|
|
|
// Process existing session if not expired or invalid.
|
|
if ( $token && ! is_wp_error( $token ) ) {
|
|
$this->_customer_id = $token->data->customer_id;
|
|
$this->_session_issued = $token->iat;
|
|
$this->_session_expiration = $token->exp;
|
|
$this->_session_expiring = $token->exp - ( 3600 );
|
|
$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;
|
|
|
|
// 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();
|
|
}
|
|
|
|
$this->save_data( $guest_session_id );
|
|
$this->set_customer_session_token( true );
|
|
}
|
|
|
|
// Update session expiration on each action.
|
|
$this->set_session_expiration();
|
|
if ( $token->exp < $this->_session_expiration ) {
|
|
$this->update_session_timestamp( $this->_customer_id, $this->_session_expiration );
|
|
}
|
|
} else {
|
|
|
|
// If token invalid throw warning.
|
|
if ( is_wp_error( $token ) ) {
|
|
add_filter(
|
|
'graphql_woocommerce_session_token_errors',
|
|
function( $errors ) use ( $token ) {
|
|
$errors = $token->get_error_message();
|
|
return $errors;
|
|
}
|
|
);
|
|
}
|
|
|
|
// 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();
|
|
$this->_data = $this->get_session_data();
|
|
$this->set_customer_session_token( true );
|
|
}//end if
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @return bool|object
|
|
*/
|
|
public function get_session_token() {
|
|
// Get the Auth header.
|
|
$session_header = $this->get_session_header();
|
|
|
|
if ( empty( $session_header ) ) {
|
|
return false;
|
|
}
|
|
|
|
list( $token ) = sscanf( $session_header, 'Session %s' );
|
|
|
|
/**
|
|
* Try to decode the token
|
|
*/
|
|
try {
|
|
JWT::$leeway = 60;
|
|
|
|
$secret = $this->get_secret_key();
|
|
$key = new Key( $secret, 'HS256' );
|
|
$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' ) );
|
|
}
|
|
|
|
// The Token is decoded now validate the iss.
|
|
if ( empty( $token->iss ) || get_bloginfo( 'url' ) !== $token->iss ) {
|
|
throw new \Exception( __( 'The iss do not match with this server', 'wp-graphql-woocommerce' ) );
|
|
}
|
|
|
|
// Validate the customer id in the token.
|
|
if ( empty( $token->data ) || empty( $token->data->customer_id ) ) {
|
|
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
|
|
|
|
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
|
|
: 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 );
|
|
}
|
|
|
|
/**
|
|
* Creates JSON Web Token for customer session.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function build_token() {
|
|
if ( empty( $this->_session_issued ) ) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine the "not before" value for use in the token
|
|
*
|
|
* @param string $issued The timestamp of token was issued.
|
|
* @param integer $customer_id Customer ID.
|
|
* @param array $session_data Cart session data.
|
|
*/
|
|
$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 = [
|
|
'iss' => get_bloginfo( 'url' ),
|
|
'iat' => $this->_session_issued,
|
|
'nbf' => $not_before,
|
|
'exp' => $this->_session_expiration,
|
|
'data' => [
|
|
'customer_id' => $this->_customer_id,
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Filter the token, allowing for individual systems to configure the token as needed
|
|
*
|
|
* @param array $token The token array that will be encoded
|
|
* @param integer $customer_id ID of customer associated with token.
|
|
* @param array $session_data Session data associated with token.
|
|
*/
|
|
$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.
|
|
*
|
|
* @param string $token The signed JWT token that will be returned
|
|
* @param integer $customer_id ID of customer associated with token.
|
|
* @param array $session_data Session data associated with token.
|
|
*/
|
|
$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).
|
|
*
|
|
* Warning: Headers will only be set if this is called before the headers are sent.
|
|
*
|
|
* @param bool $set Should the session cookie be set.
|
|
*/
|
|
public function set_customer_session_token( $set ) {
|
|
if ( ! empty( $this->_session_issued ) && $set ) {
|
|
/**
|
|
* Set callback session token for use in the HTTP response header and customer/user "sessionToken" field.
|
|
*/
|
|
add_filter(
|
|
'graphql_response_headers_to_send',
|
|
function( $headers ) {
|
|
$token = $this->build_token();
|
|
if ( $token ) {
|
|
$headers[ $this->_token ] = $token;
|
|
}
|
|
|
|
return $headers;
|
|
},
|
|
10
|
|
);
|
|
|
|
$this->_issuing_new_token = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if the current user has an active session, i.e. a cookie to retrieve values.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function has_session() {
|
|
// @codingStandardsIgnoreLine.
|
|
return $this->_issuing_new_token || $this->_has_token || is_user_logged_in();
|
|
}
|
|
|
|
/**
|
|
* Set session expiration.
|
|
*/
|
|
public function set_session_expiration() {
|
|
$this->_session_issued = time();
|
|
// 14 Days.
|
|
$this->_session_expiration = apply_filters(
|
|
'graphql_woocommerce_cart_session_expire',
|
|
// Seconds * Minutes * Hours * Days.
|
|
$this->_session_issued + ( 60 * 60 * 24 * 14 )
|
|
);
|
|
// 13 Days.
|
|
$this->_session_expiring = $this->_session_expiration - ( 60 * 60 * 24 );
|
|
}
|
|
|
|
/**
|
|
* Forget all session data without destroying it.
|
|
*/
|
|
public function forget_session() {
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Save any changes to database after a session mutations has been run.
|
|
*
|
|
* @param mixed $source Operation root object.
|
|
* @param array $args Operation arguments.
|
|
* @param \WPGraphQL\AppContext $context AppContext instance.
|
|
* @param \GraphQL\ResolveInfo $info Operation ResolveInfo object.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// Bail if no changes.
|
|
if ( ! $this->_dirty ) {
|
|
return;
|
|
}
|
|
|
|
$this->save_data();
|
|
}
|
|
|
|
/**
|
|
* For refreshing session data mid-request when changes occur in concurrent requests.
|
|
*/
|
|
public function reload_data() {
|
|
\WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP );
|
|
$this->_data = $this->get_session( $this->_customer_id );
|
|
}
|
|
|
|
/**
|
|
* Noop for \WC_Session_Handler method.
|
|
*
|
|
* Prevents potential crticial errors when calling this method.
|
|
*
|
|
* @param bool $set Should the session cookie be set.
|
|
*/
|
|
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 ) {
|
|
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;
|
|
}
|
|
|
|
}
|