├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── .travis.yml ├── LICENSE ├── Readme.md ├── Upgrade.md ├── composer.json ├── phpunit.xml.dist ├── src ├── AccessToken.php ├── Authenticator.php ├── AuthenticatorInterface.php ├── Exception │ ├── InvalidArgumentException.php │ ├── LinkedInException.php │ ├── LinkedInTransferException.php │ └── LoginError.php ├── Http │ ├── CurrentUrlGeneratorInterface.php │ ├── GlobalVariableGetter.php │ ├── LinkedInUrlGeneratorInterface.php │ ├── RequestManager.php │ ├── RequestManagerInterface.php │ ├── ResponseConverter.php │ ├── UrlGenerator.php │ └── UrlGeneratorInterface.php ├── LinkedIn.php ├── LinkedInInterface.php └── Storage │ ├── BaseDataStorage.php │ ├── DataStorageInterface.php │ ├── IlluminateSessionStorage.php │ └── SessionStorage.php └── tests ├── AccessTokenTest.php ├── AuthenticatorTest.php ├── Exceptions └── LoginErrorTest.php ├── Http ├── ResponseConverterTest.php └── UrlGeneratorTest.php ├── LinkedInTest.php └── Storage ├── IlluminateSessionStorageTest.php └── SessionStorageTest.php /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ----------------------- | --- 3 | | Bug? | no|yes 4 | | New Feature? | no|yes 5 | | Are you using composer? | no|yes 6 | | Version | Specific version or SHA of a commit 7 | 8 | 9 | #### Actual Behavior 10 | 11 | What is the actual behavior? 12 | 13 | 14 | #### Expected Behavior 15 | 16 | What is the behavior you expect? 17 | 18 | 19 | #### Steps to Reproduce 20 | 21 | What are the steps to reproduce this bug? Please add code examples, 22 | screenshots or links to GitHub repositories that reproduce the problem. 23 | 24 | 25 | #### Possible Solutions 26 | 27 | If you have already ideas how to solve the issue, add them here. 28 | (remove this section if not needed) 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | --------------- | --- 3 | | Bug fix? | no|yes 4 | | New feature? | no|yes 5 | | BC breaks? | no|yes 6 | | Deprecations? | no|yes 7 | | Related tickets | fixes #X, partially #Y, mentioned in #Z 8 | | License | MIT 9 | 10 | 11 | #### What's in this PR? 12 | 13 | Explain what the changes in this PR do. 14 | 15 | 16 | #### Why? 17 | 18 | Which problem does the PR fix? (remove this section if you linked an issue above) 19 | 20 | 21 | #### Example Usage 22 | 23 | ``` php 24 | // If you added new features, show examples of how to use them here 25 | // (remove this section if not a new feature) 26 | 27 | $foo = new Foo(); 28 | 29 | // Now we can do 30 | $foo->doSomething(); 31 | ``` 32 | 33 | 34 | #### Checklist 35 | 36 | - [ ] Updated CHANGELOG.md to describe BC breaks / deprecations | new feature | bugfix 37 | - [ ] Documentation pull request created (if not simply a bugfix) 38 | 39 | 40 | #### To Do 41 | 42 | - [ ] If the PR is not complete but you want to discuss the approach, list what remains to be done here 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | composer.lock 3 | index.php 4 | phpunit.xml 5 | /vendor/ 6 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: [src/*] 3 | checks: 4 | php: 5 | code_rating: true 6 | duplication: true 7 | tools: 8 | external_code_coverage: true 9 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: symfony 2 | 3 | finder: 4 | path: 5 | - "src" 6 | - "tests" 7 | 8 | enabled: 9 | - short_array_syntax 10 | 11 | disabled: 12 | - phpdoc_annotation_without_dot 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | cache: 5 | directories: 6 | - $HOME/.composer/cache/files 7 | 8 | php: 9 | - 5.5 10 | - 5.6 11 | - 7.0 12 | - 7.1 13 | - hhvm 14 | 15 | env: 16 | global: 17 | - TEST_COMMAND="composer test" 18 | 19 | matrix: 20 | fast_finish: true 21 | include: 22 | - php: 5.5 23 | env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" 24 | 25 | before_install: 26 | - composer self-update 27 | 28 | install: 29 | - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction 30 | 31 | script: 32 | - $TEST_COMMAND 33 | 34 | after_success: 35 | - if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi 36 | - if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tobias Nyholm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # LinkedIn API client in PHP 2 | 3 | [![Latest Version](https://img.shields.io/github/release/Happyr/LinkedIn-API-client.svg?style=flat-square)](https://github.com/Happyr/LinkedIn-API-client/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Build Status](https://img.shields.io/travis/Happyr/LinkedIn-API-client/master.svg?style=flat-square)](https://travis-ci.org/Happyr/LinkedIn-API-client) 6 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/44c425af-90f6-4c25-b789-4ece28b01a2b/mini.png)](https://insight.sensiolabs.com/projects/44c425af-90f6-4c25-b789-4ece28b01a2b) 7 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/Happyr/LinkedIn-API-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/LinkedIn-API-client) 8 | [![Quality Score](https://img.shields.io/scrutinizer/g/Happyr/LinkedIn-API-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/LinkedIn-API-client) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/happyr/linkedin-api-client.svg?style=flat-square)](https://packagist.org/packages/happyr/linkedin-api-client) 10 | 11 | 12 | A PHP library to handle authentication and communication with LinkedIn API. The library/SDK helps you to get an access 13 | token and when authenticated it helps you to send API requests. You will not get *everything* for free though... You 14 | have to read the [LinkedIn documentation][api-doc-core] to understand how you should query the API. 15 | 16 | To get an overview what this library actually is doing for you. Take a look at the authentication page from 17 | the [API docs][api-doc-authentication]. 18 | 19 | ## Features 20 | 21 | Here is a list of features that might convince you to choose this LinkedIn client over some of our competitors'. 22 | 23 | * Flexible and easy to extend 24 | * Developed with modern PHP standards 25 | * Not developed for a specific framework. 26 | * Handles the authentication process 27 | * Respects the CSRF protection 28 | 29 | ## Installation 30 | 31 | **TL;DR** 32 | ```bash 33 | composer require php-http/curl-client guzzlehttp/psr7 php-http/message happyr/linkedin-api-client 34 | ``` 35 | 36 | This library does not have a dependency on Guzzle or any other library that sends HTTP requests. We use the awesome 37 | HTTPlug to achieve the decoupling. We want you to choose what library to use for sending HTTP requests. Consult this list 38 | of packages that support [php-http/client-implementation](https://packagist.org/providers/php-http/client-implementation) 39 | find clients to use. For more information about virtual packages please refer to 40 | [HTTPlug](http://docs.php-http.org/en/latest/httplug/users.html). Example: 41 | 42 | ```bash 43 | composer require php-http/guzzle6-adapter 44 | ``` 45 | 46 | You do also need to install a PSR-7 implementation and a factory to create PSR-7 messages (PSR-17 whenever that is 47 | released). You could use Guzzles PSR-7 implementation and factories from php-http: 48 | 49 | ```bash 50 | composer require guzzlehttp/psr7 php-http/message 51 | ``` 52 | 53 | Now you may install the library by running the following: 54 | 55 | ```bash 56 | composer require happyr/linkedin-api-client 57 | ``` 58 | 59 | If you are updating form a previous version make sure to read [the upgrade documentation](Upgrade.md). 60 | 61 | ### Finding the HTTP client (optional) 62 | 63 | The LinkedIn client need to know what library you are using to send HTTP messages. You could provide an instance of 64 | HttpClient and MessageFactory or you could fallback on auto discovery. Below is an example on where you provide a Guzzle6 65 | instance. 66 | 67 | ```php 68 | $linkedIn=new Happyr\LinkedIn\LinkedIn('app_id', 'app_secret'); 69 | $linkedIn->setHttpClient(new \Http\Adapter\Guzzle6\Client()); 70 | $linkedIn->setHttpMessageFactory(new Http\Message\MessageFactory\GuzzleMessageFactory()); 71 | 72 | ``` 73 | 74 | ## Usage 75 | 76 | In order to use this API client (or any other LinkedIn clients) you have to [register your application][register-app] 77 | with LinkedIn to receive an API key. Once you've registered your LinkedIn app, you will be provided with 78 | an *API Key* and *Secret Key*. 79 | 80 | ### LinkedIn login 81 | 82 | This example below is showing how to login with LinkedIn. 83 | 84 | ```php 85 | isAuthenticated()) { 100 | //we know that the user is authenticated now. Start query the API 101 | $user=$linkedIn->get('v1/people/~:(firstName,lastName)'); 102 | echo "Welcome ".$user['firstName']; 103 | 104 | exit(); 105 | } elseif ($linkedIn->hasError()) { 106 | echo "User canceled the login."; 107 | exit(); 108 | } 109 | 110 | //if not authenticated 111 | $url = $linkedIn->getLoginUrl(); 112 | echo "Login with LinkedIn"; 113 | 114 | ``` 115 | 116 | ### How to post on LinkedIn wall 117 | 118 | The example below shows how you can post on a users wall. The access token is fetched from the database. 119 | 120 | ```php 121 | $linkedIn=new Happyr\LinkedIn\LinkedIn('app_id', 'app_secret'); 122 | $linkedIn->setAccessToken('access_token_from_db'); 123 | 124 | $options = array('json'=> 125 | array( 126 | 'comment' => 'Im testing Happyr LinkedIn client! https://github.com/Happyr/LinkedIn-API-client', 127 | 'visibility' => array( 128 | 'code' => 'anyone' 129 | ) 130 | ) 131 | ); 132 | 133 | $result = $linkedIn->post('v1/people/~/shares', $options); 134 | 135 | var_dump($result); 136 | 137 | // Prints: 138 | // array (size=2) 139 | // 'updateKey' => string 'UPDATE-01234567-0123456789012345678' (length=35) 140 | // 'updateUrl' => string 'https://www.linkedin.com/updates?discuss=&scope=01234567&stype=M&topic=0123456789012345678&type=U&a=mVKU' (length=104) 141 | 142 | ``` 143 | 144 | You may of course do the same in xml. Use the following options array. 145 | ```php 146 | $options = array( 147 | 'format' => 'xml', 148 | 'body' => ' 149 | Im testing Happyr LinkedIn client! https://github.com/Happyr/LinkedIn-API-client 150 | 151 | anyone 152 | 153 | '); 154 | ``` 155 | 156 | ## Configuration 157 | 158 | ### The api options 159 | 160 | The third parameter of `LinkedIn::api` is an array with options. Below is a table of array keys that you may use. 161 | 162 | | Option name | Description 163 | | ----------- | ----------- 164 | | body | The body of a HTTP request. Put your xml string here. 165 | | format | Set this to 'json', 'xml' or 'simple_xml' to override the default value. 166 | | headers | This is HTTP headers to the request 167 | | json | This is an array with json data that will be encoded to a json string. Using this option you do need to specify a format. 168 | | response_data_type | To override the response format for one request 169 | | query | This is an array with query parameters 170 | 171 | 172 | 173 | ### Changing request format 174 | 175 | The default format when communicating with LinkedIn API is json. You can let the API do `json_encode` for you. 176 | The following code shows you how. 177 | 178 | ```php 179 | $body = array( 180 | 'comment' => 'Im testing Happyr LinkedIn client! https://github.com/Happyr/LinkedIn-API-client', 181 | 'visibility' => array('code' => 'anyone') 182 | ); 183 | 184 | $linkedIn->post('v1/people/~/shares', array('json'=>$body)); 185 | $linkedIn->post('v1/people/~/shares', array('body'=>json_encode($body))); 186 | ``` 187 | 188 | When using `array('json'=>$body)` as option the format will always be `json`. You can change the request format in three ways. 189 | 190 | ```php 191 | // By constructor argument 192 | $linkedIn=new Happyr\LinkedIn\LinkedIn('app_id', 'app_secret', 'xml'); 193 | 194 | // By setter 195 | $linkedIn->setFormat('xml'); 196 | 197 | // Set format for just one request 198 | $linkedIn->post('v1/people/~/shares', array('format'=>'xml', 'body'=>$body)); 199 | ``` 200 | 201 | 202 | ### Understanding response data type 203 | 204 | The data type returned from `LinkedIn::api` can be configured. You may use the forth construtor argument, the 205 | `LinkedIn::setResponseDataType` or as an option for `LinkedIn::api` 206 | 207 | ```php 208 | // By constructor argument 209 | $linkedIn=new Happyr\LinkedIn\LinkedIn('app_id', 'app_secret', 'json', 'array'); 210 | 211 | // By setter 212 | $linkedIn->setResponseDataType('simple_xml'); 213 | 214 | // Set format for just one request 215 | $linkedIn->get('v1/people/~:(firstName,lastName)', array('response_data_type'=>'psr7')); 216 | 217 | ``` 218 | 219 | Below is a table that specifies what the possible return data types are when you call `LinkedIn::api`. 220 | 221 | | Type | Description 222 | | ------ | ------------ 223 | | array | An assosiative array. This can only be used with the `json` format. 224 | | simple_xml | A SimpleXMLElement. See [PHP manual](http://php.net/manual/en/class.simplexmlelement.php). This can only be used with the `xml` format. 225 | | psr7 | A PSR7 response. 226 | | stream | A file stream. 227 | | string | A plain old string. 228 | 229 | 230 | ### Use different Session classes 231 | 232 | You might want to use an other storage than the default `SessionStorage`. If you are using Laravel 233 | you are more likely to inject the `IlluminateSessionStorage`. 234 | ```php 235 | $linkedIn=new Happyr\LinkedIn\LinkedIn('app_id', 'app_secret'); 236 | $linkedIn->setStorage(new IlluminateSessionStorage()); 237 | ``` 238 | 239 | You can inject any class implementing `DataStorageInterface`. You can also inject different `UrlGenerator` classes. 240 | 241 | ### Using different scopes 242 | 243 | If you want to define special scopes when you authenticate the user you should specify them when you are generating the 244 | login url. If you don't specify scopes LinkedIn will use the default scopes that you have configured for the app. 245 | 246 | ```php 247 | $scope = 'r_fullprofile,r_emailaddress,w_share'; 248 | //or 249 | $scope = array('rw_groups', 'r_contactinfo', 'r_fullprofile', 'w_messages'); 250 | 251 | $url = $linkedIn->getLoginUrl(array('scope'=>$scope)); 252 | echo "Login with LinkedIn"; 253 | ``` 254 | 255 | ## Framework integration 256 | 257 | If you want an easier integration with a framwork you may want to check out these repositories: 258 | 259 | * [HappyrLinkedInBundle](https://github.com/Happyr/LinkedInBundle) for Symfony 260 | * [Laravel-Linkedin by mauri870](https://github.com/artesaos/laravel-linkedin) for Laravel 5 261 | 262 | 263 | [register-app]: https://www.linkedin.com/secure/developer 264 | [linkedin-code-samples]: https://developer.linkedin.com/documents/code-samples 265 | [api-doc-authentication]: https://developer.linkedin.com/documents/authentication 266 | [api-doc-core]: https://developer.linkedin.com/core-concepts 267 | -------------------------------------------------------------------------------- /Upgrade.md: -------------------------------------------------------------------------------- 1 | # Upgrade 2 | 3 | This document explains how you upgrade from one version to another. 4 | 5 | ## Upgrade from 0.7.2 to 1.0 6 | 7 | ### Changes 8 | 9 | * We do not longer require `php-http/message`. You have to make sure to put that in your own composer.json. 10 | 11 | ## Upgrade from 0.7.1 to 0.7.2 12 | 13 | ### Changes 14 | 15 | * Using `php-http/discovery:1.0` 16 | * Code style changes. 17 | 18 | ## Upgrade from 0.7.0 to 0.7.1 19 | 20 | ### Changes 21 | 22 | * Using `php-http/discovery:0.9` which makes Puli optional 23 | * Using new URL's to LinkedIn API so users are provided with the new authentication UX. (Thanks to @mbarwick83) 24 | 25 | ## Upgrade from 0.6 to 0.7 26 | 27 | ### Changes 28 | 29 | * Introduced PHP-HTTP and PSR-7 messages 30 | * Added constructor argument for responseDataType 31 | * Added setResponseDataType() 32 | * Moved authentication functions to `Authenticator` class 33 | 34 | To make sure you can upgrade you need to install a HTTP adapter. 35 | 36 | ```bash 37 | php composer.phar require php-http/guzzle6-adapter 38 | ``` 39 | 40 | ### BC breaks 41 | 42 | * Removed `LinkedIn::setRequest` in favor of `LinkedIn::setHttpAdapter` 43 | * Removed `LinkedIn::getAppSecret` and `LinkedIn::getAppId` 44 | * Removed `LinkedIn::getUser` 45 | * Removed `LinkedInApiException` in favor of `LinkedInException`, `InvalidArgumentException` and `LinkedInTransferException` 46 | * Removed `LinkedIn::getLastHeaders` in favor of `LinkedIn::getLastResponse` 47 | * Made the public functions `LinkedIn::getResponseDataType` and `LinkedIn::getFormat` protected 48 | 49 | ## Upgrade from 0.5 to 0.6 50 | 51 | ### Changes 52 | 53 | * When exchanging the code for an access token we are now using the post body instead of query parameters 54 | * Better error handling when exchange from code to access token fails 55 | 56 | ### BC breaks 57 | 58 | There are a few minor BC breaks. We removed the functions below: 59 | 60 | * `LinkedIn::getUserId`, use `LinkedIn::getUser` instead 61 | * `AccessToken::constructFromJson`, Use the constructor instead. 62 | 63 | ## Upgrade from 0.4 to 0.5 64 | 65 | ### Changed signature of `LinkedIn::api` 66 | 67 | The signature of `LinkedIn::api` has changed to be more easy to work with. 68 | ```php 69 | // Version 0.4 70 | public function api($resource, array $urlParams=array(), $method='GET', $postParams=array()) 71 | 72 | // Version 0.5 73 | public function api($method, $resource, array $options=array()) 74 | ``` 75 | 76 | This means that you have to modify your calls to: 77 | ```php 78 | // Version 0.5 79 | $options = array('query'=>$urlParams, 'body'=>$postParams); 80 | $linkedIn->api('POST', $resource, $options) 81 | ``` 82 | See the Readme about more options to the API function. 83 | 84 | ### Must inject IlluminateSessionStorage 85 | 86 | We have removed the protected `LinkedIn::init` function. That means if you were using `IlluminateSessionStorage` you have 87 | to make a minor adjustment to your code. 88 | 89 | ```php 90 | // Version 0.4 91 | $linkedIn=new Happyr\LinkedIn\LinkedIn('app_id', 'app_secret'); 92 | 93 | // Version 0.5 94 | $linkedIn=new Happyr\LinkedIn\LinkedIn('app_id', 'app_secret'); 95 | $linkedIn->setStorage(new IlluminateSessionStorage()); 96 | ``` 97 | 98 | If you don't know about `IlluminateSessionStorage` you are probably good ignoring this. 99 | 100 | ### Default format 101 | 102 | The default format when communicating with LinkedIn API is changed to json. 103 | 104 | ### Updated RequestInterface 105 | 106 | The `RequestInterface::send` was updated with a new signature. We did also introduce `RequestInterface::getHeadersFromLastResponse`. 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happyr/linkedin-api-client", 3 | "type": "library", 4 | "description": "LinkedIn API client. Handles OAuth, CSRF protection. Easy to implement and extend. This is a standalone library for any composer project.", 5 | "keywords": ["LinkedIn", "OAuth", "API", "Client", "SDK"], 6 | "homepage": "http://developer.happyr.com/libraries/linkedin-php-client", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Tobias Nyholm", 11 | "email": "tobias@happyr.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^5.5 || ^7.0", 16 | "php-http/client-implementation": "^1.0", 17 | "php-http/httplug": "^1.0", 18 | "php-http/message-factory": "^1.0", 19 | "php-http/discovery": "^1.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^4.5 || ^5.0", 23 | "php-http/guzzle5-adapter": "^1.0", 24 | "guzzlehttp/psr7": "^1.2", 25 | "mockery/mockery": "^0.9", 26 | "illuminate/support": "^5.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { "Happyr\\LinkedIn\\": "src/" } 30 | }, 31 | "scripts": { 32 | "test": "vendor/bin/phpunit", 33 | "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "1.1.x-dev" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | src 32 | 33 | vendor 34 | tests 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/AccessToken.php: -------------------------------------------------------------------------------- 1 | token = $token; 27 | 28 | if ($expiresIn !== null) { 29 | if ($expiresIn instanceof \DateTime) { 30 | $this->expiresAt = $expiresIn; 31 | } else { 32 | $this->expiresAt = new \DateTime(sprintf('+%dseconds', $expiresIn)); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function __toString() 41 | { 42 | return $this->token ?: ''; 43 | } 44 | 45 | /** 46 | * Does a token string exist? 47 | * 48 | * @return bool 49 | */ 50 | public function hasToken() 51 | { 52 | return !empty($this->token); 53 | } 54 | 55 | /** 56 | * @param \DateTime $expiresAt 57 | * 58 | * @return $this 59 | */ 60 | public function setExpiresAt(\DateTime $expiresAt = null) 61 | { 62 | $this->expiresAt = $expiresAt; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @return \DateTime 69 | */ 70 | public function getExpiresAt() 71 | { 72 | return $this->expiresAt; 73 | } 74 | 75 | /** 76 | * @param null|string $token 77 | * 78 | * @return $this 79 | */ 80 | public function setToken($token) 81 | { 82 | $this->token = $token; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @return null|string 89 | */ 90 | public function getToken() 91 | { 92 | return $this->token; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Authenticator.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Authenticator implements AuthenticatorInterface 19 | { 20 | /** 21 | * The application ID. 22 | * 23 | * @var string 24 | */ 25 | protected $appId; 26 | 27 | /** 28 | * The application secret. 29 | * 30 | * @var string 31 | */ 32 | protected $appSecret; 33 | 34 | /** 35 | * A storage to use to store data between requests. 36 | * 37 | * @var DataStorageInterface storage 38 | */ 39 | private $storage; 40 | 41 | /** 42 | * @var RequestManagerInterface 43 | */ 44 | private $requestManager; 45 | 46 | /** 47 | * @param RequestManagerInterface $requestManager 48 | * @param string $appId 49 | * @param string $appSecret 50 | */ 51 | public function __construct(RequestManagerInterface $requestManager, $appId, $appSecret) 52 | { 53 | $this->appId = $appId; 54 | $this->appSecret = $appSecret; 55 | $this->requestManager = $requestManager; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function fetchNewAccessToken(LinkedInUrlGeneratorInterface $urlGenerator) 62 | { 63 | $storage = $this->getStorage(); 64 | $code = $this->getCode(); 65 | 66 | if ($code === null) { 67 | /* 68 | * As a fallback, just return whatever is in the persistent 69 | * store, knowing nothing explicit (signed request, authorization 70 | * code, etc.) was present to shadow it. 71 | */ 72 | return $storage->get('access_token'); 73 | } 74 | 75 | try { 76 | $accessToken = $this->getAccessTokenFromCode($urlGenerator, $code); 77 | } catch (LinkedInException $e) { 78 | // code was bogus, so everything based on it should be invalidated. 79 | $storage->clearAll(); 80 | throw $e; 81 | } 82 | 83 | $storage->set('code', $code); 84 | $storage->set('access_token', $accessToken); 85 | 86 | return $accessToken; 87 | } 88 | 89 | /** 90 | * Retrieves an access token for the given authorization code 91 | * (previously generated from www.linkedin.com on behalf of 92 | * a specific user). The authorization code is sent to www.linkedin.com 93 | * and a legitimate access token is generated provided the access token 94 | * and the user for which it was generated all match, and the user is 95 | * either logged in to LinkedIn or has granted an offline access permission. 96 | * 97 | * @param LinkedInUrlGeneratorInterface $urlGenerator 98 | * @param string $code An authorization code. 99 | * 100 | * @return AccessToken An access token exchanged for the authorization code. 101 | * 102 | * @throws LinkedInException 103 | */ 104 | protected function getAccessTokenFromCode(LinkedInUrlGeneratorInterface $urlGenerator, $code) 105 | { 106 | if (empty($code)) { 107 | throw new LinkedInException('Could not get access token: The code was empty.'); 108 | } 109 | 110 | $redirectUri = $this->getStorage()->get('redirect_uri'); 111 | try { 112 | $url = $urlGenerator->getUrl('www', 'oauth/v2/accessToken'); 113 | $headers = ['Content-Type' => 'application/x-www-form-urlencoded']; 114 | $body = http_build_query( 115 | [ 116 | 'grant_type' => 'authorization_code', 117 | 'code' => $code, 118 | 'redirect_uri' => $redirectUri, 119 | 'client_id' => $this->appId, 120 | 'client_secret' => $this->appSecret, 121 | ] 122 | ); 123 | 124 | $response = ResponseConverter::convertToArray($this->getRequestManager()->sendRequest('POST', $url, $headers, $body)); 125 | } catch (LinkedInTransferException $e) { 126 | // most likely that user very recently revoked authorization. 127 | // In any event, we don't have an access token, so throw an exception. 128 | throw new LinkedInException('Could not get access token: The user may have revoked the authorization response from LinkedIn.com was empty.', $e->getCode(), $e); 129 | } 130 | 131 | if (empty($response)) { 132 | throw new LinkedInException('Could not get access token: The response from LinkedIn.com was empty.'); 133 | } 134 | 135 | $tokenData = array_merge(['access_token' => null, 'expires_in' => null], $response); 136 | $token = new AccessToken($tokenData['access_token'], $tokenData['expires_in']); 137 | 138 | if (!$token->hasToken()) { 139 | throw new LinkedInException('Could not get access token: The response from LinkedIn.com did not contain a token.'); 140 | } 141 | 142 | return $token; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function getLoginUrl(LinkedInUrlGeneratorInterface $urlGenerator, $options = []) 149 | { 150 | // Generate a state 151 | $this->establishCSRFTokenState(); 152 | 153 | // Build request params 154 | $requestParams = array_merge([ 155 | 'response_type' => 'code', 156 | 'client_id' => $this->appId, 157 | 'state' => $this->getStorage()->get('state'), 158 | 'redirect_uri' => null, 159 | ], $options); 160 | 161 | // Save the redirect url for later 162 | $this->getStorage()->set('redirect_uri', $requestParams['redirect_uri']); 163 | 164 | // if 'scope' is passed as an array, convert to space separated list 165 | $scopeParams = isset($options['scope']) ? $options['scope'] : null; 166 | if ($scopeParams) { 167 | //if scope is an array 168 | if (is_array($scopeParams)) { 169 | $requestParams['scope'] = implode(' ', $scopeParams); 170 | } elseif (is_string($scopeParams)) { 171 | //if scope is a string with ',' => make it to an array 172 | $requestParams['scope'] = str_replace(',', ' ', $scopeParams); 173 | } 174 | } 175 | 176 | return $urlGenerator->getUrl('www', 'oauth/v2/authorization', $requestParams); 177 | } 178 | 179 | /** 180 | * Get the authorization code from the query parameters, if it exists, 181 | * and otherwise return null to signal no authorization code was 182 | * discovered. 183 | * 184 | * @return string|null The authorization code, or null if the authorization code not exists. 185 | * 186 | * @throws LinkedInException on invalid CSRF tokens 187 | */ 188 | protected function getCode() 189 | { 190 | $storage = $this->getStorage(); 191 | 192 | if (!GlobalVariableGetter::has('code')) { 193 | return; 194 | } 195 | 196 | if ($storage->get('code') === GlobalVariableGetter::get('code')) { 197 | //we have already validated this code 198 | return; 199 | } 200 | 201 | // if stored state does not exists 202 | if (null === $state = $storage->get('state')) { 203 | throw new LinkedInException('Could not find a stored CSRF state token.'); 204 | } 205 | 206 | // if state not exists in the request 207 | if (!GlobalVariableGetter::has('state')) { 208 | throw new LinkedInException('Could not find a CSRF state token in the request.'); 209 | } 210 | 211 | // if state exists in session and in request and if they are not equal 212 | if ($state !== GlobalVariableGetter::get('state')) { 213 | throw new LinkedInException('The CSRF state token from the request does not match the stored token.'); 214 | } 215 | 216 | // CSRF state has done its job, so clear it 217 | $storage->clear('state'); 218 | 219 | return GlobalVariableGetter::get('code'); 220 | } 221 | 222 | /** 223 | * Lays down a CSRF state token for this process. 224 | */ 225 | protected function establishCSRFTokenState() 226 | { 227 | $storage = $this->getStorage(); 228 | if ($storage->get('state') === null) { 229 | $storage->set('state', md5(uniqid(mt_rand(), true))); 230 | } 231 | } 232 | 233 | /** 234 | * {@inheritdoc} 235 | */ 236 | public function clearStorage() 237 | { 238 | $this->getStorage()->clearAll(); 239 | 240 | return $this; 241 | } 242 | 243 | /** 244 | * @return DataStorageInterface 245 | */ 246 | protected function getStorage() 247 | { 248 | if ($this->storage === null) { 249 | $this->storage = new SessionStorage(); 250 | } 251 | 252 | return $this->storage; 253 | } 254 | 255 | /** 256 | * {@inheritdoc} 257 | */ 258 | public function setStorage(DataStorageInterface $storage) 259 | { 260 | $this->storage = $storage; 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * @return RequestManager 267 | */ 268 | protected function getRequestManager() 269 | { 270 | return $this->requestManager; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/AuthenticatorInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface AuthenticatorInterface 15 | { 16 | /** 17 | * Tries to get a new access token from data storage or code. If it fails, it will return null. 18 | * 19 | * @param LinkedInUrlGeneratorInterface $urlGenerator 20 | * 21 | * @return AccessToken|null A valid user access token, or null if one could not be fetched. 22 | * 23 | * @throws LinkedInException 24 | */ 25 | public function fetchNewAccessToken(LinkedInUrlGeneratorInterface $urlGenerator); 26 | 27 | /** 28 | * Generate a login url. 29 | * 30 | * @param LinkedInUrlGeneratorInterface $urlGenerator 31 | * @param array $options 32 | * 33 | * @return string 34 | */ 35 | public function getLoginUrl(LinkedInUrlGeneratorInterface $urlGenerator, $options = []); 36 | 37 | /** 38 | * Clear the storage. 39 | * 40 | * @return $this 41 | */ 42 | public function clearStorage(); 43 | 44 | /** 45 | * @param DataStorageInterface $storage 46 | * 47 | * @return $this 48 | */ 49 | public function setStorage(DataStorageInterface $storage); 50 | } 51 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class LinkedInException extends \Exception 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/LinkedInTransferException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class LinkedInTransferException extends LinkedInException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/LoginError.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | $this->description = $description; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getName() 36 | { 37 | return $this->name; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getDescription() 44 | { 45 | return $this->description; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function __toString() 52 | { 53 | return sprintf('Name: %s, Description: %s', $this->getName(), $this->getDescription()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Http/CurrentUrlGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface CurrentUrlGeneratorInterface 11 | { 12 | /** 13 | * Returns the current URL. 14 | * 15 | * @return string The current URL 16 | */ 17 | public function getCurrentUrl(); 18 | 19 | /** 20 | * Should we trust forwarded headers? 21 | * 22 | * @param bool $trustForwarded 23 | * 24 | * @return $this 25 | */ 26 | public function setTrustForwarded($trustForwarded); 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/GlobalVariableGetter.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class GlobalVariableGetter 11 | { 12 | /** 13 | * Returns true iff the $_REQUEST or $_GET variables has a key with $name. 14 | * 15 | * @param string $name 16 | * 17 | * @return bool 18 | */ 19 | public static function has($name) 20 | { 21 | if (isset($_REQUEST[$name])) { 22 | return true; 23 | } 24 | 25 | return isset($_GET[$name]); 26 | } 27 | 28 | /** 29 | * Returns the value in $_REQUEST[$name] or $_GET[$name] if the former was empty. If no value found, return null. 30 | * 31 | * @param string $name 32 | * 33 | * @return mixed|null 34 | */ 35 | public static function get($name) 36 | { 37 | if (isset($_REQUEST[$name])) { 38 | return $_REQUEST[$name]; 39 | } 40 | 41 | if (isset($_GET[$name])) { 42 | return $_GET[$name]; 43 | } 44 | 45 | return; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Http/LinkedInUrlGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface LinkedInUrlGeneratorInterface 11 | { 12 | /** 13 | * Build the URL for given domain alias, path and parameters. 14 | * 15 | * @param $name string The name of the domain, 'www' or 'api' 16 | * @param $path string without a leading slash 17 | * @param $params array query parameters 18 | * 19 | * @return string The URL for the given parameters. The URL query MUST be build with PHP_QUERY_RFC3986 20 | */ 21 | public function getUrl($name, $path = '', $params = []); 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/RequestManager.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class RequestManager implements RequestManagerInterface 18 | { 19 | /** 20 | * @var \Http\Client\HttpClient 21 | */ 22 | private $httpClient; 23 | 24 | /** 25 | * @var \Http\Message\MessageFactory 26 | */ 27 | private $messageFactory; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function sendRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1') 33 | { 34 | $request = $this->getMessageFactory()->createRequest($method, $uri, $headers, $body, $protocolVersion); 35 | 36 | try { 37 | return $this->getHttpClient()->sendRequest($request); 38 | } catch (TransferException $e) { 39 | throw new LinkedInTransferException('Error while requesting data from LinkedIn.com: '.$e->getMessage(), $e->getCode(), $e); 40 | } 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function setHttpClient(HttpClient $httpClient) 47 | { 48 | $this->httpClient = $httpClient; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @return HttpClient 55 | */ 56 | protected function getHttpClient() 57 | { 58 | if ($this->httpClient === null) { 59 | $this->httpClient = HttpClientDiscovery::find(); 60 | } 61 | 62 | return $this->httpClient; 63 | } 64 | 65 | /** 66 | * @param MessageFactory $messageFactory 67 | * 68 | * @return RequestManager 69 | */ 70 | public function setMessageFactory(MessageFactory $messageFactory) 71 | { 72 | $this->messageFactory = $messageFactory; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * @return \Http\Message\MessageFactory 79 | */ 80 | private function getMessageFactory() 81 | { 82 | if ($this->messageFactory === null) { 83 | $this->messageFactory = MessageFactoryDiscovery::find(); 84 | } 85 | 86 | return $this->messageFactory; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Http/RequestManagerInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface RequestManagerInterface 14 | { 15 | /** 16 | * Send a request. 17 | * 18 | * @param string $method 19 | * @param string $uri 20 | * @param array $headers 21 | * @param string $body 22 | * @param string $protocolVersion 23 | * 24 | * @return \Psr\Http\Message\ResponseInterface 25 | * 26 | * @throws LinkedInTransferException 27 | */ 28 | public function sendRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'); 29 | 30 | /** 31 | * @param \Http\Client\HttpClient $httpClient 32 | * 33 | * @return RequestManager 34 | */ 35 | public function setHttpClient(HttpClient $httpClient); 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/ResponseConverter.php: -------------------------------------------------------------------------------- 1 | getBody()->__toString(); 35 | case 'simple_xml': 36 | return self::convertToSimpleXml($response); 37 | case 'stream': 38 | return $response->getBody(); 39 | case 'psr7': 40 | return $response; 41 | default: 42 | throw new InvalidArgumentException('Format "%s" is not supported', $dataType); 43 | } 44 | } 45 | 46 | /** 47 | * @param ResponseInterface $response 48 | * 49 | * @return string 50 | */ 51 | public static function convertToArray(ResponseInterface $response) 52 | { 53 | return json_decode($response->getBody(), true); 54 | } 55 | 56 | /** 57 | * @param ResponseInterface $response 58 | * 59 | * @return \SimpleXMLElement 60 | * 61 | * @throws LinkedInTransferException 62 | */ 63 | public static function convertToSimpleXml(ResponseInterface $response) 64 | { 65 | $body = $response->getBody(); 66 | try { 67 | return new \SimpleXMLElement((string) $body ?: ''); 68 | } catch (\Exception $e) { 69 | throw new LinkedInTransferException('Unable to parse response body into XML.'); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Http/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class UrlGenerator implements UrlGeneratorInterface 9 | { 10 | /** 11 | * @var array knownLinkedInParams 12 | * 13 | * A list of params that might be in the query string 14 | */ 15 | public static $knownLinkedInParams = ['state', 'code', 'access_token', 'user']; 16 | 17 | /** 18 | * @var array domainMap 19 | * 20 | * Maps aliases to LinkedIn domains. 21 | */ 22 | public static $domainMap = [ 23 | 'api' => 'https://api.linkedin.com/', 24 | 'www' => 'https://www.linkedin.com/', 25 | ]; 26 | 27 | /** 28 | * @var bool 29 | * 30 | * Indicates if we trust HTTP_X_FORWARDED_* headers. 31 | */ 32 | protected $trustForwarded = false; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getUrl($name, $path = '', $params = []) 38 | { 39 | $url = self::$domainMap[$name]; 40 | if ($path) { 41 | if ($path[0] === '/') { 42 | $path = substr($path, 1); 43 | } 44 | $url .= $path; 45 | } 46 | 47 | if (!empty($params)) { 48 | // does it exist a query string? 49 | $queryString = parse_url($url, PHP_URL_QUERY); 50 | if (empty($queryString)) { 51 | $url .= '?'; 52 | } else { 53 | $url .= '&'; 54 | } 55 | 56 | // it needs to be PHP_QUERY_RFC3986. We want to have %20 between scopes 57 | $url .= http_build_query($params, null, '&', PHP_QUERY_RFC3986); 58 | } 59 | 60 | return $url; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getCurrentUrl() 67 | { 68 | $protocol = $this->getHttpProtocol().'://'; 69 | $host = $this->getHttpHost(); 70 | $currentUrl = $protocol.$host.$_SERVER['REQUEST_URI']; 71 | $parts = parse_url($currentUrl); 72 | 73 | $query = ''; 74 | if (!empty($parts['query'])) { 75 | // drop known linkedin params 76 | $query = $this->dropLinkedInParams($parts['query']); 77 | } 78 | 79 | // use port if non default 80 | $port = 81 | isset($parts['port']) && 82 | (($protocol === 'http://' && $parts['port'] !== 80) || 83 | ($protocol === 'https://' && $parts['port'] !== 443)) 84 | ? ':'.$parts['port'] : ''; 85 | 86 | // rebuild 87 | return $protocol.$parts['host'].$port.$parts['path'].$query; 88 | } 89 | 90 | /** 91 | * Drop known LinkedIn params. Ie those in self::$knownLinkeInParams. 92 | * 93 | * @param string $query 94 | * 95 | * @return string query without LinkedIn params. This string is prepended with a question mark '?' 96 | */ 97 | protected function dropLinkedInParams($query) 98 | { 99 | if ($query == '') { 100 | return ''; 101 | } 102 | 103 | $params = explode('&', $query); 104 | foreach ($params as $i => $param) { 105 | /* 106 | * A key or key/value pair might me 'foo=bar', 'foo=', or 'foo'. 107 | */ 108 | //get the first value of the array you will get when you explode() 109 | list($key) = explode('=', $param, 2); 110 | if (in_array($key, self::$knownLinkedInParams)) { 111 | unset($params[$i]); 112 | } 113 | } 114 | 115 | //assert: params is an array. It might be empty 116 | if (!empty($params)) { 117 | return '?'.implode($params, '&'); 118 | } 119 | 120 | return ''; 121 | } 122 | 123 | /** 124 | * Get the host. 125 | * 126 | * 127 | * @return mixed 128 | */ 129 | protected function getHttpHost() 130 | { 131 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { 132 | return $_SERVER['HTTP_X_FORWARDED_HOST']; 133 | } 134 | 135 | return $_SERVER['HTTP_HOST']; 136 | } 137 | 138 | /** 139 | * Get the protocol. 140 | * 141 | * 142 | * @return string 143 | */ 144 | protected function getHttpProtocol() 145 | { 146 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 147 | if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { 148 | return 'https'; 149 | } 150 | 151 | return 'http'; 152 | } 153 | 154 | /*apache + variants specific way of checking for https*/ 155 | if (isset($_SERVER['HTTPS']) && 156 | ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) { 157 | return 'https'; 158 | } 159 | 160 | /*nginx way of checking for https*/ 161 | if (isset($_SERVER['SERVER_PORT']) && 162 | ($_SERVER['SERVER_PORT'] === '443')) { 163 | return 'https'; 164 | } 165 | 166 | return 'http'; 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function setTrustForwarded($trustForwarded) 173 | { 174 | $this->trustForwarded = $trustForwarded; 175 | 176 | return $this; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Http/UrlGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface UrlGeneratorInterface extends LinkedInUrlGeneratorInterface, CurrentUrlGeneratorInterface 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/LinkedIn.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class LinkedIn implements LinkedInInterface 34 | { 35 | /** 36 | * The OAuth access token received in exchange for a valid authorization 37 | * code. null means the access token has yet to be determined. 38 | * 39 | * @var AccessToken 40 | */ 41 | protected $accessToken = null; 42 | 43 | /** 44 | * @var string format 45 | */ 46 | private $format; 47 | 48 | /** 49 | * @var string responseFormat 50 | */ 51 | private $responseDataType; 52 | 53 | /** 54 | * @var ResponseInterface 55 | */ 56 | private $lastResponse; 57 | 58 | /** 59 | * @var RequestManager 60 | */ 61 | private $requestManager; 62 | 63 | /** 64 | * @var Authenticator 65 | */ 66 | private $authenticator; 67 | 68 | /** 69 | * @var UrlGeneratorInterface 70 | */ 71 | private $urlGenerator; 72 | 73 | /** 74 | * Constructor. 75 | * 76 | * @param string $appId 77 | * @param string $appSecret 78 | * @param string $format 'json', 'xml' 79 | * @param string $responseDataType 'array', 'string', 'simple_xml' 'psr7', 'stream' 80 | */ 81 | public function __construct($appId, $appSecret, $format = 'json', $responseDataType = 'array') 82 | { 83 | $this->format = $format; 84 | $this->responseDataType = $responseDataType; 85 | 86 | $this->requestManager = new RequestManager(); 87 | $this->authenticator = new Authenticator($this->requestManager, $appId, $appSecret); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function isAuthenticated() 94 | { 95 | $accessToken = $this->getAccessToken(); 96 | if ($accessToken === null) { 97 | return false; 98 | } 99 | 100 | $user = $this->api('GET', '/v1/people/~:(id,firstName,lastName)', ['format' => 'json', 'response_data_type' => 'array']); 101 | 102 | return !empty($user['id']); 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function api($method, $resource, array $options = []) 109 | { 110 | // Add access token to the headers 111 | $options['headers']['Authorization'] = sprintf('Bearer %s', (string) $this->getAccessToken()); 112 | 113 | // Do logic and adjustments to the options 114 | $requestFormat = $this->filterRequestOption($options); 115 | 116 | // Generate an url 117 | $url = $this->getUrlGenerator()->getUrl( 118 | 'api', 119 | $resource, 120 | isset($options['query']) ? $options['query'] : [] 121 | ); 122 | 123 | $body = isset($options['body']) ? $options['body'] : null; 124 | $this->lastResponse = $this->getRequestManager()->sendRequest($method, $url, $options['headers'], $body); 125 | 126 | //Get the response data format 127 | if (isset($options['response_data_type'])) { 128 | $responseDataType = $options['response_data_type']; 129 | } else { 130 | $responseDataType = $this->getResponseDataType(); 131 | } 132 | 133 | return ResponseConverter::convert($this->lastResponse, $requestFormat, $responseDataType); 134 | } 135 | 136 | /** 137 | * Modify and filter the request options. Make sure we use the correct query parameters and headers. 138 | * 139 | * @param array &$options 140 | * 141 | * @return string the request format to use 142 | */ 143 | protected function filterRequestOption(array &$options) 144 | { 145 | if (isset($options['json'])) { 146 | $options['format'] = 'json'; 147 | $options['body'] = json_encode($options['json']); 148 | } elseif (!isset($options['format'])) { 149 | // Make sure we always have a format 150 | $options['format'] = $this->getFormat(); 151 | } 152 | 153 | // Set correct headers for this format 154 | switch ($options['format']) { 155 | case 'xml': 156 | $options['headers']['Content-Type'] = 'text/xml'; 157 | break; 158 | case 'json': 159 | $options['headers']['Content-Type'] = 'application/json'; 160 | $options['headers']['x-li-format'] = 'json'; 161 | $options['query']['format'] = 'json'; 162 | break; 163 | default: 164 | // Do nothing 165 | } 166 | 167 | return $options['format']; 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function getLoginUrl($options = []) 174 | { 175 | $urlGenerator = $this->getUrlGenerator(); 176 | 177 | // Set redirect_uri to current URL if not defined 178 | if (!isset($options['redirect_uri'])) { 179 | $options['redirect_uri'] = $urlGenerator->getCurrentUrl(); 180 | } 181 | 182 | return $this->getAuthenticator()->getLoginUrl($urlGenerator, $options); 183 | } 184 | 185 | /** 186 | * See docs for LinkedIn::api(). 187 | * 188 | * @param string $resource 189 | * @param array $options 190 | * 191 | * @return mixed 192 | */ 193 | public function get($resource, array $options = []) 194 | { 195 | return $this->api('GET', $resource, $options); 196 | } 197 | 198 | /** 199 | * See docs for LinkedIn::api(). 200 | * 201 | * @param string $resource 202 | * @param array $options 203 | * 204 | * @return mixed 205 | */ 206 | public function post($resource, array $options = []) 207 | { 208 | return $this->api('POST', $resource, $options); 209 | } 210 | 211 | /** 212 | * {@inheritdoc} 213 | */ 214 | public function clearStorage() 215 | { 216 | $this->getAuthenticator()->clearStorage(); 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * {@inheritdoc} 223 | */ 224 | public function hasError() 225 | { 226 | return GlobalVariableGetter::has('error'); 227 | } 228 | 229 | /** 230 | * {@inheritdoc} 231 | */ 232 | public function getError() 233 | { 234 | if ($this->hasError()) { 235 | return new LoginError(GlobalVariableGetter::get('error'), GlobalVariableGetter::get('error_description')); 236 | } 237 | } 238 | 239 | /** 240 | * Get the default format to use when sending requests. 241 | * 242 | * @return string 243 | */ 244 | protected function getFormat() 245 | { 246 | return $this->format; 247 | } 248 | 249 | /** 250 | * {@inheritdoc} 251 | */ 252 | public function setFormat($format) 253 | { 254 | $this->format = $format; 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * Get the default data type to be returned as a response. 261 | * 262 | * @return string 263 | */ 264 | protected function getResponseDataType() 265 | { 266 | return $this->responseDataType; 267 | } 268 | 269 | /** 270 | * {@inheritdoc} 271 | */ 272 | public function setResponseDataType($responseDataType) 273 | { 274 | $this->responseDataType = $responseDataType; 275 | 276 | return $this; 277 | } 278 | 279 | /** 280 | * {@inheritdoc} 281 | */ 282 | public function getLastResponse() 283 | { 284 | return $this->lastResponse; 285 | } 286 | 287 | /** 288 | * {@inheritdoc} 289 | */ 290 | public function getAccessToken() 291 | { 292 | if ($this->accessToken === null) { 293 | if (null !== $newAccessToken = $this->getAuthenticator()->fetchNewAccessToken($this->getUrlGenerator())) { 294 | $this->setAccessToken($newAccessToken); 295 | } 296 | } 297 | 298 | // return the new access token or null if none found 299 | return $this->accessToken; 300 | } 301 | 302 | /** 303 | * {@inheritdoc} 304 | */ 305 | public function setAccessToken($accessToken) 306 | { 307 | if (!($accessToken instanceof AccessToken)) { 308 | $accessToken = new AccessToken($accessToken); 309 | } 310 | 311 | $this->accessToken = $accessToken; 312 | 313 | return $this; 314 | } 315 | 316 | /** 317 | * {@inheritdoc} 318 | */ 319 | public function setUrlGenerator(UrlGeneratorInterface $urlGenerator) 320 | { 321 | $this->urlGenerator = $urlGenerator; 322 | 323 | return $this; 324 | } 325 | 326 | /** 327 | * @return UrlGeneratorInterface 328 | */ 329 | protected function getUrlGenerator() 330 | { 331 | if ($this->urlGenerator === null) { 332 | $this->urlGenerator = new UrlGenerator(); 333 | } 334 | 335 | return $this->urlGenerator; 336 | } 337 | 338 | /** 339 | * {@inheritdoc} 340 | */ 341 | public function setStorage(DataStorageInterface $storage) 342 | { 343 | $this->getAuthenticator()->setStorage($storage); 344 | 345 | return $this; 346 | } 347 | 348 | /** 349 | * {@inheritdoc} 350 | */ 351 | public function setHttpClient(HttpClient $client) 352 | { 353 | $this->getRequestManager()->setHttpClient($client); 354 | 355 | return $this; 356 | } 357 | 358 | /** 359 | * {@inheritdoc} 360 | */ 361 | public function setHttpMessageFactory(MessageFactory $factory) 362 | { 363 | $this->getRequestManager()->setMessageFactory($factory); 364 | 365 | return $this; 366 | } 367 | 368 | /** 369 | * @return RequestManager 370 | */ 371 | protected function getRequestManager() 372 | { 373 | return $this->requestManager; 374 | } 375 | 376 | /** 377 | * @return Authenticator 378 | */ 379 | protected function getAuthenticator() 380 | { 381 | return $this->authenticator; 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/LinkedInInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface LinkedInInterface 18 | { 19 | /** 20 | * Is the current user authenticated? 21 | * 22 | * @return bool 23 | */ 24 | public function isAuthenticated(); 25 | 26 | /** 27 | * Make an API call. Read about what calls that are possible here: https://developer.linkedin.com/docs/rest-api. 28 | * 29 | * Example: 30 | * $linkedIn->api('GET', '/v1/people/~:(id,firstName,lastName,headline)'); 31 | * 32 | * The options: 33 | * - body: the body of the request 34 | * - format: the format you are using to send the request 35 | * - headers: array with headers to use 36 | * - response_data_type: the data type to get back 37 | * - query: query parameters to the request 38 | * 39 | * @param string $method This is the HTTP verb 40 | * @param string $resource everything after the domain in the URL. 41 | * @param array $options See the readme for option description. 42 | * 43 | * @return mixed this depends on the response_data_type parameter. 44 | */ 45 | public function api($method, $resource, array $options = []); 46 | 47 | /** 48 | * Get a login URL where the user can put his/hers LinkedIn credentials and authorize the application. 49 | * 50 | * The options: 51 | * - redirect_uri: the url to go to after a successful login 52 | * - scope: comma (or space) separated list of requested extended permissions 53 | * 54 | * @param array $options Provide custom parameters 55 | * 56 | * @return string The URL for the login flow 57 | */ 58 | public function getLoginUrl($options = []); 59 | 60 | /** 61 | * See docs for LinkedIn::api(). 62 | * 63 | * @param string $resource 64 | * @param array $options 65 | * 66 | * @return mixed 67 | */ 68 | public function get($resource, array $options = []); 69 | 70 | /** 71 | * See docs for LinkedIn::api(). 72 | * 73 | * @param string $resource 74 | * @param array $options 75 | * 76 | * @return mixed 77 | */ 78 | public function post($resource, array $options = []); 79 | 80 | /** 81 | * Clear the data storage. This will forget everything about the user and authentication process. 82 | * 83 | * @return $this 84 | */ 85 | public function clearStorage(); 86 | 87 | /** 88 | * If the user has canceled the login we will return with an error. 89 | * 90 | * @return bool 91 | */ 92 | public function hasError(); 93 | 94 | /** 95 | * Returns a LoginError or null. 96 | * 97 | * @return LoginError|null 98 | */ 99 | public function getError(); 100 | 101 | /** 102 | * Set the default format to use when sending requests. 103 | * 104 | * @param string $format 105 | * 106 | * @return $this 107 | */ 108 | public function setFormat($format); 109 | 110 | /** 111 | * Set the default data type to be returned as a response. 112 | * 113 | * @param string $responseDataType 114 | * 115 | * @return $this 116 | */ 117 | public function setResponseDataType($responseDataType); 118 | 119 | /** 120 | * Get the last response. This will always return a PSR-7 response no matter of the data type used. 121 | * 122 | * @return ResponseInterface|null 123 | */ 124 | public function getLastResponse(); 125 | 126 | /** 127 | * Returns an access token. If we do not have one in memory, try to fetch one from a *code* in the $_REQUEST. 128 | * 129 | * @return AccessToken|null The access token of null if the access token is not found 130 | */ 131 | public function getAccessToken(); 132 | 133 | /** 134 | * If you have stored a previous access token in a storage (database) you could set it here. After setting an 135 | * access token you have to make sure to verify it is still valid by running LinkedIn::isAuthenticated. 136 | * 137 | * @param string|AccessToken $accessToken 138 | * 139 | * @return $this 140 | */ 141 | public function setAccessToken($accessToken); 142 | 143 | /** 144 | * Set a URL generator. 145 | * 146 | * @param UrlGeneratorInterface $urlGenerator 147 | * 148 | * @return $this 149 | */ 150 | public function setUrlGenerator(UrlGeneratorInterface $urlGenerator); 151 | 152 | /** 153 | * Set a data storage. 154 | * 155 | * @param DataStorageInterface $storage 156 | * 157 | * @return $this 158 | */ 159 | public function setStorage(DataStorageInterface $storage); 160 | 161 | /** 162 | * Set a http client. 163 | * 164 | * @param HttpClient $client 165 | * 166 | * @return $this 167 | */ 168 | public function setHttpClient(HttpClient $client); 169 | 170 | /** 171 | * Set a http message factory. 172 | * 173 | * @param MessageFactory $factory 174 | * 175 | * @return $this 176 | */ 177 | public function setHttpMessageFactory(MessageFactory $factory); 178 | } 179 | -------------------------------------------------------------------------------- /src/Storage/BaseDataStorage.php: -------------------------------------------------------------------------------- 1 | clear($key); 21 | } 22 | } 23 | 24 | /** 25 | * Validate key. Throws an exception if key is not valid. 26 | * 27 | * @param string $key 28 | * 29 | * @throws InvalidArgumentException 30 | */ 31 | protected function validateKey($key) 32 | { 33 | if (!in_array($key, self::$validKeys)) { 34 | throw new InvalidArgumentException('Unsupported key "%s" passed to LinkedIn data storage. Valid keys are: %s', $key, implode(', ', self::$validKeys)); 35 | } 36 | } 37 | 38 | /** 39 | * Generate an ID to use with the data storage. 40 | * 41 | * @param $key 42 | * 43 | * @return string 44 | */ 45 | protected function getStorageKeyId($key) 46 | { 47 | return 'linkedIn_'.$key; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Storage/DataStorageInterface.php: -------------------------------------------------------------------------------- 1 | validateKey($key); 20 | $name = $this->getStorageKeyId($key); 21 | 22 | return Session::put($name, $value); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function get($key) 29 | { 30 | $this->validateKey($key); 31 | $name = $this->getStorageKeyId($key); 32 | 33 | return Session::get($name); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function clear($key) 40 | { 41 | $this->validateKey($key); 42 | $name = $this->getStorageKeyId($key); 43 | 44 | return Session::forget($name); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Storage/SessionStorage.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class SessionStorage extends BaseDataStorage 11 | { 12 | public function __construct() 13 | { 14 | //start the session if it not already been started 15 | if (php_sapi_name() !== 'cli') { 16 | if (session_id() === '') { 17 | session_start(); 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function set($key, $value) 26 | { 27 | $this->validateKey($key); 28 | 29 | $name = $this->getStorageKeyId($key); 30 | $_SESSION[$name] = $value; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function get($key) 37 | { 38 | $this->validateKey($key); 39 | $name = $this->getStorageKeyId($key); 40 | 41 | return isset($_SESSION[$name]) ? $_SESSION[$name] : null; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function clear($key) 48 | { 49 | $this->validateKey($key); 50 | 51 | $name = $this->getStorageKeyId($key); 52 | if (isset($_SESSION[$name])) { 53 | unset($_SESSION[$name]); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/AccessTokenTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('', $token); 14 | 15 | $token->setToken('foobar'); 16 | $this->assertEquals('foobar', $token); 17 | } 18 | 19 | public function testConstructor() 20 | { 21 | $token = new AccessToken('foobar', 10); 22 | $this->assertInstanceOf('\DateTime', $token->getExpiresAt()); 23 | $this->assertEquals('foobar', $token->getToken()); 24 | 25 | $token = new AccessToken(); 26 | $this->assertNull($token->getExpiresAt()); 27 | $this->assertEmpty($token->getToken()); 28 | 29 | $token = new AccessToken(null, new \DateTime('+2minutes')); 30 | $this->assertInstanceOf('\DateTime', $token->getExpiresAt()); 31 | } 32 | 33 | public function testSetExpiresAt() 34 | { 35 | $token = new AccessToken(); 36 | $token->setExpiresAt(new \DateTime('+2minutes')); 37 | $this->assertInstanceOf('\DateTime', $token->getExpiresAt()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/AuthenticatorTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class AuthenticatorTest extends \PHPUnit_Framework_TestCase 13 | { 14 | const APP_ID = '123456789'; 15 | const APP_SECRET = '987654321'; 16 | 17 | private function getRequestManagerMock() 18 | { 19 | return m::mock('Happyr\LinkedIn\Http\RequestManager'); 20 | } 21 | 22 | public function testGetLoginUrl() 23 | { 24 | $expected = 'loginUrl'; 25 | $state = 'random'; 26 | $params = [ 27 | 'response_type' => 'code', 28 | 'client_id' => self::APP_ID, 29 | 'redirect_uri' => null, 30 | 'state' => $state, 31 | ]; 32 | 33 | $storage = $this->getMock('Happyr\LinkedIn\Storage\DataStorageInterface'); 34 | $storage->method('get')->with('state')->willReturn($state); 35 | 36 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['establishCSRFTokenState', 'getStorage'], [$this->getRequestManagerMock(), self::APP_ID, self::APP_SECRET]); 37 | $auth->expects($this->exactly(2))->method('establishCSRFTokenState')->willReturn(null); 38 | $auth->method('getStorage')->will($this->returnValue($storage)); 39 | 40 | $generator = m::mock('Happyr\LinkedIn\Http\LinkedInUrlGeneratorInterface') 41 | ->shouldReceive('getUrl')->once()->with('www', 'oauth/v2/authorization', $params)->andReturn($expected) 42 | ->getMock(); 43 | 44 | $this->assertEquals($expected, $auth->getLoginUrl($generator)); 45 | 46 | /* 47 | * Test with a url in the param 48 | */ 49 | $otherUrl = 'otherUrl'; 50 | $scope = ['foo', 'bar', 'baz']; 51 | $params = [ 52 | 'response_type' => 'code', 53 | 'client_id' => self::APP_ID, 54 | 'redirect_uri' => $otherUrl, 55 | 'state' => $state, 56 | 'scope' => 'foo bar baz', 57 | ]; 58 | 59 | $generator = m::mock('Happyr\LinkedIn\Http\LinkedInUrlGeneratorInterface') 60 | ->shouldReceive('getUrl')->once()->with('www', 'oauth/v2/authorization', $params)->andReturn($expected) 61 | ->getMock(); 62 | 63 | $this->assertEquals($expected, $auth->getLoginUrl($generator, ['redirect_uri' => $otherUrl, 'scope' => $scope])); 64 | } 65 | 66 | public function testFetchNewAccessToken() 67 | { 68 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator'); 69 | $code = 'newCode'; 70 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 71 | ->shouldReceive('set')->once()->with('code', $code) 72 | ->shouldReceive('set')->once()->with('access_token', 'at') 73 | ->getMock(); 74 | 75 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getCode', 'getStorage', 'getAccessTokenFromCode'], [], '', false); 76 | $auth->expects($this->any())->method('getStorage')->will($this->returnValue($storage)); 77 | $auth->expects($this->once())->method('getAccessTokenFromCode')->with($generator, $code)->will($this->returnValue('at')); 78 | $auth->expects($this->once())->method('getCode')->will($this->returnValue($code)); 79 | 80 | $this->assertEquals('at', $auth->fetchNewAccessToken($generator)); 81 | } 82 | 83 | /** 84 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInException 85 | */ 86 | public function testFetchNewAccessTokenFail() 87 | { 88 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator'); 89 | $code = 'newCode'; 90 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 91 | ->shouldReceive('clearAll')->once() 92 | ->getMock(); 93 | 94 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getCode', 'getStorage', 'getAccessTokenFromCode'], [], '', false); 95 | $auth->expects($this->any())->method('getStorage')->will($this->returnValue($storage)); 96 | $auth->expects($this->once())->method('getAccessTokenFromCode')->with($generator, $code)->willThrowException(new LinkedInException()); 97 | $auth->expects($this->once())->method('getCode')->will($this->returnValue($code)); 98 | 99 | $auth->fetchNewAccessToken($generator); 100 | } 101 | 102 | public function testFetchNewAccessTokenNoCode() 103 | { 104 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator'); 105 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 106 | ->shouldReceive('get')->with('code')->andReturn('foobar') 107 | ->shouldReceive('get')->once()->with('access_token')->andReturn('baz') 108 | ->getMock(); 109 | 110 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getCode', 'getStorage'], [], '', false); 111 | $auth->expects($this->any())->method('getStorage')->will($this->returnValue($storage)); 112 | $auth->expects($this->once())->method('getCode'); 113 | 114 | $this->assertEquals('baz', $auth->fetchNewAccessToken($generator)); 115 | } 116 | 117 | /** 118 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInException 119 | */ 120 | public function testGetAccessTokenFromCodeEmptyString() 121 | { 122 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator'); 123 | 124 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getAccessTokenFromCode'); 125 | $method->setAccessible(true); 126 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', [], [], '', false); 127 | 128 | $method->invoke($auth, $generator, ''); 129 | } 130 | 131 | /** 132 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInException 133 | */ 134 | public function testGetAccessTokenFromCodeNull() 135 | { 136 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator'); 137 | 138 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getAccessTokenFromCode'); 139 | $method->setAccessible(true); 140 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', [], [], '', false); 141 | 142 | $method->invoke($auth, $generator, null); 143 | } 144 | 145 | /** 146 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInException 147 | */ 148 | public function testGetAccessTokenFromCodeFalse() 149 | { 150 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator'); 151 | 152 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getAccessTokenFromCode'); 153 | $method->setAccessible(true); 154 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', [], [], '', false); 155 | 156 | $method->invoke($auth, $generator, false); 157 | } 158 | 159 | public function testGetAccessTokenFromCode() 160 | { 161 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getAccessTokenFromCode'); 162 | $method->setAccessible(true); 163 | 164 | $code = 'code'; 165 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator') 166 | ->shouldReceive('getUrl')->with( 167 | 'www', 168 | 'oauth/v2/accessToken' 169 | )->andReturn('url') 170 | ->getMock(); 171 | 172 | $response = ['access_token' => 'foobar', 'expires_in' => 10]; 173 | $auth = $this->prepareGetAccessTokenFromCode($code, $response); 174 | $token = $method->invoke($auth, $generator, $code); 175 | $this->assertEquals('foobar', $token, 'Standard get access token form code'); 176 | } 177 | 178 | /** 179 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInException 180 | */ 181 | public function testGetAccessTokenFromCodeNoTokenInResponse() 182 | { 183 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getAccessTokenFromCode'); 184 | $method->setAccessible(true); 185 | 186 | $code = 'code'; 187 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator') 188 | ->shouldReceive('getUrl')->with( 189 | 'www', 190 | 'oauth/v2/accessToken' 191 | )->andReturn('url') 192 | ->getMock(); 193 | 194 | $response = ['foo' => 'bar']; 195 | $auth = $this->prepareGetAccessTokenFromCode($code, $response); 196 | $this->assertNull($method->invoke($auth, $generator, $code), 'Found array but no access token'); 197 | } 198 | 199 | /** 200 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInException 201 | */ 202 | public function testGetAccessTokenFromCodeEmptyResponse() 203 | { 204 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getAccessTokenFromCode'); 205 | $method->setAccessible(true); 206 | 207 | $code = 'code'; 208 | $generator = m::mock('Happyr\LinkedIn\Http\UrlGenerator') 209 | ->shouldReceive('getUrl')->with( 210 | 'www', 211 | 'oauth/v2/accessToken' 212 | )->andReturn('url') 213 | ->getMock(); 214 | 215 | $response = ''; 216 | $auth = $this->prepareGetAccessTokenFromCode($code, $response); 217 | $this->assertNull($method->invoke($auth, $generator, $code), 'Empty result'); 218 | } 219 | 220 | /** 221 | * Default stuff for GetAccessTokenFromCode. 222 | * 223 | * @param $response 224 | * 225 | * @return array 226 | */ 227 | protected function prepareGetAccessTokenFromCode($code, $responseData) 228 | { 229 | $response = new Response(200, [], json_encode($responseData)); 230 | $currentUrl = 'foobar'; 231 | 232 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 233 | ->shouldReceive('get')->with('redirect_uri')->andReturn($currentUrl) 234 | ->getMock(); 235 | 236 | $requestManager = m::mock('Happyr\LinkedIn\Http\RequestManager') 237 | ->shouldReceive('sendRequest')->once()->with('POST', 'url', [ 238 | 'Content-Type' => 'application/x-www-form-urlencoded', 239 | ], http_build_query([ 240 | 'grant_type' => 'authorization_code', 241 | 'code' => $code, 242 | 'redirect_uri' => $currentUrl, 243 | 'client_id' => self::APP_ID, 244 | 'client_secret' => self::APP_SECRET, 245 | ]))->andReturn($response) 246 | ->getMock(); 247 | 248 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getStorage'], [$requestManager, self::APP_ID, self::APP_SECRET]); 249 | $auth->expects($this->any())->method('getStorage')->will($this->returnValue($storage)); 250 | 251 | return $auth; 252 | } 253 | 254 | public function testEstablishCSRFTokenState() 255 | { 256 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'establishCSRFTokenState'); 257 | $method->setAccessible(true); 258 | 259 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 260 | ->shouldReceive('get')->with('state')->andReturn(null, 'state') 261 | ->shouldReceive('set')->once()->with('state', \Mockery::on(function (&$param) { 262 | return !empty($param); 263 | })) 264 | ->getMock(); 265 | 266 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getStorage'], [], '', false); 267 | $auth->expects($this->any())->method('getStorage')->will($this->returnValue($storage)); 268 | 269 | // Make sure we only set the state once 270 | $method->invoke($auth); 271 | $method->invoke($auth); 272 | } 273 | 274 | public function testGetCodeEmpty() 275 | { 276 | unset($_REQUEST['code']); 277 | unset($_GET['code']); 278 | 279 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getCode'); 280 | $method->setAccessible(true); 281 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', [], [], '', false); 282 | 283 | $this->assertNull($method->invoke($auth)); 284 | } 285 | 286 | public function testGetCode() 287 | { 288 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getCode'); 289 | $method->setAccessible(true); 290 | $state = 'bazbar'; 291 | 292 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 293 | ->shouldReceive('clear')->once()->with('state') 294 | ->shouldReceive('get')->once()->with('code')->andReturn(null) 295 | ->shouldReceive('get')->once()->with('state')->andReturn($state) 296 | ->getMock(); 297 | 298 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getStorage'], [], '', false); 299 | $auth->expects($this->once())->method('getStorage')->will($this->returnValue($storage)); 300 | 301 | $_REQUEST['code'] = 'foobar'; 302 | $_REQUEST['state'] = $state; 303 | 304 | $this->assertEquals('foobar', $method->invoke($auth)); 305 | } 306 | 307 | /** 308 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInException 309 | */ 310 | public function testGetCodeInvalidCode() 311 | { 312 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getCode'); 313 | $method->setAccessible(true); 314 | 315 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 316 | ->shouldReceive('get')->once()->with('code')->andReturn(null) 317 | ->shouldReceive('get')->once()->with('state')->andReturn('bazbar') 318 | ->getMock(); 319 | 320 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getStorage'], [], '', false); 321 | $auth->expects($this->once())->method('getStorage')->will($this->returnValue($storage)); 322 | 323 | $_REQUEST['code'] = 'foobar'; 324 | $_REQUEST['state'] = 'invalid'; 325 | 326 | $this->assertEquals('foobar', $method->invoke($auth)); 327 | } 328 | 329 | public function testGetCodeUsedCode() 330 | { 331 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getCode'); 332 | $method->setAccessible(true); 333 | 334 | $storage = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface') 335 | ->shouldReceive('get')->once()->with('code')->andReturn('foobar') 336 | ->getMock(); 337 | 338 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getStorage'], [], '', false); 339 | $auth->expects($this->once())->method('getStorage')->will($this->returnValue($storage)); 340 | 341 | $_REQUEST['code'] = 'foobar'; 342 | 343 | $this->assertEquals(null, $method->invoke($auth)); 344 | } 345 | 346 | public function testStorageAccessors() 347 | { 348 | $method = new \ReflectionMethod('Happyr\LinkedIn\Authenticator', 'getStorage'); 349 | $method->setAccessible(true); 350 | $requestManager = $this->getRequestManagerMock(); 351 | $auth = new Authenticator($requestManager, self::APP_ID, self::APP_SECRET); 352 | 353 | // test default 354 | $this->assertInstanceOf('Happyr\LinkedIn\Storage\SessionStorage', $method->invoke($auth)); 355 | 356 | $object = m::mock('Happyr\LinkedIn\Storage\DataStorageInterface'); 357 | $auth->setStorage($object); 358 | $this->assertEquals($object, $method->invoke($auth)); 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /tests/Exceptions/LoginErrorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('foo', $error->getName()); 17 | $this->assertEquals('bar', $error->getDescription()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Http/ResponseConverterTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Psr\Http\Message\ResponseInterface', $result); 16 | 17 | $result = ResponseConverter::convert($response, 'json', 'stream'); 18 | $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $result); 19 | 20 | $result = ResponseConverter::convert($response, 'json', 'string'); 21 | $this->assertTrue(is_string($result)); 22 | $this->assertEquals($body, $result); 23 | 24 | $result = ResponseConverter::convert($response, 'json', 'array'); 25 | $this->assertTrue(is_array($result)); 26 | 27 | $body = ' 28 | 29 | foo 30 | bar 31 | 32 | '; 33 | $response = new Response(200, [], $body); 34 | $result = ResponseConverter::convert($response, 'xml', 'simple_xml'); 35 | $this->assertInstanceOf('\SimpleXMLElement', $result); 36 | } 37 | 38 | /** 39 | * @expectedException \Happyr\LinkedIn\Exception\InvalidArgumentException 40 | */ 41 | public function testConvertJsonToSimpleXml() 42 | { 43 | $body = '{"foo":"bar"}'; 44 | $response = new Response(200, [], $body); 45 | 46 | ResponseConverter::convert($response, 'json', 'simple_xml'); 47 | } 48 | 49 | /** 50 | * @expectedException \Happyr\LinkedIn\Exception\InvalidArgumentException 51 | */ 52 | public function testConvertXmlToArray() 53 | { 54 | $body = ' 55 | 56 | foo 57 | bar 58 | 59 | '; 60 | $response = new Response(200, [], $body); 61 | 62 | ResponseConverter::convert($response, 'xml', 'array'); 63 | } 64 | 65 | /** 66 | * @expectedException \Happyr\LinkedIn\Exception\InvalidArgumentException 67 | */ 68 | public function testConvertJsonToFoobar() 69 | { 70 | $body = '{"foo":"bar"}'; 71 | $response = new Response(200, [], $body); 72 | 73 | ResponseConverter::convert($response, 'json', 'foobar'); 74 | } 75 | 76 | public function testConvertToSimpleXml() 77 | { 78 | $body = ' 79 | 80 | foo 81 | bar 82 | 83 | '; 84 | 85 | $response = new Response(200, [], $body); 86 | $result = ResponseConverter::convertToSimpleXml($response); 87 | 88 | $this->assertInstanceOf('\SimpleXMLElement', $result); 89 | $this->assertEquals('foo', $result->firstname); 90 | } 91 | 92 | /** 93 | * @expectedException \Happyr\LinkedIn\Exception\LinkedInTransferException 94 | */ 95 | public function testConvertToSimpleXmlError() 96 | { 97 | $body = '{Foo: bar}'; 98 | 99 | $response = new Response(200, [], $body); 100 | $result = ResponseConverter::convertToSimpleXml($response); 101 | 102 | $this->assertInstanceOf('\SimpleXMLElement', $result); 103 | $this->assertEquals('foo', $result->firstname); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/Http/UrlGeneratorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $gen->dropLinkedInParams($test)); 19 | 20 | $test = 'code=foobar&baz=foo'; 21 | $expected = '?baz=foo'; 22 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 23 | 24 | $test = 'foo=bar&code=foobar'; 25 | $expected = '?foo=bar'; 26 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 27 | 28 | $test = 'code=foobar'; 29 | $expected = ''; 30 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 31 | 32 | $test = ''; 33 | $expected = ''; 34 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 35 | 36 | /* ----------------- */ 37 | 38 | $test = 'foo=bar&code='; 39 | $expected = '?foo=bar'; 40 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 41 | 42 | $test = 'code='; 43 | $expected = ''; 44 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 45 | 46 | $test = 'foo=bar&code'; 47 | $expected = '?foo=bar'; 48 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 49 | 50 | $test = 'code'; 51 | $expected = ''; 52 | $this->assertEquals($expected, $gen->dropLinkedInParams($test)); 53 | } 54 | 55 | public function testGetUrl() 56 | { 57 | $gen = new DummyUrlGenerator(); 58 | 59 | $expected = 'https://api.linkedin.com/?bar=baz'; 60 | $this->assertEquals($expected, $gen->getUrl('api', '', ['bar' => 'baz']), 'No path'); 61 | 62 | $expected = 'https://api.linkedin.com/foobar'; 63 | $this->assertEquals($expected, $gen->getUrl('api', 'foobar'), 'Path does not begin with forward slash'); 64 | $this->assertEquals($expected, $gen->getUrl('api', '/foobar'), 'Path begins with forward slash'); 65 | 66 | $expected = 'https://api.linkedin.com/foobar?bar=baz'; 67 | $this->assertEquals($expected, $gen->getUrl('api', 'foobar', ['bar' => 'baz']), 'One parameter'); 68 | 69 | $expected = 'https://api.linkedin.com/foobar?bar=baz&a=b&c=d'; 70 | $this->assertEquals($expected, $gen->getUrl('api', 'foobar', ['bar' => 'baz', 'a' => 'b', 'c' => 'd']), 'Many parameters'); 71 | 72 | $expected = 'https://api.linkedin.com/foobar?bar=baz%20a%20b'; 73 | $notExpected = 'https://api.linkedin.com/foobar?bar=baz+a+b'; 74 | $this->assertEquals($expected, $gen->getUrl('api', 'foobar', ['bar' => 'baz a b']), 'Use of PHP_QUERY_RFC3986'); 75 | $this->assertNotEquals($notExpected, $gen->getUrl('api', 'foobar', ['bar' => 'baz a b']), 'Dont use PHP_QUERY_RFC1738'); 76 | } 77 | 78 | public function testGetUrlWithParams() 79 | { 80 | $gen = new UrlGenerator(); 81 | 82 | $expected = 'https://api.linkedin.com/endpoint?bar=baz&format=json'; 83 | $this->assertEquals($expected, $gen->getUrl('api', 'endpoint?bar=baz', ['format' => 'json'])); 84 | 85 | $expected = 'https://api.linkedin.com/endpoint?bar=baz&bar=baz'; 86 | $this->assertEquals($expected, $gen->getUrl('api', 'endpoint?bar=baz', ['bar' => 'baz'])); 87 | } 88 | 89 | public function testGetCurrentURL() 90 | { 91 | $gen = $this->getMock('Happyr\LinkedIn\Http\UrlGenerator', ['getHttpProtocol', 'getHttpHost', 'dropLinkedInParams'], []); 92 | $gen->expects($this->any())->method('getHttpProtocol')->will($this->returnValue('http')); 93 | $gen->expects($this->any())->method('getHttpHost')->will($this->returnValue('www.test.com')); 94 | $gen->expects($this->any())->method('dropLinkedInParams')->will($this->returnCallback(function ($arg) { 95 | return empty($arg) ? '' : '?'.$arg; 96 | })); 97 | 98 | // fake the HPHP $_SERVER globals 99 | $_SERVER['REQUEST_URI'] = '/unit-tests.php?one=one&two=two&three=three'; 100 | $this->assertEquals( 101 | 'http://www.test.com/unit-tests.php?one=one&two=two&three=three', 102 | $gen->getCurrentUrl(), 103 | 'getCurrentUrl function is changing the current URL'); 104 | 105 | // ensure structure of valueless GET params is retained (sometimes 106 | // an = sign was present, and sometimes it was not) 107 | // first test when equal signs are present 108 | $_SERVER['REQUEST_URI'] = '/unit-tests.php?one=&two=&three='; 109 | $this->assertEquals( 110 | 'http://www.test.com/unit-tests.php?one=&two=&three=', 111 | $gen->getCurrentUrl(), 112 | 'getCurrentUrl function is changing the current URL'); 113 | 114 | // now confirm that 115 | $_SERVER['REQUEST_URI'] = '/unit-tests.php?one&two&three'; 116 | $this->assertEquals( 117 | 'http://www.test.com/unit-tests.php?one&two&three', 118 | $gen->getCurrentUrl(), 119 | 'getCurrentUrl function is changing the current URL' 120 | ); 121 | } 122 | 123 | public function testGetCurrentURLPort80() 124 | { 125 | $gen = $this->getMock('Happyr\LinkedIn\Http\UrlGenerator', ['getHttpProtocol', 'getHttpHost', 'dropLinkedInParams'], []); 126 | $gen->expects($this->any())->method('getHttpProtocol')->will($this->returnValue('http')); 127 | $gen->expects($this->any())->method('getHttpHost')->will($this->returnValue('www.test.com:80')); 128 | $gen->expects($this->any())->method('dropLinkedInParams')->will($this->returnCallback(function ($arg) { 129 | return empty($arg) ? '' : '?'.$arg; 130 | })); 131 | 132 | //test port 80 133 | $_SERVER['REQUEST_URI'] = '/foobar.php'; 134 | $this->assertEquals( 135 | 'http://www.test.com/foobar.php', 136 | $gen->getCurrentUrl(), 137 | 'port 80 should not be shown' 138 | ); 139 | } 140 | 141 | public function testGetCurrentURLPort8080() 142 | { 143 | $gen = $this->getMock('Happyr\LinkedIn\Http\UrlGenerator', ['getHttpProtocol', 'getHttpHost', 'dropLinkedInParams'], []); 144 | $gen->expects($this->any())->method('getHttpProtocol')->will($this->returnValue('http')); 145 | $gen->expects($this->any())->method('getHttpHost')->will($this->returnValue('www.test.com:8080')); 146 | $gen->expects($this->any())->method('dropLinkedInParams')->will($this->returnCallback(function ($arg) { 147 | return empty($arg) ? '' : '?'.$arg; 148 | })); 149 | 150 | //test non default port 8080 151 | $_SERVER['REQUEST_URI'] = '/foobar.php'; 152 | $this->assertEquals( 153 | 'http://www.test.com:8080/foobar.php', 154 | $gen->getCurrentUrl(), 155 | 'port 80 should not be shown' 156 | ); 157 | } 158 | 159 | public function testHttpHost() 160 | { 161 | $real = 'foo.com'; 162 | $_SERVER['HTTP_HOST'] = $real; 163 | $_SERVER['HTTP_X_FORWARDED_HOST'] = 'evil.com'; 164 | $gen = new DummyUrlGenerator(); 165 | $this->assertEquals($real, $gen->GetHttpHost()); 166 | } 167 | 168 | public function testHttpProtocolApache() 169 | { 170 | $_SERVER['HTTPS'] = 'on'; 171 | $gen = new DummyUrlGenerator(); 172 | $this->assertEquals('https', $gen->GetHttpProtocol()); 173 | } 174 | 175 | public function testHttpProtocolNginx() 176 | { 177 | $_SERVER['SERVER_PORT'] = '443'; 178 | $gen = new DummyUrlGenerator(); 179 | $this->assertEquals('https', $gen->GetHttpProtocol()); 180 | } 181 | 182 | public function testHttpHostForwarded() 183 | { 184 | $real = 'foo.com'; 185 | $_SERVER['HTTP_HOST'] = 'localhost'; 186 | $_SERVER['HTTP_X_FORWARDED_HOST'] = $real; 187 | $gen = new DummyUrlGenerator(); 188 | $gen->setTrustForwarded(true); 189 | $this->assertEquals($real, $gen->GetHttpHost()); 190 | } 191 | 192 | public function testHttpProtocolForwarded() 193 | { 194 | $_SERVER['HTTPS'] = 'on'; 195 | $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'http'; 196 | $gen = new DummyUrlGenerator(); 197 | $gen->setTrustForwarded(true); 198 | $this->assertEquals('http', $gen->GetHttpProtocol()); 199 | } 200 | 201 | public function testHttpProtocolForwardedSecure() 202 | { 203 | $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'https'; 204 | $gen = new DummyUrlGenerator(); 205 | $this->assertEquals('http', $gen->GetHttpProtocol()); 206 | 207 | $gen->setTrustForwarded(true); 208 | $this->assertEquals('https', $gen->GetHttpProtocol()); 209 | } 210 | 211 | protected function tearDown() 212 | { 213 | unset($_SERVER['HTTPS']); 214 | unset($_SERVER['HTTP_X_FORWARDED_PROTO']); 215 | $_SERVER['HTTP_HOST'] = 'localhost'; 216 | unset($_SERVER['HTTP_X_FORWARDED_HOST']); 217 | $_SERVER['SERVER_PORT'] = '80'; 218 | $_SERVER['REQUEST_URI'] = ''; 219 | } 220 | } 221 | 222 | class DummyUrlGenerator extends UrlGenerator 223 | { 224 | public function getHttpHost() 225 | { 226 | return parent::getHttpHost(); 227 | } 228 | 229 | public function getHttpProtocol() 230 | { 231 | return parent::getHttpProtocol(); 232 | } 233 | 234 | public function dropLinkedInParams($query) 235 | { 236 | return parent::dropLinkedInParams($query); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /tests/LinkedInTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class LinkedInTest extends \PHPUnit_Framework_TestCase 11 | { 12 | const APP_ID = '123456789'; 13 | const APP_SECRET = '987654321'; 14 | 15 | public function testApi() 16 | { 17 | $resource = 'resource'; 18 | $token = 'token'; 19 | $urlParams = ['url' => 'foo']; 20 | $postParams = ['post' => 'bar']; 21 | $method = 'GET'; 22 | $expected = ['foobar' => 'test']; 23 | $response = new Response(200, [], json_encode($expected)); 24 | $url = 'http://example.com/test'; 25 | 26 | $headers = ['Authorization' => 'Bearer '.$token, 'Content-Type' => 'application/json', 'x-li-format' => 'json']; 27 | 28 | $generator = $this->getMock('Happyr\LinkedIn\Http\UrlGenerator', ['getUrl']); 29 | $generator->expects($this->once())->method('getUrl')->with( 30 | $this->equalTo('api'), 31 | $this->equalTo($resource), 32 | $this->equalTo([ 33 | 'url' => 'foo', 34 | 'format' => 'json', 35 | ])) 36 | ->willReturn($url); 37 | 38 | $requestManager = $this->getMock('Happyr\LinkedIn\Http\RequestManager', ['sendRequest']); 39 | $requestManager->expects($this->once())->method('sendRequest')->with( 40 | $this->equalTo($method), 41 | $this->equalTo($url), 42 | $this->equalTo($headers), 43 | $this->equalTo(json_encode($postParams))) 44 | ->willReturn($response); 45 | 46 | $linkedIn = $this->getMock('Happyr\LinkedIn\LinkedIn', ['getAccessToken', 'getUrlGenerator', 'getRequestManager'], [self::APP_ID, self::APP_SECRET]); 47 | 48 | $linkedIn->expects($this->once())->method('getAccessToken')->willReturn($token); 49 | $linkedIn->expects($this->once())->method('getUrlGenerator')->willReturn($generator); 50 | $linkedIn->expects($this->once())->method('getRequestManager')->willReturn($requestManager); 51 | 52 | $result = $linkedIn->api($method, $resource, ['query' => $urlParams, 'json' => $postParams]); 53 | $this->assertEquals($expected, $result); 54 | } 55 | 56 | public function testIsAuthenticated() 57 | { 58 | $linkedIn = $this->getMock('Happyr\LinkedIn\LinkedIn', ['getAccessToken'], [self::APP_ID, self::APP_SECRET]); 59 | $linkedIn->expects($this->once())->method('getAccessToken')->willReturn(null); 60 | $this->assertFalse($linkedIn->isAuthenticated()); 61 | 62 | $linkedIn = $this->getMock('Happyr\LinkedIn\LinkedIn', ['api', 'getAccessToken'], [self::APP_ID, self::APP_SECRET]); 63 | $linkedIn->expects($this->once())->method('getAccessToken')->willReturn('token'); 64 | $linkedIn->expects($this->once())->method('api')->willReturn(['id' => 4711]); 65 | $this->assertTrue($linkedIn->isAuthenticated()); 66 | 67 | $linkedIn = $this->getMock('Happyr\LinkedIn\LinkedIn', ['api', 'getAccessToken'], [self::APP_ID, self::APP_SECRET]); 68 | $linkedIn->expects($this->once())->method('getAccessToken')->willReturn('token'); 69 | $linkedIn->expects($this->once())->method('api')->willReturn(['foobar' => 4711]); 70 | $this->assertFalse($linkedIn->isAuthenticated()); 71 | } 72 | 73 | /** 74 | * Test a call to getAccessToken when there is no token. 75 | */ 76 | public function testAccessTokenAccessors() 77 | { 78 | $token = 'token'; 79 | 80 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['fetchNewAccessToken'], [], '', false); 81 | $auth->expects($this->once())->method('fetchNewAccessToken')->will($this->returnValue($token)); 82 | 83 | $linkedIn = $this->getMock('Happyr\LinkedIn\LinkedIn', ['getAuthenticator'], [], '', false); 84 | $linkedIn->expects($this->once())->method('getAuthenticator')->willReturn($auth); 85 | 86 | // Make sure we go to the authenticator only once 87 | $this->assertEquals($token, $linkedIn->getAccessToken()); 88 | $this->assertEquals($token, $linkedIn->getAccessToken()); 89 | } 90 | 91 | public function testGeneratorAccessors() 92 | { 93 | $get = new \ReflectionMethod('Happyr\LinkedIn\LinkedIn', 'getUrlGenerator'); 94 | $get->setAccessible(true); 95 | $linkedIn = new LinkedIn(self::APP_ID, self::APP_SECRET); 96 | 97 | // test default 98 | $this->assertInstanceOf('Happyr\LinkedIn\Http\UrlGenerator', $get->invoke($linkedIn)); 99 | 100 | $object = $this->getMock('Happyr\LinkedIn\Http\UrlGenerator'); 101 | $linkedIn->setUrlGenerator($object); 102 | $this->assertEquals($object, $get->invoke($linkedIn)); 103 | } 104 | 105 | public function testHasError() 106 | { 107 | $linkedIn = new LinkedIn(self::APP_ID, self::APP_SECRET); 108 | 109 | unset($_GET['error']); 110 | $this->assertFalse($linkedIn->hasError()); 111 | 112 | $_GET['error'] = 'foobar'; 113 | $this->assertTrue($linkedIn->hasError()); 114 | } 115 | 116 | public function testGetError() 117 | { 118 | $linkedIn = new LinkedIn(self::APP_ID, self::APP_SECRET); 119 | 120 | unset($_GET['error']); 121 | unset($_GET['error_description']); 122 | 123 | $this->assertNull($linkedIn->getError()); 124 | 125 | $_GET['error'] = 'foo'; 126 | $_GET['error_description'] = 'bar'; 127 | 128 | $this->assertEquals('foo', $linkedIn->getError()->getName()); 129 | $this->assertEquals('bar', $linkedIn->getError()->getDescription()); 130 | } 131 | 132 | public function testGetErrorWithMissingDescription() 133 | { 134 | $linkedIn = new LinkedIn(self::APP_ID, self::APP_SECRET); 135 | 136 | unset($_GET['error']); 137 | unset($_GET['error_description']); 138 | 139 | $_GET['error'] = 'foo'; 140 | 141 | $this->assertEquals('foo', $linkedIn->getError()->getName()); 142 | $this->assertNull($linkedIn->getError()->getDescription()); 143 | } 144 | 145 | public function testFormatAccessors() 146 | { 147 | $get = new \ReflectionMethod('Happyr\LinkedIn\LinkedIn', 'getFormat'); 148 | $get->setAccessible(true); 149 | $linkedIn = new LinkedIn(self::APP_ID, self::APP_SECRET); 150 | 151 | //test default 152 | $this->assertEquals('json', $get->invoke($linkedIn)); 153 | 154 | $format = 'foo'; 155 | $linkedIn->setFormat($format); 156 | $this->assertEquals($format, $get->invoke($linkedIn)); 157 | } 158 | 159 | public function testLoginUrl() 160 | { 161 | $currentUrl = 'currentUrl'; 162 | $loginUrl = 'result'; 163 | 164 | $generator = $this->getMock('Happyr\LinkedIn\Http\UrlGenerator', ['getCurrentUrl']); 165 | $generator->expects($this->once())->method('getCurrentUrl')->willReturn($currentUrl); 166 | 167 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getLoginUrl'], [], '', false); 168 | $auth->expects($this->once())->method('getLoginUrl') 169 | ->with($generator, ['redirect_uri' => $currentUrl]) 170 | ->will($this->returnValue($loginUrl)); 171 | 172 | $linkedIn = $this->getMock('Happyr\LinkedIn\LinkedIn', ['getAuthenticator', 'getUrlGenerator'], [], '', false); 173 | $linkedIn->expects($this->once())->method('getAuthenticator')->willReturn($auth); 174 | $linkedIn->expects($this->once())->method('getUrlGenerator')->willReturn($generator); 175 | 176 | $linkedIn->getLoginUrl(); 177 | } 178 | 179 | public function testLoginUrlWithParameter() 180 | { 181 | $loginUrl = 'result'; 182 | $otherUrl = 'otherUrl'; 183 | 184 | $generator = $this->getMock('Happyr\LinkedIn\Http\UrlGenerator'); 185 | 186 | $auth = $this->getMock('Happyr\LinkedIn\Authenticator', ['getLoginUrl'], [], '', false); 187 | $auth->expects($this->once())->method('getLoginUrl') 188 | ->with($generator, ['redirect_uri' => $otherUrl]) 189 | ->will($this->returnValue($loginUrl)); 190 | 191 | $linkedIn = $this->getMock('Happyr\LinkedIn\LinkedIn', ['getAuthenticator', 'getUrlGenerator'], [], '', false); 192 | $linkedIn->expects($this->once())->method('getAuthenticator')->willReturn($auth); 193 | $linkedIn->expects($this->once())->method('getUrlGenerator')->willReturn($generator); 194 | 195 | $linkedIn->getLoginUrl(['redirect_uri' => $otherUrl]); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/Storage/IlluminateSessionStorageTest.php: -------------------------------------------------------------------------------- 1 | storage = new IlluminateSessionStorage(); 24 | } 25 | 26 | public function testSet() 27 | { 28 | Session::shouldReceive('put')->once()->with($this->prefix.'code', 'foobar'); 29 | 30 | $this->storage->set('code', 'foobar'); 31 | } 32 | 33 | /** 34 | * @expectedException \Happyr\LinkedIn\Exception\InvalidArgumentException 35 | */ 36 | public function testSetFail() 37 | { 38 | $this->storage->set('foobar', 'baz'); 39 | } 40 | 41 | public function testGet() 42 | { 43 | $expected = 'foobar'; 44 | Session::shouldReceive('get')->once()->with($this->prefix.'code')->andReturn($expected); 45 | $result = $this->storage->get('code'); 46 | $this->assertEquals($expected, $result); 47 | 48 | Session::shouldReceive('get')->once()->with($this->prefix.'state')->andReturn(null); 49 | $result = $this->storage->get('state'); 50 | $this->assertNull($result); 51 | } 52 | 53 | public function testClear() 54 | { 55 | Session::shouldReceive('forget')->once()->with($this->prefix.'code')->andReturn(true); 56 | $this->storage->clear('code'); 57 | } 58 | 59 | /** 60 | * @expectedException \Happyr\LinkedIn\Exception\InvalidArgumentException 61 | */ 62 | public function testClearFail() 63 | { 64 | $this->storage->clear('foobar'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Storage/SessionStorageTest.php: -------------------------------------------------------------------------------- 1 | storage = new SessionStorage(); 24 | } 25 | 26 | public function testSet() 27 | { 28 | $this->storage->set('code', 'foobar'); 29 | $this->assertEquals($_SESSION[$this->prefix.'code'], 'foobar'); 30 | } 31 | 32 | /** 33 | * @expectedException \Happyr\LinkedIn\Exception\InvalidArgumentException 34 | */ 35 | public function testSetFail() 36 | { 37 | $this->storage->set('foobar', 'baz'); 38 | } 39 | 40 | public function testGet() 41 | { 42 | unset($_SESSION[$this->prefix.'state']); 43 | $result = $this->storage->get('state'); 44 | $this->assertNull($result); 45 | 46 | $expected = 'foobar'; 47 | $_SESSION[$this->prefix.'code'] = $expected; 48 | $result = $this->storage->get('code'); 49 | $this->assertEquals($expected, $result); 50 | } 51 | 52 | public function testClear() 53 | { 54 | $_SESSION[$this->prefix.'code'] = 'foobar'; 55 | $this->storage->clear('code'); 56 | $this->assertFalse(isset($_SESSION[$this->prefix.'code'])); 57 | } 58 | 59 | /** 60 | * @expectedException \Happyr\LinkedIn\Exception\InvalidArgumentException 61 | */ 62 | public function testClearFail() 63 | { 64 | $this->storage->clear('foobar'); 65 | } 66 | 67 | public function testClearAll() 68 | { 69 | $validKeys = SessionStorage::$validKeys; 70 | 71 | $storage = m::mock('Happyr\LinkedIn\Storage\SessionStorage[clear]') 72 | ->shouldReceive('clear')->times(count($validKeys)) 73 | ->with(m::on(function ($arg) use ($validKeys) { 74 | return in_array($arg, $validKeys); 75 | })) 76 | ->getMock(); 77 | 78 | $storage->clearAll(); 79 | } 80 | } 81 | --------------------------------------------------------------------------------