Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 62 additions & 45 deletions src/class-convertkit-api-v4.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 2.1.1
*
* @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;
}

Expand Down Expand Up @@ -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 2.1.1
*
* @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;
}

Expand Down Expand Up @@ -1432,39 +1454,55 @@ 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.
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 ) ) {
return $result;
switch ( $error ) {
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 ) ) {
return $result;
}

// Attempt the request again, now we have a new access token.
return $this->request( $endpoint, $method, $params, false );

case 'The access token is invalid':
$error = new WP_Error(
'convertkit_api_error',
$error,
$http_response_code
);

/**
* Perform any actions when an invalid access token was used.
*
* @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 );

// Return error.
return $error;

default:
return new WP_Error(
'convertkit_api_error',
$error,
$http_response_code
);
}

// Attempt the request again, now we have a new access token.
return $this->request( $endpoint, $method, $params, false );

// 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 ) {
Expand All @@ -1491,27 +1529,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.
*
Expand Down
26 changes: 0 additions & 26 deletions tests/Integration/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down