diff --git a/.gitignore b/.gitignore index c0654d4..96dcef1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ /vendor/ -/examples/ diff --git a/.travis.yml b/.travis.yml index 9281de6..4d46f85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,46 @@ language: php +cache: + directories: + - vendor + - $HOME/.composer/cache + +env: + matrix: + - DEPENDENCIES=latest + - DEPENDENCIES=oldest + +install: + - > + echo; + if [ "$DEPENDENCIES" = "latest" ]; then + echo "Installing the latest dependencies"; + composer update --with-dependencies + else + echo "Installing the lowest dependencies"; + composer update --with-dependencies --prefer-lowest + fi; + composer show; + php: - - 5.5 - 5.6 - 7.0 - - hhvm + - 7.1 + - 7.2 + - 7.3 -install: - - travis_retry composer install --no-interaction +script: + - > + echo; + echo "Validating the composer.json"; + composer validate --no-check-all --no-check-lock --strict; + + - > + echo; + echo "Linting all PHP files"; + composer ci:lint; + + - > + echo; + echo "Running the PHPUnit tests"; + composer ci:tests; diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e54ced..fdfffe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to `iamstuartwilson/strava` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +## [1.4.0] - 2019-09-16 + +### Added + +- Support for Strava's new oAuth2 flow (short-lived access tokens, refresh tokens). +- Example file which demonstrates how the oAuth2 flow works. + +### Changed + +- Unified Markdown style in README. + +## [1.3.0] - 2017-03-02 + +### Added + +- Possibility to use absolute URL for an endpoint to work with new [webhook functionality](https://developers.strava.com/docs/webhooks/). + ## [1.2.2] - 2016-10-26 ### Added diff --git a/README.md b/README.md index 9b128be..eb3111e 100644 --- a/README.md +++ b/README.md @@ -3,30 +3,26 @@ ![Packagist](https://img.shields.io/packagist/v/iamstuartwilson/strava.svg) ![Packagist Downloads](https://img.shields.io/packagist/dt/iamstuartwilson/strava.svg) -StravaApi -============= +# StravaApi -The class simply houses methods to help send data to and recieve data from the API. Please read the [API documentation](https://developers.strava.com/docs/reference/) to see what endpoints are available. +The class simply houses methods to help send data to and receive data from the API. Please read the [API documentation](https://developers.strava.com/docs/reference/) to see what endpoints are available. -*There is currently no file upload support at this time* +*There is no file upload support at this time.* -Installation ------------- +## Installation ### With Composer +``` shell +composer require iamstuartwilson/strava ``` -$ composer require iamstuartwilson/strava -``` - -**Or** -Add `iamstuartwilson/strava` to your `composer.json`: +Or add it manually to your `composer.json`: ``` json { "require" : { - "iamstuartwilson/strava" : "~1.3" + "iamstuartwilson/strava" : "^1.4" } } ``` @@ -35,10 +31,9 @@ Add `iamstuartwilson/strava` to your `composer.json`: Copy `StravaApi.php` to your project and *require* it in your application as described in the next section. -Getting Started ------------- +## Getting Started -Include the class and instantiate with your **client_id** and **client_secret** from your [registered app](https://www.strava.com/settings/api): +Instantiate the class with your **client_id** and **client_secret** from your [registered app](https://www.strava.com/settings/api): ``` php require_once 'StravaApi.php'; @@ -49,34 +44,58 @@ $api = new Iamstuartwilson\StravaApi( ); ``` -You will then need to [authenticate](http://strava.github.io/api/v3/oauth/) your strava account by requesting an access code1. You can generate a URL for authentication using the following method: +If you're just testing endpoints/methods you can skip the authentication flow and just use the access token from your [settings page](https://www.strava.com/settings/api). + +You will then need to [authenticate](https://developers.strava.com/docs/authentication/) your strava account by requesting an access code. You can generate a URL for authentication using the following method: ``` php $api->authenticationUrl($redirect, $approvalPrompt = 'auto', $scope = null, $state = null); ``` -When a code is returned you must then exchange it for an [access token](http://strava.github.io/api/v3/oauth/#post-token) for the authenticated user: +When a code is returned you must then exchange it for an [access token and a refresh token](http://developers.strava.com/docs/authentication/#token-exchange) for the authenticated user: ``` php -$api->tokenExchange($code); +$result = $api->tokenExchange($code); ``` -Before making any requests you must set the access token as returned from your token exchange or via your own private token from Strava: +The token exchange result contains among other data the tokens. You can access them as attributes of the result object: + +```php +$accessToken = $result->access_token; +$refreshToken = $result->refresh_token; +$expiresAt = $result->expires_at; +``` + +Before making any requests you must set the access and refresh tokens as returned from your token exchange result or via your own private token from Strava: ``` php -$api->setAccessToken($accessToken); +$api->setAccessToken($accessToken, $refreshToken, $expiresAt); ``` -Example Requests ------------- +## Example oAuth2 Authentication Flow + +`examples/oauth-flow.php` demonstrates how the oAuth2 authentication flow works. + +1. Choose how to load the `StravaApi.php` – either via Composer autoloader or by manually *requiring* it. +2. Replace the three config values `CALLBACK_URL`, `STRAVA_API_ID`, and `STRAVA_API_SECRET` at the top of the file +3. Place the file on your server so that it's accessible at `CALLBACK_URL` +4. Point your browser to `CALLBACK_URL` and start the authentication flow. + +The scripts prints a lot of verbose information so you get an idea on how the Strava oAuth flow works. -Get the most recent 100 KOMs from any athlete +## Example Requests + +Once successfully authenticated you're able to communicate with Strava's API. + +All actions that change Strava contents (`post`, `put`, `delete`) will need the **scope** set to *write* in the authentication flow. + +### Get the most recent 100 KOMs from any athlete ``` php $api->get('athletes/:id/koms', ['per_page' => 100]); ``` -Post a new activity2 +### Post a new activity ``` php $api->post('activities', [ @@ -87,34 +106,18 @@ $api->post('activities', [ ]); ``` -Update a athlete's weight2 +### Update a athlete's weight ``` php $api->put('athlete', ['weight' => 70]); ``` -Delete an activity2 +### Delete an activity ``` php $api->delete('activities/:id'); ``` -### Notes - -**1**. The account you register your app will give you an access token, so you can skip this step if you're just testing endpoints/methods. - -**2**. These actions will need the **scope** set to *write* when authenticating a user - ---- - -Releases ---- -Latest version **1.3.0** - -- Adds possibility to use absolute URL for an endpoint to work with new [webhook functionality](https://developers.strava.com/docs/webhooks/). - -Previous version **1.2.2** +## Releases -- Possibility to access the HTTP response headers -- PHP 7 compatibility -- Basic PHPUnit test cases for Auth URL generation +See CHANGELOG.md. diff --git a/composer.json b/composer.json index 2063fe8..1db75d2 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,21 @@ }, "autoload": { "psr-0" : { - "Iamstuartwilson" : "src" + "Iamstuartwilson\\" : "src/" } + }, + "scripts": { + "ci:lint": "find config src tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l", + "ci:tests": "./vendor/bin/phpunit tests/", + "ci:static": [ + "@ci:lint" + ], + "ci:dynamic": [ + "@ci:tests" + ], + "ci": [ + "@ci:static", + "@ci:dynamic" + ] } } diff --git a/examples/oauth-flow.php b/examples/oauth-flow.php new file mode 100644 index 0000000..3d81914 --- /dev/null +++ b/examples/oauth-flow.php @@ -0,0 +1,116 @@ +setAccessToken( + $_SESSION['strava_access_token'], + $_SESSION['strava_refresh_token'], + $_SESSION['strava_access_token_expires_at'] + ); +} + +$action = isset($_GET['action']) ? $_GET['action'] : 'default'; + +switch ($action) { + case 'auth': + header('Location: ' . $api->authenticationUrl(CALLBACK_URL, 'auto', 'read_all')); + + return; + + case 'callback': + echo '
'; + print_r($_GET); + echo ''; + $code = $_GET['code']; + $response = $api->tokenExchange($code); + echo '
(after swapping the code from callback against tokens)
'; + echo ''; + print_r($response); + echo ''; + + $_SESSION['strava_access_token'] = isset($response->access_token) ? $response->access_token : null; + $_SESSION['strava_refresh_token'] = isset($response->refresh_token) ? $response->refresh_token : null; + $_SESSION['strava_access_token_expires_at'] = isset($response->expires_at) ? $response->expires_at : null; + + echo '
'; + print_r($_SESSION); + echo ''; + + echo ''; + echo ''; + + return; + + case 'refresh_token': + echo '
'; + print_r($_SESSION); + echo ''; + + echo '
'; + print_r($response); + echo ''; + + $_SESSION['strava_access_token'] = isset($response->access_token) ? $response->access_token : null; + $_SESSION['strava_refresh_token'] = isset($response->refresh_token) ? $response->refresh_token : null; + $_SESSION['strava_access_token_expires_at'] = isset($response->expires_at) ? $response->expires_at : null; + + echo '
'; + print_r($_SESSION); + echo ''; + + return; + + case 'test_request': + echo '
'; + print_r($_SESSION); + echo ''; + + $response = $api->get('/athlete'); + echo '
'; + print_r($response); + echo ''; + + return; + + case 'default': + default: + echo '
Start authentication flow.
'; + echo 'Start oAuth Authentication Flow (Strava oAuth URL: '
+ . $api->authenticationUrl(CALLBACK_URL)
+ . '
)
'; + print_r($_SESSION); + echo ''; + echo ''; +} diff --git a/src/Iamstuartwilson/StravaApi.php b/src/Iamstuartwilson/StravaApi.php index 49fc868..faa6e65 100644 --- a/src/Iamstuartwilson/StravaApi.php +++ b/src/Iamstuartwilson/StravaApi.php @@ -12,7 +12,12 @@ class StravaApi { - const BASE_URL = 'https://www.strava.com/'; + const BASE_URL = 'https://www.strava.com'; + + /** + * If the access token expires in less than 3600 seconds, a refresh is required. + */ + const ACCESS_TOKEN_MINIMUM_VALIDITY = 3600; public $lastRequest; public $lastRequestData; @@ -41,6 +46,8 @@ class StravaApi protected $clientSecret; private $accessToken; + private $refreshToken; + private $expiresAt; /** * Sets up the class with the $clientId and $clientSecret @@ -52,8 +59,8 @@ public function __construct($clientId = 1, $clientSecret = '') { $this->clientId = $clientId; $this->clientSecret = $clientSecret; - $this->apiUrl = self::BASE_URL . 'api/v3/'; - $this->authUrl = self::BASE_URL . 'oauth/'; + $this->apiUrl = self::BASE_URL . '/api/v3/'; + $this->authUrl = self::BASE_URL . '/oauth/'; } /** @@ -123,6 +130,10 @@ protected function request($url, $parameters = array(), $request = false) $this->lastRequestData = $parameters; $this->responseHeaders = array(); + if (strpos($url, '/oauth/token') === false && $this->isTokenRefreshNeeded()) { + throw new \RuntimeException('Strava access token needs to be refreshed'); + } + $curl = curl_init($url); $curlOptions = array( @@ -167,7 +178,7 @@ protected function request($url, $parameters = array(), $request = false) * @param string $scope * @param string $state * - * @link http://strava.github.io/api/v3/oauth/#get-authorize + * @link http://developers.strava.com/docs/authentication/ * * @return string */ @@ -196,7 +207,7 @@ public function authenticationUrl($redirect, $approvalPrompt = 'auto', $scope = * * @param string $code * - * @link http://strava.github.io/api/v3/oauth/#post-token + * @link http://developers.strava.com/docs/authentication/#token-exchange * * @return string */ @@ -206,6 +217,32 @@ public function tokenExchange($code) 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $code, + 'grant_type' => 'authorization_code' + ); + + return $this->request( + $this->authUrl . 'token', + $parameters + ); + } + + /** + * Refresh expired access tokens + * + * @link https://developers.strava.com/docs/authentication/#refresh-expired-access-tokens + * + * @return mixed + */ + public function tokenExchangeRefresh() + { + if (! isset($this->refreshToken)) { + return null; + } + $parameters = array( + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'refresh_token' => $this->refreshToken, + 'grant_type' => 'refresh_token' ); return $this->request( @@ -233,11 +270,23 @@ public function deauthorize() * Sets the access token used to authenticate API requests * * @param string $token + * @param string $refreshToken + * @param int $expiresAt * * @return string */ - public function setAccessToken($token) + public function setAccessToken($token, $refreshToken = null, $expiresAt = null) { + if (isset($refreshToken)) { + $this->refreshToken = $refreshToken; + } + if (isset($expiresAt)) { + $this->expiresAt = $expiresAt; + if ($this->isTokenRefreshNeeded()) { + throw new \RuntimeException('Strava access token needs to be refreshed'); + } + } + return $this->accessToken = $token; } @@ -383,4 +432,16 @@ protected function getAbsoluteUrl($request) return $this->apiUrl . $request; } + + /** + * @return bool + */ + public function isTokenRefreshNeeded() + { + if (empty($this->expiresAt)) { + return false; + } + + return $this->expiresAt - time() < self::ACCESS_TOKEN_MINIMUM_VALIDITY; + } } diff --git a/tests/StravaApiTest.php b/tests/StravaApiTest.php index bc637f2..0cc77f7 100644 --- a/tests/StravaApiTest.php +++ b/tests/StravaApiTest.php @@ -1,5 +1,7 @@ assertEquals($expected, $url); } + + public function testIfTokenRefreshCheckReturnsTrueIfNoExpiresTimestampIsSet() + { + $this->stravaApi->setAccessToken('access_token', 'refresh_token', null); + + self::assertFalse($this->stravaApi->isTokenRefreshNeeded()); + } + + /** + * @expectedException \RuntimeException + */ + public function testIfTokenRefreshCheckReturnsTrueIfExpiresTimestampIsInThePast() + { + $this->stravaApi->setAccessToken('access_token', 'refresh_token', time() - 86400); + + self::assertTrue($this->stravaApi->isTokenRefreshNeeded()); + } + + /** + * @expectedException \RuntimeException + */ + public function testIfTokenRefreshCheckReturnsTrueIfExpiresTimestampIsDueInLessThanOneHour() + { + $this->stravaApi->setAccessToken('access_token', 'refresh_token', time() + 1800); + + self::assertTrue($this->stravaApi->isTokenRefreshNeeded()); + } + + public function testIfTokenRefreshCheckReturnsFalseIfExpiresTimestampIsMoreThanOneHourInTheFuture() + { + $this->stravaApi->setAccessToken('access_token', 'refresh_token', time() + 7200); + + self::assertFalse($this->stravaApi->isTokenRefreshNeeded()); + } }