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
15 changes: 11 additions & 4 deletions includes/class-convertkit-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function __construct() {
}

// Update Access Token when refreshed by the API class.
add_action( 'convertkit_api_get_access_token', array( $this, 'update_credentials' ), 10, 2 );
add_action( 'convertkit_api_refresh_token', array( $this, 'update_credentials' ), 10, 2 );

}
Expand Down Expand Up @@ -500,18 +501,18 @@ public function get_defaults() {
}

/**
* Saves the new access token, refresh token and its expiry when the API
* class automatically refreshes an outdated access token.
* Saves the new access token, refresh token and its expiry, and schedules
* a WordPress Cron event to refresh the token on expiry.
*
* @since 2.5.0
* @since 2.8.3
*
* @param array $result New Access Token, Refresh Token and Expiry.
* @param string $client_id OAuth Client ID used for the Access and Refresh Tokens.
*/
public function update_credentials( $result, $client_id ) {

// Don't save these credentials if they're not for this Client ID.
// They're for another ConvertKit Plugin that uses OAuth.
// They're for another Kit Plugin that uses OAuth.
if ( $client_id !== CONVERTKIT_OAUTH_CLIENT_ID ) {
return;
}
Expand All @@ -524,6 +525,12 @@ public function update_credentials( $result, $client_id ) {
)
);

// Clear any existing scheduled WordPress Cron event.
wp_clear_scheduled_hook( 'convertkit_refresh_token' );

// Schedule a WordPress Cron event to refresh the token on expiry.
wp_schedule_single_event( ( $result['created_at'] + $result['expires_in'] ), 'convertkit_refresh_token' );

}

/**
Expand Down
51 changes: 51 additions & 0 deletions includes/cron-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,57 @@
* @author ConvertKit
*/

/**
* Refresh the OAuth access token, triggered by WordPress' Cron.
*
* @since 2.8.3
*/
function convertkit_refresh_token() {

// Get Settings and Log classes.
$settings = new ConvertKit_Settings();

// Bail if no existing access and refresh token exists.
if ( ! $settings->has_access_token() ) {
return;
}
if ( ! $settings->has_refresh_token() ) {
return;
}

// Initialize the API.
$api = new ConvertKit_API_V4(
CONVERTKIT_OAUTH_CLIENT_ID,
CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
$settings->get_access_token(),
$settings->get_refresh_token(),
$settings->debug_enabled(),
'cron_refresh_token'
);

// Refresh the token.
$result = $api->refresh_token();

// If an error occured, don't save the new tokens.
// Logging is handled by the ConvertKit_API_V4 class.
if ( is_wp_error( $result ) ) {
return;
}

$settings->save(
array(
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'token_expires' => ( $result['created_at'] + $result['expires_in'] ),
)
);

}

// Register action to run above function; this action is created by WordPress' wp_schedule_event() function
// in update_credentials() in the ConvertKit_Settings class.
add_action( 'convertkit_refresh_token', 'convertkit_refresh_token' );

/**
* Refresh the Posts Resource cache, triggered by WordPress' Cron.
*
Expand Down
61 changes: 57 additions & 4 deletions tests/Integration/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ class APITest extends WPTestCase
*/
private $api;

/**
* Holds the current timestamp, defined in setUp to fix
* it for all tests.
*
* @since 2.8.3
*
* @var int
*/
private $now = 0;

/**
* Performs actions before each test.
*
Expand All @@ -36,6 +46,9 @@ public function setUp(): void
{
parent::setUp();

// Set the current timestamp to the start of the test.
$this->now = strtotime( 'now' );

// Activate Plugin, to include the Plugin's constants in tests.
activate_plugins('convertkit/wp-convertkit.php');

Expand Down Expand Up @@ -77,7 +90,7 @@ public function testAccessTokenRefreshedAndSavedWhenExpired()

// 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 );
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );

// Run request, which will trigger the above filters as if the token expired and refreshes automatically.
$result = $this->api->get_account();
Expand All @@ -88,6 +101,46 @@ public function testAccessTokenRefreshedAndSavedWhenExpired()
$this->assertEquals( $settings->get_refresh_token(), $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'] );
}

/**
* Test that a WordPress Cron event is created when an access token is obtained.
*
* @since 2.8.3
*/
public function testCronEventCreatedWhenAccessTokenObtained()
{
// Mock request as if the API returned an access and refresh token when a request
// was made to refresh the token.
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );

// Run request, as if the access token was obtained successfully.
$result = $this->api->get_access_token( 'mockAuthCode' );

// Confirm the Cron event to refresh the access token was created, and the timestamp to
// run the refresh token call matches the expiry of the access token.
$nextScheduledTimestamp = wp_next_scheduled( 'convertkit_refresh_token' );
$this->assertEquals( $nextScheduledTimestamp, $this->now + 10000 );
}

/**
* Test that a WordPress Cron event is created when an access token is refreshed.
*
* @since 2.8.3
*/
public function testCronEventCreatedWhenTokenRefreshed()
{
// Mock request as if the API returned an access and refresh token when a request
// was made to refresh the token.
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );

// Run request, as if the token was refreshed.
$result = $this->api->refresh_token();

// Confirm the Cron event to refresh the access token was created, and the timestamp to
// run the refresh token call matches the expiry of the access token.
$nextScheduledTimestamp = wp_next_scheduled( 'convertkit_refresh_token' );
$this->assertEquals( $nextScheduledTimestamp, $this->now + 10000 );
}

/**
* Mocks an API response as if the Access Token expired.
*
Expand Down Expand Up @@ -138,15 +191,15 @@ public function mockAccessTokenExpiredResponse( $response, $parsed_args, $url )
* @param string $url Request URL.
* @return mixed
*/
public function mockRefreshTokenResponse( $response, $parsed_args, $url )
public function mockTokenResponse( $response, $parsed_args, $url )
{
// Only mock requests made to the /token endpoint.
if ( strpos( $url, 'https://api.kit.com/oauth/token' ) === false ) {
return $response;
}

// Remove this filter, so we don't end up in a loop when retrying the request.
remove_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ) );
remove_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ) );

// Return a mock access and refresh token for this API request, as calling
// refresh_token results in a new access and refresh token being provided,
Expand All @@ -158,7 +211,7 @@ public function mockRefreshTokenResponse( $response, $parsed_args, $url )
'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'],
'token_type' => 'bearer',
'created_at' => strtotime( 'now' ),
'created_at' => $this->now,
'expires_in' => 10000,
'scope' => 'public',
)
Expand Down