diff --git a/includes/class-filters.php b/includes/class-filters.php index 96a501748..3e2423631 100644 --- a/includes/class-filters.php +++ b/includes/class-filters.php @@ -15,6 +15,7 @@ use WPGraphQL\Extensions\WooCommerce\Data\Factory; use WPGraphQL\Extensions\WooCommerce\Data\Loader\WC_Customer_Loader; use WPGraphQL\Extensions\WooCommerce\Data\Loader\WC_Post_Crud_Loader; +use WPGraphQL\Extensions\WooCommerce\Utils\QL_Session_Handler; /** * Class Filters @@ -34,12 +35,24 @@ class Filters { */ private static $post_crud_loader; + /** + * Stores instance session header name. + * + * @var string + */ + private static $session_header; + /** * Register filters */ public static function load() { + // Registers WooCommerce taxonomies. add_filter( 'register_taxonomy_args', array( __CLASS__, 'register_taxonomy_args' ), 10, 2 ); + + // Add data-loaders to AppContext. add_filter( 'graphql_data_loaders', array( __CLASS__, 'graphql_data_loaders' ), 10, 2 ); + + // Filter core connection resolutions. add_filter( 'graphql_post_object_connection_query_args', array( __CLASS__, 'graphql_post_object_connection_query_args' ), @@ -52,6 +65,13 @@ public static function load() { 10, 5 ); + + // Setup QL session handler. + self::$session_header = apply_filters( 'woocommerce_session_header_name', 'woocommerce-session' ); + add_filter( 'woocommerce_cookie', array( __CLASS__, 'woocommerce_cookie' ) ); + add_filter( 'woocommerce_session_handler', array( __CLASS__, 'init_ql_session_handler' ) ); + add_filter( 'graphql_response_headers_to_send', array( __CLASS__, 'add_session_header_to_expose_headers' ) ); + add_filter( 'graphql_access_control_allow_headers', array( __CLASS__, 'add_session_header_to_allow_headers' ) ); } /** @@ -182,4 +202,55 @@ public static function graphql_post_object_connection_query_args( $query_args, $ public static function graphql_term_object_connection_query_args( $query_args, $source, $args, $context, $info ) { return WC_Terms_Connection_Resolver::get_query_args( $query_args, $source, $args, $context, $info ); } + + /** + * Filters WooCommerce cookie key to be used as a HTTP Header on GraphQL HTTP requests + * + * @param string $cookie WooCommerce cookie key. + * + * @return string + */ + public static function woocommerce_cookie( $cookie ) { + return self::$session_header; + } + + /** + * Filters WooCommerce session handler class on GraphQL HTTP requests + * + * @param string $session_class Classname of the current session handler class. + * + * @return string + */ + public static function init_ql_session_handler( $session_class ) { + return QL_Session_Handler::class; + } + + /** + * Append session header to the exposed headers in GraphQL responses + * + * @param array $headers GraphQL responser headers. + * + * @return array + */ + public static function add_session_header_to_expose_headers( $headers ) { + if ( empty( $headers['Access-Control-Expose-Headers'] ) ) { + $headers['Access-Control-Expose-Headers'] = apply_filters( 'woocommerce_cookie', self::$session_header ); + } else { + $headers['Access-Control-Expose-Headers'] .= ', ' . apply_filters( 'woocommerce_cookie', self::$session_header ); + } + + return $headers; + } + + /** + * Append the session header to the allowed headers in GraphQL responses + * + * @param array $allowed_headers The existing allowed headers. + * + * @return array + */ + public static function add_session_header_to_allow_headers( array $allowed_headers ) { + $allowed_headers[] = self::$session_header; + return $allowed_headers; + } } diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php new file mode 100644 index 000000000..634d10684 --- /dev/null +++ b/includes/utils/class-ql-session-handler.php @@ -0,0 +1,137 @@ + + * @author Geoff Taylor + * @link http://nazmulahsan.me/simple-two-way-function-encrypt-decrypt-string/ + * + * @param string $string string to be encrypted/decrypted. + * @param string $action what to do with this? e for encrypt, d for decrypt. + * + * @return string + */ + private function crypt( $string, $action = 'e' ) { + // you may change these values to your own. + $secret_key = apply_filters( 'woographql_session_header_secret_key', 'my_simple_secret_key' ); + $secret_iv = apply_filters( 'woographql_session_header_secret_iv', 'my_simple_secret_iv' ); + + $output = false; + $encrypt_method = 'AES-256-CBC'; + $key = hash( 'sha256', $secret_key ); + $iv = substr( hash( 'sha256', $secret_iv ), 0, 16 ); + + if ( 'e' === $action ) { + $output = base64_encode( openssl_encrypt( $string, $encrypt_method, $key, 0, $iv ) ); + } elseif ( 'd' === $action ) { + $output = openssl_decrypt( base64_decode( $string ), $encrypt_method, $key, 0, $iv ); + } + + return $output; + } + + /** + * Returns formatted $_SERVER index from provided string. + * + * @param string $string String to be formatted. + * + * @return string + */ + private function get_server_key( $string ) { + return 'HTTP_' . strtoupper( preg_replace( '#[^A-z0-9]#', '_', $string ) ); + } + + /** + * Encrypts and 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_cookie( $set ) { + if ( $set ) { + $to_hash = $this->_customer_id . '|' . $this->_session_expiration; + $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); + $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash; + $this->_has_cookie = true; + if ( ! isset( $_SERVER[ $this->_cookie ] ) || $_SERVER[ $this->_cookie ] !== $cookie_value ) { + add_filter( + 'graphql_response_headers_to_send', + function( $headers ) use ( $cookie_value ) { + $headers[ $this->_cookie ] = $this->crypt( $cookie_value, 'e' ); + return $headers; + } + ); + } + } + } + + /** + * 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 isset( $_SERVER[ $this->get_server_key( $this->_cookie ) ] ) || $this->_has_cookie || is_user_logged_in(); + } + + /** + * Retrieve and decrypt the session data from session, if set. Otherwise return false. + * + * Session cookies without a customer ID are invalid. + * + * @return bool|array + */ + public function get_session_cookie() { + // @codingStandardsIgnoreStart. + $cookie_value = isset( $_SERVER[ $this->get_server_key( $this->_cookie ) ] ) + ? $this->crypt( $_SERVER[ $this->get_server_key( $this->_cookie ) ], 'd' ) + : false; + // @codingStandardsIgnoreEnd. + if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) { + return false; + } + list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value ); + if ( empty( $customer_id ) ) { + return false; + } + // Validate hash. + $to_hash = $customer_id . '|' . $session_expiration; + $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); + if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) { + return false; + } + return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ); + } + + /** + * Forget all session data without destroying it. + */ + public function forget_session() { + add_filter( + 'graphql_response_headers_to_send', + function( $headers ) { + $headers[ $this->_cookie ] = 'false'; + return $headers; + } + ); + wc_empty_cart(); + $this->_data = array(); + $this->_dirty = false; + $this->_customer_id = $this->generate_customer_id(); + } +} diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 546c98203..3a72502ea 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -94,5 +94,6 @@ 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPObject\\Shipping_Method_Type' => $baseDir . '/includes/type/object/class-shipping-method-type.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPObject\\Tax_Rate_Type' => $baseDir . '/includes/type/object/class-tax-rate-type.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPObject\\Variation_Attribute_Type' => $baseDir . '/includes/type/object/class-variation-attribute-type.php', + 'WPGraphQL\\Extensions\\WooCommerce\\Utils\\QL_Session_Handler' => $baseDir . '/includes/utils/class-ql-session-handler.php', 'WP_GraphQL_WooCommerce' => $baseDir . '/includes/class-wp-graphql-woocommerce.php', ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index b68cd8e7b..1b5f47c5e 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -109,6 +109,7 @@ class ComposerStaticInitee0d17af17b841ed3a93c4a0e5cc5e5f 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPObject\\Shipping_Method_Type' => __DIR__ . '/../..' . '/includes/type/object/class-shipping-method-type.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPObject\\Tax_Rate_Type' => __DIR__ . '/../..' . '/includes/type/object/class-tax-rate-type.php', 'WPGraphQL\\Extensions\\WooCommerce\\Type\\WPObject\\Variation_Attribute_Type' => __DIR__ . '/../..' . '/includes/type/object/class-variation-attribute-type.php', + 'WPGraphQL\\Extensions\\WooCommerce\\Utils\\QL_Session_Handler' => __DIR__ . '/../..' . '/includes/utils/class-ql-session-handler.php', 'WP_GraphQL_WooCommerce' => __DIR__ . '/../..' . '/includes/class-wp-graphql-woocommerce.php', );