From 0c0541f8a03999e4702751e6ac2a7f0594306d19 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 10 Nov 2025 15:43:41 +0800 Subject: [PATCH 1/7] Add hook when refreshing token fails --- src/class-convertkit-api-v4.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index babda61..ccf0fb2 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1458,6 +1458,16 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i // If an error occured, bail. if ( is_wp_error( $result ) ) { + /** + * Perform any actions when refreshing an expired access token fails. + * + * @since 3.1.0 + * + * @param WP_Error $result Error. + * @param string $client_id OAuth Client ID. + */ + do_action( 'convertkit_api_request_refresh_token_error', $result, $this->client_id ); + return $result; } From 29ceaced634723330f7b3851698bc7c7850bb860 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 10 Nov 2025 16:15:00 +0800 Subject: [PATCH 2/7] Add Hooks on request() error --- src/class-convertkit-api-v4.php | 69 +++++++++++++++++---------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index ccf0fb2..ebec5c2 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1432,47 +1432,50 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i // Return the API error message as a WP_Error if the HTTP response code is a 4xx code. if ( $http_response_code >= 400 ) { + // Define the error message. $error = $this->get_error_message_string( $response ); $this->log( 'API: Error: ' . $error ); switch ( $http_response_code ) { - // If the HTTP response code is 401, and the error matches 'The access token expired', refresh the access token now - // and re-attempt the request. + // If the HTTP response code is 401, check the error matches 'The access token expired', refresh the access token now + // and attempt to re-attempt the request. case 401: - if ( $error !== 'The access token expired' ) { - break; - } - - // Don't automatically refresh the expired access token if we're not on a production environment. - // This prevents the same ConvertKit account used on both a staging and production site from - // reaching a race condition where the staging site refreshes the token first, resulting in - // the production site unable to later refresh its same expired access token. - if ( ! $this->is_production_site() ) { - break; - } - - // Refresh the access token. - $result = $this->refresh_token(); - - // If an error occured, bail. - if ( is_wp_error( $result ) ) { - /** - * Perform any actions when refreshing an expired access token fails. - * - * @since 3.1.0 - * - * @param WP_Error $result Error. - * @param string $client_id OAuth Client ID. - */ - do_action( 'convertkit_api_request_refresh_token_error', $result, $this->client_id ); - - return $result; + switch ( $error ) { + case 'The access token is invalid': + /** + * Perform any actions when an invalid access token was used. + * + * @since 3.1.0 + * + * @param string $client_id OAuth Client ID. + */ + do_action( 'convertkit_api_request_error_access_token_invalid', $this->client_id ); + break; + + case 'The access token expired': + // Attempt to refresh the access token. + $result = $this->refresh_token(); + + // If an error occured, bail. + if ( is_wp_error( $result ) ) { + /** + * Perform any actions when refreshing an expired access token fails. + * + * @since 3.1.0 + * + * @param string $client_id OAuth Client ID. + */ + do_action( 'convertkit_api_request_error_refresh_token_failed', $this->client_id ); + + return $result; + } + + // Attempt the request again, now we have a new access token. + return $this->request( $endpoint, $method, $params, false ); } - - // Attempt the request again, now we have a new access token. - return $this->request( $endpoint, $method, $params, false ); + break; // If a rate limit was hit, maybe try again. case 429: From 38ca4b420478e28a331c219e02cf5f8848a3fd4d Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 10 Nov 2025 16:18:02 +0800 Subject: [PATCH 3/7] Remove `is_production_site` private method This was used to prevent refreshing tokens on non-production sites, prior to the tenant_name option which allowed different tokens to be issued to the same client + oauth user on different sites --- src/class-convertkit-api-v4.php | 21 --------------------- tests/Integration/APITest.php | 26 -------------------------- 2 files changed, 47 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index ebec5c2..e4f6a1a 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1504,27 +1504,6 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i } - /** - * Helper method to determine the WordPress environment type, checking - * if the wp_get_environment_type() function exists in WordPress (versions - * older than WordPress 5.5 won't have this function). - * - * @since 2.0.2 - * - * @return bool - */ - private function is_production_site() { - - // If the WordPress wp_get_environment_type() function isn't available, - // assume this is a production site. - if ( ! function_exists( 'wp_get_environment_type' ) ) { - return true; - } - - return ( wp_get_environment_type() === 'production' ); - - } - /** * Inspects the given API response for errors, returning them as a string. * diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index fc8b465..ff9e405 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -528,32 +528,6 @@ public function testRefreshTokenWithInvalidToken() $this->assertEquals($result->get_error_code(), 'convertkit_api_error'); } - /** - * Test that making a call with an expired access token results in refresh_token() - * not being automatically called, when the WordPress site isn't a production site. - * - * @since 2.0.2 - * - * @return void - */ - public function testRefreshTokenWhenAccessTokenExpiredErrorOnNonProductionSite() - { - // If the refresh token action in the libraries is triggered when calling get_account(), the test failed. - add_action( - 'convertkit_api_refresh_token', - function() { - $this->fail('`convertkit_api_refresh_token` was triggered when calling `get_account` with an expired access token on a non-production site.'); - } - ); - - // Filter requests to mock the token expiry and refreshing the token. - add_filter( 'pre_http_request', array( $this, 'mockAccessTokenExpiredResponse' ), 10, 3 ); - add_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ), 10, 3 ); - - // Run request, which will trigger the above filters as if the token expired and refreshes automatically. - $result = $this->api->get_account(); - } - /** * Test that supplying no API credentials to the API class returns a WP_Error. * From 1116a0dafb74f7555b37bce939957e09e5c4b935 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 10 Nov 2025 17:48:42 +0800 Subject: [PATCH 4/7] Add actions --- src/class-convertkit-api-v4.php | 61 ++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index e4f6a1a..db5f220 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -377,6 +377,17 @@ public function get_access_token( $authorization_code ) { // If an error occured, log and return it now. if ( is_wp_error( $result ) ) { $this->log( 'API: Error: ' . $result->get_error_message() ); + + /** + * Perform any actions when obtaining an access token fails. + * + * @since 3.1.0 + * + * @param WP_Error $result Error from API. + * @param string $client_id OAuth Client ID. + */ + do_action( 'convertkit_api_get_access_token_error', $result, $this->client_id ); + return $result; } @@ -414,6 +425,17 @@ public function refresh_token() { // If an error occured, log and return it now. if ( is_wp_error( $result ) ) { $this->log( 'API: Error: ' . $result->get_error_message() ); + + /** + * Perform any actions when refreshing an expired access token fails. + * + * @since 3.1.0 + * + * @param WP_Error $result Error from API. + * @param string $client_id OAuth Client ID. + */ + do_action( 'convertkit_api_refresh_token_error', $result, $this->client_id ); + return $result; } @@ -1443,37 +1465,36 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i // and attempt to re-attempt the request. case 401: switch ( $error ) { - case 'The access token is invalid': - /** - * Perform any actions when an invalid access token was used. - * - * @since 3.1.0 - * - * @param string $client_id OAuth Client ID. - */ - do_action( 'convertkit_api_request_error_access_token_invalid', $this->client_id ); - break; - case 'The access token expired': // Attempt to refresh the access token. $result = $this->refresh_token(); // If an error occured, bail. if ( is_wp_error( $result ) ) { - /** - * Perform any actions when refreshing an expired access token fails. - * - * @since 3.1.0 - * - * @param string $client_id OAuth Client ID. - */ - do_action( 'convertkit_api_request_error_refresh_token_failed', $this->client_id ); - return $result; } // Attempt the request again, now we have a new access token. return $this->request( $endpoint, $method, $params, false ); + + default: + $error = new WP_Error( + 'convertkit_api_error', + $error, + $http_response_code + ); + + /** + * Perform any actions when an invalid access token was used. + * + * @since 3.1.0 + * + * @param string $client_id OAuth Client ID. + */ + do_action( 'convertkit_api_access_token_invalid', $error, $this->client_id ); + + // Return error. + return $error; } break; From dbad03b2054bf0de763d244bae0958dffe8e991e Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 10 Nov 2025 17:50:10 +0800 Subject: [PATCH 5/7] Fix version numbers --- src/class-convertkit-api-v4.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index db5f220..2b3ae8a 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -381,7 +381,7 @@ public function get_access_token( $authorization_code ) { /** * Perform any actions when obtaining an access token fails. * - * @since 3.1.0 + * @since 2.1.1 * * @param WP_Error $result Error from API. * @param string $client_id OAuth Client ID. @@ -429,7 +429,7 @@ public function refresh_token() { /** * Perform any actions when refreshing an expired access token fails. * - * @since 3.1.0 + * @since 2.1.1 * * @param WP_Error $result Error from API. * @param string $client_id OAuth Client ID. @@ -1487,7 +1487,7 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i /** * Perform any actions when an invalid access token was used. * - * @since 3.1.0 + * @since 2.1.1 * * @param string $client_id OAuth Client ID. */ From 3fc38922d6095547425594e5f9e0bce2f2241ad1 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 10 Nov 2025 19:39:13 +0800 Subject: [PATCH 6/7] PHPStan compat. --- src/class-convertkit-api-v4.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 2b3ae8a..957006e 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1461,8 +1461,6 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i $this->log( 'API: Error: ' . $error ); switch ( $http_response_code ) { - // If the HTTP response code is 401, check the error matches 'The access token expired', refresh the access token now - // and attempt to re-attempt the request. case 401: switch ( $error ) { case 'The access token expired': @@ -1489,6 +1487,7 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i * * @since 2.1.1 * + * @param WP_Error $error WP_Error object. * @param string $client_id OAuth Client ID. */ do_action( 'convertkit_api_access_token_invalid', $error, $this->client_id ); @@ -1496,9 +1495,7 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i // Return error. return $error; } - break; - // If a rate limit was hit, maybe try again. case 429: // If retry on rate limit hit is disabled, return a WP_Error. if ( ! $retry_if_rate_limit_hit ) { From c0e0d63bc2c9b385d4ba8f4e80a5b891d39510c0 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 11 Nov 2025 13:56:34 +0800 Subject: [PATCH 7/7] Only trigger `convertkit_api_access_token_invalid` when an invalid access token is used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Otherwise a 401 for e.g. member content will result in the access token wrongly being removed, when it’s a valid access token. --- src/class-convertkit-api-v4.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 957006e..bbe820f 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -1475,7 +1475,7 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i // Attempt the request again, now we have a new access token. return $this->request( $endpoint, $method, $params, false ); - default: + case 'The access token is invalid': $error = new WP_Error( 'convertkit_api_error', $error, @@ -1494,6 +1494,13 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i // Return error. return $error; + + default: + return new WP_Error( + 'convertkit_api_error', + $error, + $http_response_code + ); } case 429: