From 308ae093046637c944fa11d865fcb3b27b4a0f01 Mon Sep 17 00:00:00 2001 From: Roman Anasal Date: Tue, 25 Feb 2020 13:17:52 +0100 Subject: [PATCH 1/3] Update to Instagram Basic Display API Instagram is deprecating its legacy API (https://api.instagram.com/v1/...) and moving to a Graph-based API (https://graph.instagram.com/...). For more info see https://developers.facebook.com/blog/post/2019/10/15/launch-instagram-basic-display-api/ https://developers.facebook.com/docs/instagram-basic-display-api --- .../InstagramIdentityProviderException.php | 8 ++-- src/Provider/Instagram.php | 40 ++++++++++++++++--- src/Provider/InstagramResourceOwner.php | 36 ++--------------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/Provider/Exception/InstagramIdentityProviderException.php b/src/Provider/Exception/InstagramIdentityProviderException.php index d5e0c93..95dcdaf 100644 --- a/src/Provider/Exception/InstagramIdentityProviderException.php +++ b/src/Provider/Exception/InstagramIdentityProviderException.php @@ -20,11 +20,11 @@ public static function clientException(ResponseInterface $response, $data) $code = $response->getStatusCode(); $body = (string) $response->getBody(); - if (isset($data['meta'], $data['meta']['error_message'])) { - $message = $data['meta']['error_message']; + if (isset($data['error'], $data['error']['message'])) { + $message = $data['error']['message']; } - if (isset($data['meta'], $data['meta']['code'])) { - $code = $data['meta']['code']; + if (isset($data['error'], $data['error']['code'])) { + $code = $data['error']['code']; } return new static($message, $code, $body); diff --git a/src/Provider/Instagram.php b/src/Provider/Instagram.php index f8f537b..4f2f587 100644 --- a/src/Provider/Instagram.php +++ b/src/Provider/Instagram.php @@ -11,14 +11,14 @@ class Instagram extends AbstractProvider /** * @var string Key used in a token response to identify the resource owner. */ - const ACCESS_TOKEN_RESOURCE_OWNER_ID = 'user.id'; + const ACCESS_TOKEN_RESOURCE_OWNER_ID = 'user_id'; /** * Default scopes * * @var array */ - public $defaultScopes = ['basic']; + public $defaultScopes = ['user_profile']; /** * Default host @@ -27,6 +27,13 @@ class Instagram extends AbstractProvider */ protected $host = 'https://api.instagram.com'; + /** + * Default Graph API host + * + * @var string + */ + protected $graphHost = 'https://graph.instagram.com'; + /** * Gets host. * @@ -37,6 +44,16 @@ public function getHost() return $this->host; } + /** + * Gets Graph API host. + * + * @return string + */ + public function getGraphHost() + { + return $this->graphHost; + } + /** * Get the string used to separate scopes. * @@ -78,7 +95,7 @@ public function getBaseAccessTokenUrl(array $params) */ public function getResourceOwnerDetailsUrl(AccessToken $token) { - return $this->host.'/v1/users/self?access_token='.$token; + return $this->graphHost.'/me?fields=id,username&access_token='.$token; } /** @@ -127,7 +144,6 @@ protected function getDefaultScopes() /** * Check a provider response for errors. * - * @link https://instagram.com/developer/endpoints/ * @throws IdentityProviderException * @param ResponseInterface $response * @param string $data Parsed response data @@ -136,7 +152,7 @@ protected function getDefaultScopes() protected function checkResponse(ResponseInterface $response, $data) { // Standard error response format - if (!empty($data['meta']['error_type'])) { + if (!empty($data['error'])) { throw InstagramIdentityProviderException::clientException($response, $data); } @@ -171,4 +187,18 @@ public function setHost($host) return $this; } + + /** + * Sets Graph API host. + * + * @param string $host + * + * @return string + */ + public function setGraphHost($host) + { + $this->graphHost = $host; + + return $this; + } } diff --git a/src/Provider/InstagramResourceOwner.php b/src/Provider/InstagramResourceOwner.php index d34e661..61615ac 100644 --- a/src/Provider/InstagramResourceOwner.php +++ b/src/Provider/InstagramResourceOwner.php @@ -26,27 +26,7 @@ public function __construct(array $response = array()) */ public function getId() { - return $this->response['data']['id'] ?: null; - } - - /** - * Get user imageurl - * - * @return string|null - */ - public function getImageurl() - { - return $this->response['data']['profile_picture'] ?: null; - } - - /** - * Get user name - * - * @return string|null - */ - public function getName() - { - return $this->response['data']['full_name'] ?: null; + return $this->response['id'] ?: null; } /** @@ -56,17 +36,7 @@ public function getName() */ public function getNickname() { - return $this->response['data']['username'] ?: null; - } - - /** - * Get user description - * - * @return string|null - */ - public function getDescription() - { - return $this->response['data']['bio'] ?: null; + return $this->response['username'] ?: null; } /** @@ -76,6 +46,6 @@ public function getDescription() */ public function toArray() { - return $this->response['data']; + return $this->response; } } From 73ef834b9ac040df342ecec13b10a542cb98a1a7 Mon Sep 17 00:00:00 2001 From: Roman Anasal Date: Tue, 25 Feb 2020 13:44:50 +0100 Subject: [PATCH 2/3] Update test to reflect new Instagram API --- tests/src/Provider/InstagramTest.php | 45 ++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/tests/src/Provider/InstagramTest.php b/tests/src/Provider/InstagramTest.php index 7af6b60..bcbce9d 100644 --- a/tests/src/Provider/InstagramTest.php +++ b/tests/src/Provider/InstagramTest.php @@ -56,6 +56,29 @@ public function testSetHostAfterConfig() $this->assertEquals($host, $this->provider->getHost()); } + public function testSetGraphHostInConfig() + { + $host = uniqid(); + + $provider = new \League\OAuth2\Client\Provider\Instagram([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + 'graphHost' => $host + ]); + + $this->assertEquals($host, $provider->getGraphHost()); + } + + public function testSetGraphHostAfterConfig() + { + $host = uniqid(); + + $this->provider->setGraphHost($host); + + $this->assertEquals($host, $this->provider->getGraphHost()); + } + public function testScopes() { $scopeSeparator = ' '; @@ -87,7 +110,7 @@ public function testGetBaseAccessTokenUrl() public function testGetAccessToken() { $response = m::mock('Psr\Http\Message\ResponseInterface'); - $response->shouldReceive('getBody')->andReturn('{"access_token":"mock_access_token","user": {"id": "123","username": "snoopdogg","full_name": "Snoop Dogg","profile_picture": "..."}}'); + $response->shouldReceive('getBody')->andReturn('{"access_token":"mock_access_token","user_id": "123"}'); $response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $client = m::mock('GuzzleHttp\ClientInterface'); @@ -105,17 +128,14 @@ public function testGetAccessToken() public function testUserData() { $userId = rand(1000,9999); - $name = uniqid(); $nickname = uniqid(); - $picture = uniqid(); - $description = uniqid(); $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); - $postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token","user": {"id": "1574083","username": "snoopdogg","full_name": "Snoop Dogg","profile_picture": "..."}}'); + $postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token","user_id": "1574083"}'); $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $userResponse = m::mock('Psr\Http\Message\ResponseInterface'); - $userResponse->shouldReceive('getBody')->andReturn('{"data": {"id": "'.$userId.'", "username": "'.$nickname.'", "full_name": "'.$name.'", "bio": "'.$description.'", "profile_picture": "'.$picture.'"}}'); + $userResponse->shouldReceive('getBody')->andReturn('{"id": "'.$userId.'", "username": "'.$nickname.'"}'); $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $client = m::mock('GuzzleHttp\ClientInterface'); @@ -129,14 +149,8 @@ public function testUserData() $this->assertEquals($userId, $user->getId()); $this->assertEquals($userId, $user->toArray()['id']); - $this->assertEquals($name, $user->getName()); - $this->assertEquals($name, $user->toArray()['full_name']); $this->assertEquals($nickname, $user->getNickname()); $this->assertEquals($nickname, $user->toArray()['username']); - $this->assertEquals($picture, $user->getImageurl()); - $this->assertEquals($picture, $user->toArray()['profile_picture']); - $this->assertEquals($description, $user->getDescription()); - $this->assertEquals($description, $user->toArray()['bio']); } public function testExceptionThrownWhenErrorObjectReceived() @@ -144,8 +158,9 @@ public function testExceptionThrownWhenErrorObjectReceived() $this->expectException('League\OAuth2\Client\Provider\Exception\IdentityProviderException'); $message = uniqid(); $status = rand(400,600); + $traceId = uniqid(); $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); - $postResponse->shouldReceive('getBody')->andReturn('{"meta": {"error_type": "OAuthException","code": '.$status.',"error_message": "'.$message.'"}}'); + $postResponse->shouldReceive('getBody')->andReturn('{"error": {"type": "IGApiException","code": '.$status.',"message": "'.$message.'","fbtrace_id":"'.$traceId.'"}}'); $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $postResponse->shouldReceive('getReasonPhrase'); $postResponse->shouldReceive('getStatusCode')->andReturn($status); @@ -180,10 +195,10 @@ public function testExceptionThrownWhenAuthErrorObjectReceived() public function testGetAuthenticatedRequest() { $method = 'GET'; - $url = 'https://api.instagram.com/v1/users/self/feed'; + $url = 'https://graph.instagram.com/me'; $accessTokenResponse = m::mock('Psr\Http\Message\ResponseInterface'); - $accessTokenResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token","user": {"id": "1574083","username": "snoopdogg","full_name": "Snoop Dogg","profile_picture": "..."}}'); + $accessTokenResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token","user_id": "1574083"}'); $accessTokenResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $client = m::mock('GuzzleHttp\ClientInterface'); From f3db8d507cae5663914933d228099e09fc31c791 Mon Sep 17 00:00:00 2001 From: Roman Anasal Date: Tue, 25 Feb 2020 14:05:16 +0100 Subject: [PATCH 3/3] Update changelog and readme --- CHANGELOG.md | 24 ++++++++++++++++++++++++ README.md | 15 +++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3444cda..bf618a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ # Changelog All Notable changes to `oauth2-instagram` will be documented in this file +## 3.0.0 - 2020-02-25 + +### Added +- Support for Instagram Basic Display API + - get Resource Owner Details from https://graph.instagram.com/me + - changed default scopes to `['user_profile']` +- Custom host configuration for Graph API host + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Removed +- Support for Instagram Legacy API (https://api.instagram.com/v1/...) +- Short-hand functions for now removed attributes + - `InstagramResourceOwner::getImageUrl()` + - `InstagramResourceOwner::getName()` + - `InstagramResourceOwner::getDescription()` + +### Security +- Nothing + ## 2.0.0 - 2017-01-25 ### Added diff --git a/README.md b/README.md index 79bee2d..d777a6a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ $provider = new League\OAuth2\Client\Provider\Instagram([ 'clientId' => '{instagram-client-id}', 'clientSecret' => '{instagram-client-secret}', 'redirectUri' => 'https://example.com/callback-url', - 'host' => 'https://api.instagram.com' // Optional, defaults to https://api.instagram.com + 'host' => 'https://api.instagram.com', // Optional, defaults to https://api.instagram.com + 'graphHost' => 'https://graph.instagram.com' // Optional, defaults to https://graph.instagram.com ]); if (!isset($_GET['code'])) { @@ -58,7 +59,7 @@ if (!isset($_GET['code'])) { $user = $provider->getResourceOwner($token); // Use these details to create a new profile - printf('Hello %s!', $user->getName()); + printf('Hello %s!', $user->getNickname()); } catch (Exception $e) { @@ -78,19 +79,17 @@ When creating your Instagram authorization URL, you can specify the state and sc ```php $options = [ 'state' => 'OPTIONAL_CUSTOM_CONFIGURED_STATE', - 'scope' => ['basic','likes','comments'] // array or string + 'scope' => ['user_profile', 'user_media'] // array or string ]; $authorizationUrl = $provider->getAuthorizationUrl($options); ``` If neither are defined, the provider will utilize internal defaults. -At the time of authoring this documentation, the [following scopes are available](https://instagram.com/developer/authentication/#scope). +At the time of authoring this documentation, the [following scopes are available](https://developers.facebook.com/docs/instagram-basic-display-api/overview#permissions). -- basic -- comments -- relationships -- likes +- user_profile +- user_media ## Testing