diff --git a/.github/workflows/lint-code.yml b/.github/workflows/lint-code.yml
index bb8384b1..42211af8 100644
--- a/.github/workflows/lint-code.yml
+++ b/.github/workflows/lint-code.yml
@@ -23,7 +23,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: 7.3
+ php-version: 8.0
extensions: mbstring, intl
tools: composer
diff --git a/access-functions.php b/access-functions.php
index c5d0202a..796b7fca 100644
--- a/access-functions.php
+++ b/access-functions.php
@@ -229,12 +229,6 @@ if ( ! function_exists( 'wc_graphql_camel_case_to_underscore' ) ) {
}
}//end if
-/**
- * Plugin global functions.
- *
- * @package Axis\Plugin_Distributor
- */
-
if ( ! function_exists( 'woographql_setting' ) ) :
/**
* Get an option value from WooGraphQL settings
@@ -275,6 +269,92 @@ if ( ! function_exists( 'woographql_setting' ) ) :
}
endif;
+if ( ! function_exists( 'woographql_get_session_uid' ) ) :
+ /**
+ * Returns end-user's customer ID.
+ *
+ * @return string|int
+ */
+ function woographql_get_session_uid() {
+ return WC()->session->get_customer_id();
+ }
+endif;
+
+if ( ! function_exists( 'woographql_get_session_token' ) ) :
+ /**
+ * Returns session user's "client_session_id"
+ *
+ * @return string
+ */
+ function woographql_get_session_token() {
+ return WC()->session->get_client_session_id();
+ }
+endif;
+
+if ( ! function_exists( 'woographql_create_nonce' ) ) :
+ /**
+ * Creates WooGraphQL session transfer nonces.
+ *
+ * @param string $action Nonce name.
+ */
+ function woographql_create_nonce( $action = -1 ) {
+ $uid = woographql_get_session_uid();
+ $token = woographql_get_session_token();
+ $i = wp_nonce_tick( $action );
+
+ return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
+ }
+endif;
+
+if ( ! function_exists( 'woographql_verify_nonce' ) ) :
+ /**
+ * Validate WooGraphQL session transfer nonces.
+ *
+ * @param string $nonce Nonce to validated.
+ * @param integer|string $action Nonce name.
+ *
+ * @return bool
+ */
+ function woographql_verify_nonce( $nonce, $action = -1 ) {
+ $nonce = (string) $nonce;
+ $uid = woographql_get_session_uid();
+
+ if ( empty( $nonce ) ) {
+ return false;
+ }
+
+ $token = woographql_get_session_token();
+ $i = wp_nonce_tick( $action );
+
+ // Nonce generated 0-12 hours ago.
+ $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
+ if ( hash_equals( $expected, $nonce ) ) {
+ return 1;
+ }
+
+ // Nonce generated 12-24 hours ago.
+ $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
+ if ( hash_equals( $expected, $nonce ) ) {
+ return 2;
+ }
+
+ /**
+ * Fires when nonce verification fails.
+ *
+ * @since 4.4.0
+ *
+ * @param string $nonce The invalid nonce.
+ * @param string|int $action The nonce action.
+ * @param WP_User $user The current user object.
+ * @param string $token The user's session token.
+ */
+ do_action( 'graphql_verify_nonce_failed', $nonce, $action, $uid, $token );
+
+ // Invalid nonce.
+ return false;
+ }
+endif;
+
diff --git a/codeception.dist.yml b/codeception.dist.yml
index 423b3b21..a8ab65bd 100644
--- a/codeception.dist.yml
+++ b/codeception.dist.yml
@@ -57,6 +57,7 @@ modules:
REST:
depends: WPBrowser
url: '%WORDPRESS_URL%'
+ cookies: false
WPFilesystem:
wpRootFolder: '%WP_CORE_DIR%'
plugins: '/wp-content/plugins'
diff --git a/codeclimate.yml b/codeclimate.yml
new file mode 100644
index 00000000..71b63a6a
--- /dev/null
+++ b/codeclimate.yml
@@ -0,0 +1,5 @@
+plugins:
+ phpcodesniffer:
+ enabled: true
+ config:
+ standard: "phpcs.xml.dist"
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 5020b6bd..355caf2b 100644
--- a/composer.json
+++ b/composer.json
@@ -25,6 +25,7 @@
"firebase/php-jwt": "^6.1.0"
},
"require-dev": {
+ "automattic/vipwpcs": "^2.3",
"squizlabs/php_codesniffer": "^3.5",
"wp-coding-standards/wpcs": "^2.3"
},
@@ -34,6 +35,7 @@
"sort-packages": true,
"allow-plugins": {
"johnpbloch/wordpress-core-installer": true,
+ "dealerdirect/phpcodesniffer-composer-installer": true,
"composer/installers": true
}
},
diff --git a/composer.lock b/composer.lock
index ccbacc6c..aa08ff8d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,29 +4,29 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "49e1ecf4a41487b3de0547b8e3860c6a",
+ "content-hash": "57cb19ef33e8b4bd0343bb725e4c0f6b",
"packages": [
{
"name": "firebase/php-jwt",
- "version": "v6.4.0",
+ "version": "v6.5.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
- "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224"
+ "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/firebase/php-jwt/zipball/4dd1e007f22a927ac77da5a3fbb067b42d3bc224",
- "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/e94e7353302b0c11ec3cfff7180cd0b1743975d2",
+ "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2",
"shasum": ""
},
"require": {
- "php": "^7.1||^8.0"
+ "php": "^7.4||^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^6.5||^7.4",
- "phpspec/prophecy-phpunit": "^1.1",
- "phpunit/phpunit": "^7.5||^9.5",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
"psr/cache": "^1.0||^2.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
@@ -65,24 +65,209 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
- "source": "https://github.com/firebase/php-jwt/tree/v6.4.0"
+ "source": "https://github.com/firebase/php-jwt/tree/v6.5.0"
},
- "time": "2023-02-09T21:01:23+00:00"
+ "time": "2023-05-12T15:47:07+00:00"
}
],
"packages-dev": [
{
- "name": "squizlabs/php_codesniffer",
- "version": "3.7.1",
+ "name": "automattic/vipwpcs",
+ "version": "2.3.3",
"source": {
"type": "git",
- "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
+ "url": "https://github.com/Automattic/VIP-Coding-Standards.git",
+ "reference": "6cd0a6a82bc0ac988dbf9d6a7c2e293dc8ac640b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
- "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
+ "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/6cd0a6a82bc0ac988dbf9d6a7c2e293dc8ac640b",
+ "reference": "6cd0a6a82bc0ac988dbf9d6a7c2e293dc8ac640b",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7",
+ "php": ">=5.4",
+ "sirbrillig/phpcs-variable-analysis": "^2.11.1",
+ "squizlabs/php_codesniffer": "^3.5.5",
+ "wp-coding-standards/wpcs": "^2.3"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-console-highlighter": "^0.5",
+ "php-parallel-lint/php-parallel-lint": "^1.0",
+ "phpcompatibility/php-compatibility": "^9",
+ "phpcsstandards/phpcsdevtools": "^1.0",
+ "phpunit/phpunit": "^4 || ^5 || ^6 || ^7"
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/Automattic/VIP-Coding-Standards/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress VIP minimum coding conventions",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "wordpress"
+ ],
+ "support": {
+ "issues": "https://github.com/Automattic/VIP-Coding-Standards/issues",
+ "source": "https://github.com/Automattic/VIP-Coding-Standards",
+ "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki"
+ },
+ "time": "2021-09-29T16:20:23+00:00"
+ },
+ {
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v0.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git",
+ "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db",
+ "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0",
+ "php": ">=5.3",
+ "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0"
+ },
+ "require-dev": {
+ "composer/composer": "*",
+ "php-parallel-lint/php-parallel-lint": "^1.3.1",
+ "phpcompatibility/php-compatibility": "^9.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Franck Nijhof",
+ "email": "franck.nijhof@dealerdirect.com",
+ "homepage": "http://www.frenck.nl",
+ "role": "Developer / IT Manager"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
+ "homepage": "http://www.dealerdirect.com",
+ "keywords": [
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcbf",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
+ "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
+ },
+ "time": "2022-02-04T12:51:07+00:00"
+ },
+ {
+ "name": "sirbrillig/phpcs-variable-analysis",
+ "version": "v2.11.16",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git",
+ "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/dc5582dc5a93a235557af73e523c389aac9a8e88",
+ "reference": "dc5582dc5a93a235557af73e523c389aac9a8e88",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0",
+ "squizlabs/php_codesniffer": "^3.5.6"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0",
+ "phpcsstandards/phpcsdevcs": "^1.1",
+ "phpstan/phpstan": "^1.7",
+ "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0",
+ "sirbrillig/phpcs-import-detection": "^1.1",
+ "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta"
+ },
+ "type": "phpcodesniffer-standard",
+ "autoload": {
+ "psr-4": {
+ "VariableAnalysis\\": "VariableAnalysis/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sam Graham",
+ "email": "php-codesniffer-variableanalysis@illusori.co.uk"
+ },
+ {
+ "name": "Payton Swick",
+ "email": "payton@foolord.com"
+ }
+ ],
+ "description": "A PHPCS sniff to detect problems with variables.",
+ "keywords": [
+ "phpcs",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues",
+ "source": "https://github.com/sirbrillig/phpcs-variable-analysis",
+ "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki"
+ },
+ "time": "2023-03-31T16:46:32+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
+ "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
+ "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
"shasum": ""
},
"require": {
@@ -118,14 +303,15 @@
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
- "standards"
+ "standards",
+ "static analysis"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
- "time": "2022-06-18T07:21:10+00:00"
+ "time": "2023-02-22T23:07:41+00:00"
},
{
"name": "wp-coding-standards/wpcs",
diff --git a/docs/harmonizing-with-wordpress.md b/docs/harmonizing-with-wordpress.md
index e69de29b..2e7d093f 100644
--- a/docs/harmonizing-with-wordpress.md
+++ b/docs/harmonizing-with-wordpress.md
@@ -0,0 +1,221 @@
+---
+title: "Harmonizing with WordPress"
+description: "Learn how to create a secure checkout button and understand the security behind the URL validation."
+keywords: "WooGraphQL, WooCommerce, checkout functionality, secure checkout, session transferring URLs, nonce generation, session meta, URL validation, Client Session ID, UpdateSession mutation, GraphQL, cart page, WordPress, frontend security"
+author: "Geoff Taylor"
+---
+
+# Harmonizing with WordPress
+
+In our [previous guide](using-cart-data.md), we created a cart page for our WooCommerce store. Now, we are going to add a "Checkout" button to this page and discuss how to ensure the security of the process.
+
+Our checkout button's `href` attribute will be a nonced URL that directs the user to a particular endpoint on the WP backend. The endpoint performs a set of operations such as validating the URL, loading the WooCommerce session, authenticating the registered user, and redirecting to the checkout page. If the URL validation fails, the user will be redirected to the WP homepage.
+
+## Enabling Dedicated Router and Nonce Generation
+
+Before starting with the checkout button creation, we need to activate the dedicated router and nonce generation feature introduced in WooGraphQL v0.13.0. To do this, enable the "User Session transferring URLs" option in your WooGraphQL settings. Also, make sure to check the "Checkout URL" checkbox under this option.
+
+
+
+Although it's not mandatory, it's recommended to set the "Endpoint for Authorizing URLs" and "Checkout URL nonce name" settings with secure values for increased security.
+
+
+
+## Creating the Checkout Button
+
+To create a checkout button in our CartPage component, we can use the `checkoutUrl` field from the `customer` query. This URL is dynamically generated by our GraphQL server and is now ready for use. Let's update our CartPage component:
+
+```jsx
+// ...existing code...
+Checkout
+// ...existing code...
+```
+
+## Understanding URL Validation and Security
+
+Although our checkout button is now functional, it's important to note that the default URL validation process can be improved in terms of security. In the next part of this guide, we will discuss the security measures and how to enhance them.
+
+### Improving Security with Client Session ID
+
+The first step is to create a "Client Session ID" in our app and pass it to the WooCommerce session using the `updateSession` mutation. This mutation allows us to set session metadata directly in the WooCommerce session data object. The `client_session_id` meta is used by WooGraphQL to create the nonce.
+
+If WooGraphQL cannot find this meta, it generates one on the server, making the URL potentially usable on any machine. This is why it's important to avoid an arbitrary string as the session ID. Instead, we should use a value tied to the user's machine, like their IP or User Agent. Afterward, this value should be one-way hashed for increased security.
+
+Furthermore, we also need to set the `client_session_id_expiration` as a string value representing the expiration time in seconds. For example, to set an expiration time of one hour from now, we could use: `Math.floor(new Date().getTime() / 1000) + 3600`.
+
+We can achieve all of this by using the `updateSession` mutation as follows:
+
+```graphql
+mutation($input: UpdateSessionInput!) {
+ updateSession(input: $input) {
+ customer {
+ checkoutUrl
+ }
+ }
+}
+```
+
+With the input fields populated with secure values:
+
+```js
+const input = {
+ sessionData: [
+ {
+ key: 'client_session_id',
+ value: 'secure_hashed_value', // Replace this with your secure hashed value.
+ },
+ {
+ key: 'client_session_id_expiration',
+ value: `${Math.floor(new Date().getTime() / 1000) + 3600}`,
+ },
+ ]
+}
+```
+
+When the `client_session_id` or `client_session_id_expiration` values become invalid or expired, WooGraphQL generates new values with an expiration time of one hour. To avoid this, we recommend you to periodically update these values from the client side and retrieve a new `checkoutUrl` each time.
+
+## Reinventing Security: The Client-Side Nonce
+
+Next we're going to explore an advanced approach to enhance the security of our checkout procedure by generating a nonce on the client side. By doing this, and not pulling the Nonces or Auth URLs from WooGraphQL we remove any risk of leakage thru GraphQL request and further protect the end-user's data and the WordPress backend. This process will involve recreating some of PHP and WordPress core functions in JavaScript.
+
+1. **PHP `time` Function in JavaScript**
+
+ Our first stop is to rewrite the PHP `time` function in JavaScript, which returns the current Unix timestamp. Here's how we can do it:
+
+ ```js
+ function time() {
+ return Math.floor(new Date().getTime() / 1000);
+ }
+ ```
+
+2. **WordPress `wp_nonce_tick` Function in JavaScript**
+
+ Next, we translate the WordPress function `wp_nonce_tick` to JavaScript. This function returns a time-dependent variable for nonce creation:
+
+ ```js
+ const MINUTE_IN_SECONDS = 60;
+ const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS;
+ const DAY_IN_SECONDS = 24 * HOUR_IN_SECONDS;
+
+ function nonceTick() {
+ const nonceLife = DAY_IN_SECONDS;
+ return Math.ceil(time() / (nonceLife / 2));
+ }
+ ```
+
+3. **WordPress `wp_hash` Function in JavaScript**
+
+ The `wp_hash` function, another WordPress core function, will be adapted to JavaScript as well. This function uses the `wp_salt` function to retrieve the salt from WordPress Salt constants, usually defined in the `wp-config.php` file. In our context, we only need the `nonce` salt. Therefore, it's crucial to ensure the `NONCE_KEY` and `NONCE_SALT` constants are set on the WordPress installation, and their values are accessible in our front-end application.
+
+ `wp_hash` also uses `hash_hmac` and `md5` encryption to create the hash. For this, we'll utilize `crypto-js`, which you can install with `npm` using the command `npm install crypto-js`. Here's how to write `wp_hash` in JavaScript:
+
+ ```js
+ import { HmacMD5 } from 'crypto-js';
+
+ export function wpNonceHash(data) {
+ const nonceSalt = process.env.NONCE_KEY + process.env.NONCE_SALT;
+ const hash = HmacMD5(data, nonceSalt).toString();
+
+ return hash;
+ }
+ ```
+
+With these functions ready, we can essentially recreate the `woographql_create_nonce` PHP function employed by WooGraphQL to create the nonce. Below is the JavaScript version:
+
+```js
+export function createNonce(action, uId, token) {
+ const i = nonceTick();
+
+ const nonce = wpNonceHash(`${i}|${action}|${uId}|${token}`).slice(-12, -2);
+
+ return nonce;
+}
+```
+
+In the function above:
+- The `action` parameter represents the nonce action name.
+- The `uId` parameter represents the end-user's session ID, either their WP User Database ID (if they are authenticated) or a random string (if they are a guest). To retrieve this value, we'll have to decode the WooCommerce Session Token used by ApolloClient.
+- The `token` is our Client Session ID mentioned earlier.
+
+4. **Generating the URLs**
+
+Having the nonce alone is not enough, so let's move on to generating our URLs. The process involves three functions and the `jwt-decode` library. Install it using npm with the command `npm install jwt-decode`.
+
+Here's the JavaScript code to generate the URL:
+
+```js
+import jwtDecode from 'jwt-decode';
+
+function getAction(action, uId) {
+ switch (action) {
+ case 'cart':
+ return `load-cart_${uId}`;
+ case 'checkout':
+ return `load-checkout_${uId}`;
+ case 'new-payment':
+ return `load-account_${uId}`;
+ case 'change-sub':
+ return `change-sub_${uId}`;
+ case 'renew-sub':
+ return `renew-sub_${uId}`;
+ default:
+ throw new Error('Invalid nonce action provided.');
+ }
+}
+
+function getNonceParam(action) {
+ switch (action) {
+ case 'cart':
+ return '_wc_cart';
+ case 'checkout':
+ return '_wc_checkout';
+ case 'new-payment':
+ return '_wc_payment';
+ case 'change-sub':
+ return '_wc_change_sub';
+ case 'renew-sub':
+ return '_wc_renew_sub';
+ default:
+ throw new Error('Invalid nonce action provided.');
+ }
+}
+
+export function generateUrl(sessionToken, clientSessionId, actionType) {
+ const decodedToken = jwtDecode(sessionToken);
+ if (!decodedToken?.data?.customer_id) {
+ throw new Error('Failed to decode session token');
+ }
+ const uId = decodedToken.data.customer_id;
+ const action = getAction(actionType, uId);
+
+ // Create nonce
+ const nonce = createNonce(action, uId, clientSessionId);
+
+ // Create URL.
+ const param = getNonceParam(actionType);
+ let url = `${process.env.WORDPRESS_URL}/transfer-session?session_id=${uId}&${param}=${nonce}`;
+
+ // Add subscription ID placeholder if subscription action.
+ if (actionType === 'change-sub' || actionType === 'renew-sub') {
+ url = `${url}&sub=%SUBSCRIPTION_ID%`;
+ }
+
+ return url;
+}
+```
+
+Note that for the `change-sub` and `renew-sub` actions from WooGraphQL Pro, we are passing a `%SUBSCRIPTION_ID%` placeholder to be replaced with a subscription database ID before use.
+
+To get our checkout URL, you would run:
+
+```js
+const checkoutUrl = generateUrl(sessionToken, clientSessionId, 'checkout');
+```
+
+Also, `transfer-session` is the default name of the authorization endpoint. This can be altered in the WooGraphQL settings on the WP Dashboard.
+
+To confirm the validity of your URL, compare it with the Auth URLs generated by WooGraphQL with the same `client_session_id` and ensure they are identical.
+
+## Conclusion
+
+With this guide, you should now be able to add a secure checkout button to your WooCommerce cart page using WooGraphQL. Keep in mind that even though we've improved security by setting a client-side session ID and expiration, these measures should be part of a broader security strategy. Always ensure to follow best practices to keep your application and user data safe.
diff --git a/docs/images/auth-endpoint-and-checkout-nonce-name-settings-screenshot.png b/docs/images/auth-endpoint-and-checkout-nonce-name-settings-screenshot.png
new file mode 100644
index 00000000..af2c294e
Binary files /dev/null and b/docs/images/auth-endpoint-and-checkout-nonce-name-settings-screenshot.png differ
diff --git a/docs/images/authorizing-url-settings.png b/docs/images/authorizing-url-settings.png
new file mode 100644
index 00000000..166e61c5
Binary files /dev/null and b/docs/images/authorizing-url-settings.png differ
diff --git a/docs/images/enable-user-session-transferring-urls-setting-screenshot.png b/docs/images/enable-user-session-transferring-urls-setting-screenshot.png
new file mode 100644
index 00000000..2541aff9
Binary files /dev/null and b/docs/images/enable-user-session-transferring-urls-setting-screenshot.png differ
diff --git a/docs/images/overview-screenshot.png b/docs/images/overview-screenshot.png
index 2117fcf6..8bb87abe 100644
Binary files a/docs/images/overview-screenshot.png and b/docs/images/overview-screenshot.png differ
diff --git a/docs/settings.md b/docs/settings.md
index bde6e049..af6beff6 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -25,6 +25,21 @@ The default WooCommerce User Session Handler is responsible for capturing cart a
The settings is simple to understand and likely to be enabled if your using a WC extension that uses a product type that isn't support by WooGraphQL out-of-box. When enabled it will substitute the missing type with the SimpleProduct type. This way you can still you the product type and possibly pull what extra data you need from the `Product`'s `metaData` field.
+
+
+## Enable User Session transferring URLs
+
+This settings when enabled activated the WooCommerce Session-backed nonce generator and transfer session endpoint for passing a user's session from a client to the WordPress installation, the primary use of these nonces are to create authorizing URLs that enable the user to travel to the backend as if it were a part of the front-end application. This setting is disabled if the QL Session Handler is disabled as it required for nonce generation to work. The next four settings are all about customizing the names of different parts of the authorizing URL..
+
+### Endpoint for Authorizing URLs
+
+The endpoint (path) for transferring user sessions on the site. Defaults to `transfer-session`.
+
+### Cart URL nonce name, Checkout URL nonce name, and Add Payment Method URL nonce name
+
+The name of the nonce param for each respective URLs. They have to be unique and cannot be identical.
+
+Uses these settings alone is very insecure. It's highly recommended that specific measures be taken on the client to further secure the WP backend and end-user's data.
## WooGraphQL Pro Settings
These settings allow you to enable or disable the GraphQL schema types, queries, and mutations for various WooCommerce extensions supported by WooGraphQL Pro. This is useful if you have one of the supported extensions installed and activated but don't need it exposed to the GraphQL API, keeping the schema lightweight.
diff --git a/includes/admin/class-general.php b/includes/admin/class-general.php
index 217a1070..e67833a9 100644
--- a/includes/admin/class-general.php
+++ b/includes/admin/class-general.php
@@ -12,19 +12,56 @@ namespace WPGraphQL\WooCommerce\Admin;
*/
class General extends Section {
+ /**
+ * Returns the other nonce values besides the one provided.
+ *
+ * @param string $excluded Slug of nonce value to be excluded.
+ *
+ * @return array
+ */
+ public static function get_other_nonce_values( $excluded ) {
+ $nonce_values = apply_filters(
+ 'woographql_authorizing_url_nonce_values',
+ [
+ 'cart_url' => woographql_setting( 'cart_url_nonce_param', '_wc_cart' ),
+ 'checkout_url' => woographql_setting( 'checkout_url_nonce_param', '_wc_checkout' ),
+ 'add_payment_method_url' => woographql_setting( 'add_payment_method_url_nonce_param', '_wc_payment' ),
+ ]
+ );
+
+ return array_values( array_diff_key( $nonce_values, [ $excluded => '' ] ) );
+ }
+
/**
* Returns General settings fields.
*
* @return array
*/
public static function get_fields() {
+ $custom_endpoint = apply_filters( 'woographql_authorizing_url_endpoint', null );
+ $enabled_authorizing_url_fields = array_keys( woographql_setting( 'enable_authorizing_url_fields', [] ) );
+ $all_urls_checked = apply_filters(
+ 'woographql_enabled_authorizing_url_fields',
+ [
+ 'cart_url' => 'cart_url',
+ 'checkout_url' => 'checkout_url',
+ 'add_payment_method_url' => 'add_payment_method_url',
+ ]
+ );
+
+ $cart_url_hardcoded = defined( 'CART_URL_NONCE_PARAM' ) && ! empty( CART_URL_NONCE_PARAM );
+ $checkout_url_hardcoded = defined( 'CHECKOUT_URL_NONCE_PARAM' ) && ! empty( CHECKOUT_URL_NONCE_PARAM );
+ $add_payment_method_url_hardcoded = defined( 'ADD_PAYMENT_METHOD_URL_NONCE_PARAM' ) && ! empty( ADD_PAYMENT_METHOD_URL_NONCE_PARAM );
+
return [
[
- 'name' => 'disable_ql_session_handler',
- 'label' => __( 'Disable QL Session Handler', 'wp-graphql-woocommerce' ),
- 'desc' => __( 'The QL Session Handler takes over management of WooCommerce Session Management on WPGraphQL request replacing the usage of HTTP Cookies with JSON Web Tokens.', 'wp-graphql-woocommerce' ),
- 'type' => 'checkbox',
- 'default' => 'off',
+ 'name' => 'disable_ql_session_handler',
+ 'label' => __( 'Disable QL Session Handler', 'wp-graphql-woocommerce' ),
+ 'desc' => __( 'The QL Session Handler takes over management of WooCommerce Session Management on WPGraphQL request replacing the usage of HTTP Cookies with JSON Web Tokens.', 'wp-graphql-woocommerce' )
+ . ( defined( 'NO_QL_SESSION_HANDLER' ) ? __( ' This setting is disabled. The "NO_QL_SESSION_HANDLER" flag has been triggered with code', 'wp-graphql-woocommerce' ) : '' ),
+ 'type' => 'checkbox',
+ 'value' => defined( 'NO_QL_SESSION_HANDLER' ) ? 'on' : woographql_setting( 'disable_ql_session_handler', 'off' ),
+ 'disabled' => defined( 'NO_QL_SESSION_HANDLER' ) ? true : false,
],
[
'name' => 'enable_unsupported_product_type',
@@ -33,6 +70,108 @@ class General extends Section {
'type' => 'checkbox',
'default' => 'off',
],
+ [
+ 'name' => 'enable_authorizing_url_fields',
+ 'label' => __( 'Enable User Session transferring URLs', 'wp-graphql-woocommerce' ),
+ 'desc' => __( 'URL fields to add to the Customer type.', 'wp-graphql-woocommerce' )
+ . ( defined( 'WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS' ) ? __( ' This setting is disabled. The "WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS" flag has been triggered with code', 'wp-graphql-woocommerce' ) : '' ),
+ 'type' => 'multicheck',
+ 'options' => apply_filters(
+ 'woographql_settings_enable_authorizing_url_options',
+ [
+ 'cart_url' => __( 'Cart URL. Field name: cartUrl', 'wp-graphql-woocommerce' ),
+ 'checkout_url' => __( 'Checkout URL. Field name: checkoutUrl', 'wp-graphql-woocommerce' ),
+ 'add_payment_method_url' => __( 'Add Payment Method URL. Field name: addPaymentMethodUrl', 'wp-graphql-woocommerce' ),
+ ]
+ ),
+ 'value' => defined( 'WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS' ) ? $all_urls_checked : woographql_setting( 'enable_authorizing_url_fields', [] ),
+ 'disabled' => defined( 'WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS' ),
+ ],
+ [
+ 'name' => 'authorizing_url_endpoint',
+ 'label' => __( 'Endpoint for Authorizing URLs', 'wp-graphql-woocommerce' ),
+ 'desc' => sprintf(
+ /* translators: %1$s: Site URL, %2$s: WooGraphQL Auth Endpoint */
+ __( 'The endpoint (path) for transferring user sessions on the site. %1$s/%2$s.', 'wp-graphql-woocommerce' ),
+ site_url(),
+ woographql_setting( 'authorizing_url_endpoint', 'transfer-session' )
+ ),
+ 'type' => 'text',
+ 'default' => ! empty( $custom_endpoint ) ? $custom_endpoint : 'transfer-session',
+ 'disabled' => empty( $enabled_authorizing_url_fields ),
+ ],
+ [
+ 'name' => 'cart_url_nonce_param',
+ 'label' => __( 'Cart URL nonce name', 'wp-graphql-woocommerce' ),
+ 'desc' => __( 'Query parameter name of the nonce included in the "cartUrl" field', 'wp-graphql-woocommerce' )
+ . ( $cart_url_hardcoded ? __( ' This setting is disabled. The "CART_URL_NONCE_PARAM" flag has been set with code', 'wp-graphql-woocommerce' ) : '' ),
+ 'type' => 'text',
+ 'value' => $cart_url_hardcoded ? CART_URL_NONCE_PARAM : woographql_setting( 'cart_url_nonce_param', '_wc_cart' ),
+ 'disabled' => defined( 'CART_URL_NONCE_PARAM' ) || ! in_array( 'cart_url', $enabled_authorizing_url_fields, true ),
+ 'sanitize_callback' => function ( $value ) {
+ $other_nonces = self::get_other_nonce_values( 'cart_url' );
+ if ( in_array( $value, $other_nonces, true ) ) {
+ add_settings_error(
+ 'cart_url_nonce_param',
+ 'unique',
+ __( 'The Cart URL nonce name field must be unique', 'wp-graphql-woocommerce' ),
+ 'error'
+ );
+
+ return '_wc_cart';
+ }
+
+ return $value;
+ },
+ ],
+ [
+ 'name' => 'checkout_url_nonce_param',
+ 'label' => __( 'Checkout URL nonce name', 'wp-graphql-woocommerce' ),
+ 'desc' => __( 'Query parameter name of the nonce included in the "checkoutUrl" field', 'wp-graphql-woocommerce' )
+ . ( $checkout_url_hardcoded ? __( ' This setting is disabled. The "CHECKOUT_URL_NONCE_PARAM" flag has been set with code', 'wp-graphql-woocommerce' ) : '' ),
+ 'type' => 'text',
+ 'value' => $checkout_url_hardcoded ? CHECKOUT_URL_NONCE_PARAM : woographql_setting( 'checkout_url_nonce_param', '_wc_checkout' ),
+ 'disabled' => defined( 'CHECKOUT_URL_NONCE_PARAM' ) || ! in_array( 'checkout_url', $enabled_authorizing_url_fields, true ),
+ 'sanitize_callback' => function ( $value ) {
+ $other_nonces = self::get_other_nonce_values( 'checkout_url' );
+ if ( in_array( $value, $other_nonces, true ) ) {
+ add_settings_error(
+ 'checkout_url_nonce_param',
+ 'unique',
+ __( 'The Checkout URL nonce name field must be unique', 'wp-graphql-woocommerce' ),
+ 'error'
+ );
+
+ return '_wc_checkout';
+ }
+
+ return $value;
+ },
+ ],
+ [
+ 'name' => 'add_payment_method_url_nonce_param',
+ 'label' => __( 'Add Payment Method URL nonce name', 'wp-graphql-woocommerce' ),
+ 'desc' => __( 'Query parameter name of the nonce included in the "addPaymentMethodUrl" field', 'wp-graphql-woocommerce' )
+ . ( $add_payment_method_url_hardcoded ? __( ' This setting is disabled. The "ADD_PAYMENT_METHOD_URL_NONCE_PARAM" flag has been set with code', 'wp-graphql-woocommerce' ) : '' ),
+ 'type' => 'text',
+ 'value' => $add_payment_method_url_hardcoded ? ADD_PAYMENT_METHOD_URL_NONCE_PARAM : woographql_setting( 'add_payment_method_url_nonce_param', '_wc_payment' ),
+ 'disabled' => defined( 'ADD_PAYMENT_METHOD_URL_NONCE_PARAM' ) || ! in_array( 'add_payment_method_url', $enabled_authorizing_url_fields, true ),
+ 'sanitize_callback' => function ( $value ) {
+ $other_nonces = self::get_other_nonce_values( 'add_payment_method_url' );
+ if ( in_array( $value, $other_nonces, true ) ) {
+ add_settings_error(
+ 'add_payment_method_url_nonce_param',
+ 'unique',
+ __( 'The Add Payment Method URL nonce name field must be unique', 'wp-graphql-woocommerce' ),
+ 'error'
+ );
+
+ return '_wc_payment';
+ }
+
+ return $value;
+ },
+ ],
];
}
}
diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php
index f18874d2..c103973d 100644
--- a/includes/class-type-registry.php
+++ b/includes/class-type-registry.php
@@ -13,13 +13,16 @@ namespace WPGraphQL\WooCommerce;
*/
class Type_Registry {
+
/**
* Registers WooGraphQL types, connections, unions, and mutations to GraphQL schema
*
* @param \WPGraphQL\Registry\TypeRegistry $type_registry Instance of the WPGraphQL TypeRegistry.
*/
public function init( \WPGraphQL\Registry\TypeRegistry $type_registry ) {
- // Enumerations.
+ /**
+ * Enumerations.
+ */
Type\WPEnum\Backorders::register();
Type\WPEnum\Catalog_Visibility::register();
Type\WPEnum\Countries::register();
@@ -43,7 +46,9 @@ class Type_Registry {
Type\WPEnum\Id_Type_Enums::register();
Type\WPEnum\Cart_Error_Type::register();
- // InputObjects.
+ /**
+ * InputObjects.
+ */
Type\WPInputObject\Cart_Item_Input::register();
Type\WPInputObject\Customer_Address_Input::register();
Type\WPInputObject\Product_Attribute_Input::register();
@@ -58,14 +63,18 @@ class Type_Registry {
Type\WPInputObject\Product_Taxonomy_Input::register();
Type\WPInputObject\Orderby_Inputs::register();
- // Interfaces.
+ /**
+ * Interfaces.
+ */
Type\WPInterface\Product::register_interface();
Type\WPInterface\Attribute::register_interface( $type_registry );
Type\WPInterface\Product_Attribute::register_interface( $type_registry );
Type\WPInterface\Cart_Error::register_interface( $type_registry );
Type\WPInterface\Payment_Token::register_interface( $type_registry );
- // Objects.
+ /**
+ * Objects.
+ */
Type\WPObject\Meta_Data_Type::register();
Type\WPObject\Downloadable_Item_Type::register();
Type\WPObject\Coupon_Type::register();
@@ -90,11 +99,27 @@ class Type_Registry {
Type\WPObject\Payment_Token_Types::register();
Type\WPObject\Country_State_Type::register();
- // Object fields.
+ /**
+ * Object fields.
+ */
Type\WPObject\Product_Category_Type::register_fields();
Type\WPObject\Root_Query::register_fields();
- // Connections.
+ // Register the following fields only if "disable_ql_session_handler" option is not on.
+ $ql_session_handled_enabled = ! WooCommerce_Filters::is_session_handler_disabled();
+ if ( $ql_session_handled_enabled ) {
+ Type\WPObject\Customer_Type::register_session_handler_fields();
+ }
+
+ // Register the following fields only if "disable_ql_session_handler" option is not "on" and some fields under the "enable_authorizing_url_fields" option are "selected".
+ $enabled_url_fields = WooCommerce_Filters::enabled_authorizing_url_fields();
+ if ( $ql_session_handled_enabled && ! empty( $enabled_url_fields ) ) {
+ Type\WPObject\Customer_Type::register_authorizing_url_fields( array_keys( $enabled_url_fields ) );
+ }
+
+ /**
+ * Connections.
+ */
Connection\Posts::register_connections();
Connection\WC_Terms::register_connections();
Connection\Comments::register_connections();
@@ -108,7 +133,9 @@ class Type_Registry {
Connection\Shipping_Methods::register_connections();
Connection\Payment_Gateways::register_connections();
- // Mutations.
+ /**
+ * Mutations.
+ */
Mutation\Customer_Register::register_mutation();
Mutation\Customer_Update::register_mutation();
Mutation\Cart_Add_Item::register_mutation();
@@ -135,5 +162,6 @@ class Type_Registry {
Mutation\Coupon_Delete::register_mutation();
Mutation\Payment_Method_Delete::register_mutation();
Mutation\Payment_Method_Set_Default::register_mutation();
+ Mutation\Update_Session::register_mutation();
}
}
diff --git a/includes/class-woocommerce-filters.php b/includes/class-woocommerce-filters.php
index e4183fa2..8a2428f7 100644
--- a/includes/class-woocommerce-filters.php
+++ b/includes/class-woocommerce-filters.php
@@ -8,6 +8,8 @@
namespace WPGraphQL\WooCommerce;
+use WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce as WooGraphQL;
+
/**
* Class WooCommerce_Filters
*/
@@ -43,7 +45,43 @@ class WooCommerce_Filters {
* @return boolean
*/
public static function is_session_handler_disabled() {
- return 'on' === woographql_setting( 'disable_ql_session_handler', 'off' );
+ return defined( 'NO_QL_SESSION_HANDLER' ) || 'on' === woographql_setting( 'disable_ql_session_handler', 'off' );
+ }
+
+ /**
+ * Returns array of enabled authorizing URL field slugs.
+ *
+ * @return array
+ */
+ public static function enabled_authorizing_url_fields() {
+ if ( defined( 'WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS' ) ) {
+ return apply_filters(
+ 'woographql_enabled_authorizing_url_fields',
+ [
+ 'cart_url' => 'cart_url',
+ 'checkout_url' => 'checkout_url',
+ 'add_payment_method_url' => 'add_payment_method_url',
+ ]
+ );
+ }
+ return woographql_setting( 'enable_authorizing_url_fields', [] );
+ }
+
+ /**
+ * Return the nonce query parameter name for the provided field.
+ *
+ * @param string $field URL field slug.
+ *
+ * @return string null
+ */
+ public static function get_authorizing_url_nonce_param_name( $field ) {
+ $flag_name = strtoupper( $field );
+ $hardcoded_name = defined( "{$flag_name}_NONCE_PARAM" ) ? constant( "{$flag_name}_NONCE_PARAM" ) : false;
+ if ( ! empty( $hardcoded_name ) ) {
+ return $hardcoded_name;
+ }
+
+ return woographql_setting( "{$field}_nonce_param", null );
}
/**
@@ -55,6 +93,11 @@ class WooCommerce_Filters {
public static function woocommerce_session_handler( $session_class ) {
if ( \WPGraphQL\Router::is_graphql_http_request() ) {
$session_class = '\WPGraphQL\WooCommerce\Utils\QL_Session_Handler';
+ } elseif ( WooGraphQL::auth_router_is_enabled() ) {
+ require_once get_includes_directory() . 'utils/class-protected-router.php';
+ require_once get_includes_directory() . 'utils/class-transfer-session-handler.php';
+
+ $session_class = Utils\Protected_Router::is_auth_request() ? '\WPGraphQL\WooCommerce\Utils\Transfer_Session_Handler' : $session_class;
}
return $session_class;
diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php
index 4c163fff..f23cce14 100644
--- a/includes/class-wp-graphql-woocommerce.php
+++ b/includes/class-wp-graphql-woocommerce.php
@@ -302,6 +302,7 @@ if ( ! class_exists( '\WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce' ) ) :
require $include_directory_path . 'mutation/class-review-update.php';
require $include_directory_path . 'mutation/class-payment-method-delete.php';
require $include_directory_path . 'mutation/class-payment-method-set-default.php';
+ require $include_directory_path . 'mutation/class-update-session.php';
// Include connection class/function files.
require $include_directory_path . 'connection/wc-cpt-connection-args.php';
@@ -376,6 +377,26 @@ if ( ! class_exists( '\WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce' ) ) :
}//end if
}
+ /**
+ * Returns true if any authorizing urls are enabled.
+ *
+ * @return bool
+ */
+ public static function auth_router_is_enabled() {
+ return defined( 'WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS' )
+ || ! empty( array_keys( woographql_setting( 'enable_authorizing_url_fields', [] ) ) );
+ }
+
+ /**
+ * Import and setups Protected_Router class instance.
+ *
+ * @return void
+ */
+ public static function load_auth_router() {
+ require get_includes_directory() . 'utils/class-protected-router.php';
+ add_action( 'after_setup_theme', [ Utils\Protected_Router::class, 'instance' ] );
+ }
+
/**
* Sets up WooGraphQL schema.
*/
diff --git a/includes/model/class-customer.php b/includes/model/class-customer.php
index d7908bb3..cb06be3a 100644
--- a/includes/model/class-customer.php
+++ b/includes/model/class-customer.php
@@ -58,7 +58,7 @@ class Customer extends Model {
if ( empty( $this->fields ) ) {
$this->fields = [
'ID' => function() {
- return ( ! empty( $this->data->get_id() ) ) ? $this->data->get_id() : \WC()->session->_customer_id;
+ return ( ! empty( $this->data->get_id() ) ) ? $this->data->get_id() : \WC()->session->get_customer_id();
},
'id' => function() {
return ( ! empty( $this->data->get_id() ) )
diff --git a/includes/mutation/class-update-session.php b/includes/mutation/class-update-session.php
new file mode 100644
index 00000000..0eed1ff9
--- /dev/null
+++ b/includes/mutation/class-update-session.php
@@ -0,0 +1,121 @@
+ self::get_input_fields(),
+ 'outputFields' => self::get_output_fields(),
+ 'mutateAndGetPayload' => self::mutate_and_get_payload(),
+ ]
+ );
+ }
+
+ /**
+ * Defines the mutation input field configuration
+ *
+ * @return array
+ */
+ public static function get_input_fields() {
+ return [
+ 'sessionData' => [
+ 'type' => [ 'list_of' => 'MetaDataInput' ],
+ 'description' => __( 'Data to be persisted in the session.', 'wp-graphql-woocommerce' ),
+ ],
+ ];
+ }
+
+ /**
+ * Defines the mutation output field configuration
+ *
+ * @return array
+ */
+ public static function get_output_fields() {
+ return [
+ 'session' => [
+ 'type' => [ 'list_of' => 'MetaData' ],
+ 'resolve' => function ( $payload ) {
+ $session_data = \WC()->session->get_session_data();
+ $session = [];
+ foreach ( $session_data as $key => $value ) {
+ $meta = new \stdClass();
+ $meta->id = null;
+ $meta->key = $key;
+ $meta->value = maybe_unserialize( $value );
+ $session[] = $meta;
+ }
+
+ return $session;
+ },
+ ],
+ 'customer' => [
+ 'type' => 'Customer',
+ 'resolve' => function () {
+ return new Customer( 'session' );
+ },
+ ],
+ ];
+ }
+
+ /**
+ * Defines the mutation data modification closure.
+ *
+ * @return callable
+ */
+ public static function mutate_and_get_payload() {
+ return function( $input, AppContext $context, ResolveInfo $info ) {
+ Cart_Mutation::check_session_token();
+
+ // Guard against missing input.
+ if ( empty( $input['sessionData'] ) ) {
+ throw new UserError( __( 'No session data provided', 'wp-graphql-woocommerce' ) );
+ }
+ $session_data_input = $input['sessionData'];
+
+ // Save session data input.
+ foreach ( $session_data_input as $meta ) {
+ \WC()->session->set( $meta['key'], $meta['value'] );
+ }
+
+ if ( is_a( \WC()->session, '\WC_Session_Handler' ) ) {
+ \WC()->session->save_data();
+ }
+
+ do_action( 'woographql_update_session', true );
+
+ // Process errors or return successful.
+ $notices = \WC()->session->get( 'wc_notices' );
+ if ( ! empty( $notices['error'] ) ) {
+ $error_messages = implode( ' ', array_column( $notices['error'], 'notice' ) );
+ \wc_clear_notices();
+ throw new UserError( $error_messages );
+ } else {
+ return [ 'status' => 'SUCCESS' ];
+ }
+ };
+ }
+}
diff --git a/includes/type/object/class-customer-type.php b/includes/type/object/class-customer-type.php
index d0bb7c9e..1fc80be9 100644
--- a/includes/type/object/class-customer-type.php
+++ b/includes/type/object/class-customer-type.php
@@ -38,6 +38,10 @@ class Customer_Type {
'databaseId' => [
'type' => 'Int',
'description' => __( 'The ID of the customer in the database', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ $database_id = absint( $source->ID );
+ return ! empty( $database_id ) ? $database_id : null;
+ },
],
'isVatExempt' => [
'type' => 'Boolean',
@@ -110,8 +114,28 @@ class Customer_Type {
'type' => 'Boolean',
'description' => __( 'Return the date customer was last updated', 'wp-graphql-woocommerce' ),
],
-
'metaData' => Meta_Data_Type::get_metadata_field_definition(),
+ 'session' => [
+ 'type' => [ 'list_of' => 'MetaData' ],
+ 'description' => __( 'Session data for the viewing customer', 'wp-graphql-woocommerce' ),
+ 'resolve' => function ( $source ) {
+ if ( (string) \WC()->session->get_customer_id() === (string) $source->ID ) {
+ $session_data = \WC()->session->get_session_data();
+ $session = [];
+ foreach ( $session_data as $key => $value ) {
+ $meta = new \stdClass();
+ $meta->id = null;
+ $meta->key = $key;
+ $meta->value = maybe_unserialize( $value );
+ $session[] = $meta;
+ }
+
+ return $session;
+ }
+
+ throw new UserError( __( 'It\'s not possible to access another user\'s session data', 'wp-graphql-woocommerce' ) );
+ },
+ ],
],
$other_fields,
);
@@ -155,6 +179,8 @@ class Customer_Type {
/**
* Registers Customer WPObject type and related fields.
+ *
+ * @return void
*/
public static function register() {
register_graphql_object_type(
@@ -181,46 +207,6 @@ class Customer_Type {
]
);
- // Register session token fields if QL_Session_Handler is enabled.
- if ( 'off' === woographql_setting( 'disable_ql_session_handler', 'off' ) ) {
- /**
- * Register the "sessionToken" field to the "Customer" type.
- */
- register_graphql_field(
- 'Customer',
- 'sessionToken',
- [
- 'type' => 'String',
- 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ),
- 'resolve' => function( $source ) {
- if ( \get_current_user_id() === $source->ID || 'guest' === $source->id ) {
- return apply_filters( 'graphql_customer_session_token', \WC()->session->build_token() );
- }
-
- return null;
- },
- ]
- );
- /**
- * Register the "wooSessionToken" field to the "User" type.
- */
- register_graphql_field(
- 'User',
- 'wooSessionToken',
- [
- 'type' => 'String',
- 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ),
- 'resolve' => function( $source ) {
- if ( \get_current_user_id() === $source->userId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
- return apply_filters( 'graphql_customer_session_token', \WC()->session->build_token() );
- }
-
- return null;
- },
- ]
- );
- }//end if
-
/**
* Register "availablePaymentMethods" field to "Customer" type.
*/
@@ -275,4 +261,211 @@ class Customer_Type {
]
);
}
+
+ /**
+ * Registers fields that require the "QL_Session_Handler" class to work.
+ *
+ * @return void
+ */
+ public static function register_session_handler_fields() {
+ /**
+ * Register the "sessionToken" field to the "Customer" type.
+ */
+ register_graphql_field(
+ 'Customer',
+ 'sessionToken',
+ [
+ 'type' => 'String',
+ 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ if ( \get_current_user_id() === $source->ID || 'guest' === $source->id ) {
+ return apply_filters( 'graphql_customer_session_token', \WC()->session->build_token() );
+ }
+
+ return null;
+ },
+ ]
+ );
+ /**
+ * Register the "wooSessionToken" field to the "User" type.
+ */
+ register_graphql_field(
+ 'User',
+ 'wooSessionToken',
+ [
+ 'type' => 'String',
+ 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ if ( \get_current_user_id() === $source->userId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+ return apply_filters( 'graphql_customer_session_token', \WC()->session->build_token() );
+ }
+
+ return null;
+ },
+ ]
+ );
+ }
+
+
+ /**
+ * Registers selected authorizing_url_fields
+ *
+ * @param array $fields_to_register Slugs of fields.
+ * @return void
+ */
+ public static function register_authorizing_url_fields( $fields_to_register ) {
+ if ( in_array( 'cart_url', $fields_to_register, true ) ) {
+ register_graphql_fields(
+ 'Customer',
+ [
+ 'cartUrl' => [
+ 'type' => 'String',
+ 'description' => __( 'A nonced link to the cart page. By default, it expires in 1 hour.', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ // Get current customer and user ID.
+ $customer_id = $source->ID;
+ $current_user_id = get_current_user_id();
+
+ // Return null if current user not user being queried.
+ if ( 0 !== $current_user_id && $current_user_id !== $customer_id ) {
+ return null;
+ }
+
+ // Build nonced url as an unauthenticated user.
+ $nonce_name = woographql_setting( 'cart_url_nonce_param', '_wc_cart' );
+ $url = add_query_arg(
+ [
+ 'session_id' => $customer_id,
+ $nonce_name => woographql_create_nonce( "load-cart_{$customer_id}" ),
+ ],
+ site_url( woographql_setting( 'authorizing_url_endpoint', 'transfer-session' ) )
+ );
+
+ return esc_url_raw( $url );
+ },
+ ],
+ 'cartNonce' => [
+ 'type' => 'String',
+ 'description' => __( 'A nonce for the cart page. By default, it expires in 1 hour.', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ // Get current customer and user ID.
+ $customer_id = $source->ID;
+ $current_user_id = get_current_user_id();
+
+ // Return null if current user not user being queried.
+ if ( 0 !== $current_user_id && $current_user_id !== $customer_id ) {
+ return null;
+ }
+
+ return woographql_create_nonce( "load-cart_{$customer_id}" );
+ },
+ ],
+ ]
+ );
+ }//end if
+
+ if ( in_array( 'checkout_url', $fields_to_register, true ) ) {
+ register_graphql_fields(
+ 'Customer',
+ [
+ 'checkoutUrl' => [
+ 'type' => 'String',
+ 'description' => __( 'A nonce link to the checkout page for session user. Expires in 24 hours.', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ // Get current customer and user ID.
+ $customer_id = $source->ID;
+ $current_user_id = get_current_user_id();
+
+ // Return null if current user not user being queried.
+ if ( 0 !== $current_user_id && $current_user_id !== $customer_id ) {
+ return null;
+ }
+
+ // Build nonced url as an unauthenticated user.
+ $nonce_name = woographql_setting( 'checkout_url_nonce_param', '_wc_checkout' );
+ $url = add_query_arg(
+ [
+ 'session_id' => $customer_id,
+ $nonce_name => woographql_create_nonce( "load-checkout_{$customer_id}" ),
+ ],
+ site_url( woographql_setting( 'authorizing_url_endpoint', 'transfer-session' ) )
+ );
+
+ return esc_url_raw( $url );
+ },
+ ],
+ 'checkoutNonce' => [
+ 'type' => 'String',
+ 'description' => __( 'A nonce for the checkout page. By default, it expires in 1 hour.', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ // Get current customer and user ID.
+ $customer_id = $source->ID;
+ $current_user_id = get_current_user_id();
+
+ // Return null if current user not user being queried.
+ if ( 0 !== $current_user_id && $current_user_id !== $customer_id ) {
+ return null;
+ }
+
+ return woographql_create_nonce( "load-checkout_{$customer_id}" );
+ },
+ ],
+ ]
+ );
+ }//end if
+
+ if ( in_array( 'add_payment_method_url', $fields_to_register, true ) ) {
+ register_graphql_fields(
+ 'Customer',
+ [
+ 'addPaymentMethodUrl' => [
+ 'type' => 'String',
+ 'description' => __( 'A nonce link to the add payment method page for the authenticated user. Expires in 24 hours.', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ if ( ! is_user_logged_in() ) {
+ return null;
+ }
+
+ // Get current customer and user ID.
+ $customer_id = $source->ID;
+ $current_user_id = get_current_user_id();
+
+ // Return null if current user not user being queried.
+ if ( $current_user_id !== $customer_id ) {
+ return null;
+ }
+
+ // Build nonced url as an unauthenticated user.
+ $nonce_name = woographql_setting( 'add_payment_method_url_nonce_param', '_wc_payment' );
+ $url = add_query_arg(
+ [
+ 'session_id' => $customer_id,
+ $nonce_name => woographql_create_nonce( "load-account_{$customer_id}" ),
+ ],
+ site_url( woographql_setting( 'authorizing_url_endpoint', 'transfer-session' ) )
+ );
+
+ return esc_url_raw( $url );
+ },
+ ],
+ 'addPaymentMethodNonce' => [
+ 'type' => 'String',
+ 'description' => __( 'A nonce for the add payment method page. By default, it expires in 1 hour.', 'wp-graphql-woocommerce' ),
+ 'resolve' => function( $source ) {
+ // Get current customer and user ID.
+ $customer_id = $source->ID;
+ $current_user_id = get_current_user_id();
+
+ // Return null if current user not user being queried.
+ if ( 0 !== $current_user_id && $current_user_id !== $customer_id ) {
+ return null;
+ }
+
+ return woographql_create_nonce( "load-account_{$customer_id}" );
+ },
+ ],
+ ]
+ );
+ }//end if
+ }
}
diff --git a/includes/utils/class-protected-router.php b/includes/utils/class-protected-router.php
new file mode 100644
index 00000000..beebe00a
--- /dev/null
+++ b/includes/utils/class-protected-router.php
@@ -0,0 +1,345 @@
+is_auth_request() ) {
+ return;
+ }
+
+ /**
+ * Set is_home to false
+ */
+ $wp_query->is_home = false;
+
+ /**
+ * Process the GraphQL query Request
+ */
+ $this->process_auth_request();
+ }
+
+ /**
+ * Returns the name of all the valid nonce names.
+ *
+ * @return array
+ */
+ public static function get_nonce_names() {
+ $enabled_authorizing_url_fields = WooCommerce_Filters::enabled_authorizing_url_fields();
+ if ( empty( $enabled_authorizing_url_fields ) ) {
+ return [];
+ }
+ $nonce_names = [];
+ foreach ( array_keys( $enabled_authorizing_url_fields ) as $field ) {
+ $nonce_names[ $field ] = WooCommerce_Filters::get_authorizing_url_nonce_param_name( $field );
+ }
+ return array_filter( $nonce_names );
+ }
+
+ /**
+ * Returns the nonce action prefix for the provided field.
+ *
+ * @param string $field Field.
+ * @return string|null
+ */
+ public function get_nonce_prefix( $field ) {
+ switch ( $field ) {
+ case 'cart_url':
+ return 'load-cart_';
+ case 'checkout_url':
+ return 'load-checkout_';
+ case 'add_payment_method_url':
+ return 'load-account_';
+ default:
+ return apply_filters( 'woographql_auth_nonce_prefix', null, $field, $this );
+ }
+ }
+
+ /**
+ * Returns the target endpoint url for the provided field.
+ *
+ * @param string $field Field.
+ * @return string|null
+ */
+ public function get_target_endpoint( $field ) {
+ switch ( $field ) {
+ case 'cart_url':
+ return wc_get_endpoint_url( 'cart' );
+ case 'checkout_url':
+ return wc_get_endpoint_url( 'checkout' );
+ case 'add_payment_method_url':
+ return wc_get_account_endpoint_url( 'add-payment-method' );
+ default:
+ return apply_filters( 'woographql_auth_target_endpoint', null, $field, $this );
+ }
+ }
+
+ /**
+ * Redirects to homepage.
+ *
+ * @return void
+ */
+ private function redirect_to_home() {
+ status_header( 404 );
+ wp_safe_redirect( home_url() );
+ exit;
+ }
+
+ /**
+ * Send stable version of plugin to download.
+ *
+ * @throws \Exception Session not found.
+ *
+ * @return void
+ */
+ private function process_auth_request() {
+ // Bail early if session ID or nonce not found.
+ $nonce_names = $this->get_nonce_names();
+ if ( empty( $nonce_names ) ) {
+ $this->redirect_to_home();
+ }
+
+ $nonce_prefix = null;
+ $session_id = null;
+ $nonce = null;
+ foreach ( $nonce_names as $field => $nonce_param ) {
+ if ( in_array( $nonce_param, array_keys( $_REQUEST ), true ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $nonce_prefix = $this->get_nonce_prefix( $field );
+ $session_id = isset( $_REQUEST['session_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['session_id'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $nonce = isset( $_REQUEST[ $nonce_param ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_param ] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ break;
+ }
+ }
+
+ if ( empty( $nonce_prefix ) | empty( $session_id ) | empty( $nonce ) ) {
+ $this->redirect_to_home();
+ }
+
+ // Bail early if session user already authenticated.
+ if ( 0 !== get_current_user_id() && get_current_user_id() === absint( $session_id ) ) {
+ wp_safe_redirect( $this->get_target_endpoint( $field ) );
+ exit;
+ }
+
+ // Unauthenticate if current user not session user.
+ if ( 0 !== get_current_user_id() ) {
+ wp_clear_auth_cookie();
+ wp_set_current_user( 0 );
+ }
+
+ // Verify nonce.
+ if ( ! woographql_verify_nonce( $nonce, $nonce_prefix . $session_id ) ) {
+ $this->redirect_to_home();
+ }
+
+ // If Session ID is a user ID authenticate as session user.
+ if ( 0 !== absint( $session_id ) ) {
+ wp_clear_auth_cookie();
+ wp_set_current_user( $session_id );
+ wp_set_auth_cookie( $session_id );
+ }
+
+ // Read session data connected to session ID.
+ $session_data = \WC()->session->get_session( $session_id );
+
+ // We were passed a session ID, yet no session was found. Let's log this and bail.
+ if ( empty( $session_data ) ) {
+ // TODO: Switch to WC Notices.
+ throw new \Exception( 'Could not locate WooCommerce session on checkout' );
+ }
+
+ // Reinitialize session and save session cookie before redirect.
+ \WC()->session->init_session_cookie();
+
+ // Set the session variable.
+ foreach ( $session_data as $key => $value ) {
+ \WC()->session->set( $key, maybe_unserialize( $value ) );
+ }
+ \WC()->session->set_customer_session_cookie( true );
+
+ // After session has been restored on redirect to destination.
+ wp_safe_redirect( $this->get_target_endpoint( $field ) );
+ exit;
+ }
+}
+
diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php
index 23d81e04..de45a11d 100644
--- a/includes/utils/class-ql-session-handler.php
+++ b/includes/utils/class-ql-session-handler.php
@@ -98,6 +98,7 @@ class QL_Session_Handler extends WC_Session_Handler {
$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' ] );
@@ -115,7 +116,7 @@ class QL_Session_Handler extends WC_Session_Handler {
public function init_session_token() {
$token = $this->get_session_token();
- // Process existing session.
+ // 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;
@@ -357,7 +358,7 @@ class QL_Session_Handler extends WC_Session_Handler {
$this->_session_expiration = apply_filters(
'graphql_woocommerce_cart_session_expire',
// Seconds * Minutes * Hours * Days.
- time() + ( 60 * 60 * 24 * 14 )
+ $this->_session_issued + ( 60 * 60 * 24 * 14 )
);
// 13 Days.
$this->_session_expiring = $this->_session_expiration - ( 60 * 60 * 24 );
@@ -426,4 +427,38 @@ class QL_Session_Handler extends WC_Session_Handler {
*/
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;
+ }
+
}
diff --git a/includes/utils/class-session-transaction-manager.php b/includes/utils/class-session-transaction-manager.php
index 30705a61..4171a0df 100644
--- a/includes/utils/class-session-transaction-manager.php
+++ b/includes/utils/class-session-transaction-manager.php
@@ -103,6 +103,7 @@ class Session_Transaction_Manager {
'updateItemQuantities',
'updateShippingMethod',
'updateCustomer',
+ 'updateSession',
]
);
}
@@ -240,7 +241,7 @@ class Session_Transaction_Manager {
}
// Save transaction queue.
- set_transient( "woo_session_transactions_queue_{$this->session_handler->get_customer_id()}", $queue );
+ set_transient( "woo_session_transactions_queue_{$this->session_handler->get_customer_id()}", $queue, 5 * MINUTE_IN_SECONDS );
}
/**
diff --git a/includes/utils/class-transfer-session-handler.php b/includes/utils/class-transfer-session-handler.php
new file mode 100644
index 00000000..a33a55ef
--- /dev/null
+++ b/includes/utils/class-transfer-session-handler.php
@@ -0,0 +1,92 @@
+verify_auth_request_credentials_exists() ) {
+ return 0;
+ }
+ if ( ! isset( $_REQUEST['session_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ return 0;
+ }
+
+ return sanitize_text_field( wp_unslash( $_REQUEST['session_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ }
+
+ /**
+ * Reads in customer ID from query parameters if specific conditions are met otherwise
+ * a guest ID are generated as usual.
+ *
+ * @return string
+ */
+ public function generate_customer_id() {
+ $session_id = $this->get_posted_session_id();
+ if ( 0 !== $session_id ) {
+ return $session_id;
+ }
+
+ return parent::generate_customer_id();
+ }
+
+ /**
+ * Returns client session ID.
+ *
+ * @return string
+ */
+ public function get_client_session_id() {
+ $session_id = $this->get_posted_session_id();
+ $session_data = 0 !== $session_id ? $this->get_session( $session_id ) : null;
+
+ if ( ! empty( $session_data ) ) {
+ $client_session_id = $session_data['client_session_id'];
+ $client_session_id_expiration = $session_data['client_session_id_expiration'];
+ } else {
+ $client_session_id = $this->get( 'client_session_id', false );
+ $client_session_id_expiration = absint( $this->get( 'client_session_id_expiration', 0 ) );
+ }
+
+ if ( false !== $client_session_id && time() < $client_session_id_expiration ) {
+ return $client_session_id;
+ }
+
+ $client_session_id = '';
+
+ return $client_session_id;
+ }
+}
diff --git a/local/.htaccess b/local/.htaccess
index 12fdcfc8..2130d24f 100644
--- a/local/.htaccess
+++ b/local/.htaccess
@@ -4,8 +4,8 @@
# Any changes to the directives between these markers will be overwritten.
RewriteEngine On
+RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
-SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 0646b43b..86c6f924 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -1,6 +1,6 @@
-
+
Generally-applicable sniffs for WordPress plugins.
diff --git a/tests/_data/config.php b/tests/_data/config.php
index 4f126ffc..7951570c 100644
--- a/tests/_data/config.php
+++ b/tests/_data/config.php
@@ -12,6 +12,19 @@ if ( ! defined( 'WPGRAPHQL_WOOCOMMERCE_AUTOLOAD' ) && false !== getenv( 'WPGRAPH
define( 'WPGRAPHQL_WOOCOMMERCE_AUTOLOAD', true );
}
+if ( ! defined( 'WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS' ) ) {
+ define( 'WPGRAPHQL_WOOCOMMERCE_ENABLE_AUTH_URLS', true );
+}
+if ( ! defined( 'CART_URL_NONCE_PARAM' ) ) {
+ define( 'CART_URL_NONCE_PARAM', '_wc_cart' );
+}
+if ( ! defined( 'CHECKOUT_URL_NONCE_PARAM' ) ) {
+ define( 'CHECKOUT_URL_NONCE_PARAM', '_wc_checkout' );
+}
+if ( ! defined( 'ADD_PAYMENT_METHOD_URL_NONCE_PARAM' ) ) {
+ define( 'ADD_PAYMENT_METHOD_URL_NONCE_PARAM', '_wc_payment' );
+}
+
if ( ! defined( 'GRAPHQL_JWT_AUTH_SECRET_KEY' ) ) {
define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'testingtesting123' );
}
@@ -23,4 +36,3 @@ if ( ! defined( 'STRIPE_API_PUBLISHABLE_KEY' ) && false !== getenv( 'STRIPE_API_
if ( ! defined( 'STRIPE_API_SECRET_KEY' ) && false !== getenv( 'STRIPE_API_SECRET_KEY' ) ) {
define( 'STRIPE_API_SECRET_KEY', getenv( 'STRIPE_API_SECRET_KEY' ) );
}
-
diff --git a/tests/_support/Helper/GraphQLE2E.php b/tests/_support/Helper/GraphQLE2E.php
index da589030..d6b4ed86 100644
--- a/tests/_support/Helper/GraphQLE2E.php
+++ b/tests/_support/Helper/GraphQLE2E.php
@@ -705,6 +705,37 @@ class GraphQLE2E extends \Codeception\Module {
$product_catalog[ $product['post_title'] ] = $product_id;
}
+ // Create cart page.
+ $wpdb = $this->getModule( 'WPDb' );
+ $cart_page_id = $wpdb->havePostInDatabase(
+ [
+ 'post_type' => 'page',
+ 'post_title' => 'Cart',
+ 'post_name' => 'cart',
+ 'post_author' => 1,
+ 'post_content' => '[woocommerce_cart]',
+ 'post_status' => 'publish',
+ ]
+ );
+ update_option( 'woocommerce_cart_page_id', $cart_page_id );
+ $checkout_page_id = $wpdb->havePostInDatabase(
+ [
+ 'post_type' => 'page',
+ 'post_title' => 'Checkout',
+ 'post_name' => 'checkout',
+ 'post_author' => 1,
+ 'post_content' => '[woocommerce_checkout]',
+ 'post_status' => 'publish',
+ ]
+ );
+ update_option( 'woocommerce_checkout_page_id', $checkout_page_id );
+
+ global $wp_rewrite;
+ // Set the permalink structure
+ $wp_rewrite->set_permalink_structure( '/%postname%/' );
+ // Flush the rules and tell it to write htaccess
+ $wp_rewrite->flush_rules( true );
+
return $product_catalog;
}
@@ -790,6 +821,7 @@ class GraphQLE2E extends \Codeception\Module {
*/
public function haveAProductInTheDatabase( $args, &$product_id, $term = 'simple', &$term_id = 0 ) {
$wpdb = $this->getModule( 'WPDb' );
+
// Create Product
$product_id = $wpdb->havePostInDatabase(
array_replace_recursive(
@@ -867,4 +899,22 @@ class GraphQLE2E extends \Codeception\Module {
]
);
}
+
+ public function verifyRedirect( $startUrl, $endUrl, $redirectCode = 301 ) {
+ $phpBrowser = $this->getModule( 'WPBrowser' );
+ $guzzle = $phpBrowser->client;
+
+ // Disable the following of redirects
+ $guzzle->followRedirects( false );
+
+ $phpBrowser->_loadPage( 'GET', $startUrl );
+ $response = $guzzle->getInternalResponse();
+ $responseCode = $response->getStatusCode();
+ $locationHeader = $response->getHeader( 'Location' );
+
+ $this->assertEquals( $responseCode, $redirectCode );
+ $this->assertEquals( $endUrl, $locationHeader );
+
+ $guzzle->followRedirects( true );
+ }
}
diff --git a/tests/acceptance/CustomerProceedsToCheckoutCept.php b/tests/acceptance/CustomerProceedsToCheckoutCept.php
new file mode 100644
index 00000000..31cd0995
--- /dev/null
+++ b/tests/acceptance/CustomerProceedsToCheckoutCept.php
@@ -0,0 +1,97 @@
+getCatalog();
+
+// Flush permalinks.
+$I->loginAsAdmin();
+$I->amOnAdminPage( 'options-permalink.php' );
+$I->click( '#submit' );
+$I->logOut();
+
+// Make quick helper for managing the session token.
+$request_headers = function () use ( $I, &$last_request_headers ) {
+ $last_request_headers = [
+ 'woocommerce-session' => 'Session ' . $I->wantHTTPResponseHeaders( 'woocommerce-session' ),
+ ];
+
+ return $last_request_headers;
+};
+
+// Begin test.
+$I->wantTo( 'add items to the cart' );
+
+/**
+ * Add "T-Shirt" to cart and confirm response data.
+ */
+$add_to_cart_input = [
+ 'clientMutationId' => 'someId',
+ 'productId' => $product_catalog['t-shirt'],
+ 'quantity' => 3,
+];
+
+$success = $I->addToCart( $add_to_cart_input );
+
+$I->assertArrayNotHasKey( 'errors', $success );
+$I->assertArrayHasKey( 'data', $success );
+$I->assertArrayHasKey( 'addToCart', $success['data'] );
+$I->assertArrayHasKey( 'cartItem', $success['data']['addToCart'] );
+$I->assertArrayHasKey( 'key', $success['data']['addToCart']['cartItem'] );
+$shirt_key = $success['data']['addToCart']['cartItem']['key'];
+
+
+$I->wantTo( 'Set "client_session_id" and get nonced cart URL' );
+$update_session_mutation = '
+ mutation($input: UpdateSessionInput!) {
+ updateSession(input: $input) {
+ session {
+ id
+ key
+ value
+ }
+ customer { cartUrl }
+ }
+ }
+';
+$success = $I->sendGraphQLRequest(
+ $update_session_mutation,
+ [
+ 'sessionData' => [
+ [
+ 'key' => 'client_session_id',
+ 'value' => 'test-client-session-id',
+ ],
+ [
+ 'key' => 'client_session_id_expiration',
+ 'value' => (string) ( time() + 3600 ),
+ ],
+ ],
+ ],
+ $request_headers()
+);
+
+$I->assertArrayNotHasKey( 'errors', $success );
+$I->assertArrayHasKey( 'data', $success );
+$I->assertArrayHasKey( 'updateSession', $success['data'] );
+$I->assertArrayHasKey( 'session', $success['data']['updateSession'] );
+$session = $success['data']['updateSession']['session'];
+$session = array_column( $session, 'value', 'key' );
+$I->assertEquals( $session['client_session_id'], 'test-client-session-id' );
+$I->assertArrayHasKey( 'customer', $success['data']['updateSession'] );
+$I->assertArrayHasKey( 'cartUrl', $success['data']['updateSession']['customer'] );
+$cart_url = $success['data']['updateSession']['customer']['cartUrl'];
+
+$I->wantTo( 'Go cart page and confirm empty and session not seen' );
+$I->amOnPage( '/cart' );
+$I->see( 'Your cart is currently empty.' );
+
+$I->wantTo( 'Authenticate with cart url and confirm page redirect' );
+$I->stopFollowingRedirects();
+$I->amOnUrl( $cart_url );
+$I->seeResponseCodeIs( 302 );
+$I->followRedirect();
+$I->seeInCurrentUrl( '/cart/' );
+$I->startFollowingRedirects();
+
+$I->wantTo( 'Confirm cart not empty and T-shirt in cart.' );
+$I->see( 'T-Shirt' );
diff --git a/tests/functional/ProtectedRouterCest.php b/tests/functional/ProtectedRouterCest.php
new file mode 100644
index 00000000..7be45362
--- /dev/null
+++ b/tests/functional/ProtectedRouterCest.php
@@ -0,0 +1,226 @@
+product_catalog = $I->getCatalog();
+
+ // Flush permalinks.
+ $I->loginAsAdmin();
+ $I->amOnAdminPage( 'options-permalink.php' );
+ $I->click( '#submit' );
+ $I->logOut();
+
+ if ( ! defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) ) {
+ define( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY', 'testestestestest' );
+ }
+ }
+
+ public function _startNewSession( FunctionalTester $I ) {
+ $I->wantTo( 'Start new session by adding an item to the cart' );
+ /**
+ * Add t-shirt to the cart
+ */
+ $success = $I->addToCart(
+ [
+ 'clientMutationId' => 'someId',
+ 'productId' => $this->product_catalog['t-shirt'],
+ 'quantity' => 5,
+ ],
+ );
+
+ $I->assertArrayNotHasKey( 'errors', $success );
+ $I->assertArrayHasKey( 'data', $success );
+ $I->assertArrayHasKey( 'addToCart', $success['data'] );
+ $I->assertArrayHasKey( 'cartItem', $success['data']['addToCart'] );
+ $I->assertArrayHasKey( 'key', $success['data']['addToCart']['cartItem'] );
+ $key = $success['data']['addToCart']['cartItem']['key'];
+
+ /**
+ * Assert existence and validity of "woocommerce-session" HTTP header.
+ */
+ $I->seeHttpHeaderOnce( 'woocommerce-session' );
+ $session_token = $I->grabHttpHeader( 'woocommerce-session' );
+
+ return compact( 'key', 'session_token' );
+ }
+
+ public function _getLastRequestHeaders( $I ) {
+ $headers = [
+ 'woocommerce-session' => 'Session ' . $I->wantHTTPResponseHeaders( 'woocommerce-session' ),
+ ];
+
+ return $headers;
+ }
+
+ public function tryToProceedToCheckoutPage( FunctionalTester $I ) {
+ $session_data = $this->_startNewSession( $I );
+ $session_token = $session_data['session_token'];
+ // Retrieve and decode token for session_id.
+ JWT::$leeway = 60;
+ $token_data = ! empty( $session_token )
+ ? JWT::decode( $session_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) )
+ : null;
+ $session_token = $token_data->data->customer_id;
+
+ $I->wantTo( 'Get the session checkout URL' );
+ $query = 'query { customer { checkoutNonce } }';
+ $success = $I->sendGraphQLRequest(
+ $query,
+ null,
+ $this->_getLastRequestHeaders( $I )
+ );
+
+ // Assert "checkoutUrl" was received.
+ $I->assertArrayNotHasKey( 'errors', $success );
+ $I->assertArrayHasKey( 'data', $success );
+ $I->assertArrayHasKey( 'customer', $success['data'] );
+ $I->assertArrayHasKey( 'checkoutNonce', $success['data']['customer'] );
+ $checkout_nonce = $success['data']['customer']['checkoutNonce'];
+
+ $I->wantTo( 'Go checkout page and confirm session not seen' );
+ $I->amOnPage( '/checkout' );
+ $I->see( 'Your cart is currently empty.' );
+
+ $I->wantTo( 'Authenticate with nonced url and confirm page redirect to checkout page' );
+ $I->stopFollowingRedirects();
+
+ $wp_url = getenv( 'WORDPRESS_URL' );
+
+ $I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session_token}&_wc_checkout={$checkout_nonce}" );
+ $I->seeResponseCodeIs( 302 );
+ $I->followRedirect();
+ $I->seeInCurrentUrl( '/checkout/' );
+ $I->startFollowingRedirects();
+
+ $I->wantTo( 'Confirm session has been loaded.' );
+ $I->see( 'Checkout' );
+ $I->see( 'Apply Coupon' );
+ $I->see( 'T-Shirt' );
+ }
+
+
+ public function tryToProceedToCheckoutPageWithExpiredUrl( FunctionalTester $I ) {
+ $this->_startNewSession( $I );
+
+ $I->wantTo( 'Get the session checkout URL' );
+ $query = '
+ mutation($input: UpdateSessionInput!) {
+ updateSession(input: $input) {
+ session {
+ id
+ key
+ value
+ }
+ customer { checkoutUrl }
+ }
+ }
+ ';
+ $success = $I->sendGraphQLRequest(
+ $query,
+ [
+ 'sessionData' => [
+ [
+ 'key' => 'client_session_id',
+ 'value' => 'test-client-session-id',
+ ],
+ ],
+ ],
+ $this->_getLastRequestHeaders( $I )
+ );
+
+ // Assert updateSession was success.
+ $I->assertArrayNotHasKey( 'errors', $success );
+ $I->assertArrayHasKey( 'data', $success );
+ $I->assertArrayHasKey( 'updateSession', $success['data'] );
+ $I->assertArrayHasKey( 'session', $success['data']['updateSession'] );
+ $session = $success['data']['updateSession']['session'];
+ $session = array_column( $session, 'value', 'key' );
+ $I->assertEquals( $session['client_session_id'], 'test-client-session-id' );
+
+ // Assert "checkoutUrl" was received.
+ $I->assertArrayHasKey( 'customer', $success['data']['updateSession'] );
+ $I->assertArrayHasKey( 'checkoutUrl', $success['data']['updateSession']['customer'] );
+ $expired_checkout_url = $success['data']['updateSession']['customer']['checkoutUrl'];
+
+ $I->wantTo( 'Invalidate Checkout URL by updating the "client_session_id"' );
+ $success = $I->sendGraphQLRequest(
+ $query,
+ [
+ 'sessionData' => [
+ [
+ 'key' => 'client_session_id',
+ 'value' => 'new-test-client-session-id',
+ ],
+ ],
+ ],
+ $this->_getLastRequestHeaders( $I )
+ );
+
+ // Assert updateSession was success.
+ $I->assertArrayNotHasKey( 'errors', $success );
+ $I->assertArrayHasKey( 'data', $success );
+ $I->assertArrayHasKey( 'updateSession', $success['data'] );
+ $I->assertArrayHasKey( 'session', $success['data']['updateSession'] );
+ $session = $success['data']['updateSession']['session'];
+ $session = array_column( $session, 'value', 'key' );
+ $I->assertEquals( $session['client_session_id'], 'new-test-client-session-id' );
+
+ $I->wantTo( 'Go checkout page and confirm session not seen' );
+ $I->amOnPage( '/checkout' );
+ $I->see( 'Your cart is currently empty.' );
+
+ $I->wantTo( 'Attempt to authenticate with expired url and confirm page redirect to checkout page' );
+ $I->stopFollowingRedirects();
+ $I->amOnUrl( $expired_checkout_url );
+ $I->seeResponseCodeIs( 302 );
+ $I->followRedirect();
+ $I->dontSeeInCurrentUrl( '/checkout/' );
+ $I->startFollowingRedirects();
+ }
+
+
+ public function tryToProceedToCheckoutPageWithInvalidNonce( FunctionalTester $I ) {
+ $session_data = $this->_startNewSession( $I );
+ $session_token = $session_data['session_token'];
+ // Retrieve and decode token for session_id.
+ JWT::$leeway = 60;
+ $token_data = ! empty( $session_token )
+ ? JWT::decode( $session_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) )
+ : null;
+ $session_token = $token_data->data->customer_id;
+
+ $I->wantTo( 'Get the session checkout URL' );
+ $query = 'query { customer { checkoutUrl } }';
+ $success = $I->sendGraphQLRequest(
+ $query,
+ null,
+ $this->_getLastRequestHeaders( $I )
+ );
+
+ // Assert "checkoutUrl" was received.
+ $I->assertArrayNotHasKey( 'errors', $success );
+ $I->assertArrayHasKey( 'data', $success );
+ $I->assertArrayHasKey( 'customer', $success['data'] );
+ $I->assertArrayHasKey( 'checkoutUrl', $success['data']['customer'] );
+
+ $I->wantTo( 'Go checkout page and confirm session not seen' );
+ $I->amOnPage( '/checkout' );
+ $I->see( 'Your cart is currently empty.' );
+
+ $I->wantTo( 'Attempt to authenticate with nonced url and confirm page redirect to checkout page' );
+ $I->stopFollowingRedirects();
+
+ $wp_url = getenv( 'WORDPRESS_URL' );
+
+ $I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session_token}&_wc_checkout=12345" );
+ $I->seeResponseCodeIs( 302 );
+ $I->followRedirect();
+ $I->dontSeeInCurrentUrl( '/checkout/' );
+ $I->startFollowingRedirects();
+ }
+}
diff --git a/tests/wpunit/CustomerQueriesTest.php b/tests/wpunit/CustomerQueriesTest.php
index 388793f5..e3d01bad 100644
--- a/tests/wpunit/CustomerQueriesTest.php
+++ b/tests/wpunit/CustomerQueriesTest.php
@@ -1,6 +1,5 @@
assertQuerySuccessful( $response, $expected );
}
+
+ public function testAuthorizingUrlFields() {
+ // Reinitialize WC session with QL_Session_Handler set.
+ add_filter(
+ 'woocommerce_session_handler',
+ function( $session_class ) {
+ return '\WPGraphQL\WooCommerce\Utils\QL_Session_Handler';
+ }
+ );
+ \WC()->initialize_session();
+
+ // Create customer for later use.
+ $customer_id = $this->factory->customer->create();
+
+ // Create auth URLs query.
+ $query = '
+ query($id: ID) {
+ customer(id: $id) {
+ id
+ cartUrl
+ cartNonce
+ checkoutUrl
+ checkoutNonce
+ addPaymentMethodUrl
+ addPaymentMethodNonce
+ }
+ }
+ ';
+
+ /**
+ * Assert NULL values when querying as admin
+ */
+ $this->loginAsShopManager();
+ $variables = [ 'id' => $this->toRelayId( 'customer', $customer_id ) ];
+ $response = $this->graphql( compact( 'query', 'variables' ) );
+ $expected = [
+ $this->expectedField( 'customer.id', $this->toRelayId( 'customer', $customer_id ) ),
+ $this->expectedField( 'customer.cartUrl', self::IS_NULL ),
+ $this->expectedField( 'customer.cartNonce', self::IS_NULL ),
+ $this->expectedField( 'customer.checkoutUrl', self::IS_NULL ),
+ $this->expectedField( 'customer.checkoutNonce', self::IS_NULL ),
+ $this->expectedField( 'customer.addPaymentMethodUrl', self::IS_NULL ),
+ $this->expectedField( 'customer.addPaymentMethodNonce', self::IS_NULL ),
+ ];
+ $this->assertQuerySuccessful( $response, $expected );
+
+ /**
+ * Assert NOT NULL values when querying as admin
+ */
+ $this->loginAs( $customer_id );
+ $response = $this->graphql( compact( 'query' ) );
+ $expected = [
+ $this->expectedField( 'customer.id', $this->toRelayId( 'customer', $customer_id ) ),
+ $this->expectedField( 'customer.cartUrl', self::NOT_NULL ),
+ $this->expectedField( 'customer.cartNonce', self::NOT_NULL ),
+ $this->expectedField( 'customer.checkoutUrl', self::NOT_NULL ),
+ $this->expectedField( 'customer.checkoutNonce', self::NOT_NULL ),
+ $this->expectedField( 'customer.addPaymentMethodUrl', self::NOT_NULL ),
+ $this->expectedField( 'customer.addPaymentMethodNonce', self::NOT_NULL ),
+ ];
+ $this->assertQuerySuccessful( $response, $expected );
+ }
}
diff --git a/tests/wpunit/ProtectedRouterTest.php b/tests/wpunit/ProtectedRouterTest.php
new file mode 100644
index 00000000..224ef98e
--- /dev/null
+++ b/tests/wpunit/ProtectedRouterTest.php
@@ -0,0 +1,54 @@
+assertEquals( 'transfer-session', apply_filters( 'woographql_authorizing_url_endpoint', \WPGraphQL\WooCommerce\Utils\Protected_Router::$route ) );
+ }
+
+ /**
+ * Test to make sure that the rewrite rules properly include the graphql route
+ */
+ public function testGraphQLRewriteRule() {
+ global $wp_rewrite;
+ $route = apply_filters( 'woographql_authorizing_url_endpoint', \WPGraphQL\WooCommerce\Utils\Protected_Router::$route );
+ $this->assertArrayHasKey( $route . '/?$', $wp_rewrite->extra_rules_top );
+ }
+
+ public function testAddQueryVar() {
+ $query_vars = [];
+ $router = \WPGraphQL\WooCommerce\Utils\Protected_Router::instance();
+ $actual = $router->add_query_var( $query_vars );
+ $this->assertEquals( $actual, [ apply_filters( 'woographql_authorizing_url_endpoint', \WPGraphQL\WooCommerce\Utils\Protected_Router::$route ) ] );
+ }
+
+ public function testGetNonceNames() {
+ $router = \WPGraphQL\WooCommerce\Utils\Protected_Router::instance();
+ $this->assertEquals(
+ [
+ 'cart_url' => '_wc_cart',
+ 'checkout_url' => '_wc_checkout',
+ 'add_payment_method_url' => '_wc_payment',
+ ],
+ $router->get_nonce_names()
+ );
+ }
+
+ public function testGetNoncePrefix() {
+ $router = \WPGraphQL\WooCommerce\Utils\Protected_Router::instance();
+ $this->assertEquals( 'load-cart_', $router->get_nonce_prefix( 'cart_url' ) );
+ $this->assertEquals( 'load-checkout_', $router->get_nonce_prefix( 'checkout_url' ) );
+ $this->assertEquals( 'load-account_', $router->get_nonce_prefix( 'add_payment_method_url' ) );
+ $this->assertEquals( null, $router->get_nonce_prefix( 'invalid' ) );
+ }
+
+ public function testGetTargetEndpoint() {
+ $router = \WPGraphQL\WooCommerce\Utils\Protected_Router::instance();
+ $this->assertEquals( wc_get_endpoint_url( 'cart' ), $router->get_target_endpoint( 'cart_url' ) );
+ $this->assertEquals( wc_get_endpoint_url( 'checkout' ), $router->get_target_endpoint( 'checkout_url' ) );
+ $this->assertEquals( wc_get_account_endpoint_url( 'add-payment-method' ), $router->get_target_endpoint( 'add_payment_method_url' ) );
+ $this->assertEquals( null, $router->get_nonce_prefix( 'invalid' ) );
+ }
+}
diff --git a/tests/wpunit/QLSessionHandlerTest.php b/tests/wpunit/QLSessionHandlerTest.php
index e7aaddd4..3a71fada 100644
--- a/tests/wpunit/QLSessionHandlerTest.php
+++ b/tests/wpunit/QLSessionHandlerTest.php
@@ -148,4 +148,28 @@ class QLSessionHandlerTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap
$this->assertNotEquals( $old_token, $new_token, 'Tokens should not match' );
}
+
+ public function test_get_client_session_id() {
+ // Create session handler.
+ $session = new QL_Session_Handler();
+
+ // Assert an random string returned, when valid "client_session_id" and "client_session_id_expiration" are not set.
+ $session->init_session_cookie();
+ $this->assertNotEquals( '', $session->get_client_session_id() );
+
+ $session->init_session_cookie();
+ $_REQUEST['_wc_cart'] = 'test';
+ $this->assertNotEquals( '', $session->get_client_session_id() );
+
+ $session->init_session_cookie();
+ $session->set( 'client_session_id', 'test-client-session-id' );
+ $session->set( 'client_session_id_expiration', '1' );
+ $this->assertNotEquals( 'test-client-session-id', $session->get_client_session_id() );
+
+ // Assert "test-client-session-id" is returned, when valid "client_session_id" and "client_session_id_expiration" are set.
+ $session->init_session_cookie();
+ $session->set( 'client_session_id', 'test-client-session-id' );
+ $session->set( 'client_session_id_expiration', ( time() + 3600 ) );
+ $this->assertEquals( 'test-client-session-id', $session->get_client_session_id() );
+ }
}
diff --git a/tests/wpunit/TransferSessionHandlerTest.php b/tests/wpunit/TransferSessionHandlerTest.php
new file mode 100644
index 00000000..29937547
--- /dev/null
+++ b/tests/wpunit/TransferSessionHandlerTest.php
@@ -0,0 +1,73 @@
+session = new Transfer_Session_Handler();
+ }
+
+ public function testGenerateCustomerId() {
+ // Assert random customer ID is generated when invalid creds are provided.
+ $this->session->init_session_cookie();
+ $this->assertNotEquals( 'test-session-id', $this->session->get_customer_id() );
+
+ $_REQUEST['_wc_cart'] = 'test';
+ $this->session->init_session_cookie();
+ $this->assertNotEquals( 'test-session-id', $this->session->get_customer_id() );
+
+ unset( $_REQUEST['_wc_cart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $_REQUEST['session_id'] = 'test-session-id';
+ $this->session->init_session_cookie();
+ $this->assertNotEquals( 'test-session-id', $this->session->get_customer_id() );
+
+ // Assert "session_id" is returned, if proper creds are provided.
+ $_REQUEST['_wc_cart'] = 'test';
+ $_REQUEST['session_id'] = 'test-session-id';
+ $this->session->init_session_cookie();
+ $this->assertEquals( 'test-session-id', $this->session->get_customer_id() );
+ }
+
+ public function testGetClientSessionId() {
+ // Assert an empty string is return, when invalid creds are provided.
+ $this->session->init_session_cookie();
+ $this->assertEquals( '', $this->session->get_client_session_id() );
+
+ $this->session->init_session_cookie();
+ $_REQUEST['_wc_cart'] = 'test';
+ $this->assertEquals( '', $this->session->get_client_session_id() );
+
+ $this->session->init_session_cookie();
+ unset( $_REQUEST['_wc_cart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $_REQUEST['session_id'] = $this->session->get_client_session_id();
+ $this->assertEquals( '', $this->session->get_client_session_id() );
+
+ // Assert "test-client-session-id" is returned, if proper creds are provided.
+ $this->session->init_session_cookie();
+ $this->session->set( 'client_session_id', 'test-client-session-id' );
+ $this->session->set( 'client_session_id_expiration', ( time() + 3600 ) );
+ $_REQUEST['_wc_cart'] = 'test';
+ $_REQUEST['session_id'] = $this->session->get_customer_id();
+ $this->assertEquals( 'test-client-session-id', $this->session->get_client_session_id() );
+
+ // Assert an empty string is returned, because "client_session_id_expiration" is expired.
+ $this->session->init_session_cookie();
+ $this->session->set( 'client_session_id', 'test-client-session-id-2' );
+ $this->session->set( 'client_session_id_expiration', '1' );
+ $_REQUEST['_wc_cart'] = 'test';
+ $_REQUEST['session_id'] = $this->session->get_customer_id();
+ $this->assertEquals( '', $this->session->get_client_session_id() );
+ }
+}
diff --git a/tests/wpunit/UpdateSessionMutationTest.php b/tests/wpunit/UpdateSessionMutationTest.php
new file mode 100644
index 00000000..f38afad0
--- /dev/null
+++ b/tests/wpunit/UpdateSessionMutationTest.php
@@ -0,0 +1,66 @@
+factory->customer->create();
+ $this->loginAs( $registered );
+
+ // Create query.
+ $query = '
+ mutation($input: UpdateSessionInput!) {
+ updateSession(input: $input) {
+ session {
+ id
+ key
+ value
+ }
+ customer {
+ id
+ session {
+ id
+ key
+ value
+ }
+ }
+ }
+ }
+ ';
+
+ $variables = [
+ 'input' => [
+ 'sessionData' => [
+ [
+ 'key' => 'test-2',
+ 'value' => 'test-value',
+ ],
+ ],
+ ],
+ ];
+
+ /**
+ * Assert working.
+ */
+ $response = $this->graphql( compact( 'query', 'variables' ) );
+ $expected = [
+ $this->expectedObject(
+ 'updateSession.session.#',
+ [
+ $this->expectedField( 'key', 'test-2' ),
+ $this->expectedField( 'value', 'test-value' ),
+ ]
+ ),
+ $this->expectedField( 'updateSession.customer.id', $this->toRelayId( 'customer', $registered ) ),
+ $this->expectedObject(
+ 'updateSession.customer.session.#',
+ [
+ $this->expectedField( 'key', 'test-2' ),
+ $this->expectedField( 'value', 'test-value' ),
+ ]
+ ),
+ ];
+
+ $this->assertQuerySuccessful( $response, $expected );
+ }
+}
diff --git a/tests/wpunit/bootstrap.php b/tests/wpunit/bootstrap.php
index 863c96a9..546aceef 100644
--- a/tests/wpunit/bootstrap.php
+++ b/tests/wpunit/bootstrap.php
@@ -1,8 +1,4 @@