diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..898df38eb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +.gitattributes export-ignore +.gitignore export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore +CHANGELOG.md export-ignore +CONTRIBUTING.md export-ignore +docs export-ignore +phpunit.xml.dist export-ignore +README.md export-ignore +tests export-ignore diff --git a/.gitignore b/.gitignore index c4c77e40f..56b4feb4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ vendor/ composer.lock -composer.phar -.DS_Store -.idea/ tests/FacebookTestCredentials.php - diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 000000000..05bdf778c --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,9 @@ +filter: + paths: + - 'src/*' + - 'tests/*' + +tools: + php_code_sniffer: + config: + standard: PSR2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..a5d059210 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - 7.3 + +sudo: false + +cache: + directories: + - $HOME/.composer/cache + +matrix: + include: + - php: hhvm + dist: trusty + +before_install: + - travis_retry composer self-update + +install: + - travis_retry composer require --dev --no-update squizlabs/php_codesniffer + - travis_retry composer install --prefer-dist --no-interaction + +script: + - vendor/bin/phpcs + - vendor/bin/phpunit --coverage-text --exclude-group integration diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..c0f80d0b9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,202 @@ +# CHANGELOG + +Starting with version 5, the Facebook PHP SDK follows [SemVer](http://semver.org/). + +## 5.x + +Version 5 of the Facebook PHP SDK is a complete refactor of version 4. It comes loaded with lots of new features and a friendlier API. + +- 5.7.1 (2018-XX-XX) +- 5.7.0 (2018-12-12) + - Add `joined` to list of fields to be cast to `\DateTime` (#950) + - Add `GraphPage::getFanCount()` to get the number of people who like the page (#815) + - Fixed HTTP/2 support (#1079) + - Fixed resumable upload error (#1001) + - Strip 'enforce_https' param (#1084) + - Conserve id when next to data key, resolves #700 (#1034) +- 5.6.3 (2018-07-01) + - Add fix for countable error in PHP 7.2 (originally #969 by @andreybolonin) +- 5.6.2 (2018-02-15) + - Strip 'code' param (#913) +- 5.6.1 (2017-08-16) + - Fixed doc block syntax that interfered with Doctrine (#844) +- 5.6.0 (2017-07-23) + - Bump Graph API version to v2.10 (#829) +- 5.5.0 (2017-04-20) + - Added support for batch options (#713) + - Bump Graph API version to v2.9. +- 5.4.4 (2017-01-19) + - Added the `application/octet-stream` MIME type for SRT files (#734) +- 5.4.3 (2016-12-30) + - Fixed a bug that would throw a type error in `GraphEdge` in some cases (#715) +- 5.4.2 (2016-11-15) + - Added check for [PHP 7 CSPRNG](http://php.net/manual/en/function.random-bytes.php) first to keep mcrypt deprecation messages from appearing in PHP 7.1 (#692) +- 5.4.1 (2016-10-18) + - Fixed a bug that was not properly parsing response headers when they contained the colon `:` character. (#679) +- 5.4.0 (2016-10-12) + - Bump Graph API version to v2.8. + - Auto-cast `cover` field to `GraphCoverPhoto` and `picture` field to `GraphPicture` in `GraphPage`. (#655) + - Added `getCover()` and `getPicture()` to `GraphPage`. (#655) +- 5.3.1 + - Fixed a bug where the `polyfills.php` file wasn't being included properly when using the built-in auto loader (#633) +- 5.3.0 + - Bump Graph API version to v2.7. +- 5.2.1 + - Fix notice that is raised in `FacebookUrlDetectionHandler` (#626) + - Fix bug in `FacebookRedirectLoginHelper::getLoginUrl()` where the CSRF token gets overwritten in certain scenarios (#613) + - Fix bug with polyfills not getting loaded when installing the Facebook PHP SDK manually (#599) +- 5.2.0 + - Added new Birthday class to handle Graph API response variations + - Bumped Graph version to v2.6 + - Added better error checking for app IDs that are cast as int when they are greater than PHP_INT_MAX +- 5.1.5 + - Removed mbstring extension dependency + - Updated required PHP version syntax in composer.json +- 5.1.4 + - Breaking changes + - Changes the serialization method of FacebookApp + - FacebookApps serialized by versions prior 5.1.4 cannot be unserialized by this version + - Fixed redirect_uri injection vulnerability +- 5.0 (2015-07-09) + - New features + - Added the `Facebook\Facebook` super service for an easier API + - Improved "reauthentication" and "rerequest" support + - Requests/Responses + - Added full batch support + - Added full file upload support for videos & photos + - Added methods to make pagination easier + - Added "deep" pagination support so that Graph edges embedded in a Graph node can be paginated over easily + - Beta support at `graph.beta.facebook.com` + - Added `getMetaData()` to `GraphEdge` to obtain all the metadata associated with a list of Graph nodes + - Full nested param support + - Many improvements to the Graph node subtypes + - New injectable interfaces + - Added a `PersistentDataInterface` for custom persistent data handling + - Added a `PseudoRandomStringGeneratorInterface` for customizable CSPRNG's + - Added a `UrlDetectionInterface` for custom URL-detection logic + - Codebase changes + - Moved exception classes to `Exception\*` directory + - Moved response collection objects to `GraphNodes\*` directory + - Moved helpers to `Helpers\*` directory + - Killed `FacebookSession` in favor of the `AccessToken` entity + - Added `FacebookClient` service + - Renamed `FacebookRequestException` to `FacebookResponseException` + - Renamed `FacebookHttpable` to `FacebookHttpClientInterface` + - Added `FacebookApp` entity that contains info about the Facebook app + - Updated the API for the helpers + - Added `HttpClients`, `PersistentData` and `PseudoRandomString` factories to reduce main class' complexity + - Tests + - Added namespaces to the tests + - Grouped functional tests under `functional` group + - Other changes + - Made PSR-2 compliant + - Adopted SemVer + - Completely refactored request/response handling + - Refactored the OAuth 2.0 logic + - Added `ext-mbstring` to composer require + - Added this CHANGELOG. Hi! :) + +## 4.1-dev + +Since the Facebook PHP SDK didn't follow SemVer in version 4.x, the master branch was going to be released as 4.1. However, the SDK switched to SemVer in v5.0. So any references on the internet to version 4.1 can be assumed to be an alias to version `5.0.0` + +## 4.0.x + +Version 4.0 of the Facebook PHP SDK did not follow [SemVer](http://semver.org/). The versioning format used was as follows: `4.MAJOR.(MINOR|PATCH)`. The `MINOR` and `PATCH` versions were squashed together. + +- 4.0.23 (2015-04-03) + - Added support for new JSON response types in Graph v2.3 when requesting access tokens +- 4.0.22 (2015-04-02) + - Fixed issues related to multidimensional params + - **Bumped default fallback Graph version to `v2.3`** +- 4.0.21 (2015-03-31) + - Added a `FacebookPermissions` class to reference all the Facebook permissions +- 4.0.20 (2015-03-02) + - Fixed a bug introduced in `4.0.19` related to CSRF comparisons +- 4.0.19 (2015-03-02) + - Added stricter CSRF comparison checks to `SignedRequest` and `FacebookRedirectLoginHelper` +- 4.0.18 (2015-02-24) + - [`FacebookHttpable`] Reverted a breaking change from `4.0.17` that changed the method signatures +- 4.0.17 (2015-02-19) + - [`FacebookRedirectLoginHelper`] Added multiple auth types to `getLoginUrl()` + - [`GraphUser`] Added `getTimezone()` + - [`FacebookCurl`] Additional fix for `curl_init()` handling + - Added support for https://graph-video.facebook.com when path ends with `/videos` +- 4.0.16 (2015-02-03) + - [`FacebookRedirectLoginHelper`] Added "reauthenticate" functionality to `getLoginUrl()` + - [`FacebookCurl`] Fixed `curl_init()` issue +- 4.0.15 (2015-01-06) + - [`FacebookRedirectLoginHelper`] Added guard against accidental exposure of app secret via the logout link +- 4.0.14 (2014-12-29) + - [`GraphUser`] Added `getGender()` + - [`FacebookRedirectLoginHelper`] Added CSRF protection for rerequest links + - [`GraphAlbum`] Fixed bugs in getter methods +- 4.0.13 (2014-12-12) + - [`FacebookRedirectLoginHelper`] Added `$displayAsPopup` param to `getLoginUrl()` + - [`FacebookResponse`] Fixed minor pagination bug + - Removed massive cert bundle and replaced with `DigiCertHighAssuranceEVRootCA` for peer verification +- 4.0.12 (2014-10-30) + - **Updated default fallback Graph version to `v2.2`** + - Fixed potential duplicate `type` param in URL's + - [`FacebookRedirectLoginHelper`] Added `getReRequestUrl()` + - [`GraphUser`] Added `getEmail()` +- 4.0.11 (2014-08-25) + - [`FacebookCurlHttpClient`] Added a method to disable IPv6 resolution +- 4.0.10 (2014-08-12) + - [`GraphObject`] Fixed improper usage of `stdClass` + - Fixed warnings when `open_basedir` directive set + - Fixed long lived sessions forgetting the signed request + - [`CanvasLoginHelper`] Removed GET processing + - Updated visibility on `FacebookSession::useAppSecretProof` +- 4.0.9 (2014-06-27) + - [`FacebookPageTabHelper`] Added ability to fetch `app_data` + - Added `GraphUserPage` Graph node collection + - Cleaned up test files + - Decoupled signed request handling + - Added some stronger type hinting + - Explicitly added separator in `http_build_query()` + - [`FacebookCurlHttpClient`] Updated the calculation of the request body size + - Decoupled access token handling + - [`FacebookRedirectLoginHelper`] Implemented better CSPRNG + - Added autoloader for those poor non-composer peeps +- 4.0.8 (2014-06-10) + - Enabled `appsecret_proof` by default + - Added stream wrapper and Guzzle HTTP client implementations +- 4.0.7 (2014-05-31) + - Improved testing environment + - Added `FacebookPageTabHelper` + - [`FacebookSession`] Fixed issue where `validateSessionInfo()` would return incorrect results +- 4.0.6 (2014-05-24) + - Added feature to inject custom HTTP clients + - [`FacebookCanvasLoginHelper`] Fixed bug that would throw when logging out + - Removed appToken from test credentials file + - [`FacebookRequest`] Added `appsecret_proof` handling +- 4.0.5 (2014-05-19) + - Fixed bug in cURL where proxy headers are not included in header_size + - Added internal SDK error codes for thrown exceptions + - Added stream wrapper fallback for hosting environments without cURL + - Added getter methods for signed requests + - Fixed warning that showed up in tests + - Changed SDK error code for stream failure + - Added `GraphAlbum` Graph node collection +- 4.0.4 (2014-05-15) + - Added more error codes to accommodate more Graph error responses + - [`JavaScriptLoginHelper`] Fixed bug that would try to get a new access token when one already existed +- 4.0.3 (2014-05-14) + - Fixed bug for "Missing client_id parameter" error + - Fixed bug for eTag support when "Network is unreachable" error occurs + - Fixed pagination issue related to `sdtClass` +- 4.0.2 (2014-05-07) + - [`composer.json`] Upgraded to use PSR-4 autoloading instead of Composer's `classmap` + - [`FacebookCanvasLoginHelper`] Abstracted access to super globals + - [`FacebookRequest`] Fixed bug that blindly appended params to a url + - [`FacebookRequest`] Added support for `DELETE` and `PUT` methods + - Added eTag support to Graph requests +- 4.0.1 (2014-05-05) + - All exceptions are now extend from `FacebookSDKException` + - [`FacebookSession`] Signed request parsing will throw on malformed signed request input + - Excluded test credentials from tests + - [`FacebookRedirectLoginHelper`] Changed scope on `$state` property + - [`phpunit.xml`] Normalized +- 4.0.0 (2014-04-30) + - Initial release. Yay! diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..0a45f9bd5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e45caf42a..84b53c1f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,60 @@ Contributing ------------ -For us to accept contributions you will have to first have signed the -[Contributor License Agreement](https://developers.facebook.com/opensource/cla). +Contributions are **welcome** and will be fully **credited**. -When committing, keep all lines to less than 80 characters, and try to -follow the existing style. +We accept contributions via Pull Requests on [Github](https://github.com/facebook/php-graph-sdk/pull/new). -Before creating a pull request, squash your commits into a single commit. +The current stable major version is v5. The v6 is under active development. -Add the comments where needed, and provide ample explanation in the -commit message. +This means any new feature MUST target v6 (`master` branch). + +The v5 (`5.x` branch) is maintained only for bug fixes, node/edge updates or documentation improvements. + +## Code of Conduct +The code of conduct is described in [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) + +## Pull Requests + +- **Sign the CLA** - For us to accept contributions you will have to first have signed the + [Contributor License Agreement](https://developers.facebook.com/opensource/cla). + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to run [PHP Code Sniffer](#running-php-code-sniffer) as you code. + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the README and the [documentation](https://github.com/facebook/php-graph-sdk/tree/master/docs) are kept up-to-date. + +- **Consider our release cycle** - As of version 5.0.0, we try to follow [SemVer](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create topic branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. + +- **Ensure tests pass!** - Please [run the tests](#running-tests) before submitting your pull request, and make sure they pass. We won't accept a patch until all tests pass. + +- **Ensure no coding standards violations** - Please [run PHP Code Sniffer](#running-php-code-sniffer) using the PSR-2 standard before submitting your pull request. A violation will cause the build to fail, so please make sure there are no violations. We can't accept a patch if the build fails. + +## Running Tests + +``` bash +$ ./vendor/bin/phpunit +``` + +## Running PHP Code Sniffer + +You can install [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) globally with composer. + +``` bash +$ composer global require squizlabs/php_codesniffer +``` + +Then you can `cd` into the Facebook PHP SDK folder and run Code Sniffer against the `src/` directory. + +``` bash +$ ~/.composer/vendor/bin/phpcs +``` + +**Happy coding**! diff --git a/LICENSE b/LICENSE index be3927bf1..8b93109ab 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2014 Facebook, Inc. +Copyright 2017 Facebook, Inc. You are hereby granted a non-exclusive, worldwide, royalty-free license to use, copy, modify, and distribute this software in source code or binary diff --git a/README.md b/README.md index 2bb28bf1d..aa1bf390a 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,91 @@ -Facebook SDK for PHP -==================== +# Facebook SDK for PHP (v5) -[![Latest Stable Version](http://img.shields.io/packagist/v/facebook/php-sdk-v4.svg)](https://packagist.org/packages/facebook/php-sdk-v4) +[![Build Status](https://img.shields.io/travis/facebook/php-graph-sdk/5.x.svg)](https://travis-ci.org/facebook/php-graph-sdk) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/facebook/php-graph-sdk/badges/quality-score.png?b=5.x)](https://scrutinizer-ci.com/g/facebook/php-graph-sdk/?branch=5.x) +[![Latest Stable Version](http://img.shields.io/badge/Latest%20Stable-5.7.0-blue.svg)](https://packagist.org/packages/facebook/graph-sdk) +This repository contains the open source PHP SDK that allows you to access the Facebook Platform from your PHP app. -This repository contains the open source PHP SDK that allows you to access Facebook -Platform from your PHP app. +## Installation +The Facebook PHP SDK can be installed with [Composer](https://getcomposer.org/). Run this command: -Usage ------ +```sh +composer require facebook/graph-sdk +``` -This version of the Facebook SDK for PHP requires PHP 5.4 or greater. +Please be aware, that there are issues when using the Facebook SDK together with [Guzzle](https://github.com/guzzle/guzzle) 6.x. php-graph-sdk v5.x only works with Guzzle 5.x out of the box. However, [there is a workaround to make it work with Guzzle 6.x](https://www.sammyk.me/how-to-inject-your-own-http-client-in-the-facebook-php-sdk-v5#writing-a-guzzle-6-http-client-implementation-from-scratch). -Minimal example: +## Upgrading to v5.x -```php -use Facebook\FacebookSession; -use Facebook\FacebookRequest; -use Facebook\GraphUser; -use Facebook\FacebookRequestException; +Upgrading from v4.x? Facebook PHP SDK v5.x introduced breaking changes. Please [read the upgrade guide](https://www.sammyk.me/upgrading-the-facebook-php-sdk-from-v4-to-v5) before upgrading. + +## Usage + +> **Note:** This version of the Facebook SDK for PHP requires PHP 5.4 or greater. + +Simple GET example of a user's profile. -FacebookSession::setDefaultApplication('YOUR_APP_ID','YOUR_APP_SECRET'); +```php +require_once __DIR__ . '/vendor/autoload.php'; // change path as needed -// Use one of the helper classes to get a FacebookSession object. -// FacebookRedirectLoginHelper -// FacebookCanvasLoginHelper -// FacebookJavaScriptLoginHelper -// or create a FacebookSession with a valid access token: -$session = new FacebookSession('access-token-here'); +$fb = new \Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + //'default_access_token' => '{access-token}', // optional +]); -// Get the GraphUser object for the current user: +// Use one of the helper classes to get a Facebook\Authentication\AccessToken entity. +// $helper = $fb->getRedirectLoginHelper(); +// $helper = $fb->getJavaScriptHelper(); +// $helper = $fb->getCanvasHelper(); +// $helper = $fb->getPageTabHelper(); try { - $me = (new FacebookRequest( - $session, 'GET', '/me' - ))->execute()->getGraphObject(GraphUser::className()); - echo $me->getName(); -} catch (FacebookRequestException $e) { - // The Graph API returned an error -} catch (\Exception $e) { - // Some other error occurred + // Get the \Facebook\GraphNodes\GraphUser object for the current user. + // If you provided a 'default_access_token', the '{access-token}' is optional. + $response = $fb->get('/me', '{access-token}'); +} catch(\Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(\Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; } +$me = $response->getGraphUser(); +echo 'Logged in as ' . $me->getName(); ``` -Complete documentation, installation instructions, and examples are available at: -[https://developers.facebook.com/docs/php](https://developers.facebook.com/docs/php) - - -Tests ------ +Complete documentation, installation instructions, and examples are available [here](docs/). -1) [Composer](https://getcomposer.org/) is a prerequisite for running the tests. +## Tests -Install composer globally, then run `composer install` to install required files. +1. [Composer](https://getcomposer.org/) is a prerequisite for running the tests. Install composer globally, then run `composer install` to install required files. +2. Create a test app on [Facebook Developers](https://developers.facebook.com), then create `tests/FacebookTestCredentials.php` from `tests/FacebookTestCredentials.php.dist` and edit it to add your credentials. +3. The tests can be executed by running this command from the root directory: -2) Create a test app on [Facebook Developers](https://developers.facebook.com), then -create `tests/FacebookTestCredentials.php` from `tests/FacebookTestCredentials.php.dist` -and edit it to add your credentials. +```bash +$ ./vendor/bin/phpunit +``` -3) The tests can be executed by running this command from the root directory: +By default the tests will send live HTTP requests to the Graph API. If you are without an internet connection you can skip these tests by excluding the `integration` group. ```bash -./vendor/bin/phpunit +$ ./vendor/bin/phpunit --exclude-group integration ``` +## Contributing -Contributing ------------- +For us to accept contributions you will have to first have signed the [Contributor License Agreement](https://developers.facebook.com/opensource/cla). Please see [CONTRIBUTING](https://github.com/facebook/php-graph-sdk/blob/master/CONTRIBUTING.md) for details. -For us to accept contributions you will have to first have signed the -[Contributor License Agreement](https://developers.facebook.com/opensource/cla). +## License -When committing, keep all lines to less than 80 characters, and try to -follow the existing style. +Please see the [license file](https://github.com/facebook/php-graph-sdk/blob/master/LICENSE) for more information. -Before creating a pull request, squash your commits into a single commit. +## Security Vulnerabilities -Add the comments where needed, and provide ample explanation in the -commit message. +If you have found a security issue, please contact the maintainers directly at [me@sammyk.me](mailto:me@sammyk.me). diff --git a/composer.json b/composer.json index 70ba459a6..9d54abc37 100644 --- a/composer.json +++ b/composer.json @@ -1,30 +1,42 @@ { - "name": "facebook/php-sdk-v4", + "name": "facebook/graph-sdk", "description": "Facebook SDK for PHP", "keywords": ["facebook", "sdk"], "type": "library", - "homepage": "https://github.com/facebook/facebook-php-sdk-v4", + "homepage": "https://github.com/facebook/php-graph-sdk", "license": "Facebook Platform", "authors": [ { "name": "Facebook", - "homepage": "https://github.com/facebook/facebook-php-sdk-v4/contributors" + "homepage": "https://github.com/facebook/php-graph-sdk/contributors" } ], "require": { - "php": ">=5.4.0" + "php": "^5.4|^7.0" }, "require-dev": { - "phpunit/phpunit": "3.7.*", - "mockery/mockery": "dev-master", - "guzzlehttp/guzzle": "~4.0" + "phpunit/phpunit": "~4.0", + "mockery/mockery": "~0.8", + "guzzlehttp/guzzle": "~5.0" }, "suggest": { + "paragonie/random_compat": "Provides a better CSPRNG option in PHP 5", "guzzlehttp/guzzle": "Allows for implementation of the Guzzle HTTP client" }, "autoload": { "psr-4": { "Facebook\\": "src/Facebook/" + }, + "files": ["src/Facebook/polyfills.php"] + }, + "autoload-dev": { + "psr-4": { + "Facebook\\Tests\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" } } } diff --git a/dad4 b/dad4 new file mode 100644 index 000000000..e888a8e00 --- /dev/null +++ b/dad4 @@ -0,0 +1,14 @@ +try { + // Returns a `FacebookFacebookResponse` object + $response = $fb->get( + '/from{1242960046}to{100000534500991}/friendrequests', + '{EAAHWtzoclv0BACSGt78YaP5FLIe3VALFL0aDrOZCsEWyj3AWLMIqclKZBkiaxBlHwvwpxNlKCdzcpIE99ZCjK9fU7agYOlTmTR8QU4hfXwBh6m4O2259ZBleN0Gu79Nh787kjTNqs8M8qEGAz5HzH2AYz6VPHKlySeIBKQjksyXAlx4n41byTZAmY4gHZCvXatd7ZAAhTZACIHWL84TWDn3G}' + ); +} catch(FacebookExceptionsFacebookResponseException $e) { + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(FacebookExceptionsFacebookSDKException $e) { + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} +$graphNode = $response->getGraphNode(); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..229a2d763 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,34 @@ +# Facebook SDK for PHP + +The Facebook SDK for PHP is a library with powerful features that enable PHP developers to easily integrate Facebook login and make requests to the Graph API. It also plays well with the [Facebook SDK for JavaScript](https://developers.facebook.com/docs/javascript) to give the front-end user the best possible user experience. But it doesn't end there, the Facebook SDK for PHP makes it easy to upload photos and videos and send batch requests to the Graph API among other things. And SDK for PHP has many extensibility points giving PHP developers full control of how the SDK for PHP interacts with their specific hosting environment and web framework. + +Whether you're developing a website with Facebook login, creating a Facebook Canvas app or Page tab, the Facebook SDK for PHP does all the heavy lifting for you making it as easy as possible to deeply integrate into the Facebook platform. + +For installation & implementation instructions, look through the [Getting Started with the Facebook SDK for PHP](./getting_started.md) guide, and then check out some of the examples below. + +--- + +## Examples + +The following examples demonstrate how you would accomplish common tasks with the Facebook SDK for PHP. + +- **Authentication & Signed Requests** + - [Facebook Login (OAuth 2.0)](./examples/facebook_login.md) + - [Obtaining an access token from the SDK for JavaScript](./examples/access_token_from_javascript.md) + - [Obtaining an access token within a Facebook Canvas context](./examples/access_token_from_canvas.md) + - [Obtaining an access token within a Facebook Page tab context](./examples/access_token_from_page_tab.md) +- **User profile** + - [Retrieve a user's profile](./examples/retrieve_user_profile.md) + - [Post a link to a user's feed](./examples/post_links.md) +- **File Uploads** + - [Upload a photo to a user's profile](./examples/upload_photo.md) + - [Upload a video to a user's profile](./examples/upload_video.md) +- **Batch Requests** + - [Sending requests in a batch](./examples/batch_request.md) + - [Uploading files in a batch](./examples/batch_upload.md) +- **Pagination** + - [Basic pagination](./examples/pagination_basic.md) + +## API Reference + +For a full list of classes, see the API [reference page](./reference.md). diff --git a/docs/examples/access_token_from_canvas.md b/docs/examples/access_token_from_canvas.md new file mode 100644 index 000000000..447582f09 --- /dev/null +++ b/docs/examples/access_token_from_canvas.md @@ -0,0 +1,41 @@ +# Get Access Token From App Canvas Example + +This example covers obtaining an access token and signed request from within the context of an app canvas with the Facebook SDK for PHP. + +## Example + +A signed request will be sent to your app via the HTTP POST method within the context of app canvas. The PHP SDK provides a helper to easily obtain, validate & decode the signed request. If the proper OAuth data exists in the signed request payload data, an attempt can be made to obtain an access token from the Graph API. + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$helper = $fb->getCanvasHelper(); + +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (! isset($accessToken)) { + echo 'No OAuth data could be obtained from the signed request. User has not authorized your app yet.'; + exit; +} + +// Logged in +echo '

Signed Request

'; +var_dump($helper->getSignedRequest()); + +echo '

Access Token

'; +var_dump($accessToken->getValue()); +``` diff --git a/docs/examples/access_token_from_javascript.md b/docs/examples/access_token_from_javascript.md new file mode 100644 index 000000000..f67ab5a59 --- /dev/null +++ b/docs/examples/access_token_from_javascript.md @@ -0,0 +1,86 @@ +# Getting Access Token From The JavaScript SDK Example + +This example covers obtaining an access token and signed request from the Facebook JavaScript SDK with the Facebook SDK for PHP. + +## Example + +In order to have the JavaScript SDK set a cookie containing a signed request (which contains information about the logged in user), you must first initialize the JavaScript SDK with the `{cookie: true}` option. + +```html + + + +

Log In with the JavaScript SDK

+ + + + +``` + +After the user successfully logs in, redirect the user (or make an AJAX request) to a PHP script that obtains an access token from the signed request that exists in the cookie. + +```php +# /js-login.php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$helper = $fb->getJavaScriptHelper(); + +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (! isset($accessToken)) { + echo 'No cookie set or no OAuth data could be obtained from cookie.'; + exit; +} + +// Logged in +echo '

Access Token

'; +var_dump($accessToken->getValue()); + +$_SESSION['fb_access_token'] = (string) $accessToken; + +// User is logged in! +// You can redirect them to a members-only page. +//header('Location: https://example.com/members.php'); +``` diff --git a/docs/examples/access_token_from_page_tab.md b/docs/examples/access_token_from_page_tab.md new file mode 100644 index 000000000..26748337d --- /dev/null +++ b/docs/examples/access_token_from_page_tab.md @@ -0,0 +1,47 @@ +# Get Access Token From Page Tab Example + +This example covers obtaining an access token and signed request from within the context of a page tab with the Facebook SDK for PHP. + +## Example + +Page tabs behave much like the app canvas. The PHP SDK provides a helper for page tabs that delivers specific methods unique to page tabs. + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$helper = $fb->getPageTabHelper(); + +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (! isset($accessToken)) { + echo 'No OAuth data could be obtained from the signed request. User has not authorized your app yet.'; + exit; +} + +// Logged in +echo '

Page ID

'; +var_dump($helper->getPageId()); + +echo '

User is admin of page

'; +var_dump($helper->isAdmin()); + +echo '

Signed Request

'; +var_dump($helper->getSignedRequest()); + +echo '

Access Token

'; +var_dump($accessToken->getValue()); +``` diff --git a/docs/examples/batch_request.md b/docs/examples/batch_request.md new file mode 100644 index 000000000..64b296e8e --- /dev/null +++ b/docs/examples/batch_request.md @@ -0,0 +1,278 @@ +# Batch Request Example + +This example covers sending a batch request with the Facebook SDK for PHP. + +## Example + +The following example assumes we have the following permissions granted from the user: `user_likes`, `user_events`, `user_photos`, `publish_actions`. The example makes use of [JSONPath to reference specific batch operations](https://developers.facebook.com/docs/graph-api/making-multiple-requests/#operations). + +```php + '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', +]); + +// Since all the requests will be sent on behalf of the same user, +// we'll set the default fallback access token here. +$fb->setDefaultAccessToken('user-access-token'); + +/** + * Generate some requests and then send them in a batch request. + */ + +// Get the name of the logged in user +$requestUserName = $fb->request('GET', '/me?fields=id,name'); + +// Get user likes +$requestUserLikes = $fb->request('GET', '/me/likes?fields=id,name&limit=1'); + +// Get user events +$requestUserEvents = $fb->request('GET', '/me/events?fields=id,name&limit=2'); + +// Post a status update with reference to the user's name +$message = 'My name is {result=user-profile:$.name}.' . "\n\n"; +$message .= 'I like this page: {result=user-likes:$.data.0.name}.' . "\n\n"; +$message .= 'My next 2 events are {result=user-events:$.data.*.name}.'; +$statusUpdate = ['message' => $message]; +$requestPostToFeed = $fb->request('POST', '/me/feed', $statusUpdate); + +// Get user photos +$requestUserPhotos = $fb->request('GET', '/me/photos?fields=id,source,name&limit=2'); + +$batch = [ + 'user-profile' => $requestUserName, + 'user-likes' => $requestUserLikes, + 'user-events' => $requestUserEvents, + 'post-to-feed' => $requestPostToFeed, + 'user-photos' => $requestUserPhotos, + ]; + +echo '

Make a batch request

' . "\n\n"; + +try { + $responses = $fb->sendBatchRequest($batch); +} catch (Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch (Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +foreach ($responses as $key => $response) { + if ($response->isError()) { + $e = $response->getThrownException(); + echo '

Error! Facebook SDK Said: ' . $e->getMessage() . "\n\n"; + echo '

Graph Said: ' . "\n\n"; + var_dump($e->getResponse()); + } else { + echo "

(" . $key . ") HTTP status code: " . $response->getHttpStatusCode() . "
\n"; + echo "Response: " . $response->getBody() . "

\n\n"; + echo "
\n\n"; + } +} + +``` + +There five requests being made in this batch requests. + +- Get the user's full `name` and `id`. +- Get one thing the user likes (which is a [Page node](https://developers.facebook.com/docs/graph-api/reference/page)). +- Get two events the user has been invited to (which are [Event nodes](https://developers.facebook.com/docs/graph-api/reference/event)). +- Compose a message using the data obtained from the 3 requests above and post it on the user's timeline. +- Get two photos from the user. + +If the request was successful, the user should have a new status update similar to this: + +``` +My name is Foo User. + +I like this page: Facebook Developers. + +My next 2 events are House Warming Party,Some Foo Event. +``` + +It should also contain a response containing two photos from the user. + +> **Warning:** The response object should return a `null` response for any request that was pointed to with JSONPath as is [the behaviour of the batch functionality of the Graph API](https://developers.facebook.com/docs/graph-api/making-multiple-requests/#operations). If we want to receive the response anyway we have to set the `omit_response_on_success` option to `false`. [See the example below](#force-response-example). + +## Force Response Example + +The following example is a subset of the [first example](#example). We will only use the `user-events` and `post-to-feed` requests of the [first example](#example), but in this case we will force the server to return the response of the `user-events` request. + +```php + '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', +]); + +// Since all the requests will be sent on behalf of the same user, +// we'll set the default fallback access token here. +$fb->setDefaultAccessToken('user-access-token'); + +// Get user events +$requestUserEvents = $fb->request('GET', '/me/events?fields=id,name&limit=2'); + +// Post a status update with reference to the user's events +$message = 'My next 2 events are {result=user-events:$.data.*.name}.'; +$statusUpdate = ['message' => $message]; +$requestPostToFeed = $fb->request('POST', '/me/feed', $statusUpdate); + +// Create an empty batch request +$batch = $fb->newBatchRequest(); + +// Populate the batch request +// Set the 'omit_response_on_success' option to false to force the server return the response +$batch->add($requestUserEvents, [ + "name" => "user-events", + "omit_response_on_success" => false +]); +$batch->add($requestPostToFeed, "post-to-feed"); + +// Send the batch request +try { + $responses = $fb->getClient()->sendBatchRequest($batch); +} catch (Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch (Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +foreach ($responses as $key => $response) { + if ($response->isError()) { + $e = $response->getThrownException(); + echo '

Error! Facebook SDK Said: ' . $e->getMessage() . "\n\n"; + echo '

Graph Said: ' . "\n\n"; + var_dump($e->getResponse()); + } else { + echo "

(" . $key . ") HTTP status code: " . $response->getHttpStatusCode() . "
\n"; + echo "Response: " . $response->getBody() . "

\n\n"; + echo "
\n\n"; + } +} + +``` + +## Explicit Dependency Example + +In the following example we will make two requests. +* One to post a status update on the user's feed +* and one to receive the last post of the user (which should be the one that we posted with first request). + +Since we want the second request to be executed after the first one is completed, we have to set the `depends_on` option of the second request to point to the name of the first request. We assume that we have the following options granted from the user: `user_posts`, `publish_actions`. + +```php + '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', +]); + +// Since all the requests will be sent on behalf of the same user, +// we'll set the default fallback access token here. +$fb->setDefaultAccessToken('user-access-token'); + +// Post a status update to the user's feed +$message = 'Random status update'; +$statusUpdate = ['message' => $message]; +$requestPostToFeed = $fb->request('POST', '/me/feed', $statusUpdate); + +// Get last post of the user +$requestLastPost = $fb->request('GET', '/me/feed?limit=1'); + +// Create an empty batch request +$batch = $fb->newBatchRequest(); + +// Populate the batch request +$batch->add($requestPostToFeed, "post-to-feed"); + +// Set the 'depends_on' property to point to the first request +$batch->add($requestLastPost, [ + "name" => "last-post", + "depends_on" => "post-to-feed" +]); + +// Send the batch request +try { + $responses = $fb->getClient()->sendBatchRequest($batch); +} catch (Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch (Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +foreach ($responses as $key => $response) { + if ($response->isError()) { + $e = $response->getThrownException(); + echo '

Error! Facebook SDK Said: ' . $e->getMessage() . "\n\n"; + echo '

Graph Said: ' . "\n\n"; + var_dump($e->getResponse()); + } else { + echo "

(" . $key . ") HTTP status code: " . $response->getHttpStatusCode() . "
\n"; + echo "Response: " . $response->getBody() . "

\n\n"; + echo "
\n\n"; + } +} +``` + +> **Warning:** The response object should return a `null` response for any request that was pointed to with the `depends_on` option as is [the behaviour of the batch functionality of the Graph API](https://developers.facebook.com/docs/graph-api/making-multiple-requests/#operations). If we want to receive the response anyway we have to set the `omit_response_on_success` option to `false`. [See example](#force-response-example). + +## Multiple User Example + +Since the requests sent in a batch are unrelated by default, we can make requests on behalf of multiple users and pages in the same batch request. + +```php + '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', +]); + +$batch = [ + $fb->request('GET', '/me?fields=id,name', 'user-access-token-one'), + $fb->request('GET', '/me?fields=id,name', 'user-access-token-two'), + $fb->request('GET', '/me?fields=id,name', 'page-access-token-one'), + $fb->request('GET', '/me?fields=id,name', 'page-access-token-two'), +]; + +try { + $responses = $fb->sendBatchRequest($batch); +} catch (Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch (Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +foreach ($responses as $key => $response) { + if ($response->isError()) { + $e = $response->getThrownException(); + echo '

Error! Facebook SDK Said: ' . $e->getMessage() . "\n\n"; + echo '

Graph Said: ' . "\n\n"; + var_dump($e->getResponse()); + } else { + echo "

(" . $key . ") HTTP status code: " . $response->getHttpStatusCode() . "
\n"; + echo "Response: " . $response->getBody() . "

\n\n"; + echo "
\n\n"; + } +} +``` diff --git a/docs/examples/batch_upload.md b/docs/examples/batch_upload.md new file mode 100644 index 000000000..25dc87467 --- /dev/null +++ b/docs/examples/batch_upload.md @@ -0,0 +1,62 @@ +# Batch File Upload Example + +This example covers uploading files in a batch request with the Facebook SDK for PHP. + +## Example + +The Graph API supports [file uploads in batch requests](https://developers.facebook.com/docs/graph-api/making-multiple-requests#binary) and the Facebook PHP SDK does all the heavy lifting to make it super easy to upload photos and videos in a batch request. + +The following example will upload two photos and one video. + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +// Since all the requests will be sent on behalf of the same user, +// we'll set the default fallback access token here. +$fb->setDefaultAccessToken('user-access-token'); + +$batch = [ + 'photo-one' => $fb->request('POST', '/me/photos', [ + 'message' => 'Foo photo', + 'source' => $fb->fileToUpload('/path/to/photo-one.jpg'), + ]), + 'photo-two' => $fb->request('POST', '/me/photos', [ + 'message' => 'Bar photo', + 'source' => $fb->fileToUpload('/path/to/photo-two.jpg'), + ]), + 'video-one' => $fb->request('POST', '/me/videos', [ + 'title' => 'Baz video', + 'description' => 'My neat baz video', + 'source' => $fb->videoToUpload('/path/to/video-one.mp4'), + ]), +]; + +try { + $responses = $fb->sendBatchRequest($batch); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +foreach ($responses as $key => $response) { + if ($response->isError()) { + $e = $response->getThrownException(); + echo '

Error! Facebook SDK Said: ' . $e->getMessage() . "\n\n"; + echo '

Graph Said: ' . "\n\n"; + var_dump($e->getResponse()); + } else { + echo "

(" . $key . ") HTTP status code: " . $response->getHttpStatusCode() . "
\n"; + echo "Response: " . $response->getBody() . "

\n\n"; + echo "
\n\n"; + } +} +``` diff --git a/docs/examples/facebook_login.md b/docs/examples/facebook_login.md new file mode 100644 index 000000000..d1a6a3403 --- /dev/null +++ b/docs/examples/facebook_login.md @@ -0,0 +1,101 @@ +# Facebook Login Example + +This example covers Facebook Login with the Facebook SDK for PHP. + +## Example + +Although it's common to see examples of Facebook Login being implemented in one PHP script, is best to use two separate PHP scripts for more separation and more control over the responses. + +In this example, the PHP script that generates the login link is called `/login.php`. The callback URL that Facebook redirects the user to after login dialog is called `/fb-callback.php`. + +## /login.php + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$helper = $fb->getRedirectLoginHelper(); + +$permissions = ['email']; // Optional permissions +$loginUrl = $helper->getLoginUrl('https://example.com/fb-callback.php', $permissions); + +echo 'Log in with Facebook!'; +``` + +## /fb-callback.php + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$helper = $fb->getRedirectLoginHelper(); + +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (! isset($accessToken)) { + if ($helper->getError()) { + header('HTTP/1.0 401 Unauthorized'); + echo "Error: " . $helper->getError() . "\n"; + echo "Error Code: " . $helper->getErrorCode() . "\n"; + echo "Error Reason: " . $helper->getErrorReason() . "\n"; + echo "Error Description: " . $helper->getErrorDescription() . "\n"; + } else { + header('HTTP/1.0 400 Bad Request'); + echo 'Bad request'; + } + exit; +} + +// Logged in +echo '

Access Token

'; +var_dump($accessToken->getValue()); + +// The OAuth 2.0 client handler helps us manage access tokens +$oAuth2Client = $fb->getOAuth2Client(); + +// Get the access token metadata from /debug_token +$tokenMetadata = $oAuth2Client->debugToken($accessToken); +echo '

Metadata

'; +var_dump($tokenMetadata); + +// Validation (these will throw FacebookSDKException's when they fail) +$tokenMetadata->validateAppId($config['app_id']); +// If you know the user ID this access token belongs to, you can validate it here +//$tokenMetadata->validateUserId('123'); +$tokenMetadata->validateExpiration(); + +if (! $accessToken->isLongLived()) { + // Exchanges a short-lived access token for a long-lived one + try { + $accessToken = $oAuth2Client->getLongLivedAccessToken($accessToken); + } catch (Facebook\Exceptions\FacebookSDKException $e) { + echo "

Error getting long-lived access token: " . $e->getMessage() . "

\n\n"; + exit; + } + + echo '

Long-lived

'; + var_dump($accessToken->getValue()); +} + +$_SESSION['fb_access_token'] = (string) $accessToken; + +// User is logged in with a long-lived access token. +// You can redirect them to a members-only page. +//header('Location: https://example.com/members.php'); +``` diff --git a/docs/examples/pagination_basic.md b/docs/examples/pagination_basic.md new file mode 100644 index 000000000..635798f3c --- /dev/null +++ b/docs/examples/pagination_basic.md @@ -0,0 +1,44 @@ +# Pagination Example + +This example covers basic cursor pagination with the Facebook SDK for PHP. + +## Example + +The Graph API supports [several methods to paginate over response data](https://developers.facebook.com/docs/graph-api/using-graph-api/#paging). The PHP SDK supports cursor-based pagination out of the box. It does all the heavy lifting of managing page cursors for you. + +In this example we'll pull five entries from a user's feed (assuming the user approved the `read_stream` permission for your app). Then we'll use the `next()` method to grab the next page of results. Naturally you'd provide some sort of pagination navigation in your app, but this is just an example to get you started. + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +try { + // Requires the "read_stream" permission + $response = $fb->get('/me/feed?fields=id,message&limit=5'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +// Page 1 +$feedEdge = $response->getGraphEdge(); + +foreach ($feedEdge as $status) { + var_dump($status->asArray()); +} + +// Page 2 (next 5 results) +$nextFeed = $fb->next($feedEdge); + +foreach ($nextFeed as $status) { + var_dump($status->asArray()); +} +``` diff --git a/docs/examples/post_links.md b/docs/examples/post_links.md new file mode 100644 index 000000000..29cd09bbb --- /dev/null +++ b/docs/examples/post_links.md @@ -0,0 +1,39 @@ +# Post Links Using the Graph API + +This example covers posting a link to the current user's timeline using the Graph API and Facebook SDK for PHP. + +It assumes that you've already obtained an access token from one of the helpers found [here](../reference.md). The access token must have the `publish_actions` permission for this to work. + +For more information, see the documentation for [`Facebook\Facebook`](../reference/Facebook.md), [`Facebook\FacebookResponse`](../reference/FacebookResponse.md), [`Facebook\GraphNodes\GraphNode`](../reference/GraphNode.md), [`Facebook\Exceptions\FacebookSDKException`](../reference/FacebookSDKException.md) and [`Facebook\Exceptions\FacebookResponseException`](../reference/FacebookResponseException.md). + +## Example + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$linkData = [ + 'link' => 'http://www.example.com', + 'message' => 'User provided message', + ]; + +try { + // Returns a `Facebook\FacebookResponse` object + $response = $fb->post('/me/feed', $linkData, '{access-token}'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'Posted with id: ' . $graphNode['id']; +``` + +Note that the 'message' field must come from the user, as pre-filled content is forbidden by the [Platform Policies](https://developers.intern.facebook.com/policy/#control) (2.3). diff --git a/docs/examples/retrieve_user_profile.md b/docs/examples/retrieve_user_profile.md new file mode 100644 index 000000000..6df0d8a56 --- /dev/null +++ b/docs/examples/retrieve_user_profile.md @@ -0,0 +1,34 @@ +# Retrieve User Profile via the Graph API + +This example covers getting profile information for the current user and printing their name, using the Graph API and the Facebook SDK for PHP. + +It assumes that you've already obtained an access token from one of the helpers found [here](../reference.md). + +For more information, see the documentation for [`Facebook\Facebook`](../reference/Facebook.md), [`Facebook\FacebookResponse`](../reference/FacebookResponse.md), [`Facebook\GraphNodes\GraphUser`](../reference/GraphNode.md#graphuser-instance-methods), [`Facebook\Exceptions\FacebookSDKException`](../reference/FacebookSDKException.md) and [`Facebook\Exceptions\FacebookResponseException`](../reference/FacebookResponseException.md). + +## Example + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +try { + // Returns a `Facebook\FacebookResponse` object + $response = $fb->get('/me?fields=id,name', '{access-token}'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +$user = $response->getGraphUser(); + +echo 'Name: ' . $user['name']; +// OR +// echo 'Name: ' . $user->getName(); +``` diff --git a/docs/examples/upload_photo.md b/docs/examples/upload_photo.md new file mode 100644 index 000000000..6b6041907 --- /dev/null +++ b/docs/examples/upload_photo.md @@ -0,0 +1,39 @@ +# Upload Photos to a User's Profile + +This example covers uploading a photo to the current User's profile using the Graph API and the Facebook SDK for PHP. + +It assumes that you've already acquired an access token using one of the helper classes found [here](../reference.md). The access token must have the `publish_actions` permission for this to work. + +For more information, see the documentation for [`Facebook\Facebook`](../reference/Facebook.md), [`Facebook\FileUpload\FacebookFile`](../reference/FacebookFile.md), [`Facebook\FacebookResponse`](../reference/FacebookResponse.md), [`Facebook\GraphNodes\GraphNode`](../reference/GraphNode.md), [`Facebook\Exceptions\FacebookSDKException`](../reference/FacebookSDKException.md) and [`Facebook\Exceptions\FacebookResponseException`](../reference/FacebookResponseException.md). + +## Example + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$data = [ + 'message' => 'My awesome photo upload example.', + 'source' => $fb->fileToUpload('/path/to/photo.jpg'), +]; + +try { + // Returns a `Facebook\FacebookResponse` object + $response = $fb->post('/me/photos', $data, '{access-token}'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'Photo ID: ' . $graphNode['id']; +``` + +Note that the `message` field must come from the user, as pre-filled content is forbidden by the [Platform Policies](https://developers.intern.facebook.com/policy/#control) (2.3). diff --git a/docs/examples/upload_video.md b/docs/examples/upload_video.md new file mode 100644 index 000000000..2aea43eb7 --- /dev/null +++ b/docs/examples/upload_video.md @@ -0,0 +1,66 @@ +# Video Upload Example + +This example covers uploading & posting a video to a user's timeline with the Facebook SDK for PHP. + +## Example + +> **Warning:** Before you upload, check out the [video publishing options & requirements](https://developers.facebook.com/docs/graph-api/reference/video#publishing) for the specific video endpoint you want to publish to. + +The following example will upload a video in chunks using the [resumable upload](https://developers.facebook.com/docs/graph-api/video-uploads#resumable) feature added in Graph v2.3. + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); + +$data = [ + 'title' => 'My Foo Video', + 'description' => 'This video is full of foo and bar action.', +]; + +try { + $response = $fb->uploadVideo('me', '/path/to/foo_bar.mp4', $data, '{user-access-token}'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +echo 'Video ID: ' . $response['video_id']; +``` + +See more about the [`uploadVideo()` method](../reference/Facebook.md#uploadvideo). + +For versions of Graph before v2.3, videos had to be uploaded in one request. + +```php +$fb = new Facebook\Facebook([/* . . . */]); + +$data = [ + 'title' => 'My Foo Video', + 'description' => 'This video is full of foo and bar action.', + 'source' => $fb->videoToUpload('/path/to/foo_bar.mp4'), +]; + +try { + $response = $fb->post('/me/videos', $data, 'user-access-token'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'Video ID: ' . $graphNode['id']; +``` diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 000000000..57b816fc4 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,267 @@ +# Getting started with the Facebook SDK for PHP + +Whether you're developing a website with Facebook login, creating a Facebook Canvas app or Page tab, the Facebook SDK for PHP does all the heavy lifting for you making it as easy as possible to deeply integrate into the Facebook platform. + +## Autoloading & namespaces + +The Facebook SDK for PHP v5 is coded in compliance with [PSR-4](http://www.php-fig.org/psr/psr-4/). This means it relies heavily on namespaces so that class files can be loaded for you automatically. + +It would be advantageous to familiarize yourself with the concepts of [namespacing](http://php.net/manual/en/language.namespaces.rationale.php) and [autoloading](http://php.net/manual/en/function.spl-autoload-register.php) if you are not already acquainted with them. + +## System requirements + +- PHP 5.4 or greater +- [Composer](https://getcomposer.org/) *(optional)* + +## Installing the Facebook SDK for PHP + +There are two methods to install the Facebook SDK for PHP. The recommended installation method is by using [Composer](#installing-with-composer-recommended). If are unable to use Composer for your project, you can still [install the SDK manually](#manually-installing-if-you-really-have-to) by downloading the source files and including the autoloader. + +## Installing with Composer (recommended) + +[Composer](https://getcomposer.org/) is the recommended way to install the Facebook SDK for PHP. Simply run the following in the root of your project. + +``` +composer require facebook/graph-sdk +``` + +> The Facebook SDK starting adhering to [SemVer](http://semver.org/) with version 5. Previous to version 5, the SDK did not follow SemVer. + +Once you do this, composer will edit your `composer.json` file and download the latest version of the SDK and put it in the `/vendor/` directory. + +Make sure to include the Composer autoloader at the top of your script. + +```php +require_once __DIR__ . '/vendor/autoload.php'; +``` + +## Manually installing (if you really have to) + +First, download the source code and unzip it wherever you like in your project. + +[Download the SDK for PHP v5](https://github.com/facebook/php-graph-sdk/archive/5.7.zip) + +Then include the autoloader provided in the SDK at the top of your script. + +```php +require_once __DIR__ . '/path/to/php-graph-sdk/src/Facebook/autoload.php'; +``` + +The autoloader should be able to auto-detect the proper location of the source code. + +### Keeping things tidy + +The source code includes myriad files that aren't necessary for use in a production environment. If you'd like to strip out everything except the core files, follow this example. + +> For this example we'll assume the root of your website is `/var/html`. + +After downloading the source code with the button above, extract the files in a temporary directory. + +Move the folder `src/Facebook` to the root of your website installation or where ever you like to put third-party code. For this example we'll rename the `Facebook` directory to `facebook-sdk-v5`. + +The path the the core SDK files should now be located in `/var/html/facebook-sdk-v5` and inside will also be the `autoload.php` file. + +Assuming we have a script called `index.php` in the root of our web project, we need to include the autoloader at the top of our script. + +```php +require_once __DIR__ . '/facebook-sdk-v5/autoload.php'; +``` + +If the autoloader is having trouble detecting the path to the source files, we can define the location of the source code before the `require_once` statement. + +```php +define('FACEBOOK_SDK_V4_SRC_DIR', __DIR__ . '/facebook-sdk-v5/'); +require_once __DIR__ . '/facebook-sdk-v5/autoload.php'; +``` + +## Configuration and setup + +> **Warning:** This assumes you have already created and configured a Facebook App, which you can obtain from the [App Dashboard](https://developers.facebook.com/apps). + +Before we can send requests to the Graph API, we need to load our app configuration into the `Facebook\Facebook` service. + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + ]); +``` + +You'll need to replace the `{app-id}` and `{app-secret}` with your Facebook app's ID and secret which can be obtained from the [app settings tab](https://developers.facebook.com/apps). + +> **Warning:** It's important that you specify a `default_graph_version` value as this will give you more control over which version of Graph you want to use. If you don't specify a `default_graph_version`, the SDK for PHP will choose one for you and it might not be one that is compatible with your app. + +The `Facebook\Facebook` service ties all the components of the SDK for PHP together. [See the full reference for the `Facebook\Facebook` service](reference/Facebook.md). + +## Authentication and authorization + +The SDK can be used to support logging a Facebook user into your site using Facebook Login which is based on OAuth 2.0. + +Most all request made to the Graph API require an access token. We can obtain user access tokens with the SDK using the [helper classes](reference.md). + +### Obtaining an access token from redirect + +For most websites, you'll use the [`Facebook\Helpers\FacebookRedirectLoginHelper`](reference/FacebookRedirectLoginHelper.md) to generate a login URL with the `getLoginUrl()` method. The link will take the user to an app authorization screen and upon approval, will redirect them back to a URL that you specified. On the redirect callback page we can obtain the user access token as an [`AccessToken`](reference/AccessToken.md) entity. + +> For this example we'll assume `login.php` will present the login link and the user will be redirected to `login-callback.php` where we will obtain the access token. + +```php +# login.php +$fb = new Facebook\Facebook([/* . . . */]); + +$helper = $fb->getRedirectLoginHelper(); +$permissions = ['email', 'user_likes']; // optional +$loginUrl = $helper->getLoginUrl('http://{your-website}/login-callback.php', $permissions); + +echo 'Log in with Facebook!'; +``` + +> **Warning:** The `FacebookRedirectLoginHelper` makes use of sessions to store a [CSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) value. You need to make sure you have sessions enabled before invoking the `getLoginUrl()` method. This is usually done automatically in most web frameworks, but if you're not using a web framework you can add [`session_start();`](http://php.net/session_start) to the top of your `login.php` & `login-callback.php` scripts. + +```php +# login-callback.php +$fb = new Facebook\Facebook([/* . . . */]); + +$helper = $fb->getRedirectLoginHelper(); +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (isset($accessToken)) { + // Logged in! + $_SESSION['facebook_access_token'] = (string) $accessToken; + + // Now you can redirect to another page and use the + // access token from $_SESSION['facebook_access_token'] +} +``` + +### Obtaining an access token from a Facebook Canvas context + +If your app is on Facebook Canvas, use the `getAccessToken()` method on [`Facebook\Helpers\FacebookCanvasHelper`](reference/FacebookCanvasHelper.md) to get an [`AccessToken`](reference/AccessToken.md) entity for the user. + +> **Warning:** The `FacebookCanvasHelper` will detect a [signed request](reference.md#signed-requests) for you and attempt to obtain an access token using the payload data from the signed request. The signed request will only contain the data needed to obtain an access token if the user has already authorized your app sometime in the past. If they have not yet authorized your app the `getAccessToken()` will return `null` and you will need to log the user in with either the [redirect method](#obtaining-an-access-token-from-redirect) or by using the [SDK for JavaScript](https://developers.facebook.com/docs/javascript) and then use the SDK for PHP to [obtain the access token from the cookie](#obtaining-an-access-token-from-the-sdk-for-javascript) the SDK for JavaScript set. + +```php +# example-canvas-app.php +$fb = new Facebook\Facebook([/* . . . */]); + +$helper = $fb->getCanvasHelper(); +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (isset($accessToken)) { + // Logged in. +} +``` + +> If your app exists within the context of a Page tab, you can obtain an access token using the example above since a Page tab is very similar to a Facebook Canvas app. But if you'd like to use a Page-tab-specific helper, you can use the [`Facebook\Helpers\FacebookPageTabHelper`](reference/FacebookPageTabHelper.md) + +### Obtaining an access token from the SDK for JavaScript + +If you're already using the Facebook SDK for JavaScript to authenticate users, you can obtain the access token with PHP by using the [FacebookJavaScriptHelper](reference/FacebookJavaScriptHelper.md). The `getAccessToken()` method will return an [`AccessToken`](reference/AccessToken.md) entity. + +```php +# example-obtain-from-js-cookie-app.php +$fb = new Facebook\Facebook([/* . . . */]); + +$helper = $fb->getJavaScriptHelper(); +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (isset($accessToken)) { + // Logged in +} +``` + +> **Warning:** Make sure you set the `{cookie:true}` option when you [initialize the SDK for JavaScript](https://developers.facebook.com/docs/javascript/reference/FB.init/v2.10). This will make the SDK for JavaScript set a cookie on your domain containing information about the user in the form of a signed request. + +## Extending the access token + +When a user first logs into your app, the access token your app receives will be a short-lived access token that lasts about 2 hours. It's generally a good idea to exchange the short-lived access token for a long-lived access token that lasts about 60 days. + +To extend an access token, you can make use of the [`OAuth2Client`](reference/Facebook.md#getoauth2client). + +```php +// OAuth 2.0 client handler +$oAuth2Client = $fb->getOAuth2Client(); + +// Exchanges a short-lived access token for a long-lived one +$longLivedAccessToken = $oAuth2Client->getLongLivedAccessToken('{access-token}'); +``` + +[See more about long-lived and short-lived access tokens](https://developers.facebook.com/docs/facebook-login/access-tokens#usertokens). + +## Making Requests to the Graph API + +Once you have an instance of the `Facebook\Facebook` service and obtained an access token, you can begin making calls to the Graph API. + +In this example we will send a GET request to the Graph API endpoint `/me`. The `/me` endpoint is a special alias to the [user node endpoint](https://developers.facebook.com/docs/graph-api/reference/user) that references the user or Page making the request. + +```php +$fb = new Facebook\Facebook([/* . . . */]); + +// Sets the default fallback access token so we don't have to pass it to each request +$fb->setDefaultAccessToken('{access-token}'); + +try { + $response = $fb->get('/me'); + $userNode = $response->getGraphUser(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +echo 'Logged in as ' . $userNode->getName(); +``` + +The `get()` method will return a [`Facebook\FacebookResponse`](reference/FacebookResponse.md) which is an entity that represents an HTTP response from the Graph API. + +To get the response in the form of a nifty collection, we call `getGraphUser()` which returns a [`Facebook\GraphNodes\GraphUser`](reference/GraphNode.md#graphuser-instance-methods) entity which represents a user node. + +If you don't care about fancy collections and just want the response as a plain-old array, you can call the `getDecodedBody()` method on the `FacebookResponse` entity. + +```php +try { + $response = $fb->get('/me'); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // . . . + exit; +} + +$plainOldArray = $response->getDecodedBody(); +``` + +For a full list of all of the components that make up the SDK for PHP, see the [SDK for PHP reference page](reference.md). diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 000000000..a98acfc57 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,91 @@ +# Facebook SDK for PHP Reference (v5) + +Below is the API reference for the Facebook SDK for PHP. + +# Core API + +These classes are at the core of the Facebook SDK for PHP. + +| Class name | Description | +| ------------- | ------------- | +| [`Facebook\Facebook`](reference/Facebook.md) | The main service object that helps tie all the SDK components together. | +| [`Facebook\FacebookApp`](reference/FacebookApp.md) | An entity that represents a Facebook app and is required to send requests to Graph. | + +# Authentication + +These classes facilitate authenticating a Facebook user with OAuth 2.0. + +| Class name | Description | +| ------------- | ------------- | +| [`Facebook\Helpers\FacebookRedirectLoginHelper`](reference/FacebookRedirectLoginHelper.md) | An OAuth 2.0 service to obtain a user access token from a redirect using a "Log in with Facebook" link. | +| [`Facebook\Authentication\AccessToken`](reference/AccessToken.md) | An entity that represents an access token. | +| `Facebook\Authentication\AccessTokenMetadata` | An entity that represents metadata from an access token. | +| `Facebook\Authentication\OAuth2Client` | An OAuth 2.0 client that sends and receives HTTP requests related to user authentication. | + +# Requests and Responses + +These classes are used in a Graph API request/response cycle. + +| Class name | Description | +| ------------- | ------------- | +| [`Facebook\FacebookRequest`](reference/FacebookRequest.md) | An entity that represents an HTTP request to be sent to Graph. | +| [`Facebook\FacebookResponse`](reference/FacebookResponse.md) | An entity that represents an HTTP response from Graph. | +| [`Facebook\FacebookBatchRequest`](reference/FacebookBatchRequest.md) | An entity that represents an HTTP batch request to be sent to Graph. | +| [`Facebook\FacebookBatchResponse`](reference/FacebookBatchResponse.md) | An entity that represents an HTTP response from Graph after sending a batch request. | +| [`Facebook\FacebookClient`](reference/FacebookClient.md) | A service object that sends HTTP requests and receives HTTP responses to and from the Graph API. | + +# Signed Requests + +Classes to help obtain and manage signed requests. + +| Class name | Description | +| ------------- | ------------- | +| [`Facebook\Helpers\FacebookJavaScriptHelper`](reference/FacebookJavaScriptHelper.md) | Used to obtain an access token or signed request from the cookie set by the JavaScript SDK. | +| [`Facebook\Helpers\FacebookCanvasHelper`](reference/FacebookCanvasHelper.md) | Used to obtain an access token or signed request from within the context of an app canvas. | +| [`Facebook\Helpers\FacebookPageTabHelper`](reference/FacebookPageTabHelper.md) | Used to obtain an access token or signed request from within the context of a page tab. | +| [`Facebook\SignedRequest`](reference/SignedRequest.md) | An entity that represents a signed request. | + +# Core Exceptions + +These are the core exceptions that the SDK will throw when an error occurs. + +| Class name | Description | +| ------------- | ------------- | +| [`Facebook\Exceptions\FacebookSDKException`](reference/FacebookSDKException.md) | The base exception to all exceptions thrown by the SDK. Thrown when there is a non-Graph-response-related error. | +| [`Facebook\Exceptions\FacebookResponseException`](reference/FacebookResponseException.md) | The base exception to all Graph error responses. This exception is never thrown directly. | + +# Graph Nodes and Edges + +Graph nodes are collections that represent nodes returned by the Graph API. And Graph edges are a collection of nodes returned from an edge on the Graph API. + +| Class name | Description | +| ------------- | ------------- | +| [`Facebook\GraphNodes\GraphNode`](reference/GraphNode.md) | The base collection object that represents a generic node. | +| [`Facebook\GraphNodes\GraphEdge`](reference/GraphEdge.md) | A collection of GraphNode\'s with special methods to help paginate over the edge. | +| [`Facebook\GraphNodes\GraphAchievement`](reference/GraphNode.md#graphachievement-instance-methods) | A collection that represents an Achievement node. | +| [`Facebook\GraphNodes\GraphAlbum`](reference/GraphNode.md#graphalbum-instance-methods) | A collection that represents an Album node. | +| [`Facebook\GraphNodes\GraphLocation`](reference/GraphNode.md#graphlocation-instance-methods) | A collection that represents a Location node. | +| [`Facebook\GraphNodes\GraphPage`](reference/GraphNode.md#graphpage-instance-methods) | A collection that represents a Page node. | +| [`Facebook\GraphNodes\GraphPicture`](reference/GraphNode.md#graphpicture-instance-methods) | A collection that represents a Picture node. | +| [`Facebook\GraphNodes\GraphUser`](reference/GraphNode.md#graphuser-instance-methods) | A collection that represents a User node. | + +# File Uploads + +These are entities that represent files to be uploaded with a Graph request. + +| Class name | Description | +| ------------- | ------------- | +| [`Facebook\FileUpload\FacebookFile`](reference/FacebookFile.md) | Represents a generic file to be uploaded to the Graph API. | +| [`Facebook\FileUpload\FacebookVideo`](reference/FacebookVideo.md) | Represents a video file to be uploaded to the Graph API. | + +# Extensibility + +You can overwrite certain functionality of the SDK by coding to an interface and injecting an instance of your custom functionality. + +| Interface name | Description | +| ------------- | ------------- | +| `Facebook\HttpClients\ FacebookHttpClientInterface` | An interface to code your own HTTP client implementation. | +| `Facebook\Http\GraphRawResponse` | An entity that is returned from an instance of a `FacebookHttpClientInterface` that represents a raw HTTP response from the Graph API. | +| [`Facebook\PersistentData\PersistentDataInterface`](reference/PersistentDataInterface.md) | An interface to code your own persistent data storage implementation. | +| [`Facebook\Url\UrlDetectionInterface`](reference/UrlDetectionInterface.md) | An interface to code your own URL detection logic. | +| [`Facebook\PseudoRandomString\ PseudoRandomStringGeneratorInterface`](reference/PseudoRandomStringGeneratorInterface.md) | An interface to code your own cryptographically secure pseudo-random string generator. | diff --git a/docs/reference/AccessToken.md b/docs/reference/AccessToken.md new file mode 100644 index 000000000..ec4539086 --- /dev/null +++ b/docs/reference/AccessToken.md @@ -0,0 +1,56 @@ +# AccessToken for the Facebook SDK for PHP + +Requests to the Graph API need to have an access token sent with them to identify the app, user and/or page that is making the request. The `Facebook\Authentication\AccessToken` entity represents an access token. + +## Facebook\Authentication\AccessToken + +Whenever you use the PHP SDK to obtain an access token, the access token will be returned as an instance of `AccessToken`. The `AccessToken` entity contains a number of methods that make it easier to handle access tokens. + +### getValue() +```php +public string getValue() +``` +Returns the access token as a string. The `AccessToken` entity also makes use of the [magic method `__toString()`](http://php.net/manual/en/language.oop5.magic.php#object.tostring) so you can cast an `AccessToken` entity to a string with: `$token = (string) $accessTokenEntity;` + +### getExpiresAt() +```php +public \DateTime|null getExpiresAt() +``` +If the expiration date was provided when the `AccessToken` entity was instantiated, the `getExpiresAt()` method will return the access token expiration date as a [`DateTime` entity](http://php.net/manual/en/class.datetime.php). If the expiration date was not originally provided, the method will return `null`. + +### isExpired() +```php +public boolean|null isExpired() +``` +If the expiration date was provided when the `AccessToken` entity was instantiated, the `isExpired()` method will return `true` if the access token has expired. If the access token is still active, the method will return `false`. If the expiration date was not +originally provided, the method will return `null`. + +### isLongLived() +```php +public boolean|null isLongLived() +``` +If the expiration date was provided when the `AccessToken` entity was instantiated, the `isLongLived()` method will return `true` if the access token is long-lived. If the token is short-lived, the method will return `false`. If the expiration date was not +originally provided, the method will return `false`. [See more about long-lived and short-lived access tokens](https://developers.facebook.com/docs/facebook-login/access-tokens#extending). + +### isAppAccessToken() +```php +public boolean isAppAccessToken() +``` +Since app access tokens contain the app secret in plain-text, it's very important that app access tokens aren't used in client-side contexts where someone might be able to grab the app secret. For this reason you should do a check on the access token to ensure it is not an app access token before using it on the client-side. The `isAppAccessToken()` will return `true` if the access token is an app access token and `false` if it is not. + +### getAppSecretProof() +```php +public string getAppSecretProof(string $appSecret) +``` +For better security, all requests to the Graph API should be [signed with an app secret proof](https://developers.facebook.com/docs/graph-api/securing-requests#appsecret_proof) and your app settings should enable the app secret proof requirement for all requests. The PHP SDK will generate the app secret proof for each request automatically, but if you need to generate one, pass your app secret to the `getAppSecretProof()` method and it will return the HMAC hash that is the app secret proof. + +## Making an entity from a string + +If you already have an access token in the form of a string (from a session or database for example), you can make an `AccessToken` entity with it by passing the access token string as the first argument in the `AccessToken` the constructor. + +You can optionally pass an expiration date in the form of timestamp as the second argument. + +```php +$expires = time() + 60 * 60 * 2; +$accessToken = new Facebook\Authentication\AccessToken('{example-access-token}', $expires); +``` diff --git a/docs/reference/Birthday.md b/docs/reference/Birthday.md new file mode 100644 index 000000000..20901da68 --- /dev/null +++ b/docs/reference/Birthday.md @@ -0,0 +1,55 @@ +# Birthday for the Facebook SDK for PHP + +Extends `\DateTime` and represents a user's birthday returned from the Graph API which can be returned omitting certain information. + +Users may opt not to share birth day or month, or may not share birth year. Possible returns: + +* MM/DD/YYYY +* MM/DD +* YYYY + +## Facebook\GraphNodes\Birthday + +After retrieving a GraphUser from the Graph API, the `getBirthday()` method will return the birthday in the form of a `Facebook\GraphNodes\Birthday` entity which indicates which aspects of the birthday the user opted to share. + +The `Facebook\GraphNodes\Birthday` entity extends `DateTime` so `format` may be used to present the information appropriately depending on what information it contains. + +Usage: + +```php +$fb = new Facebook\Facebook(\* *\); +// Returns a `Facebook\FacebookResponse` object +$response = $fb->get('/me'); + +// Get the response typed as a GraphUser +$user = $response->getGraphUser(); + +// Gets birthday value, assume Graph return was format MM/DD +$birthday = $user->getBirthday(); + +var_dump($birthday); +// class Facebook\GraphNodes\Birthday ... + +var_dump($birthday->hasDate()); +// true + +var_dump($birthday->hasYear()); +// false + +var_dump($birthday->format('m/d')); +// 03/21 +``` + +## Instance Methods + +### hasDate() +```php +public boolean hasDate() +``` +Returns whether or not the birthday object contains the day and month of birth. + +### hasYear() +```php +public boolean hasYear() +``` +Returns whether or not the birthday object contains the year of birth. diff --git a/docs/reference/Facebook.md b/docs/reference/Facebook.md new file mode 100644 index 000000000..732f9abf6 --- /dev/null +++ b/docs/reference/Facebook.md @@ -0,0 +1,562 @@ +# Facebook service class for the Facebook SDK for PHP + +The Facebook SDK for PHP is made up of many components. The `Facebook\Facebook` service class provides an easy interface for working with all the components of the SDK. + +## Facebook\Facebook + +To instantiate a new `Facebook\Facebook` service, pass an array of configuration options to the constructor. + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_graph_version' => 'v2.10', + // . . . + ]); +``` + +Usage: + +```php +// Send a GET request +$response = $fb->get('/me'); + +// Send a POST request +$response = $fb->post('/me/feed', ['message' => 'Foo message']); + +// Send a DELETE request +$response = $fb->delete('/{node-id}'); +``` + +If you don't provide a `default_access_token` in the configuration options, or you wish to use a different access token than the default, you can explicitly pass the access token as an argument to the `get()`, `post()`, and `delete()` methods. + +```php +$res = $fb->get('/me', '{access-token}'); +$res = $fb->post('/me/feed', ['foo' => 'bar'], '{access-token}'); +$res = $fb->delete('/{node-id}', '{access-token}'); +``` + +## Configuration options + +Although the `Facebook\Facebook` service tries to make the SDK as easy as possible to use, it also makes it easy to customize with configuration options. + +Full configuration options list: + +```php +$fb = new Facebook\Facebook([ + 'app_id' => '{app-id}', + 'app_secret' => '{app-secret}', + 'default_access_token' => '{access-token}', + 'enable_beta_mode' => true, + 'default_graph_version' => 'v2.10', + 'http_client_handler' => 'guzzle', + 'persistent_data_handler' => 'memory', + 'url_detection_handler' => new MyUrlDetectionHandler(), + 'pseudo_random_string_generator' => new MyPseudoRandomStringGenerator(), +]); +``` + +### `app_id` +The ID of your Facebook app (required). + +### `app_secret` +The secret of your Facebook app (required). + +### `default_access_token` +The default fallback access token to use if one is not explicitly provided. The value can be of type `string` or `Facebook\AccessToken`. If any other value is provided an `InvalidArgumentException` will be thrown. Defaults to `null`. + +### `enable_beta_mode` +Enable [beta mode](https://developers.facebook.com/docs/apps/beta-tier) so that request are made to the [https://graph.beta.facebook.com](https://graph.beta.facebook.com/) endpoint. Set to boolean `true` to enable or `false` to disable. Defaults to `false`. + +### `default_graph_version` +Allows you to overwrite the default Graph version number set in `Facebook\Facebook::DEFAULT_GRAPH_VERSION`. Set this as a string as it would appear in the Graph url, e.g. `v2.10`. Defaults to the [latest version of Graph](https://developers.facebook.com/docs/apps/changelog). + +### `http_client_handler` +Allows you to overwrite the default HTTP client. + +By default, the SDK will try to use cURL as the HTTP client. If a cURL implementation cannot be found, it will fallback to a stream wrapper HTTP client. You can force either HTTP client implementations by setting this value to `curl` or `stream`. + +If you wish to use Guzzle, you can set this value to `guzzle`, but it requires that you [install Guzzle](http://docs.guzzlephp.org/en/latest/) with composer. + +If you wish to write your own HTTP client, you can code your HTTP client to the `Facebook\HttpClients\FacebookHttpClientInterface` and set this value to an instance of your custom client. + +```php +$fb = new Facebook([ + 'http_client_handler' => new MyCustomHttpClient(), +]); +``` + +If any other value is provided an `InvalidArgumentException` will be thrown. + +### `persistent_data_handler` +Allows you to overwrite the default persistent data store. + +By default, the SDK will try to use the native PHP session for the persistent data store. There is also an in-memory persistent data handler which is useful when running your script from the command line for example. You can force either implementation by setting this value to `session` or `memory`. + +If you wish to write your own persistent data handler, you can code your persistent data handler to the [`Facebook\PersistentData\PersistentDataInterface`](PersistentDataInterface.md) and set the value of `persistent_data_handler` to an instance of your custom handler. + +```php +$fb = new Facebook([ + 'persistent_data_handler' => new MyCustomPersistentDataHandler(), +]); +``` + +If any other value is provided an `InvalidArgumentException` will be thrown. + +### `url_detection_handler` +Allows you to overwrite the default URL detection logic. + +The SDK will do its best to detect the proper current URL but this can sometimes get tricky if you have a very customized environment. You can write your own URL detection logic that implements the ['Facebook\Url\UrlDetectionInterface'](UrlDetectionInterface.md)` and set the value of `url_detection_handler` to an instance of your custom URL detector. + +```php +$fb = new Facebook([ + 'url_detection_handler' => new MyUrlDetectionHandler(), +]); +``` + +If any other value is provided an `InvalidArgumentException` will be thrown. + +### `pseudo_random_string_generator` +Allows you to overwrite the default cryptographically secure pseudo-random string generator. + +Generating random strings in PHP is easy but generating _cryptographically secure_ random strings is another matter. By default the SDK will attempt to detect a suitable to cryptographically secure random string generator for you. If a cryptographically secure method cannot be detected, a `Facebook\Exceptions\FacebookSDKException` will be thrown. + +You can force a specific implementation of the CSPRSG's provided in the SDK by setting `pseudo_random_string_generator` to one of the following methods: `mcrypt`, `openssl` and `urandom`. + +```php +$fb = new Facebook([ + 'pseudo_random_string_generator' => 'openssl', +]); +``` + +You can write your own CSPRSG that implements the [`Facebook\PseudoRandomString\PseudoRandomStringGeneratorInterface`](PseudoRandomStringGeneratorInterface.md) and set the value of `pseudo_random_string_generator` to an instance of your custom generator. + +```php +$fb = new Facebook([ + 'pseudo_random_string_generator' => new MyPseudoRandomStringGenerator(), +]); +``` + +If any other value is provided an `InvalidArgumentException` will be thrown. + +## Environment variables fallback + +The only required configuration options are `app_id` and `app_secret`. However, the SDK will look to environment variables for the app ID and app secret. + +To take advantage of this feature, simply set an environment variable named `FACEBOOK_APP_ID` with your Facebook app ID and set an environment variable named `FACEBOOK_APP_SECRET` with your Facebook app secret and you will be able to instantiate the `Facebook\Facebook` service without setting any configuration in the constructor. + +```php +$fb = new Facebook\Facebook(); +``` + +# Instance Methods + +## getApp() +```php +public FacebookApp getApp() +``` +Returns the instance of `Facebook\FacebookApp` for the instantiated service. + +## getClient() +```php +public Facebook\FacebookClient getClient() +``` +Returns the instance of [`Facebook\FacebookClient`](FacebookClient.md) for the instantiated service. + +## getOAuth2Client() +```php +public Facebook\Authentication\OAuth2Client getOAuth2Client() +``` +Returns an instance of `Facebook\Authentication\OAuth2Client`. + +## getLastResponse() +```php +public Facebook\FacebookResponse|Facebook\FacebookBatchResponse|null getLastResponse() +``` +Returns the last response received from the Graph API in the form of a `Facebook\FacebookResponse` or `Facebook\FacebookBatchResponse`. + +## getUrlDetectionHandler() +```php +public Facebook\Url\UrlDetectionInterface getUrlDetectionHandler() +``` +Returns an instance of [`Facebook\Url\UrlDetectionInterface`](UrlDetectionInterface.md). + +## getDefaultAccessToken() +```php +public Facebook\Authentication\AccessToken|null getDefaultAccessToken() +``` +Returns the default fallback [`AccessToken`](AccessToken.md) entity that is being used with every request to Graph. This value can be set with the configuration option `default_access_token` or by using `setDefaultAccessToken()`. + +## setDefaultAccessToken() +```php +public setDefaultAccessToken(string|Facebook\AccessToken $accessToken) +``` +Sets the default fallback access token to be use with all requests sent to Graph. The access token can be a string or an instance of [`AccessToken`](AccessToken.md). + +```php +$fb->setDefaultAccessToken('{my-access-token}'); + +// . . . OR . . . + +$accessToken = new Facebook\Authentication\AccessToken('{my-access-token}'); +$fb->setDefaultAccessToken($accessToken); +``` + +This setting will overwrite the value from `default_access_token` option if it was passed to the `Facebook\Facebook` constructor. + +## getDefaultGraphVersion() +```php +public string getDefaultGraphVersion() +``` +Returns the default version of Graph. If the `default_graph_version` configuration option was not set, this will default to `Facebook\Facebook::DEFAULT_GRAPH_VERSION`. + +## get() +```php +public Facebook\FacebookResponse get( + string $endpoint, + string|AccessToken|null $accessToken, + string|null $eTag, + string|null $graphVersion +) +``` + +Sends a GET request to Graph and returns a `Facebook\FacebookResponse`. + +`$endpoint` +The url to send to Graph without the version prefix (required). + +```php +$fb->get('/me'); +``` + +`$accessToken` +The access token (as a string or `AccessToken` entity) to use for the request. If none is provided, the SDK will assume the value from the `default_access_token` configuration option if it was set. + +`$eTag` +[Graph supports eTags](https://developers.facebook.com/docs/marketing-api/etags). Set this to the eTag from a previous request to get a `304 Not Modified` response if the data has not changed. + +`$graphVersion` +This will overwrite the Graph version that was set in the `default_graph_version` configuration option. + +## post() +```php +public Facebook\FacebookResponse post( + string $endpoint, + array $params, + string|AccessToken|null $accessToken, + string|null $eTag, + string|null $graphVersion +) +``` + +Sends a POST request to Graph and returns a `Facebook\FacebookResponse`. + +The arguments are the same as `get()` above with the exception of `$params`. + +`$params` +The associative array of params you want to send in the body of the POST request. + +```php +$response = $fb->post('/me/feed', ['message' => 'Foo message']); +``` + +## delete() +```php +public Facebook\FacebookResponse delete( + string $endpoint, + array $params, + string|AccessToken|null $accessToken, + string|null $eTag, + string|null $graphVersion +) +``` + +Sends a DELETE request to Graph and returns a `Facebook\FacebookResponse`. + +The arguments are the same as `post()` above. + +```php +$response = $fb->delete('/{node-id}', ['object' => '1234']); +``` + +## request() +```php +public Facebook\FacebookRequest request( + string $method, + string $endpoint, + array $params, + string|AccessToken|null $accessToken, + string|null $eTag, + string|null $graphVersion +) +``` + +Instantiates a new `Facebook\FacebookRequest` entity **but does not send the request to Graph**. This is useful for creating a number of requests to be sent later in a batch request (see `sendBatchRequest()` below). + +The arguments are the same as `post()` above with the exception of `$method`. + +`$method` +The HTTP request verb to use for this request. This can be set to any verb that the `$graphVersion` of Graph supports, e.g. `GET`, `POST`, `DELETE`, etc. + +```php +$request = $fb->request('GET', '/{node-id}'); +``` + +## sendRequest() +```php +public Facebook\FacebookResponse sendRequest( + string $method, + string $endpoint, + array $params, + string|AccessToken|null $accessToken, + string|null $eTag, + string|null $graphVersion +) +``` + +Sends a request to the Graph API. + +```php +$response = $fb->sendRequest('GET', '/me', [], '{access-token}', 'eTag', 'v2.10'); +``` + +## sendBatchRequest() +```php +public Facebook\FacebookBatchResponse sendBatchRequest( + array $requests, + string|AccessToken|null $accessToken, + string|null $graphVersion +) +``` + +Sends an array of `Facebook\FacebookRequest` entities as a batch request to Graph. + +The `$accessToken` and `$graphVersion` arguments are the same as `get()` above. + +`$requests` +An array of `Facebook\FacebookRequest` entities. This can be a numeric or associative array but every value of the array has to be an instance of `Facebook\FacebookRequest`. + +If the requests are sent as an associative array, the key will be used as the `name` of the request so that it can be referenced by another request. See more on [batch request naming and using JSONPath](https://developers.facebook.com/docs/graph-api/making-multiple-requests). + +```php +$requests = [ + 'me' => $fb->request('GET', '/me'), + 'you' => $fb->request('GET', '/1337', [], '{user-b-access-token}'), + 'my_post' => $fb->request('POST', '/1337/feed', ['message' => 'Hi!']), +]; +$batchResponse = $fb->sendBatchRequest($requests); +``` + +[See a full batch example](../examples/batch_request.md). + +## newBatchRequest() +```php +public Facebook\FacebookBatchRequest newBatchRequest( + string|AccessToken|null $accessToken, + string|null $graphVersion +) +``` + +Instantiates an empty `Facebook\FacebookBatchRequest`. +To populate it use the [`Facebook\FacebookBatchRequest::add()`](FacebookBatchRequest.md#add) method. + +The `$accessToken` and `$graphVersion` arguments are the same as `get()` above. +If any of the requests contained in the batch request does not have either the `$accessToken` or the `$graphVersion` set, +it fallbacks to the values provided in the instantiation of the batch request. + +[See a full batch example](../examples/batch_request.md). + +## getRedirectLoginHelper() +```php +public Facebook\Helpers\FacebookRedirectLoginHelper getRedirectLoginHelper() +``` + +Returns a [`Facebook\Helpers\FacebookRedirectLoginHelper`](FacebookRedirectLoginHelper.md) which is used to generate a "Login with Facebook" link and obtain an access token from a redirect. + +```php +$helper = $fb->getRedirectLoginHelper(); +``` + +## getJavaScriptHelper() +```php +public Facebook\Helpers\FacebookJavaScriptHelper getJavaScriptHelper() +``` + +Returns a [`Facebook\Helpers\FacebookJavaScriptHelper`](FacebookJavaScriptHelper.md) which is used to access the signed request stored in the cookie set by the SDK for JavaScript. + +```php +$helper = $fb->getJavaScriptHelper(); +``` + +## getCanvasHelper() +```php +public Facebook\Helpers\FacebookCanvasHelper getCanvasHelper() +``` + +Returns a [`Facebook\Helpers\FacebookCanvasHelper`](FacebookCanvasHelper.md) which is used to access the signed request that is `POST`ed to canvas apps. + +```php +$helper = $fb->getCanvasHelper(); +``` + +## getPageTabHelper() +```php +public Facebook\Helpers\FacebookPageTabHelper getPageTabHelper() +``` + +Returns a [`Facebook\Helpers\FacebookPageTabHelper`](FacebookPageTabHelper.md) which is used to access the signed request that is `POST`ed to canvas apps and provides a number of helper methods useful for apps living in a page tab context. + +```php +$helper = $fb->getPageTabHelper(); +``` + +## next() +```php +public Facebook\GraphNodes\GraphEdge|null next(Facebook\GraphNodes\GraphEdge $graphEdge) +``` + +Requests and returns the next page of results in a [`Facebook\GraphNodes\GraphEdge`](GraphEdge.md) collection. If the next page returns no results, `null` will be returned. + +```php +// Iterate over 5 pages max +$maxPages = 5; + +// Get a list of photo nodes from the /photos edge +$response = $fb->get('/me/photos?fields=id,source,likes&limit=5'); + +$photosEdge = $response->getGraphEdge(); + +if (count($photosEdge) > 0) { + $pageCount = 0; + do { + foreach ($photosEdge as $photo) { + var_dump($photo->asArray()); + + // Deep pagination is supported on child GraphEdge's + $likes = $photo['likes']; + do { + echo '

Likes:

' . "\n\n"; + var_dump($likes->asArray()); + } while ($likes = $fb->next($likes)); + } + $pageCount++; + } while ($pageCount < $maxPages && $photosEdge = $fb->next($photosEdge)); +} +``` + +## previous() +```php +public Facebook\GraphNodes\GraphEdge|null previous(Facebook\GraphNodes\GraphEdge $graphEdge) +``` + +Requests and returns the previous page of results in a `Facebook\GraphNodes\GraphEdge` collection. Functions just like `next()` above, but in the opposite direction of pagination. + +## fileToUpload() +```php +public Facebook\FileUpload\FacebookFile fileToUpload(string $pathToFile) +``` + +When a valid path to a local or remote file is provided, `fileToUpload()` will returns a `FacebookFile` entity that can be used in the params in a POST request to Graph. + +```php +// Upload a photo for a user +$data = [ + 'message' => 'A neat photo upload example. Neat.', + 'source' => $fb->fileToUpload('/path/to/photo.jpg'), +]; + +try { + $response = $fb->post('/me/photos', $data); +} catch(FacebookSDKException $e) { + echo 'Error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'Photo ID: ' . $graphNode['id']; +``` + +## videoToUpload() +```php +public Facebook\FileUpload\FacebookVideo videoToUpload(string $pathToVideoFile) +``` + +Uploading videos to Graph requires that you send the request to `https://graph-video.facebook.com` instead of the normal `https://graph.facebook.com` host name. When you use `videoToUpload()` to upload a video, the SDK for PHP will automatically point the request to the `graph-video.facebook.com` host name for you. + +```php +// Upload a video for a user +$data = [ + 'title' => 'My awesome video', + 'description' => 'More info about my awesome video.', + 'source' => $fb->videoToUpload('/path/to/video.mp4'), +]; + +try { + $response = $fb->post('/me/videos', $data); +} catch(FacebookSDKException $e) { + echo 'Error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'Video ID: ' . $graphNode['id']; +``` + +## uploadVideo() +```php +public array videoToUpload( + string $target, + string $pathToFile, + array $metadata = [], + string|Facebook\AccessToken $accessToken = null, + int $maxTransferTries = 5, + string $graphVersion = null + ) +``` + +Functionality to [upload video files in chunks](https://developers.facebook.com/docs/graph-api/video-uploads#resumable) was added to the Graph API in v2.3. The `uploadVideo()` method provides an easy API to take advantage of this new feature. + +### Parameters + +`$target` +The ID or alias of the target node. This can be a user ID, page ID, event ID, group ID or `me`. + +`$pathToFile` +The absolute or relative path to the video file to upload. + +`$metadata` +All the metadata associated with the [Video node](https://developers.facebook.com/docs/graph-api/reference/video). + +`$accessToken` +The access token to use for this request. Falls back to the default access token if one exists. + +`$maxTransferTries` +During the [transfer phase](https://developers.facebook.com/docs/graph-api/video-uploads#transfer) an upload can fail for a number of reasons. If the Graph API responds with an error that is resumable, the PHP SDK will retry uploading the chunk automatically. By default the PHP SDK will try to upload each chunk five times before throwing a `FacebookResponseException`. + +`$graphVersion` +The version of the Graph API to use. The resumable upload feature did not become available until Graph v2.3. + +### Return Value + +The array that is returned will contain two keys; `video_id` with the ID of the video node, and `success` with a boolean value that represents a successful or failed transfer. + +### Example + +```php +// Upload a video for a user (chunked) +$data = [ + 'title' => 'My awesome video', + 'description' => 'More info about my awesome video.', +]; + +try { + $response = $fb->uploadVideo('me', '/path/to/video.mp4', $data, '{user-access-token}'); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + echo 'Error: ' . $e->getMessage(); + exit; +} + +echo 'Video ID: ' . $response['video_id']; +``` diff --git a/docs/reference/FacebookApp.md b/docs/reference/FacebookApp.md new file mode 100644 index 000000000..2506fd301 --- /dev/null +++ b/docs/reference/FacebookApp.md @@ -0,0 +1,57 @@ +# FacebookApp for the Facebook SDK for PHP + +In order to make requests to the Graph API, you need to [create a Facebook app](https://developers.facebook.com/apps) and obtain the app ID and the app secret. The `Facebook\FacebookApp` entity represents the Facebook app that is making the requests to the Graph API. + +> **Warning:** It is quite uncommon to work with the `FacebookApp` entity directly since the `Facebook\Facebook` service handles injecting it into the required classes for you. + +## Facebook\FacebookApp + +To instantiate a new `Facebook\FacebookApp` entity, pass the app ID and app secret to the constructor. + +```php +$fbApp = new Facebook\FacebookApp('{app-id}', '{app-secret}'); +``` + +Alternatively you can obtain the `Facebook\FacebookApp` entity from the [`Facebook\Facebook`](Facebook.md) super service class. + +```php +$fb = new Facebook\Facebook([/* . . . */]); +$fbApp = $fb->getApp(); +``` + +You'll rarely be using the `FacebookApp` entity directly unless you're doing some extreme customizations of the SDK for PHP. But this entity plays an important role in the internal workings of the SDK for PHP. + +## Instance Methods + +## getAccessToken() +```php +public Facebook\Authentication\AccessToken getAccessToken() +``` +Returns an app access token in the form of an [`AccessToken`](AccessToken.md) entity. + +## getId() +```php +public string getId() +``` +Returns the app id. + +## getSecret() +```php +public string getSecret() +``` +Returns the app secret. + +## Serialization + +The `Facebook\FacebookApp` entity can be serialized and unserialized. + +```php +$fbApp = new Facebook\FacebookApp('foo-app-id', 'foo-app-secret'); + +$serializedFacebookApp = serialize($fbApp); +// C:29:"Facebook\\FacebookApp":54:{a:2:{i:0;s:10:"foo-app-id";i:1;s:14:"foo-app-secret";}} + +$unserializedFacebookApp = unserialize($serializedFacebookApp); +echo $unserializedFacebookApp->getAccessToken(); +// foo-app-id|foo-app-secret +``` diff --git a/docs/reference/FacebookBatchRequest.md b/docs/reference/FacebookBatchRequest.md new file mode 100644 index 000000000..c3d40dc53 --- /dev/null +++ b/docs/reference/FacebookBatchRequest.md @@ -0,0 +1,98 @@ +# FacebookBatchRequest for the Facebook SDK for PHP + +Represents a batch request that will be sent to the Graph API. + +## Facebook\FacebookBatchRequest + +You can instantiate a new `FacebookBatchRequest` entity directly by sending the arguments to the constructor or +by using the [`Facebook\Facebook::newBatchRequest()`](Facebook.md#newBatchRequest) factory method. + +```php +use Facebook\FacebookBatchRequest; + +$request = new FacebookBatchRequest( + Facebook\FacebookApp $app, + array $requests, + string|null $accessToken, + string|null $graphVersion +); +``` + +The `$requests` array is an array of [`Facebook\FacebookRequest`'s](FacebookRequest.md) to be sent as a batch request. + +The `FacebookBatchRequest` entity does not actually make any calls to the Graph API, but instead just represents a batch request that can be sent to the Graph API later. The batch request can be sent by using [`Facebook\Facebook::sendBatchRequest()`](Facebook.md#sendbatchrequest) or [`Facebook\FacebookClient::sendBatchRequest()`](FacebookClient.md#sendbatchrequest.md). + +Usage: + +```php +$fb = new Facebook\Facebook(/* . . . */); + +$requests = [ + $fb->request('GET', '/me'), + $fb->request('POST', '/me/feed', [/* */]), +]; + +// Send the batch request to Graph +try { + $batchResponse = $fb->sendBatchRequest($requests, '{access-token}'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +foreach ($batchResponse as $key => $response) { + if ($response->isError()) { + $error = $response->getThrownException(); + echo $key . ' error: ' . $error->getMessage(); + } else { + // Success + } +} +``` + +## Instance Methods + +Since the `Facebook\FacebookBatchRequest` is extended from the [`Facebook\FacebookRequest`](FacebookRequest.md) entity, all the methods are inherited. + +### add() +```php +public add( + array|Facebook\FacebookBatchRequest $request, + string|null $name +) +``` +Adds a request to be sent in the batch request. The `$request` can be a single [`Facebook\FacebookRequest`](FacebookRequest.md) or an array of `Facebook\FacebookRequest`'s. + +The `$name` argument is optional and is used to identify the request in the batch. + +### getRequests() +```php +public array getRequests() +``` +Returns the array of [`Facebook\FacebookRequest`'s](FacebookRequest.md) to be sent in the batch request. + +## Array Access + +Since `Facebook\FacebookBatchRequest` implements `\IteratorAggregate` and `\ArrayAccess`, the requests can be accessed via array syntax and can also be iterated over. + +```php +$fb = new Facebook\Facebook(/* . . . */); +$requests = [ + 'foo' => $fb->request('GET', '/me'), + 'bar' => $fb->request('POST', '/me/feed', [/* */]), +]; +$batchRequest = new Facebook\FacebookBatchRequest($fb->getApp(), $requests, '{access-token}'); + +var_dump($batchRequest[0]); +/* +array(2) { + 'name' => string(3) "foo" + 'request' => class Facebook\FacebookRequest + . . . +*/ +``` diff --git a/docs/reference/FacebookBatchResponse.md b/docs/reference/FacebookBatchResponse.md new file mode 100644 index 000000000..e71b24f20 --- /dev/null +++ b/docs/reference/FacebookBatchResponse.md @@ -0,0 +1,76 @@ +# FacebookBatchResponse for the Facebook SDK for PHP + +Represents a batch response returned from the Graph API. + +## Facebook\FacebookBatchResponse + +After sending a batch request to the Graph API, the response will be returned in the form of a `Facebook\FacebookBatchResponse` entity. + +Usage: + +```php +$fb = new Facebook\Facebook(/* . . . */); +$requests = [ + $fb->request('GET', '/me'), + $fb->request('POST', '/me/feed', [/* */]), +]; + +// Send the batch request to Graph +try { + $batchResponse = $fb->sendBatchRequest($requests, '{access-token}'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +foreach ($batchResponse as $key => $response) { + if ($response->isError()) { + $error = $response->getThrownException(); + echo $key . ' error: ' . $error->getMessage(); + } else { + // Success + } +} + +var_dump($batchResponse); +// class Facebook\FacebookBatchResponse . . . +``` + +## Instance Methods + +Since the `Facebook\FacebookBatchResponse` is extended from the [`Facebook\FacebookResponse`](FacebookResponse.md) entity, all the methods are inherited. + +### getResponses() +```php +public array getResponses() +``` +Returns the array of [`Facebook\FacebookResponse`](FacebookResponse.md) entities that were returned from Graph. + +## Array Access + +Since `Facebook\FacebookBatchResponse` implements `\IteratorAggregate` and `\ArrayAccess`, the responses can be accessed via array syntax and can also be iterated over. + +```php +$requests = [ + 'foo' => $fb->request('GET', '/me'), + 'bar' => $fb->request('POST', '/me/feed', [/* */]), +]; +$batchResponse = $fb->sendBatchRequest($requests); + +foreach ($batchResponse as $key => $response) { + if ($response->isError()) { + $error = $response->getThrownException(); + echo $key . ' error: ' . $error->getMessage(); + } else { + // Success + } +} + +var_dump($batchResponse['foo']); +// class Facebook\FacebookResponse . . . +``` diff --git a/docs/reference/FacebookCanvasHelper.md b/docs/reference/FacebookCanvasHelper.md new file mode 100644 index 000000000..18624574c --- /dev/null +++ b/docs/reference/FacebookCanvasHelper.md @@ -0,0 +1,103 @@ +# Facebook\Helpers\FacebookCanvasHelper + +The `FacebookCanvasHelper` is used to obtain an access token or signed request when working within the context of an [app canvas](https://developers.facebook.com/docs/games/canvas). + +```php +Facebook\Helpers\FacebookCanvasHelper( Facebook\FacebookApp $facebookApp ) +``` + +## Usage + +If your app is loaded through Canvas, Facebook sends a POST request to your app with a signed request. This helper will handle validating and decrypting the signed request. + +```php +$fb = new Facebook\Facebook([/* */]); +$canvasHelper = $fb->getCanvasHelper(); +$signedRequest = $canvasHelper->getSignedRequest(); + +if ($signedRequest) { + $payload = $signedRequest->getPayload(); + var_dump($payload); +} +``` + +If a user has already authenticated your app, you can also obtain an access token. + +```php +$fb = new Facebook\Facebook([/* */]); +$canvasHelper = $fb->getCanvasHelper(); + +try { + $accessToken = $canvasHelper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); +} + +if (isset($accessToken)) { + // Logged in. +} +``` + +The `$accessToken` will be `null` if the signed request did not contain any OAuth 2.0 data to obtain the access token. + +## Instance Methods + +### __construct() +```php +public FacebookCanvasHelper __construct(FacebookApp $app, FacebookClient $client, $graphVersion = null) +``` +Upon instantiation, `FacebookCanvasHelper` validates and decrypts the signed request that was sent via POST if present. + +### getAccessToken() +```php +public Facebook\AccessToken|null getAccessToken() +``` +Checks the signed request for authentication data and tries to obtain an access token access token. + +### getUserId() +```php +public string|null getUserId() +``` +A convenience method for obtaining a user's ID from the signed request if present. This will only return the user's ID if a valid signed request can be obtained and decrypted and the user has already authorized the app. + +```php +$userId = $canvasHelper->getUserId(); + +if ($userId) { + // User is logged in +} +``` + +This is equivalent to accessing the user ID from the signed request entity. + +```php +$signedRequest = $canvasHelper->getSignedRequest(); + +if ($signedRequest) { + $userId = $signedRequest->getUserId(); + // OR + $userId = $signedRequest->get('user_id'); +} +``` + +### getAppData() +```php +public string|null getAppData() +``` +Gets the value that is set in the `app_data` property if present. + +### getSignedRequest() +```php +public Facebook\SignedRequest|null getSignedRequest() +``` +Returns the signed request as an instance of [`Facebook\SignedRequest`](SignedRequest.md) if present. + +### getRawSignedRequest() +```php +public string|null getRawSignedRequest() +``` +Returns the raw encoded signed request as a `string` if present in the POST variables or `null`. diff --git a/docs/reference/FacebookClient.md b/docs/reference/FacebookClient.md new file mode 100644 index 000000000..91e4be8c9 --- /dev/null +++ b/docs/reference/FacebookClient.md @@ -0,0 +1,72 @@ +# FacebookClient service class for the Facebook SDK for PHP + +The `Facebook\FacebookClient` service class juggles the dependencies needed to make requests to the Graph API. + +## Facebook\FacebookClient + +You most likely won't be working with the `Facebook\FacebookClient` service directly if you're using the `Facebook\Facebook` super service class, but if you have a highly customized environment, you might need to send requests with an instance of `Facebook\FacebookClient`. + +You can grab an instance of a `Facebook\FacebookClient` service, from the `Facebook\Facebook` super service class. + +```php +$fb = new Facebook\Facebook([/* */]); +$fbClient = $fb->getClient(); +``` + +Alternatively you could instantiate a new `Facebook\FacebookClient` service directly. + +```php +$fbClient = new Facebook\FacebookClient($httpClientHandler, $enableBeta = false); +``` + +The Graph API has a number of different base URL's based on what request you want to send. For example, if you wanted to send requests to the beta version of Graph, you'd need to send requests to [https://graph.beta.facebook.com](https://graph.beta.facebook.com) instead [https://graph.facebook.com](https://graph.facebook.com). And if you wanted to upload a video, that request would need to be sent to [https://graph-video.facebook.com](https://graph-video.facebook.com). + +The `Facebook\FacebookClient` service takes the guess-work out of managing those base URL's by automatically sending your requests to the proper URL. + +## Instance Methods + +### getHttpClientHandler() +```php +public Facebook\HttpClients\FacebookHttpClientInterface getHttpClientHandler() +``` +Returns the instance of `Facebook\HttpClients\FacebookHttpClientInterface` that the service is using. + +### setHttpClientHandler() +```php +public setHttpClientHandler(Facebook\HttpClients\FacebookHttpClientInterface $client) +``` +If you've coded your own HTTP client to the `Facebook\HttpClients\FacebookHttpClientInterface`, you can inject it into the service using this method. + +### enableBetaMode() +```php +public enableBetaMode(boolean $enable = true) +``` +Tells the service to send requests to the beta URL's which include [https://graph.beta.facebook.com](https://graph.beta.facebook.com) and [https://graph-video.beta.facebook.com](https://graph-video.beta.facebook.com). + +### sendRequest() +```php +public Facebook\FacebookResponse sendRequest(Facebook\FacebookRequest $request) +``` +Sends a non-batch request to Graph. + +Takes a [`Facebook\FacebookRequest`](FacebookRequest.md) and sends it to the Graph API in the proper `application/x-www-form-urlencoded` or `multipart/form-data` encoded format. + +Returns the response from Graph in the form of a [`Facebook\FacebookResponse`](FacebookResponse.md). + +If there was an error processing the request before sending, a [`Facebook\Exceptions\FacebookSDKException`](FacebookSDKException.md) will be thrown. + +If an error response from Graph was returned, a [`Facebook\Exceptions\FacebookResponseException`](FacebookResponseException.md) will be thrown. + +### sendBatchRequest() +```php +public Facebook\FacebookBatchResponse sendBatchRequest(Facebook\FacebookBatchRequest $batchRequest) +``` +Sends a batch request to Graph. + +Takes a [`Facebook\FacebookBatchRequest`](FacebookBatchRequest.md) and sends it to the Graph API in the proper `application/x-www-form-urlencoded` or `multipart/form-data` encoded format. + +Returns the response from Graph in the form of a [`Facebook\FacebookBatchResponse`](FacebookBatchResponse.md). + +If there was an error processing the request before sending, a [`Facebook\Exceptions\FacebookSDKException`](FacebookSDKException.md) will be thrown. + +If an error response from Graph was returned, a [`Facebook\Exceptions\FacebookResponseException`](FacebookResponseException.md) will be thrown. diff --git a/docs/reference/FacebookFile.md b/docs/reference/FacebookFile.md new file mode 100644 index 000000000..304699538 --- /dev/null +++ b/docs/reference/FacebookFile.md @@ -0,0 +1,61 @@ +# File Uploading with the Facebook SDK for PHP + +Uploading files to the Graph API is made a breeze with the Facebook SDK for PHP. + +## Facebook\FileUpload\FacebookFile(string $pathToFile, int $maxLength = -1, int $offset = -1) + +The `FacebookFile` entity represents a local or remote file to be uploaded with a request to Graph. + +There are two ways to instantiate a `FacebookFile` entity. One way is to instantiate it directly: + +```php +use Facebook\FileUpload\FacebookFile; + +$myFileToUpload = new FacebookFile('/path/to/file.jpg'); +``` + +Alternatively, you can use the `fileToUpload()` factory on the `Facebook\Facebook` super service to instantiate a new `FacebookFile` entity. + +```php +$fb = new Facebook\Facebook(/* . . . */); + +$myFileToUpload = $fb->fileToUpload('/path/to/file.jpg'); +``` + +Partial file uploads are possible using the `$maxLength` and `$offset` parameters which provide the same functionality as the `$maxlen` and `$offset` parameters on the [`stream_get_contents()` PHP function](http://php.net/stream_get_contents). + +## Usage + +The following example uploads a photo for a user. + +```php +$data = [ + 'message' => 'My awesome photo upload example.', + 'source' => $fb->fileToUpload('/path/to/photo.jpg'), + // Or you can provide a remote file location + //'source' => $fb->fileToUpload('https://example.com/photo.jpg'), +]; + +try { + $response = $fb->post('/me/photos', $data); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + echo 'Error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'Photo ID: ' . $graphNode['id']; +``` + +> **Note:** Although you can use `fileToUpload()` to upload a remote file, it is more efficient to just point the Graph request to the the remote file with the `url` param. + +```php +// Upload a remote photo for a user without using the FacebookFile entity +$data = [ + 'message' => 'A neat photo upload example. Neat.', + 'url' => 'https://example.com/photo.jpg', +]; + +$response = $fb->post('/me/photos', $data); +``` diff --git a/docs/reference/FacebookJavaScriptHelper.md b/docs/reference/FacebookJavaScriptHelper.md new file mode 100644 index 000000000..d9520c779 --- /dev/null +++ b/docs/reference/FacebookJavaScriptHelper.md @@ -0,0 +1,93 @@ +# Facebook\Helpers\FacebookJavaScriptHelper + +If you're using the [JavaScript SDK](https://developers.facebook.com/docs/javascript) on your site, information on the logged in user is stored in a cookie. Use the `FacebookJavaScriptHelper` to obtain an access token or signed request from the cookie. + +## Usage + +This helper will handle validating and decode the signed request from the cookie set by the JavaScript SDK. + +```php +$fb = new Facebook\Facebook([/* */]); +$jsHelper = $fb->getJavaScriptHelper(); +$signedRequest = $jsHelper->getSignedRequest(); + +if ($signedRequest) { + $payload = $signedRequest->getPayload(); + var_dump($payload); +} +``` + +If a user has already authenticated your app, you can also obtain an access token. + +```php +$fb = new Facebook\Facebook([/* */]); +$jsHelper = $fb->getJavaScriptHelper(); + +try { + $accessToken = $jsHelper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); +} + +if (isset($accessToken)) { + // Logged in. +} +``` + +You will likely want to make an Ajax request when the login state changes in the Facebook SDK for JavaScript. Information about that here: [FB.event.subscribe](https://developers.facebook.com/docs/reference/javascript/FB.getLoginStatus/#events) + +## Instance Methods + +### __construct() +```php +public FacebookJavaScriptHelper __construct(FacebookApp $app, FacebookClient $client, $graphVersion = null) +``` +Upon instantiation, `FacebookJavaScriptHelper` validates and decodes the signed request that exists in the cookie set by the JavaScript SDK if present. + +### getAccessToken() +```php +public Facebook\AccessToken|null getAccessToken( Facebook\FacebookClient $client ) +``` +Checks the signed request for authentication data and tries to obtain an access token access token. + +### getUserId() +```php +public string|null getUserId() +``` +A convenience method for obtaining a user's ID from the signed request if present. This will only return the user's ID if a valid signed request can be obtained and decoded and the user has already authorized the app. + +```php +$userId = $jsHelper->getUserId(); + +if ($userId) { + // User is logged in +} +``` + +This is equivalent to accessing the user ID from the signed request entity. + +```php +$signedRequest = $jsHelper->getSignedRequest(); + +if ($signedRequest) { + $userId = $signedRequest->getUserId(); + // OR + $userId = $signedRequest->get('user_id'); +} +``` + +### getSignedRequest() +```php +public Facebook\SignedRequest|null getSignedRequest() +``` +Returns the signed request as a [`Facebook\SignedRequest`](SignedRequest.md) entity if present. + +### getRawSignedRequest() +```php +public string|null getRawSignedRequest() +``` +Returns the raw encoded signed request as a `string` or `null`. diff --git a/docs/reference/FacebookPageTabHelper.md b/docs/reference/FacebookPageTabHelper.md new file mode 100644 index 000000000..3666a8cc6 --- /dev/null +++ b/docs/reference/FacebookPageTabHelper.md @@ -0,0 +1,59 @@ +# Facebook\Helpers\FacebookPageTabHelper + +Page tabs are similar to the context to app canvases but are treated slightly differently. Use the `FacebookPageTabHelper` to obtain an access token or signed request within the context of a page tab. + +## Usage + +The usage of the `FacebookPageTabHelper` is exactly the same as [`FacebookCanvasHelper`](FacebookCanvasHelper.md) with additional methods to obtain the `page` data from the signed request. + +```php +$fb = new Facebook\Facebook([/* */]); +$pageHelper = $fb->getPageTabHelper(); +$signedRequest = $pageHelper->getSignedRequest(); + +if ($signedRequest) { + $payload = $signedRequest->getPayload(); + var_dump($payload); +} +``` + +If a user has already authenticated your app, you can also obtain an access token. + +```php +$fb = new Facebook\Facebook([/* */]); +$pageHelper = $fb->getPageTabHelper(); + +try { + $accessToken = $pageHelper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); +} + +if (isset($accessToken)) { + // Logged in. +} +``` + +## Instance Methods + +### getPageData() +```php +public string|null getPageData($key, $default = null) +``` +Gets a value from the `page` property if present. + +### isAdmin() +```php +public boolean isAdmin() +``` +Returns `true` is the user has authenticated your app and is an admin of the parent page. + +### getPageId() +```php +public string|null getPageId() +``` +Returns the ID of the parent page if it can be obtained from the `page` property in the signed request. diff --git a/docs/reference/FacebookRedirectLoginHelper.md b/docs/reference/FacebookRedirectLoginHelper.md new file mode 100644 index 000000000..5639177f3 --- /dev/null +++ b/docs/reference/FacebookRedirectLoginHelper.md @@ -0,0 +1,130 @@ +# Facebook\Helpers\FacebookRedirectLoginHelper + +The most commonly used helper is the `FacebookRedirectLoginHelper` which allows you to obtain a user access token from a redirect using a "Log in with Facebook" link. + +## Usage + +Facebook Login is achieved via OAuth 2.0. But you don't really have to know much about OAuth 2.0 since the SDK for PHP does all the heavy lifting for you. + +### Obtaining an instance of FacebookRedirectLoginHelper + +You can obtain an instance of the `FacebookRedirectLoginHelper` from the `getRedirectLoginHelper()` method on the `Facebook\Facebook` service. + +```php +$fb = new Facebook\Facebook([/* . . . */]); + +$helper = $fb->getRedirectLoginHelper(); +``` + +### Login with Facebook + +The basic login flow goes like this: + +1. A user is presented with a unique "log in with Facebook" link that was generated by the `FacebookRedirectLoginHelper`. +2. Once the user clicks on the link they will be taken to Facebook's website and presented with an app authorization modal. +3. After the user confirms or denies the app authorization, they will be redirected to a specific callback URL on your website. +4. In your callback URL you can analyse the response to obtain a user access token or display an error if the user denied the request. + +```php +# login.php +$fb = new Facebook\Facebook([/* . . . */]); + +$helper = $fb->getRedirectLoginHelper(); +$permissions = ['email', 'user_likes']; // optional +$loginUrl = $helper->getLoginUrl('http://{your-website}/login-callback.php', $permissions); + +echo 'Log in with Facebook!'; +``` + +> **Warning:** The `FacebookRedirectLoginHelper` makes use of sessions to store a [CSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) value. You need to make sure you have sessions enabled before invoking the `getLoginUrl()` method. This is usually done automatically in most web frameworks, but if you're not using a web framework you can add [`session_start();`](http://php.net/session_start) to the top of your `login.php` & `login-callback.php` scripts. You can overwrite the default session handling - see [extensibility points](#extensibility-points) below. + +Then, in your callback page (at the redirect url) when Facebook sends the user back: + +```php +# login-callback.php +$fb = new Facebook\Facebook([/* . . . */]); + +$helper = $fb->getRedirectLoginHelper(); +try { + $accessToken = $helper->getAccessToken(); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +if (isset($accessToken)) { + // Logged in! + $_SESSION['facebook_access_token'] = (string) $accessToken; + + // Now you can redirect to another page and use the + // access token from $_SESSION['facebook_access_token'] +} elseif ($helper->getError()) { + // The user denied the request + exit; +} +``` + +## Instance Methods + +### getLoginUrl() +```php +public string getLoginUrl(string $redirectUrl, array $scope = [], string $separator = '&') +``` +Generates an authorization URL to ask a user for access to their profile on behalf of your app. + +#### Arguments +- `$redirectUrl` (_Required_) The callback URL that the user will be redirected to after being presented with the app authorization modal. +- `$scope` (_Optional_) A numeric array of permissions to ask the user for. +- `$separator` (_Optional_) The URL parameter separator. When working with XML documents, you can set this to `&` for example. + +### getReRequestUrl() +```php +public string getReRequestUrl(string $redirectUrl, array $scope = [], string $separator = '&') +``` +Generates a URL to rerequest permissions from a user. The arguments are the same as the `getLoginUrl()` method above. + +### getReAuthenticationUrl() +```php +public string getReAuthenticationUrl(string $redirectUrl, array $scope = [], string $separator = '&') +``` +Generates a URL to ask the user to reauthenticate. The arguments are the same as the `getLoginUrl()` method above. + +### getLogoutUrl() +```php +public string getLogoutUrl(string $accessToken, string $next, string $separator = '&') +``` +Generates the URL log a user out of Facebook. This will throw an `FacebookSDKException` if you try to use an app access token. + +### getAccessToken() +```php +public Facebook\Authentication\AccessToken|null getAccessToken(string $redirectUrl = null) +``` +Attempts to obtain an access token from an authorization code. This method will make a request to the Graph API and return a response. If there was an error in that process a `FacebookSDKException` will be thrown. A `FacebookSDKException` will also be thrown if the CSRF validation fails. + +If no authorization code could be found from the `code` param in the URL, this method will return `null`. + +#### Arguments +- `$redirectUrl` (_Optional_) The URL of the callback that the user is currently on. This should be the same as the one used when creating the login URL. If no URL is provided, it will be detected automatically. + +## Extensibility Points + +The `FacebookRedirectLoginHelper` has to orchestrate a number of components from the hosting environment to make the OAuth 2.0 authorization process as easy as possible to integrate. Out of the box it auto-detects all the things it needs, but sometimes you'll want to control these components. + +### Sessions (persistent data) + +In order to prevent [CSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery)'s, a unique value is generated with each login link and stored in a session. + +Most modern web frameworks have custom session handlers that allow you to manage your sessions with something other than the default flat-file storage. You can integrate your framework's custom session handling by coding to the [`PersistentDataInterface`](PersistentDataInterface.md). + +### CSPRNG + +The CSRF value that the `getLoginUrl()`, `getReRequestUrl()`, and `getReAuthenticationUrl()` methods generate are all _cryptographically secure_ random strings. PHP's native support of CSPRNG's is spotty at best. The PHP SDK goes to great lengths to to detect a suitable CSPRNG but in rare cases, it might not find a suitable one. The [`PseudoRandomStringGeneratorInterface`](PseudoRandomStringGeneratorInterface.md) allows you to inject your own custom CSPRNG. + +### URL detection + +In order to not make you pass the callback URL to the `getAccessToken()` method, the SDK will do its best to detect the callback's URL for you. Most modern web frameworks have URL detection built-in. You can code your specific web framework's URL detection logic by coding to the [`UrlDetectionInterface`](UrlDetectionInterface.md). diff --git a/docs/reference/FacebookRequest.md b/docs/reference/FacebookRequest.md new file mode 100644 index 000000000..cc8b7b203 --- /dev/null +++ b/docs/reference/FacebookRequest.md @@ -0,0 +1,180 @@ +# FacebookRequest for the Facebook SDK for PHP + +Represents a request that will be sent to the Graph API. + +## Facebook\FacebookRequest + +You can instantiate a new `FacebookRequest` entity directly by sending the arguments to the constructor. + +```php +use Facebook\FacebookRequest; + +$request = new FacebookRequest( + Facebook\FacebookApp $app, + string $accessToken, + string $method, + string $endpoint, + array $params, + string $eTag, + string $graphVersion +); +``` + +Alternatively, you can make use of the [`request()` factory provided by `Facebook\Facebook`](Facebook.md#request) to create new `FacebookRequest` instances. + +The `FacebookRequest` entity does not actually make any calls to the Graph API, but instead just represents a request that can be sent to the Graph API later. This is most useful for making batch requests using [`Facebook\Facebook::sendBatchRequest()`](Facebook.md#sendbatchrequest) or [`Facebook\FacebookClient::sendBatchRequest()`](FacebookClient.md#sendbatchrequest). + +Usage: + +```php +$fbApp = new Facebook\FacebookApp('{app-id}', '{app-secret}'); +$request = new Facebook\FacebookRequest($fbApp, '{access-token}', 'GET', '/me'); + +// OR + +$fb = new Facebook\Facebook(/* . . . */); +$request = $fb->request('GET', '/me'); + +// Send the request to Graph +try { + $response = $fb->getClient()->sendRequest($request); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'User name: ' . $graphNode['name']; +``` + +## Instance Methods + +### setAccessToken() +```php +public setAccessToken(string|Facebook\AccessToken $accessToken) +``` +Sets the access token to be used for the request. + +### getAccessToken() +```php +public string getAccessToken() +``` +Returns the access token to be used for the request in the form of a string. + +### setApp() +```php +public setApp(Facebook\FacebookApp $app) +``` +Sets the [`Facebook\FacebookApp`](FacebookApp.md) entity used with this request. + +### getApp() +```php +public Facebook\FacebookApp getApp() +``` +Returns the [`Facebook\FacebookApp`](FacebookApp.md) entity used with this request. + +### getAppSecretProof() +```php +public string getAppSecretProof() +``` +Returns the [app secret proof](https://developers.facebook.com/docs/graph-api/securing-requests/#appsecret_proof) to sign the request. + +### setMethod() +```php +public setMethod(string $method) +``` +Sets the HTTP verb to use for the request. + +### getMethod() +```php +public string setMethod() +``` +Returns the HTTP verb to use for the request. + +### setEndpoint() +```php +public setEndpoint(string $endpoint) +``` +Sets the Graph URL endpoint to be used with the request. The endpoint must be excluding the host name and Graph version number prefix. + +```php +$request->setEndpoint('/me'); +``` + +### getEndpoint() +```php +public string getEndpoint() +``` +Returns the Graph URL endpoint to be used with the request. + +### setHeaders() +```php +public setHeaders(array $headers) +``` +Sets additional request headers to be use with the request. The supplied headers will be merged with the existing headers. The headers should be sent as an associative array with the key being the header name and the value being the header value. + +```php +$request->setHeaders([ + 'X-foo-header' => 'Something', +]); +``` + +### getHeaders() +```php +public array getHeaders() +``` +Returns the request headers that will be sent with the request. The eTag headers `If-None-Match` are appended automatically. + +### setETag() +```php +public setETag(string $eTag) +``` +Sets the eTag that will be using for matching the `If-None-Match` header. + +### setParams() +```php +public setParams(array $params) +``` +For `GET` requests, the array of params will be converted to a query string and appended to the URL. + +```php +$request->setParams([ + 'foo' => 'bar', + 'limit' => 10, +]); +// /endpoint?foo=bar&limit=10 +``` + +For `POST` requests, the array of params will be sent in the `POST` body encoded as `application/x-www-form-urlencoded` for most request. If the request includes a file upload the params will be encoded as `multipart/form-data`. + +### getParams() +```php +public array getParams() +``` +Returns an array of params to be sent with the request. The `access_token` and `appsecret_proof` params will be automatically appended to the array of params. + +### getGraphVersion() +```php +public string getGraphVersion() +``` +Returns the Graph version prefix to be used with the request. + +### getUrl() +```php +public string getUrl() +``` +Returns the endpoint of the Graph URL for the request. This will include the Graph version prefix but will not include the host name. The host name is determined after the request is sent to [`Facebook\FacebookClient`](FacebookClient.md). + +```php +$fb = new Facebook\Facebook(/* . . . */); +$request = $fb->request('GET', '/me', ['fields' => 'id,name']); + +$url = $request->getUrl(); +// /v2.10/me?fields=id,name&access_token=token&appsecret_proof=proof +``` diff --git a/docs/reference/FacebookResponse.md b/docs/reference/FacebookResponse.md new file mode 100644 index 000000000..8a60a76ce --- /dev/null +++ b/docs/reference/FacebookResponse.md @@ -0,0 +1,161 @@ +# FacebookResponse for the Facebook SDK for PHP + +Represents a response from the Graph API. + +## Facebook\FacebookResponse + +After sending a request to the Graph API, the response will be returned in the form of a `Facebook\FacebookResponse` entity. + +Usage: + +```php +$fb = new Facebook\Facebook(/* . . . */); + +// Send the request to Graph +try { + $response = $fb->get('/me'); +} catch(Facebook\Exceptions\FacebookResponseException $e) { + // When Graph returns an error + echo 'Graph returned an error: ' . $e->getMessage(); + exit; +} catch(Facebook\Exceptions\FacebookSDKException $e) { + // When validation fails or other local issues + echo 'Facebook SDK returned an error: ' . $e->getMessage(); + exit; +} + +var_dump($response); +// class Facebook\FacebookResponse . . . +``` + +## Instance Methods + +### getRequest() +```php +public Facebook\FacebookRequest getRequest() +``` +Returns the original [`Facebook\FacebookRequest`](FacebookRequest.md) entity that was used to solicit this response. + +### getAccessToken() +```php +public string getAccessToken() +``` +Returns the access token that was used for the original request in the form of a string. + +### getApp() +```php +public Facebook\FacebookApp getApp() +``` +Returns the [`Facebook\FacebookApp`](FacebookApp.md) entity that was used with the original request. + +### getHttpStatusCode() +```php +public int getHttpStatusCode() +``` +Returns the HTTP response code for this response. + +### getHeaders() +```php +public array getHeaders() +``` +Returns the response headers that were returned. + +### getBody() +```php +public string getBody() +``` +Returns the raw, unparsed body of the response as a string. + +### getDecodedBody() +```php +public array getDecodedBody() +``` +Returns the parsed body of the response as an array. + +### getAppSecretProof() +```php +public string getAppSecretProof() +``` +Returns the original [app secret proof](https://developers.facebook.com/docs/graph-api/securing-requests/#appsecret_proof) that was used with the original request. + +### getETag() +```php +public string getETag() +``` +Returns the `ETag` response header if it exists. If the header does not exist in the response headers, `null` will be returned instead. + +### getGraphVersion() +```php +public string getGraphVersion() +``` +Returns the Graph version that was used by returning the value from the `Facebook-API-Version` response header if it exists. If the header does not exist in the response headers, `null` will be returned instead. + +### isError() +```php +public boolean isError() +``` +If the Graph API returned an error response `isError()` will return `true`. If a successful response was returned, `isError()` will return `false`. + +### throwException() +```php +public throwException() +``` +Throws the [`Facebook\Exceptions\FacebookResponseException`](FacebookResponseException.md) that was generated by an error response from Graph. + +### getThrownException() +```php +public Facebook\Exceptions\FacebookResponseException getThrownException() +``` +Returns the [`Facebook\Exceptions\FacebookResponseException`](FacebookResponseException.md) that was generated by an error response from Graph. This is mainly useful for dealing with [responses to batch requests](FacebookBatchResponse.md). + +### getGraphNode() +```php +public Facebook\GraphNodes\GraphNode getGraphNode() +``` +Returns the response data in the form of a [`Facebook\GraphNodes\GraphNode`](GraphNode.md) collection. + +### getGraphAlbum() +```php +public Facebook\GraphNodes\GraphAlbum getGraphAlbum() +``` +Returns the response data in the form of a [`Facebook\GraphNodes\GraphAlbum`](GraphNode.md#graphalbum-instance-methods) collection. + +### getGraphPage() +```php +public Facebook\GraphNodes\GraphPage getGraphPage() +``` +Returns the response data in the form of a [`Facebook\GraphNodes\GraphPage`](GraphNode.md#graphpage-instance-methods) collection. + +### getGraphSessionInfo() +```php +public Facebook\GraphNodes\GraphSessionInfo getGraphSessionInfo() +``` +Returns the response data in the form of a [`Facebook\GraphNodes\GraphSessionInfo`](GraphNode.md) collection. + +### getGraphUser() +```php +public Facebook\GraphNodes\GraphUser getGraphUser() +``` +Returns the response data in the form of a [`Facebook\GraphNodes\GraphUser`](GraphNode.md#graphuser-instance-methods) collection. + +### getGraphEdge() +```php +public Facebook\GraphNodes\GraphEdge getGraphEdge( + string|null $subclassName, + boolean $auto_prefix) +``` +Returns the response data in the form of a [`Facebook\GraphNodes\GraphEdge`](GraphEdge.md) collection. + +`$subclassName` +The `Facebook\GraphNodes\GraphNode` subclass to cast list items to. If none is provided, default is `Facebook\GraphNodes\GraphNode`. + +`$auto_prefix` +Toggle to auto-prefix the subclass name. If none is provided, default is `true`. + +```php +$res = $fb->get('/{facebook-page}/events', '{access-token}'); +$events = $res->getGraphEdge("GraphEvent"); +foreach ($events as $event) { + // . . . +} +``` diff --git a/docs/reference/FacebookResponseException.md b/docs/reference/FacebookResponseException.md new file mode 100644 index 000000000..6fe777cbf --- /dev/null +++ b/docs/reference/FacebookResponseException.md @@ -0,0 +1,57 @@ +# FacebookResponseException for the Facebook SDK for PHP + +Represents an error response from the Graph API. + +## Facebook\Exceptions\FacebookResponseException + +Whenever a `FacebookResponseException` is thrown, you can access it's previous exception with the `getPrevious()` method to get more information on the specific type of error response that the Graph API returned. + +```php +try { + // Some request to the Graph API +} catch (Facebook\Exceptions\FacebookResponseException $e) { + echo 'Message: ' . $e->getMessage(); + $previousException = $e->getPrevious(); + // Do some further processing on $previousException + exit; +} +``` + +| Class name | Description | +| ------------- | ------------- | +| `Facebook\Exceptions\FacebookAuthenticationException` | Thrown when Graph returns an authentication error. | +| `Facebook\Exceptions\FacebookAuthorizationException` | Thrown when Graph returns a user permissions error. | +| `Facebook\Exceptions\FacebookClientException` | Thrown when Graph returns a duplicate post error. | +| `Facebook\Exceptions\FacebookOtherException` | Thrown when Graph returns an error that is unknown to the SDK. | +| `Facebook\Exceptions\FacebookServerException` | Thrown when Graph returns a server error. | +| `Facebook\Exceptions\FacebookThrottleException` | Thrown when Graph returns a throttle error. | + +These exceptions are derived from the [error responses from the Graph API](https://developers.facebook.com/docs/graph-api/using-graph-api#errors). + +## Instance Methods + +`FacebookResponseException` extends from the base `\Exception` class, so `getCode()` and `getMessage()` are available by default. + +### getHttpStatusCode +`getHttpStatusCode()` +Returns the HTTP status code returned with this exception. + +### getSubErrorCode +`getSubErrorCode()` +Returns the numeric sub-error code returned from the Graph API. + +### getErrorType +`getErrorType()` +Returns the type of error as a string. + +### getResponseData +`getResponseData()` +Returns the decoded response body used to create the exception as an array. + +### getRawResponse +`getRawResponse()` +Returns the raw response body used to create the exception as a string. + +### getResponse +`getResponse()` +Returns the `FacebookResponse` entity which represents the HTTP response. diff --git a/docs/reference/FacebookSDKException.md b/docs/reference/FacebookSDKException.md new file mode 100644 index 000000000..2166ec148 --- /dev/null +++ b/docs/reference/FacebookSDKException.md @@ -0,0 +1,13 @@ +# FacebookSDKException for the Facebook SDK for PHP + +Represents an exception thrown by the SDK. + +## Facebook\Exceptions\FacebookSDKException + +A `FacebookSDKException` is thrown when something goes wrong. For example if an invalid signed request is sent to the `Facebook\SignedRequest` entity, it will throw an `FacebookSDKException`. + +When an error response is returned from the Graph API, it will be thrown as a `FacebookSDKException` subtype called a [Facebook\Exceptions\FacebookResponseException](FacebookResponseException.md). + +## Instance Methods + +`FacebookSDKException` extends from the base `\Exception` class, so `getCode()` and `getMessage()` are available by default. diff --git a/docs/reference/FacebookVideo.md b/docs/reference/FacebookVideo.md new file mode 100644 index 000000000..df7e424c1 --- /dev/null +++ b/docs/reference/FacebookVideo.md @@ -0,0 +1,68 @@ +# Video Uploading with the Facebook SDK for PHP + +Uploading video files to the Graph API is made a breeze with the SDK for PHP. + +## Facebook\FileUpload\FacebookVideo(string $pathToVideoFile, int $maxLength = -1, int $offset = -1) + +The `FacebookVideo` entity represents a local or remote video file to be uploaded with a request to Graph. + +There are two ways to instantiate a `FacebookVideo` entity. One way is to instantiate it directly: + +```php +use Facebook\FileUpload\FacebookVideo; + +$myVideoFileToUpload = new FacebookVideo('/path/to/video-file.mp4'); +``` + +Alternatively, you can use the `videoToUpload()` factory on the `Facebook\Facebook` super service to instantiate a new `FacebookVideo` entity. + +```php +$fb = new Facebook\Facebook(/* . . . */); + +$myVideoFileToUpload = $fb->videoToUpload('/path/to/video-file.mp4'), +``` + +Partial file uploads are possible using the `$maxLength` and `$offset` parameters which provide the same functionality as the `$maxlen` and `$offset` parameters on the [`stream_get_contents()` PHP function](http://php.net/stream_get_contents). + +## Usage + +In Graph v2.3, functionality was added to [upload video files in chunks](https://developers.facebook.com/docs/graph-api/video-uploads#resumable). The PHP SDK provides a handy API to easily upload video files in chunks via the [`uploadVideo()` method](Facebook.md#uploadvideo). + +```php +// Upload a video for a user (chunked) +$data = [ + 'title' => 'My awesome video', + 'description' => 'More info about my awesome video.', +]; + +try { + $response = $fb->uploadVideo('me', '/path/to/video.mp4', $data, '{user-access-token}'); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + echo 'Error: ' . $e->getMessage(); + exit; +} + +echo 'Video ID: ' . $response['video_id']; +``` + +For versions of Graph before v2.3, videos had to be uploaded in one request. + +```php +// Upload a video for a user +$data = [ + 'title' => 'My awesome video', + 'description' => 'More info about my awesome video.', + 'source' => $fb->videoToUpload('/path/to/video.mp4'), +]; + +try { + $response = $fb->post('/me/videos', $data); +} catch(Facebook\Exceptions\FacebookSDKException $e) { + echo 'Error: ' . $e->getMessage(); + exit; +} + +$graphNode = $response->getGraphNode(); + +echo 'Video ID: ' . $graphNode['id']; +``` diff --git a/docs/reference/GraphEdge.md b/docs/reference/GraphEdge.md new file mode 100644 index 000000000..30508d37b --- /dev/null +++ b/docs/reference/GraphEdge.md @@ -0,0 +1,114 @@ +# GraphEdge for the Facebook SDK for PHP + +When a list of nodes is returned from a Graph request, it can be cast as a `GraphEdge` which provides convenient ways of interacting with the data which includes pagination. + +## Facebook\GraphNodes\GraphEdge + +You can grab a `GraphEdge` from a response from Graph. + +```php +$graphEdge = $request->getGraphEdge(); +``` + +Usage: + +```php +// Iterate over all the GraphNode's returned from the edge +foreach ($graphEdge as $graphNode) { + // . . . +} +``` + +## Pagination + +With the help of the `Facebook\Facebook` super service class, the `GraphEdge` collection can grab the next and previous sets of data. + +```php +$albumsEdge = $response->getGraphEdge(); + +// Get the next page of results +$nextPageOfAlbums = $fb->next($albumsEdge); +// Or the previous page of results +$previousPageOfAlbums = $fb->previous($previousOfAlbums); +``` + +When the next or previous page returns no results, `$fb->next()` will return `null`. + +## Deep Pagination + +Sometimes Graph will return a list of nodes within a node. Paginating on these sub lists can be non-trivial. Fortunately, the `GraphEdge` collection takes the guesswork out and allows you to paginate deeply within a `GraphEdge`. + +The following example paginates over the first 5 pages of a list of Facebook pages. For each page it paginates over all the likes for that page. + +```php +$pagesEdge = $response->getGraphEdge(); +// Only grab 5 pages +$maxPages = 5; +$pageCount = 0; + +do { + echo '

Page #' . $pageCount . ':

' . "\n\n"; + + foreach ($pagesEdge as $page) { + var_dump($page->asArray()); + + $likes = $page['likes']; + do { + echo '

Likes:

' . "\n\n"; + var_dump($likes->asArray()); + } while ($likes = $fb->next($likes)); + } + $pageCount++; +} while ($pageCount < $maxPages && $pagesEdge = $fb->next($pagesEdge)); +``` + +## Method Reference + +### getMetaData() +```php +public array getMetaData() +``` + +Sometimes Graph will return additional data associated with an edge. You can access this raw data as an array with `getMetaData()`. + +```php +$metaData = $graphEdge->getMetaData(); +``` + +### getNextCursor() +```php +public string|null getNextCursor() +``` + +Returns the `$.paging.cursors.after` value if it exists or `null` if it does not exist. Since cursors are sort of like bookmarks for paginating over an edge, it is sometimes handy to store the last cursor used so that you can revisit the exact position at a later time. + +```php +$nextCursor = $graphEdge->getNextCursor(); +// Returns: MMAyDDM5NjA0OTEyMDc0OTM= +``` + +### getPreviousCursor() +```php +public string|null getPreviousCursor() +``` + +Returns the `$.paging.cursors.before` value if it exists or `null` if it does not exist. + +```php +$previousCursor = $graphEdge->getPreviousCursor(); +// Returns: ODOxMTUzMjQzNTg5zzU5 +``` + +### getTotalCount() +```php +public int|null getTotalCount() +``` + +Some endpoints and edges of Graph support a summary of data. If the `summary=true` modifier was sent with a request on a supported endpoint or edge, Graph will return the total count of results in the meta data under `$.summary.total_count`. `getTotalCount()` will return that value or `null` if it does not exist. + +```php +$response = $fb->get('/{post-id}/likes?summary=true'); +$likesEdge = $response->getGraphEdge(); +$totalCount = $likesEdge->getTotalCount(); +// Returns: 10 +``` diff --git a/docs/reference/GraphNode.md b/docs/reference/GraphNode.md new file mode 100644 index 000000000..a60940247 --- /dev/null +++ b/docs/reference/GraphNode.md @@ -0,0 +1,591 @@ +# GraphNode for the Facebook SDK for PHP + +A `Facebook\GraphNodes\GraphNode` is a collection that represents a node returned by the Graph API. + +## Facebook\GraphNodes\GraphNode + +This base class has several subclasses: + +[__GraphUser__](#graphuser-instance-methods) +[__GraphPage__](#graphpage-instance-methods) +[__GraphAlbum__](#graphalbum-instance-methods) +[__GraphLocation__](#graphlocation-instance-methods) +[__GraphPicture__](#graphpicture-instance-methods) +[__GraphAchievement__](#graphachievement-instance-methods) + +`GraphNode`s are obtained from a [`Facebook\FacebookResponse`](FacebookResponse.md) object which represents an HTTP response from the Graph API. + +Usage: + +```php +$fb = new Facebook\Facebook(\* *\); +// Returns a `Facebook\FacebookResponse` object +$response = $fb->get('/something'); + +// Get the base class GraphNode from the response +$graphNode = $response->getGraphNode(); + +// Get the response typed as a GraphUser +$user = $response->getGraphUser(); + +// Get the response typed as a GraphPage +$page = $response->getGraphPage(); + +// User example +echo $graphNode->getField('name'); // From GraphNode +echo $user->getName(); // From GraphUser + +// Location example +echo $graphNode->getField('country'); // From GraphNode +echo $location->getCountry(); // From GraphLocation +``` + +## SPL Libraries + +The `GraphNode` collection and its subclasses implement several [SPL](http://php.net/manual/en/book.spl.php) libraries and [predefined PHP interfaces and classes](http://php.net/manual/en/reserved.interfaces.php) which make it convenient to work with the object in PHP. The supported libraries are `ArrayAccess`, `ArrayIterator`, `Countable`, and `IteratorAggregate`. + +All of the following operations are possible on a `GraphNode`. + +```php +$graphNode = $response->getGraphNode(); + +// Array access +$id = $graphNode['id']; + +// Iteration +foreach ($graphNode as $key => $value) { + // . . . +} + +// Counting +$total = count($graphNode); +``` + +## GraphNode Instance Methods + +### asArray +`asArray()` +Returns the raw representation (associative arrays, nested) of the node's underlying data. + +### asJson +`asJson()` +Returns the data as a JSON string. + +### getField +`getField(string $name, string $default = 'foo')` +Gets the value from the field of a Graph node. If the value is a scalar (string, number, etc.) it will be returned. If it's an associative array, it will be returned as a GraphNode. + +The second argument lets you define a default value to return if the field doesn't exist. + +### getFieldNames +`getFieldNames()` +Returns an array with the names of all fields present on the graph node. + +### map +`map(Closure $callback)` +Provides a way to map over the data within the collection just like `array_map()`. + +## GraphUser Instance Methods + +The `GraphUser` collection represents a [User](https://developers.facebook.com/docs/graph-api/reference/user) Graph node. + +### Auto-cast properties + +The following properties on the `GraphUser` collection will get automatically cast as `GraphNode` subtypes: + +| Property | GraphNode subtype | +| ------------- | ------------- | +| `hometown` | [`Facebook\GraphNodes\GraphPage`](#graphpage-instance-methods) | +| `location` | [`Facebook\GraphNodes\GraphPage`](#graphpage-instance-methods) | +| `significant_other` | [`Facebook\GraphNodes\GraphUser`](#graphuser-instance-methods) | + +All getter methods return `null` if the property does not exist on the node. + +### getId() +```php +public string|null getId() +``` +Returns the `id` property for the user as a string if present. + +### getName() +```php +public string|null getName() +``` +Returns the `name` property for the user as a string if present. + +### getFirstName() +```php +public string|null getFirstName() +``` +Returns the `first_name` property for the user as a string if present. + +### getMiddleName() +```php +public string|null getMiddleName() +``` +Returns the `middle_name` property for the user as a string if present. + +### getLastName() +```php +public string|null getLastName() +``` +Returns the `last_name` property for the user as a string if present. + +### getLink() +```php +public string|null getLink() +``` +Returns the `link` property for the user as a string if present. + +### getBirthday() +```php +public \Facebook\GraphNodes\Birthday|null getBirthday() +``` +Returns the `birthday` property for the user as a [`Facebook\GraphNodes\Birthday`](Birthday.md) if present. + +### getLocation() +```php +public Facebook\GraphNodes\GraphPage|null getLocation() +``` +Returns the `location` property for the user as a `Facebook\GraphNodes\GraphPage` if present. + +### getHometown() +```php +public Facebook\GraphNodes\GraphPage|null getHometown() +``` +Returns the `hometown` property for the user as a `Facebook\GraphNodes\GraphPage` if present. + +### getSignificantOther() +```php +public Facebook\GraphNodes\GraphUser|null getSignificantOther() +``` +Returns the `significant_other` property for the user as a `Facebook\GraphNodes\GraphUser` if present. + +## GraphPage Instance Methods + +The `GraphPage` collection represents a [Page](https://developers.facebook.com/docs/graph-api/reference/page) Graph node. + +### Auto-cast properties + +The following properties on the `GraphPage` collection will get automatically cast as `GraphNode` subtypes: + +| Property | GraphNode subtype | +| ------------- | ------------- | +| `best_page` | [`Facebook\GraphNodes\GraphPage`](#graphpage-instance-methods) | +| `global_brand_parent_page` | [`Facebook\GraphNodes\GraphPage`](graph#page-instance-methods) | +| `location` | [`Facebook\GraphNodes\GraphLocation`](#graphlocation-instance-methods) | + +All getter methods return `null` if the property does not exist on the node. + +### getId() +```php +public string|null getId() +``` +Returns the `id` property for the page as a string if present. + +### getName() +```php +public string|null getName() +``` +Returns the `name` property for the page as a string if present. + +### getCategory() +```php +public string|null getCategory() +``` +Returns the `category` property for the page as a string if present. + +### getBestPage() +```php +public Facebook\GraphNodes\GraphPage|null getBestPage() +``` +Returns the `best_page` property for the page as a `Facebook\GraphNodes\GraphPage` if present. + +### getGlobalBrandParentPage() +```php +public Facebook\GraphNodes\GraphPage|null getGlobalBrandParentPage() +``` +Returns the `global_brand_parent_page` property for the page as a `Facebook\GraphNodes\GraphPage` if present. + +### getLocation() +```php +public Facebook\GraphNodes\GraphLocation|null getLocation() +``` +Returns the `location` property for the page as a `Facebook\GraphNodes\GraphLocation` if present. + +### getAccessToken() +```php +public string|null getAccessToken() +``` +Returns the `access_token` property for the page if present. (Only available in the `/me/accounts` context.) + +### getPerms() +```php +public array|null getAccessToken() +``` +Returns the `perms` property for the page as an `array` if present. (Only available in the `/me/accounts` context.) + +## GraphAlbum Instance Methods + +The `GraphAlbum` collection represents an [Album](https://developers.facebook.com/docs/graph-api/reference/album) Graph node. + +### Auto-cast properties + +The following properties on the `GraphAlbum` collection will get automatically cast as `GraphNode` subtypes: + +| Property | GraphNode subtype | +| ------------- | ------------- | +| `from` | [`Facebook\GraphNodes\GraphUser`](#graphuser-instance-methods) | +| `place` | [`Facebook\GraphNodes\GraphPage`](#graphpage-instance-methods) | + +All getter methods return `null` if the property does not exist on the node. + +### getId() +```php +public string|null getId() +``` +Returns the `id` property for the album as a string if present. + +### getName() +```php +public string|null getName() +``` +Returns the `name` property for the album as a string if present. + +### getCanUpload() +```php +public boolean|null getCanUpload() +``` +Returns the `can_upload` property for the album as a boolean if present. + +### getCount() +```php +public int|null getCount() +``` +Returns the `count` property for the album as an integer if present. + +### getCoverPhoto() +```php +public string|null getCoverPhoto() +``` +Returns the `cover_photo` property for the album as a string if present. + +### getCreatedTime() +```php +public \DateTime|null getCreatedTime() +``` +Returns the `created_time` property for the album as a `\DateTime` if present. + +### getUpdatedTime() +```php +public \DateTime|null getUpdatedTime() +``` +Returns the `updated_time` property for the album as a `\DateTime` if present. + +### getDescription() +```php +public string|null getDescription() +``` +Returns the `description` property for the album as a string if present. + +### getFrom() +```php +public Facebook\GraphNodes\GraphUser|null getFrom() +``` +Returns the `from` property for the album as a `Facebook\GraphNodes\GraphUser` if present. + +### getPlace() +```php +public Facebook\GraphNodes\GraphPage|null getPlace() +``` +Returns the `place` property for the album as a `Facebook\GraphNodes\GraphPage` if present. + +### getLink() +```php +public string|null getLink() +``` +Returns the `link` property for the album as a string if present. + +### getLocation() +```php +public Facebook\GraphNodes\GraphNode|string|null getLocation() +``` +Returns the `location` property for the album as a `Facebook\GraphNodes\GraphNode` or string if present. + +### getPrivacy() +```php +public string|null getPrivacy() +``` +Returns the `privacy` property for the album as a string if present. + +### getType() +```php +public string|null getType() +``` +Returns the `type` property for the album as a string (`profile`, `mobile`, `wall`, `normal` or `album`) if present. + +## GraphLocation Instance Methods + +All getter methods return `null` if the property does not exist on the node. + +### getStreet() +```php +public string|null getStreet() +``` +Returns the `street` property for the location as a string if present. + +### getCity() +```php +public string|null getCity() +``` +Returns the `city` property for the location as a string if present. + +### getCountry() +```php +public string|null getCountry() +``` +Returns the `country` property for the location as a string if present. + +### getZip() +```php +public string|null getZip() +``` +Returns the `zip` property for the location as a string if present. + +### getLatitude() +```php +public float|null getLatitude() +``` +Returns the `latitude` property for the location as a float if present. + +### getLongitude() +```php +public float|null getLongitude() +``` +Returns the `longitude` property for the location as a float if present. + +## GraphPicture Instance Methods + +All getter methods return `null` if the property does not exist on the node. + +### getUrl() +```php +public string|null getUrl() +``` +Returns the `url` property for the picture as a string if present. + +## GraphAchievement Instance Methods + +All getter methods return `null` if the property does not exist on the node. + +### getId() +```php +public string|null getId() +``` +Returns the `id` property for the achievement as a string if present. + +## GraphEvent Instance Methods + +All getter methods return `null` if the property does not exist on the node. + +### getId() +```php +public string|null getId() +``` +Returns the `id` property (The event ID) for the event as a string if present. + +### getCover() +```php +public GraphCoverPhoto|null getCover() +``` +Returns the `cover` property (Cover picture) for the event as a GraphCoverPhoto if present. + +### getDescription() +```php +public string|null getDescription() +``` +Returns the `description` property (Long-form description) for the event as a string if present. + +### getEndTime() +```php +public DateTime|null getEndTime() +``` +Returns the `end_time` property (End time, if one has been set) for the event as a DateTime if present. + +### getIsDateOnly() +```php +public bool|null getIsDateOnly() +``` +Returns the `is_date_only` property (Whether the event only has a date specified, but no time) for the event as a bool if present. + +### getName() +```php +public string|null getName() +``` +Returns the `name` property (Event name) for the event as a string if present. + +### getOwner() +```php +public GraphNode|null getOwner() +``` +Returns the `owner` property (The profile that created the event) for the event as a GraphNode if present. + +### getParentGroup() +```php +public GraphGroup|null getParentGroup() +``` +Returns the `parent_group` property (The group the event belongs to) for the event as a GraphGroup if present. + +### getPlace() +```php +public GraphPage|null getPlace() +``` +Returns the `place` property (Event Place information) for the event as a GraphPage if present. + +### getPrivacy() +```php +public string|null getPrivacy() +``` +Returns the `privacy` property (Who can see the event) for the event as a string if present. + +### getStartTime() +```php +public DateTime|null getStartTime() +``` +Returns the `start_time` property (Start time) for the event as a DateTime if present. + +### getTicketUri() +```php +public string|null getTicketUri() +``` +Returns the `ticket_uri` property (The link users can visit to buy a ticket to this event) for the event as a string if present. + +### getTimezone() +```php +public string|null getTimezone() +``` +Returns the `timezone` property (Timezone) for the event as a string if present. + +### getUpdatedTime() +```php +public DateTime|null getUpdatedTime() +``` +Returns the `updated_time` property (Last update time) for the event as a DateTime if present. + +### getPicture() +```php +public GraphPicture|null getPicture() +``` +Returns the `picture` property (Event picture) for the event as a GraphPicture if present. + +### getAttendingCount() +```php +public int|null getAttendingCount() +``` +Returns the `attending_count` property (Number of people attending the event) for the event as a int if present. + +### getDeclinedCount() +```php +public int|null getDeclinedCount() +``` +Returns the `declined_count` property (Number of people who declined the event) for the event as a int if present. + +### getMaybeCount() +```php +public int|null getMaybeCount() +``` +Returns the `maybe_count` property (Number of people who maybe going to the event) for the event as a int if present. + +### getNoreplyCount() +```php +public int|null getNoreplyCount() +``` +Returns the `noreply_count` property (Number of people who did not reply to the event) for the event as a int if present. + +### getInvitedCount() +```php +public int|null getInvitedCount() +``` +Returns the `invited_count` property (Number of people invited to the event) for the event as a int if present. + +## GraphGroup Instance Methods + +All getter methods return `null` if the field does not exist on the node. + +### getId() +```php +public string|null getId() +``` +Returns the `id` field (The Group ID) for the group as a string if present. + +### getCover() +```php +public GraphCoverPhoto|null getCover() +``` +Returns the `cover` field (The cover photo of the Group) for the group as a GraphCoverPhoto if present. + +### getDescription() +```php +public string|null getDescription() +``` +Returns the `description` field (A brief description of the Group) for the group as a string if present. + +### getEmail() +```php +public string|null getEmail() +``` +Returns the `email` field (The email address to upload content to the Group. Only current members of the Group can use this) for the group as a string if present. + +### getIcon() +```php +public string|null getIcon() +``` +Returns the `icon` field (The URL for the Group's icon) for the group as a string if present. + +### getLink() +```php +public string|null getLink() +``` +Returns the `link` field (The Group's website) for the group as a string if present. + +### getName() +```php +public string|null getName() +``` +Returns the `name` field (The name of the Group) for the group as a string if present. + +### getMemberRequestCount() +```php +public int|null getMemberRequestCount() +``` +Returns the `member_request_count` field (Number of people asking to join the group.) for the group as a int if present. + +### getOwner() +```php +public GraphNode|null getOwner() +``` +Returns the `owner` field (The profile that created this Group) for the group as a GraphNode if present. + +### getParent() +```php +public GraphNode|null getParent() +``` +Returns the `parent` field (The parent Group of this Group, if it exists) for the group as a GraphNode if present. + +### getPrivacy() +```php +public string|null getPrivacy() +``` +Returns the `privacy` field (The privacy setting of the Group) for the group as a string if present. + +### getUpdatedTime() +```php +public DateTime|null getUpdatedTime() +``` +Returns the `updated_time` field (The last time the Group was updated (this includes changes in the Group's properties and changes in posts and comments if user can see them)) for the group as a DateTime if present. + +### getVenue() +```php +public GraphLocation|null getVenue() +``` +Returns the `venue` field (The location for the Group) for the group as a GraphLocation if present. diff --git a/docs/reference/PersistentDataInterface.md b/docs/reference/PersistentDataInterface.md new file mode 100644 index 000000000..9f2283981 --- /dev/null +++ b/docs/reference/PersistentDataInterface.md @@ -0,0 +1,70 @@ +# The persistent data handler interface for the Facebook SDK for PHP + +The persistent data handler interface stores values in a persistent data store. By default the SDK for PHP uses native PHP sessions to store the persistent data. You can overwrite this behavior by coding to the `Facebook\PersistentData\PersistentDataInterface`. + +## Facebook\PersistentData\PersistentDataInterface + +If you're using a web framework that handles persistent data for you, you might want to code a custom persistent data handler to ensure that your persistent storage is being handled consistently. + +For example if you are using Laravel, a custom handler might look like this: + +```php +use Facebook\PersistentData\PersistentDataInterface; + +class MyLaravelPersistentDataHandler implements PersistentDataInterface +{ + /** + * @var string Prefix to use for session variables. + */ + protected $sessionPrefix = 'FBRLH_'; + + /** + * @inheritdoc + */ + public function get($key) + { + return \Session::get($this->sessionPrefix . $key); + } + + /** + * @inheritdoc + */ + public function set($key, $value) + { + \Session::put($this->sessionPrefix . $key, $value); + } +} +``` + +To enable your custom persistent data handler implementation in the SDK, you can set an instance of the handler to the `persistent_data_handler` config of the `Facebook\Facebook` super service. + +```php +$fb = new Facebook\Facebook([ + // . . . + 'persistent_data_handler' => new MyLaravelPersistentDataHandler(), + // . . . + ]); +``` + +Alternatively, if you're working with the `Facebook\Helpers\FacebookRedirectLoginHelper` directly, you can inject your custom handler via the constructor. + +```php +use Facebook\Helpers\FacebookRedirectLoginHelper; + +$myPersistentDataHandler = new MyLaravelPersistentDataHandler(); +$helper = new FacebookRedirectLoginHelper($fbApp, $myPersistentDataHandler); +``` + +## Method Reference + +### get() +```php +public mixed get(string $key) +``` +Returns a value from the persistent data store or `null` if the value does not exist. + +### set() +```php +public void set(string $key, mixed $value) +``` +Sets a value to the persistent data store. diff --git a/docs/reference/PseudoRandomStringGeneratorInterface.md b/docs/reference/PseudoRandomStringGeneratorInterface.md new file mode 100644 index 000000000..a6409ed30 --- /dev/null +++ b/docs/reference/PseudoRandomStringGeneratorInterface.md @@ -0,0 +1,59 @@ +# The cryptographically secure pseudo-random string generator interface for the Facebook SDK for PHP + +The cryptographically secure pseudo-random string generator interface allows you to overwrite the default CSPRSG logic by coding to the `Facebook\PseudoRandomString\PseudoRandomStringGeneratorInterface`. + +## Facebook\PseudoRandomString\PseudoRandomStringGeneratorInterface + +By default the SDK will attempt to generate a cryptographically secure random string using a number of methods. If a cryptographically secure method is not detected, a `Facebook\Exceptions\FacebookSDKException` will be thrown. + +If your hosting environment does not support any of the CSPRSG methods used by the SDK or if you have preferred CSPRSG, you can provide your own CSPRSG to the SDK using this interface. + +> **Caution:** Although it is popular to use `rand()`, `mt_rand()` and `uniqid()` to generate random strings in PHP, these methods are not cryptographically secure. Since the pseudo-random string generator is used to validate against Cross-Site Request Forgery (CSRF) attacks, the random strings _must_ be cryptographically secure. Only overwrite this functionality if your custom pseudo-random string generator is a cryptographically strong one. + +An example of implementing a custom CSPRSG: + +```php +use Facebook\PseudoRandomString\PseudoRandomStringGeneratorInterface; + +class MyCustomPseudoRandomStringGenerator implements PseudoRandomStringGeneratorInterface +{ + /** + * @inheritdoc + */ + public function getPseudoRandomString($length) + { + $randomString = ''; + + // . . . Do CSPRSG logic here . . . + + return $randomString; + } +} +``` + +To enable your custom CSPRSG implementation in the SDK, you can set an instance of the generator to the `pseudo_random_string_generator` config of the `Facebook\Facebook` super service. + +```php +$fb = new Facebook\Facebook([ + // . . . + 'pseudo_random_string_generator' => new MyCustomPseudoRandomStringGenerator(), + // . . . + ]); +``` + +Alternatively, if you're working with the `Facebook\Helpers\FacebookRedirectLoginHelper` directly, you can inject your custom generator via the constructor. + +```php +use Facebook\Helpers\FacebookRedirectLoginHelper; + +$myPseudoRandomStringGenerator = new MyCustomPseudoRandomStringGenerator(); +$helper = new FacebookRedirectLoginHelper($fbApp, null, null, $myPseudoRandomStringGenerator); +``` + +## Method Reference + +### getPseudoRandomString() +```php +public string getPseudoRandomString(int $length) +``` +Returns a cryptographically secure pseudo-random string that is `$length` characters long. diff --git a/docs/reference/SignedRequest.md b/docs/reference/SignedRequest.md new file mode 100644 index 000000000..70539f69c --- /dev/null +++ b/docs/reference/SignedRequest.md @@ -0,0 +1,85 @@ +# SignedRequest entity for the Facebook SDK for PHP + +The `Facebook\SignedRequest` entity represents a signed request. + +## Facebook\SignedRequest + +[Signed requests](https://developers.facebook.com/docs/games/gamesonfacebook/login#detectingloginstatus) contain payloads of data that can be validated against a hash signature to ensure it is from Facebook. The `Facebook\SignedRequest` entity can validate a signed request signature and decode the payload. + +To instantiate a new `Facebook\SignedRequest` entity, pass the [`Facebook\FacebookApp`](FacebookApp.md) entity and raw signed request to the constructor. + +```php +$fbApp = new Facebook\FacebookApp('{app-id}', '{app-secret}'); +$signedRequest = new Facebook\SignedRequest($fbApp, 'raw.signed_request'); +``` + +Usually `Facebook\SignedRequest` entities are obtained using one of the [helpers](../reference.md). + +```php +$fb = new Facebook\Facebook([/* . . . */]); + +// Obtain a signed request entity from the cookie set by the JavaScript SDK +$helper = $fb->getJavaScriptHelper(); +$signedRequest = $helper->getSignedRequest(); + +// Obtain a signed request entity from a canvas app +$helper = $fb->getCanvasHelper(); +$signedRequest = $helper->getSignedRequest(); + +// Obtain a signed request entity from a page tab +$helper = $fb->getPageTabHelper(); +$signedRequest = $helper->getSignedRequest(); +``` + +## Instance Methods + +### getRawSignedRequest() +```php +public string|null getRawSignedRequest() +``` +Returns the original raw encoded signed request in the form of a string. + +### getPayload() +```php +public array|null getPayload() +``` +Returns the [signed request payload](https://developers.facebook.com/docs/reference/login/signed-request/) in the form of an array. + +### get() +```php +public string|null get(string $key, string|null $default) +``` +Returns a [field from the signed request payload](https://developers.facebook.com/docs/reference/login/signed-request) or `$default` if the value does not exist. + +### getUserId() +```php +public string|null getUserId() +``` +Returns the `user_id` field from the signed request payload if it exists or `null` if it does not exists. + +### hasOAuthData() +```php +public boolean hasOAuthData() +``` +Returns `true` if the payload data contains either an `oauth_token` or `code` field. Returns `false` if neither value exists. + +### make() +```php +public string make(array $payload) +``` +Generates a valid raw signed request as a string that contains the data from the `$payload` array. The signature is signed using the app secret from the `Facebook\FacebookApp` entity. This can be useful for testing purposes. + +```php +$fbApp = new Facebook\FacebookApp('{app-id}', '{app-secret}'); +$signedRequest = new Facebook\SignedRequest($fbApp); + +$payload = [ + 'algorithm' => 'HMAC-SHA256', + 'issued_at' => time(), + 'foo' => 'bar', + ]; +$rawSignedRequest = $signedRequest->make($payload); + +var_dump($rawSignedRequest); +// string(129) "c9RNpwW4vGYTGc7_E-_XQu5aoEQrWrx_KDOdz3x9Ec0=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZF9hdCI6MTQxODE4MjI1NSwiZm9vIjoiYmFyIn0=" +``` diff --git a/docs/reference/UrlDetectionInterface.md b/docs/reference/UrlDetectionInterface.md new file mode 100644 index 000000000..7e14b2445 --- /dev/null +++ b/docs/reference/UrlDetectionInterface.md @@ -0,0 +1,51 @@ +# The URL detection interface for the Facebook SDK for PHP + +The URL detection interface allows you to overwrite the default URL detection logic by coding to the `Facebook\Url\UrlDetectionInterface`. + +## Facebook\Url\UrlDetectionInterface + +If you're using a web framework that handles routes and URL generation for you, you might want to code a custom URL detection handler to ensure that your URL's are being generated consistently. + +For example if you are using Laravel, a custom handler might look like this: + +```php +use Facebook\Url\UrlDetectionInterface; + +class MyLaravelUrlDetectionHandler implements UrlDetectionInterface +{ + /** + * @inheritdoc + */ + public function getCurrentUrl() + { + return \Request::url(); + } +} +``` + +To enable your custom URL detection implementation in the SDK, you can set an instance of the handler to the `url_detection_handler` config of the `Facebook\Facebook` super service. + +```php +$fb = new Facebook\Facebook([ + // . . . + 'url_detection_handler' => new MyLaravelUrlDetectionHandler(), + // . . . + ]); +``` + +Alternatively, if you're working with the `Facebook\Helpers\FacebookRedirectLoginHelper` directly, you can inject your custom handler via the constructor. + +```php +use Facebook\Helpers\FacebookRedirectLoginHelper; + +$myUrlDetectionHandler = new MyLaravelUrlDetectionHandler(); +$helper = new FacebookRedirectLoginHelper($fbApp, null, $myUrlDetectionHandler); +``` + +## Method Reference + +### getCurrentUrl() +```php +public string getCurrentUrl() +``` +Returns the full and currently active URL. diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 000000000..96c56f886 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,7 @@ + + + src/ + tests/ + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fcca4e79e..f6707bdb7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,7 +7,7 @@ stopOnFailure="false" bootstrap="tests/bootstrap.php"> - + ./tests @@ -16,4 +16,7 @@ ./src/Facebook + + + diff --git a/src/Facebook/Authentication/AccessToken.php b/src/Facebook/Authentication/AccessToken.php new file mode 100644 index 000000000..5d7007334 --- /dev/null +++ b/src/Facebook/Authentication/AccessToken.php @@ -0,0 +1,160 @@ +value = $accessToken; + if ($expiresAt) { + $this->setExpiresAtFromTimeStamp($expiresAt); + } + } + + /** + * Generate an app secret proof to sign a request to Graph. + * + * @param string $appSecret The app secret. + * + * @return string + */ + public function getAppSecretProof($appSecret) + { + return hash_hmac('sha256', $this->value, $appSecret); + } + + /** + * Getter for expiresAt. + * + * @return \DateTime|null + */ + public function getExpiresAt() + { + return $this->expiresAt; + } + + /** + * Determines whether or not this is an app access token. + * + * @return bool + */ + public function isAppAccessToken() + { + return strpos($this->value, '|') !== false; + } + + /** + * Determines whether or not this is a long-lived token. + * + * @return bool + */ + public function isLongLived() + { + if ($this->expiresAt) { + return $this->expiresAt->getTimestamp() > time() + (60 * 60 * 2); + } + + if ($this->isAppAccessToken()) { + return true; + } + + return false; + } + + /** + * Checks the expiration of the access token. + * + * @return boolean|null + */ + public function isExpired() + { + if ($this->getExpiresAt() instanceof \DateTime) { + return $this->getExpiresAt()->getTimestamp() < time(); + } + + if ($this->isAppAccessToken()) { + return false; + } + + return null; + } + + /** + * Returns the access token as a string. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the access token as a string. + * + * @return string + */ + public function __toString() + { + return $this->getValue(); + } + + /** + * Setter for expires_at. + * + * @param int $timeStamp + */ + protected function setExpiresAtFromTimeStamp($timeStamp) + { + $dt = new \DateTime(); + $dt->setTimestamp($timeStamp); + $this->expiresAt = $dt; + } +} diff --git a/src/Facebook/Authentication/AccessTokenMetadata.php b/src/Facebook/Authentication/AccessTokenMetadata.php new file mode 100644 index 000000000..165433cb6 --- /dev/null +++ b/src/Facebook/Authentication/AccessTokenMetadata.php @@ -0,0 +1,390 @@ +metadata = $metadata['data']; + + $this->castTimestampsToDateTime(); + } + + /** + * Returns a value from the metadata. + * + * @param string $field The property to retrieve. + * @param mixed $default The default to return if the property doesn't exist. + * + * @return mixed + */ + public function getField($field, $default = null) + { + if (isset($this->metadata[$field])) { + return $this->metadata[$field]; + } + + return $default; + } + + /** + * Returns a value from the metadata. + * + * @param string $field The property to retrieve. + * @param mixed $default The default to return if the property doesn't exist. + * + * @return mixed + * + * @deprecated 5.0.0 getProperty() has been renamed to getField() + * @todo v6: Remove this method + */ + public function getProperty($field, $default = null) + { + return $this->getField($field, $default); + } + + /** + * Returns a value from a child property in the metadata. + * + * @param string $parentField The parent property. + * @param string $field The property to retrieve. + * @param mixed $default The default to return if the property doesn't exist. + * + * @return mixed + */ + public function getChildProperty($parentField, $field, $default = null) + { + if (!isset($this->metadata[$parentField])) { + return $default; + } + + if (!isset($this->metadata[$parentField][$field])) { + return $default; + } + + return $this->metadata[$parentField][$field]; + } + + /** + * Returns a value from the error metadata. + * + * @param string $field The property to retrieve. + * @param mixed $default The default to return if the property doesn't exist. + * + * @return mixed + */ + public function getErrorProperty($field, $default = null) + { + return $this->getChildProperty('error', $field, $default); + } + + /** + * Returns a value from the "metadata" metadata. *Brain explodes* + * + * @param string $field The property to retrieve. + * @param mixed $default The default to return if the property doesn't exist. + * + * @return mixed + */ + public function getMetadataProperty($field, $default = null) + { + return $this->getChildProperty('metadata', $field, $default); + } + + /** + * The ID of the application this access token is for. + * + * @return string|null + */ + public function getAppId() + { + return $this->getField('app_id'); + } + + /** + * Name of the application this access token is for. + * + * @return string|null + */ + public function getApplication() + { + return $this->getField('application'); + } + + /** + * Any error that a request to the graph api + * would return due to the access token. + * + * @return bool|null + */ + public function isError() + { + return $this->getField('error') !== null; + } + + /** + * The error code for the error. + * + * @return int|null + */ + public function getErrorCode() + { + return $this->getErrorProperty('code'); + } + + /** + * The error message for the error. + * + * @return string|null + */ + public function getErrorMessage() + { + return $this->getErrorProperty('message'); + } + + /** + * The error subcode for the error. + * + * @return int|null + */ + public function getErrorSubcode() + { + return $this->getErrorProperty('subcode'); + } + + /** + * DateTime when this access token expires. + * + * @return \DateTime|null + */ + public function getExpiresAt() + { + return $this->getField('expires_at'); + } + + /** + * Whether the access token is still valid or not. + * + * @return boolean|null + */ + public function getIsValid() + { + return $this->getField('is_valid'); + } + + /** + * DateTime when this access token was issued. + * + * Note that the issued_at field is not returned + * for short-lived access tokens. + * + * @see https://developers.facebook.com/docs/facebook-login/access-tokens#debug + * + * @return \DateTime|null + */ + public function getIssuedAt() + { + return $this->getField('issued_at'); + } + + /** + * General metadata associated with the access token. + * Can contain data like 'sso', 'auth_type', 'auth_nonce'. + * + * @return array|null + */ + public function getMetadata() + { + return $this->getField('metadata'); + } + + /** + * The 'sso' child property from the 'metadata' parent property. + * + * @return string|null + */ + public function getSso() + { + return $this->getMetadataProperty('sso'); + } + + /** + * The 'auth_type' child property from the 'metadata' parent property. + * + * @return string|null + */ + public function getAuthType() + { + return $this->getMetadataProperty('auth_type'); + } + + /** + * The 'auth_nonce' child property from the 'metadata' parent property. + * + * @return string|null + */ + public function getAuthNonce() + { + return $this->getMetadataProperty('auth_nonce'); + } + + /** + * For impersonated access tokens, the ID of + * the page this token contains. + * + * @return string|null + */ + public function getProfileId() + { + return $this->getField('profile_id'); + } + + /** + * List of permissions that the user has granted for + * the app in this access token. + * + * @return array + */ + public function getScopes() + { + return $this->getField('scopes'); + } + + /** + * The ID of the user this access token is for. + * + * @return string|null + */ + public function getUserId() + { + return $this->getField('user_id'); + } + + /** + * Ensures the app ID from the access token + * metadata is what we expect. + * + * @param string $appId + * + * @throws FacebookSDKException + */ + public function validateAppId($appId) + { + if ($this->getAppId() !== $appId) { + throw new FacebookSDKException('Access token metadata contains unexpected app ID.', 401); + } + } + + /** + * Ensures the user ID from the access token + * metadata is what we expect. + * + * @param string $userId + * + * @throws FacebookSDKException + */ + public function validateUserId($userId) + { + if ($this->getUserId() !== $userId) { + throw new FacebookSDKException('Access token metadata contains unexpected user ID.', 401); + } + } + + /** + * Ensures the access token has not expired yet. + * + * @throws FacebookSDKException + */ + public function validateExpiration() + { + if (!$this->getExpiresAt() instanceof \DateTime) { + return; + } + + if ($this->getExpiresAt()->getTimestamp() < time()) { + throw new FacebookSDKException('Inspection of access token metadata shows that the access token has expired.', 401); + } + } + + /** + * Converts a unix timestamp into a DateTime entity. + * + * @param int $timestamp + * + * @return \DateTime + */ + private function convertTimestampToDateTime($timestamp) + { + $dt = new \DateTime(); + $dt->setTimestamp($timestamp); + + return $dt; + } + + /** + * Casts the unix timestamps as DateTime entities. + */ + private function castTimestampsToDateTime() + { + foreach (static::$dateProperties as $key) { + if (isset($this->metadata[$key]) && $this->metadata[$key] !== 0) { + $this->metadata[$key] = $this->convertTimestampToDateTime($this->metadata[$key]); + } + } + } +} diff --git a/src/Facebook/Authentication/OAuth2Client.php b/src/Facebook/Authentication/OAuth2Client.php new file mode 100644 index 000000000..94df9b7b5 --- /dev/null +++ b/src/Facebook/Authentication/OAuth2Client.php @@ -0,0 +1,292 @@ +app = $app; + $this->client = $client; + $this->graphVersion = $graphVersion ?: Facebook::DEFAULT_GRAPH_VERSION; + } + + /** + * Returns the last FacebookRequest that was sent. + * Useful for debugging and testing. + * + * @return FacebookRequest|null + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * Get the metadata associated with the access token. + * + * @param AccessToken|string $accessToken The access token to debug. + * + * @return AccessTokenMetadata + */ + public function debugToken($accessToken) + { + $accessToken = $accessToken instanceof AccessToken ? $accessToken->getValue() : $accessToken; + $params = ['input_token' => $accessToken]; + + $this->lastRequest = new FacebookRequest( + $this->app, + $this->app->getAccessToken(), + 'GET', + '/debug_token', + $params, + null, + $this->graphVersion + ); + $response = $this->client->sendRequest($this->lastRequest); + $metadata = $response->getDecodedBody(); + + return new AccessTokenMetadata($metadata); + } + + /** + * Generates an authorization URL to begin the process of authenticating a user. + * + * @param string $redirectUrl The callback URL to redirect to. + * @param string $state The CSPRNG-generated CSRF value. + * @param array $scope An array of permissions to request. + * @param array $params An array of parameters to generate URL. + * @param string $separator The separator to use in http_build_query(). + * + * @return string + */ + public function getAuthorizationUrl($redirectUrl, $state, array $scope = [], array $params = [], $separator = '&') + { + $params += [ + 'client_id' => $this->app->getId(), + 'state' => $state, + 'response_type' => 'code', + 'sdk' => 'php-sdk-' . Facebook::VERSION, + 'redirect_uri' => $redirectUrl, + 'scope' => implode(',', $scope) + ]; + + return static::BASE_AUTHORIZATION_URL . '/' . $this->graphVersion . '/dialog/oauth?' . http_build_query($params, null, $separator); + } + + /** + * Get a valid access token from a code. + * + * @param string $code + * @param string $redirectUri + * + * @return AccessToken + * + * @throws FacebookSDKException + */ + public function getAccessTokenFromCode($code, $redirectUri = '') + { + $params = [ + 'code' => $code, + 'redirect_uri' => $redirectUri, + ]; + + return $this->requestAnAccessToken($params); + } + + /** + * Exchanges a short-lived access token with a long-lived access token. + * + * @param AccessToken|string $accessToken + * + * @return AccessToken + * + * @throws FacebookSDKException + */ + public function getLongLivedAccessToken($accessToken) + { + $accessToken = $accessToken instanceof AccessToken ? $accessToken->getValue() : $accessToken; + $params = [ + 'grant_type' => 'fb_exchange_token', + 'fb_exchange_token' => $accessToken, + ]; + + return $this->requestAnAccessToken($params); + } + + /** + * Get a valid code from an access token. + * + * @param AccessToken|string $accessToken + * @param string $redirectUri + * + * @return AccessToken + * + * @throws FacebookSDKException + */ + public function getCodeFromLongLivedAccessToken($accessToken, $redirectUri = '') + { + $params = [ + 'redirect_uri' => $redirectUri, + ]; + + $response = $this->sendRequestWithClientParams('/oauth/client_code', $params, $accessToken); + $data = $response->getDecodedBody(); + + if (!isset($data['code'])) { + throw new FacebookSDKException('Code was not returned from Graph.', 401); + } + + return $data['code']; + } + + /** + * Send a request to the OAuth endpoint. + * + * @param array $params + * + * @return AccessToken + * + * @throws FacebookSDKException + */ + protected function requestAnAccessToken(array $params) + { + $response = $this->sendRequestWithClientParams('/oauth/access_token', $params); + $data = $response->getDecodedBody(); + + if (!isset($data['access_token'])) { + throw new FacebookSDKException('Access token was not returned from Graph.', 401); + } + + // Graph returns two different key names for expiration time + // on the same endpoint. Doh! :/ + $expiresAt = 0; + if (isset($data['expires'])) { + // For exchanging a short lived token with a long lived token. + // The expiration time in seconds will be returned as "expires". + $expiresAt = time() + $data['expires']; + } elseif (isset($data['expires_in'])) { + // For exchanging a code for a short lived access token. + // The expiration time in seconds will be returned as "expires_in". + // See: https://developers.facebook.com/docs/facebook-login/access-tokens#long-via-code + $expiresAt = time() + $data['expires_in']; + } + + return new AccessToken($data['access_token'], $expiresAt); + } + + /** + * Send a request to Graph with an app access token. + * + * @param string $endpoint + * @param array $params + * @param AccessToken|string|null $accessToken + * + * @return FacebookResponse + * + * @throws FacebookResponseException + */ + protected function sendRequestWithClientParams($endpoint, array $params, $accessToken = null) + { + $params += $this->getClientParams(); + + $accessToken = $accessToken ?: $this->app->getAccessToken(); + + $this->lastRequest = new FacebookRequest( + $this->app, + $accessToken, + 'GET', + $endpoint, + $params, + null, + $this->graphVersion + ); + + return $this->client->sendRequest($this->lastRequest); + } + + /** + * Returns the client_* params for OAuth requests. + * + * @return array + */ + protected function getClientParams() + { + return [ + 'client_id' => $this->app->getId(), + 'client_secret' => $this->app->getSecret(), + ]; + } +} diff --git a/src/Facebook/Entities/AccessToken.php b/src/Facebook/Entities/AccessToken.php deleted file mode 100644 index 111e5a681..000000000 --- a/src/Facebook/Entities/AccessToken.php +++ /dev/null @@ -1,370 +0,0 @@ -accessToken = $accessToken; - if ($expiresAt) { - $this->setExpiresAtFromTimeStamp($expiresAt); - } - $this->machineId = $machineId; - } - - /** - * Setter for expires_at. - * - * @param int $timeStamp - */ - protected function setExpiresAtFromTimeStamp($timeStamp) - { - $dt = new \DateTime(); - $dt->setTimestamp($timeStamp); - $this->expiresAt = $dt; - } - - /** - * Getter for expiresAt. - * - * @return \DateTime|null - */ - public function getExpiresAt() - { - return $this->expiresAt; - } - - /** - * Getter for machineId. - * - * @return string|null - */ - public function getMachineId() - { - return $this->machineId; - } - - /** - * Determines whether or not this is a long-lived token. - * - * @return bool - */ - public function isLongLived() - { - if ($this->expiresAt) { - return $this->expiresAt->getTimestamp() > time() + (60 * 60 * 2); - } - return false; - } - - /** - * Checks the validity of the access token. - * - * @param string|null $appId Application ID to use - * @param string|null $appSecret App secret value to use - * @param string|null $machineId - * - * @return boolean - */ - public function isValid($appId = null, $appSecret = null, $machineId = null) - { - $accessTokenInfo = $this->getInfo($appId, $appSecret); - $machineId = $machineId ?: $this->machineId; - return static::validateAccessToken($accessTokenInfo, $appId, $machineId); - } - - /** - * Ensures the provided GraphSessionInfo object is valid, - * throwing an exception if not. Ensures the appId matches, - * that the machineId matches if it's being used, - * that the token is valid and has not expired. - * - * @param GraphSessionInfo $tokenInfo - * @param string|null $appId Application ID to use - * @param string|null $machineId - * - * @return boolean - */ - public static function validateAccessToken(GraphSessionInfo $tokenInfo, - $appId = null, $machineId = null) - { - $targetAppId = FacebookSession::_getTargetAppId($appId); - - $appIdIsValid = $tokenInfo->getAppId() == $targetAppId; - $machineIdIsValid = $tokenInfo->getProperty('machine_id') == $machineId; - $accessTokenIsValid = $tokenInfo->isValid(); - - // Not all access tokens return an expiration. E.g. an app access token. - if ($tokenInfo->getExpiresAt() instanceof \DateTime) { - $accessTokenIsStillAlive = $tokenInfo->getExpiresAt()->getTimestamp() >= time(); - } else { - $accessTokenIsStillAlive = true; - } - - return $appIdIsValid && $machineIdIsValid && $accessTokenIsValid && $accessTokenIsStillAlive; - } - - /** - * Get a valid access token from a code. - * - * @param string $code - * @param string|null $appId - * @param string|null $appSecret - * @param string|null $machineId - * - * @return AccessToken - */ - public static function getAccessTokenFromCode($code, $appId = null, $appSecret = null, $machineId = null) - { - $params = array( - 'code' => $code, - 'redirect_uri' => '', - ); - - if ($machineId) { - $params['machine_id'] = $machineId; - } - - return static::requestAccessToken($params, $appId, $appSecret); - } - - /** - * Get a valid code from an access token. - * - * @param AccessToken|string $accessToken - * @param string|null $appId - * @param string|null $appSecret - * - * @return AccessToken - */ - public static function getCodeFromAccessToken($accessToken, $appId = null, $appSecret = null) - { - $accessToken = (string) $accessToken; - - $params = array( - 'access_token' => $accessToken, - 'redirect_uri' => '', - ); - - return static::requestCode($params, $appId, $appSecret); - } - - /** - * Exchanges a short lived access token with a long lived access token. - * - * @param string|null $appId - * @param string|null $appSecret - * - * @return AccessToken - */ - public function extend($appId = null, $appSecret = null) - { - $params = array( - 'grant_type' => 'fb_exchange_token', - 'fb_exchange_token' => $this->accessToken, - ); - - return static::requestAccessToken($params, $appId, $appSecret); - } - - /** - * Request an access token based on a set of params. - * - * @param array $params - * @param string|null $appId - * @param string|null $appSecret - * - * @return AccessToken - * - * @throws FacebookRequestException - */ - public static function requestAccessToken(array $params, $appId = null, $appSecret = null) - { - $response = static::request('/oauth/access_token', $params, $appId, $appSecret); - $data = $response->getResponse(); - - /** - * @TODO fix this malarkey - getResponse() should always return an object - * @see https://github.com/facebook/facebook-php-sdk-v4/issues/36 - */ - if (is_array($data)) { - if (isset($data['access_token'])) { - $expiresAt = isset($data['expires']) ? time() + $data['expires'] : 0; - return new static($data['access_token'], $expiresAt); - } - } elseif($data instanceof \stdClass) { - if (isset($data->access_token)) { - $expiresAt = isset($data->expires_in) ? time() + $data->expires_in : 0; - $machineId = isset($data->machine_id) ? (string) $data->machine_id : null; - return new static((string) $data->access_token, $expiresAt, $machineId); - } - } - - throw FacebookRequestException::create( - $response->getRawResponse(), - $data, - 401 - ); - } - - /** - * Request a code from a long lived access token. - * - * @param array $params - * @param string|null $appId - * @param string|null $appSecret - * - * @return string - * - * @throws FacebookRequestException - */ - public static function requestCode(array $params, $appId = null, $appSecret = null) - { - $response = static::request('/oauth/client_code', $params, $appId, $appSecret); - $data = $response->getResponse(); - - if (isset($data->code)) { - return (string) $data->code; - } - - throw FacebookRequestException::create( - $response->getRawResponse(), - $data, - 401 - ); - } - - /** - * Send a request to Graph with an app access token. - * - * @param string $endpoint - * @param array $params - * @param string|null $appId - * @param string|null $appSecret - * - * @return \Facebook\FacebookResponse - * - * @throws FacebookRequestException - */ - protected static function request($endpoint, array $params, $appId = null, $appSecret = null) - { - $targetAppId = FacebookSession::_getTargetAppId($appId); - $targetAppSecret = FacebookSession::_getTargetAppSecret($appSecret); - - if (!isset($params['client_id'])) { - $params['client_id'] = $targetAppId; - } - if (!isset($params['client_secret'])) { - $params['client_secret'] = $targetAppSecret; - } - - // The response for this endpoint is not JSON, so it must be handled - // differently, not as a GraphObject. - $request = new FacebookRequest( - FacebookSession::newAppSession($targetAppId, $targetAppSecret), - 'GET', - $endpoint, - $params - ); - return $request->execute(); - } - - /** - * Get more info about an access token. - * - * @param string|null $appId - * @param string|null $appSecret - * - * @return GraphSessionInfo - */ - public function getInfo($appId = null, $appSecret = null) - { - $params = array('input_token' => $this->accessToken); - - $request = new FacebookRequest( - FacebookSession::newAppSession($appId, $appSecret), - 'GET', - '/debug_token', - $params - ); - $response = $request->execute()->getGraphObject(GraphSessionInfo::className()); - - // Update the data on this token - if ($response->getExpiresAt()) { - $this->expiresAt = $response->getExpiresAt(); - } - - return $response; - } - - /** - * Returns the access token as a string. - * - * @return string - */ - public function __toString() - { - return $this->accessToken; - } - -} diff --git a/src/Facebook/Entities/SignedRequest.php b/src/Facebook/Entities/SignedRequest.php deleted file mode 100644 index 09134c5bf..000000000 --- a/src/Facebook/Entities/SignedRequest.php +++ /dev/null @@ -1,386 +0,0 @@ -rawSignedRequest = $rawSignedRequest; - $this->payload = static::parse($rawSignedRequest, $state, $appSecret); - } - - /** - * Returns the raw signed request data. - * - * @return string|null - */ - public function getRawSignedRequest() - { - return $this->rawSignedRequest; - } - - /** - * Returns the parsed signed request data. - * - * @return array|null - */ - public function getPayload() - { - return $this->payload; - } - - /** - * Returns a property from the signed request data if available. - * - * @param string $key - * @param mixed|null $default - * - * @return mixed|null - */ - public function get($key, $default = null) - { - if (isset($this->payload[$key])) { - return $this->payload[$key]; - } - return $default; - } - - /** - * Returns user_id from signed request data if available. - * - * @return string|null - */ - public function getUserId() - { - return $this->get('user_id'); - } - - /** - * Checks for OAuth data in the payload. - * - * @return boolean - */ - public function hasOAuthData() - { - return isset($this->payload['oauth_token']) || isset($this->payload['code']); - } - - /** - * Creates a signed request from an array of data. - * - * @param array $payload - * @param string|null $appSecret - * - * @return string - */ - public static function make(array $payload, $appSecret = null) - { - $payload['algorithm'] = 'HMAC-SHA256'; - $payload['issued_at'] = time(); - $encodedPayload = static::base64UrlEncode(json_encode($payload)); - - $hashedSig = static::hashSignature($encodedPayload, $appSecret); - $encodedSig = static::base64UrlEncode($hashedSig); - - return $encodedSig.'.'.$encodedPayload; - } - - /** - * Validates and decodes a signed request and returns - * the payload as an array. - * - * @param string $signedRequest - * @param string|null $state - * @param string|null $appSecret - * - * @return array - */ - public static function parse($signedRequest, $state = null, $appSecret = null) - { - list($encodedSig, $encodedPayload) = static::split($signedRequest); - - // Signature validation - $sig = static::decodeSignature($encodedSig); - $hashedSig = static::hashSignature($encodedPayload, $appSecret); - static::validateSignature($hashedSig, $sig); - - // Payload validation - $data = static::decodePayload($encodedPayload); - static::validateAlgorithm($data); - if ($state) { - static::validateCsrf($data, $state); - } - - return $data; - } - - /** - * Validates the format of a signed request. - * - * @param string $signedRequest - * - * @throws FacebookSDKException - */ - public static function validateFormat($signedRequest) - { - if (strpos($signedRequest, '.') !== false) { - return; - } - - throw new FacebookSDKException( - 'Malformed signed request.', 606 - ); - } - - /** - * Decodes a raw valid signed request. - * - * @param string $signedRequest - * - * @returns array - */ - public static function split($signedRequest) - { - static::validateFormat($signedRequest); - - return explode('.', $signedRequest, 2); - } - - /** - * Decodes the raw signature from a signed request. - * - * @param string $encodedSig - * - * @returns string - * - * @throws FacebookSDKException - */ - public static function decodeSignature($encodedSig) - { - $sig = static::base64UrlDecode($encodedSig); - - if ($sig) { - return $sig; - } - - throw new FacebookSDKException( - 'Signed request has malformed encoded signature data.', 607 - ); - } - - /** - * Decodes the raw payload from a signed request. - * - * @param string $encodedPayload - * - * @returns array - * - * @throws FacebookSDKException - */ - public static function decodePayload($encodedPayload) - { - $payload = static::base64UrlDecode($encodedPayload); - - if ($payload) { - $payload = json_decode($payload, true); - } - - if (is_array($payload)) { - return $payload; - } - - throw new FacebookSDKException( - 'Signed request has malformed encoded payload data.', 607 - ); - } - - /** - * Validates the algorithm used in a signed request. - * - * @param array $data - * - * @throws FacebookSDKException - */ - public static function validateAlgorithm(array $data) - { - if (isset($data['algorithm']) && $data['algorithm'] === 'HMAC-SHA256') { - return; - } - - throw new FacebookSDKException( - 'Signed request is using the wrong algorithm.', 605 - ); - } - - /** - * Hashes the signature used in a signed request. - * - * @param string $encodedData - * @param string|null $appSecret - * - * @return string - * - * @throws FacebookSDKException - */ - public static function hashSignature($encodedData, $appSecret = null) - { - $hashedSig = hash_hmac( - 'sha256', $encodedData, FacebookSession::_getTargetAppSecret($appSecret), $raw_output = true - ); - - if ($hashedSig) { - return $hashedSig; - } - - throw new FacebookSDKException( - 'Unable to hash signature from encoded payload data.', 602 - ); - } - - /** - * Validates the signature used in a signed request. - * - * @param string $hashedSig - * @param string $sig - * - * @throws FacebookSDKException - */ - public static function validateSignature($hashedSig, $sig) - { - if (mb_strlen($hashedSig) === mb_strlen($sig)) { - $validate = 0; - for ($i = 0; $i < mb_strlen($sig); $i++) { - $validate |= ord($hashedSig[$i]) ^ ord($sig[$i]); - } - if ($validate === 0) { - return; - } - } - - throw new FacebookSDKException( - 'Signed request has an invalid signature.', 602 - ); - } - - /** - * Validates a signed request against CSRF. - * - * @param array $data - * @param string $state - * - * @throws FacebookSDKException - */ - public static function validateCsrf(array $data, $state) - { - if (isset($data['state']) && $data['state'] === $state) { - return; - } - - throw new FacebookSDKException( - 'Signed request did not pass CSRF validation.', 604 - ); - } - - /** - * Base64 decoding which replaces characters: - * + instead of - - * / instead of _ - * @link http://en.wikipedia.org/wiki/Base64#URL_applications - * - * @param string $input base64 url encoded input - * - * @return string decoded string - */ - public static function base64UrlDecode($input) - { - $urlDecodedBase64 = strtr($input, '-_', '+/'); - static::validateBase64($urlDecodedBase64); - return base64_decode($urlDecodedBase64); - } - - /** - * Base64 encoding which replaces characters: - * + instead of - - * / instead of _ - * @link http://en.wikipedia.org/wiki/Base64#URL_applications - * - * @param string $input string to encode - * - * @return string base64 url encoded input - */ - public static function base64UrlEncode($input) - { - return strtr(base64_encode($input), '+/', '-_'); - } - - /** - * Validates a base64 string. - * - * @param string $input base64 value to validate - * - * @throws FacebookSDKException - */ - public static function validateBase64($input) - { - $pattern = '/^[a-zA-Z0-9\/\r\n+]*={0,2}$/'; - if (preg_match($pattern, $input)) { - return; - } - - throw new FacebookSDKException( - 'Signed request contains malformed base64 encoding.', 608 - ); - } - -} diff --git a/src/Facebook/Exceptions/FacebookAuthenticationException.php b/src/Facebook/Exceptions/FacebookAuthenticationException.php new file mode 100644 index 000000000..c5e45fa39 --- /dev/null +++ b/src/Facebook/Exceptions/FacebookAuthenticationException.php @@ -0,0 +1,33 @@ +response = $response; + $this->responseData = $response->getDecodedBody(); + + $errorMessage = $this->get('message', 'Unknown error from Graph.'); + $errorCode = $this->get('code', -1); + + parent::__construct($errorMessage, $errorCode, $previousException); + } + + /** + * A factory for creating the appropriate exception based on the response from Graph. + * + * @param FacebookResponse $response The response that threw the exception. + * + * @return FacebookResponseException + */ + public static function create(FacebookResponse $response) + { + $data = $response->getDecodedBody(); + + if (!isset($data['error']['code']) && isset($data['code'])) { + $data = ['error' => $data]; + } + + $code = isset($data['error']['code']) ? $data['error']['code'] : null; + $message = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown error from Graph.'; + + if (isset($data['error']['error_subcode'])) { + switch ($data['error']['error_subcode']) { + // Other authentication issues + case 458: + case 459: + case 460: + case 463: + case 464: + case 467: + return new static($response, new FacebookAuthenticationException($message, $code)); + // Video upload resumable error + case 1363030: + case 1363019: + case 1363033: + case 1363021: + case 1363041: + return new static($response, new FacebookResumableUploadException($message, $code)); + case 1363037: + $previousException = new FacebookResumableUploadException($message, $code); + + $startOffset = isset($data['error']['error_data']['start_offset']) ? (int) $data['error']['error_data']['start_offset'] : null; + $previousException->setStartOffset($startOffset); + + $endOffset = isset($data['error']['error_data']['end_offset']) ? (int) $data['error']['error_data']['end_offset'] : null; + $previousException->setEndOffset($endOffset); + + return new static($response, $previousException); + } + } + + switch ($code) { + // Login status or token expired, revoked, or invalid + case 100: + case 102: + case 190: + return new static($response, new FacebookAuthenticationException($message, $code)); + + // Server issue, possible downtime + case 1: + case 2: + return new static($response, new FacebookServerException($message, $code)); + + // API Throttling + case 4: + case 17: + case 32: + case 341: + case 613: + return new static($response, new FacebookThrottleException($message, $code)); + + // Duplicate Post + case 506: + return new static($response, new FacebookClientException($message, $code)); + } + + // Missing Permissions + if ($code == 10 || ($code >= 200 && $code <= 299)) { + return new static($response, new FacebookAuthorizationException($message, $code)); + } + + // OAuth authentication error + if (isset($data['error']['type']) && $data['error']['type'] === 'OAuthException') { + return new static($response, new FacebookAuthenticationException($message, $code)); + } + + // All others + return new static($response, new FacebookOtherException($message, $code)); + } + + /** + * Checks isset and returns that or a default value. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + private function get($key, $default = null) + { + if (isset($this->responseData['error'][$key])) { + return $this->responseData['error'][$key]; + } + + return $default; + } + + /** + * Returns the HTTP status code + * + * @return int + */ + public function getHttpStatusCode() + { + return $this->response->getHttpStatusCode(); + } + + /** + * Returns the sub-error code + * + * @return int + */ + public function getSubErrorCode() + { + return $this->get('error_subcode', -1); + } + + /** + * Returns the error type + * + * @return string + */ + public function getErrorType() + { + return $this->get('type', ''); + } + + /** + * Returns the raw response used to create the exception. + * + * @return string + */ + public function getRawResponse() + { + return $this->response->getBody(); + } + + /** + * Returns the decoded response used to create the exception. + * + * @return array + */ + public function getResponseData() + { + return $this->responseData; + } + + /** + * Returns the response entity used to create the exception. + * + * @return FacebookResponse + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/src/Facebook/Exceptions/FacebookResumableUploadException.php b/src/Facebook/Exceptions/FacebookResumableUploadException.php new file mode 100644 index 000000000..6d41c63cc --- /dev/null +++ b/src/Facebook/Exceptions/FacebookResumableUploadException.php @@ -0,0 +1,68 @@ +startOffset; + } + + /** + * @param int|null $startOffset + */ + public function setStartOffset($startOffset) + { + $this->startOffset = $startOffset; + } + + /** + * @return int|null + */ + public function getEndOffset() + { + return $this->endOffset; + } + + /** + * @param int|null $endOffset + */ + public function setEndOffset($endOffset) + { + $this->endOffset = $endOffset; + } +} diff --git a/src/Facebook/FacebookSDKException.php b/src/Facebook/Exceptions/FacebookSDKException.php similarity index 94% rename from src/Facebook/FacebookSDKException.php rename to src/Facebook/Exceptions/FacebookSDKException.php index 92d1412db..d8bef1ab6 100644 --- a/src/Facebook/FacebookSDKException.php +++ b/src/Facebook/Exceptions/FacebookSDKException.php @@ -1,6 +1,6 @@ getenv(static::APP_ID_ENV_NAME), + 'app_secret' => getenv(static::APP_SECRET_ENV_NAME), + 'default_graph_version' => static::DEFAULT_GRAPH_VERSION, + 'enable_beta_mode' => false, + 'http_client_handler' => null, + 'persistent_data_handler' => null, + 'pseudo_random_string_generator' => null, + 'url_detection_handler' => null, + ], $config); + + if (!$config['app_id']) { + throw new FacebookSDKException('Required "app_id" key not supplied in config and could not find fallback environment variable "' . static::APP_ID_ENV_NAME . '"'); + } + if (!$config['app_secret']) { + throw new FacebookSDKException('Required "app_secret" key not supplied in config and could not find fallback environment variable "' . static::APP_SECRET_ENV_NAME . '"'); + } + + $this->app = new FacebookApp($config['app_id'], $config['app_secret']); + $this->client = new FacebookClient( + HttpClientsFactory::createHttpClient($config['http_client_handler']), + $config['enable_beta_mode'] + ); + $this->pseudoRandomStringGenerator = PseudoRandomStringGeneratorFactory::createPseudoRandomStringGenerator( + $config['pseudo_random_string_generator'] + ); + $this->setUrlDetectionHandler($config['url_detection_handler'] ?: new FacebookUrlDetectionHandler()); + $this->persistentDataHandler = PersistentDataFactory::createPersistentDataHandler( + $config['persistent_data_handler'] + ); + + if (isset($config['default_access_token'])) { + $this->setDefaultAccessToken($config['default_access_token']); + } + + // @todo v6: Throw an InvalidArgumentException if "default_graph_version" is not set + $this->defaultGraphVersion = $config['default_graph_version']; + } + + /** + * Returns the FacebookApp entity. + * + * @return FacebookApp + */ + public function getApp() + { + return $this->app; + } + + /** + * Returns the FacebookClient service. + * + * @return FacebookClient + */ + public function getClient() + { + return $this->client; + } + + /** + * Returns the OAuth 2.0 client service. + * + * @return OAuth2Client + */ + public function getOAuth2Client() + { + if (!$this->oAuth2Client instanceof OAuth2Client) { + $app = $this->getApp(); + $client = $this->getClient(); + $this->oAuth2Client = new OAuth2Client($app, $client, $this->defaultGraphVersion); + } + + return $this->oAuth2Client; + } + + /** + * Returns the last response returned from Graph. + * + * @return FacebookResponse|FacebookBatchResponse|null + */ + public function getLastResponse() + { + return $this->lastResponse; + } + + /** + * Returns the URL detection handler. + * + * @return UrlDetectionInterface + */ + public function getUrlDetectionHandler() + { + return $this->urlDetectionHandler; + } + + /** + * Changes the URL detection handler. + * + * @param UrlDetectionInterface $urlDetectionHandler + */ + private function setUrlDetectionHandler(UrlDetectionInterface $urlDetectionHandler) + { + $this->urlDetectionHandler = $urlDetectionHandler; + } + + /** + * Returns the default AccessToken entity. + * + * @return AccessToken|null + */ + public function getDefaultAccessToken() + { + return $this->defaultAccessToken; + } + + /** + * Sets the default access token to use with requests. + * + * @param AccessToken|string $accessToken The access token to save. + * + * @throws \InvalidArgumentException + */ + public function setDefaultAccessToken($accessToken) + { + if (is_string($accessToken)) { + $this->defaultAccessToken = new AccessToken($accessToken); + + return; + } + + if ($accessToken instanceof AccessToken) { + $this->defaultAccessToken = $accessToken; + + return; + } + + throw new \InvalidArgumentException('The default access token must be of type "string" or Facebook\AccessToken'); + } + + /** + * Returns the default Graph version. + * + * @return string + */ + public function getDefaultGraphVersion() + { + return $this->defaultGraphVersion; + } + + /** + * Returns the redirect login helper. + * + * @return FacebookRedirectLoginHelper + */ + public function getRedirectLoginHelper() + { + return new FacebookRedirectLoginHelper( + $this->getOAuth2Client(), + $this->persistentDataHandler, + $this->urlDetectionHandler, + $this->pseudoRandomStringGenerator + ); + } + + /** + * Returns the JavaScript helper. + * + * @return FacebookJavaScriptHelper + */ + public function getJavaScriptHelper() + { + return new FacebookJavaScriptHelper($this->app, $this->client, $this->defaultGraphVersion); + } + + /** + * Returns the canvas helper. + * + * @return FacebookCanvasHelper + */ + public function getCanvasHelper() + { + return new FacebookCanvasHelper($this->app, $this->client, $this->defaultGraphVersion); + } + + /** + * Returns the page tab helper. + * + * @return FacebookPageTabHelper + */ + public function getPageTabHelper() + { + return new FacebookPageTabHelper($this->app, $this->client, $this->defaultGraphVersion); + } + + /** + * Sends a GET request to Graph and returns the result. + * + * @param string $endpoint + * @param AccessToken|string|null $accessToken + * @param string|null $eTag + * @param string|null $graphVersion + * + * @return FacebookResponse + * + * @throws FacebookSDKException + */ + public function get($endpoint, $accessToken = null, $eTag = null, $graphVersion = null) + { + return $this->sendRequest( + 'GET', + $endpoint, + $params = [], + $accessToken, + $eTag, + $graphVersion + ); + } + + /** + * Sends a POST request to Graph and returns the result. + * + * @param string $endpoint + * @param array $params + * @param AccessToken|string|null $accessToken + * @param string|null $eTag + * @param string|null $graphVersion + * + * @return FacebookResponse + * + * @throws FacebookSDKException + */ + public function post($endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null) + { + return $this->sendRequest( + 'POST', + $endpoint, + $params, + $accessToken, + $eTag, + $graphVersion + ); + } + + /** + * Sends a DELETE request to Graph and returns the result. + * + * @param string $endpoint + * @param array $params + * @param AccessToken|string|null $accessToken + * @param string|null $eTag + * @param string|null $graphVersion + * + * @return FacebookResponse + * + * @throws FacebookSDKException + */ + public function delete($endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null) + { + return $this->sendRequest( + 'DELETE', + $endpoint, + $params, + $accessToken, + $eTag, + $graphVersion + ); + } + + /** + * Sends a request to Graph for the next page of results. + * + * @param GraphEdge $graphEdge The GraphEdge to paginate over. + * + * @return GraphEdge|null + * + * @throws FacebookSDKException + */ + public function next(GraphEdge $graphEdge) + { + return $this->getPaginationResults($graphEdge, 'next'); + } + + /** + * Sends a request to Graph for the previous page of results. + * + * @param GraphEdge $graphEdge The GraphEdge to paginate over. + * + * @return GraphEdge|null + * + * @throws FacebookSDKException + */ + public function previous(GraphEdge $graphEdge) + { + return $this->getPaginationResults($graphEdge, 'previous'); + } + + /** + * Sends a request to Graph for the next page of results. + * + * @param GraphEdge $graphEdge The GraphEdge to paginate over. + * @param string $direction The direction of the pagination: next|previous. + * + * @return GraphEdge|null + * + * @throws FacebookSDKException + */ + public function getPaginationResults(GraphEdge $graphEdge, $direction) + { + $paginationRequest = $graphEdge->getPaginationRequest($direction); + if (!$paginationRequest) { + return null; + } + + $this->lastResponse = $this->client->sendRequest($paginationRequest); + + // Keep the same GraphNode subclass + $subClassName = $graphEdge->getSubClassName(); + $graphEdge = $this->lastResponse->getGraphEdge($subClassName, false); + + return count($graphEdge) > 0 ? $graphEdge : null; + } + + /** + * Sends a request to Graph and returns the result. + * + * @param string $method + * @param string $endpoint + * @param array $params + * @param AccessToken|string|null $accessToken + * @param string|null $eTag + * @param string|null $graphVersion + * + * @return FacebookResponse + * + * @throws FacebookSDKException + */ + public function sendRequest($method, $endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null) + { + $accessToken = $accessToken ?: $this->defaultAccessToken; + $graphVersion = $graphVersion ?: $this->defaultGraphVersion; + $request = $this->request($method, $endpoint, $params, $accessToken, $eTag, $graphVersion); + + return $this->lastResponse = $this->client->sendRequest($request); + } + + /** + * Sends a batched request to Graph and returns the result. + * + * @param array $requests + * @param AccessToken|string|null $accessToken + * @param string|null $graphVersion + * + * @return FacebookBatchResponse + * + * @throws FacebookSDKException + */ + public function sendBatchRequest(array $requests, $accessToken = null, $graphVersion = null) + { + $accessToken = $accessToken ?: $this->defaultAccessToken; + $graphVersion = $graphVersion ?: $this->defaultGraphVersion; + $batchRequest = new FacebookBatchRequest( + $this->app, + $requests, + $accessToken, + $graphVersion + ); + + return $this->lastResponse = $this->client->sendBatchRequest($batchRequest); + } + + /** + * Instantiates an empty FacebookBatchRequest entity. + * + * @param AccessToken|string|null $accessToken The top-level access token. Requests with no access token + * will fallback to this. + * @param string|null $graphVersion The Graph API version to use. + * @return FacebookBatchRequest + */ + public function newBatchRequest($accessToken = null, $graphVersion = null) + { + $accessToken = $accessToken ?: $this->defaultAccessToken; + $graphVersion = $graphVersion ?: $this->defaultGraphVersion; + + return new FacebookBatchRequest( + $this->app, + [], + $accessToken, + $graphVersion + ); + } + + /** + * Instantiates a new FacebookRequest entity. + * + * @param string $method + * @param string $endpoint + * @param array $params + * @param AccessToken|string|null $accessToken + * @param string|null $eTag + * @param string|null $graphVersion + * + * @return FacebookRequest + * + * @throws FacebookSDKException + */ + public function request($method, $endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null) + { + $accessToken = $accessToken ?: $this->defaultAccessToken; + $graphVersion = $graphVersion ?: $this->defaultGraphVersion; + + return new FacebookRequest( + $this->app, + $accessToken, + $method, + $endpoint, + $params, + $eTag, + $graphVersion + ); + } + + /** + * Factory to create FacebookFile's. + * + * @param string $pathToFile + * + * @return FacebookFile + * + * @throws FacebookSDKException + */ + public function fileToUpload($pathToFile) + { + return new FacebookFile($pathToFile); + } + + /** + * Factory to create FacebookVideo's. + * + * @param string $pathToFile + * + * @return FacebookVideo + * + * @throws FacebookSDKException + */ + public function videoToUpload($pathToFile) + { + return new FacebookVideo($pathToFile); + } + + /** + * Upload a video in chunks. + * + * @param int $target The id of the target node before the /videos edge. + * @param string $pathToFile The full path to the file. + * @param array $metadata The metadata associated with the video file. + * @param string|null $accessToken The access token. + * @param int $maxTransferTries The max times to retry a failed upload chunk. + * @param string|null $graphVersion The Graph API version to use. + * + * @return array + * + * @throws FacebookSDKException + */ + public function uploadVideo($target, $pathToFile, $metadata = [], $accessToken = null, $maxTransferTries = 5, $graphVersion = null) + { + $accessToken = $accessToken ?: $this->defaultAccessToken; + $graphVersion = $graphVersion ?: $this->defaultGraphVersion; + + $uploader = new FacebookResumableUploader($this->app, $this->client, $accessToken, $graphVersion); + $endpoint = '/'.$target.'/videos'; + $file = $this->videoToUpload($pathToFile); + $chunk = $uploader->start($endpoint, $file); + + do { + $chunk = $this->maxTriesTransfer($uploader, $endpoint, $chunk, $maxTransferTries); + } while (!$chunk->isLastChunk()); + + return [ + 'video_id' => $chunk->getVideoId(), + 'success' => $uploader->finish($endpoint, $chunk->getUploadSessionId(), $metadata), + ]; + } + + /** + * Attempts to upload a chunk of a file in $retryCountdown tries. + * + * @param FacebookResumableUploader $uploader + * @param string $endpoint + * @param FacebookTransferChunk $chunk + * @param int $retryCountdown + * + * @return FacebookTransferChunk + * + * @throws FacebookSDKException + */ + private function maxTriesTransfer(FacebookResumableUploader $uploader, $endpoint, FacebookTransferChunk $chunk, $retryCountdown) + { + $newChunk = $uploader->transfer($endpoint, $chunk, $retryCountdown < 1); + + if ($newChunk !== $chunk) { + return $newChunk; + } + + $retryCountdown--; + + // If transfer() returned the same chunk entity, the transfer failed but is resumable. + return $this->maxTriesTransfer($uploader, $endpoint, $chunk, $retryCountdown); + } +} diff --git a/src/Facebook/FacebookApp.php b/src/Facebook/FacebookApp.php new file mode 100644 index 000000000..804c9bb56 --- /dev/null +++ b/src/Facebook/FacebookApp.php @@ -0,0 +1,110 @@ +id = (string) $id; + $this->secret = $secret; + } + + /** + * Returns the app ID. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the app secret. + * + * @return string + */ + public function getSecret() + { + return $this->secret; + } + + /** + * Returns an app access token. + * + * @return AccessToken + */ + public function getAccessToken() + { + return new AccessToken($this->id . '|' . $this->secret); + } + + /** + * Serializes the FacebookApp entity as a string. + * + * @return string + */ + public function serialize() + { + return implode('|', [$this->id, $this->secret]); + } + + /** + * Unserializes a string as a FacebookApp entity. + * + * @param string $serialized + */ + public function unserialize($serialized) + { + list($id, $secret) = explode('|', $serialized); + + $this->__construct($id, $secret); + } +} diff --git a/src/Facebook/FacebookBatchRequest.php b/src/Facebook/FacebookBatchRequest.php new file mode 100644 index 000000000..9297e77d7 --- /dev/null +++ b/src/Facebook/FacebookBatchRequest.php @@ -0,0 +1,322 @@ +add($requests); + } + + /** + * Adds a new request to the array. + * + * @param FacebookRequest|array $request + * @param string|null|array $options Array of batch request options e.g. 'name', 'omit_response_on_success'. + * If a string is given, it is the value of the 'name' option. + * + * @return FacebookBatchRequest + * + * @throws \InvalidArgumentException + */ + public function add($request, $options = null) + { + if (is_array($request)) { + foreach ($request as $key => $req) { + $this->add($req, $key); + } + + return $this; + } + + if (!$request instanceof FacebookRequest) { + throw new \InvalidArgumentException('Argument for add() must be of type array or FacebookRequest.'); + } + + if (null === $options) { + $options = []; + } elseif (!is_array($options)) { + $options = ['name' => $options]; + } + + $this->addFallbackDefaults($request); + + // File uploads + $attachedFiles = $this->extractFileAttachments($request); + + $name = isset($options['name']) ? $options['name'] : null; + + unset($options['name']); + + $requestToAdd = [ + 'name' => $name, + 'request' => $request, + 'options' => $options, + 'attached_files' => $attachedFiles, + ]; + + $this->requests[] = $requestToAdd; + + return $this; + } + + /** + * Ensures that the FacebookApp and access token fall back when missing. + * + * @param FacebookRequest $request + * + * @throws FacebookSDKException + */ + public function addFallbackDefaults(FacebookRequest $request) + { + if (!$request->getApp()) { + $app = $this->getApp(); + if (!$app) { + throw new FacebookSDKException('Missing FacebookApp on FacebookRequest and no fallback detected on FacebookBatchRequest.'); + } + $request->setApp($app); + } + + if (!$request->getAccessToken()) { + $accessToken = $this->getAccessToken(); + if (!$accessToken) { + throw new FacebookSDKException('Missing access token on FacebookRequest and no fallback detected on FacebookBatchRequest.'); + } + $request->setAccessToken($accessToken); + } + } + + /** + * Extracts the files from a request. + * + * @param FacebookRequest $request + * + * @return string|null + * + * @throws FacebookSDKException + */ + public function extractFileAttachments(FacebookRequest $request) + { + if (!$request->containsFileUploads()) { + return null; + } + + $files = $request->getFiles(); + $fileNames = []; + foreach ($files as $file) { + $fileName = uniqid(); + $this->addFile($fileName, $file); + $fileNames[] = $fileName; + } + + $request->resetFiles(); + + // @TODO Does Graph support multiple uploads on one endpoint? + return implode(',', $fileNames); + } + + /** + * Return the FacebookRequest entities. + * + * @return array + */ + public function getRequests() + { + return $this->requests; + } + + /** + * Prepares the requests to be sent as a batch request. + */ + public function prepareRequestsForBatch() + { + $this->validateBatchRequestCount(); + + $params = [ + 'batch' => $this->convertRequestsToJson(), + 'include_headers' => true, + ]; + $this->setParams($params); + } + + /** + * Converts the requests into a JSON(P) string. + * + * @return string + */ + public function convertRequestsToJson() + { + $requests = []; + foreach ($this->requests as $request) { + $options = []; + + if (null !== $request['name']) { + $options['name'] = $request['name']; + } + + $options += $request['options']; + + $requests[] = $this->requestEntityToBatchArray($request['request'], $options, $request['attached_files']); + } + + return json_encode($requests); + } + + /** + * Validate the request count before sending them as a batch. + * + * @throws FacebookSDKException + */ + public function validateBatchRequestCount() + { + $batchCount = count($this->requests); + if ($batchCount === 0) { + throw new FacebookSDKException('There are no batch requests to send.'); + } elseif ($batchCount > 50) { + // Per: https://developers.facebook.com/docs/graph-api/making-multiple-requests#limits + throw new FacebookSDKException('You cannot send more than 50 batch requests at a time.'); + } + } + + /** + * Converts a Request entity into an array that is batch-friendly. + * + * @param FacebookRequest $request The request entity to convert. + * @param string|null|array $options Array of batch request options e.g. 'name', 'omit_response_on_success'. + * If a string is given, it is the value of the 'name' option. + * @param string|null $attachedFiles Names of files associated with the request. + * + * @return array + */ + public function requestEntityToBatchArray(FacebookRequest $request, $options = null, $attachedFiles = null) + { + + if (null === $options) { + $options = []; + } elseif (!is_array($options)) { + $options = ['name' => $options]; + } + + $compiledHeaders = []; + $headers = $request->getHeaders(); + foreach ($headers as $name => $value) { + $compiledHeaders[] = $name . ': ' . $value; + } + + $batch = [ + 'headers' => $compiledHeaders, + 'method' => $request->getMethod(), + 'relative_url' => $request->getUrl(), + ]; + + // Since file uploads are moved to the root request of a batch request, + // the child requests will always be URL-encoded. + $body = $request->getUrlEncodedBody()->getBody(); + if ($body) { + $batch['body'] = $body; + } + + $batch += $options; + + if (null !== $attachedFiles) { + $batch['attached_files'] = $attachedFiles; + } + + return $batch; + } + + /** + * Get an iterator for the items. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->requests); + } + + /** + * @inheritdoc + */ + public function offsetSet($offset, $value) + { + $this->add($value, $offset); + } + + /** + * @inheritdoc + */ + public function offsetExists($offset) + { + return isset($this->requests[$offset]); + } + + /** + * @inheritdoc + */ + public function offsetUnset($offset) + { + unset($this->requests[$offset]); + } + + /** + * @inheritdoc + */ + public function offsetGet($offset) + { + return isset($this->requests[$offset]) ? $this->requests[$offset] : null; + } +} diff --git a/src/Facebook/FacebookBatchResponse.php b/src/Facebook/FacebookBatchResponse.php new file mode 100644 index 000000000..8e1464c98 --- /dev/null +++ b/src/Facebook/FacebookBatchResponse.php @@ -0,0 +1,174 @@ +batchRequest = $batchRequest; + + $request = $response->getRequest(); + $body = $response->getBody(); + $httpStatusCode = $response->getHttpStatusCode(); + $headers = $response->getHeaders(); + parent::__construct($request, $body, $httpStatusCode, $headers); + + $responses = $response->getDecodedBody(); + $this->setResponses($responses); + } + + /** + * Returns an array of FacebookResponse entities. + * + * @return array + */ + public function getResponses() + { + return $this->responses; + } + + /** + * The main batch response will be an array of requests so + * we need to iterate over all the responses. + * + * @param array $responses + */ + public function setResponses(array $responses) + { + $this->responses = []; + + foreach ($responses as $key => $graphResponse) { + $this->addResponse($key, $graphResponse); + } + } + + /** + * Add a response to the list. + * + * @param int $key + * @param array|null $response + */ + public function addResponse($key, $response) + { + $originalRequestName = isset($this->batchRequest[$key]['name']) ? $this->batchRequest[$key]['name'] : $key; + $originalRequest = isset($this->batchRequest[$key]['request']) ? $this->batchRequest[$key]['request'] : null; + + $httpResponseBody = isset($response['body']) ? $response['body'] : null; + $httpResponseCode = isset($response['code']) ? $response['code'] : null; + // @TODO With PHP 5.5 support, this becomes array_column($response['headers'], 'value', 'name') + $httpResponseHeaders = isset($response['headers']) ? $this->normalizeBatchHeaders($response['headers']) : []; + + $this->responses[$originalRequestName] = new FacebookResponse( + $originalRequest, + $httpResponseBody, + $httpResponseCode, + $httpResponseHeaders + ); + } + + /** + * @inheritdoc + */ + public function getIterator() + { + return new ArrayIterator($this->responses); + } + + /** + * @inheritdoc + */ + public function offsetSet($offset, $value) + { + $this->addResponse($offset, $value); + } + + /** + * @inheritdoc + */ + public function offsetExists($offset) + { + return isset($this->responses[$offset]); + } + + /** + * @inheritdoc + */ + public function offsetUnset($offset) + { + unset($this->responses[$offset]); + } + + /** + * @inheritdoc + */ + public function offsetGet($offset) + { + return isset($this->responses[$offset]) ? $this->responses[$offset] : null; + } + + /** + * Converts the batch header array into a standard format. + * @TODO replace with array_column() when PHP 5.5 is supported. + * + * @param array $batchHeaders + * + * @return array + */ + private function normalizeBatchHeaders(array $batchHeaders) + { + $headers = []; + + foreach ($batchHeaders as $header) { + $headers[$header['name']] = $header['value']; + } + + return $headers; + } +} diff --git a/src/Facebook/FacebookClient.php b/src/Facebook/FacebookClient.php new file mode 100644 index 000000000..dbf759238 --- /dev/null +++ b/src/Facebook/FacebookClient.php @@ -0,0 +1,250 @@ +httpClientHandler = $httpClientHandler ?: $this->detectHttpClientHandler(); + $this->enableBetaMode = $enableBeta; + } + + /** + * Sets the HTTP client handler. + * + * @param FacebookHttpClientInterface $httpClientHandler + */ + public function setHttpClientHandler(FacebookHttpClientInterface $httpClientHandler) + { + $this->httpClientHandler = $httpClientHandler; + } + + /** + * Returns the HTTP client handler. + * + * @return FacebookHttpClientInterface + */ + public function getHttpClientHandler() + { + return $this->httpClientHandler; + } + + /** + * Detects which HTTP client handler to use. + * + * @return FacebookHttpClientInterface + */ + public function detectHttpClientHandler() + { + return extension_loaded('curl') ? new FacebookCurlHttpClient() : new FacebookStreamHttpClient(); + } + + /** + * Toggle beta mode. + * + * @param boolean $betaMode + */ + public function enableBetaMode($betaMode = true) + { + $this->enableBetaMode = $betaMode; + } + + /** + * Returns the base Graph URL. + * + * @param boolean $postToVideoUrl Post to the video API if videos are being uploaded. + * + * @return string + */ + public function getBaseGraphUrl($postToVideoUrl = false) + { + if ($postToVideoUrl) { + return $this->enableBetaMode ? static::BASE_GRAPH_VIDEO_URL_BETA : static::BASE_GRAPH_VIDEO_URL; + } + + return $this->enableBetaMode ? static::BASE_GRAPH_URL_BETA : static::BASE_GRAPH_URL; + } + + /** + * Prepares the request for sending to the client handler. + * + * @param FacebookRequest $request + * + * @return array + */ + public function prepareRequestMessage(FacebookRequest $request) + { + $postToVideoUrl = $request->containsVideoUploads(); + $url = $this->getBaseGraphUrl($postToVideoUrl) . $request->getUrl(); + + // If we're sending files they should be sent as multipart/form-data + if ($request->containsFileUploads()) { + $requestBody = $request->getMultipartBody(); + $request->setHeaders([ + 'Content-Type' => 'multipart/form-data; boundary=' . $requestBody->getBoundary(), + ]); + } else { + $requestBody = $request->getUrlEncodedBody(); + $request->setHeaders([ + 'Content-Type' => 'application/x-www-form-urlencoded', + ]); + } + + return [ + $url, + $request->getMethod(), + $request->getHeaders(), + $requestBody->getBody(), + ]; + } + + /** + * Makes the request to Graph and returns the result. + * + * @param FacebookRequest $request + * + * @return FacebookResponse + * + * @throws FacebookSDKException + */ + public function sendRequest(FacebookRequest $request) + { + if (get_class($request) === 'Facebook\FacebookRequest') { + $request->validateAccessToken(); + } + + list($url, $method, $headers, $body) = $this->prepareRequestMessage($request); + + // Since file uploads can take a while, we need to give more time for uploads + $timeOut = static::DEFAULT_REQUEST_TIMEOUT; + if ($request->containsFileUploads()) { + $timeOut = static::DEFAULT_FILE_UPLOAD_REQUEST_TIMEOUT; + } elseif ($request->containsVideoUploads()) { + $timeOut = static::DEFAULT_VIDEO_UPLOAD_REQUEST_TIMEOUT; + } + + // Should throw `FacebookSDKException` exception on HTTP client error. + // Don't catch to allow it to bubble up. + $rawResponse = $this->httpClientHandler->send($url, $method, $body, $headers, $timeOut); + + static::$requestCount++; + + $returnResponse = new FacebookResponse( + $request, + $rawResponse->getBody(), + $rawResponse->getHttpResponseCode(), + $rawResponse->getHeaders() + ); + + if ($returnResponse->isError()) { + throw $returnResponse->getThrownException(); + } + + return $returnResponse; + } + + /** + * Makes a batched request to Graph and returns the result. + * + * @param FacebookBatchRequest $request + * + * @return FacebookBatchResponse + * + * @throws FacebookSDKException + */ + public function sendBatchRequest(FacebookBatchRequest $request) + { + $request->prepareRequestsForBatch(); + $facebookResponse = $this->sendRequest($request); + + return new FacebookBatchResponse($request, $facebookResponse); + } +} diff --git a/src/Facebook/FacebookPageTabHelper.php b/src/Facebook/FacebookPageTabHelper.php deleted file mode 100644 index 60c96e308..000000000 --- a/src/Facebook/FacebookPageTabHelper.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ -class FacebookPageTabHelper extends FacebookCanvasLoginHelper -{ - - /** - * @var array|null - */ - protected $pageData; - - /** - * Initialize the helper and process available signed request data. - * - * @param string|null $appId - * @param string|null $appSecret - */ - public function __construct($appId = null, $appSecret = null) - { - parent::__construct($appId, $appSecret); - - if (!$this->signedRequest) { - return; - } - - $this->pageData = $this->signedRequest->get('page'); - } - - /** - * Returns a value from the page data. - * - * @param string $key - * @param mixed|null $default - * - * @return mixed|null - */ - public function getPageData($key, $default = null) - { - if (isset($this->pageData[$key])) { - return $this->pageData[$key]; - } - return $default; - } - - /** - * Returns true if the page is liked by the user. - * - * @return boolean - */ - public function isLiked() - { - return $this->getPageData('liked') === true; - } - - /** - * Returns true if the user is an admin. - * - * @return boolean - */ - public function isAdmin() - { - return $this->getPageData('admin') === true; - } - - /** - * Returns the page id if available. - * - * @return string|null - */ - public function getPageId() - { - return $this->getPageData('id'); - } - -} diff --git a/src/Facebook/FacebookRedirectLoginHelper.php b/src/Facebook/FacebookRedirectLoginHelper.php deleted file mode 100644 index c29d509f7..000000000 --- a/src/Facebook/FacebookRedirectLoginHelper.php +++ /dev/null @@ -1,277 +0,0 @@ - - * @author David Poll - */ -class FacebookRedirectLoginHelper -{ - - /** - * @var string The application id - */ - private $appId; - - /** - * @var string The application secret - */ - private $appSecret; - - /** - * @var string The redirect URL for the application - */ - private $redirectUrl; - - /** - * @var string Prefix to use for session variables - */ - private $sessionPrefix = 'FBRLH_'; - - /** - * @var string State token for CSRF validation - */ - protected $state; - - /** - * @var boolean Toggle for PHP session status check - */ - protected $checkForSessionStatus = true; - - /** - * Constructs a RedirectLoginHelper for a given appId and redirectUrl. - * - * @param string $redirectUrl The URL Facebook should redirect users to - * after login - * @param string $appId The application id - * @param string $appSecret The application secret - */ - public function __construct($redirectUrl, $appId = null, $appSecret = null) - { - $this->appId = FacebookSession::_getTargetAppId($appId); - $this->appSecret = FacebookSession::_getTargetAppSecret($appSecret); - $this->redirectUrl = $redirectUrl; - } - - /** - * Stores CSRF state and returns a URL to which the user should be sent to - * in order to continue the login process with Facebook. The - * provided redirectUrl should invoke the handleRedirect method. - * - * @param array $scope List of permissions to request during login - * @param string $version Optional Graph API version if not default (v2.0) - * - * @return string - */ - public function getLoginUrl($scope = array(), $version = null) - { - $version = ($version ?: FacebookRequest::GRAPH_API_VERSION); - $this->state = $this->random(16); - $this->storeState($this->state); - $params = array( - 'client_id' => $this->appId, - 'redirect_uri' => $this->redirectUrl, - 'state' => $this->state, - 'sdk' => 'php-sdk-' . FacebookRequest::VERSION, - 'scope' => implode(',', $scope) - ); - return 'https://www.facebook.com/' . $version . '/dialog/oauth?' . - http_build_query($params, null, '&'); - } - - /** - * Returns the URL to send the user in order to log out of Facebook. - * - * @param FacebookSession $session The session that will be logged out - * @param string $next The url Facebook should redirect the user to after - * a successful logout - * - * @return string - */ - public function getLogoutUrl(FacebookSession $session, $next) - { - $params = array( - 'next' => $next, - 'access_token' => $session->getToken() - ); - return 'https://www.facebook.com/logout.php?' . http_build_query($params, null, '&'); - } - - /** - * Handles a response from Facebook, including a CSRF check, and returns a - * FacebookSession. - * - * @return FacebookSession|null - */ - public function getSessionFromRedirect() - { - $this->loadState(); - if ($this->isValidRedirect()) { - $params = array( - 'client_id' => FacebookSession::_getTargetAppId($this->appId), - 'redirect_uri' => $this->redirectUrl, - 'client_secret' => - FacebookSession::_getTargetAppSecret($this->appSecret), - 'code' => $this->getCode() - ); - $response = (new FacebookRequest( - FacebookSession::newAppSession($this->appId, $this->appSecret), - 'GET', - '/oauth/access_token', - $params - ))->execute()->getResponse(); - if (isset($response['access_token'])) { - return new FacebookSession($response['access_token']); - } - } - return null; - } - - /** - * Check if a redirect has a valid state. - * - * @return bool - */ - protected function isValidRedirect() - { - return $this->getCode() && isset($_GET['state']) - && $_GET['state'] == $this->state; - } - - /** - * Return the code. - * - * @return string|null - */ - protected function getCode() - { - return isset($_GET['code']) ? $_GET['code'] : null; - } - - /** - * Stores a state string in session storage for CSRF protection. - * Developers should subclass and override this method if they want to store - * this state in a different location. - * - * @param string $state - * - * @throws FacebookSDKException - */ - protected function storeState($state) - { - if ($this->checkForSessionStatus === true - && session_status() !== PHP_SESSION_ACTIVE) { - throw new FacebookSDKException( - 'Session not active, could not store state.', 720 - ); - } - $_SESSION[$this->sessionPrefix . 'state'] = $state; - } - - /** - * Loads a state string from session storage for CSRF validation. May return - * null if no object exists. Developers should subclass and override this - * method if they want to load the state from a different location. - * - * @return string|null - * - * @throws FacebookSDKException - */ - protected function loadState() - { - if ($this->checkForSessionStatus === true - && session_status() !== PHP_SESSION_ACTIVE) { - throw new FacebookSDKException( - 'Session not active, could not load state.', 721 - ); - } - if (isset($_SESSION[$this->sessionPrefix . 'state'])) { - $this->state = $_SESSION[$this->sessionPrefix . 'state']; - return $this->state; - } - return null; - } - - /** - * Generate a cryptographically secure pseudrandom number - * - * @param integer $bytes - number of bytes to return - * - * @return string - * - * @throws FacebookSDKException - * - * @todo Support Windows platforms - */ - public function random($bytes) - { - if (!is_numeric($bytes)) { - throw new FacebookSDKException( - "random() expects an integer" - ); - } - if ($bytes < 1) { - throw new FacebookSDKException( - "random() expects an integer greater than zero" - ); - } - $buf = ''; - // http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/ - if (is_readable('/dev/urandom')) { - $fp = fopen('/dev/urandom', 'rb'); - if ($fp !== FALSE) { - $buf = fread($fp, $bytes); - fclose($fp); - if($buf !== FALSE) { - return bin2hex($buf); - } - } - } - - if (function_exists('mcrypt_create_iv')) { - $buf = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM); - if ($buf !== FALSE) { - return bin2hex($buf); - } - } - - while (strlen($buf) < $bytes) { - $buf .= md5(uniqid(mt_rand(), true), true); - // We are appending raw binary - } - return bin2hex(substr($buf, 0, $bytes)); - } - - /** - * Disables the session_status() check when using $_SESSION - */ - public function disableSessionStatusCheck() - { - $this->checkForSessionStatus = false; - } - -} diff --git a/src/Facebook/FacebookRequest.php b/src/Facebook/FacebookRequest.php index bab9466f4..2b1008991 100644 --- a/src/Facebook/FacebookRequest.php +++ b/src/Facebook/FacebookRequest.php @@ -1,6 +1,6 @@ - * @author David Poll */ class FacebookRequest { + /** + * @var FacebookApp The Facebook app entity. + */ + protected $app; + + /** + * @var string|null The access token to use for this request. + */ + protected $accessToken; + + /** + * @var string The HTTP method for this request. + */ + protected $method; + + /** + * @var string The Graph endpoint for this request. + */ + protected $endpoint; + + /** + * @var array The headers to send with this request. + */ + protected $headers = []; + + /** + * @var array The parameters to send with this request. + */ + protected $params = []; + + /** + * @var array The files to send with this request. + */ + protected $files = []; + + /** + * @var string ETag to send with this request. + */ + protected $eTag; + + /** + * @var string Graph version to use for this request. + */ + protected $graphVersion; + + /** + * Creates a new Request entity. + * + * @param FacebookApp|null $app + * @param AccessToken|string|null $accessToken + * @param string|null $method + * @param string|null $endpoint + * @param array|null $params + * @param string|null $eTag + * @param string|null $graphVersion + */ + public function __construct(FacebookApp $app = null, $accessToken = null, $method = null, $endpoint = null, array $params = [], $eTag = null, $graphVersion = null) + { + $this->setApp($app); + $this->setAccessToken($accessToken); + $this->setMethod($method); + $this->setEndpoint($endpoint); + $this->setParams($params); + $this->setETag($eTag); + $this->graphVersion = $graphVersion ?: Facebook::DEFAULT_GRAPH_VERSION; + } + + /** + * Set the access token for this request. + * + * @param AccessToken|string|null + * + * @return FacebookRequest + */ + public function setAccessToken($accessToken) + { + $this->accessToken = $accessToken; + if ($accessToken instanceof AccessToken) { + $this->accessToken = $accessToken->getValue(); + } + + return $this; + } + + /** + * Sets the access token with one harvested from a URL or POST params. + * + * @param string $accessToken The access token. + * + * @return FacebookRequest + * + * @throws FacebookSDKException + */ + public function setAccessTokenFromParams($accessToken) + { + $existingAccessToken = $this->getAccessToken(); + if (!$existingAccessToken) { + $this->setAccessToken($accessToken); + } elseif ($accessToken !== $existingAccessToken) { + throw new FacebookSDKException('Access token mismatch. The access token provided in the FacebookRequest and the one provided in the URL or POST params do not match.'); + } + + return $this; + } + + /** + * Return the access token for this request. + * + * @return string|null + */ + public function getAccessToken() + { + return $this->accessToken; + } + + /** + * Return the access token for this request as an AccessToken entity. + * + * @return AccessToken|null + */ + public function getAccessTokenEntity() + { + return $this->accessToken ? new AccessToken($this->accessToken) : null; + } + + /** + * Set the FacebookApp entity used for this request. + * + * @param FacebookApp|null $app + */ + public function setApp(FacebookApp $app = null) + { + $this->app = $app; + } + + /** + * Return the FacebookApp entity used for this request. + * + * @return FacebookApp + */ + public function getApp() + { + return $this->app; + } + + /** + * Generate an app secret proof to sign this request. + * + * @return string|null + */ + public function getAppSecretProof() + { + if (!$accessTokenEntity = $this->getAccessTokenEntity()) { + return null; + } + + return $accessTokenEntity->getAppSecretProof($this->app->getSecret()); + } + + /** + * Validate that an access token exists for this request. + * + * @throws FacebookSDKException + */ + public function validateAccessToken() + { + $accessToken = $this->getAccessToken(); + if (!$accessToken) { + throw new FacebookSDKException('You must provide an access token.'); + } + } + + /** + * Set the HTTP method for this request. + * + * @param string + */ + public function setMethod($method) + { + $this->method = strtoupper($method); + } + + /** + * Return the HTTP method for this request. + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Validate that the HTTP method is set. + * + * @throws FacebookSDKException + */ + public function validateMethod() + { + if (!$this->method) { + throw new FacebookSDKException('HTTP method not specified.'); + } + + if (!in_array($this->method, ['GET', 'POST', 'DELETE'])) { + throw new FacebookSDKException('Invalid HTTP method specified.'); + } + } + + /** + * Set the endpoint for this request. + * + * @param string + * + * @return FacebookRequest + * + * @throws FacebookSDKException + */ + public function setEndpoint($endpoint) + { + // Harvest the access token from the endpoint to keep things in sync + $params = FacebookUrlManipulator::getParamsAsArray($endpoint); + if (isset($params['access_token'])) { + $this->setAccessTokenFromParams($params['access_token']); + } + + // Clean the token & app secret proof from the endpoint. + $filterParams = ['access_token', 'appsecret_proof']; + $this->endpoint = FacebookUrlManipulator::removeParamsFromUrl($endpoint, $filterParams); + + return $this; + } + + /** + * Return the endpoint for this request. + * + * @return string + */ + public function getEndpoint() + { + // For batch requests, this will be empty + return $this->endpoint; + } + + /** + * Generate and return the headers for this request. + * + * @return array + */ + public function getHeaders() + { + $headers = static::getDefaultHeaders(); + + if ($this->eTag) { + $headers['If-None-Match'] = $this->eTag; + } + + return array_merge($this->headers, $headers); + } + + /** + * Set the headers for this request. + * + * @param array $headers + */ + public function setHeaders(array $headers) + { + $this->headers = array_merge($this->headers, $headers); + } + + /** + * Sets the eTag value. + * + * @param string $eTag + */ + public function setETag($eTag) + { + $this->eTag = $eTag; + } + + /** + * Set the params for this request. + * + * @param array $params + * + * @return FacebookRequest + * + * @throws FacebookSDKException + */ + public function setParams(array $params = []) + { + if (isset($params['access_token'])) { + $this->setAccessTokenFromParams($params['access_token']); + } + + // Don't let these buggers slip in. + unset($params['access_token'], $params['appsecret_proof']); + + // @TODO Refactor code above with this + //$params = $this->sanitizeAuthenticationParams($params); + $params = $this->sanitizeFileParams($params); + $this->dangerouslySetParams($params); + + return $this; + } + + /** + * Set the params for this request without filtering them first. + * + * @param array $params + * + * @return FacebookRequest + */ + public function dangerouslySetParams(array $params = []) + { + $this->params = array_merge($this->params, $params); + + return $this; + } + + /** + * Iterate over the params and pull out the file uploads. + * + * @param array $params + * + * @return array + */ + public function sanitizeFileParams(array $params) + { + foreach ($params as $key => $value) { + if ($value instanceof FacebookFile) { + $this->addFile($key, $value); + unset($params[$key]); + } + } + + return $params; + } + + /** + * Add a file to be uploaded. + * + * @param string $key + * @param FacebookFile $file + */ + public function addFile($key, FacebookFile $file) + { + $this->files[$key] = $file; + } - /** - * @const string Version number of the Facebook PHP SDK. - */ - const VERSION = '4.0.9'; - - /** - * @const string Default Graph API version for requests - */ - const GRAPH_API_VERSION = 'v2.0'; - - /** - * @const string Graph API URL - */ - const BASE_GRAPH_URL = 'https://graph.facebook.com'; - - /** - * @var FacebookSession The session used for this request - */ - private $session; - - /** - * @var string The HTTP method for the request - */ - private $method; - - /** - * @var string The path for the request - */ - private $path; - - /** - * @var array The parameters for the request - */ - private $params; - - /** - * @var string The Graph API version for the request - */ - private $version; - - /** - * @var string ETag sent with the request - */ - private $etag; - - /** - * @var FacebookHttpable HTTP client handler - */ - private static $httpClientHandler; - - /** - * @var int The number of calls that have been made to Graph. - */ - public static $requestCount = 0; - - /** - * getSession - Returns the associated FacebookSession. - * - * @return FacebookSession - */ - public function getSession() - { - return $this->session; - } - - /** - * getPath - Returns the associated path. - * - * @return string - */ - public function getPath() - { - return $this->path; - } - - /** - * getParameters - Returns the associated parameters. - * - * @return array - */ - public function getParameters() - { - return $this->params; - } - - /** - * getMethod - Returns the associated method. - * - * @return string - */ - public function getMethod() - { - return $this->method; - } - - /** - * getETag - Returns the ETag sent with the request. - * - * @return string - */ - public function getETag() - { - return $this->etag; - } - - /** - * setHttpClientHandler - Returns an instance of the HTTP client - * handler - * - * @param \Facebook\HttpClients\FacebookHttpable - */ - public static function setHttpClientHandler(FacebookHttpable $handler) - { - static::$httpClientHandler = $handler; - } - - /** - * getHttpClientHandler - Returns an instance of the HTTP client - * data handler - * - * @return FacebookHttpable - */ - public static function getHttpClientHandler() - { - if (static::$httpClientHandler) { - return static::$httpClientHandler; - } - return function_exists('curl_init') ? new FacebookCurlHttpClient() : new FacebookStreamHttpClient(); - } - - /** - * FacebookRequest - Returns a new request using the given session. optional - * parameters hash will be sent with the request. This object is - * immutable. - * - * @param FacebookSession $session - * @param string $method - * @param string $path - * @param array|null $parameters - * @param string|null $version - * @param string|null $etag - */ - public function __construct( - FacebookSession $session, $method, $path, $parameters = null, $version = null, $etag = null - ) - { - $this->session = $session; - $this->method = $method; - $this->path = $path; - if ($version) { - $this->version = $version; - } else { - $this->version = static::GRAPH_API_VERSION; - } - $this->etag = $etag; - - $params = ($parameters ?: array()); - if ($session - && !isset($params["access_token"])) { - $params["access_token"] = $session->getToken(); - } - if (FacebookSession::useAppSecretProof() - && !isset($params["appsecret_proof"])) { - $params["appsecret_proof"] = $this->getAppSecretProof( - $params["access_token"] - ); - } - $this->params = $params; - } - - /** - * Returns the base Graph URL. - * - * @return string - */ - protected function getRequestURL() - { - return static::BASE_GRAPH_URL . '/' . $this->version . $this->path; - } - - /** - * execute - Makes the request to Facebook and returns the result. - * - * @return FacebookResponse - * - * @throws FacebookSDKException - * @throws FacebookRequestException - */ - public function execute() - { - $url = $this->getRequestURL(); - $params = $this->getParameters(); - - if ($this->method === "GET") { - $url = self::appendParamsToUrl($url, $params); - $params = array(); - } - - $connection = self::getHttpClientHandler(); - $connection->addRequestHeader('User-Agent', 'fb-php-' . self::VERSION); - $connection->addRequestHeader('Accept-Encoding', '*'); // Support all available encodings. - - // ETag - if (isset($this->etag)) { - $connection->addRequestHeader('If-None-Match', $this->etag); - } - - // Should throw `FacebookSDKException` exception on HTTP client error. - // Don't catch to allow it to bubble up. - $result = $connection->send($url, $this->method, $params); - - static::$requestCount++; - - $etagHit = 304 == $connection->getResponseHttpStatusCode(); - - $headers = $connection->getResponseHeaders(); - $etagReceived = isset($headers['ETag']) ? $headers['ETag'] : null; - - $decodedResult = json_decode($result); - if ($decodedResult === null) { - $out = array(); - parse_str($result, $out); - return new FacebookResponse($this, $out, $result, $etagHit, $etagReceived); - } - if (isset($decodedResult->error)) { - throw FacebookRequestException::create( - $result, - $decodedResult->error, - $connection->getResponseHttpStatusCode() - ); - } - - return new FacebookResponse($this, $decodedResult, $result, $etagHit, $etagReceived); - } - - /** - * Generate and return the appsecret_proof value for an access_token - * - * @param string $token - * - * @return string - */ - public function getAppSecretProof($token) - { - return hash_hmac('sha256', $token, FacebookSession::_getTargetAppSecret()); - } - - /** - * appendParamsToUrl - Gracefully appends params to the URL. - * - * @param string $url - * @param array $params - * - * @return string - */ - public static function appendParamsToUrl($url, $params = array()) - { - if (!$params) { - return $url; - } - - if (strpos($url, '?') === false) { - return $url . '?' . http_build_query($params, null, '&'); - } - - list($path, $query_string) = explode('?', $url, 2); - parse_str($query_string, $query_array); - - // Favor params from the original URL over $params - $params = array_merge($params, $query_array); - - return $path . '?' . http_build_query($params, null, '&'); - } + /** + * Removes all the files from the upload queue. + */ + public function resetFiles() + { + $this->files = []; + } + + /** + * Get the list of files to be uploaded. + * + * @return array + */ + public function getFiles() + { + return $this->files; + } + + /** + * Let's us know if there is a file upload with this request. + * + * @return boolean + */ + public function containsFileUploads() + { + return !empty($this->files); + } + /** + * Let's us know if there is a video upload with this request. + * + * @return boolean + */ + public function containsVideoUploads() + { + foreach ($this->files as $file) { + if ($file instanceof FacebookVideo) { + return true; + } + } + + return false; + } + + /** + * Returns the body of the request as multipart/form-data. + * + * @return RequestBodyMultipart + */ + public function getMultipartBody() + { + $params = $this->getPostParams(); + + return new RequestBodyMultipart($params, $this->files); + } + + /** + * Returns the body of the request as URL-encoded. + * + * @return RequestBodyUrlEncoded + */ + public function getUrlEncodedBody() + { + $params = $this->getPostParams(); + + return new RequestBodyUrlEncoded($params); + } + + /** + * Generate and return the params for this request. + * + * @return array + */ + public function getParams() + { + $params = $this->params; + + $accessToken = $this->getAccessToken(); + if ($accessToken) { + $params['access_token'] = $accessToken; + $params['appsecret_proof'] = $this->getAppSecretProof(); + } + + return $params; + } + + /** + * Only return params on POST requests. + * + * @return array + */ + public function getPostParams() + { + if ($this->getMethod() === 'POST') { + return $this->getParams(); + } + + return []; + } + + /** + * The graph version used for this request. + * + * @return string + */ + public function getGraphVersion() + { + return $this->graphVersion; + } + + /** + * Generate and return the URL for this request. + * + * @return string + */ + public function getUrl() + { + $this->validateMethod(); + + $graphVersion = FacebookUrlManipulator::forceSlashPrefix($this->graphVersion); + $endpoint = FacebookUrlManipulator::forceSlashPrefix($this->getEndpoint()); + + $url = $graphVersion . $endpoint; + + if ($this->getMethod() !== 'POST') { + $params = $this->getParams(); + $url = FacebookUrlManipulator::appendParamsToUrl($url, $params); + } + + return $url; + } + + /** + * Return the default headers that every request should use. + * + * @return array + */ + public static function getDefaultHeaders() + { + return [ + 'User-Agent' => 'fb-php-' . Facebook::VERSION, + 'Accept-Encoding' => '*', + ]; + } } diff --git a/src/Facebook/FacebookRequestException.php b/src/Facebook/FacebookRequestException.php deleted file mode 100644 index 8d3fe7167..000000000 --- a/src/Facebook/FacebookRequestException.php +++ /dev/null @@ -1,222 +0,0 @@ - - * @author David Poll - */ -class FacebookRequestException extends FacebookSDKException -{ - - /** - * @var int Status code for the response causing the exception - */ - private $statusCode; - - /** - * @var string Raw response - */ - private $rawResponse; - - /** - * @var array Decoded response - */ - private $responseData; - - /** - * Creates a FacebookRequestException. - * - * @param string $rawResponse The raw response from the Graph API - * @param array $responseData The decoded response from the Graph API - * @param int $statusCode - */ - public function __construct($rawResponse, $responseData, $statusCode) - { - $this->rawResponse = $rawResponse; - $this->statusCode = $statusCode; - $this->responseData = self::convertToArray($responseData); - parent::__construct( - $this->get('message', 'Unknown Exception'), $this->get('code', -1), null - ); - } - - /** - * Process an error payload from the Graph API and return the appropriate - * exception subclass. - * - * @param string $raw the raw response from the Graph API - * @param array $data the decoded response from the Graph API - * @param int $statusCode the HTTP response code - * - * @return FacebookRequestException - */ - public static function create($raw, $data, $statusCode) - { - $data = self::convertToArray($data); - if (!isset($data['error']['code']) && isset($data['code'])) { - $data = array('error' => $data); - } - $code = (isset($data['error']['code']) ? $data['error']['code'] : null); - - if (isset($data['error']['error_subcode'])) { - switch ($data['error']['error_subcode']) { - // Other authentication issues - case 458: - case 459: - case 460: - case 463: - case 464: - case 467: - return new FacebookAuthorizationException($raw, $data, $statusCode); - break; - } - } - - switch ($code) { - // Login status or token expired, revoked, or invalid - case 100: - case 102: - case 190: - return new FacebookAuthorizationException($raw, $data, $statusCode); - break; - - // Server issue, possible downtime - case 1: - case 2: - return new FacebookServerException($raw, $data, $statusCode); - break; - - // API Throttling - case 4: - case 17: - case 341: - return new FacebookThrottleException($raw, $data, $statusCode); - break; - - // Duplicate Post - case 506: - return new FacebookClientException($raw, $data, $statusCode); - break; - } - - // Missing Permissions - if ($code == 10 || ($code >= 200 && $code <= 299)) { - return new FacebookPermissionException($raw, $data, $statusCode); - } - - // OAuth authentication error - if (isset($data['error']['type']) - and $data['error']['type'] === 'OAuthException') { - return new FacebookAuthorizationException($raw, $data, $statusCode); - } - - // All others - return new FacebookOtherException($raw, $data, $statusCode); - } - - /** - * Checks isset and returns that or a default value. - * - * @param string $key - * @param mixed $default - * - * @return mixed - */ - private function get($key, $default = null) - { - if (isset($this->responseData['error'][$key])) { - return $this->responseData['error'][$key]; - } - return $default; - } - - /** - * Returns the HTTP status code - * - * @return int - */ - public function getHttpStatusCode() - { - return $this->statusCode; - } - - /** - * Returns the sub-error code - * - * @return int - */ - public function getSubErrorCode() - { - return $this->get('error_subcode', -1); - } - - /** - * Returns the error type - * - * @return string - */ - public function getErrorType() - { - return $this->get('type', ''); - } - - /** - * Returns the raw response used to create the exception. - * - * @return string - */ - public function getRawResponse() - { - return $this->rawResponse; - } - - /** - * Returns the decoded response used to create the exception. - * - * @return array - */ - public function getResponse() - { - return $this->responseData; - } - - /** - * Converts a stdClass object to an array - * - * @param mixed $object - * - * @return array - */ - private static function convertToArray($object) - { - if ($object instanceof \stdClass) { - return get_object_vars($object); - } - return $object; - } - -} \ No newline at end of file diff --git a/src/Facebook/FacebookResponse.php b/src/Facebook/FacebookResponse.php index e174a9459..251ca2f79 100644 --- a/src/Facebook/FacebookResponse.php +++ b/src/Facebook/FacebookResponse.php @@ -1,6 +1,6 @@ - * @author David Poll */ class FacebookResponse { + /** + * @var int The HTTP status code response from Graph. + */ + protected $httpStatusCode; + + /** + * @var array The headers returned from Graph. + */ + protected $headers; + + /** + * @var string The raw body of the response from Graph. + */ + protected $body; + + /** + * @var array The decoded body of the Graph response. + */ + protected $decodedBody = []; + + /** + * @var FacebookRequest The original request that returned this response. + */ + protected $request; + + /** + * @var FacebookSDKException The exception thrown by this request. + */ + protected $thrownException; + + /** + * Creates a new Response entity. + * + * @param FacebookRequest $request + * @param string|null $body + * @param int|null $httpStatusCode + * @param array|null $headers + */ + public function __construct(FacebookRequest $request, $body = null, $httpStatusCode = null, array $headers = []) + { + $this->request = $request; + $this->body = $body; + $this->httpStatusCode = $httpStatusCode; + $this->headers = $headers; + + $this->decodeBody(); + } + + /** + * Return the original request that returned this response. + * + * @return FacebookRequest + */ + public function getRequest() + { + return $this->request; + } + + /** + * Return the FacebookApp entity used for this response. + * + * @return FacebookApp + */ + public function getApp() + { + return $this->request->getApp(); + } + + /** + * Return the access token that was used for this response. + * + * @return string|null + */ + public function getAccessToken() + { + return $this->request->getAccessToken(); + } + + /** + * Return the HTTP status code for this response. + * + * @return int + */ + public function getHttpStatusCode() + { + return $this->httpStatusCode; + } - /** - * @var FacebookRequest The request which produced this response - */ - private $request; - - /** - * @var array The decoded response from the Graph API - */ - private $responseData; - - /** - * @var string The raw response from the Graph API - */ - private $rawResponse; - - /** - * @var bool Indicates whether sent ETag matched the one on the FB side - */ - private $etagHit; - - /** - * @var string ETag received with the response. `null` in case of ETag hit. - */ - private $etag; - - /** - * Creates a FacebookResponse object for a given request and response. - * - * @param FacebookRequest $request - * @param array $responseData JSON Decoded response data - * @param string $rawResponse Raw string response - * @param bool $etagHit Indicates whether sent ETag matched the one on the FB side - * @param string|null $etag ETag received with the response. `null` in case of ETag hit. - */ - public function __construct($request, $responseData, $rawResponse, $etagHit = false, $etag = null) - { - $this->request = $request; - $this->responseData = $responseData; - $this->rawResponse = $rawResponse; - $this->etagHit = $etagHit; - $this->etag = $etag; - } - - /** - * Returns the request which produced this response. - * - * @return FacebookRequest - */ - public function getRequest() - { - return $this->request; - } - - /** - * Returns the decoded response data. - * - * @return array - */ - public function getResponse() - { - return $this->responseData; - } - - /** - * Returns the raw response - * - * @return string - */ - public function getRawResponse() - { - return $this->rawResponse; - } - - /** - * Returns true if ETag matched the one sent with a request - * - * @return bool - */ - public function isETagHit() - { - return $this->etagHit; - } - - /** - * Returns the ETag - * - * @return string - */ - public function getETag() - { - return $this->etag; - } - - /** - * Gets the result as a GraphObject. If a type is specified, returns the - * strongly-typed subclass of GraphObject for the data. - * - * @param string $type - * - * @return mixed - */ - public function getGraphObject($type = 'Facebook\GraphObject') { - return (new GraphObject($this->responseData))->cast($type); - } - - /** - * Returns an array of GraphObject returned by the request. If a type is - * specified, returns the strongly-typed subclass of GraphObject for the data. - * - * @param string $type - * - * @return mixed - */ - public function getGraphObjectList($type = 'Facebook\GraphObject') { - $out = array(); - $data = $this->responseData->data; - for ($i = 0; $i < count($data); $i++) { - $out[] = (new GraphObject($data[$i]))->cast($type); - } - return $out; - } - - /** - * If this response has paginated data, returns the FacebookRequest for the - * next page, or null. - * - * @return FacebookRequest|null - */ - public function getRequestForNextPage() - { - return $this->handlePagination('next'); - } - - /** - * If this response has paginated data, returns the FacebookRequest for the - * previous page, or null. - * - * @return FacebookRequest|null - */ - public function getRequestForPreviousPage() - { - return $this->handlePagination('previous'); - } - - /** - * Returns the FacebookRequest for the previous or next page, or null. - * - * @param string $direction - * - * @return FacebookRequest|null - */ - private function handlePagination($direction) { - if (isset($this->responseData->paging->$direction)) { - $url = parse_url($this->responseData->paging->$direction); - parse_str($url['query'], $params); - - return new FacebookRequest( - $this->request->getSession(), - $this->request->getMethod(), - $this->request->getPath(), - $params - ); - } else { - return null; - } - } + /** + * Return the HTTP headers for this response. + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Return the raw body response. + * + * @return string + */ + public function getBody() + { + return $this->body; + } + + /** + * Return the decoded body response. + * + * @return array + */ + public function getDecodedBody() + { + return $this->decodedBody; + } + + /** + * Get the app secret proof that was used for this response. + * + * @return string|null + */ + public function getAppSecretProof() + { + return $this->request->getAppSecretProof(); + } + + /** + * Get the ETag associated with the response. + * + * @return string|null + */ + public function getETag() + { + return isset($this->headers['ETag']) ? $this->headers['ETag'] : null; + } + + /** + * Get the version of Graph that returned this response. + * + * @return string|null + */ + public function getGraphVersion() + { + return isset($this->headers['Facebook-API-Version']) ? $this->headers['Facebook-API-Version'] : null; + } + + /** + * Returns true if Graph returned an error message. + * + * @return boolean + */ + public function isError() + { + return isset($this->decodedBody['error']); + } + + /** + * Throws the exception. + * + * @throws FacebookSDKException + */ + public function throwException() + { + throw $this->thrownException; + } + + /** + * Instantiates an exception to be thrown later. + */ + public function makeException() + { + $this->thrownException = FacebookResponseException::create($this); + } + /** + * Returns the exception that was thrown for this request. + * + * @return FacebookResponseException|null + */ + public function getThrownException() + { + return $this->thrownException; + } + + /** + * Convert the raw response into an array if possible. + * + * Graph will return 2 types of responses: + * - JSON(P) + * Most responses from Graph are JSON(P) + * - application/x-www-form-urlencoded key/value pairs + * Happens on the `/oauth/access_token` endpoint when exchanging + * a short-lived access token for a long-lived access token + * - And sometimes nothing :/ but that'd be a bug. + */ + public function decodeBody() + { + $this->decodedBody = json_decode($this->body, true); + + if ($this->decodedBody === null) { + $this->decodedBody = []; + parse_str($this->body, $this->decodedBody); + } elseif (is_bool($this->decodedBody)) { + // Backwards compatibility for Graph < 2.1. + // Mimics 2.1 responses. + // @TODO Remove this after Graph 2.0 is no longer supported + $this->decodedBody = ['success' => $this->decodedBody]; + } elseif (is_numeric($this->decodedBody)) { + $this->decodedBody = ['id' => $this->decodedBody]; + } + + if (!is_array($this->decodedBody)) { + $this->decodedBody = []; + } + + if ($this->isError()) { + $this->makeException(); + } + } + + /** + * Instantiate a new GraphObject from response. + * + * @param string|null $subclassName The GraphNode subclass to cast to. + * + * @return \Facebook\GraphNodes\GraphObject + * + * @throws FacebookSDKException + * + * @deprecated 5.0.0 getGraphObject() has been renamed to getGraphNode() + * @todo v6: Remove this method + */ + public function getGraphObject($subclassName = null) + { + return $this->getGraphNode($subclassName); + } + + /** + * Instantiate a new GraphNode from response. + * + * @param string|null $subclassName The GraphNode subclass to cast to. + * + * @return \Facebook\GraphNodes\GraphNode + * + * @throws FacebookSDKException + */ + public function getGraphNode($subclassName = null) + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphNode($subclassName); + } + + /** + * Convenience method for creating a GraphAlbum collection. + * + * @return \Facebook\GraphNodes\GraphAlbum + * + * @throws FacebookSDKException + */ + public function getGraphAlbum() + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphAlbum(); + } + + /** + * Convenience method for creating a GraphPage collection. + * + * @return \Facebook\GraphNodes\GraphPage + * + * @throws FacebookSDKException + */ + public function getGraphPage() + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphPage(); + } + + /** + * Convenience method for creating a GraphSessionInfo collection. + * + * @return \Facebook\GraphNodes\GraphSessionInfo + * + * @throws FacebookSDKException + */ + public function getGraphSessionInfo() + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphSessionInfo(); + } + + /** + * Convenience method for creating a GraphUser collection. + * + * @return \Facebook\GraphNodes\GraphUser + * + * @throws FacebookSDKException + */ + public function getGraphUser() + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphUser(); + } + + /** + * Convenience method for creating a GraphEvent collection. + * + * @return \Facebook\GraphNodes\GraphEvent + * + * @throws FacebookSDKException + */ + public function getGraphEvent() + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphEvent(); + } + + /** + * Convenience method for creating a GraphGroup collection. + * + * @return \Facebook\GraphNodes\GraphGroup + * + * @throws FacebookSDKException + */ + public function getGraphGroup() + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphGroup(); + } + + /** + * Instantiate a new GraphList from response. + * + * @param string|null $subclassName The GraphNode subclass to cast list items to. + * @param boolean $auto_prefix Toggle to auto-prefix the subclass name. + * + * @return \Facebook\GraphNodes\GraphList + * + * @throws FacebookSDKException + * + * @deprecated 5.0.0 getGraphList() has been renamed to getGraphEdge() + * @todo v6: Remove this method + */ + public function getGraphList($subclassName = null, $auto_prefix = true) + { + return $this->getGraphEdge($subclassName, $auto_prefix); + } + + /** + * Instantiate a new GraphEdge from response. + * + * @param string|null $subclassName The GraphNode subclass to cast list items to. + * @param boolean $auto_prefix Toggle to auto-prefix the subclass name. + * + * @return \Facebook\GraphNodes\GraphEdge + * + * @throws FacebookSDKException + */ + public function getGraphEdge($subclassName = null, $auto_prefix = true) + { + $factory = new GraphNodeFactory($this); + + return $factory->makeGraphEdge($subclassName, $auto_prefix); + } } diff --git a/src/Facebook/FacebookSession.php b/src/Facebook/FacebookSession.php deleted file mode 100644 index 580966ff7..000000000 --- a/src/Facebook/FacebookSession.php +++ /dev/null @@ -1,367 +0,0 @@ - - * @author David Poll - */ -class FacebookSession -{ - - /** - * @var string - */ - private static $defaultAppId; - - /** - * @var string - */ - private static $defaultAppSecret; - - /** - * @var AccessToken The AccessToken entity for this connection. - */ - private $accessToken; - - /** - * @var SignedRequest - */ - private $signedRequest; - - /** - * @var bool - */ - private static $useAppSecretProof = true; - - /** - * When creating a Session from an access_token, use: - * var $session = new FacebookSession($accessToken); - * This will validate the token and provide a Session object ready for use. - * It will throw a SessionException in case of error. - * - * @param AccessToken|string $accessToken - * @param SignedRequest $signedRequest The SignedRequest entity - */ - public function __construct($accessToken, SignedRequest $signedRequest = null) - { - $this->accessToken = $accessToken instanceof AccessToken ? $accessToken : new AccessToken($accessToken); - $this->signedRequest = $signedRequest; - } - - /** - * Returns the access token. - * - * @return string - */ - public function getToken() - { - return (string) $this->accessToken; - } - - /** - * Returns the access token entity. - * - * @return AccessToken - */ - public function getAccessToken() - { - return $this->accessToken; - } - - /** - * Returns the SignedRequest entity. - * - * @return SignedRequest - */ - public function getSignedRequest() - { - return $this->signedRequest; - } - - /** - * Returns the signed request payload. - * - * @return null|array - */ - public function getSignedRequestData() - { - return $this->signedRequest ? $this->signedRequest->getPayload() : null; - } - - /** - * Returns a property from the signed request data if available. - * - * @param string $key - * - * @return null|mixed - */ - public function getSignedRequestProperty($key) - { - return $this->signedRequest ? $this->signedRequest->get($key) : null; - } - - /** - * Returns user_id from signed request data if available. - * - * @return null|string - */ - public function getUserId() - { - return $this->signedRequest ? $this->signedRequest->getUserId() : null; - } - - // @TODO Remove getSessionInfo() in 4.1: can be accessed from AccessToken directly - /** - * getSessionInfo - Makes a request to /debug_token with the appropriate - * arguments to get debug information about the sessions token. - * - * @param string|null $appId - * @param string|null $appSecret - * - * @return GraphSessionInfo - */ - public function getSessionInfo($appId = null, $appSecret = null) - { - return $this->accessToken->getInfo($appId, $appSecret); - } - - // @TODO Remove getLongLivedSession() in 4.1: can be accessed from AccessToken directly - /** - * getLongLivedSession - Returns a new Facebook session resulting from - * extending a short-lived access token. If this session is not - * short-lived, returns $this. - * - * @param string|null $appId - * @param string|null $appSecret - * - * @return FacebookSession - */ - public function getLongLivedSession($appId = null, $appSecret = null) - { - $longLivedAccessToken = $this->accessToken->extend($appId, $appSecret); - return new static($longLivedAccessToken); - } - - // @TODO Remove getExchangeToken() in 4.1: can be accessed from AccessToken directly - /** - * getExchangeToken - Returns an exchange token string which can be sent - * back to clients and exchanged for a device-linked access token. - * - * @param string|null $appId - * @param string|null $appSecret - * - * @return string - */ - public function getExchangeToken($appId = null, $appSecret = null) - { - return AccessToken::getCodeFromAccessToken($this->accessToken, $appId, $appSecret); - } - - // @TODO Remove validate() in 4.1: can be accessed from AccessToken directly - /** - * validate - Ensures the current session is valid, throwing an exception if - * not. Fetches token info from Facebook. - * - * @param string|null $appId Application ID to use - * @param string|null $appSecret App secret value to use - * @param string|null $machineId - * - * @return boolean - * - * @throws FacebookSDKException - */ - public function validate($appId = null, $appSecret = null, $machineId = null) - { - if ($this->accessToken->isValid($appId, $appSecret, $machineId)) { - return true; - } - - // @TODO For v4.1 this should not throw an exception, but just return false. - throw new FacebookSDKException( - 'Session has expired, or is not valid for this app.', 601 - ); - } - - // @TODO Remove validateSessionInfo() in 4.1: can be accessed from AccessToken directly - /** - * validateTokenInfo - Ensures the provided GraphSessionInfo object is valid, - * throwing an exception if not. Ensures the appId matches, - * that the token is valid and has not expired. - * - * @param GraphSessionInfo $tokenInfo - * @param string|null $appId Application ID to use - * @param string|null $machineId - * - * @return boolean - * - * @throws FacebookSDKException - */ - public static function validateSessionInfo(GraphSessionInfo $tokenInfo, - $appId = null, - $machineId = null) - { - if (AccessToken::validateAccessToken($tokenInfo, $appId, $machineId)) { - return true; - } - - // @TODO For v4.1 this should not throw an exception, but just return false. - throw new FacebookSDKException( - 'Session has expired, or is not valid for this app.', 601 - ); - } - - /** - * newSessionFromSignedRequest - Returns a FacebookSession for a - * given signed request. - * - * @param SignedRequest $signedRequest - * - * @return FacebookSession - */ - public static function newSessionFromSignedRequest(SignedRequest $signedRequest) - { - if ($signedRequest->get('code') - && !$signedRequest->get('oauth_token')) { - return self::newSessionAfterValidation($signedRequest); - } - $accessToken = $signedRequest->get('oauth_token'); - $expiresAt = $signedRequest->get('expires', 0); - $accessToken = new AccessToken($accessToken, $expiresAt); - return new static($accessToken, $signedRequest); - } - - /** - * newSessionAfterValidation - Returns a FacebookSession for a - * validated & parsed signed request. - * - * @param SignedRequest $signedRequest - * - * @return FacebookSession - */ - protected static function newSessionAfterValidation(SignedRequest $signedRequest) - { - $code = $signedRequest->get('code'); - $accessToken = AccessToken::getAccessTokenFromCode($code); - return new static($accessToken, $signedRequest); - } - - /** - * newAppSession - Returns a FacebookSession configured with a token for the - * application which can be used for publishing and requesting app-level - * information. - * - * @param string|null $appId Application ID to use - * @param string|null $appSecret App secret value to use - * - * @return FacebookSession - */ - public static function newAppSession($appId = null, $appSecret = null) - { - $targetAppId = static::_getTargetAppId($appId); - $targetAppSecret = static::_getTargetAppSecret($appSecret); - return new FacebookSession( - $targetAppId . '|' . $targetAppSecret - ); - } - - /** - * setDefaultApplication - Will set the static default appId and appSecret - * to be used for API requests. - * - * @param string $appId Application ID to use by default - * @param string $appSecret App secret value to use by default - */ - public static function setDefaultApplication($appId, $appSecret) - { - self::$defaultAppId = $appId; - self::$defaultAppSecret = $appSecret; - } - - /** - * _getTargetAppId - Will return either the provided app Id or the default, - * throwing if neither are populated. - * - * @param string $appId - * - * @return string - * - * @throws FacebookSDKException - */ - public static function _getTargetAppId($appId = null) { - $target = ($appId ?: self::$defaultAppId); - if (!$target) { - throw new FacebookSDKException( - 'You must provide or set a default application id.', 700 - ); - } - return $target; - } - - /** - * _getTargetAppSecret - Will return either the provided app secret or the - * default, throwing if neither are populated. - * - * @param string $appSecret - * - * @return string - * - * @throws FacebookSDKException - */ - public static function _getTargetAppSecret($appSecret = null) { - $target = ($appSecret ?: self::$defaultAppSecret); - if (!$target) { - throw new FacebookSDKException( - 'You must provide or set a default application secret.', 701 - ); - } - return $target; - } - - /** - * Enable or disable sending the appsecret_proof with requests. - * - * @param bool $on - */ - public static function enableAppSecretProof($on = true) - { - static::$useAppSecretProof = ($on ? true : false); - } - - /** - * Get whether or not appsecret_proof should be sent with requests. - * - * @return bool - */ - public static function useAppSecretProof() - { - return static::$useAppSecretProof; - } - -} diff --git a/src/Facebook/FacebookSignedRequestFromInputHelper.php b/src/Facebook/FacebookSignedRequestFromInputHelper.php deleted file mode 100644 index c497246a2..000000000 --- a/src/Facebook/FacebookSignedRequestFromInputHelper.php +++ /dev/null @@ -1,166 +0,0 @@ -appId = FacebookSession::_getTargetAppId($appId); - $this->appSecret = FacebookSession::_getTargetAppSecret($appSecret); - - $this->instantiateSignedRequest(); - } - - /** - * Instantiates a new SignedRequest entity. - * - * @param string|null - */ - public function instantiateSignedRequest($rawSignedRequest = null) - { - $rawSignedRequest = $rawSignedRequest ?: $this->getRawSignedRequest(); - - if (!$rawSignedRequest) { - return; - } - - $this->signedRequest = new SignedRequest($rawSignedRequest, $this->state, $this->appSecret); - } - - /** - * Instantiates a FacebookSession from the signed request from input. - * - * @return FacebookSession|null - */ - public function getSession() - { - if ($this->signedRequest && $this->signedRequest->hasOAuthData()) { - return FacebookSession::newSessionFromSignedRequest($this->signedRequest); - } - return null; - } - - /** - * Returns the SignedRequest entity. - * - * @return \Facebook\Entities\SignedRequest|null - */ - public function getSignedRequest() - { - return $this->signedRequest; - } - - /** - * Returns the user_id if available. - * - * @return string|null - */ - public function getUserId() - { - return $this->signedRequest ? $this->signedRequest->getUserId() : null; - } - - /** - * Get raw signed request from input. - * - * @return string|null - */ - abstract public function getRawSignedRequest(); - - /** - * Get raw signed request from GET input. - * - * @return string|null - */ - public function getRawSignedRequestFromGet() - { - if (isset($_GET['signed_request'])) { - return $_GET['signed_request']; - } - - return null; - } - - /** - * Get raw signed request from POST input. - * - * @return string|null - */ - public function getRawSignedRequestFromPost() - { - if (isset($_POST['signed_request'])) { - return $_POST['signed_request']; - } - - return null; - } - - /** - * Get raw signed request from cookie set from the Javascript SDK. - * - * @return string|null - */ - public function getRawSignedRequestFromCookie() - { - if (isset($_COOKIE['fbsr_' . $this->appId])) { - return $_COOKIE['fbsr_' . $this->appId]; - } - return null; - } - -} diff --git a/src/Facebook/FileUpload/FacebookFile.php b/src/Facebook/FileUpload/FacebookFile.php new file mode 100644 index 000000000..3c1536d43 --- /dev/null +++ b/src/Facebook/FileUpload/FacebookFile.php @@ -0,0 +1,169 @@ +path = $filePath; + $this->maxLength = $maxLength; + $this->offset = $offset; + $this->open(); + } + + /** + * Closes the stream when destructed. + */ + public function __destruct() + { + $this->close(); + } + + /** + * Opens a stream for the file. + * + * @throws FacebookSDKException + */ + public function open() + { + if (!$this->isRemoteFile($this->path) && !is_readable($this->path)) { + throw new FacebookSDKException('Failed to create FacebookFile entity. Unable to read resource: ' . $this->path . '.'); + } + + $this->stream = fopen($this->path, 'r'); + + if (!$this->stream) { + throw new FacebookSDKException('Failed to create FacebookFile entity. Unable to open resource: ' . $this->path . '.'); + } + } + + /** + * Stops the file stream. + */ + public function close() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + + /** + * Return the contents of the file. + * + * @return string + */ + public function getContents() + { + return stream_get_contents($this->stream, $this->maxLength, $this->offset); + } + + /** + * Return the name of the file. + * + * @return string + */ + public function getFileName() + { + return basename($this->path); + } + + /** + * Return the path of the file. + * + * @return string + */ + public function getFilePath() + { + return $this->path; + } + + /** + * Return the size of the file. + * + * @return int + */ + public function getSize() + { + return filesize($this->path); + } + + /** + * Return the mimetype of the file. + * + * @return string + */ + public function getMimetype() + { + return Mimetypes::getInstance()->fromFilename($this->path) ?: 'text/plain'; + } + + /** + * Returns true if the path to the file is remote. + * + * @param string $pathToFile + * + * @return boolean + */ + protected function isRemoteFile($pathToFile) + { + return preg_match('/^(https?|ftp):\/\/.*/', $pathToFile) === 1; + } +} diff --git a/src/Facebook/FileUpload/FacebookResumableUploader.php b/src/Facebook/FileUpload/FacebookResumableUploader.php new file mode 100644 index 000000000..46a2727b9 --- /dev/null +++ b/src/Facebook/FileUpload/FacebookResumableUploader.php @@ -0,0 +1,177 @@ +app = $app; + $this->client = $client; + $this->accessToken = $accessToken; + $this->graphVersion = $graphVersion; + } + + /** + * Upload by chunks - start phase + * + * @param string $endpoint + * @param FacebookFile $file + * + * @return FacebookTransferChunk + * + * @throws FacebookSDKException + */ + public function start($endpoint, FacebookFile $file) + { + $params = [ + 'upload_phase' => 'start', + 'file_size' => $file->getSize(), + ]; + $response = $this->sendUploadRequest($endpoint, $params); + + return new FacebookTransferChunk($file, $response['upload_session_id'], $response['video_id'], $response['start_offset'], $response['end_offset']); + } + + /** + * Upload by chunks - transfer phase + * + * @param string $endpoint + * @param FacebookTransferChunk $chunk + * @param boolean $allowToThrow + * + * @return FacebookTransferChunk + * + * @throws FacebookResponseException + */ + public function transfer($endpoint, FacebookTransferChunk $chunk, $allowToThrow = false) + { + $params = [ + 'upload_phase' => 'transfer', + 'upload_session_id' => $chunk->getUploadSessionId(), + 'start_offset' => $chunk->getStartOffset(), + 'video_file_chunk' => $chunk->getPartialFile(), + ]; + + try { + $response = $this->sendUploadRequest($endpoint, $params); + } catch (FacebookResponseException $e) { + $preException = $e->getPrevious(); + if ($allowToThrow || !$preException instanceof FacebookResumableUploadException) { + throw $e; + } + + if (null !== $preException->getStartOffset() && null !== $preException->getEndOffset()) { + return new FacebookTransferChunk( + $chunk->getFile(), + $chunk->getUploadSessionId(), + $chunk->getVideoId(), + $preException->getStartOffset(), + $preException->getEndOffset() + ); + } + + // Return the same chunk entity so it can be retried. + return $chunk; + } + + return new FacebookTransferChunk($chunk->getFile(), $chunk->getUploadSessionId(), $chunk->getVideoId(), $response['start_offset'], $response['end_offset']); + } + + /** + * Upload by chunks - finish phase + * + * @param string $endpoint + * @param string $uploadSessionId + * @param array $metadata The metadata associated with the file. + * + * @return boolean + * + * @throws FacebookSDKException + */ + public function finish($endpoint, $uploadSessionId, $metadata = []) + { + $params = array_merge($metadata, [ + 'upload_phase' => 'finish', + 'upload_session_id' => $uploadSessionId, + ]); + $response = $this->sendUploadRequest($endpoint, $params); + + return $response['success']; + } + + /** + * Helper to make a FacebookRequest and send it. + * + * @param string $endpoint The endpoint to POST to. + * @param array $params The params to send with the request. + * + * @return array + */ + private function sendUploadRequest($endpoint, $params = []) + { + $request = new FacebookRequest($this->app, $this->accessToken, 'POST', $endpoint, $params, null, $this->graphVersion); + + return $this->client->sendRequest($request)->getDecodedBody(); + } +} diff --git a/src/Facebook/FileUpload/FacebookTransferChunk.php b/src/Facebook/FileUpload/FacebookTransferChunk.php new file mode 100644 index 000000000..99ea7752a --- /dev/null +++ b/src/Facebook/FileUpload/FacebookTransferChunk.php @@ -0,0 +1,141 @@ +file = $file; + $this->uploadSessionId = $uploadSessionId; + $this->videoId = $videoId; + $this->startOffset = $startOffset; + $this->endOffset = $endOffset; + } + + /** + * Return the file entity. + * + * @return FacebookFile + */ + public function getFile() + { + return $this->file; + } + + /** + * Return a FacebookFile entity with partial content. + * + * @return FacebookFile + */ + public function getPartialFile() + { + $maxLength = $this->endOffset - $this->startOffset; + + return new FacebookFile($this->file->getFilePath(), $maxLength, $this->startOffset); + } + + /** + * Return upload session Id + * + * @return int + */ + public function getUploadSessionId() + { + return $this->uploadSessionId; + } + + /** + * Check whether is the last chunk + * + * @return bool + */ + public function isLastChunk() + { + return $this->startOffset === $this->endOffset; + } + + /** + * @return int + */ + public function getStartOffset() + { + return $this->startOffset; + } + + /** + * @return int + */ + public function getEndOffset() + { + return $this->endOffset; + } + + /** + * Get uploaded video Id + * + * @return int + */ + public function getVideoId() + { + return $this->videoId; + } +} diff --git a/src/Facebook/FacebookPermissionException.php b/src/Facebook/FileUpload/FacebookVideo.php similarity index 87% rename from src/Facebook/FacebookPermissionException.php rename to src/Facebook/FileUpload/FacebookVideo.php index 7fe970c03..ee6dd5389 100644 --- a/src/Facebook/FacebookPermissionException.php +++ b/src/Facebook/FileUpload/FacebookVideo.php @@ -1,6 +1,6 @@ 'text/vnd.in3d.3dml', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gpp', + '7z' => 'application/x-7z-compressed', + 'aab' => 'application/x-authorware-bin', + 'aac' => 'audio/x-aac', + 'aam' => 'application/x-authorware-map', + 'aas' => 'application/x-authorware-seg', + 'abw' => 'application/x-abiword', + 'ac' => 'application/pkix-attr-cert', + 'acc' => 'application/vnd.americandynamics.acc', + 'ace' => 'application/x-ace-compressed', + 'acu' => 'application/vnd.acucobol', + 'acutc' => 'application/vnd.acucorp', + 'adp' => 'audio/adpcm', + 'aep' => 'application/vnd.audiograph', + 'afm' => 'application/x-font-type1', + 'afp' => 'application/vnd.ibm.modcap', + 'ahead' => 'application/vnd.ahead.space', + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'air' => 'application/vnd.adobe.air-application-installer-package+zip', + 'ait' => 'application/vnd.dvb.ait', + 'ami' => 'application/vnd.amiga.ami', + 'apk' => 'application/vnd.android.package-archive', + 'application' => 'application/x-ms-application', + 'apr' => 'application/vnd.lotus-approach', + 'asa' => 'text/plain', + 'asax' => 'application/octet-stream', + 'asc' => 'application/pgp-signature', + 'ascx' => 'text/plain', + 'asf' => 'video/x-ms-asf', + 'ashx' => 'text/plain', + 'asm' => 'text/x-asm', + 'asmx' => 'text/plain', + 'aso' => 'application/vnd.accpac.simply.aso', + 'asp' => 'text/plain', + 'aspx' => 'text/plain', + 'asx' => 'video/x-ms-asf', + 'atc' => 'application/vnd.acucorp', + 'atom' => 'application/atom+xml', + 'atomcat' => 'application/atomcat+xml', + 'atomsvc' => 'application/atomsvc+xml', + 'atx' => 'application/vnd.antix.game-component', + 'au' => 'audio/basic', + 'avi' => 'video/x-msvideo', + 'aw' => 'application/applixware', + 'axd' => 'text/plain', + 'azf' => 'application/vnd.airzip.filesecure.azf', + 'azs' => 'application/vnd.airzip.filesecure.azs', + 'azw' => 'application/vnd.amazon.ebook', + 'bat' => 'application/x-msdownload', + 'bcpio' => 'application/x-bcpio', + 'bdf' => 'application/x-font-bdf', + 'bdm' => 'application/vnd.syncml.dm+wbxml', + 'bed' => 'application/vnd.realvnc.bed', + 'bh2' => 'application/vnd.fujitsu.oasysprs', + 'bin' => 'application/octet-stream', + 'bmi' => 'application/vnd.bmi', + 'bmp' => 'image/bmp', + 'book' => 'application/vnd.framemaker', + 'box' => 'application/vnd.previewsystems.box', + 'boz' => 'application/x-bzip2', + 'bpk' => 'application/octet-stream', + 'btif' => 'image/prs.btif', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'c' => 'text/x-c', + 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', + 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', + 'c4d' => 'application/vnd.clonk.c4group', + 'c4f' => 'application/vnd.clonk.c4group', + 'c4g' => 'application/vnd.clonk.c4group', + 'c4p' => 'application/vnd.clonk.c4group', + 'c4u' => 'application/vnd.clonk.c4group', + 'cab' => 'application/vnd.ms-cab-compressed', + 'car' => 'application/vnd.curl.car', + 'cat' => 'application/vnd.ms-pki.seccat', + 'cc' => 'text/x-c', + 'cct' => 'application/x-director', + 'ccxml' => 'application/ccxml+xml', + 'cdbcmsg' => 'application/vnd.contact.cmsg', + 'cdf' => 'application/x-netcdf', + 'cdkey' => 'application/vnd.mediastation.cdkey', + 'cdmia' => 'application/cdmi-capability', + 'cdmic' => 'application/cdmi-container', + 'cdmid' => 'application/cdmi-domain', + 'cdmio' => 'application/cdmi-object', + 'cdmiq' => 'application/cdmi-queue', + 'cdx' => 'chemical/x-cdx', + 'cdxml' => 'application/vnd.chemdraw+xml', + 'cdy' => 'application/vnd.cinderella', + 'cer' => 'application/pkix-cert', + 'cfc' => 'application/x-coldfusion', + 'cfm' => 'application/x-coldfusion', + 'cgm' => 'image/cgm', + 'chat' => 'application/x-chat', + 'chm' => 'application/vnd.ms-htmlhelp', + 'chrt' => 'application/vnd.kde.kchart', + 'cif' => 'chemical/x-cif', + 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', + 'cil' => 'application/vnd.ms-artgalry', + 'cla' => 'application/vnd.claymore', + 'class' => 'application/java-vm', + 'clkk' => 'application/vnd.crick.clicker.keyboard', + 'clkp' => 'application/vnd.crick.clicker.palette', + 'clkt' => 'application/vnd.crick.clicker.template', + 'clkw' => 'application/vnd.crick.clicker.wordbank', + 'clkx' => 'application/vnd.crick.clicker', + 'clp' => 'application/x-msclip', + 'cmc' => 'application/vnd.cosmocaller', + 'cmdf' => 'chemical/x-cmdf', + 'cml' => 'chemical/x-cml', + 'cmp' => 'application/vnd.yellowriver-custom-menu', + 'cmx' => 'image/x-cmx', + 'cod' => 'application/vnd.rim.cod', + 'com' => 'application/x-msdownload', + 'conf' => 'text/plain', + 'cpio' => 'application/x-cpio', + 'cpp' => 'text/x-c', + 'cpt' => 'application/mac-compactpro', + 'crd' => 'application/x-mscardfile', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'cryptonote' => 'application/vnd.rig.cryptonote', + 'cs' => 'text/plain', + 'csh' => 'application/x-csh', + 'csml' => 'chemical/x-csml', + 'csp' => 'application/vnd.commonspace', + 'css' => 'text/css', + 'cst' => 'application/x-director', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'curl' => 'text/vnd.curl', + 'cww' => 'application/prs.cww', + 'cxt' => 'application/x-director', + 'cxx' => 'text/x-c', + 'dae' => 'model/vnd.collada+xml', + 'daf' => 'application/vnd.mobius.daf', + 'dataless' => 'application/vnd.fdsn.seed', + 'davmount' => 'application/davmount+xml', + 'dcr' => 'application/x-director', + 'dcurl' => 'text/vnd.curl.dcurl', + 'dd2' => 'application/vnd.oma.dd2+xml', + 'ddd' => 'application/vnd.fujixerox.ddd', + 'deb' => 'application/x-debian-package', + 'def' => 'text/plain', + 'deploy' => 'application/octet-stream', + 'der' => 'application/x-x509-ca-cert', + 'dfac' => 'application/vnd.dreamfactory', + 'dic' => 'text/x-c', + 'dir' => 'application/x-director', + 'dis' => 'application/vnd.mobius.dis', + 'dist' => 'application/octet-stream', + 'distz' => 'application/octet-stream', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'dll' => 'application/x-msdownload', + 'dmg' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'dna' => 'application/vnd.dna', + 'doc' => 'application/msword', + 'docm' => 'application/vnd.ms-word.document.macroenabled.12', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotm' => 'application/vnd.ms-word.template.macroenabled.12', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dp' => 'application/vnd.osgi.dp', + 'dpg' => 'application/vnd.dpgraph', + 'dra' => 'audio/vnd.dra', + 'dsc' => 'text/prs.lines.tag', + 'dssc' => 'application/dssc+der', + 'dtb' => 'application/x-dtbook+xml', + 'dtd' => 'application/xml-dtd', + 'dts' => 'audio/vnd.dts', + 'dtshd' => 'audio/vnd.dts.hd', + 'dump' => 'application/octet-stream', + 'dvi' => 'application/x-dvi', + 'dwf' => 'model/vnd.dwf', + 'dwg' => 'image/vnd.dwg', + 'dxf' => 'image/vnd.dxf', + 'dxp' => 'application/vnd.spotfire.dxp', + 'dxr' => 'application/x-director', + 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', + 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', + 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', + 'ecma' => 'application/ecmascript', + 'edm' => 'application/vnd.novadigm.edm', + 'edx' => 'application/vnd.novadigm.edx', + 'efif' => 'application/vnd.picsel', + 'ei6' => 'application/vnd.pg.osasli', + 'elc' => 'application/octet-stream', + 'eml' => 'message/rfc822', + 'emma' => 'application/emma+xml', + 'eol' => 'audio/vnd.digital-winds', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'es3' => 'application/vnd.eszigno3+xml', + 'esf' => 'application/vnd.epson.esf', + 'et3' => 'application/vnd.eszigno3+xml', + 'etx' => 'text/x-setext', + 'exe' => 'application/x-msdownload', + 'exi' => 'application/exi', + 'ext' => 'application/vnd.novadigm.ext', + 'ez' => 'application/andrew-inset', + 'ez2' => 'application/vnd.ezpix-album', + 'ez3' => 'application/vnd.ezpix-package', + 'f' => 'text/x-fortran', + 'f4v' => 'video/x-f4v', + 'f77' => 'text/x-fortran', + 'f90' => 'text/x-fortran', + 'fbs' => 'image/vnd.fastbidsheet', + 'fcs' => 'application/vnd.isac.fcs', + 'fdf' => 'application/vnd.fdf', + 'fe_launch' => 'application/vnd.denovo.fcselayout-link', + 'fg5' => 'application/vnd.fujitsu.oasysgp', + 'fgd' => 'application/x-director', + 'fh' => 'image/x-freehand', + 'fh4' => 'image/x-freehand', + 'fh5' => 'image/x-freehand', + 'fh7' => 'image/x-freehand', + 'fhc' => 'image/x-freehand', + 'fig' => 'application/x-xfig', + 'fli' => 'video/x-fli', + 'flo' => 'application/vnd.micrografx.flo', + 'flv' => 'video/x-flv', + 'flw' => 'application/vnd.kde.kivio', + 'flx' => 'text/vnd.fmi.flexstor', + 'fly' => 'text/vnd.fly', + 'fm' => 'application/vnd.framemaker', + 'fnc' => 'application/vnd.frogans.fnc', + 'for' => 'text/x-fortran', + 'fpx' => 'image/vnd.fpx', + 'frame' => 'application/vnd.framemaker', + 'fsc' => 'application/vnd.fsc.weblaunch', + 'fst' => 'image/vnd.fst', + 'ftc' => 'application/vnd.fluxtime.clip', + 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', + 'fvt' => 'video/vnd.fvt', + 'fxp' => 'application/vnd.adobe.fxp', + 'fxpl' => 'application/vnd.adobe.fxp', + 'fzs' => 'application/vnd.fuzzysheet', + 'g2w' => 'application/vnd.geoplan', + 'g3' => 'image/g3fax', + 'g3w' => 'application/vnd.geospace', + 'gac' => 'application/vnd.groove-account', + 'gdl' => 'model/vnd.gdl', + 'geo' => 'application/vnd.dynageo', + 'gex' => 'application/vnd.geometry-explorer', + 'ggb' => 'application/vnd.geogebra.file', + 'ggt' => 'application/vnd.geogebra.tool', + 'ghf' => 'application/vnd.groove-help', + 'gif' => 'image/gif', + 'gim' => 'application/vnd.groove-identity-message', + 'gmx' => 'application/vnd.gmx', + 'gnumeric' => 'application/x-gnumeric', + 'gph' => 'application/vnd.flographit', + 'gqf' => 'application/vnd.grafeq', + 'gqs' => 'application/vnd.grafeq', + 'gram' => 'application/srgs', + 'gre' => 'application/vnd.geometry-explorer', + 'grv' => 'application/vnd.groove-injector', + 'grxml' => 'application/srgs+xml', + 'gsf' => 'application/x-font-ghostscript', + 'gtar' => 'application/x-gtar', + 'gtm' => 'application/vnd.groove-tool-message', + 'gtw' => 'model/vnd.gtw', + 'gv' => 'text/vnd.graphviz', + 'gxt' => 'application/vnd.geonext', + 'h' => 'text/x-c', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'hal' => 'application/vnd.hal+xml', + 'hbci' => 'application/vnd.hbci', + 'hdf' => 'application/x-hdf', + 'hh' => 'text/x-c', + 'hlp' => 'application/winhlp', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hpid' => 'application/vnd.hp-hpid', + 'hps' => 'application/vnd.hp-hps', + 'hqx' => 'application/mac-binhex40', + 'hta' => 'application/octet-stream', + 'htc' => 'text/html', + 'htke' => 'application/vnd.kenameaapp', + 'htm' => 'text/html', + 'html' => 'text/html', + 'hvd' => 'application/vnd.yamaha.hv-dic', + 'hvp' => 'application/vnd.yamaha.hv-voice', + 'hvs' => 'application/vnd.yamaha.hv-script', + 'i2g' => 'application/vnd.intergeo', + 'icc' => 'application/vnd.iccprofile', + 'ice' => 'x-conference/x-cooltalk', + 'icm' => 'application/vnd.iccprofile', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ief' => 'image/ief', + 'ifb' => 'text/calendar', + 'ifm' => 'application/vnd.shana.informed.formdata', + 'iges' => 'model/iges', + 'igl' => 'application/vnd.igloader', + 'igm' => 'application/vnd.insors.igm', + 'igs' => 'model/iges', + 'igx' => 'application/vnd.micrografx.igx', + 'iif' => 'application/vnd.shana.informed.interchange', + 'imp' => 'application/vnd.accpac.simply.imp', + 'ims' => 'application/vnd.ms-ims', + 'in' => 'text/plain', + 'ini' => 'text/plain', + 'ipfix' => 'application/ipfix', + 'ipk' => 'application/vnd.shana.informed.package', + 'irm' => 'application/vnd.ibm.rights-management', + 'irp' => 'application/vnd.irepository.package+xml', + 'iso' => 'application/octet-stream', + 'itp' => 'application/vnd.shana.informed.formtemplate', + 'ivp' => 'application/vnd.immervision-ivp', + 'ivu' => 'application/vnd.immervision-ivu', + 'jad' => 'text/vnd.sun.j2me.app-descriptor', + 'jam' => 'application/vnd.jam', + 'jar' => 'application/java-archive', + 'java' => 'text/x-java-source', + 'jisp' => 'application/vnd.jisp', + 'jlt' => 'application/vnd.hp-jlyt', + 'jnlp' => 'application/x-java-jnlp-file', + 'joda' => 'application/vnd.joost.joda-archive', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'jpgm' => 'video/jpm', + 'jpgv' => 'video/jpeg', + 'jpm' => 'video/jpm', + 'js' => 'text/javascript', + 'json' => 'application/json', + 'kar' => 'audio/midi', + 'karbon' => 'application/vnd.kde.karbon', + 'kfo' => 'application/vnd.kde.kformula', + 'kia' => 'application/vnd.kidspiration', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kne' => 'application/vnd.kinar', + 'knp' => 'application/vnd.kinar', + 'kon' => 'application/vnd.kde.kontour', + 'kpr' => 'application/vnd.kde.kpresenter', + 'kpt' => 'application/vnd.kde.kpresenter', + 'ksp' => 'application/vnd.kde.kspread', + 'ktr' => 'application/vnd.kahootz', + 'ktx' => 'image/ktx', + 'ktz' => 'application/vnd.kahootz', + 'kwd' => 'application/vnd.kde.kword', + 'kwt' => 'application/vnd.kde.kword', + 'lasxml' => 'application/vnd.las.las+xml', + 'latex' => 'application/x-latex', + 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', + 'les' => 'application/vnd.hhe.lesson-player', + 'lha' => 'application/octet-stream', + 'link66' => 'application/vnd.route66.link66+xml', + 'list' => 'text/plain', + 'list3820' => 'application/vnd.ibm.modcap', + 'listafp' => 'application/vnd.ibm.modcap', + 'log' => 'text/plain', + 'lostxml' => 'application/lost+xml', + 'lrf' => 'application/octet-stream', + 'lrm' => 'application/vnd.ms-lrm', + 'ltf' => 'application/vnd.frogans.ltf', + 'lvp' => 'audio/vnd.lucent.voice', + 'lwp' => 'application/vnd.lotus-wordpro', + 'lzh' => 'application/octet-stream', + 'm13' => 'application/x-msmediaview', + 'm14' => 'application/x-msmediaview', + 'm1v' => 'video/mpeg', + 'm21' => 'application/mp21', + 'm2a' => 'audio/mpeg', + 'm2v' => 'video/mpeg', + 'm3a' => 'audio/mpeg', + 'm3u' => 'audio/x-mpegurl', + 'm3u8' => 'application/vnd.apple.mpegurl', + 'm4a' => 'audio/mp4', + 'm4u' => 'video/vnd.mpegurl', + 'm4v' => 'video/mp4', + 'ma' => 'application/mathematica', + 'mads' => 'application/mads+xml', + 'mag' => 'application/vnd.ecowin.chart', + 'maker' => 'application/vnd.framemaker', + 'man' => 'text/troff', + 'mathml' => 'application/mathml+xml', + 'mb' => 'application/mathematica', + 'mbk' => 'application/vnd.mobius.mbk', + 'mbox' => 'application/mbox', + 'mc1' => 'application/vnd.medcalcdata', + 'mcd' => 'application/vnd.mcd', + 'mcurl' => 'text/vnd.curl.mcurl', + 'mdb' => 'application/x-msaccess', + 'mdi' => 'image/vnd.ms-modi', + 'me' => 'text/troff', + 'mesh' => 'model/mesh', + 'meta4' => 'application/metalink4+xml', + 'mets' => 'application/mets+xml', + 'mfm' => 'application/vnd.mfmp', + 'mgp' => 'application/vnd.osgeo.mapguide.package', + 'mgz' => 'application/vnd.proteus.magazine', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/vnd.mif', + 'mime' => 'message/rfc822', + 'mj2' => 'video/mj2', + 'mjp2' => 'video/mj2', + 'mlp' => 'application/vnd.dolby.mlp', + 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', + 'mmf' => 'application/vnd.smaf', + 'mmr' => 'image/vnd.fujixerox.edmics-mmr', + 'mny' => 'application/x-msmoney', + 'mobi' => 'application/x-mobipocket-ebook', + 'mods' => 'application/mods+xml', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp21' => 'application/mp21', + 'mp2a' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4s' => 'application/mp4', + 'mp4v' => 'video/mp4', + 'mpc' => 'application/vnd.mophun.certificate', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'mpga' => 'audio/mpeg', + 'mpkg' => 'application/vnd.apple.installer+xml', + 'mpm' => 'application/vnd.blueice.multipass', + 'mpn' => 'application/vnd.mophun.application', + 'mpp' => 'application/vnd.ms-project', + 'mpt' => 'application/vnd.ms-project', + 'mpy' => 'application/vnd.ibm.minipay', + 'mqy' => 'application/vnd.mobius.mqy', + 'mrc' => 'application/marc', + 'mrcx' => 'application/marcxml+xml', + 'ms' => 'text/troff', + 'mscml' => 'application/mediaservercontrol+xml', + 'mseed' => 'application/vnd.fdsn.mseed', + 'mseq' => 'application/vnd.mseq', + 'msf' => 'application/vnd.epson.msf', + 'msh' => 'model/mesh', + 'msi' => 'application/x-msdownload', + 'msl' => 'application/vnd.mobius.msl', + 'msty' => 'application/vnd.muvee.style', + 'mts' => 'model/vnd.mts', + 'mus' => 'application/vnd.musician', + 'musicxml' => 'application/vnd.recordare.musicxml+xml', + 'mvb' => 'application/x-msmediaview', + 'mwf' => 'application/vnd.mfer', + 'mxf' => 'application/mxf', + 'mxl' => 'application/vnd.recordare.musicxml', + 'mxml' => 'application/xv+xml', + 'mxs' => 'application/vnd.triscape.mxs', + 'mxu' => 'video/vnd.mpegurl', + 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', + 'n3' => 'text/n3', + 'nb' => 'application/mathematica', + 'nbp' => 'application/vnd.wolfram.player', + 'nc' => 'application/x-netcdf', + 'ncx' => 'application/x-dtbncx+xml', + 'ngdat' => 'application/vnd.nokia.n-gage.data', + 'nlu' => 'application/vnd.neurolanguage.nlu', + 'nml' => 'application/vnd.enliven', + 'nnd' => 'application/vnd.noblenet-directory', + 'nns' => 'application/vnd.noblenet-sealer', + 'nnw' => 'application/vnd.noblenet-web', + 'npx' => 'image/vnd.net-fpx', + 'nsf' => 'application/vnd.lotus-notes', + 'oa2' => 'application/vnd.fujitsu.oasys2', + 'oa3' => 'application/vnd.fujitsu.oasys3', + 'oas' => 'application/vnd.fujitsu.oasys', + 'obd' => 'application/x-msbinder', + 'oda' => 'application/oda', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odft' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'onepkg' => 'application/onenote', + 'onetmp' => 'application/onenote', + 'onetoc' => 'application/onenote', + 'onetoc2' => 'application/onenote', + 'opf' => 'application/oebps-package+xml', + 'oprc' => 'application/vnd.palm', + 'org' => 'application/vnd.lotus-organizer', + 'osf' => 'application/vnd.yamaha.openscoreformat', + 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'otf' => 'application/x-font-otf', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'p' => 'text/x-pascal', + 'p10' => 'application/pkcs10', + 'p12' => 'application/x-pkcs12', + 'p7b' => 'application/x-pkcs7-certificates', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'p8' => 'application/pkcs8', + 'pas' => 'text/x-pascal', + 'paw' => 'application/vnd.pawaafile', + 'pbd' => 'application/vnd.powerbuilder6', + 'pbm' => 'image/x-portable-bitmap', + 'pcf' => 'application/x-font-pcf', + 'pcl' => 'application/vnd.hp-pcl', + 'pclxl' => 'application/vnd.hp-pclxl', + 'pct' => 'image/x-pict', + 'pcurl' => 'application/vnd.curl.pcurl', + 'pcx' => 'image/x-pcx', + 'pdb' => 'application/vnd.palm', + 'pdf' => 'application/pdf', + 'pfa' => 'application/x-font-type1', + 'pfb' => 'application/x-font-type1', + 'pfm' => 'application/x-font-type1', + 'pfr' => 'application/font-tdpfr', + 'pfx' => 'application/x-pkcs12', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'pgp' => 'application/pgp-encrypted', + 'php' => 'text/x-php', + 'phps' => 'application/x-httpd-phps', + 'pic' => 'image/x-pict', + 'pkg' => 'application/octet-stream', + 'pki' => 'application/pkixcmp', + 'pkipath' => 'application/pkix-pkipath', + 'plb' => 'application/vnd.3gpp.pic-bw-large', + 'plc' => 'application/vnd.mobius.plc', + 'plf' => 'application/vnd.pocketlearn', + 'pls' => 'application/pls+xml', + 'pml' => 'application/vnd.ctc-posml', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'portpkg' => 'application/vnd.macports.portpkg', + 'pot' => 'application/vnd.ms-powerpoint', + 'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12', + 'ppd' => 'application/vnd.cups-ppd', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pqa' => 'application/vnd.palm', + 'prc' => 'application/x-mobipocket-ebook', + 'pre' => 'application/vnd.lotus-freelance', + 'prf' => 'application/pics-rules', + 'ps' => 'application/postscript', + 'psb' => 'application/vnd.3gpp.pic-bw-small', + 'psd' => 'image/vnd.adobe.photoshop', + 'psf' => 'application/x-font-linux-psf', + 'pskcxml' => 'application/pskc+xml', + 'ptid' => 'application/vnd.pvi.ptid1', + 'pub' => 'application/x-mspublisher', + 'pvb' => 'application/vnd.3gpp.pic-bw-var', + 'pwn' => 'application/vnd.3m.post-it-notes', + 'pya' => 'audio/vnd.ms-playready.media.pya', + 'pyv' => 'video/vnd.ms-playready.media.pyv', + 'qam' => 'application/vnd.epson.quickanime', + 'qbo' => 'application/vnd.intu.qbo', + 'qfx' => 'application/vnd.intu.qfx', + 'qps' => 'application/vnd.publishare-delta-tree', + 'qt' => 'video/quicktime', + 'qwd' => 'application/vnd.quark.quarkxpress', + 'qwt' => 'application/vnd.quark.quarkxpress', + 'qxb' => 'application/vnd.quark.quarkxpress', + 'qxd' => 'application/vnd.quark.quarkxpress', + 'qxl' => 'application/vnd.quark.quarkxpress', + 'qxt' => 'application/vnd.quark.quarkxpress', + 'ra' => 'audio/x-pn-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'rar' => 'application/x-rar-compressed', + 'ras' => 'image/x-cmu-raster', + 'rb' => 'text/plain', + 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', + 'rdf' => 'application/rdf+xml', + 'rdz' => 'application/vnd.data-vision.rdz', + 'rep' => 'application/vnd.businessobjects', + 'res' => 'application/x-dtbresource+xml', + 'resx' => 'text/xml', + 'rgb' => 'image/x-rgb', + 'rif' => 'application/reginfo+xml', + 'rip' => 'audio/vnd.rip', + 'rl' => 'application/resource-lists+xml', + 'rlc' => 'image/vnd.fujixerox.edmics-rlc', + 'rld' => 'application/resource-lists-diff+xml', + 'rm' => 'application/vnd.rn-realmedia', + 'rmi' => 'audio/midi', + 'rmp' => 'audio/x-pn-realaudio-plugin', + 'rms' => 'application/vnd.jcp.javame.midlet-rms', + 'rnc' => 'application/relax-ng-compact-syntax', + 'roff' => 'text/troff', + 'rp9' => 'application/vnd.cloanto.rp9', + 'rpss' => 'application/vnd.nokia.radio-presets', + 'rpst' => 'application/vnd.nokia.radio-preset', + 'rq' => 'application/sparql-query', + 'rs' => 'application/rls-services+xml', + 'rsd' => 'application/rsd+xml', + 'rss' => 'application/rss+xml', + 'rtf' => 'application/rtf', + 'rtx' => 'text/richtext', + 's' => 'text/x-asm', + 'saf' => 'application/vnd.yamaha.smaf-audio', + 'sbml' => 'application/sbml+xml', + 'sc' => 'application/vnd.ibm.secure-container', + 'scd' => 'application/x-msschedule', + 'scm' => 'application/vnd.lotus-screencam', + 'scq' => 'application/scvp-cv-request', + 'scs' => 'application/scvp-cv-response', + 'scurl' => 'text/vnd.curl.scurl', + 'sda' => 'application/vnd.stardivision.draw', + 'sdc' => 'application/vnd.stardivision.calc', + 'sdd' => 'application/vnd.stardivision.impress', + 'sdkd' => 'application/vnd.solent.sdkm+xml', + 'sdkm' => 'application/vnd.solent.sdkm+xml', + 'sdp' => 'application/sdp', + 'sdw' => 'application/vnd.stardivision.writer', + 'see' => 'application/vnd.seemail', + 'seed' => 'application/vnd.fdsn.seed', + 'sema' => 'application/vnd.sema', + 'semd' => 'application/vnd.semd', + 'semf' => 'application/vnd.semf', + 'ser' => 'application/java-serialized-object', + 'setpay' => 'application/set-payment-initiation', + 'setreg' => 'application/set-registration-initiation', + 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', + 'sfs' => 'application/vnd.spotfire.sfs', + 'sgl' => 'application/vnd.stardivision.writer-global', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'shf' => 'application/shf+xml', + 'sig' => 'application/pgp-signature', + 'silo' => 'model/mesh', + 'sis' => 'application/vnd.symbian.install', + 'sisx' => 'application/vnd.symbian.install', + 'sit' => 'application/x-stuffit', + 'sitx' => 'application/x-stuffitx', + 'skd' => 'application/vnd.koan', + 'skm' => 'application/vnd.koan', + 'skp' => 'application/vnd.koan', + 'skt' => 'application/vnd.koan', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'slt' => 'application/vnd.epson.salt', + 'sm' => 'application/vnd.stepmania.stepchart', + 'smf' => 'application/vnd.stardivision.math', + 'smi' => 'application/smil+xml', + 'smil' => 'application/smil+xml', + 'snd' => 'audio/basic', + 'snf' => 'application/x-font-snf', + 'so' => 'application/octet-stream', + 'spc' => 'application/x-pkcs7-certificates', + 'spf' => 'application/vnd.yamaha.smaf-phrase', + 'spl' => 'application/x-futuresplash', + 'spot' => 'text/vnd.in3d.spot', + 'spp' => 'application/scvp-vp-response', + 'spq' => 'application/scvp-vp-request', + 'spx' => 'audio/ogg', + 'src' => 'application/x-wais-source', + 'srt' => 'application/octet-stream', + 'sru' => 'application/sru+xml', + 'srx' => 'application/sparql-results+xml', + 'sse' => 'application/vnd.kodak-descriptor', + 'ssf' => 'application/vnd.epson.ssf', + 'ssml' => 'application/ssml+xml', + 'st' => 'application/vnd.sailingtracker.track', + 'stc' => 'application/vnd.sun.xml.calc.template', + 'std' => 'application/vnd.sun.xml.draw.template', + 'stf' => 'application/vnd.wt.stf', + 'sti' => 'application/vnd.sun.xml.impress.template', + 'stk' => 'application/hyperstudio', + 'stl' => 'application/vnd.ms-pki.stl', + 'str' => 'application/vnd.pg.format', + 'stw' => 'application/vnd.sun.xml.writer.template', + 'sub' => 'image/vnd.dvb.subtitle', + 'sus' => 'application/vnd.sus-calendar', + 'susp' => 'application/vnd.sus-calendar', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svc' => 'application/vnd.dvb.service', + 'svd' => 'application/vnd.svd', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swa' => 'application/x-director', + 'swf' => 'application/x-shockwave-flash', + 'swi' => 'application/vnd.aristanetworks.swi', + 'sxc' => 'application/vnd.sun.xml.calc', + 'sxd' => 'application/vnd.sun.xml.draw', + 'sxg' => 'application/vnd.sun.xml.writer.global', + 'sxi' => 'application/vnd.sun.xml.impress', + 'sxm' => 'application/vnd.sun.xml.math', + 'sxw' => 'application/vnd.sun.xml.writer', + 't' => 'text/troff', + 'tao' => 'application/vnd.tao.intent-module-archive', + 'tar' => 'application/x-tar', + 'tcap' => 'application/vnd.3gpp2.tcap', + 'tcl' => 'application/x-tcl', + 'teacher' => 'application/vnd.smart.teacher', + 'tei' => 'application/tei+xml', + 'teicorpus' => 'application/tei+xml', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'text' => 'text/plain', + 'tfi' => 'application/thraud+xml', + 'tfm' => 'application/x-tex-tfm', + 'thmx' => 'application/vnd.ms-officetheme', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tmo' => 'application/vnd.tmobile-livetv', + 'torrent' => 'application/x-bittorrent', + 'tpl' => 'application/vnd.groove-tool-template', + 'tpt' => 'application/vnd.trid.tpt', + 'tr' => 'text/troff', + 'tra' => 'application/vnd.trueapp', + 'trm' => 'application/x-msterminal', + 'tsd' => 'application/timestamped-data', + 'tsv' => 'text/tab-separated-values', + 'ttc' => 'application/x-font-ttf', + 'ttf' => 'application/x-font-ttf', + 'ttl' => 'text/turtle', + 'twd' => 'application/vnd.simtech-mindmapper', + 'twds' => 'application/vnd.simtech-mindmapper', + 'txd' => 'application/vnd.genomatix.tuxedo', + 'txf' => 'application/vnd.mobius.txf', + 'txt' => 'text/plain', + 'u32' => 'application/x-authorware-bin', + 'udeb' => 'application/x-debian-package', + 'ufd' => 'application/vnd.ufdl', + 'ufdl' => 'application/vnd.ufdl', + 'umj' => 'application/vnd.umajin', + 'unityweb' => 'application/vnd.unity', + 'uoml' => 'application/vnd.uoml+xml', + 'uri' => 'text/uri-list', + 'uris' => 'text/uri-list', + 'urls' => 'text/uri-list', + 'ustar' => 'application/x-ustar', + 'utz' => 'application/vnd.uiq.theme', + 'uu' => 'text/x-uuencode', + 'uva' => 'audio/vnd.dece.audio', + 'uvd' => 'application/vnd.dece.data', + 'uvf' => 'application/vnd.dece.data', + 'uvg' => 'image/vnd.dece.graphic', + 'uvh' => 'video/vnd.dece.hd', + 'uvi' => 'image/vnd.dece.graphic', + 'uvm' => 'video/vnd.dece.mobile', + 'uvp' => 'video/vnd.dece.pd', + 'uvs' => 'video/vnd.dece.sd', + 'uvt' => 'application/vnd.dece.ttml+xml', + 'uvu' => 'video/vnd.uvvu.mp4', + 'uvv' => 'video/vnd.dece.video', + 'uvva' => 'audio/vnd.dece.audio', + 'uvvd' => 'application/vnd.dece.data', + 'uvvf' => 'application/vnd.dece.data', + 'uvvg' => 'image/vnd.dece.graphic', + 'uvvh' => 'video/vnd.dece.hd', + 'uvvi' => 'image/vnd.dece.graphic', + 'uvvm' => 'video/vnd.dece.mobile', + 'uvvp' => 'video/vnd.dece.pd', + 'uvvs' => 'video/vnd.dece.sd', + 'uvvt' => 'application/vnd.dece.ttml+xml', + 'uvvu' => 'video/vnd.uvvu.mp4', + 'uvvv' => 'video/vnd.dece.video', + 'uvvx' => 'application/vnd.dece.unspecified', + 'uvx' => 'application/vnd.dece.unspecified', + 'vcd' => 'application/x-cdlink', + 'vcf' => 'text/x-vcard', + 'vcg' => 'application/vnd.groove-vcard', + 'vcs' => 'text/x-vcalendar', + 'vcx' => 'application/vnd.vcx', + 'vis' => 'application/vnd.visionary', + 'viv' => 'video/vnd.vivo', + 'vor' => 'application/vnd.stardivision.writer', + 'vox' => 'application/x-authorware-bin', + 'vrml' => 'model/vrml', + 'vsd' => 'application/vnd.visio', + 'vsf' => 'application/vnd.vsf', + 'vss' => 'application/vnd.visio', + 'vst' => 'application/vnd.visio', + 'vsw' => 'application/vnd.visio', + 'vtu' => 'model/vnd.vtu', + 'vxml' => 'application/voicexml+xml', + 'w3d' => 'application/x-director', + 'wad' => 'application/x-doom', + 'wav' => 'audio/x-wav', + 'wax' => 'audio/x-ms-wax', + 'wbmp' => 'image/vnd.wap.wbmp', + 'wbs' => 'application/vnd.criticaltools.wbs+xml', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wcm' => 'application/vnd.ms-works', + 'wdb' => 'application/vnd.ms-works', + 'weba' => 'audio/webm', + 'webm' => 'video/webm', + 'webp' => 'image/webp', + 'wg' => 'application/vnd.pmi.widget', + 'wgt' => 'application/widget', + 'wks' => 'application/vnd.ms-works', + 'wm' => 'video/x-ms-wm', + 'wma' => 'audio/x-ms-wma', + 'wmd' => 'application/x-ms-wmd', + 'wmf' => 'application/x-msmetafile', + 'wml' => 'text/vnd.wap.wml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wmz' => 'application/x-ms-wmz', + 'woff' => 'application/x-font-woff', + 'wpd' => 'application/vnd.wordperfect', + 'wpl' => 'application/vnd.ms-wpl', + 'wps' => 'application/vnd.ms-works', + 'wqd' => 'application/vnd.wqd', + 'wri' => 'application/x-mswrite', + 'wrl' => 'model/vrml', + 'wsdl' => 'application/wsdl+xml', + 'wspolicy' => 'application/wspolicy+xml', + 'wtb' => 'application/vnd.webturbo', + 'wvx' => 'video/x-ms-wvx', + 'x32' => 'application/x-authorware-bin', + 'x3d' => 'application/vnd.hzn-3d-crossword', + 'xap' => 'application/x-silverlight-app', + 'xar' => 'application/vnd.xara', + 'xbap' => 'application/x-ms-xbap', + 'xbd' => 'application/vnd.fujixerox.docuworks.binder', + 'xbm' => 'image/x-xbitmap', + 'xdf' => 'application/xcap-diff+xml', + 'xdm' => 'application/vnd.syncml.dm+xml', + 'xdp' => 'application/vnd.adobe.xdp+xml', + 'xdssc' => 'application/dssc+xml', + 'xdw' => 'application/vnd.fujixerox.docuworks', + 'xenc' => 'application/xenc+xml', + 'xer' => 'application/patch-ops-error+xml', + 'xfdf' => 'application/vnd.adobe.xfdf', + 'xfdl' => 'application/vnd.xfdl', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'xhvml' => 'application/xv+xml', + 'xif' => 'image/vnd.xiff', + 'xla' => 'application/vnd.ms-excel', + 'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12', + 'xlc' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlt' => 'application/vnd.ms-excel', + 'xltm' => 'application/vnd.ms-excel.template.macroenabled.12', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlw' => 'application/vnd.ms-excel', + 'xml' => 'application/xml', + 'xo' => 'application/vnd.olpc-sugar', + 'xop' => 'application/xop+xml', + 'xpi' => 'application/x-xpinstall', + 'xpm' => 'image/x-xpixmap', + 'xpr' => 'application/vnd.is-xpr', + 'xps' => 'application/vnd.ms-xpsdocument', + 'xpw' => 'application/vnd.intercon.formnet', + 'xpx' => 'application/vnd.intercon.formnet', + 'xsl' => 'application/xml', + 'xslt' => 'application/xslt+xml', + 'xsm' => 'application/vnd.syncml+xml', + 'xspf' => 'application/xspf+xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'xvm' => 'application/xv+xml', + 'xvml' => 'application/xv+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-xyz', + 'yaml' => 'text/yaml', + 'yang' => 'application/yang', + 'yin' => 'application/yin+xml', + 'yml' => 'text/yaml', + 'zaz' => 'application/vnd.zzazz.deck+xml', + 'zip' => 'application/zip', + 'zir' => 'application/vnd.zul', + 'zirz' => 'application/vnd.zul', + 'zmm' => 'application/vnd.handheld-entertainment+xml' + ]; + + /** + * Get a singleton instance of the class + * + * @return self + * @codeCoverageIgnore + */ + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Get a mimetype value from a file extension + * + * @param string $extension File extension + * + * @return string|null + */ + public function fromExtension($extension) + { + $extension = strtolower($extension); + + return isset($this->mimetypes[$extension]) ? $this->mimetypes[$extension] : null; + } + + /** + * Get a mimetype from a filename + * + * @param string $filename Filename to generate a mimetype from + * + * @return string|null + */ + public function fromFilename($filename) + { + return $this->fromExtension(pathinfo($filename, PATHINFO_EXTENSION)); + } +} diff --git a/src/Facebook/GraphLocation.php b/src/Facebook/GraphLocation.php deleted file mode 100644 index 5326ea53a..000000000 --- a/src/Facebook/GraphLocation.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @author David Poll - */ -class GraphLocation extends GraphObject -{ - - /** - * Returns the street component of the location - * - * @return string|null - */ - public function getStreet() - { - return $this->getProperty('street'); - } - - /** - * Returns the city component of the location - * - * @return string|null - */ - public function getCity() - { - return $this->getProperty('city'); - } - - /** - * Returns the state component of the location - * - * @return string|null - */ - public function getState() - { - return $this->getProperty('state'); - } - - /** - * Returns the country component of the location - * - * @return string|null - */ - public function getCountry() - { - return $this->getProperty('country'); - } - - /** - * Returns the zipcode component of the location - * - * @return string|null - */ - public function getZip() - { - return $this->getProperty('zip'); - } - - /** - * Returns the latitude component of the location - * - * @return float|null - */ - public function getLatitude() - { - return $this->getProperty('latitude'); - } - - /** - * Returns the street component of the location - * - * @return float|null - */ - public function getLongitude() - { - return $this->getProperty('longitude'); - } - -} \ No newline at end of file diff --git a/src/Facebook/GraphNodes/Birthday.php b/src/Facebook/GraphNodes/Birthday.php new file mode 100644 index 000000000..4338b65e4 --- /dev/null +++ b/src/Facebook/GraphNodes/Birthday.php @@ -0,0 +1,85 @@ +hasYear = count($parts) === 3 || count($parts) === 1; + $this->hasDate = count($parts) === 3 || count($parts) === 2; + + parent::__construct($date); + } + + /** + * Returns whether date object contains birth day and month + * + * @return bool + */ + public function hasDate() + { + return $this->hasDate; + } + + /** + * Returns whether date object contains birth year + * + * @return bool + */ + public function hasYear() + { + return $this->hasYear; + } +} diff --git a/src/Facebook/GraphNodes/Collection.php b/src/Facebook/GraphNodes/Collection.php new file mode 100644 index 000000000..424b7cf3b --- /dev/null +++ b/src/Facebook/GraphNodes/Collection.php @@ -0,0 +1,242 @@ +items = $items; + } + + /** + * Gets the value of a field from the Graph node. + * + * @param string $name The field to retrieve. + * @param mixed $default The default to return if the field doesn't exist. + * + * @return mixed + */ + public function getField($name, $default = null) + { + if (isset($this->items[$name])) { + return $this->items[$name]; + } + + return $default; + } + + /** + * Gets the value of the named property for this graph object. + * + * @param string $name The property to retrieve. + * @param mixed $default The default to return if the property doesn't exist. + * + * @return mixed + * + * @deprecated 5.0.0 getProperty() has been renamed to getField() + * @todo v6: Remove this method + */ + public function getProperty($name, $default = null) + { + return $this->getField($name, $default); + } + + /** + * Returns a list of all fields set on the object. + * + * @return array + */ + public function getFieldNames() + { + return array_keys($this->items); + } + + /** + * Returns a list of all properties set on the object. + * + * @return array + * + * @deprecated 5.0.0 getPropertyNames() has been renamed to getFieldNames() + * @todo v6: Remove this method + */ + public function getPropertyNames() + { + return $this->getFieldNames(); + } + + /** + * Get all of the items in the collection. + * + * @return array + */ + public function all() + { + return $this->items; + } + + /** + * Get the collection of items as a plain array. + * + * @return array + */ + public function asArray() + { + return array_map(function ($value) { + return $value instanceof Collection ? $value->asArray() : $value; + }, $this->items); + } + + /** + * Run a map over each of the items. + * + * @param \Closure $callback + * + * @return static + */ + public function map(\Closure $callback) + { + return new static(array_map($callback, $this->items, array_keys($this->items))); + } + + /** + * Get the collection of items as JSON. + * + * @param int $options + * + * @return string + */ + public function asJson($options = 0) + { + return json_encode($this->asArray(), $options); + } + + /** + * Count the number of items in the collection. + * + * @return int + */ + public function count() + { + return count($this->items); + } + + /** + * Get an iterator for the items. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->items); + } + + /** + * Determine if an item exists at an offset. + * + * @param mixed $key + * + * @return bool + */ + public function offsetExists($key) + { + return array_key_exists($key, $this->items); + } + + /** + * Get an item at a given offset. + * + * @param mixed $key + * + * @return mixed + */ + public function offsetGet($key) + { + return $this->items[$key]; + } + + /** + * Set the item at a given offset. + * + * @param mixed $key + * @param mixed $value + * + * @return void + */ + public function offsetSet($key, $value) + { + if (is_null($key)) { + $this->items[] = $value; + } else { + $this->items[$key] = $value; + } + } + + /** + * Unset the item at a given offset. + * + * @param string $key + * + * @return void + */ + public function offsetUnset($key) + { + unset($this->items[$key]); + } + + /** + * Convert the collection to its string representation. + * + * @return string + */ + public function __toString() + { + return $this->asJson(); + } +} diff --git a/src/Facebook/GraphNodes/GraphAchievement.php b/src/Facebook/GraphNodes/GraphAchievement.php new file mode 100644 index 000000000..31508ee45 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphAchievement.php @@ -0,0 +1,112 @@ + '\Facebook\GraphNodes\GraphUser', + 'application' => '\Facebook\GraphNodes\GraphApplication', + ]; + + /** + * Returns the ID for the achievement. + * + * @return string|null + */ + public function getId() + { + return $this->getField('id'); + } + + /** + * Returns the user who achieved this. + * + * @return GraphUser|null + */ + public function getFrom() + { + return $this->getField('from'); + } + + /** + * Returns the time at which this was achieved. + * + * @return \DateTime|null + */ + public function getPublishTime() + { + return $this->getField('publish_time'); + } + + /** + * Returns the app in which the user achieved this. + * + * @return GraphApplication|null + */ + public function getApplication() + { + return $this->getField('application'); + } + + /** + * Returns information about the achievement type this instance is connected with. + * + * @return array|null + */ + public function getData() + { + return $this->getField('data'); + } + + /** + * Returns the type of achievement. + * + * @see https://developers.facebook.com/docs/graph-api/reference/achievement + * + * @return string + */ + public function getType() + { + return 'game.achievement'; + } + + /** + * Indicates whether gaining the achievement published a feed story for the user. + * + * @return boolean|null + */ + public function isNoFeedStory() + { + return $this->getField('no_feed_story'); + } +} diff --git a/src/Facebook/GraphAlbum.php b/src/Facebook/GraphNodes/GraphAlbum.php similarity index 70% rename from src/Facebook/GraphAlbum.php rename to src/Facebook/GraphNodes/GraphAlbum.php index 5f9dc8ce8..52f19b51f 100644 --- a/src/Facebook/GraphAlbum.php +++ b/src/Facebook/GraphNodes/GraphAlbum.php @@ -1,6 +1,6 @@ */ -class GraphAlbum extends GraphObject +class GraphAlbum extends GraphNode { + /** + * @var array Maps object key names to Graph object types. + */ + protected static $graphObjectMap = [ + 'from' => '\Facebook\GraphNodes\GraphUser', + 'place' => '\Facebook\GraphNodes\GraphPage', + ]; + /** * Returns the ID for the album. * @@ -38,7 +46,7 @@ class GraphAlbum extends GraphObject */ public function getId() { - return $this->getProperty('id'); + return $this->getField('id'); } /** @@ -46,9 +54,9 @@ public function getId() * * @return boolean|null */ - public function canUpload() + public function getCanUpload() { - return $this->getProperty('can_upload'); + return $this->getField('can_upload'); } /** @@ -58,7 +66,7 @@ public function canUpload() */ public function getCount() { - return$this->getProperty('count'); + return $this->getField('count'); } /** @@ -68,7 +76,7 @@ public function getCount() */ public function getCoverPhoto() { - return$this->getProperty('cover_photo'); + return $this->getField('cover_photo'); } /** @@ -78,11 +86,7 @@ public function getCoverPhoto() */ public function getCreatedTime() { - $value = $this->getProperty('created_time'); - if ($value) { - return new \DateTime($value); - } - return null; + return $this->getField('created_time'); } /** @@ -92,11 +96,7 @@ public function getCreatedTime() */ public function getUpdatedTime() { - $value = $this->getProperty('updated_time'); - if ($value) { - return new \DateTime($value); - } - return null; + return $this->getField('updated_time'); } /** @@ -106,7 +106,7 @@ public function getUpdatedTime() */ public function getDescription() { - return$this->getProperty('description'); + return $this->getField('description'); } /** @@ -116,7 +116,17 @@ public function getDescription() */ public function getFrom() { - return $this->getProperty('from', GraphUser::className()); + return $this->getField('from'); + } + + /** + * Returns profile that created the album. + * + * @return GraphPage|null + */ + public function getPlace() + { + return $this->getField('place'); } /** @@ -126,7 +136,7 @@ public function getFrom() */ public function getLink() { - return$this->getProperty('link'); + return $this->getField('link'); } /** @@ -136,7 +146,7 @@ public function getLink() */ public function getLocation() { - return$this->getProperty('location'); + return $this->getField('location'); } /** @@ -146,7 +156,7 @@ public function getLocation() */ public function getName() { - return$this->getProperty('name'); + return $this->getField('name'); } /** @@ -156,18 +166,18 @@ public function getName() */ public function getPrivacy() { - return$this->getProperty('privacy'); + return $this->getField('privacy'); } /** - * Returns the type of the album. enum{profile, mobile, wall, normal, album} + * Returns the type of the album. + * + * enum{ profile, mobile, wall, normal, album } * * @return string|null */ public function getType() { - return$this->getProperty('type'); + return $this->getField('type'); } - - //TODO: public function getPlace() that should return GraphPage } diff --git a/src/Facebook/GraphNodes/GraphApplication.php b/src/Facebook/GraphNodes/GraphApplication.php new file mode 100644 index 000000000..aa07c825d --- /dev/null +++ b/src/Facebook/GraphNodes/GraphApplication.php @@ -0,0 +1,43 @@ +getField('id'); + } +} diff --git a/src/Facebook/GraphNodes/GraphCoverPhoto.php b/src/Facebook/GraphNodes/GraphCoverPhoto.php new file mode 100644 index 000000000..824275bbe --- /dev/null +++ b/src/Facebook/GraphNodes/GraphCoverPhoto.php @@ -0,0 +1,72 @@ +getField('id'); + } + + /** + * Returns the source of cover if it exists + * + * @return string|null + */ + public function getSource() + { + return $this->getField('source'); + } + + /** + * Returns the offset_x of cover if it exists + * + * @return int|null + */ + public function getOffsetX() + { + return $this->getField('offset_x'); + } + + /** + * Returns the offset_y of cover if it exists + * + * @return int|null + */ + public function getOffsetY() + { + return $this->getField('offset_y'); + } +} diff --git a/src/Facebook/GraphNodes/GraphEdge.php b/src/Facebook/GraphNodes/GraphEdge.php new file mode 100644 index 000000000..f6f4970c0 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphEdge.php @@ -0,0 +1,252 @@ +request = $request; + $this->metaData = $metaData; + $this->parentEdgeEndpoint = $parentEdgeEndpoint; + $this->subclassName = $subclassName; + + parent::__construct($data); + } + + /** + * Gets the parent Graph edge endpoint that generated the list. + * + * @return string|null + */ + public function getParentGraphEdge() + { + return $this->parentEdgeEndpoint; + } + + /** + * Gets the subclass name that the child GraphNode's are cast as. + * + * @return string|null + */ + public function getSubClassName() + { + return $this->subclassName; + } + + /** + * Returns the raw meta data associated with this GraphEdge. + * + * @return array + */ + public function getMetaData() + { + return $this->metaData; + } + + /** + * Returns the next cursor if it exists. + * + * @return string|null + */ + public function getNextCursor() + { + return $this->getCursor('after'); + } + + /** + * Returns the previous cursor if it exists. + * + * @return string|null + */ + public function getPreviousCursor() + { + return $this->getCursor('before'); + } + + /** + * Returns the cursor for a specific direction if it exists. + * + * @param string $direction The direction of the page: after|before + * + * @return string|null + */ + public function getCursor($direction) + { + if (isset($this->metaData['paging']['cursors'][$direction])) { + return $this->metaData['paging']['cursors'][$direction]; + } + + return null; + } + + /** + * Generates a pagination URL based on a cursor. + * + * @param string $direction The direction of the page: next|previous + * + * @return string|null + * + * @throws FacebookSDKException + */ + public function getPaginationUrl($direction) + { + $this->validateForPagination(); + + // Do we have a paging URL? + if (!isset($this->metaData['paging'][$direction])) { + return null; + } + + $pageUrl = $this->metaData['paging'][$direction]; + + return FacebookUrlManipulator::baseGraphUrlEndpoint($pageUrl); + } + + /** + * Validates whether or not we can paginate on this request. + * + * @throws FacebookSDKException + */ + public function validateForPagination() + { + if ($this->request->getMethod() !== 'GET') { + throw new FacebookSDKException('You can only paginate on a GET request.', 720); + } + } + + /** + * Gets the request object needed to make a next|previous page request. + * + * @param string $direction The direction of the page: next|previous + * + * @return FacebookRequest|null + * + * @throws FacebookSDKException + */ + public function getPaginationRequest($direction) + { + $pageUrl = $this->getPaginationUrl($direction); + if (!$pageUrl) { + return null; + } + + $newRequest = clone $this->request; + $newRequest->setEndpoint($pageUrl); + + return $newRequest; + } + + /** + * Gets the request object needed to make a "next" page request. + * + * @return FacebookRequest|null + * + * @throws FacebookSDKException + */ + public function getNextPageRequest() + { + return $this->getPaginationRequest('next'); + } + + /** + * Gets the request object needed to make a "previous" page request. + * + * @return FacebookRequest|null + * + * @throws FacebookSDKException + */ + public function getPreviousPageRequest() + { + return $this->getPaginationRequest('previous'); + } + + /** + * The total number of results according to Graph if it exists. + * + * This will be returned if the summary=true modifier is present in the request. + * + * @return int|null + */ + public function getTotalCount() + { + if (isset($this->metaData['summary']['total_count'])) { + return $this->metaData['summary']['total_count']; + } + + return null; + } + + /** + * @inheritDoc + */ + public function map(\Closure $callback) + { + return new static( + $this->request, + array_map($callback, $this->items, array_keys($this->items)), + $this->metaData, + $this->parentEdgeEndpoint, + $this->subclassName + ); + } +} diff --git a/src/Facebook/GraphNodes/GraphEvent.php b/src/Facebook/GraphNodes/GraphEvent.php new file mode 100644 index 000000000..a470d89f9 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphEvent.php @@ -0,0 +1,242 @@ + '\Facebook\GraphNodes\GraphCoverPhoto', + 'place' => '\Facebook\GraphNodes\GraphPage', + 'picture' => '\Facebook\GraphNodes\GraphPicture', + 'parent_group' => '\Facebook\GraphNodes\GraphGroup', + ]; + + /** + * Returns the `id` (The event ID) as string if present. + * + * @return string|null + */ + public function getId() + { + return $this->getField('id'); + } + + /** + * Returns the `cover` (Cover picture) as GraphCoverPhoto if present. + * + * @return GraphCoverPhoto|null + */ + public function getCover() + { + return $this->getField('cover'); + } + + /** + * Returns the `description` (Long-form description) as string if present. + * + * @return string|null + */ + public function getDescription() + { + return $this->getField('description'); + } + + /** + * Returns the `end_time` (End time, if one has been set) as DateTime if present. + * + * @return \DateTime|null + */ + public function getEndTime() + { + return $this->getField('end_time'); + } + + /** + * Returns the `is_date_only` (Whether the event only has a date specified, but no time) as bool if present. + * + * @return bool|null + */ + public function getIsDateOnly() + { + return $this->getField('is_date_only'); + } + + /** + * Returns the `name` (Event name) as string if present. + * + * @return string|null + */ + public function getName() + { + return $this->getField('name'); + } + + /** + * Returns the `owner` (The profile that created the event) as GraphNode if present. + * + * @return GraphNode|null + */ + public function getOwner() + { + return $this->getField('owner'); + } + + /** + * Returns the `parent_group` (The group the event belongs to) as GraphGroup if present. + * + * @return GraphGroup|null + */ + public function getParentGroup() + { + return $this->getField('parent_group'); + } + + /** + * Returns the `place` (Event Place information) as GraphPage if present. + * + * @return GraphPage|null + */ + public function getPlace() + { + return $this->getField('place'); + } + + /** + * Returns the `privacy` (Who can see the event) as string if present. + * + * @return string|null + */ + public function getPrivacy() + { + return $this->getField('privacy'); + } + + /** + * Returns the `start_time` (Start time) as DateTime if present. + * + * @return \DateTime|null + */ + public function getStartTime() + { + return $this->getField('start_time'); + } + + /** + * Returns the `ticket_uri` (The link users can visit to buy a ticket to this event) as string if present. + * + * @return string|null + */ + public function getTicketUri() + { + return $this->getField('ticket_uri'); + } + + /** + * Returns the `timezone` (Timezone) as string if present. + * + * @return string|null + */ + public function getTimezone() + { + return $this->getField('timezone'); + } + + /** + * Returns the `updated_time` (Last update time) as DateTime if present. + * + * @return \DateTime|null + */ + public function getUpdatedTime() + { + return $this->getField('updated_time'); + } + + /** + * Returns the `picture` (Event picture) as GraphPicture if present. + * + * @return GraphPicture|null + */ + public function getPicture() + { + return $this->getField('picture'); + } + + /** + * Returns the `attending_count` (Number of people attending the event) as int if present. + * + * @return int|null + */ + public function getAttendingCount() + { + return $this->getField('attending_count'); + } + + /** + * Returns the `declined_count` (Number of people who declined the event) as int if present. + * + * @return int|null + */ + public function getDeclinedCount() + { + return $this->getField('declined_count'); + } + + /** + * Returns the `maybe_count` (Number of people who maybe going to the event) as int if present. + * + * @return int|null + */ + public function getMaybeCount() + { + return $this->getField('maybe_count'); + } + + /** + * Returns the `noreply_count` (Number of people who did not reply to the event) as int if present. + * + * @return int|null + */ + public function getNoreplyCount() + { + return $this->getField('noreply_count'); + } + + /** + * Returns the `invited_count` (Number of people invited to the event) as int if present. + * + * @return int|null + */ + public function getInvitedCount() + { + return $this->getField('invited_count'); + } +} diff --git a/src/Facebook/GraphNodes/GraphGroup.php b/src/Facebook/GraphNodes/GraphGroup.php new file mode 100644 index 000000000..6217bd4dc --- /dev/null +++ b/src/Facebook/GraphNodes/GraphGroup.php @@ -0,0 +1,170 @@ + '\Facebook\GraphNodes\GraphCoverPhoto', + 'venue' => '\Facebook\GraphNodes\GraphLocation', + ]; + + /** + * Returns the `id` (The Group ID) as string if present. + * + * @return string|null + */ + public function getId() + { + return $this->getField('id'); + } + + /** + * Returns the `cover` (The cover photo of the Group) as GraphCoverPhoto if present. + * + * @return GraphCoverPhoto|null + */ + public function getCover() + { + return $this->getField('cover'); + } + + /** + * Returns the `description` (A brief description of the Group) as string if present. + * + * @return string|null + */ + public function getDescription() + { + return $this->getField('description'); + } + + /** + * Returns the `email` (The email address to upload content to the Group. Only current members of the Group can use this) as string if present. + * + * @return string|null + */ + public function getEmail() + { + return $this->getField('email'); + } + + /** + * Returns the `icon` (The URL for the Group's icon) as string if present. + * + * @return string|null + */ + public function getIcon() + { + return $this->getField('icon'); + } + + /** + * Returns the `link` (The Group's website) as string if present. + * + * @return string|null + */ + public function getLink() + { + return $this->getField('link'); + } + + /** + * Returns the `name` (The name of the Group) as string if present. + * + * @return string|null + */ + public function getName() + { + return $this->getField('name'); + } + + /** + * Returns the `member_request_count` (Number of people asking to join the group.) as int if present. + * + * @return int|null + */ + public function getMemberRequestCount() + { + return $this->getField('member_request_count'); + } + + /** + * Returns the `owner` (The profile that created this Group) as GraphNode if present. + * + * @return GraphNode|null + */ + public function getOwner() + { + return $this->getField('owner'); + } + + /** + * Returns the `parent` (The parent Group of this Group, if it exists) as GraphNode if present. + * + * @return GraphNode|null + */ + public function getParent() + { + return $this->getField('parent'); + } + + /** + * Returns the `privacy` (The privacy setting of the Group) as string if present. + * + * @return string|null + */ + public function getPrivacy() + { + return $this->getField('privacy'); + } + + /** + * Returns the `updated_time` (The last time the Group was updated (this includes changes in the Group's properties and changes in posts and comments if user can see them)) as \DateTime if present. + * + * @return \DateTime|null + */ + public function getUpdatedTime() + { + return $this->getField('updated_time'); + } + + /** + * Returns the `venue` (The location for the Group) as GraphLocation if present. + * + * @return GraphLocation|null + */ + public function getVenue() + { + return $this->getField('venue'); + } +} diff --git a/src/Facebook/GraphNodes/GraphList.php b/src/Facebook/GraphNodes/GraphList.php new file mode 100644 index 000000000..3dfbd4975 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphList.php @@ -0,0 +1,36 @@ +getField('street'); + } + + /** + * Returns the city component of the location + * + * @return string|null + */ + public function getCity() + { + return $this->getField('city'); + } + + /** + * Returns the state component of the location + * + * @return string|null + */ + public function getState() + { + return $this->getField('state'); + } + + /** + * Returns the country component of the location + * + * @return string|null + */ + public function getCountry() + { + return $this->getField('country'); + } + + /** + * Returns the zipcode component of the location + * + * @return string|null + */ + public function getZip() + { + return $this->getField('zip'); + } + + /** + * Returns the latitude component of the location + * + * @return float|null + */ + public function getLatitude() + { + return $this->getField('latitude'); + } + + /** + * Returns the street component of the location + * + * @return float|null + */ + public function getLongitude() + { + return $this->getField('longitude'); + } +} diff --git a/src/Facebook/GraphNodes/GraphNode.php b/src/Facebook/GraphNodes/GraphNode.php new file mode 100644 index 000000000..a81c47b7b --- /dev/null +++ b/src/Facebook/GraphNodes/GraphNode.php @@ -0,0 +1,198 @@ +castItems($data)); + } + + /** + * Iterates over an array and detects the types each node + * should be cast to and returns all the items as an array. + * + * @TODO Add auto-casting to AccessToken entities. + * + * @param array $data The array to iterate over. + * + * @return array + */ + public function castItems(array $data) + { + $items = []; + + foreach ($data as $k => $v) { + if ($this->shouldCastAsDateTime($k) + && (is_numeric($v) + || $this->isIso8601DateString($v)) + ) { + $items[$k] = $this->castToDateTime($v); + } elseif ($k === 'birthday') { + $items[$k] = $this->castToBirthday($v); + } else { + $items[$k] = $v; + } + } + + return $items; + } + + /** + * Uncasts any auto-casted datatypes. + * Basically the reverse of castItems(). + * + * @return array + */ + public function uncastItems() + { + $items = $this->asArray(); + + return array_map(function ($v) { + if ($v instanceof \DateTime) { + return $v->format(\DateTime::ISO8601); + } + + return $v; + }, $items); + } + + /** + * Get the collection of items as JSON. + * + * @param int $options + * + * @return string + */ + public function asJson($options = 0) + { + return json_encode($this->uncastItems(), $options); + } + + /** + * Detects an ISO 8601 formatted string. + * + * @param string $string + * + * @return boolean + * + * @see https://developers.facebook.com/docs/graph-api/using-graph-api/#readmodifiers + * @see http://www.cl.cam.ac.uk/~mgk25/iso-time.html + * @see http://en.wikipedia.org/wiki/ISO_8601 + */ + public function isIso8601DateString($string) + { + // This insane regex was yoinked from here: + // http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/ + // ...and I'm all like: + // http://thecodinglove.com/post/95378251969/when-code-works-and-i-dont-know-why + $crazyInsaneRegexThatSomehowDetectsIso8601 = '/^([\+-]?\d{4}(?!\d{2}\b))' + . '((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?' + . '|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d' + . '|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])' + . '((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d' + . '([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/'; + + return preg_match($crazyInsaneRegexThatSomehowDetectsIso8601, $string) === 1; + } + + /** + * Determines if a value from Graph should be cast to DateTime. + * + * @param string $key + * + * @return boolean + */ + public function shouldCastAsDateTime($key) + { + return in_array($key, [ + 'created_time', + 'updated_time', + 'start_time', + 'end_time', + 'backdated_time', + 'issued_at', + 'expires_at', + 'publish_time', + 'joined' + ], true); + } + + /** + * Casts a date value from Graph to DateTime. + * + * @param int|string $value + * + * @return \DateTime + */ + public function castToDateTime($value) + { + if (is_int($value)) { + $dt = new \DateTime(); + $dt->setTimestamp($value); + } else { + $dt = new \DateTime($value); + } + + return $dt; + } + + /** + * Casts a birthday value from Graph to Birthday + * + * @param string $value + * + * @return Birthday + */ + public function castToBirthday($value) + { + return new Birthday($value); + } + + /** + * Getter for $graphObjectMap. + * + * @return array + */ + public static function getObjectMap() + { + return static::$graphObjectMap; + } +} diff --git a/src/Facebook/GraphNodes/GraphNodeFactory.php b/src/Facebook/GraphNodes/GraphNodeFactory.php new file mode 100644 index 000000000..937128bb3 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphNodeFactory.php @@ -0,0 +1,394 @@ +response = $response; + $this->decodedBody = $response->getDecodedBody(); + } + + /** + * Tries to convert a FacebookResponse entity into a GraphNode. + * + * @param string|null $subclassName The GraphNode sub class to cast to. + * + * @return GraphNode + * + * @throws FacebookSDKException + */ + public function makeGraphNode($subclassName = null) + { + $this->validateResponseAsArray(); + $this->validateResponseCastableAsGraphNode(); + + return $this->castAsGraphNodeOrGraphEdge($this->decodedBody, $subclassName); + } + + /** + * Convenience method for creating a GraphAchievement collection. + * + * @return GraphAchievement + * + * @throws FacebookSDKException + */ + public function makeGraphAchievement() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphAchievement'); + } + + /** + * Convenience method for creating a GraphAlbum collection. + * + * @return GraphAlbum + * + * @throws FacebookSDKException + */ + public function makeGraphAlbum() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphAlbum'); + } + + /** + * Convenience method for creating a GraphPage collection. + * + * @return GraphPage + * + * @throws FacebookSDKException + */ + public function makeGraphPage() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphPage'); + } + + /** + * Convenience method for creating a GraphSessionInfo collection. + * + * @return GraphSessionInfo + * + * @throws FacebookSDKException + */ + public function makeGraphSessionInfo() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphSessionInfo'); + } + + /** + * Convenience method for creating a GraphUser collection. + * + * @return GraphUser + * + * @throws FacebookSDKException + */ + public function makeGraphUser() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphUser'); + } + + /** + * Convenience method for creating a GraphEvent collection. + * + * @return GraphEvent + * + * @throws FacebookSDKException + */ + public function makeGraphEvent() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphEvent'); + } + + /** + * Convenience method for creating a GraphGroup collection. + * + * @return GraphGroup + * + * @throws FacebookSDKException + */ + public function makeGraphGroup() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphGroup'); + } + + /** + * Tries to convert a FacebookResponse entity into a GraphEdge. + * + * @param string|null $subclassName The GraphNode sub class to cast the list items to. + * @param boolean $auto_prefix Toggle to auto-prefix the subclass name. + * + * @return GraphEdge + * + * @throws FacebookSDKException + */ + public function makeGraphEdge($subclassName = null, $auto_prefix = true) + { + $this->validateResponseAsArray(); + $this->validateResponseCastableAsGraphEdge(); + + if ($subclassName && $auto_prefix) { + $subclassName = static::BASE_GRAPH_OBJECT_PREFIX . $subclassName; + } + + return $this->castAsGraphNodeOrGraphEdge($this->decodedBody, $subclassName); + } + + /** + * Validates the decoded body. + * + * @throws FacebookSDKException + */ + public function validateResponseAsArray() + { + if (!is_array($this->decodedBody)) { + throw new FacebookSDKException('Unable to get response from Graph as array.', 620); + } + } + + /** + * Validates that the return data can be cast as a GraphNode. + * + * @throws FacebookSDKException + */ + public function validateResponseCastableAsGraphNode() + { + if (isset($this->decodedBody['data']) && static::isCastableAsGraphEdge($this->decodedBody['data'])) { + throw new FacebookSDKException( + 'Unable to convert response from Graph to a GraphNode because the response looks like a GraphEdge. Try using GraphNodeFactory::makeGraphEdge() instead.', + 620 + ); + } + } + + /** + * Validates that the return data can be cast as a GraphEdge. + * + * @throws FacebookSDKException + */ + public function validateResponseCastableAsGraphEdge() + { + if (!(isset($this->decodedBody['data']) && static::isCastableAsGraphEdge($this->decodedBody['data']))) { + throw new FacebookSDKException( + 'Unable to convert response from Graph to a GraphEdge because the response does not look like a GraphEdge. Try using GraphNodeFactory::makeGraphNode() instead.', + 620 + ); + } + } + + /** + * Safely instantiates a GraphNode of $subclassName. + * + * @param array $data The array of data to iterate over. + * @param string|null $subclassName The subclass to cast this collection to. + * + * @return GraphNode + * + * @throws FacebookSDKException + */ + public function safelyMakeGraphNode(array $data, $subclassName = null) + { + $subclassName = $subclassName ?: static::BASE_GRAPH_NODE_CLASS; + static::validateSubclass($subclassName); + + // Remember the parent node ID + $parentNodeId = isset($data['id']) ? $data['id'] : null; + + $items = []; + + foreach ($data as $k => $v) { + // Array means could be recurable + if (is_array($v)) { + // Detect any smart-casting from the $graphObjectMap array. + // This is always empty on the GraphNode collection, but subclasses can define + // their own array of smart-casting types. + $graphObjectMap = $subclassName::getObjectMap(); + $objectSubClass = isset($graphObjectMap[$k]) + ? $graphObjectMap[$k] + : null; + + // Could be a GraphEdge or GraphNode + $items[$k] = $this->castAsGraphNodeOrGraphEdge($v, $objectSubClass, $k, $parentNodeId); + } else { + $items[$k] = $v; + } + } + + return new $subclassName($items); + } + + /** + * Takes an array of values and determines how to cast each node. + * + * @param array $data The array of data to iterate over. + * @param string|null $subclassName The subclass to cast this collection to. + * @param string|null $parentKey The key of this data (Graph edge). + * @param string|null $parentNodeId The parent Graph node ID. + * + * @return GraphNode|GraphEdge + * + * @throws FacebookSDKException + */ + public function castAsGraphNodeOrGraphEdge(array $data, $subclassName = null, $parentKey = null, $parentNodeId = null) + { + if (isset($data['data'])) { + // Create GraphEdge + if (static::isCastableAsGraphEdge($data['data'])) { + return $this->safelyMakeGraphEdge($data, $subclassName, $parentKey, $parentNodeId); + } + // Sometimes Graph is a weirdo and returns a GraphNode under the "data" key + $outerData = $data; + unset($outerData['data']); + $data = $data['data'] + $outerData; + } + + // Create GraphNode + return $this->safelyMakeGraphNode($data, $subclassName); + } + + /** + * Return an array of GraphNode's. + * + * @param array $data The array of data to iterate over. + * @param string|null $subclassName The GraphNode subclass to cast each item in the list to. + * @param string|null $parentKey The key of this data (Graph edge). + * @param string|null $parentNodeId The parent Graph node ID. + * + * @return GraphEdge + * + * @throws FacebookSDKException + */ + public function safelyMakeGraphEdge(array $data, $subclassName = null, $parentKey = null, $parentNodeId = null) + { + if (!isset($data['data'])) { + throw new FacebookSDKException('Cannot cast data to GraphEdge. Expected a "data" key.', 620); + } + + $dataList = []; + foreach ($data['data'] as $graphNode) { + $dataList[] = $this->safelyMakeGraphNode($graphNode, $subclassName); + } + + $metaData = $this->getMetaData($data); + + // We'll need to make an edge endpoint for this in case it's a GraphEdge (for cursor pagination) + $parentGraphEdgeEndpoint = $parentNodeId && $parentKey ? '/' . $parentNodeId . '/' . $parentKey : null; + $className = static::BASE_GRAPH_EDGE_CLASS; + + return new $className($this->response->getRequest(), $dataList, $metaData, $parentGraphEdgeEndpoint, $subclassName); + } + + /** + * Get the meta data from a list in a Graph response. + * + * @param array $data The Graph response. + * + * @return array + */ + public function getMetaData(array $data) + { + unset($data['data']); + + return $data; + } + + /** + * Determines whether or not the data should be cast as a GraphEdge. + * + * @param array $data + * + * @return boolean + */ + public static function isCastableAsGraphEdge(array $data) + { + if ($data === []) { + return true; + } + + // Checks for a sequential numeric array which would be a GraphEdge + return array_keys($data) === range(0, count($data) - 1); + } + + /** + * Ensures that the subclass in question is valid. + * + * @param string $subclassName The GraphNode subclass to validate. + * + * @throws FacebookSDKException + */ + public static function validateSubclass($subclassName) + { + if ($subclassName == static::BASE_GRAPH_NODE_CLASS || is_subclass_of($subclassName, static::BASE_GRAPH_NODE_CLASS)) { + return; + } + + throw new FacebookSDKException('The given subclass "' . $subclassName . '" is not valid. Cannot cast to an object that is not a GraphNode subclass.', 620); + } +} diff --git a/src/Facebook/GraphNodes/GraphObject.php b/src/Facebook/GraphNodes/GraphObject.php new file mode 100644 index 000000000..0633c405b --- /dev/null +++ b/src/Facebook/GraphNodes/GraphObject.php @@ -0,0 +1,36 @@ +makeGraphNode($subclassName); + } + + /** + * Convenience method for creating a GraphEvent collection. + * + * @return GraphEvent + * + * @throws FacebookSDKException + */ + public function makeGraphEvent() + { + return $this->makeGraphNode(static::BASE_GRAPH_OBJECT_PREFIX . 'GraphEvent'); + } + + /** + * Tries to convert a FacebookResponse entity into a GraphEdge. + * + * @param string|null $subclassName The GraphNode sub class to cast the list items to. + * @param boolean $auto_prefix Toggle to auto-prefix the subclass name. + * + * @return GraphEdge + * + * @deprecated 5.0.0 GraphObjectFactory has been renamed to GraphNodeFactory + */ + public function makeGraphList($subclassName = null, $auto_prefix = true) + { + return $this->makeGraphEdge($subclassName, $auto_prefix); + } +} diff --git a/src/Facebook/GraphNodes/GraphPage.php b/src/Facebook/GraphNodes/GraphPage.php new file mode 100644 index 000000000..503b96b55 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphPage.php @@ -0,0 +1,157 @@ + '\Facebook\GraphNodes\GraphPage', + 'global_brand_parent_page' => '\Facebook\GraphNodes\GraphPage', + 'location' => '\Facebook\GraphNodes\GraphLocation', + 'cover' => '\Facebook\GraphNodes\GraphCoverPhoto', + 'picture' => '\Facebook\GraphNodes\GraphPicture', + ]; + + /** + * Returns the ID for the user's page as a string if present. + * + * @return string|null + */ + public function getId() + { + return $this->getField('id'); + } + + /** + * Returns the Category for the user's page as a string if present. + * + * @return string|null + */ + public function getCategory() + { + return $this->getField('category'); + } + + /** + * Returns the Name of the user's page as a string if present. + * + * @return string|null + */ + public function getName() + { + return $this->getField('name'); + } + + /** + * Returns the best available Page on Facebook. + * + * @return GraphPage|null + */ + public function getBestPage() + { + return $this->getField('best_page'); + } + + /** + * Returns the brand's global (parent) Page. + * + * @return GraphPage|null + */ + public function getGlobalBrandParentPage() + { + return $this->getField('global_brand_parent_page'); + } + + /** + * Returns the location of this place. + * + * @return GraphLocation|null + */ + public function getLocation() + { + return $this->getField('location'); + } + + /** + * Returns CoverPhoto of the Page. + * + * @return GraphCoverPhoto|null + */ + public function getCover() + { + return $this->getField('cover'); + } + + /** + * Returns Picture of the Page. + * + * @return GraphPicture|null + */ + public function getPicture() + { + return $this->getField('picture'); + } + + /** + * Returns the page access token for the admin user. + * + * Only available in the `/me/accounts` context. + * + * @return string|null + */ + public function getAccessToken() + { + return $this->getField('access_token'); + } + + /** + * Returns the roles of the page admin user. + * + * Only available in the `/me/accounts` context. + * + * @return array|null + */ + public function getPerms() + { + return $this->getField('perms'); + } + + /** + * Returns the `fan_count` (Number of people who likes to page) as int if present. + * + * @return int|null + */ + public function getFanCount() + { + return $this->getField('fan_count'); + } +} diff --git a/src/Facebook/GraphNodes/GraphPicture.php b/src/Facebook/GraphNodes/GraphPicture.php new file mode 100644 index 000000000..10274ec50 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphPicture.php @@ -0,0 +1,72 @@ +getField('is_silhouette'); + } + + /** + * Returns the url of user picture if it exists + * + * @return string|null + */ + public function getUrl() + { + return $this->getField('url'); + } + + /** + * Returns the width of user picture if it exists + * + * @return int|null + */ + public function getWidth() + { + return $this->getField('width'); + } + + /** + * Returns the height of user picture if it exists + * + * @return int|null + */ + public function getHeight() + { + return $this->getField('height'); + } +} diff --git a/src/Facebook/GraphNodes/GraphSessionInfo.php b/src/Facebook/GraphNodes/GraphSessionInfo.php new file mode 100644 index 000000000..df8dd358b --- /dev/null +++ b/src/Facebook/GraphNodes/GraphSessionInfo.php @@ -0,0 +1,102 @@ +getField('app_id'); + } + + /** + * Returns the application name the token was issued for. + * + * @return string|null + */ + public function getApplication() + { + return $this->getField('application'); + } + + /** + * Returns the date & time that the token expires. + * + * @return \DateTime|null + */ + public function getExpiresAt() + { + return $this->getField('expires_at'); + } + + /** + * Returns whether the token is valid. + * + * @return boolean + */ + public function getIsValid() + { + return $this->getField('is_valid'); + } + + /** + * Returns the date & time the token was issued at. + * + * @return \DateTime|null + */ + public function getIssuedAt() + { + return $this->getField('issued_at'); + } + + /** + * Returns the scope permissions associated with the token. + * + * @return array + */ + public function getScopes() + { + return $this->getField('scopes'); + } + + /** + * Returns the login id of the user associated with the token. + * + * @return string|null + */ + public function getUserId() + { + return $this->getField('user_id'); + } +} diff --git a/src/Facebook/GraphNodes/GraphUser.php b/src/Facebook/GraphNodes/GraphUser.php new file mode 100644 index 000000000..6e1ed8f54 --- /dev/null +++ b/src/Facebook/GraphNodes/GraphUser.php @@ -0,0 +1,172 @@ + '\Facebook\GraphNodes\GraphPage', + 'location' => '\Facebook\GraphNodes\GraphPage', + 'significant_other' => '\Facebook\GraphNodes\GraphUser', + 'picture' => '\Facebook\GraphNodes\GraphPicture', + ]; + + /** + * Returns the ID for the user as a string if present. + * + * @return string|null + */ + public function getId() + { + return $this->getField('id'); + } + + /** + * Returns the name for the user as a string if present. + * + * @return string|null + */ + public function getName() + { + return $this->getField('name'); + } + + /** + * Returns the first name for the user as a string if present. + * + * @return string|null + */ + public function getFirstName() + { + return $this->getField('first_name'); + } + + /** + * Returns the middle name for the user as a string if present. + * + * @return string|null + */ + public function getMiddleName() + { + return $this->getField('middle_name'); + } + + /** + * Returns the last name for the user as a string if present. + * + * @return string|null + */ + public function getLastName() + { + return $this->getField('last_name'); + } + + /** + * Returns the email for the user as a string if present. + * + * @return string|null + */ + public function getEmail() + { + return $this->getField('email'); + } + + /** + * Returns the gender for the user as a string if present. + * + * @return string|null + */ + public function getGender() + { + return $this->getField('gender'); + } + + /** + * Returns the Facebook URL for the user as a string if available. + * + * @return string|null + */ + public function getLink() + { + return $this->getField('link'); + } + + /** + * Returns the users birthday, if available. + * + * @return Birthday|null + */ + public function getBirthday() + { + return $this->getField('birthday'); + } + + /** + * Returns the current location of the user as a GraphPage. + * + * @return GraphPage|null + */ + public function getLocation() + { + return $this->getField('location'); + } + + /** + * Returns the current location of the user as a GraphPage. + * + * @return GraphPage|null + */ + public function getHometown() + { + return $this->getField('hometown'); + } + + /** + * Returns the current location of the user as a GraphUser. + * + * @return GraphUser|null + */ + public function getSignificantOther() + { + return $this->getField('significant_other'); + } + + /** + * Returns the picture of the user as a GraphPicture + * + * @return GraphPicture|null + */ + public function getPicture() + { + return $this->getField('picture'); + } +} diff --git a/src/Facebook/GraphObject.php b/src/Facebook/GraphObject.php deleted file mode 100644 index 4f22d2b22..000000000 --- a/src/Facebook/GraphObject.php +++ /dev/null @@ -1,167 +0,0 @@ - - * @author David Poll - */ -class GraphObject -{ - - /** - * @var array - Holds the raw associative data for this object - */ - protected $backingData; - - /** - * Creates a GraphObject using the data provided. - * - * @param array $raw - */ - public function __construct($raw) - { - if ($raw instanceof \stdClass) { - $raw = get_object_vars($raw); - } - $this->backingData = $raw; - - if (isset($this->backingData['data']) && count($this->backingData) === 1) { - $this->backingData = $this->backingData['data']; - } - } - - /** - * cast - Return a new instance of a FacebookGraphObject subclass for this - * objects underlying data. - * - * @param string $type The GraphObject subclass to cast to - * - * @return GraphObject - * - * @throws FacebookSDKException - */ - public function cast($type) - { - if ($this instanceof $type) { - return $this; - } - if (is_subclass_of($type, GraphObject::className())) { - return new $type($this->backingData); - } else { - throw new FacebookSDKException( - 'Cannot cast to an object that is not a GraphObject subclass', 620 - ); - } - } - - /** - * asArray - Return a key-value associative array for the given graph object. - * - * @return array - */ - public function asArray() - { - return $this->backingData; - } - - /** - * getProperty - Gets the value of the named property for this graph object, - * cast to the appropriate subclass type if provided. - * - * @param string $name The property to retrieve - * @param string $type The subclass of GraphObject, optionally - * - * @return mixed - */ - public function getProperty($name, $type = 'Facebook\GraphObject') - { - if (isset($this->backingData[$name])) { - $value = $this->backingData[$name]; - if (is_scalar($value)) { - return $value; - } else { - return (new GraphObject($value))->cast($type); - } - } else { - return null; - } - } - - /** - * getPropertyAsArray - Get the list value of a named property for this graph - * object, where each item has been cast to the appropriate subclass type - * if provided. - * - * Calling this for a property that is not an array, the behavior - * is undefined, so don’t do this. - * - * @param string $name The property to retrieve - * @param string $type The subclass of GraphObject, optionally - * - * @return array - */ - public function getPropertyAsArray($name, $type = 'Facebook\GraphObject') - { - $target = array(); - if (isset($this->backingData[$name]['data'])) { - $target = $this->backingData[$name]['data']; - } else if (isset($this->backingData[$name]) - && !is_scalar($this->backingData[$name])) { - $target = $this->backingData[$name]; - } - $out = array(); - foreach ($target as $key => $value) { - if (is_scalar($value)) { - $out[$key] = $value; - } else { - $out[$key] = (new GraphObject($value))->cast($type); - } - } - return $out; - } - - /** - * getPropertyNames - Returns a list of all properties set on the object. - * - * @return array - */ - public function getPropertyNames() - { - return array_keys($this->backingData); - } - - /** - * Returns the string class name of the GraphObject or subclass. - * - * @return string - */ - public static function className() - { - return get_called_class(); - } - -} \ No newline at end of file diff --git a/src/Facebook/GraphSessionInfo.php b/src/Facebook/GraphSessionInfo.php deleted file mode 100644 index 4b97580ae..000000000 --- a/src/Facebook/GraphSessionInfo.php +++ /dev/null @@ -1,115 +0,0 @@ - - * @author David Poll - */ -class GraphSessionInfo extends GraphObject -{ - - /** - * Returns the application id the token was issued for. - * - * @return string|null - */ - public function getAppId() - { - return $this->getProperty('app_id'); - } - - /** - * Returns the application name the token was issued for. - * - * @return string|null - */ - public function getApplication() - { - return $this->getProperty('application'); - } - - /** - * Returns the date & time that the token expires. - * - * @return \DateTime|null - */ - public function getExpiresAt() - { - $stamp = $this->getProperty('expires_at'); - if ($stamp) { - return (new \DateTime())->setTimestamp($stamp); - } else { - return null; - } - } - - /** - * Returns whether the token is valid. - * - * @return boolean - */ - public function isValid() - { - return $this->getProperty('is_valid'); - } - - /** - * Returns the date & time the token was issued at. - * - * @return \DateTime|null - */ - public function getIssuedAt() - { - $stamp = $this->getProperty('issued_at'); - if ($stamp) { - return (new \DateTime())->setTimestamp($stamp); - } else { - return null; - } - } - - /** - * Returns the scope permissions associated with the token. - * - * @return array - */ - public function getScopes() - { - return $this->getPropertyAsArray('scopes'); - } - - /** - * Returns the login id of the user associated with the token. - * - * @return string|null - */ - public function getId() - { - return $this->getProperty('user_id'); - } - -} \ No newline at end of file diff --git a/src/Facebook/GraphUser.php b/src/Facebook/GraphUser.php deleted file mode 100644 index 8da369f9c..000000000 --- a/src/Facebook/GraphUser.php +++ /dev/null @@ -1,120 +0,0 @@ - - * @author David Poll - */ -class GraphUser extends GraphObject -{ - - /** - * Returns the ID for the user as a string if present. - * - * @return string|null - */ - public function getId() - { - return $this->getProperty('id'); - } - - /** - * Returns the name for the user as a string if present. - * - * @return string|null - */ - public function getName() - { - return $this->getProperty('name'); - } - - /** - * Returns the first name for the user as a string if present. - * - * @return string|null - */ - public function getFirstName() - { - return $this->getProperty('first_name'); - } - - /** - * Returns the middle name for the user as a string if present. - * - * @return string|null - */ - public function getMiddleName() - { - return $this->getProperty('middle_name'); - } - - /** - * Returns the last name for the user as a string if present. - * - * @return string|null - */ - public function getLastName() - { - return $this->getProperty('last_name'); - } - - /** - * Returns the Facebook URL for the user as a string if available. - * - * @return string|null - */ - public function getLink() - { - return $this->getProperty('link'); - } - - /** - * Returns the users birthday, if available. - * - * @return \DateTime|null - */ - public function getBirthday() - { - $value = $this->getProperty('birthday'); - if ($value) { - return new \DateTime($value); - } - return null; - } - - /** - * Returns the current location of the user as a FacebookGraphLocation - * if available. - * - * @return GraphLocation|null - */ - public function getLocation() - { - return $this->getProperty('location', GraphLocation::className()); - } - -} \ No newline at end of file diff --git a/src/Facebook/GraphUserPage.php b/src/Facebook/GraphUserPage.php deleted file mode 100644 index 9115da113..000000000 --- a/src/Facebook/GraphUserPage.php +++ /dev/null @@ -1,84 +0,0 @@ - - */ -class GraphUserPage extends GraphObject -{ - - /** - * Returns the ID for the user's page as a string if present. - * - * @return string|null - */ - public function getId() - { - return $this->getProperty('id'); - } - - /** - * Returns the Category for the user's page as a string if present. - * - * @return string|null - */ - public function getCategory() - { - return $this->getProperty('category'); - } - - /** - * Returns the Name of the user's page as a string if present. - * - * @return string|null - */ - public function getName() - { - return $this->getProperty('name'); - } - - /** - * Returns the Access Token used to access the user's page as a string if present. - * - * @return string|null - */ - public function getAccessToken() - { - return $this->getProperty('access_token'); - } - - /** - * Returns the Permissions for the user's page as an array if present. - * - * @return array|null - */ - public function getPermissions() - { - return $this->getProperty('perms'); - } - -} \ No newline at end of file diff --git a/src/Facebook/FacebookCanvasLoginHelper.php b/src/Facebook/Helpers/FacebookCanvasHelper.php similarity index 54% rename from src/Facebook/FacebookCanvasLoginHelper.php rename to src/Facebook/Helpers/FacebookCanvasHelper.php index f673d9e6d..7f3466ff7 100644 --- a/src/Facebook/FacebookCanvasLoginHelper.php +++ b/src/Facebook/Helpers/FacebookCanvasHelper.php @@ -1,6 +1,6 @@ - * @author David Poll */ -class FacebookCanvasLoginHelper extends FacebookSignedRequestFromInputHelper +class FacebookCanvasHelper extends FacebookSignedRequestFromInputHelper { - - /** - * Returns the app data value. - * - * @return mixed|null - */ - public function getAppData() - { - return $this->signedRequest ? $this->signedRequest->get('app_data') : null; - } - - /** - * Get raw signed request from either GET or POST. - * - * @return string|null - */ - public function getRawSignedRequest() - { /** - * v2.0 apps use GET for Canvas signed requests. + * Returns the app data value. + * + * @return mixed|null */ - $rawSignedRequest = $this->getRawSignedRequestFromGet(); - if ($rawSignedRequest) { - return $rawSignedRequest; + public function getAppData() + { + return $this->signedRequest ? $this->signedRequest->get('app_data') : null; } /** - * v1.0 apps use POST for Canvas signed requests, will eventually be - * deprecated. + * Get raw signed request from POST. + * + * @return string|null */ - $rawSignedRequest = $this->getRawSignedRequestFromPost(); - if ($rawSignedRequest) { - return $rawSignedRequest; + public function getRawSignedRequest() + { + return $this->getRawSignedRequestFromPost() ?: null; } - - return null; - } - } diff --git a/src/Facebook/FacebookJavaScriptLoginHelper.php b/src/Facebook/Helpers/FacebookJavaScriptHelper.php similarity index 73% rename from src/Facebook/FacebookJavaScriptLoginHelper.php rename to src/Facebook/Helpers/FacebookJavaScriptHelper.php index b0a3e7e21..01a76b8b2 100644 --- a/src/Facebook/FacebookJavaScriptLoginHelper.php +++ b/src/Facebook/Helpers/FacebookJavaScriptHelper.php @@ -1,6 +1,6 @@ - * @author David Poll */ -class FacebookJavaScriptLoginHelper extends FacebookSignedRequestFromInputHelper +class FacebookJavaScriptHelper extends FacebookSignedRequestFromInputHelper { - - /** - * Get raw signed request from the cookie. - * - * @return string|null - */ - public function getRawSignedRequest() - { - return $this->getRawSignedRequestFromCookie(); - } - + /** + * Get raw signed request from the cookie. + * + * @return string|null + */ + public function getRawSignedRequest() + { + return $this->getRawSignedRequestFromCookie(); + } } diff --git a/src/Facebook/Helpers/FacebookPageTabHelper.php b/src/Facebook/Helpers/FacebookPageTabHelper.php new file mode 100644 index 000000000..da2c356c7 --- /dev/null +++ b/src/Facebook/Helpers/FacebookPageTabHelper.php @@ -0,0 +1,95 @@ +signedRequest) { + return; + } + + $this->pageData = $this->signedRequest->get('page'); + } + + /** + * Returns a value from the page data. + * + * @param string $key + * @param mixed|null $default + * + * @return mixed|null + */ + public function getPageData($key, $default = null) + { + if (isset($this->pageData[$key])) { + return $this->pageData[$key]; + } + + return $default; + } + + /** + * Returns true if the user is an admin. + * + * @return boolean + */ + public function isAdmin() + { + return $this->getPageData('admin') === true; + } + + /** + * Returns the page id if available. + * + * @return string|null + */ + public function getPageId() + { + return $this->getPageData('id'); + } +} diff --git a/src/Facebook/Helpers/FacebookRedirectLoginHelper.php b/src/Facebook/Helpers/FacebookRedirectLoginHelper.php new file mode 100644 index 000000000..6003a20f3 --- /dev/null +++ b/src/Facebook/Helpers/FacebookRedirectLoginHelper.php @@ -0,0 +1,333 @@ +oAuth2Client = $oAuth2Client; + $this->persistentDataHandler = $persistentDataHandler ?: new FacebookSessionPersistentDataHandler(); + $this->urlDetectionHandler = $urlHandler ?: new FacebookUrlDetectionHandler(); + $this->pseudoRandomStringGenerator = PseudoRandomStringGeneratorFactory::createPseudoRandomStringGenerator($prsg); + } + + /** + * Returns the persistent data handler. + * + * @return PersistentDataInterface + */ + public function getPersistentDataHandler() + { + return $this->persistentDataHandler; + } + + /** + * Returns the URL detection handler. + * + * @return UrlDetectionInterface + */ + public function getUrlDetectionHandler() + { + return $this->urlDetectionHandler; + } + + /** + * Returns the cryptographically secure pseudo-random string generator. + * + * @return PseudoRandomStringGeneratorInterface + */ + public function getPseudoRandomStringGenerator() + { + return $this->pseudoRandomStringGenerator; + } + + /** + * Stores CSRF state and returns a URL to which the user should be sent to in order to continue the login process with Facebook. + * + * @param string $redirectUrl The URL Facebook should redirect users to after login. + * @param array $scope List of permissions to request during login. + * @param array $params An array of parameters to generate URL. + * @param string $separator The separator to use in http_build_query(). + * + * @return string + */ + private function makeUrl($redirectUrl, array $scope, array $params = [], $separator = '&') + { + $state = $this->persistentDataHandler->get('state') ?: $this->pseudoRandomStringGenerator->getPseudoRandomString(static::CSRF_LENGTH); + $this->persistentDataHandler->set('state', $state); + + return $this->oAuth2Client->getAuthorizationUrl($redirectUrl, $state, $scope, $params, $separator); + } + + /** + * Returns the URL to send the user in order to login to Facebook. + * + * @param string $redirectUrl The URL Facebook should redirect users to after login. + * @param array $scope List of permissions to request during login. + * @param string $separator The separator to use in http_build_query(). + * + * @return string + */ + public function getLoginUrl($redirectUrl, array $scope = [], $separator = '&') + { + return $this->makeUrl($redirectUrl, $scope, [], $separator); + } + + /** + * Returns the URL to send the user in order to log out of Facebook. + * + * @param AccessToken|string $accessToken The access token that will be logged out. + * @param string $next The url Facebook should redirect the user to after a successful logout. + * @param string $separator The separator to use in http_build_query(). + * + * @return string + * + * @throws FacebookSDKException + */ + public function getLogoutUrl($accessToken, $next, $separator = '&') + { + if (!$accessToken instanceof AccessToken) { + $accessToken = new AccessToken($accessToken); + } + + if ($accessToken->isAppAccessToken()) { + throw new FacebookSDKException('Cannot generate a logout URL with an app access token.', 722); + } + + $params = [ + 'next' => $next, + 'access_token' => $accessToken->getValue(), + ]; + + return 'https://www.facebook.com/logout.php?' . http_build_query($params, null, $separator); + } + + /** + * Returns the URL to send the user in order to login to Facebook with permission(s) to be re-asked. + * + * @param string $redirectUrl The URL Facebook should redirect users to after login. + * @param array $scope List of permissions to request during login. + * @param string $separator The separator to use in http_build_query(). + * + * @return string + */ + public function getReRequestUrl($redirectUrl, array $scope = [], $separator = '&') + { + $params = ['auth_type' => 'rerequest']; + + return $this->makeUrl($redirectUrl, $scope, $params, $separator); + } + + /** + * Returns the URL to send the user in order to login to Facebook with user to be re-authenticated. + * + * @param string $redirectUrl The URL Facebook should redirect users to after login. + * @param array $scope List of permissions to request during login. + * @param string $separator The separator to use in http_build_query(). + * + * @return string + */ + public function getReAuthenticationUrl($redirectUrl, array $scope = [], $separator = '&') + { + $params = ['auth_type' => 'reauthenticate']; + + return $this->makeUrl($redirectUrl, $scope, $params, $separator); + } + + /** + * Takes a valid code from a login redirect, and returns an AccessToken entity. + * + * @param string|null $redirectUrl The redirect URL. + * + * @return AccessToken|null + * + * @throws FacebookSDKException + */ + public function getAccessToken($redirectUrl = null) + { + if (!$code = $this->getCode()) { + return null; + } + + $this->validateCsrf(); + $this->resetCsrf(); + + $redirectUrl = $redirectUrl ?: $this->urlDetectionHandler->getCurrentUrl(); + // At minimum we need to remove the 'code', 'enforce_https' and 'state' params + $redirectUrl = FacebookUrlManipulator::removeParamsFromUrl($redirectUrl, ['code', 'enforce_https', 'state']); + + return $this->oAuth2Client->getAccessTokenFromCode($code, $redirectUrl); + } + + /** + * Validate the request against a cross-site request forgery. + * + * @throws FacebookSDKException + */ + protected function validateCsrf() + { + $state = $this->getState(); + if (!$state) { + throw new FacebookSDKException('Cross-site request forgery validation failed. Required GET param "state" missing.'); + } + $savedState = $this->persistentDataHandler->get('state'); + if (!$savedState) { + throw new FacebookSDKException('Cross-site request forgery validation failed. Required param "state" missing from persistent data.'); + } + + if (\hash_equals($savedState, $state)) { + return; + } + + throw new FacebookSDKException('Cross-site request forgery validation failed. The "state" param from the URL and session do not match.'); + } + + /** + * Resets the CSRF so that it doesn't get reused. + */ + private function resetCsrf() + { + $this->persistentDataHandler->set('state', null); + } + + /** + * Return the code. + * + * @return string|null + */ + protected function getCode() + { + return $this->getInput('code'); + } + + /** + * Return the state. + * + * @return string|null + */ + protected function getState() + { + return $this->getInput('state'); + } + + /** + * Return the error code. + * + * @return string|null + */ + public function getErrorCode() + { + return $this->getInput('error_code'); + } + + /** + * Returns the error. + * + * @return string|null + */ + public function getError() + { + return $this->getInput('error'); + } + + /** + * Returns the error reason. + * + * @return string|null + */ + public function getErrorReason() + { + return $this->getInput('error_reason'); + } + + /** + * Returns the error description. + * + * @return string|null + */ + public function getErrorDescription() + { + return $this->getInput('error_description'); + } + + /** + * Returns a value from a GET param. + * + * @param string $key + * + * @return string|null + */ + private function getInput($key) + { + return isset($_GET[$key]) ? $_GET[$key] : null; + } +} diff --git a/src/Facebook/Helpers/FacebookSignedRequestFromInputHelper.php b/src/Facebook/Helpers/FacebookSignedRequestFromInputHelper.php new file mode 100644 index 000000000..4044da107 --- /dev/null +++ b/src/Facebook/Helpers/FacebookSignedRequestFromInputHelper.php @@ -0,0 +1,166 @@ +app = $app; + $graphVersion = $graphVersion ?: Facebook::DEFAULT_GRAPH_VERSION; + $this->oAuth2Client = new OAuth2Client($this->app, $client, $graphVersion); + + $this->instantiateSignedRequest(); + } + + /** + * Instantiates a new SignedRequest entity. + * + * @param string|null + */ + public function instantiateSignedRequest($rawSignedRequest = null) + { + $rawSignedRequest = $rawSignedRequest ?: $this->getRawSignedRequest(); + + if (!$rawSignedRequest) { + return; + } + + $this->signedRequest = new SignedRequest($this->app, $rawSignedRequest); + } + + /** + * Returns an AccessToken entity from the signed request. + * + * @return AccessToken|null + * + * @throws \Facebook\Exceptions\FacebookSDKException + */ + public function getAccessToken() + { + if ($this->signedRequest && $this->signedRequest->hasOAuthData()) { + $code = $this->signedRequest->get('code'); + $accessToken = $this->signedRequest->get('oauth_token'); + + if ($code && !$accessToken) { + return $this->oAuth2Client->getAccessTokenFromCode($code); + } + + $expiresAt = $this->signedRequest->get('expires', 0); + + return new AccessToken($accessToken, $expiresAt); + } + + return null; + } + + /** + * Returns the SignedRequest entity. + * + * @return SignedRequest|null + */ + public function getSignedRequest() + { + return $this->signedRequest; + } + + /** + * Returns the user_id if available. + * + * @return string|null + */ + public function getUserId() + { + return $this->signedRequest ? $this->signedRequest->getUserId() : null; + } + + /** + * Get raw signed request from input. + * + * @return string|null + */ + abstract public function getRawSignedRequest(); + + /** + * Get raw signed request from POST input. + * + * @return string|null + */ + public function getRawSignedRequestFromPost() + { + if (isset($_POST['signed_request'])) { + return $_POST['signed_request']; + } + + return null; + } + + /** + * Get raw signed request from cookie set from the Javascript SDK. + * + * @return string|null + */ + public function getRawSignedRequestFromCookie() + { + if (isset($_COOKIE['fbsr_' . $this->app->getId()])) { + return $_COOKIE['fbsr_' . $this->app->getId()]; + } + + return null; + } +} diff --git a/src/Facebook/Http/GraphRawResponse.php b/src/Facebook/Http/GraphRawResponse.php new file mode 100644 index 000000000..44105c495 --- /dev/null +++ b/src/Facebook/Http/GraphRawResponse.php @@ -0,0 +1,138 @@ +httpResponseCode = (int)$httpStatusCode; + } + + if (is_array($headers)) { + $this->headers = $headers; + } else { + $this->setHeadersFromString($headers); + } + + $this->body = $body; + } + + /** + * Return the response headers. + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Return the body of the response. + * + * @return string + */ + public function getBody() + { + return $this->body; + } + + /** + * Return the HTTP response code. + * + * @return int + */ + public function getHttpResponseCode() + { + return $this->httpResponseCode; + } + + /** + * Sets the HTTP response code from a raw header. + * + * @param string $rawResponseHeader + */ + public function setHttpResponseCodeFromHeader($rawResponseHeader) + { + // https://tools.ietf.org/html/rfc7230#section-3.1.2 + list($version, $status, $reason) = array_pad(explode(' ', $rawResponseHeader, 3), 3, null); + $this->httpResponseCode = (int) $status; + } + + /** + * Parse the raw headers and set as an array. + * + * @param string $rawHeaders The raw headers from the response. + */ + protected function setHeadersFromString($rawHeaders) + { + // Normalize line breaks + $rawHeaders = str_replace("\r\n", "\n", $rawHeaders); + + // There will be multiple headers if a 301 was followed + // or a proxy was followed, etc + $headerCollection = explode("\n\n", trim($rawHeaders)); + // We just want the last response (at the end) + $rawHeader = array_pop($headerCollection); + + $headerComponents = explode("\n", $rawHeader); + foreach ($headerComponents as $line) { + if (strpos($line, ': ') === false) { + $this->setHttpResponseCodeFromHeader($line); + } else { + list($key, $value) = explode(': ', $line, 2); + $this->headers[$key] = $value; + } + } + } +} diff --git a/src/Facebook/Http/RequestBodyInterface.php b/src/Facebook/Http/RequestBodyInterface.php new file mode 100644 index 000000000..1c03f4fd7 --- /dev/null +++ b/src/Facebook/Http/RequestBodyInterface.php @@ -0,0 +1,39 @@ +params = $params; + $this->files = $files; + $this->boundary = $boundary ?: uniqid(); + } + + /** + * @inheritdoc + */ + public function getBody() + { + $body = ''; + + // Compile normal params + $params = $this->getNestedParams($this->params); + foreach ($params as $k => $v) { + $body .= $this->getParamString($k, $v); + } + + // Compile files + foreach ($this->files as $k => $v) { + $body .= $this->getFileString($k, $v); + } + + // Peace out + $body .= "--{$this->boundary}--\r\n"; + + return $body; + } + + /** + * Get the boundary + * + * @return string + */ + public function getBoundary() + { + return $this->boundary; + } + + /** + * Get the string needed to transfer a file. + * + * @param string $name + * @param FacebookFile $file + * + * @return string + */ + private function getFileString($name, FacebookFile $file) + { + return sprintf( + "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"%s\r\n\r\n%s\r\n", + $this->boundary, + $name, + $file->getFileName(), + $this->getFileHeaders($file), + $file->getContents() + ); + } + + /** + * Get the string needed to transfer a POST field. + * + * @param string $name + * @param string $value + * + * @return string + */ + private function getParamString($name, $value) + { + return sprintf( + "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", + $this->boundary, + $name, + $value + ); + } + + /** + * Returns the params as an array of nested params. + * + * @param array $params + * + * @return array + */ + private function getNestedParams(array $params) + { + $query = http_build_query($params, null, '&'); + $params = explode('&', $query); + $result = []; + + foreach ($params as $param) { + list($key, $value) = explode('=', $param, 2); + $result[urldecode($key)] = urldecode($value); + } + + return $result; + } + + /** + * Get the headers needed before transferring the content of a POST file. + * + * @param FacebookFile $file + * + * @return string + */ + protected function getFileHeaders(FacebookFile $file) + { + return "\r\nContent-Type: {$file->getMimetype()}"; + } +} diff --git a/src/Facebook/GraphPage.php b/src/Facebook/Http/RequestBodyUrlEncoded.php similarity index 60% rename from src/Facebook/GraphPage.php rename to src/Facebook/Http/RequestBodyUrlEncoded.php index a66068e99..c1e35f43d 100644 --- a/src/Facebook/GraphPage.php +++ b/src/Facebook/Http/RequestBodyUrlEncoded.php @@ -1,6 +1,6 @@ */ -class GraphPage extends GraphObject +class RequestBodyUrlEncoded implements RequestBodyInterface { + /** + * @var array The parameters to send with this request. + */ + protected $params = []; - /** - * Returns the ID for the user's page as a string if present. - * - * @return string|null - */ - public function getId() - { - return $this->getProperty('id'); - } - - /** - * Returns the Category for the user's page as a string if present. - * - * @return string|null - */ - public function getCategory() - { - return $this->getProperty('category'); - } - - /** - * Returns the Name of the user's page as a string if present. - * - * @return string|null - */ - public function getName() - { - return $this->getProperty('name'); - } + /** + * Creates a new GraphUrlEncodedBody entity. + * + * @param array $params + */ + public function __construct(array $params) + { + $this->params = $params; + } -} \ No newline at end of file + /** + * @inheritdoc + */ + public function getBody() + { + return http_build_query($this->params, null, '&'); + } +} diff --git a/src/Facebook/HttpClients/FacebookCurl.php b/src/Facebook/HttpClients/FacebookCurl.php old mode 100755 new mode 100644 index 5b20e92db..28e4ba598 --- a/src/Facebook/HttpClients/FacebookCurl.php +++ b/src/Facebook/HttpClients/FacebookCurl.php @@ -1,6 +1,6 @@ curl = curl_init(); - } + /** + * @var resource Curl resource instance + */ + protected $curl; - /** - * Set a curl option - * - * @param $key - * @param $value - */ - public function setopt($key, $value) - { - curl_setopt($this->curl, $key, $value); - } + /** + * Make a new curl reference instance + */ + public function init() + { + $this->curl = curl_init(); + } - /** - * Set an array of options to a curl resource - * - * @param array $options - */ - public function setopt_array(array $options) - { - curl_setopt_array($this->curl, $options); - } + /** + * Set a curl option + * + * @param $key + * @param $value + */ + public function setopt($key, $value) + { + curl_setopt($this->curl, $key, $value); + } - /** - * Send a curl request - * - * @return mixed - */ - public function exec() - { - return curl_exec($this->curl); - } + /** + * Set an array of options to a curl resource + * + * @param array $options + */ + public function setoptArray(array $options) + { + curl_setopt_array($this->curl, $options); + } - /** - * Return the curl error number - * - * @return int - */ - public function errno() - { - return curl_errno($this->curl); - } + /** + * Send a curl request + * + * @return mixed + */ + public function exec() + { + return curl_exec($this->curl); + } - /** - * Return the curl error message - * - * @return string - */ - public function error() - { - return curl_error($this->curl); - } + /** + * Return the curl error number + * + * @return int + */ + public function errno() + { + return curl_errno($this->curl); + } - /** - * Get info from a curl reference - * - * @param $type - * - * @return mixed - */ - public function getinfo($type) - { - return curl_getinfo($this->curl, $type); - } + /** + * Return the curl error message + * + * @return string + */ + public function error() + { + return curl_error($this->curl); + } - /** - * Get the currently installed curl version - * - * @return array - */ - public function version() - { - return curl_version(); - } + /** + * Get info from a curl reference + * + * @param $type + * + * @return mixed + */ + public function getinfo($type) + { + return curl_getinfo($this->curl, $type); + } - /** - * Close the resource connection to curl - */ - public function close() - { - curl_close($this->curl); - } + /** + * Get the currently installed curl version + * + * @return array + */ + public function version() + { + return curl_version(); + } + /** + * Close the resource connection to curl + */ + public function close() + { + curl_close($this->curl); + } } diff --git a/src/Facebook/HttpClients/FacebookCurlHttpClient.php b/src/Facebook/HttpClients/FacebookCurlHttpClient.php old mode 100755 new mode 100644 index bfe3fce4b..9516cc835 --- a/src/Facebook/HttpClients/FacebookCurlHttpClient.php +++ b/src/Facebook/HttpClients/FacebookCurlHttpClient.php @@ -1,6 +1,6 @@ requestHeaders[$key] = $value; - } - - /** - * The headers returned in the response - * - * @return array - */ - public function getResponseHeaders() - { - return $this->responseHeaders; - } - - /** - * The HTTP status response code - * - * @return int - */ - public function getResponseHttpStatusCode() - { - return $this->responseHttpStatusCode; - } - - /** - * Sends a request to the server - * - * @param string $url The endpoint to send the request to - * @param string $method The request method - * @param array $parameters The key value pairs to be sent in the body - * - * @return string Raw response from the server - * - * @throws \Facebook\FacebookSDKException - */ - public function send($url, $method = 'GET', $parameters = array()) - { - $this->openConnection($url, $method, $parameters); - $this->tryToSendRequest(); - - // Need to verify the peer - if ($this->curlErrorCode == 60 || $this->curlErrorCode == 77) { - $this->addBundledCert(); - $this->tryToSendRequest(); - } - - if ($this->curlErrorCode) { - throw new FacebookSDKException($this->curlErrorMessage, $this->curlErrorCode); + /** + * @var string The client error message + */ + protected $curlErrorMessage = ''; + + /** + * @var int The curl client error code + */ + protected $curlErrorCode = 0; + + /** + * @var string|boolean The raw response from the server + */ + protected $rawResponse; + + /** + * @var FacebookCurl Procedural curl as object + */ + protected $facebookCurl; + + /** + * @param FacebookCurl|null Procedural curl as object + */ + public function __construct(FacebookCurl $facebookCurl = null) + { + $this->facebookCurl = $facebookCurl ?: new FacebookCurl(); } - // Separate the raw headers from the raw body - list($rawHeaders, $rawBody) = $this->extractResponseHeadersAndBody(); - - $this->responseHeaders = self::headersToArray($rawHeaders); + /** + * @inheritdoc + */ + public function send($url, $method, $body, array $headers, $timeOut) + { + $this->openConnection($url, $method, $body, $headers, $timeOut); + $this->sendRequest(); - $this->closeConnection(); + if ($curlErrorCode = $this->facebookCurl->errno()) { + throw new FacebookSDKException($this->facebookCurl->error(), $curlErrorCode); + } - return $rawBody; - } + // Separate the raw headers from the raw body + list($rawHeaders, $rawBody) = $this->extractResponseHeadersAndBody(); - /** - * Opens a new curl connection - * - * @param string $url The endpoint to send the request to - * @param string $method The request method - * @param array $parameters The key value pairs to be sent in the body - */ - public function openConnection($url, $method = 'GET', $parameters = array()) - { - $options = array( - CURLOPT_URL => $url, - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_TIMEOUT => 60, - CURLOPT_RETURNTRANSFER => true, // Follow 301 redirects - CURLOPT_HEADER => true, // Enable header processing - ); + $this->closeConnection(); - if ($method !== "GET") { - $options[CURLOPT_POSTFIELDS] = $parameters; - } - if ($method === 'DELETE' || $method === 'PUT') { - $options[CURLOPT_CUSTOMREQUEST] = $method; + return new GraphRawResponse($rawHeaders, $rawBody); } - if (!empty($this->requestHeaders)) { - $options[CURLOPT_HTTPHEADER] = $this->compileRequestHeaders(); + /** + * Opens a new curl connection. + * + * @param string $url The endpoint to send the request to. + * @param string $method The request method. + * @param string $body The body of the request. + * @param array $headers The request headers. + * @param int $timeOut The timeout in seconds for the request. + */ + public function openConnection($url, $method, $body, array $headers, $timeOut) + { + $options = [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $this->compileRequestHeaders($headers), + CURLOPT_URL => $url, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => $timeOut, + CURLOPT_RETURNTRANSFER => true, // Return response as string + CURLOPT_HEADER => true, // Enable header processing + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_CAINFO => __DIR__ . '/certs/DigiCertHighAssuranceEVRootCA.pem', + ]; + + if ($method !== "GET") { + $options[CURLOPT_POSTFIELDS] = $body; + } + + $this->facebookCurl->init(); + $this->facebookCurl->setoptArray($options); } - self::$facebookCurl->init(); - self::$facebookCurl->setopt_array($options); - } - - /** - * Add a bundled cert to the connection - */ - public function addBundledCert() - { - self::$facebookCurl->setopt(CURLOPT_CAINFO, - dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt'); - } - - /** - * Closes an existing curl connection - */ - public function closeConnection() - { - self::$facebookCurl->close(); - } - - /** - * Try to send the request - */ - public function tryToSendRequest() - { - $this->sendRequest(); - $this->curlErrorMessage = self::$facebookCurl->error(); - $this->curlErrorCode = self::$facebookCurl->errno(); - $this->responseHttpStatusCode = self::$facebookCurl->getinfo(CURLINFO_HTTP_CODE); - } - - /** - * Send the request and get the raw response from curl - */ - public function sendRequest() - { - $this->rawResponse = self::$facebookCurl->exec(); - } - - /** - * Compiles the request headers into a curl-friendly format - * - * @return array - */ - public function compileRequestHeaders() - { - $return = array(); - - foreach ($this->requestHeaders as $key => $value) { - $return[] = $key . ': ' . $value; + /** + * Closes an existing curl connection + */ + public function closeConnection() + { + $this->facebookCurl->close(); } - return $return; - } - - /** - * Extracts the headers and the body into a two-part array - * - * @return array - */ - public function extractResponseHeadersAndBody() - { - $headerSize = self::getHeaderSize(); - - $rawHeaders = mb_substr($this->rawResponse, 0, $headerSize); - $rawBody = mb_substr($this->rawResponse, $headerSize); - - return array(trim($rawHeaders), trim($rawBody)); - } - - /** - * Converts raw header responses into an array - * - * @param string $rawHeaders - * - * @return array - */ - public static function headersToArray($rawHeaders) - { - $headers = array(); - - // Normalize line breaks - $rawHeaders = str_replace("\r\n", "\n", $rawHeaders); - - // There will be multiple headers if a 301 was followed - // or a proxy was followed, etc - $headerCollection = explode("\n\n", trim($rawHeaders)); - // We just want the last response (at the end) - $rawHeader = array_pop($headerCollection); - - $headerComponents = explode("\n", $rawHeader); - foreach ($headerComponents as $line) { - if (strpos($line, ': ') === false) { - $headers['http_code'] = $line; - } else { - list ($key, $value) = explode(': ', $line); - $headers[$key] = $value; - } + /** + * Send the request and get the raw response from curl + */ + public function sendRequest() + { + $this->rawResponse = $this->facebookCurl->exec(); } - return $headers; - } - - /** - * Return proper header size - * - * @return integer - */ - private function getHeaderSize() - { - $headerSize = self::$facebookCurl->getinfo(CURLINFO_HEADER_SIZE); - // This corrects a Curl bug where header size does not account - // for additional Proxy headers. - if ( self::needsCurlProxyFix() ) { - // Additional way to calculate the request body size. - if (preg_match('/Content-Length: (\d+)/', $this->rawResponse, $m)) { - $headerSize = mb_strlen($this->rawResponse) - $m[1]; - } elseif (stripos($this->rawResponse, self::CONNECTION_ESTABLISHED) !== false) { - $headerSize += mb_strlen(self::CONNECTION_ESTABLISHED); - } + /** + * Compiles the request headers into a curl-friendly format. + * + * @param array $headers The request headers. + * + * @return array + */ + public function compileRequestHeaders(array $headers) + { + $return = []; + + foreach ($headers as $key => $value) { + $return[] = $key . ': ' . $value; + } + + return $return; } - return $headerSize; - } - - /** - * Detect versions of Curl which report incorrect header lengths when - * using Proxies. - * - * @return boolean - */ - private static function needsCurlProxyFix() - { - $ver = self::$facebookCurl->version(); - $version = $ver['version_number']; - - return $version < self::CURL_PROXY_QUIRK_VER; - } - + /** + * Extracts the headers and the body into a two-part array + * + * @return array + */ + public function extractResponseHeadersAndBody() + { + $parts = explode("\r\n\r\n", $this->rawResponse); + $rawBody = array_pop($parts); + $rawHeaders = implode("\r\n\r\n", $parts); + + return [trim($rawHeaders), trim($rawBody)]; + } } diff --git a/src/Facebook/HttpClients/FacebookGuzzleHttpClient.php b/src/Facebook/HttpClients/FacebookGuzzleHttpClient.php old mode 100755 new mode 100644 index de6977302..8feb7cb6d --- a/src/Facebook/HttpClients/FacebookGuzzleHttpClient.php +++ b/src/Facebook/HttpClients/FacebookGuzzleHttpClient.php @@ -1,6 +1,6 @@ requestHeaders[$key] = $value; - } - - /** - * The headers returned in the response - * - * @return array - */ - public function getResponseHeaders() - { - return $this->responseHeaders; - } - - /** - * The HTTP status response code - * - * @return int - */ - public function getResponseHttpStatusCode() - { - return $this->responseHttpStatusCode; - } - - /** - * Sends a request to the server - * - * @param string $url The endpoint to send the request to - * @param string $method The request method - * @param array $parameters The key value pairs to be sent in the body - * - * @return string Raw response from the server - * - * @throws \Facebook\FacebookSDKException - */ - public function send($url, $method = 'GET', $parameters = array()) - { - $options = array(); - if ($parameters) { - $options = array('body' => $parameters); +class FacebookGuzzleHttpClient implements FacebookHttpClientInterface +{ + /** + * @var \GuzzleHttp\Client The Guzzle client. + */ + protected $guzzleClient; + + /** + * @param \GuzzleHttp\Client|null The Guzzle client. + */ + public function __construct(Client $guzzleClient = null) + { + $this->guzzleClient = $guzzleClient ?: new Client(); } - $request = self::$guzzleClient->createRequest($method, $url, $options); - - foreach($this->requestHeaders as $k => $v) { - $request->setHeader($k, $v); + /** + * @inheritdoc + */ + public function send($url, $method, $body, array $headers, $timeOut) + { + $options = [ + 'headers' => $headers, + 'body' => $body, + 'timeout' => $timeOut, + 'connect_timeout' => 10, + 'verify' => __DIR__ . '/certs/DigiCertHighAssuranceEVRootCA.pem', + ]; + $request = $this->guzzleClient->createRequest($method, $url, $options); + + try { + $rawResponse = $this->guzzleClient->send($request); + } catch (RequestException $e) { + $rawResponse = $e->getResponse(); + + if ($e->getPrevious() instanceof RingException || !$rawResponse instanceof ResponseInterface) { + throw new FacebookSDKException($e->getMessage(), $e->getCode()); + } + } + + $rawHeaders = $this->getHeadersAsString($rawResponse); + $rawBody = $rawResponse->getBody(); + $httpStatusCode = $rawResponse->getStatusCode(); + + return new GraphRawResponse($rawHeaders, $rawBody, $httpStatusCode); } - try { - $rawResponse = self::$guzzleClient->send($request); - } catch (RequestException $e) { - if ($e->getPrevious() instanceof AdapterException) { - throw new FacebookSDKException($e->getMessage(), $e->getCode()); - } - $rawResponse = $e->getResponse(); + /** + * Returns the Guzzle array of headers as a string. + * + * @param ResponseInterface $response The Guzzle response. + * + * @return string + */ + public function getHeadersAsString(ResponseInterface $response) + { + $headers = $response->getHeaders(); + $rawHeaders = []; + foreach ($headers as $name => $values) { + $rawHeaders[] = $name . ": " . implode(", ", $values); + } + + return implode("\r\n", $rawHeaders); } - - $this->responseHttpStatusCode = $rawResponse->getStatusCode(); - $this->responseHeaders = $rawResponse->getHeaders(); - - return $rawResponse->getBody(); - } - } diff --git a/src/Facebook/HttpClients/FacebookHttpClientInterface.php b/src/Facebook/HttpClients/FacebookHttpClientInterface.php new file mode 100644 index 000000000..1fbf953d8 --- /dev/null +++ b/src/Facebook/HttpClients/FacebookHttpClientInterface.php @@ -0,0 +1,47 @@ +stream = stream_context_create($options); - } + /** + * Make a new context stream reference instance + * + * @param array $options + */ + public function streamContextCreate(array $options) + { + $this->stream = stream_context_create($options); + } - /** - * The response headers from the stream wrapper - * - * @return array|null - */ - public function getResponseHeaders() - { - return $this->responseHeaders; - } + /** + * The response headers from the stream wrapper + * + * @return array + */ + public function getResponseHeaders() + { + return $this->responseHeaders; + } - /** - * Send a stream wrapped request - * - * @param string $url - * - * @return mixed - */ - public function fileGetContents($url) - { - $rawResponse = file_get_contents($url, false, $this->stream); - $this->responseHeaders = $http_response_header; - return $rawResponse; - } + /** + * Send a stream wrapped request + * + * @param string $url + * + * @return mixed + */ + public function fileGetContents($url) + { + $rawResponse = file_get_contents($url, false, $this->stream); + $this->responseHeaders = $http_response_header ?: []; + return $rawResponse; + } } diff --git a/src/Facebook/HttpClients/FacebookStreamHttpClient.php b/src/Facebook/HttpClients/FacebookStreamHttpClient.php old mode 100755 new mode 100644 index 006d1030a..1cdfd5398 --- a/src/Facebook/HttpClients/FacebookStreamHttpClient.php +++ b/src/Facebook/HttpClients/FacebookStreamHttpClient.php @@ -1,6 +1,6 @@ requestHeaders[$key] = $value; - } - - /** - * The headers returned in the response - * - * @return array - */ - public function getResponseHeaders() - { - return $this->responseHeaders; - } - - /** - * The HTTP status response code - * - * @return int - */ - public function getResponseHttpStatusCode() - { - return $this->responseHttpStatusCode; - } - - /** - * Sends a request to the server - * - * @param string $url The endpoint to send the request to - * @param string $method The request method - * @param array $parameters The key value pairs to be sent in the body - * - * @return string Raw response from the server - * - * @throws \Facebook\FacebookSDKException - */ - public function send($url, $method = 'GET', $parameters = array()) - { - $options = array( - 'http' => array( - 'method' => $method, - 'timeout' => 60, - 'ignore_errors' => true - ), - 'ssl' => array( - 'verify_peer' => true, - 'cafile' => dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt', - ), - ); - - if ($parameters) { - $options['http']['content'] = http_build_query($parameters, null, '&'); - - $this->addRequestHeader('Content-type', 'application/x-www-form-urlencoded'); +use Facebook\Http\GraphRawResponse; +use Facebook\Exceptions\FacebookSDKException; + +class FacebookStreamHttpClient implements FacebookHttpClientInterface +{ + /** + * @var FacebookStream Procedural stream wrapper as object. + */ + protected $facebookStream; + + /** + * @param FacebookStream|null Procedural stream wrapper as object. + */ + public function __construct(FacebookStream $facebookStream = null) + { + $this->facebookStream = $facebookStream ?: new FacebookStream(); } - $options['http']['header'] = $this->compileHeader(); - - self::$facebookStream->streamContextCreate($options); - $rawResponse = self::$facebookStream->fileGetContents($url); - $rawHeaders = self::$facebookStream->getResponseHeaders(); - - if ($rawResponse === false || !$rawHeaders) { - throw new FacebookSDKException('Stream returned an empty response', 660); + /** + * @inheritdoc + */ + public function send($url, $method, $body, array $headers, $timeOut) + { + $options = [ + 'http' => [ + 'method' => $method, + 'header' => $this->compileHeader($headers), + 'content' => $body, + 'timeout' => $timeOut, + 'ignore_errors' => true + ], + 'ssl' => [ + 'verify_peer' => true, + 'verify_peer_name' => true, + 'allow_self_signed' => true, // All root certificates are self-signed + 'cafile' => __DIR__ . '/certs/DigiCertHighAssuranceEVRootCA.pem', + ], + ]; + + $this->facebookStream->streamContextCreate($options); + $rawBody = $this->facebookStream->fileGetContents($url); + $rawHeaders = $this->facebookStream->getResponseHeaders(); + + if ($rawBody === false || empty($rawHeaders)) { + throw new FacebookSDKException('Stream returned an empty response', 660); + } + + $rawHeaders = implode("\r\n", $rawHeaders); + + return new GraphRawResponse($rawHeaders, $rawBody); } - $this->responseHeaders = self::formatHeadersToArray($rawHeaders); - $this->responseHttpStatusCode = self::getStatusCodeFromHeader($this->responseHeaders['http_code']); - - return $rawResponse; - } - - /** - * Formats the headers for use in the stream wrapper - * - * @return string - */ - public function compileHeader() - { - $header = []; - foreach($this->requestHeaders as $k => $v) { - $header[] = $k . ': ' . $v; + /** + * Formats the headers for use in the stream wrapper. + * + * @param array $headers The request headers. + * + * @return string + */ + public function compileHeader(array $headers) + { + $header = []; + foreach ($headers as $k => $v) { + $header[] = $k . ': ' . $v; + } + + return implode("\r\n", $header); } - - return implode("\r\n", $header); - } - - /** - * Converts array of headers returned from the wrapper into - * something standard - * - * @param array $rawHeaders - * - * @return array - */ - public static function formatHeadersToArray(array $rawHeaders) - { - $headers = array(); - - foreach ($rawHeaders as $line) { - if (strpos($line, ':') === false) { - $headers['http_code'] = $line; - } else { - list ($key, $value) = explode(': ', $line); - $headers[$key] = $value; - } - } - - return $headers; - } - - /** - * Pulls out the HTTP status code from a response header - * - * @param string $header - * - * @return int - */ - public static function getStatusCodeFromHeader($header) - { - preg_match('|HTTP/\d\.\d\s+(\d+)\s+.*|', $header, $match); - return (int) $match[1]; - } - } diff --git a/src/Facebook/HttpClients/HttpClientsFactory.php b/src/Facebook/HttpClients/HttpClientsFactory.php new file mode 100644 index 000000000..d9f2a8d3d --- /dev/null +++ b/src/Facebook/HttpClients/HttpClientsFactory.php @@ -0,0 +1,99 @@ +sessionData[$key]) ? $this->sessionData[$key] : null; + } + + /** + * @inheritdoc + */ + public function set($key, $value) + { + $this->sessionData[$key] = $value; + } +} diff --git a/src/Facebook/PersistentData/FacebookSessionPersistentDataHandler.php b/src/Facebook/PersistentData/FacebookSessionPersistentDataHandler.php new file mode 100644 index 000000000..9123e3dc7 --- /dev/null +++ b/src/Facebook/PersistentData/FacebookSessionPersistentDataHandler.php @@ -0,0 +1,76 @@ +sessionPrefix . $key])) { + return $_SESSION[$this->sessionPrefix . $key]; + } + + return null; + } + + /** + * @inheritdoc + */ + public function set($key, $value) + { + $_SESSION[$this->sessionPrefix . $key] = $value; + } +} diff --git a/src/Facebook/PersistentData/PersistentDataFactory.php b/src/Facebook/PersistentData/PersistentDataFactory.php new file mode 100644 index 000000000..18fb8fd5b --- /dev/null +++ b/src/Facebook/PersistentData/PersistentDataFactory.php @@ -0,0 +1,65 @@ +validateLength($length); + + $binaryString = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + + if ($binaryString === false) { + throw new FacebookSDKException( + static::ERROR_MESSAGE . + 'mcrypt_create_iv() returned an error.' + ); + } + + return $this->binToHex($binaryString, $length); + } +} diff --git a/src/Facebook/PseudoRandomString/OpenSslPseudoRandomStringGenerator.php b/src/Facebook/PseudoRandomString/OpenSslPseudoRandomStringGenerator.php new file mode 100644 index 000000000..4b4276dc7 --- /dev/null +++ b/src/Facebook/PseudoRandomString/OpenSslPseudoRandomStringGenerator.php @@ -0,0 +1,67 @@ +validateLength($length); + + $wasCryptographicallyStrong = false; + $binaryString = openssl_random_pseudo_bytes($length, $wasCryptographicallyStrong); + + if ($binaryString === false) { + throw new FacebookSDKException(static::ERROR_MESSAGE . 'openssl_random_pseudo_bytes() returned an unknown error.'); + } + + if ($wasCryptographicallyStrong !== true) { + throw new FacebookSDKException(static::ERROR_MESSAGE . 'openssl_random_pseudo_bytes() returned a pseudo-random string but it was not cryptographically secure and cannot be used.'); + } + + return $this->binToHex($binaryString, $length); + } +} diff --git a/src/Facebook/PseudoRandomString/PseudoRandomStringGeneratorFactory.php b/src/Facebook/PseudoRandomString/PseudoRandomStringGeneratorFactory.php new file mode 100644 index 000000000..412f48135 --- /dev/null +++ b/src/Facebook/PseudoRandomString/PseudoRandomStringGeneratorFactory.php @@ -0,0 +1,101 @@ +validateLength($length); + + return $this->binToHex(random_bytes($length), $length); + } +} diff --git a/src/Facebook/PseudoRandomString/UrandomPseudoRandomStringGenerator.php b/src/Facebook/PseudoRandomString/UrandomPseudoRandomStringGenerator.php new file mode 100644 index 000000000..5ab434e6e --- /dev/null +++ b/src/Facebook/PseudoRandomString/UrandomPseudoRandomStringGenerator.php @@ -0,0 +1,89 @@ +validateLength($length); + + $stream = fopen('/dev/urandom', 'rb'); + if (!is_resource($stream)) { + throw new FacebookSDKException( + static::ERROR_MESSAGE . + 'Unable to open stream to /dev/urandom.' + ); + } + + if (!defined('HHVM_VERSION')) { + stream_set_read_buffer($stream, 0); + } + + $binaryString = fread($stream, $length); + fclose($stream); + + if (!$binaryString) { + throw new FacebookSDKException( + static::ERROR_MESSAGE . + 'Stream to /dev/urandom returned no data.' + ); + } + + return $this->binToHex($binaryString, $length); + } +} diff --git a/src/Facebook/SignedRequest.php b/src/Facebook/SignedRequest.php new file mode 100644 index 000000000..6a175a0a2 --- /dev/null +++ b/src/Facebook/SignedRequest.php @@ -0,0 +1,326 @@ +app = $facebookApp; + + if (!$rawSignedRequest) { + return; + } + + $this->rawSignedRequest = $rawSignedRequest; + + $this->parse(); + } + + /** + * Returns the raw signed request data. + * + * @return string|null + */ + public function getRawSignedRequest() + { + return $this->rawSignedRequest; + } + + /** + * Returns the parsed signed request data. + * + * @return array|null + */ + public function getPayload() + { + return $this->payload; + } + + /** + * Returns a property from the signed request data if available. + * + * @param string $key + * @param mixed|null $default + * + * @return mixed|null + */ + public function get($key, $default = null) + { + if (isset($this->payload[$key])) { + return $this->payload[$key]; + } + + return $default; + } + + /** + * Returns user_id from signed request data if available. + * + * @return string|null + */ + public function getUserId() + { + return $this->get('user_id'); + } + + /** + * Checks for OAuth data in the payload. + * + * @return boolean + */ + public function hasOAuthData() + { + return $this->get('oauth_token') || $this->get('code'); + } + + /** + * Creates a signed request from an array of data. + * + * @param array $payload + * + * @return string + */ + public function make(array $payload) + { + $payload['algorithm'] = isset($payload['algorithm']) ? $payload['algorithm'] : 'HMAC-SHA256'; + $payload['issued_at'] = isset($payload['issued_at']) ? $payload['issued_at'] : time(); + $encodedPayload = $this->base64UrlEncode(json_encode($payload)); + + $hashedSig = $this->hashSignature($encodedPayload); + $encodedSig = $this->base64UrlEncode($hashedSig); + + return $encodedSig . '.' . $encodedPayload; + } + + /** + * Validates and decodes a signed request and saves + * the payload to an array. + */ + protected function parse() + { + list($encodedSig, $encodedPayload) = $this->split(); + + // Signature validation + $sig = $this->decodeSignature($encodedSig); + $hashedSig = $this->hashSignature($encodedPayload); + $this->validateSignature($hashedSig, $sig); + + $this->payload = $this->decodePayload($encodedPayload); + + // Payload validation + $this->validateAlgorithm(); + } + + /** + * Splits a raw signed request into signature and payload. + * + * @return array + * + * @throws FacebookSDKException + */ + protected function split() + { + if (strpos($this->rawSignedRequest, '.') === false) { + throw new FacebookSDKException('Malformed signed request.', 606); + } + + return explode('.', $this->rawSignedRequest, 2); + } + + /** + * Decodes the raw signature from a signed request. + * + * @param string $encodedSig + * + * @return string + * + * @throws FacebookSDKException + */ + protected function decodeSignature($encodedSig) + { + $sig = $this->base64UrlDecode($encodedSig); + + if (!$sig) { + throw new FacebookSDKException('Signed request has malformed encoded signature data.', 607); + } + + return $sig; + } + + /** + * Decodes the raw payload from a signed request. + * + * @param string $encodedPayload + * + * @return array + * + * @throws FacebookSDKException + */ + protected function decodePayload($encodedPayload) + { + $payload = $this->base64UrlDecode($encodedPayload); + + if ($payload) { + $payload = json_decode($payload, true); + } + + if (!is_array($payload)) { + throw new FacebookSDKException('Signed request has malformed encoded payload data.', 607); + } + + return $payload; + } + + /** + * Validates the algorithm used in a signed request. + * + * @throws FacebookSDKException + */ + protected function validateAlgorithm() + { + if ($this->get('algorithm') !== 'HMAC-SHA256') { + throw new FacebookSDKException('Signed request is using the wrong algorithm.', 605); + } + } + + /** + * Hashes the signature used in a signed request. + * + * @param string $encodedData + * + * @return string + * + * @throws FacebookSDKException + */ + protected function hashSignature($encodedData) + { + $hashedSig = hash_hmac( + 'sha256', + $encodedData, + $this->app->getSecret(), + $raw_output = true + ); + + if (!$hashedSig) { + throw new FacebookSDKException('Unable to hash signature from encoded payload data.', 602); + } + + return $hashedSig; + } + + /** + * Validates the signature used in a signed request. + * + * @param string $hashedSig + * @param string $sig + * + * @throws FacebookSDKException + */ + protected function validateSignature($hashedSig, $sig) + { + if (\hash_equals($hashedSig, $sig)) { + return; + } + + throw new FacebookSDKException('Signed request has an invalid signature.', 602); + } + + /** + * Base64 decoding which replaces characters: + * + instead of - + * / instead of _ + * + * @link http://en.wikipedia.org/wiki/Base64#URL_applications + * + * @param string $input base64 url encoded input + * + * @return string decoded string + */ + public function base64UrlDecode($input) + { + $urlDecodedBase64 = strtr($input, '-_', '+/'); + $this->validateBase64($urlDecodedBase64); + + return base64_decode($urlDecodedBase64); + } + + /** + * Base64 encoding which replaces characters: + * + instead of - + * / instead of _ + * + * @link http://en.wikipedia.org/wiki/Base64#URL_applications + * + * @param string $input string to encode + * + * @return string base64 url encoded input + */ + public function base64UrlEncode($input) + { + return strtr(base64_encode($input), '+/', '-_'); + } + + /** + * Validates a base64 string. + * + * @param string $input base64 value to validate + * + * @throws FacebookSDKException + */ + protected function validateBase64($input) + { + if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $input)) { + throw new FacebookSDKException('Signed request contains malformed base64 encoding.', 608); + } + } +} diff --git a/src/Facebook/Url/FacebookUrlDetectionHandler.php b/src/Facebook/Url/FacebookUrlDetectionHandler.php new file mode 100644 index 000000000..1d134ddcb --- /dev/null +++ b/src/Facebook/Url/FacebookUrlDetectionHandler.php @@ -0,0 +1,182 @@ +getHttpScheme() . '://' . $this->getHostName() . $this->getServerVar('REQUEST_URI'); + } + + /** + * Get the currently active URL scheme. + * + * @return string + */ + protected function getHttpScheme() + { + return $this->isBehindSsl() ? 'https' : 'http'; + } + + /** + * Tries to detect if the server is running behind an SSL. + * + * @return boolean + */ + protected function isBehindSsl() + { + // Check for proxy first + $protocol = $this->getHeader('X_FORWARDED_PROTO'); + if ($protocol) { + return $this->protocolWithActiveSsl($protocol); + } + + $protocol = $this->getServerVar('HTTPS'); + if ($protocol) { + return $this->protocolWithActiveSsl($protocol); + } + + return (string)$this->getServerVar('SERVER_PORT') === '443'; + } + + /** + * Detects an active SSL protocol value. + * + * @param string $protocol + * + * @return boolean + */ + protected function protocolWithActiveSsl($protocol) + { + $protocol = strtolower((string)$protocol); + + return in_array($protocol, ['on', '1', 'https', 'ssl'], true); + } + + /** + * Tries to detect the host name of the server. + * + * Some elements adapted from + * + * @see https://github.com/symfony/HttpFoundation/blob/master/Request.php + * + * @return string + */ + protected function getHostName() + { + // Check for proxy first + $header = $this->getHeader('X_FORWARDED_HOST'); + if ($header && $this->isValidForwardedHost($header)) { + $elements = explode(',', $header); + $host = $elements[count($elements) - 1]; + } elseif (!$host = $this->getHeader('HOST')) { + if (!$host = $this->getServerVar('SERVER_NAME')) { + $host = $this->getServerVar('SERVER_ADDR'); + } + } + + // trim and remove port number from host + // host is lowercase as per RFC 952/2181 + $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); + + // Port number + $scheme = $this->getHttpScheme(); + $port = $this->getCurrentPort(); + $appendPort = ':' . $port; + + // Don't append port number if a normal port. + if (($scheme == 'http' && $port == '80') || ($scheme == 'https' && $port == '443')) { + $appendPort = ''; + } + + return $host . $appendPort; + } + + protected function getCurrentPort() + { + // Check for proxy first + $port = $this->getHeader('X_FORWARDED_PORT'); + if ($port) { + return (string)$port; + } + + $protocol = (string)$this->getHeader('X_FORWARDED_PROTO'); + if ($protocol === 'https') { + return '443'; + } + + return (string)$this->getServerVar('SERVER_PORT'); + } + + /** + * Returns the a value from the $_SERVER super global. + * + * @param string $key + * + * @return string + */ + protected function getServerVar($key) + { + return isset($_SERVER[$key]) ? $_SERVER[$key] : ''; + } + + /** + * Gets a value from the HTTP request headers. + * + * @param string $key + * + * @return string + */ + protected function getHeader($key) + { + return $this->getServerVar('HTTP_' . $key); + } + + /** + * Checks if the value in X_FORWARDED_HOST is a valid hostname + * Could prevent unintended redirections + * + * @param string $header + * + * @return boolean + */ + protected function isValidForwardedHost($header) + { + $elements = explode(',', $header); + $host = $elements[count($elements) - 1]; + + return preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $host) //valid chars check + && 0 < strlen($host) && strlen($host) < 254 //overall length check + && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $host); //length of each label + } +} diff --git a/src/Facebook/Url/FacebookUrlManipulator.php b/src/Facebook/Url/FacebookUrlManipulator.php new file mode 100644 index 000000000..daeab9c52 --- /dev/null +++ b/src/Facebook/Url/FacebookUrlManipulator.php @@ -0,0 +1,167 @@ + 0) { + $query = '?' . http_build_query($params, null, '&'); + } + } + + $scheme = isset($parts['scheme']) ? $parts['scheme'] . '://' : ''; + $host = isset($parts['host']) ? $parts['host'] : ''; + $port = isset($parts['port']) ? ':' . $parts['port'] : ''; + $path = isset($parts['path']) ? $parts['path'] : ''; + $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : ''; + + return $scheme . $host . $port . $path . $query . $fragment; + } + + /** + * Gracefully appends params to the URL. + * + * @param string $url The URL that will receive the params. + * @param array $newParams The params to append to the URL. + * + * @return string + */ + public static function appendParamsToUrl($url, array $newParams = []) + { + if (empty($newParams)) { + return $url; + } + + if (strpos($url, '?') === false) { + return $url . '?' . http_build_query($newParams, null, '&'); + } + + list($path, $query) = explode('?', $url, 2); + $existingParams = []; + parse_str($query, $existingParams); + + // Favor params from the original URL over $newParams + $newParams = array_merge($newParams, $existingParams); + + // Sort for a predicable order + ksort($newParams); + + return $path . '?' . http_build_query($newParams, null, '&'); + } + + /** + * Returns the params from a URL in the form of an array. + * + * @param string $url The URL to parse the params from. + * + * @return array + */ + public static function getParamsAsArray($url) + { + $query = parse_url($url, PHP_URL_QUERY); + if (!$query) { + return []; + } + $params = []; + parse_str($query, $params); + + return $params; + } + + /** + * Adds the params of the first URL to the second URL. + * + * Any params that already exist in the second URL will go untouched. + * + * @param string $urlToStealFrom The URL harvest the params from. + * @param string $urlToAddTo The URL that will receive the new params. + * + * @return string The $urlToAddTo with any new params from $urlToStealFrom. + */ + public static function mergeUrlParams($urlToStealFrom, $urlToAddTo) + { + $newParams = static::getParamsAsArray($urlToStealFrom); + // Nothing new to add, return as-is + if (!$newParams) { + return $urlToAddTo; + } + + return static::appendParamsToUrl($urlToAddTo, $newParams); + } + + /** + * Check for a "/" prefix and prepend it if not exists. + * + * @param string|null $string + * + * @return string|null + */ + public static function forceSlashPrefix($string) + { + if (!$string) { + return $string; + } + + return strpos($string, '/') === 0 ? $string : '/' . $string; + } + + /** + * Trims off the hostname and Graph version from a URL. + * + * @param string $urlToTrim The URL the needs the surgery. + * + * @return string The $urlToTrim with the hostname and Graph version removed. + */ + public static function baseGraphUrlEndpoint($urlToTrim) + { + return '/' . preg_replace('/^https:\/\/.+\.facebook\.com(\/v.+?)?\//', '', $urlToTrim); + } +} diff --git a/src/Facebook/Url/UrlDetectionInterface.php b/src/Facebook/Url/UrlDetectionInterface.php new file mode 100644 index 000000000..dca38a0c3 --- /dev/null +++ b/src/Facebook/Url/UrlDetectionInterface.php @@ -0,0 +1,39 @@ + [ + 'app_id' => '123', + 'application' => 'Foo App', + 'error' => [ + 'code' => 190, + 'message' => 'Foo error message.', + 'subcode' => 463, + ], + 'issued_at' => 1422110200, + 'expires_at' => 1422115200, + 'is_valid' => false, + 'metadata' => [ + 'sso' => 'iphone-sso', + 'auth_type' => 'rerequest', + 'auth_nonce' => 'no-replicatey', + ], + 'scopes' => ['public_profile', 'basic_info', 'user_friends'], + 'profile_id' => '1000', + 'user_id' => '1337', + ], + ]; + + public function testDatesGetCastToDateTime() + { + $metadata = new AccessTokenMetadata($this->graphResponseData); + + $expires = $metadata->getExpiresAt(); + $issuedAt = $metadata->getIssuedAt(); + + $this->assertInstanceOf('DateTime', $expires); + $this->assertInstanceOf('DateTime', $issuedAt); + } + + public function testAllTheGettersReturnTheProperValue() + { + $metadata = new AccessTokenMetadata($this->graphResponseData); + + $this->assertEquals('123', $metadata->getAppId()); + $this->assertEquals('Foo App', $metadata->getApplication()); + $this->assertTrue($metadata->isError(), 'Expected an error'); + $this->assertEquals('190', $metadata->getErrorCode()); + $this->assertEquals('Foo error message.', $metadata->getErrorMessage()); + $this->assertEquals('463', $metadata->getErrorSubcode()); + $this->assertFalse($metadata->getIsValid(), 'Expected the access token to not be valid'); + $this->assertEquals('iphone-sso', $metadata->getSso()); + $this->assertEquals('rerequest', $metadata->getAuthType()); + $this->assertEquals('no-replicatey', $metadata->getAuthNonce()); + $this->assertEquals('1000', $metadata->getProfileId()); + $this->assertEquals(['public_profile', 'basic_info', 'user_friends'], $metadata->getScopes()); + $this->assertEquals('1337', $metadata->getUserId()); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testInvalidMetadataWillThrow() + { + new AccessTokenMetadata(['foo' => 'bar']); + } + + public function testAnExpectedAppIdWillNotThrow() + { + $metadata = new AccessTokenMetadata($this->graphResponseData); + $metadata->validateAppId('123'); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAnUnexpectedAppIdWillThrow() + { + $metadata = new AccessTokenMetadata($this->graphResponseData); + $metadata->validateAppId('foo'); + } + + public function testAnExpectedUserIdWillNotThrow() + { + $metadata = new AccessTokenMetadata($this->graphResponseData); + $metadata->validateUserId('1337'); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAnUnexpectedUserIdWillThrow() + { + $metadata = new AccessTokenMetadata($this->graphResponseData); + $metadata->validateUserId('foo'); + } + + public function testAnActiveAccessTokenWillNotThrow() + { + $this->graphResponseData['data']['expires_at'] = time() + 1000; + $metadata = new AccessTokenMetadata($this->graphResponseData); + $metadata->validateExpiration(); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAnExpiredAccessTokenWillThrow() + { + $this->graphResponseData['data']['expires_at'] = time() - 1000; + $metadata = new AccessTokenMetadata($this->graphResponseData); + $metadata->validateExpiration(); + } +} diff --git a/tests/Authentication/AccessTokenTest.php b/tests/Authentication/AccessTokenTest.php new file mode 100644 index 000000000..39e42d48b --- /dev/null +++ b/tests/Authentication/AccessTokenTest.php @@ -0,0 +1,111 @@ +assertEquals('foo_token', $accessToken->getValue()); + $this->assertEquals('foo_token', (string)$accessToken); + } + + public function testAnAppSecretProofWillBeProperlyGenerated() + { + $accessToken = new AccessToken('foo_token'); + + $appSecretProof = $accessToken->getAppSecretProof('shhhhh!is.my.secret'); + + $this->assertEquals('796ba0d8a6b339e476a7b166a9e8ac0a395f7de736dc37de5f2f4397f5854eb8', $appSecretProof); + } + + public function testAnAppAccessTokenCanBeDetected() + { + $normalToken = new AccessToken('foo_token'); + $isNormalToken = $normalToken->isAppAccessToken(); + + $this->assertFalse($isNormalToken, 'Normal access token not expected to look like an app access token.'); + + $appToken = new AccessToken('123|secret'); + $isAppToken = $appToken->isAppAccessToken(); + + $this->assertTrue($isAppToken, 'App access token expected to look like an app access token.'); + } + + public function testShortLivedAccessTokensCanBeDetected() + { + $anHourAndAHalf = time() + (1.5 * 60); + $accessToken = new AccessToken('foo_token', $anHourAndAHalf); + + $isLongLived = $accessToken->isLongLived(); + + $this->assertFalse($isLongLived, 'Expected access token to be short lived.'); + } + + public function testLongLivedAccessTokensCanBeDetected() + { + $accessToken = new AccessToken('foo_token', $this->aWeekFromNow()); + + $isLongLived = $accessToken->isLongLived(); + + $this->assertTrue($isLongLived, 'Expected access token to be long lived.'); + } + + public function testAnAppAccessTokenDoesNotExpire() + { + $appToken = new AccessToken('123|secret'); + $hasExpired = $appToken->isExpired(); + + $this->assertFalse($hasExpired, 'App access token not expected to expire.'); + } + + public function testAnAccessTokenCanExpire() + { + $expireTime = time() - 100; + $appToken = new AccessToken('foo_token', $expireTime); + $hasExpired = $appToken->isExpired(); + + $this->assertTrue($hasExpired, 'Expected 100 second old access token to be expired.'); + } + + public function testAccessTokenCanBeSerialized() + { + $accessToken = new AccessToken('foo', time(), 'bar'); + + $newAccessToken = unserialize(serialize($accessToken)); + + $this->assertEquals((string)$accessToken, (string)$newAccessToken); + $this->assertEquals($accessToken->getExpiresAt(), $newAccessToken->getExpiresAt()); + } + + private function aWeekFromNow() + { + return time() + (60 * 60 * 24 * 7);//a week from now + } +} diff --git a/tests/Authentication/FooFacebookClientForOAuth2Test.php b/tests/Authentication/FooFacebookClientForOAuth2Test.php new file mode 100644 index 000000000..8c59ae130 --- /dev/null +++ b/tests/Authentication/FooFacebookClientForOAuth2Test.php @@ -0,0 +1,58 @@ +response = '{"data":{"user_id":"444"}}'; + } + + public function setAccessTokenResponse() + { + $this->response = '{"access_token":"my_access_token","expires":"1422115200"}'; + } + + public function setCodeResponse() + { + $this->response = '{"code":"my_neat_code"}'; + } + + public function sendRequest(FacebookRequest $request) + { + return new FacebookResponse( + $request, + $this->response, + 200, + [] + ); + } +} diff --git a/tests/Authentication/OAuth2ClientTest.php b/tests/Authentication/OAuth2ClientTest.php new file mode 100644 index 000000000..5b144d7b7 --- /dev/null +++ b/tests/Authentication/OAuth2ClientTest.php @@ -0,0 +1,166 @@ +client = new FooFacebookClientForOAuth2Test(); + $this->oauth = new OAuth2Client($app, $this->client, static::TESTING_GRAPH_VERSION); + } + + public function testCanGetMetadataFromAnAccessToken() + { + $this->client->setMetadataResponse(); + + $metadata = $this->oauth->debugToken('baz_token'); + + $this->assertInstanceOf('Facebook\Authentication\AccessTokenMetadata', $metadata); + $this->assertEquals('444', $metadata->getUserId()); + + $expectedParams = [ + 'input_token' => 'baz_token', + 'access_token' => '123|foo_secret', + 'appsecret_proof' => 'de753c58fd58b03afca2340bbaeb4ecf987b5de4c09e39a63c944dd25efbc234', + ]; + + $request = $this->oauth->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/debug_token', $request->getEndpoint()); + $this->assertEquals($expectedParams, $request->getParams()); + $this->assertEquals(static::TESTING_GRAPH_VERSION, $request->getGraphVersion()); + } + + public function testCanBuildAuthorizationUrl() + { + $scope = ['email', 'base_foo']; + $authUrl = $this->oauth->getAuthorizationUrl('https://foo.bar', 'foo_state', $scope, ['foo' => 'bar'], '*'); + + $this->assertContains('*', $authUrl); + + $expectedUrl = 'https://www.facebook.com/' . static::TESTING_GRAPH_VERSION . '/dialog/oauth?'; + $this->assertTrue(strpos($authUrl, $expectedUrl) === 0, 'Unexpected base authorization URL returned from getAuthorizationUrl().'); + + $params = [ + 'client_id' => '123', + 'redirect_uri' => 'https://foo.bar', + 'state' => 'foo_state', + 'sdk' => 'php-sdk-' . Facebook::VERSION, + 'scope' => implode(',', $scope), + 'foo' => 'bar', + ]; + foreach ($params as $key => $value) { + $this->assertContains($key . '=' . urlencode($value), $authUrl); + } + } + + public function testCanGetAccessTokenFromCode() + { + $this->client->setAccessTokenResponse(); + + $accessToken = $this->oauth->getAccessTokenFromCode('bar_code', 'foo_uri'); + + $this->assertInstanceOf('Facebook\Authentication\AccessToken', $accessToken); + $this->assertEquals('my_access_token', $accessToken->getValue()); + + $expectedParams = [ + 'code' => 'bar_code', + 'redirect_uri' => 'foo_uri', + 'client_id' => '123', + 'client_secret' => 'foo_secret', + 'access_token' => '123|foo_secret', + 'appsecret_proof' => 'de753c58fd58b03afca2340bbaeb4ecf987b5de4c09e39a63c944dd25efbc234', + ]; + + $request = $this->oauth->getLastRequest(); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/oauth/access_token', $request->getEndpoint()); + $this->assertEquals($expectedParams, $request->getParams()); + $this->assertEquals(static::TESTING_GRAPH_VERSION, $request->getGraphVersion()); + } + + public function testCanGetLongLivedAccessToken() + { + $this->client->setAccessTokenResponse(); + + $accessToken = $this->oauth->getLongLivedAccessToken('short_token'); + + $this->assertEquals('my_access_token', $accessToken->getValue()); + + $expectedParams = [ + 'grant_type' => 'fb_exchange_token', + 'fb_exchange_token' => 'short_token', + 'client_id' => '123', + 'client_secret' => 'foo_secret', + 'access_token' => '123|foo_secret', + 'appsecret_proof' => 'de753c58fd58b03afca2340bbaeb4ecf987b5de4c09e39a63c944dd25efbc234', + ]; + + $request = $this->oauth->getLastRequest(); + $this->assertEquals($expectedParams, $request->getParams()); + } + + public function testCanGetCodeFromLongLivedAccessToken() + { + $this->client->setCodeResponse(); + + $code = $this->oauth->getCodeFromLongLivedAccessToken('long_token', 'foo_uri'); + + $this->assertEquals('my_neat_code', $code); + + $expectedParams = [ + 'access_token' => 'long_token', + 'redirect_uri' => 'foo_uri', + 'client_id' => '123', + 'client_secret' => 'foo_secret', + 'appsecret_proof' => '7e91300ea91be4166282611d4fc700b473466f3ea2981dafbf492fc096995bf1', + ]; + + $request = $this->oauth->getLastRequest(); + $this->assertEquals($expectedParams, $request->getParams()); + $this->assertEquals('/oauth/client_code', $request->getEndpoint()); + } +} diff --git a/tests/Entities/AccessTokenTest.php b/tests/Entities/AccessTokenTest.php deleted file mode 100644 index 405c51ae0..000000000 --- a/tests/Entities/AccessTokenTest.php +++ /dev/null @@ -1,247 +0,0 @@ -assertEquals('foo_token', (string) $accessToken); - } - - public function testShortLivedAccessTokensCanBeDetected() - { - $anHourAndAHalf = time() + (1.5 * 60); - $accessToken = new AccessToken('foo_token', $anHourAndAHalf); - - $isLongLived = $accessToken->isLongLived(); - - $this->assertFalse($isLongLived, 'Expected access token to be short lived.'); - } - - public function testLongLivedAccessTokensCanBeDetected() - { - $aWeek = time() + (60 * 60 * 24 * 7); - $accessToken = new AccessToken('foo_token', $aWeek); - - $isLongLived = $accessToken->isLongLived(); - - $this->assertTrue($isLongLived, 'Expected access token to be long lived.'); - } - - public function testATokenIsValidatedOnTheAppIdAndMachineIdAndTokenValidityAndTokenExpiration() - { - $aWeek = time() + (60 * 60 * 24 * 7); - $dt = new \DateTime(); - $dt->setTimestamp($aWeek); - - $graphSessionInfoMock = m::mock('Facebook\GraphSessionInfo'); - $graphSessionInfoMock - ->shouldReceive('getAppId') - ->once() - ->andReturn('123'); - $graphSessionInfoMock - ->shouldReceive('getProperty') - ->with('machine_id') - ->once() - ->andReturn('foo_machine'); - $graphSessionInfoMock - ->shouldReceive('isValid') - ->once() - ->andReturn(true); - $graphSessionInfoMock - ->shouldReceive('getExpiresAt') - ->twice() - ->andReturn($dt); - - $isValid = AccessToken::validateAccessToken($graphSessionInfoMock, '123', 'foo_machine'); - - $this->assertTrue($isValid, 'Expected access token to be valid.'); - } - - public function testATokenWillNotBeValidIfTheAppIdDoesNotMatch() - { - $aWeek = time() + (60 * 60 * 24 * 7); - $dt = new \DateTime(); - $dt->setTimestamp($aWeek); - - $graphSessionInfoMock = m::mock('Facebook\GraphSessionInfo'); - $graphSessionInfoMock - ->shouldReceive('getAppId') - ->once() - ->andReturn('123'); - $graphSessionInfoMock - ->shouldReceive('getProperty') - ->with('machine_id') - ->once() - ->andReturn('foo_machine'); - $graphSessionInfoMock - ->shouldReceive('isValid') - ->once() - ->andReturn(true); - $graphSessionInfoMock - ->shouldReceive('getExpiresAt') - ->twice() - ->andReturn($dt); - - $isValid = AccessToken::validateAccessToken($graphSessionInfoMock, '42', 'foo_machine'); - - $this->assertFalse($isValid, 'Expected access token to be invalid because the app ID does not match.'); - } - - public function testATokenWillNotBeValidIfTheMachineIdDoesNotMatch() - { - $aWeek = time() + (60 * 60 * 24 * 7); - $dt = new \DateTime(); - $dt->setTimestamp($aWeek); - - $graphSessionInfoMock = m::mock('Facebook\GraphSessionInfo'); - $graphSessionInfoMock - ->shouldReceive('getAppId') - ->once() - ->andReturn('123'); - $graphSessionInfoMock - ->shouldReceive('getProperty') - ->with('machine_id') - ->once() - ->andReturn('foo_machine'); - $graphSessionInfoMock - ->shouldReceive('isValid') - ->once() - ->andReturn(true); - $graphSessionInfoMock - ->shouldReceive('getExpiresAt') - ->twice() - ->andReturn($dt); - - $isValid = AccessToken::validateAccessToken($graphSessionInfoMock, '123', 'bar_machine'); - - $this->assertFalse($isValid, 'Expected access token to be invalid because the machine ID does not match.'); - } - - public function testATokenWillNotBeValidIfTheCollectionTellsUsItsNotValid() - { - $aWeek = time() + (60 * 60 * 24 * 7); - $dt = new \DateTime(); - $dt->setTimestamp($aWeek); - - $graphSessionInfoMock = m::mock('Facebook\GraphSessionInfo'); - $graphSessionInfoMock - ->shouldReceive('getAppId') - ->once() - ->andReturn('123'); - $graphSessionInfoMock - ->shouldReceive('getProperty') - ->with('machine_id') - ->once() - ->andReturn('foo_machine'); - $graphSessionInfoMock - ->shouldReceive('isValid') - ->once() - ->andReturn(false); - $graphSessionInfoMock - ->shouldReceive('getExpiresAt') - ->twice() - ->andReturn($dt); - - $isValid = AccessToken::validateAccessToken($graphSessionInfoMock, '123', 'foo_machine'); - - $this->assertFalse($isValid, 'Expected access token to be invalid because the collection says it is not valid.'); - } - - public function testATokenWillNotBeValidIfTheTokenHasExpired() - { - $lastWeek = time() - (60 * 60 * 24 * 7); - $dt = new \DateTime(); - $dt->setTimestamp($lastWeek); - - $graphSessionInfoMock = m::mock('Facebook\GraphSessionInfo'); - $graphSessionInfoMock - ->shouldReceive('getAppId') - ->once() - ->andReturn('123'); - $graphSessionInfoMock - ->shouldReceive('getProperty') - ->with('machine_id') - ->once() - ->andReturn('foo_machine'); - $graphSessionInfoMock - ->shouldReceive('isValid') - ->once() - ->andReturn(true); - $graphSessionInfoMock - ->shouldReceive('getExpiresAt') - ->twice() - ->andReturn($dt); - - $isValid = AccessToken::validateAccessToken($graphSessionInfoMock, '123', 'foo_machine'); - - $this->assertFalse($isValid, 'Expected access token to be invalid because it has expired.'); - } - - public function testInfoAboutAnAccessTokenCanBeObtainedFromGraph() - { - $testUserAccessToken = FacebookTestHelper::$testUserAccessToken; - - $accessToken = new AccessToken($testUserAccessToken); - $accessTokenInfo = $accessToken->getInfo(); - - $testAppId = FacebookTestCredentials::$appId; - $this->assertEquals($testAppId, $accessTokenInfo->getAppId()); - - $testUserId = FacebookTestHelper::$testUserId; - $this->assertEquals($testUserId, $accessTokenInfo->getId()); - - $expectedScopes = FacebookTestHelper::$testUserPermissions; - $actualScopes = $accessTokenInfo->getPropertyAsArray('scopes'); - foreach ($expectedScopes as $scope) { - $this->assertTrue(in_array($scope, $actualScopes), - 'Expected the following permission to be present: '.$scope); - } - } - - public function testAShortLivedAccessTokenCabBeExtended() - { - $testUserAccessToken = FacebookTestHelper::$testUserAccessToken; - - $accessToken = new AccessToken($testUserAccessToken); - $longLivedAccessToken = $accessToken->extend(); - - $this->assertInstanceOf('Facebook\Entities\AccessToken', $longLivedAccessToken); - } - - public function testALongLivedAccessTokenCanBeUsedToObtainACode() - { - $testUserAccessToken = FacebookTestHelper::$testUserAccessToken; - - $accessToken = new AccessToken($testUserAccessToken); - $longLivedAccessToken = $accessToken->extend(); - - $code = AccessToken::getCodeFromAccessToken((string) $longLivedAccessToken); - - $this->assertTrue(is_string($code)); - } - - public function testACodeCanBeUsedToObtainAnAccessToken() - { - $testUserAccessToken = FacebookTestHelper::$testUserAccessToken; - - $accessToken = new AccessToken($testUserAccessToken); - $longLivedAccessToken = $accessToken->extend(); - - $code = AccessToken::getCodeFromAccessToken($longLivedAccessToken); - $accessTokenFromCode = AccessToken::getAccessTokenFromCode($code); - - $this->assertInstanceOf('Facebook\Entities\AccessToken', $accessTokenFromCode); - } - -} diff --git a/tests/Entities/SignedRequestTest.php b/tests/Entities/SignedRequestTest.php deleted file mode 100644 index 3b42d7f8e..000000000 --- a/tests/Entities/SignedRequestTest.php +++ /dev/null @@ -1,163 +0,0 @@ - 'foo_token', - 'algorithm' => 'HMAC-SHA256', - 'issued_at' => 321, - 'code' => 'foo_code', - 'state' => 'foo_state', - 'user_id' => 123, - 'foo' => 'bar', - ); - - public function testValidSignedRequestsWillPassFormattingValidation() - { - $sr = SignedRequest::make($this->payloadData, $this->appSecret); - SignedRequest::validateFormat($sr); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testInvalidSignedRequestsWillFailFormattingValidation() - { - SignedRequest::validateFormat('invalid_signed_request'); - } - - public function testSignatureAndPayloadCanBeSeparatedInSignedRequests() - { - list($sig, $payload) = SignedRequest::split('sig.payload'); - - $this->assertEquals('sig', $sig); - $this->assertEquals('payload', $payload); - } - - public function testBase64EncodingIsUrlSafe() - { - $encodedData = SignedRequest::base64UrlEncode('aijkoprstADIJKLOPQTUVX1256!)]-:;"<>?.|~'); - - $this->assertEquals('YWlqa29wcnN0QURJSktMT1BRVFVWWDEyNTYhKV0tOjsiPD4_Lnx-', $encodedData); - } - - public function testAUrlSafeBase64EncodedStringCanBeDecoded() - { - $decodedData = SignedRequest::base64UrlDecode('YWlqa29wcnN0QURJSktMT1BRVFVWWDEyNTYhKV0tOjsiPD4/Lnx+'); - - $this->assertEquals('aijkoprstADIJKLOPQTUVX1256!)]-:;"<>?.|~', $decodedData); - } - - public function testAValidEncodedSignatureCanBeDecoded() - { - $decodedSig = SignedRequest::decodeSignature('c2ln'); - - $this->assertEquals('sig', $decodedSig); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testAnImproperlyEncodedSignatureWillThrowAnException() - { - SignedRequest::decodeSignature('foo!'); - } - - public function testAValidEncodedPayloadCanBeDecoded() - { - $decodedPayload = SignedRequest::decodePayload('WyJwYXlsb2FkIl0='); - - $this->assertEquals(array('payload'), $decodedPayload); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testAnImproperlyEncodedPayloadWillThrowAnException() - { - SignedRequest::decodePayload('foo!'); - } - - public function testSignedRequestDataMustContainTheHmacSha256Algorithm() - { - SignedRequest::validateAlgorithm($this->payloadData); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testNonApprovedAlgorithmsWillThrowAnException() - { - $signedRequestData = $this->payloadData; - $signedRequestData['algorithm'] = 'FOO-ALGORITHM'; - SignedRequest::validateAlgorithm($signedRequestData); - } - - public function testASignatureHashCanBeGeneratedFromBase64EncodedData() - { - $hashedSig = SignedRequest::hashSignature('WyJwYXlsb2FkIl0=', $this->appSecret); - - $expectedSig = base64_decode('bFofyO2sERX73y8uvuX26SLodv0mZ+Zk18d8b3zhD+s='); - $this->assertEquals($expectedSig, $hashedSig); - } - - public function testTwoBinaryStringsCanBeComparedForSignatureValidation() - { - $hashedSig = base64_decode('bFofyO2sERX73y8uvuX26SLodv0mZ+Zk18d8b3zhD+s='); - SignedRequest::validateSignature($hashedSig, $hashedSig); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testNonSameBinaryStringsWillThrowAnExceptionForSignatureValidation() - { - $hashedSig1 = base64_decode('bFofyO2sERX73y8uvuX26SLodv0mZ+Zk18d8b3zhD+s='); - $hashedSig2 = base64_decode('GJy4HzkRtCeZA0cJjdZJtGfovcdxgl/AERI20S4MY7c='); - SignedRequest::validateSignature($hashedSig1, $hashedSig2); - } - - public function testASignedRequestWillPassCsrfValidation() - { - SignedRequest::validateCsrf($this->payloadData, 'foo_state'); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testASignedRequestWithIncorrectCsrfDataWillThrowAnException() - { - SignedRequest::validateCsrf($this->payloadData, 'invalid_foo_state'); - } - - public function testARawSignedRequestCanBeValidatedAndDecoded() - { - $payload = SignedRequest::parse($this->rawSignedRequest, 'foo_state', $this->appSecret); - - $this->assertEquals($this->payloadData, $payload); - } - - public function testARawSignedRequestCanBeInjectedIntoTheConstructorToInstantiateANewEntity() - { - $signedRequest = new SignedRequest($this->rawSignedRequest, 'foo_state', $this->appSecret); - - $rawSignedRequest = $signedRequest->getRawSignedRequest(); - $payloadData = $signedRequest->getPayload(); - $userId = $signedRequest->getUserId(); - $hasOAuthData = $signedRequest->hasOAuthData(); - - $this->assertInstanceOf('\Facebook\Entities\SignedRequest', $signedRequest); - $this->assertEquals($this->rawSignedRequest, $rawSignedRequest); - $this->assertEquals($this->payloadData, $payloadData); - $this->assertEquals(123, $userId); - $this->assertTrue($hasOAuthData); - } - -} diff --git a/tests/Exceptions/FacebookResponseExceptionTest.php b/tests/Exceptions/FacebookResponseExceptionTest.php new file mode 100644 index 000000000..ae18fdec8 --- /dev/null +++ b/tests/Exceptions/FacebookResponseExceptionTest.php @@ -0,0 +1,278 @@ +request = new FacebookRequest(new FacebookApp('123', 'foo')); + } + + public function testAuthenticationExceptions() + { + $params = [ + 'error' => [ + 'code' => 100, + 'message' => 'errmsg', + 'error_subcode' => 0, + 'type' => 'exception' + ], + ]; + + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(100, $exception->getCode()); + $this->assertEquals(0, $exception->getSubErrorCode()); + $this->assertEquals('exception', $exception->getErrorType()); + $this->assertEquals('errmsg', $exception->getMessage()); + $this->assertEquals(json_encode($params), $exception->getRawResponse()); + $this->assertEquals(401, $exception->getHttpStatusCode()); + + $params['error']['code'] = 102; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(102, $exception->getCode()); + + $params['error']['code'] = 190; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(190, $exception->getCode()); + + $params['error']['type'] = 'OAuthException'; + $params['error']['code'] = 0; + $params['error']['error_subcode'] = 458; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(458, $exception->getSubErrorCode()); + + $params['error']['error_subcode'] = 460; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(460, $exception->getSubErrorCode()); + + $params['error']['error_subcode'] = 463; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(463, $exception->getSubErrorCode()); + + $params['error']['error_subcode'] = 467; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(467, $exception->getSubErrorCode()); + + $params['error']['error_subcode'] = 0; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(0, $exception->getSubErrorCode()); + } + + public function testServerExceptions() + { + $params = [ + 'error' => [ + 'code' => 1, + 'message' => 'errmsg', + 'error_subcode' => 0, + 'type' => 'exception' + ], + ]; + + $response = new FacebookResponse($this->request, json_encode($params), 500); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookServerException', $exception->getPrevious()); + $this->assertEquals(1, $exception->getCode()); + $this->assertEquals(0, $exception->getSubErrorCode()); + $this->assertEquals('exception', $exception->getErrorType()); + $this->assertEquals('errmsg', $exception->getMessage()); + $this->assertEquals(json_encode($params), $exception->getRawResponse()); + $this->assertEquals(500, $exception->getHttpStatusCode()); + + $params['error']['code'] = 2; + $response = new FacebookResponse($this->request, json_encode($params), 500); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookServerException', $exception->getPrevious()); + $this->assertEquals(2, $exception->getCode()); + } + + public function testThrottleExceptions() + { + $params = [ + 'error' => [ + 'code' => 4, + 'message' => 'errmsg', + 'error_subcode' => 0, + 'type' => 'exception' + ], + ]; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookThrottleException', $exception->getPrevious()); + $this->assertEquals(4, $exception->getCode()); + $this->assertEquals(0, $exception->getSubErrorCode()); + $this->assertEquals('exception', $exception->getErrorType()); + $this->assertEquals('errmsg', $exception->getMessage()); + $this->assertEquals(json_encode($params), $exception->getRawResponse()); + $this->assertEquals(401, $exception->getHttpStatusCode()); + + $params['error']['code'] = 17; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookThrottleException', $exception->getPrevious()); + $this->assertEquals(17, $exception->getCode()); + + $params['error']['code'] = 341; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookThrottleException', $exception->getPrevious()); + $this->assertEquals(341, $exception->getCode()); + } + + public function testUserIssueExceptions() + { + $params = [ + 'error' => [ + 'code' => 230, + 'message' => 'errmsg', + 'error_subcode' => 459, + 'type' => 'exception' + ], + ]; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(230, $exception->getCode()); + $this->assertEquals(459, $exception->getSubErrorCode()); + $this->assertEquals('exception', $exception->getErrorType()); + $this->assertEquals('errmsg', $exception->getMessage()); + $this->assertEquals(json_encode($params), $exception->getRawResponse()); + $this->assertEquals(401, $exception->getHttpStatusCode()); + + $params['error']['error_subcode'] = 464; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthenticationException', $exception->getPrevious()); + $this->assertEquals(464, $exception->getSubErrorCode()); + } + + public function testAuthorizationExceptions() + { + $params = [ + 'error' => [ + 'code' => 10, + 'message' => 'errmsg', + 'error_subcode' => 0, + 'type' => 'exception' + ], + ]; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthorizationException', $exception->getPrevious()); + $this->assertEquals(10, $exception->getCode()); + $this->assertEquals(0, $exception->getSubErrorCode()); + $this->assertEquals('exception', $exception->getErrorType()); + $this->assertEquals('errmsg', $exception->getMessage()); + $this->assertEquals(json_encode($params), $exception->getRawResponse()); + $this->assertEquals(401, $exception->getHttpStatusCode()); + + $params['error']['code'] = 200; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthorizationException', $exception->getPrevious()); + $this->assertEquals(200, $exception->getCode()); + + $params['error']['code'] = 250; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthorizationException', $exception->getPrevious()); + $this->assertEquals(250, $exception->getCode()); + + $params['error']['code'] = 299; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookAuthorizationException', $exception->getPrevious()); + $this->assertEquals(299, $exception->getCode()); + } + + public function testClientExceptions() + { + $params = [ + 'error' => [ + 'code' => 506, + 'message' => 'errmsg', + 'error_subcode' => 0, + 'type' => 'exception' + ], + ]; + $response = new FacebookResponse($this->request, json_encode($params), 401); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookClientException', $exception->getPrevious()); + $this->assertEquals(506, $exception->getCode()); + $this->assertEquals(0, $exception->getSubErrorCode()); + $this->assertEquals('exception', $exception->getErrorType()); + $this->assertEquals('errmsg', $exception->getMessage()); + $this->assertEquals(json_encode($params), $exception->getRawResponse()); + $this->assertEquals(401, $exception->getHttpStatusCode()); + } + + public function testOtherException() + { + $params = [ + 'error' => [ + 'code' => 42, + 'message' => 'ship love', + 'error_subcode' => 0, + 'type' => 'feature' + ], + ]; + $response = new FacebookResponse($this->request, json_encode($params), 200); + $exception = FacebookResponseException::create($response); + $this->assertInstanceOf('Facebook\Exceptions\FacebookOtherException', $exception->getPrevious()); + $this->assertEquals(42, $exception->getCode()); + $this->assertEquals(0, $exception->getSubErrorCode()); + $this->assertEquals('feature', $exception->getErrorType()); + $this->assertEquals('ship love', $exception->getMessage()); + $this->assertEquals(json_encode($params), $exception->getRawResponse()); + $this->assertEquals(200, $exception->getHttpStatusCode()); + } +} diff --git a/tests/FacebookAppTest.php b/tests/FacebookAppTest.php new file mode 100644 index 000000000..de0847711 --- /dev/null +++ b/tests/FacebookAppTest.php @@ -0,0 +1,81 @@ +app = new FacebookApp('id', 'secret'); + } + + public function testGetId() + { + $this->assertEquals('id', $this->app->getId()); + } + + public function testGetSecret() + { + $this->assertEquals('secret', $this->app->getSecret()); + } + + public function testAnAppAccessTokenCanBeGenerated() + { + $accessToken = $this->app->getAccessToken(); + + $this->assertInstanceOf('Facebook\Authentication\AccessToken', $accessToken); + $this->assertEquals('id|secret', (string)$accessToken); + } + + public function testSerialization() + { + $newApp = unserialize(serialize($this->app)); + + $this->assertInstanceOf('Facebook\FacebookApp', $newApp); + $this->assertEquals('id', $newApp->getId()); + $this->assertEquals('secret', $newApp->getSecret()); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testOverflowIntegersWillThrow() + { + new FacebookApp(PHP_INT_MAX + 1, "foo"); + } + + public function testUnserializedIdsWillBeString() + { + $newApp = unserialize(serialize(new FacebookApp(1, "foo"))); + + $this->assertSame('1', $newApp->getId()); + } +} diff --git a/tests/FacebookBatchRequestTest.php b/tests/FacebookBatchRequestTest.php new file mode 100755 index 000000000..a77e75450 --- /dev/null +++ b/tests/FacebookBatchRequestTest.php @@ -0,0 +1,432 @@ +app = new FacebookApp('123', 'foo_secret'); + } + + public function testABatchRequestWillInstantiateWithTheProperProperties() + { + $batchRequest = new FacebookBatchRequest($this->app, [], 'foo_token', 'v0.1337'); + + $this->assertSame($this->app, $batchRequest->getApp()); + $this->assertEquals('foo_token', $batchRequest->getAccessToken()); + $this->assertEquals('POST', $batchRequest->getMethod()); + $this->assertEquals('', $batchRequest->getEndpoint()); + $this->assertEquals('v0.1337', $batchRequest->getGraphVersion()); + } + + public function testEmptyRequestWillFallbackToBatchDefaults() + { + $request = new FacebookRequest(); + + $this->createBatchRequest()->addFallbackDefaults($request); + + $this->assertRequestContainsAppAndToken($request, $this->app, 'foo_token'); + } + + public function testRequestWithTokenOnlyWillFallbackToBatchDefaults() + { + $request = new FacebookRequest(null, 'bar_token'); + + $this->createBatchRequest()->addFallbackDefaults($request); + + $this->assertRequestContainsAppAndToken($request, $this->app, 'bar_token'); + } + + public function testRequestWithAppOnlyWillFallbackToBatchDefaults() + { + $customApp = new FacebookApp('1337', 'bar_secret'); + $request = new FacebookRequest($customApp); + + $this->createBatchRequest()->addFallbackDefaults($request); + + $this->assertRequestContainsAppAndToken($request, $customApp, 'foo_token'); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testWillThrowWhenNoThereIsNoAppFallback() + { + $batchRequest = new FacebookBatchRequest(); + + $batchRequest->addFallbackDefaults(new FacebookRequest(null, 'foo_token')); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testWillThrowWhenNoThereIsNoAccessTokenFallback() + { + $request = new FacebookBatchRequest(); + + $request->addFallbackDefaults(new FacebookRequest($this->app)); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testAnInvalidTypeGivenToAddWillThrow() + { + $request = new FacebookBatchRequest(); + + $request->add('foo'); + } + + public function testAddingRequestsWillBeFormattedInAnArrayProperly() + { + $requests = [ + null => new FacebookRequest(null, null, 'GET', '/foo'), + 'my-second-request' => new FacebookRequest(null, null, 'POST', '/bar', ['foo' => 'bar']), + 'my-third-request' => new FacebookRequest(null, null, 'DELETE', '/baz') + ]; + + $batchRequest = $this->createBatchRequest(); + $batchRequest->add($requests[null]); + $batchRequest->add($requests['my-second-request'], 'my-second-request'); + $batchRequest->add($requests['my-third-request'], 'my-third-request'); + + $formattedRequests = $batchRequest->getRequests(); + + $this->assertRequestsMatch($requests, $formattedRequests); + } + + public function testANumericArrayOfRequestsCanBeAdded() + { + $requests = [ + new FacebookRequest(null, null, 'GET', '/foo'), + new FacebookRequest(null, null, 'POST', '/bar', ['foo' => 'bar']), + new FacebookRequest(null, null, 'DELETE', '/baz'), + ]; + + $formattedRequests = $this->createBatchRequestWithRequests($requests)->getRequests(); + + $this->assertRequestsMatch($requests, $formattedRequests); + } + + public function testAnAssociativeArrayOfRequestsCanBeAdded() + { + $requests = [ + 'req-one' => new FacebookRequest(null, null, 'GET', '/foo'), + 'req-two' => new FacebookRequest(null, null, 'POST', '/bar', ['foo' => 'bar']), + 'req-three' => new FacebookRequest(null, null, 'DELETE', '/baz'), + ]; + + $formattedRequests = $this->createBatchRequestWithRequests($requests)->getRequests(); + + $this->assertRequestsMatch($requests, $formattedRequests); + } + + public function testRequestsCanBeInjectedIntoConstructor() + { + $requests = [ + new FacebookRequest(null, null, 'GET', '/foo'), + new FacebookRequest(null, null, 'POST', '/bar', ['foo' => 'bar']), + new FacebookRequest(null, null, 'DELETE', '/baz'), + ]; + + $batchRequest = new FacebookBatchRequest($this->app, $requests, 'foo_token'); + $formattedRequests = $batchRequest->getRequests(); + + $this->assertRequestsMatch($requests, $formattedRequests); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAZeroRequestCountWithThrow() + { + $batchRequest = new FacebookBatchRequest($this->app, [], 'foo_token'); + + $batchRequest->validateBatchRequestCount(); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testMoreThanFiftyRequestsWillThrow() + { + $batchRequest = $this->createBatchRequest(); + + $this->createAndAppendRequestsTo($batchRequest, 51); + + $batchRequest->validateBatchRequestCount(); + } + + public function testLessOrEqualThanFiftyRequestsWillNotThrow() + { + $batchRequest = $this->createBatchRequest(); + + $this->createAndAppendRequestsTo($batchRequest, 50); + + $batchRequest->validateBatchRequestCount(); + } + + /** + * @dataProvider requestsAndExpectedResponsesProvider + */ + public function testBatchRequestEntitiesProperlyGetConvertedToAnArray($request, $expectedArray) + { + $batchRequest = $this->createBatchRequest(); + $batchRequest->add($request, 'foo_name'); + + $requests = $batchRequest->getRequests(); + $batchRequestArray = $batchRequest->requestEntityToBatchArray($requests[0]['request'], $requests[0]['name']); + + $this->assertEquals($expectedArray, $batchRequestArray); + } + + public function requestsAndExpectedResponsesProvider() + { + $headers = $this->defaultHeaders(); + $apiVersion = Facebook::DEFAULT_GRAPH_VERSION; + + return [ + [ + new FacebookRequest(null, null, 'GET', '/foo', ['foo' => 'bar']), + [ + 'headers' => $headers, + 'method' => 'GET', + 'relative_url' => '/' . $apiVersion . '/foo?foo=bar&access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + 'name' => 'foo_name', + ], + ], + [ + new FacebookRequest(null, null, 'POST', '/bar', ['bar' => 'baz']), + [ + 'headers' => $headers, + 'method' => 'POST', + 'relative_url' => '/' . $apiVersion . '/bar', + 'body' => 'bar=baz&access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + 'name' => 'foo_name', + ], + ], + [ + new FacebookRequest(null, null, 'DELETE', '/bar'), + [ + 'headers' => $headers, + 'method' => 'DELETE', + 'relative_url' => '/' . $apiVersion . '/bar?access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + 'name' => 'foo_name', + ], + ], + ]; + } + + public function testBatchRequestsWithFilesGetConvertedToAnArray() + { + $request = new FacebookRequest(null, null, 'POST', '/bar', [ + 'message' => 'foobar', + 'source' => new FacebookFile(__DIR__ . '/foo.txt'), + ]); + + $batchRequest = $this->createBatchRequest(); + $batchRequest->add($request, 'foo_name'); + + $requests = $batchRequest->getRequests(); + + $attachedFiles = $requests[0]['attached_files']; + + $batchRequestArray = $batchRequest->requestEntityToBatchArray( + $requests[0]['request'], + $requests[0]['name'], + $attachedFiles + ); + + $this->assertEquals([ + 'headers' => $this->defaultHeaders(), + 'method' => 'POST', + 'relative_url' => '/' . Facebook::DEFAULT_GRAPH_VERSION . '/bar', + 'body' => 'message=foobar&access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + 'name' => 'foo_name', + 'attached_files' => $attachedFiles, + ], $batchRequestArray); + } + + public function testBatchRequestsWithOptionsGetConvertedToAnArray() + { + $request = new FacebookRequest(null, null, 'GET', '/bar'); + $batchRequest = $this->createBatchRequest(); + $batchRequest->add($request, [ + 'name' => 'foo_name', + 'omit_response_on_success' => false, + ]); + + $requests = $batchRequest->getRequests(); + + $options = $requests[0]['options']; + $options['name'] = $requests[0]['name']; + + $batchRequestArray = $batchRequest->requestEntityToBatchArray($requests[0]['request'], $options); + + $this->assertEquals([ + 'headers' => $this->defaultHeaders(), + 'method' => 'GET', + 'relative_url' => '/' . Facebook::DEFAULT_GRAPH_VERSION . '/bar?access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + 'name' => 'foo_name', + 'omit_response_on_success' => false, + ], $batchRequestArray); + } + + public function testPreppingABatchRequestProperlySetsThePostParams() + { + $batchRequest = $this->createBatchRequest(); + $batchRequest->add(new FacebookRequest(null, 'bar_token', 'GET', '/foo'), 'foo_name'); + $batchRequest->add(new FacebookRequest(null, null, 'POST', '/bar', ['foo' => 'bar'])); + $batchRequest->prepareRequestsForBatch(); + + $params = $batchRequest->getParams(); + + $expectedHeaders = json_encode($this->defaultHeaders()); + $version = Facebook::DEFAULT_GRAPH_VERSION; + $expectedBatchParams = [ + 'batch' => '[{"headers":' . $expectedHeaders . ',"method":"GET","relative_url":"\\/' . $version . '\\/foo?access_token=bar_token&appsecret_proof=2ceec40b7b9fd7d38fff1767b766bcc6b1f9feb378febac4612c156e6a8354bd","name":"foo_name"},' + . '{"headers":' . $expectedHeaders . ',"method":"POST","relative_url":"\\/' . $version . '\\/bar","body":"foo=bar&access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9"}]', + 'include_headers' => true, + 'access_token' => 'foo_token', + 'appsecret_proof' => 'df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + ]; + $this->assertEquals($expectedBatchParams, $params); + } + + public function testPreppingABatchRequestProperlyMovesTheFiles() + { + $batchRequest = $this->createBatchRequest(); + $batchRequest->add(new FacebookRequest(null, 'bar_token', 'GET', '/foo'), 'foo_name'); + $batchRequest->add(new FacebookRequest(null, null, 'POST', '/me/photos', [ + 'message' => 'foobar', + 'source' => new FacebookFile(__DIR__ . '/foo.txt'), + ])); + $batchRequest->prepareRequestsForBatch(); + + $params = $batchRequest->getParams(); + $files = $batchRequest->getFiles(); + + $attachedFiles = implode(',', array_keys($files)); + + $expectedHeaders = json_encode($this->defaultHeaders()); + $version = Facebook::DEFAULT_GRAPH_VERSION; + $expectedBatchParams = [ + 'batch' => '[{"headers":' . $expectedHeaders . ',"method":"GET","relative_url":"\\/' . $version . '\\/foo?access_token=bar_token&appsecret_proof=2ceec40b7b9fd7d38fff1767b766bcc6b1f9feb378febac4612c156e6a8354bd","name":"foo_name"},' + . '{"headers":' . $expectedHeaders . ',"method":"POST","relative_url":"\\/' . $version . '\\/me\\/photos","body":"message=foobar&access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9","attached_files":"' . $attachedFiles . '"}]', + 'include_headers' => true, + 'access_token' => 'foo_token', + 'appsecret_proof' => 'df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + ]; + $this->assertEquals($expectedBatchParams, $params); + } + + public function testPreppingABatchRequestWithOptionsProperlySetsThePostParams() + { + $batchRequest = $this->createBatchRequest(); + $batchRequest->add(new FacebookRequest(null, null, 'GET', '/foo'), [ + 'name' => 'foo_name', + 'omit_response_on_success' => false, + ]); + + $batchRequest->prepareRequestsForBatch(); + $params = $batchRequest->getParams(); + + $expectedHeaders = json_encode($this->defaultHeaders()); + $version = Facebook::DEFAULT_GRAPH_VERSION; + + $expectedBatchParams = [ + 'batch' => '[{"headers":' . $expectedHeaders . ',"method":"GET","relative_url":"\\/' . $version . '\\/foo?access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9",' + . '"name":"foo_name","omit_response_on_success":false}]', + 'include_headers' => true, + 'access_token' => 'foo_token', + 'appsecret_proof' => 'df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + ]; + $this->assertEquals($expectedBatchParams, $params); + } + + private function assertRequestContainsAppAndToken(FacebookRequest $request, FacebookApp $expectedApp, $expectedToken) + { + $app = $request->getApp(); + $token = $request->getAccessToken(); + + $this->assertSame($expectedApp, $app); + $this->assertEquals($expectedToken, $token); + } + + private function defaultHeaders() + { + $headers = []; + foreach (FacebookRequest::getDefaultHeaders() as $name => $value) { + $headers[] = $name . ': ' . $value; + } + + return $headers; + } + + private function createAndAppendRequestsTo(FacebookBatchRequest $batchRequest, $number) + { + for ($i = 0; $i < $number; $i++) { + $batchRequest->add(new FacebookRequest()); + } + } + + private function createBatchRequest() + { + return new FacebookBatchRequest($this->app, [], 'foo_token'); + } + + private function createBatchRequestWithRequests(array $requests) + { + $batchRequest = $this->createBatchRequest(); + $batchRequest->add($requests); + + return $batchRequest; + } + + private function assertRequestsMatch($requests, $formattedRequests) + { + $expectedRequests = []; + foreach ($requests as $name => $request) { + $expectedRequests[] = [ + 'name' => $name, + 'request' => $request, + 'attached_files' => null, + 'options' => [], + ]; + } + $this->assertEquals($expectedRequests, $formattedRequests); + } +} diff --git a/tests/FacebookBatchResponseTest.php b/tests/FacebookBatchResponseTest.php new file mode 100755 index 000000000..8f4e5a8ad --- /dev/null +++ b/tests/FacebookBatchResponseTest.php @@ -0,0 +1,164 @@ +app = new FacebookApp('123', 'foo_secret'); + $this->request = new FacebookRequest( + $this->app, + 'foo_token', + 'POST', + '/', + ['batch' => 'foo'], + 'foo_eTag', + 'v1337' + ); + } + + public function testASuccessfulJsonBatchResponseWillBeDecoded() + { + $graphResponseJson = '['; + // Single Graph object. + $graphResponseJson .= '{"code":200,"headers":[{"name":"Connection","value":"close"},{"name":"Last-Modified","value":"2013-12-24T00:34:20+0000"},{"name":"Facebook-API-Version","value":"v2.0"},{"name":"ETag","value":"\"fooTag\""},{"name":"Content-Type","value":"text\/javascript; charset=UTF-8"},{"name":"Pragma","value":"no-cache"},{"name":"Access-Control-Allow-Origin","value":"*"},{"name":"Cache-Control","value":"private, no-cache, no-store, must-revalidate"},{"name":"Expires","value":"Sat, 01 Jan 2000 00:00:00 GMT"}],"body":"{\"id\":\"123\",\"name\":\"Foo McBar\",\"updated_time\":\"2013-12-24T00:34:20+0000\",\"verified\":true}"}'; + // Paginated list of Graph objects. + $graphResponseJson .= ',{"code":200,"headers":[{"name":"Connection","value":"close"},{"name":"Facebook-API-Version","value":"v1.0"},{"name":"ETag","value":"\"barTag\""},{"name":"Content-Type","value":"text\/javascript; charset=UTF-8"},{"name":"Pragma","value":"no-cache"},{"name":"Access-Control-Allow-Origin","value":"*"},{"name":"Cache-Control","value":"private, no-cache, no-store, must-revalidate"},{"name":"Expires","value":"Sat, 01 Jan 2000 00:00:00 GMT"}],"body":"{\"data\":[{\"id\":\"1337\",\"story\":\"Foo story.\"},{\"id\":\"1338\",\"story\":\"Bar story.\"}],\"paging\":{\"previous\":\"previous_url\",\"next\":\"next_url\"}}"}'; + // After POST operation. + $graphResponseJson .= ',{"code":200,"headers":[{"name":"Connection","value":"close"},{"name":"Expires","value":"Sat, 01 Jan 2000 00:00:00 GMT"},{"name":"Cache-Control","value":"private, no-cache, no-store, must-revalidate"},{"name":"Access-Control-Allow-Origin","value":"*"},{"name":"Pragma","value":"no-cache"},{"name":"Content-Type","value":"text\/javascript; charset=UTF-8"},{"name":"Facebook-API-Version","value":"v2.0"}],"body":"{\"id\":\"123_1337\"}"}'; + // After DELETE operation. + $graphResponseJson .= ',{"code":200,"headers":[{"name":"Connection","value":"close"},{"name":"Expires","value":"Sat, 01 Jan 2000 00:00:00 GMT"},{"name":"Cache-Control","value":"private, no-cache, no-store, must-revalidate"},{"name":"Access-Control-Allow-Origin","value":"*"},{"name":"Pragma","value":"no-cache"},{"name":"Content-Type","value":"text\/javascript; charset=UTF-8"},{"name":"Facebook-API-Version","value":"v2.0"}],"body":"true"}'; + $graphResponseJson .= ']'; + $response = new FacebookResponse($this->request, $graphResponseJson, 200); + $batchRequest = new FacebookBatchRequest($this->app, [ + new FacebookRequest($this->app, 'token'), + new FacebookRequest($this->app, 'token'), + new FacebookRequest($this->app, 'token'), + new FacebookRequest($this->app, 'token'), + ]); + $batchResponse = new FacebookBatchResponse($batchRequest, $response); + + $decodedResponses = $batchResponse->getResponses(); + + // Single Graph object. + $this->assertFalse($decodedResponses[0]->isError(), 'Did not expect Response to return an error for single Graph object.'); + $this->assertInstanceOf('Facebook\GraphNodes\GraphNode', $decodedResponses[0]->getGraphNode()); + // Paginated list of Graph objects. + $this->assertFalse($decodedResponses[1]->isError(), 'Did not expect Response to return an error for paginated list of Graph objects.'); + $graphEdge = $decodedResponses[1]->getGraphEdge(); + $this->assertInstanceOf('Facebook\GraphNodes\GraphNode', $graphEdge[0]); + $this->assertInstanceOf('Facebook\GraphNodes\GraphNode', $graphEdge[1]); + } + + public function testABatchResponseCanBeIteratedOver() + { + $graphResponseJson = '['; + $graphResponseJson .= '{"code":200,"headers":[],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ',{"code":200,"headers":[],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ',{"code":200,"headers":[],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ']'; + $response = new FacebookResponse($this->request, $graphResponseJson, 200); + $batchRequest = new FacebookBatchRequest($this->app, [ + 'req_one' => new FacebookRequest($this->app, 'token'), + 'req_two' => new FacebookRequest($this->app, 'token'), + 'req_three' => new FacebookRequest($this->app, 'token'), + ]); + $batchResponse = new FacebookBatchResponse($batchRequest, $response); + + $this->assertInstanceOf('IteratorAggregate', $batchResponse); + + foreach ($batchResponse as $key => $responseEntity) { + $this->assertTrue(in_array($key, ['req_one', 'req_two', 'req_three'])); + $this->assertInstanceOf('Facebook\FacebookResponse', $responseEntity); + } + } + + public function testTheOriginalRequestCanBeObtainedForEachRequest() + { + $graphResponseJson = '['; + $graphResponseJson .= '{"code":200,"headers":[],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ',{"code":200,"headers":[],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ',{"code":200,"headers":[],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ']'; + $response = new FacebookResponse($this->request, $graphResponseJson, 200); + + $requests = [ + new FacebookRequest($this->app, 'foo_token_one', 'GET', '/me'), + new FacebookRequest($this->app, 'foo_token_two', 'POST', '/you'), + new FacebookRequest($this->app, 'foo_token_three', 'DELETE', '/123456'), + ]; + + $batchRequest = new FacebookBatchRequest($this->app, $requests); + $batchResponse = new FacebookBatchResponse($batchRequest, $response); + + $this->assertInstanceOf('Facebook\FacebookResponse', $batchResponse[0]); + $this->assertInstanceOf('Facebook\FacebookRequest', $batchResponse[0]->getRequest()); + $this->assertEquals('foo_token_one', $batchResponse[0]->getAccessToken()); + $this->assertEquals('foo_token_two', $batchResponse[1]->getAccessToken()); + $this->assertEquals('foo_token_three', $batchResponse[2]->getAccessToken()); + } + + public function testHeadersFromBatchRequestCanBeAccessed() + { + $graphResponseJson = '['; + $graphResponseJson .= '{"code":200,"headers":[{"name":"Facebook-API-Version","value":"v2.0"},{"name":"ETag","value":"\"fooTag\""}],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ',{"code":200,"headers":[{"name":"Facebook-API-Version","value":"v2.5"},{"name":"ETag","value":"\"barTag\""}],"body":"{\"foo\":\"bar\"}"}'; + $graphResponseJson .= ']'; + $response = new FacebookResponse($this->request, $graphResponseJson, 200); + + $requests = [ + new FacebookRequest($this->app, 'foo_token_one', 'GET', '/me'), + new FacebookRequest($this->app, 'foo_token_two', 'GET', '/you'), + ]; + + $batchRequest = new FacebookBatchRequest($this->app, $requests); + $batchResponse = new FacebookBatchResponse($batchRequest, $response); + + $this->assertEquals('v2.0', $batchResponse[0]->getGraphVersion()); + $this->assertEquals('"fooTag"', $batchResponse[0]->getETag()); + $this->assertEquals('v2.5', $batchResponse[1]->getGraphVersion()); + $this->assertEquals('"barTag"', $batchResponse[1]->getETag()); + $this->assertEquals([ + 'Facebook-API-Version' => 'v2.5', + 'ETag' => '"barTag"', + ], $batchResponse[1]->getHeaders()); + } +} diff --git a/tests/FacebookCanvasLoginHelperTest.php b/tests/FacebookCanvasLoginHelperTest.php deleted file mode 100644 index 83bec781c..000000000 --- a/tests/FacebookCanvasLoginHelperTest.php +++ /dev/null @@ -1,34 +0,0 @@ -helper = new FacebookCanvasLoginHelper('123', 'foo_app_secret'); - } - - public function testSignedRequestDataCanBeRetrievedFromGetData() - { - $_GET['signed_request'] = $this->rawSignedRequestAuthorized; - - $rawSignedRequest = $this->helper->getRawSignedRequest(); - - $this->assertEquals($this->rawSignedRequestAuthorized, $rawSignedRequest); - } - - public function testSignedRequestDataCanBeRetrievedFromPostData() - { - $_POST['signed_request'] = $this->rawSignedRequestAuthorized; - - $rawSignedRequest = $this->helper->getRawSignedRequest(); - - $this->assertEquals($this->rawSignedRequestAuthorized, $rawSignedRequest); - } - -} diff --git a/tests/FacebookClientTest.php b/tests/FacebookClientTest.php new file mode 100644 index 000000000..9a08fb6e8 --- /dev/null +++ b/tests/FacebookClientTest.php @@ -0,0 +1,294 @@ +fbApp = new FacebookApp('id', 'shhhh!'); + $this->fbClient = new FacebookClient(new MyFooClientHandler()); + } + + public function testACustomHttpClientCanBeInjected() + { + $handler = new MyFooClientHandler(); + $client = new FacebookClient($handler); + $httpHandler = $client->getHttpClientHandler(); + + $this->assertInstanceOf('Facebook\Tests\Fixtures\MyFooClientHandler', $httpHandler); + } + + public function testTheHttpClientWillFallbackToDefault() + { + $client = new FacebookClient(); + $httpHandler = $client->getHttpClientHandler(); + + if (function_exists('curl_init')) { + $this->assertInstanceOf('Facebook\HttpClients\FacebookCurlHttpClient', $httpHandler); + } else { + $this->assertInstanceOf('Facebook\HttpClients\FacebookStreamHttpClient', $httpHandler); + } + } + + public function testBetaModeCanBeDisabledOrEnabledViaConstructor() + { + $client = new FacebookClient(null, false); + $url = $client->getBaseGraphUrl(); + $this->assertEquals(FacebookClient::BASE_GRAPH_URL, $url); + + $client = new FacebookClient(null, true); + $url = $client->getBaseGraphUrl(); + $this->assertEquals(FacebookClient::BASE_GRAPH_URL_BETA, $url); + } + + public function testBetaModeCanBeDisabledOrEnabledViaMethod() + { + $client = new FacebookClient(); + $client->enableBetaMode(false); + $url = $client->getBaseGraphUrl(); + $this->assertEquals(FacebookClient::BASE_GRAPH_URL, $url); + + $client->enableBetaMode(true); + $url = $client->getBaseGraphUrl(); + $this->assertEquals(FacebookClient::BASE_GRAPH_URL_BETA, $url); + } + + public function testGraphVideoUrlCanBeSet() + { + $client = new FacebookClient(); + $client->enableBetaMode(false); + $url = $client->getBaseGraphUrl($postToVideoUrl = true); + $this->assertEquals(FacebookClient::BASE_GRAPH_VIDEO_URL, $url); + + $client->enableBetaMode(true); + $url = $client->getBaseGraphUrl($postToVideoUrl = true); + $this->assertEquals(FacebookClient::BASE_GRAPH_VIDEO_URL_BETA, $url); + } + + public function testAFacebookRequestEntityCanBeUsedToSendARequestToGraph() + { + $fbRequest = new FacebookRequest($this->fbApp, 'token', 'GET', '/foo'); + $response = $this->fbClient->sendRequest($fbRequest); + + $this->assertInstanceOf('Facebook\FacebookResponse', $response); + $this->assertEquals(200, $response->getHttpStatusCode()); + $this->assertEquals('{"data":[{"id":"123","name":"Foo"},{"id":"1337","name":"Bar"}]}', $response->getBody()); + } + + public function testAFacebookBatchRequestEntityCanBeUsedToSendABatchRequestToGraph() + { + $fbRequests = [ + new FacebookRequest($this->fbApp, 'token', 'GET', '/foo'), + new FacebookRequest($this->fbApp, 'token', 'POST', '/bar'), + ]; + $fbBatchRequest = new FacebookBatchRequest($this->fbApp, $fbRequests); + + $fbBatchClient = new FacebookClient(new MyFooBatchClientHandler()); + $response = $fbBatchClient->sendBatchRequest($fbBatchRequest); + + $this->assertInstanceOf('Facebook\FacebookBatchResponse', $response); + $this->assertEquals('GET', $response[0]->getRequest()->getMethod()); + $this->assertEquals('POST', $response[1]->getRequest()->getMethod()); + } + + public function testAFacebookBatchRequestWillProperlyBatchFiles() + { + $fbRequests = [ + new FacebookRequest($this->fbApp, 'token', 'POST', '/photo', [ + 'message' => 'foobar', + 'source' => new FacebookFile(__DIR__ . '/foo.txt'), + ]), + new FacebookRequest($this->fbApp, 'token', 'POST', '/video', [ + 'message' => 'foobar', + 'source' => new FacebookVideo(__DIR__ . '/foo.txt'), + ]), + ]; + $fbBatchRequest = new FacebookBatchRequest($this->fbApp, $fbRequests); + $fbBatchRequest->prepareRequestsForBatch(); + + list($url, $method, $headers, $body) = $this->fbClient->prepareRequestMessage($fbBatchRequest); + + $this->assertEquals(FacebookClient::BASE_GRAPH_VIDEO_URL . '/' . Facebook::DEFAULT_GRAPH_VERSION, $url); + $this->assertEquals('POST', $method); + $this->assertContains('multipart/form-data; boundary=', $headers['Content-Type']); + $this->assertContains('Content-Disposition: form-data; name="batch"', $body); + $this->assertContains('Content-Disposition: form-data; name="include_headers"', $body); + $this->assertContains('"name":0,"attached_files":', $body); + $this->assertContains('"name":1,"attached_files":', $body); + $this->assertContains('"; filename="foo.txt"', $body); + } + + public function testARequestOfParamsWillBeUrlEncoded() + { + $fbRequest = new FacebookRequest($this->fbApp, 'token', 'POST', '/foo', ['foo' => 'bar']); + $response = $this->fbClient->sendRequest($fbRequest); + + $headersSent = $response->getRequest()->getHeaders(); + + $this->assertEquals('application/x-www-form-urlencoded', $headersSent['Content-Type']); + } + + public function testARequestWithFilesWillBeMultipart() + { + $myFile = new FacebookFile(__DIR__ . '/foo.txt'); + $fbRequest = new FacebookRequest($this->fbApp, 'token', 'POST', '/foo', ['file' => $myFile]); + $response = $this->fbClient->sendRequest($fbRequest); + + $headersSent = $response->getRequest()->getHeaders(); + + $this->assertContains('multipart/form-data; boundary=', $headersSent['Content-Type']); + } + + public function testAFacebookRequestValidatesTheAccessTokenWhenOneIsNotProvided() + { + $this->setExpectedException('Facebook\Exceptions\FacebookSDKException'); + + $fbRequest = new FacebookRequest($this->fbApp, null, 'GET', '/foo'); + $this->fbClient->sendRequest($fbRequest); + } + + /** + * @group integration + */ + public function testCanCreateATestUserAndGetTheProfileAndThenDeleteTheTestUser() + { + $this->initializeTestApp(); + + // Create a test user + $testUserPath = '/' . FacebookTestCredentials::$appId . '/accounts/test-users'; + $params = [ + 'installed' => true, + 'name' => 'Foo Phpunit User', + 'locale' => 'en_US', + 'permissions' => implode(',', ['read_stream', 'user_photos']), + ]; + + $request = new FacebookRequest( + static::$testFacebookApp, + static::$testFacebookApp->getAccessToken(), + 'POST', + $testUserPath, + $params + ); + $response = static::$testFacebookClient->sendRequest($request)->getGraphNode(); + + $testUserId = $response->getField('id'); + $testUserAccessToken = $response->getField('access_token'); + + // Get the test user's profile + $request = new FacebookRequest( + static::$testFacebookApp, + $testUserAccessToken, + 'GET', + '/me' + ); + $graphNode = static::$testFacebookClient->sendRequest($request)->getGraphNode(); + + $this->assertInstanceOf('Facebook\GraphNodes\GraphNode', $graphNode); + $this->assertNotNull($graphNode->getField('id')); + $this->assertEquals('Foo Phpunit User', $graphNode->getField('name')); + + // Delete test user + $request = new FacebookRequest( + static::$testFacebookApp, + static::$testFacebookApp->getAccessToken(), + 'DELETE', + '/' . $testUserId + ); + $graphNode = static::$testFacebookClient->sendRequest($request)->getGraphNode(); + + $this->assertTrue($graphNode->getField('success')); + } + + public function initializeTestApp() + { + if (!file_exists(__DIR__ . '/FacebookTestCredentials.php')) { + throw new FacebookSDKException( + 'You must create a FacebookTestCredentials.php file from FacebookTestCredentials.php.dist' + ); + } + + if (!strlen(FacebookTestCredentials::$appId) || + !strlen(FacebookTestCredentials::$appSecret) + ) { + throw new FacebookSDKException( + 'You must fill out FacebookTestCredentials.php' + ); + } + static::$testFacebookApp = new FacebookApp( + FacebookTestCredentials::$appId, + FacebookTestCredentials::$appSecret + ); + + // Use default client + $client = null; + + // Uncomment to enable curl implementation. + //$client = new FacebookCurlHttpClient(); + + // Uncomment to enable stream wrapper implementation. + //$client = new FacebookStreamHttpClient(); + + // Uncomment to enable Guzzle implementation. + //$client = new FacebookGuzzleHttpClient(); + + static::$testFacebookClient = new FacebookClient($client); + } +} diff --git a/tests/FacebookJavaScriptLoginHelperTest.php b/tests/FacebookJavaScriptLoginHelperTest.php deleted file mode 100644 index a0f8c1b4d..000000000 --- a/tests/FacebookJavaScriptLoginHelperTest.php +++ /dev/null @@ -1,23 +0,0 @@ -rawSignedRequestAuthorized; - - $helper = new FacebookJavaScriptLoginHelper($this->appId, $this->appSecret); - - $rawSignedRequest = $helper->getRawSignedRequest(); - - $this->assertEquals($this->rawSignedRequestAuthorized, $rawSignedRequest); - } - -} diff --git a/tests/FacebookPageTabHelperTest.php b/tests/FacebookPageTabHelperTest.php deleted file mode 100644 index d0f11e67d..000000000 --- a/tests/FacebookPageTabHelperTest.php +++ /dev/null @@ -1,22 +0,0 @@ -rawSignedRequestAuthorized; - $helper = new FacebookPageTabHelper('123', 'foo_app_secret'); - - $this->assertTrue($helper->isLiked()); - $this->assertFalse($helper->isAdmin()); - $this->assertEquals('42', $helper->getPageId()); - $this->assertEquals('42', $helper->getPageData('id')); - $this->assertEquals('default', $helper->getPageData('foo', 'default')); - } - -} diff --git a/tests/FacebookRedirectLoginHelperTest.php b/tests/FacebookRedirectLoginHelperTest.php deleted file mode 100644 index cc45e5312..000000000 --- a/tests/FacebookRedirectLoginHelperTest.php +++ /dev/null @@ -1,71 +0,0 @@ -disableSessionStatusCheck(); - $loginUrl = $helper->getLoginUrl(); - $state = $_SESSION['FBRLH_state']; - $params = array( - 'client_id' => FacebookTestCredentials::$appId, - 'redirect_uri' => self::REDIRECT_URL, - 'state' => $state, - 'sdk' => 'php-sdk-' . FacebookRequest::VERSION, - 'scope' => implode(',', array()) - ); - $expectedUrl = 'https://www.facebook.com/v2.0/dialog/oauth?'; - $this->assertTrue(strpos($loginUrl, $expectedUrl) !== false); - foreach ($params as $key => $value) { - $this->assertTrue( - strpos($loginUrl, $key . '=' . urlencode($value)) !== false - ); - } - } - - public function testLogoutURL() - { - $helper = new FacebookRedirectLoginHelper( - self::REDIRECT_URL, - FacebookTestCredentials::$appId, - FacebookTestCredentials::$appSecret - ); - $helper->disableSessionStatusCheck(); - $logoutUrl = $helper->getLogoutUrl( - FacebookTestHelper::$testSession, self::REDIRECT_URL - ); - $params = array( - 'next' => self::REDIRECT_URL, - 'access_token' => FacebookTestHelper::$testSession->getToken() - ); - $expectedUrl = 'https://www.facebook.com/logout.php?'; - $this->assertTrue(strpos($logoutUrl, $expectedUrl) !== false); - foreach ($params as $key => $value) { - $this->assertTrue( - strpos($logoutUrl, $key . '=' . urlencode($value)) !== false - ); - } - } - - public function testCSPRNG() - { - $helper = new FacebookRedirectLoginHelper( - self::REDIRECT_URL, - FacebookTestCredentials::$appId, - FacebookTestCredentials::$appSecret - ); - $this->assertTrue(preg_match('/^([0-9a-f]+)$/', $helper->random(32))); - } - -} diff --git a/tests/FacebookRequestExceptionTest.php b/tests/FacebookRequestExceptionTest.php deleted file mode 100644 index a0ceeb169..000000000 --- a/tests/FacebookRequestExceptionTest.php +++ /dev/null @@ -1,266 +0,0 @@ - array( - 'code' => 100, - 'message' => 'errmsg', - 'error_subcode' => 0, - 'type' => 'exception' - ) - ); - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(100, $exception->getCode()); - $this->assertEquals(0, $exception->getSubErrorCode()); - $this->assertEquals('exception', $exception->getErrorType()); - $this->assertEquals('errmsg', $exception->getMessage()); - $this->assertEquals($json, $exception->getRawResponse()); - $this->assertEquals(401, $exception->getHttpStatusCode()); - - $params['error']['code'] = 102; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(102, $exception->getCode()); - - $params['error']['code'] = 190; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(190, $exception->getCode()); - - $params['error']['type'] = 'OAuthException'; - $params['error']['code'] = 0; - $params['error']['error_subcode'] = 458; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(458, $exception->getSubErrorCode()); - - $params['error']['error_subcode'] = 460; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(460, $exception->getSubErrorCode()); - - $params['error']['error_subcode'] = 463; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(463, $exception->getSubErrorCode()); - - $params['error']['error_subcode'] = 467; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(467, $exception->getSubErrorCode()); - - $params['error']['error_subcode'] = 0; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(0, $exception->getSubErrorCode()); - } - - public function testServerExceptions() - { - $params = array( - 'error' => array( - 'code' => 1, - 'message' => 'errmsg', - 'error_subcode' => 0, - 'type' => 'exception' - ) - ); - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 500); - $this->assertTrue($exception instanceof FacebookServerException); - $this->assertEquals(1, $exception->getCode()); - $this->assertEquals(0, $exception->getSubErrorCode()); - $this->assertEquals('exception', $exception->getErrorType()); - $this->assertEquals('errmsg', $exception->getMessage()); - $this->assertEquals($json, $exception->getRawResponse()); - $this->assertEquals(500, $exception->getHttpStatusCode()); - - $params['error']['code'] = 2; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookServerException); - $this->assertEquals(2, $exception->getCode()); - } - - public function testThrottleExceptions() - { - $params = array( - 'error' => array( - 'code' => 4, - 'message' => 'errmsg', - 'error_subcode' => 0, - 'type' => 'exception' - ) - ); - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookThrottleException); - $this->assertEquals(4, $exception->getCode()); - $this->assertEquals(0, $exception->getSubErrorCode()); - $this->assertEquals('exception', $exception->getErrorType()); - $this->assertEquals('errmsg', $exception->getMessage()); - $this->assertEquals($json, $exception->getRawResponse()); - $this->assertEquals(401, $exception->getHttpStatusCode()); - - $params['error']['code'] = 17; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookThrottleException); - $this->assertEquals(17, $exception->getCode()); - - $params['error']['code'] = 341; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookThrottleException); - $this->assertEquals(341, $exception->getCode()); - } - - public function testUserIssueExceptions() - { - $params = array( - 'error' => array( - 'code' => 230, - 'message' => 'errmsg', - 'error_subcode' => 459, - 'type' => 'exception' - ) - ); - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(230, $exception->getCode()); - $this->assertEquals(459, $exception->getSubErrorCode()); - $this->assertEquals('exception', $exception->getErrorType()); - $this->assertEquals('errmsg', $exception->getMessage()); - $this->assertEquals($json, $exception->getRawResponse()); - $this->assertEquals(401, $exception->getHttpStatusCode()); - - $params['error']['error_subcode'] = 464; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookAuthorizationException); - $this->assertEquals(464, $exception->getSubErrorCode()); - } - - public function testPermissionExceptions() - { - $params = array( - 'error' => array( - 'code' => 10, - 'message' => 'errmsg', - 'error_subcode' => 0, - 'type' => 'exception' - ) - ); - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookPermissionException); - $this->assertEquals(10, $exception->getCode()); - $this->assertEquals(0, $exception->getSubErrorCode()); - $this->assertEquals('exception', $exception->getErrorType()); - $this->assertEquals('errmsg', $exception->getMessage()); - $this->assertEquals($json, $exception->getRawResponse()); - $this->assertEquals(401, $exception->getHttpStatusCode()); - - $params['error']['code'] = 200; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookPermissionException); - $this->assertEquals(200, $exception->getCode()); - - $params['error']['code'] = 250; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookPermissionException); - $this->assertEquals(250, $exception->getCode()); - - $params['error']['code'] = 299; - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookPermissionException); - $this->assertEquals(299, $exception->getCode()); - } - - public function testClientExceptions() - { - $params = array( - 'error' => array( - 'code' => 506, - 'message' => 'errmsg', - 'error_subcode' => 0, - 'type' => 'exception' - ) - ); - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 401); - $this->assertTrue($exception instanceof FacebookClientException); - $this->assertEquals(506, $exception->getCode()); - $this->assertEquals(0, $exception->getSubErrorCode()); - $this->assertEquals('exception', $exception->getErrorType()); - $this->assertEquals('errmsg', $exception->getMessage()); - $this->assertEquals($json, $exception->getRawResponse()); - $this->assertEquals(401, $exception->getHttpStatusCode()); - } - - public function testOtherException() - { - $params = array( - 'error' => array( - 'code' => 42, - 'message' => 'ship love', - 'error_subcode' => 0, - 'type' => 'feature' - ) - ); - $json = json_encode($params); - $exception = FacebookRequestException::create($json, $params, 200); - $this->assertTrue($exception instanceof FacebookOtherException); - $this->assertEquals(42, $exception->getCode()); - $this->assertEquals(0, $exception->getSubErrorCode()); - $this->assertEquals('feature', $exception->getErrorType()); - $this->assertEquals('ship love', $exception->getMessage()); - $this->assertEquals($json, $exception->getRawResponse()); - $this->assertEquals(200, $exception->getHttpStatusCode()); - } - - public function testValidateThrowsException() - { - $bogusSession = new FacebookSession('invalid-token'); - $this->setExpectedException( - 'Facebook\\FacebookSDKException', 'Session has expired' - ); - $bogusSession->validate(); - } - - public function testInvalidCredentialsException() - { - $bogusSession = new FacebookSession('invalid-token'); - $this->setExpectedException( - 'Facebook\\FacebookAuthorizationException', 'Invalid OAuth access token' - ); - $bogusSession->validate('invalid-app-id', 'invalid-app-secret'); - } - -} diff --git a/tests/FacebookRequestTest.php b/tests/FacebookRequestTest.php old mode 100644 new mode 100755 index 07d9827a5..697fd94ca --- a/tests/FacebookRequestTest.php +++ b/tests/FacebookRequestTest.php @@ -1,140 +1,207 @@ execute()->getGraphObject(); - $this->assertNotNull($response->getProperty('id')); - $this->assertNotNull($response->getProperty('name')); - } - - public function testCanPostAndDelete() - { - // Create a test user - $params = array( - 'name' => 'Foo User', - ); - $response = ( - new FacebookRequest( - new FacebookSession(FacebookTestHelper::getAppToken()), - 'POST', - '/' . FacebookTestCredentials::$appId . '/accounts/test-users', - $params - ))->execute()->getGraphObject(); - $user_id = $response->getProperty('id'); - $this->assertNotNull($user_id); - - // Delete test user - $response = ( - new FacebookRequest( - new FacebookSession(FacebookTestHelper::getAppToken()), - 'DELETE', - '/' . $user_id - ))->execute()->getGraphObject()->asArray(); - $this->assertTrue($response); - } - - public function testETagHit() - { - $response = ( - new FacebookRequest( - FacebookTestHelper::$testSession, - 'GET', - '/104048449631599' - ))->execute(); - - $response = ( - new FacebookRequest( - FacebookTestHelper::$testSession, - 'GET', - '/104048449631599', - null, - null, - $response->getETag() - ))->execute(); - - $this->assertTrue($response->isETagHit()); - $this->assertNull($response->getETag()); - } - - public function testETagMiss() - { - $response = ( - new FacebookRequest( - FacebookTestHelper::$testSession, - 'GET', - '/104048449631599', - null, - null, - 'someRandomValue' - ))->execute(); - - $this->assertFalse($response->isETagHit()); - $this->assertNotNull($response->getETag()); - } - - public function testGracefullyHandlesUrlAppending() - { - $params = array(); - $url = 'https://www.foo.com/'; - $processed_url = FacebookRequest::appendParamsToUrl($url, $params); - $this->assertEquals('https://www.foo.com/', $processed_url); - - $params = array( - 'access_token' => 'foo', - ); - $url = 'https://www.foo.com/'; - $processed_url = FacebookRequest::appendParamsToUrl($url, $params); - $this->assertEquals('https://www.foo.com/?access_token=foo', $processed_url); - - $params = array( - 'access_token' => 'foo', - 'bar' => 'baz', - ); - $url = 'https://www.foo.com/?foo=bar'; - $processed_url = FacebookRequest::appendParamsToUrl($url, $params); - $this->assertEquals('https://www.foo.com/?access_token=foo&bar=baz&foo=bar', $processed_url); - - $params = array( - 'access_token' => 'foo', - ); - $url = 'https://www.foo.com/?foo=bar&access_token=bar'; - $processed_url = FacebookRequest::appendParamsToUrl($url, $params); - $this->assertEquals('https://www.foo.com/?access_token=bar&foo=bar', $processed_url); - } - - public function testAppSecretProof() - { - $enableAppSecretProof = FacebookSession::useAppSecretProof(); - - FacebookSession::enableAppSecretProof(true); - $request = new FacebookRequest( - FacebookTestHelper::$testSession, - 'GET', - '/me' - ); - $this->assertTrue(isset($request->getParameters()['appsecret_proof'])); - - - FacebookSession::enableAppSecretProof(false); - $request = new FacebookRequest( - FacebookTestHelper::$testSession, - 'GET', - '/me' - ); - $this->assertFalse(isset($request->getParameters()['appsecret_proof'])); - - FacebookSession::enableAppSecretProof($enableAppSecretProof); - } - -} \ No newline at end of file + public function testAnEmptyRequestEntityCanInstantiate() + { + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app); + + $this->assertInstanceOf('Facebook\FacebookRequest', $request); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAMissingAccessTokenWillThrow() + { + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app); + + $request->validateAccessToken(); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAMissingMethodWillThrow() + { + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app); + + $request->validateMethod(); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAnInvalidMethodWillThrow() + { + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app, 'foo_token', 'FOO'); + + $request->validateMethod(); + } + + public function testGetHeadersWillAutoAppendETag() + { + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app, null, 'GET', '/foo', [], 'fooETag'); + + $headers = $request->getHeaders(); + + $expectedHeaders = FacebookRequest::getDefaultHeaders(); + $expectedHeaders['If-None-Match'] = 'fooETag'; + + $this->assertEquals($expectedHeaders, $headers); + } + + public function testGetParamsWillAutoAppendAccessTokenAndAppSecretProof() + { + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app, 'foo_token', 'POST', '/foo', ['foo' => 'bar']); + + $params = $request->getParams(); + + $this->assertEquals([ + 'foo' => 'bar', + 'access_token' => 'foo_token', + 'appsecret_proof' => 'df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + ], $params); + } + + public function testAnAccessTokenCanBeSetFromTheParams() + { + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app, null, 'POST', '/me', ['access_token' => 'bar_token']); + + $accessToken = $request->getAccessToken(); + + $this->assertEquals('bar_token', $accessToken); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAccessTokenConflictsWillThrow() + { + $app = new FacebookApp('123', 'foo_secret'); + new FacebookRequest($app, 'foo_token', 'POST', '/me', ['access_token' => 'bar_token']); + } + + public function testAProperUrlWillBeGenerated() + { + $app = new FacebookApp('123', 'foo_secret'); + $getRequest = new FacebookRequest($app, 'foo_token', 'GET', '/foo', ['foo' => 'bar']); + + $getUrl = $getRequest->getUrl(); + $expectedParams = 'foo=bar&access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9'; + $expectedUrl = '/' . Facebook::DEFAULT_GRAPH_VERSION . '/foo?' . $expectedParams; + + $this->assertEquals($expectedUrl, $getUrl); + + $postRequest = new FacebookRequest($app, 'foo_token', 'POST', '/bar', ['foo' => 'bar']); + + $postUrl = $postRequest->getUrl(); + $expectedUrl = '/' . Facebook::DEFAULT_GRAPH_VERSION . '/bar'; + + $this->assertEquals($expectedUrl, $postUrl); + } + + public function testAuthenticationParamsAreStrippedAndReapplied() + { + $app = new FacebookApp('123', 'foo_secret'); + + $request = new FacebookRequest( + $app, + $accessToken = 'foo_token', + $method = 'GET', + $endpoint = '/foo', + $params = [ + 'access_token' => 'foo_token', + 'appsecret_proof' => 'bar_app_secret', + 'bar' => 'baz', + ] + ); + + $url = $request->getUrl(); + + $expectedParams = 'bar=baz&access_token=foo_token&appsecret_proof=df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9'; + $expectedUrl = '/' . Facebook::DEFAULT_GRAPH_VERSION . '/foo?' . $expectedParams; + $this->assertEquals($expectedUrl, $url); + + $params = $request->getParams(); + + $expectedParams = [ + 'access_token' => 'foo_token', + 'appsecret_proof' => 'df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', + 'bar' => 'baz', + ]; + $this->assertEquals($expectedParams, $params); + } + + public function testAFileCanBeAddedToParams() + { + $myFile = new FacebookFile(__DIR__ . '/foo.txt'); + $params = [ + 'name' => 'Foo Bar', + 'source' => $myFile, + ]; + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app, 'foo_token', 'POST', '/foo/photos', $params); + + $actualParams = $request->getParams(); + + $this->assertTrue($request->containsFileUploads()); + $this->assertFalse($request->containsVideoUploads()); + $this->assertTrue(!isset($actualParams['source'])); + $this->assertEquals('Foo Bar', $actualParams['name']); + } + + public function testAVideoCanBeAddedToParams() + { + $myFile = new FacebookVideo(__DIR__ . '/foo.txt'); + $params = [ + 'name' => 'Foo Bar', + 'source' => $myFile, + ]; + $app = new FacebookApp('123', 'foo_secret'); + $request = new FacebookRequest($app, 'foo_token', 'POST', '/foo/videos', $params); + + $actualParams = $request->getParams(); + + $this->assertTrue($request->containsFileUploads()); + $this->assertTrue($request->containsVideoUploads()); + $this->assertTrue(!isset($actualParams['source'])); + $this->assertEquals('Foo Bar', $actualParams['name']); + } +} diff --git a/tests/FacebookResponseTest.php b/tests/FacebookResponseTest.php new file mode 100755 index 000000000..be37cbde3 --- /dev/null +++ b/tests/FacebookResponseTest.php @@ -0,0 +1,121 @@ +request = new FacebookRequest( + $app, + 'foo_token', + 'GET', + '/me/photos?keep=me', + ['foo' => 'bar'], + 'foo_eTag', + 'v1337' + ); + } + + public function testAnETagCanBeProperlyAccessed() + { + $response = new FacebookResponse($this->request, '', 200, ['ETag' => 'foo_tag']); + + $eTag = $response->getETag(); + + $this->assertEquals('foo_tag', $eTag); + } + + public function testAProperAppSecretProofCanBeGenerated() + { + $response = new FacebookResponse($this->request); + + $appSecretProof = $response->getAppSecretProof(); + + $this->assertEquals('df4256903ba4e23636cc142117aa632133d75c642bd2a68955be1443bd14deb9', $appSecretProof); + } + + public function testASuccessfulJsonResponseWillBeDecodedToAGraphNode() + { + $graphResponseJson = '{"id":"123","name":"Foo"}'; + $response = new FacebookResponse($this->request, $graphResponseJson, 200); + + $decodedResponse = $response->getDecodedBody(); + $graphNode = $response->getGraphNode(); + + $this->assertFalse($response->isError(), 'Did not expect Response to return an error.'); + $this->assertEquals([ + 'id' => '123', + 'name' => 'Foo', + ], $decodedResponse); + $this->assertInstanceOf('Facebook\GraphNodes\GraphNode', $graphNode); + } + + public function testASuccessfulJsonResponseWillBeDecodedToAGraphEdge() + { + $graphResponseJson = '{"data":[{"id":"123","name":"Foo"},{"id":"1337","name":"Bar"}]}'; + $response = new FacebookResponse($this->request, $graphResponseJson, 200); + + $graphEdge = $response->getGraphEdge(); + + $this->assertFalse($response->isError(), 'Did not expect Response to return an error.'); + $this->assertInstanceOf('Facebook\GraphNodes\GraphNode', $graphEdge[0]); + $this->assertInstanceOf('Facebook\GraphNodes\GraphNode', $graphEdge[1]); + } + + public function testASuccessfulUrlEncodedKeyValuePairResponseWillBeDecoded() + { + $graphResponseKeyValuePairs = 'id=123&name=Foo'; + $response = new FacebookResponse($this->request, $graphResponseKeyValuePairs, 200); + + $decodedResponse = $response->getDecodedBody(); + + $this->assertFalse($response->isError(), 'Did not expect Response to return an error.'); + $this->assertEquals([ + 'id' => '123', + 'name' => 'Foo', + ], $decodedResponse); + } + + public function testErrorStatusCanBeCheckedWhenAnErrorResponseIsReturned() + { + $graphResponse = '{"error":{"message":"Foo error.","type":"OAuthException","code":190,"error_subcode":463}}'; + $response = new FacebookResponse($this->request, $graphResponse, 401); + + $exception = $response->getThrownException(); + + $this->assertTrue($response->isError(), 'Expected Response to return an error.'); + $this->assertInstanceOf('Facebook\Exceptions\FacebookResponseException', $exception); + } +} diff --git a/tests/FacebookSessionTest.php b/tests/FacebookSessionTest.php deleted file mode 100644 index 8f53866f1..000000000 --- a/tests/FacebookSessionTest.php +++ /dev/null @@ -1,84 +0,0 @@ -assertEquals( - FacebookTestHelper::getAppToken(), $session->getToken() - ); - } - - public function testGetSessionInfo() - { - $response = FacebookTestHelper::$testSession->getSessionInfo(); - $this->assertTrue($response instanceof GraphSessionInfo); - $this->assertNotNull($response->getAppId()); - $this->assertTrue($response->isValid()); - $scopes = $response->getPropertyAsArray('scopes'); - $this->assertTrue(is_array($scopes)); - $this->assertEquals(5, count($scopes)); - } - - public function testExtendAccessToken() - { - $response = FacebookTestHelper::$testSession->getLongLivedSession(); - $this->assertTrue($response instanceof FacebookSession); - $info = $response->getSessionInfo(); - $nextWeek = time() + (60 * 60 * 24 * 7); - $this->assertTrue( - $info->getProperty('expires_at') > $nextWeek - ); - } - - public function testSessionFromSignedRequest() - { - $signedRequest = m::mock('Facebook\Entities\SignedRequest'); - $signedRequest - ->shouldReceive('get') - ->with('code') - ->once() - ->andReturn(null); - $signedRequest - ->shouldReceive('get') - ->with('oauth_token') - ->once() - ->andReturn('foo_token'); - $signedRequest - ->shouldReceive('get') - ->with('expires', 0) - ->once() - ->andReturn(time() + (60 * 60 * 24)); - $signedRequest - ->shouldReceive('getUserId') - ->once() - ->andReturn('123'); - - $session = FacebookSession::newSessionFromSignedRequest($signedRequest); - $this->assertInstanceOf('Facebook\FacebookSession', $session); - $this->assertEquals('foo_token', $session->getToken()); - $this->assertEquals('123', $session->getUserId()); - } - - public function testAppSessionValidates() - { - $session = FacebookSession::newAppSession(); - try { - $session->validate(); - } catch (\Facebook\FacebookSDKException $ex) { - $this->fail('Exception thrown validating app session.'); - } - } - -} diff --git a/tests/FacebookSignedRequestFromInputHelperTest.php b/tests/FacebookSignedRequestFromInputHelperTest.php deleted file mode 100644 index 436888429..000000000 --- a/tests/FacebookSignedRequestFromInputHelperTest.php +++ /dev/null @@ -1,67 +0,0 @@ -helper = new FooSignedRequestHelper('123', 'foo_app_secret'); - } - - public function testSignedRequestDataCanBeRetrievedFromGetData() - { - $_GET['signed_request'] = 'foo_signed_request'; - - $rawSignedRequest = $this->helper->getRawSignedRequestFromGet(); - - $this->assertEquals('foo_signed_request', $rawSignedRequest); - } - - public function testSignedRequestDataCanBeRetrievedFromPostData() - { - $_POST['signed_request'] = 'foo_signed_request'; - - $rawSignedRequest = $this->helper->getRawSignedRequestFromPost(); - - $this->assertEquals('foo_signed_request', $rawSignedRequest); - } - - public function testSignedRequestDataCanBeRetrievedFromCookieData() - { - $_COOKIE['fbsr_123'] = 'foo_signed_request'; - - $rawSignedRequest = $this->helper->getRawSignedRequestFromCookie(); - - $this->assertEquals('foo_signed_request', $rawSignedRequest); - } - - public function testSessionWillBeNullWhenAUserHasNotYetAuthorizedTheApp() - { - $this->helper->instantiateSignedRequest($this->rawSignedRequestUnauthorized); - $session = $this->helper->getSession(); - - $this->assertNull($session); - } - - public function testAFacebookSessionCanBeInstantiatedWhenAUserHasAuthorizedTheApp() - { - $this->helper->instantiateSignedRequest($this->rawSignedRequestAuthorized); - $session = $this->helper->getSession(); - - $this->assertInstanceOf('Facebook\FacebookSession', $session); - $this->assertEquals('foo_token', $session->getToken()); - } - -} diff --git a/tests/FacebookTest.php b/tests/FacebookTest.php new file mode 100644 index 000000000..035e8d70f --- /dev/null +++ b/tests/FacebookTest.php @@ -0,0 +1,419 @@ + '1337', + 'app_secret' => 'foo_secret', + ]; + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testInstantiatingWithoutAppIdThrows() + { + // unset value so there is no fallback to test expected Exception + putenv(Facebook::APP_ID_ENV_NAME.'='); + $config = [ + 'app_secret' => 'foo_secret', + ]; + new Facebook($config); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testInstantiatingWithoutAppSecretThrows() + { + // unset value so there is no fallback to test expected Exception + putenv(Facebook::APP_SECRET_ENV_NAME.'='); + $config = [ + 'app_id' => 'foo_id', + ]; + new Facebook($config); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testSettingAnInvalidHttpClientHandlerThrows() + { + $config = array_merge($this->config, [ + 'http_client_handler' => 'foo_handler', + ]); + new Facebook($config); + } + + public function testCurlHttpClientHandlerCanBeForced() + { + if (!extension_loaded('curl')) { + $this->markTestSkipped('cURL must be installed to test cURL client handler.'); + } + $config = array_merge($this->config, [ + 'http_client_handler' => 'curl' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\HttpClients\FacebookCurlHttpClient', + $fb->getClient()->getHttpClientHandler() + ); + } + + public function testStreamHttpClientHandlerCanBeForced() + { + $config = array_merge($this->config, [ + 'http_client_handler' => 'stream' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\HttpClients\FacebookStreamHttpClient', + $fb->getClient()->getHttpClientHandler() + ); + } + + public function testGuzzleHttpClientHandlerCanBeForced() + { + $config = array_merge($this->config, [ + 'http_client_handler' => 'guzzle' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\HttpClients\FacebookGuzzleHttpClient', + $fb->getClient()->getHttpClientHandler() + ); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testSettingAnInvalidPersistentDataHandlerThrows() + { + $config = array_merge($this->config, [ + 'persistent_data_handler' => 'foo_handler', + ]); + new Facebook($config); + } + + public function testPersistentDataHandlerCanBeForced() + { + $config = array_merge($this->config, [ + 'persistent_data_handler' => 'memory' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\PersistentData\FacebookMemoryPersistentDataHandler', + $fb->getRedirectLoginHelper()->getPersistentDataHandler() + ); + } + + public function testSettingAnInvalidUrlHandlerThrows() + { + $expectedException = (PHP_MAJOR_VERSION > 5 && class_exists('TypeError')) + ? 'TypeError' + : 'PHPUnit_Framework_Error'; + + $this->setExpectedException($expectedException); + + $config = array_merge($this->config, [ + 'url_detection_handler' => 'foo_handler', + ]); + new Facebook($config); + } + + public function testTheUrlHandlerWillDefaultToTheFacebookImplementation() + { + $fb = new Facebook($this->config); + $this->assertInstanceOf('Facebook\Url\FacebookUrlDetectionHandler', $fb->getUrlDetectionHandler()); + } + + public function testAnAccessTokenCanBeSetAsAString() + { + $fb = new Facebook($this->config); + $fb->setDefaultAccessToken('foo_token'); + $accessToken = $fb->getDefaultAccessToken(); + + $this->assertInstanceOf('Facebook\Authentication\AccessToken', $accessToken); + $this->assertEquals('foo_token', (string)$accessToken); + } + + public function testAnAccessTokenCanBeSetAsAnAccessTokenEntity() + { + $fb = new Facebook($this->config); + $fb->setDefaultAccessToken(new AccessToken('bar_token')); + $accessToken = $fb->getDefaultAccessToken(); + + $this->assertInstanceOf('Facebook\Authentication\AccessToken', $accessToken); + $this->assertEquals('bar_token', (string)$accessToken); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testSettingAnInvalidPseudoRandomStringGeneratorThrows() + { + $config = array_merge($this->config, [ + 'pseudo_random_string_generator' => 'foo_generator', + ]); + new Facebook($config); + } + + public function testRandomBytesCsprgCanBeForced() + { + if (!function_exists('random_bytes')) { + $this->markTestSkipped( + 'Must have PHP 7 or paragonie/random_compat installed to test random_bytes().' + ); + } + + $config = array_merge($this->config, [ + 'persistent_data_handler' => 'memory', // To keep session errors from happening + 'pseudo_random_string_generator' => 'random_bytes' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\PseudoRandomString\RandomBytesPseudoRandomStringGenerator', + $fb->getRedirectLoginHelper()->getPseudoRandomStringGenerator() + ); + } + + public function testMcryptCsprgCanBeForced() + { + if (!function_exists('mcrypt_create_iv')) { + $this->markTestSkipped( + 'Mcrypt must be installed to test mcrypt_create_iv().' + ); + } + + $config = array_merge($this->config, [ + 'persistent_data_handler' => 'memory', // To keep session errors from happening + 'pseudo_random_string_generator' => 'mcrypt' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\PseudoRandomString\McryptPseudoRandomStringGenerator', + $fb->getRedirectLoginHelper()->getPseudoRandomStringGenerator() + ); + } + + public function testOpenSslCsprgCanBeForced() + { + if (!function_exists('openssl_random_pseudo_bytes')) { + $this->markTestSkipped( + 'The OpenSSL extension must be enabled to test openssl_random_pseudo_bytes().' + ); + } + + $config = array_merge($this->config, [ + 'persistent_data_handler' => 'memory', // To keep session errors from happening + 'pseudo_random_string_generator' => 'openssl' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\PseudoRandomString\OpenSslPseudoRandomStringGenerator', + $fb->getRedirectLoginHelper()->getPseudoRandomStringGenerator() + ); + } + + public function testUrandomCsprgCanBeForced() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped( + 'Cannot test /dev/urandom generator due to open_basedir constraint.' + ); + } + + if (!is_readable('/dev/urandom')) { + $this->markTestSkipped( + '/dev/urandom not found or is not readable.' + ); + } + + $config = array_merge($this->config, [ + 'persistent_data_handler' => 'memory', // To keep session errors from happening + 'pseudo_random_string_generator' => 'urandom' + ]); + $fb = new Facebook($config); + $this->assertInstanceOf( + 'Facebook\PseudoRandomString\UrandomPseudoRandomStringGenerator', + $fb->getRedirectLoginHelper()->getPseudoRandomStringGenerator() + ); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testSettingAnAccessThatIsNotStringOrAccessTokenThrows() + { + $config = array_merge($this->config, [ + 'default_access_token' => 123, + ]); + new Facebook($config); + } + + public function testCreatingANewRequestWillDefaultToTheProperConfig() + { + $config = array_merge($this->config, [ + 'default_access_token' => 'foo_token', + 'enable_beta_mode' => true, + 'default_graph_version' => 'v1337', + ]); + $fb = new Facebook($config); + + $request = $fb->request('FOO_VERB', '/foo'); + $this->assertEquals('1337', $request->getApp()->getId()); + $this->assertEquals('foo_secret', $request->getApp()->getSecret()); + $this->assertEquals('foo_token', (string)$request->getAccessToken()); + $this->assertEquals('v1337', $request->getGraphVersion()); + $this->assertEquals( + FacebookClient::BASE_GRAPH_URL_BETA, + $fb->getClient()->getBaseGraphUrl() + ); + } + + public function testCreatingANewBatchRequestWillDefaultToTheProperConfig() + { + $config = array_merge($this->config, [ + 'default_access_token' => 'foo_token', + 'enable_beta_mode' => true, + 'default_graph_version' => 'v1337', + ]); + $fb = new Facebook($config); + + $batchRequest = $fb->newBatchRequest(); + $this->assertEquals('1337', $batchRequest->getApp()->getId()); + $this->assertEquals('foo_secret', $batchRequest->getApp()->getSecret()); + $this->assertEquals('foo_token', (string)$batchRequest->getAccessToken()); + $this->assertEquals('v1337', $batchRequest->getGraphVersion()); + $this->assertEquals( + FacebookClient::BASE_GRAPH_URL_BETA, + $fb->getClient()->getBaseGraphUrl() + ); + $this->assertInstanceOf('Facebook\FacebookBatchRequest', $batchRequest); + $this->assertEquals(0, count($batchRequest->getRequests())); + } + + public function testCanInjectCustomHandlers() + { + $config = array_merge($this->config, [ + 'http_client_handler' => new FooClientInterface(), + 'persistent_data_handler' => new FooPersistentDataInterface(), + 'url_detection_handler' => new FooUrlDetectionInterface(), + 'pseudo_random_string_generator' => new FooBarPseudoRandomStringGenerator(), + ]); + $fb = new Facebook($config); + + $this->assertInstanceOf( + 'Facebook\Tests\Fixtures\FooClientInterface', + $fb->getClient()->getHttpClientHandler() + ); + $this->assertInstanceOf( + 'Facebook\Tests\Fixtures\FooPersistentDataInterface', + $fb->getRedirectLoginHelper()->getPersistentDataHandler() + ); + $this->assertInstanceOf( + 'Facebook\Tests\Fixtures\FooUrlDetectionInterface', + $fb->getRedirectLoginHelper()->getUrlDetectionHandler() + ); + $this->assertInstanceOf( + 'Facebook\Tests\Fixtures\FooBarPseudoRandomStringGenerator', + $fb->getRedirectLoginHelper()->getPseudoRandomStringGenerator() + ); + } + + public function testPaginationReturnsProperResponse() + { + $config = array_merge($this->config, [ + 'http_client_handler' => new FooClientInterface(), + ]); + $fb = new Facebook($config); + + $request = new FacebookRequest($fb->getApp(), 'foo_token', 'GET'); + $graphEdge = new GraphEdge( + $request, + [], + [ + 'paging' => [ + 'cursors' => [ + 'after' => 'bar_after_cursor', + 'before' => 'bar_before_cursor', + ], + 'previous' => 'previous_url', + 'next' => 'next_url', + ] + ], + '/1337/photos', + '\Facebook\GraphNodes\GraphUser' + ); + + $nextPage = $fb->next($graphEdge); + $this->assertInstanceOf('Facebook\GraphNodes\GraphEdge', $nextPage); + $this->assertInstanceOf('Facebook\GraphNodes\GraphUser', $nextPage[0]); + $this->assertEquals('Foo', $nextPage[0]['name']); + + $lastResponse = $fb->getLastResponse(); + $this->assertInstanceOf('Facebook\FacebookResponse', $lastResponse); + $this->assertEquals(1337, $lastResponse->getHttpStatusCode()); + } + + public function testCanGetSuccessfulTransferWithMaxTries() + { + $config = array_merge($this->config, [ + 'http_client_handler' => new FakeGraphApiForResumableUpload(), + ]); + $fb = new Facebook($config); + $response = $fb->uploadVideo('me', __DIR__.'/foo.txt', [], 'foo-token', 3); + $this->assertEquals([ + 'video_id' => '1337', + 'success' => true, + ], $response); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookResponseException + */ + public function testMaxingOutRetriesWillThrow() + { + $client = new FakeGraphApiForResumableUpload(); + $client->failOnTransfer(); + + $config = array_merge($this->config, [ + 'http_client_handler' => $client, + ]); + $fb = new Facebook($config); + $fb->uploadVideo('4', __DIR__.'/foo.txt', [], 'foo-token', 3); + } +} diff --git a/tests/FacebookTestCredentials.php.dist b/tests/FacebookTestCredentials.php.dist index aa3d6b306..4d2878a47 100644 --- a/tests/FacebookTestCredentials.php.dist +++ b/tests/FacebookTestCredentials.php.dist @@ -1,4 +1,27 @@ true, - 'name' => 'Foo Phpunit User', - 'locale' => 'en_US', - 'permissions' => implode(',', static::$testUserPermissions), - ); - - $request = new FacebookRequest(static::getAppSession(), 'POST', $testUserPath, $params); - $response = $request->execute()->getGraphObject(); - - static::$testUserId = $response->getProperty('id'); - static::$testUserAccessToken = $response->getProperty('access_token'); - } - - public static function getAppSession() - { - return new FacebookSession(static::getAppToken()); - } - - public static function getAppToken() - { - return FacebookTestCredentials::$appId . '|' . FacebookTestCredentials::$appSecret; - } - - public static function deleteTestUser() - { - if (!static::$testUserId) { - return; - } - $testUserPath = '/' . static::$testUserId; - $request = new FacebookRequest(static::getAppSession(), 'DELETE', $testUserPath); - $request->execute(); - } - -} diff --git a/tests/FileUpload/FacebookFileTest.php b/tests/FileUpload/FacebookFileTest.php new file mode 100644 index 000000000..8ef741708 --- /dev/null +++ b/tests/FileUpload/FacebookFileTest.php @@ -0,0 +1,60 @@ +testFile = __DIR__ . '/../foo.txt'; + } + + public function testCanOpenAndReadAndCloseAFile() + { + $file = new FacebookFile($this->testFile); + $fileContents = $file->getContents(); + + $this->assertEquals('This is a text file used for testing. Let\'s dance.', $fileContents); + } + + public function testPartialFilesCanBeCreated() + { + $file = new FacebookFile($this->testFile, 14, 5); + $fileContents = $file->getContents(); + + $this->assertEquals('is a text file', $fileContents); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testTryingToOpenAFileThatDoesntExistsThrows() + { + new FacebookFile('does_not_exist.file'); + } +} diff --git a/tests/FileUpload/FacebookResumableUploaderTest.php b/tests/FileUpload/FacebookResumableUploaderTest.php new file mode 100644 index 000000000..dfdbe6f41 --- /dev/null +++ b/tests/FileUpload/FacebookResumableUploaderTest.php @@ -0,0 +1,112 @@ +fbApp = new FacebookApp('app_id', 'app_secret'); + $this->graphApi = new FakeGraphApiForResumableUpload(); + $this->client = new FacebookClient($this->graphApi); + $this->file = new FacebookFile(__DIR__.'/../foo.txt'); + } + + public function testResumableUploadCanStartTransferAndFinish() + { + $uploader = new FacebookResumableUploader($this->fbApp, $this->client, 'access_token', 'v2.4'); + $endpoint = '/me/videos'; + $chunk = $uploader->start($endpoint, $this->file); + $this->assertInstanceOf('Facebook\FileUpload\FacebookTransferChunk', $chunk); + $this->assertEquals('42', $chunk->getUploadSessionId()); + $this->assertEquals('1337', $chunk->getVideoId()); + + $newChunk = $uploader->transfer($endpoint, $chunk); + $this->assertEquals(20, $newChunk->getStartOffset()); + $this->assertNotSame($newChunk, $chunk); + + $finalResponse = $uploader->finish($endpoint, $chunk->getUploadSessionId(), []); + $this->assertTrue($finalResponse); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookResponseException + */ + public function testStartWillLetErrorResponsesThrow() + { + $this->graphApi->failOnStart(); + $uploader = new FacebookResumableUploader($this->fbApp, $this->client, 'access_token', 'v2.4'); + + $uploader->start('/me/videos', $this->file); + } + + public function testFailedResumableTransferWillNotThrowAndReturnSameChunk() + { + $this->graphApi->failOnTransfer(); + $uploader = new FacebookResumableUploader($this->fbApp, $this->client, 'access_token', 'v2.4'); + + $chunk = new FacebookTransferChunk($this->file, '1', '2', '3', '4'); + $newChunk = $uploader->transfer('/me/videos', $chunk); + $this->assertSame($newChunk, $chunk); + } + + public function testFailedResumableTransferWillNotThrowAndReturnNewChunk() + { + $this->graphApi->failOnTransferAndUploadNewChunk(); + $uploader = new FacebookResumableUploader($this->fbApp, $this->client, 'access_token', 'v2.4'); + + $chunk = new FacebookTransferChunk($this->file, '1', '2', '3', '4'); + $newChunk = $uploader->transfer('/me/videos', $chunk); + $this->assertEquals(40, $newChunk->getStartOffset()); + $this->assertEquals(50, $newChunk->getEndOffset()); + } +} diff --git a/tests/FileUpload/MimetypesTest.php b/tests/FileUpload/MimetypesTest.php new file mode 100644 index 000000000..14fc2b9dd --- /dev/null +++ b/tests/FileUpload/MimetypesTest.php @@ -0,0 +1,55 @@ +assertEquals('text/x-php', Mimetypes::getInstance()->fromExtension('php')); + } + + public function testGetsFromFilename() + { + $this->assertEquals('text/x-php', Mimetypes::getInstance()->fromFilename(__FILE__)); + } + + public function testGetsFromCaseInsensitiveFilename() + { + $this->assertEquals('text/x-php', Mimetypes::getInstance()->fromFilename(strtoupper(__FILE__))); + } + + public function testReturnsNullWhenNoMatchFound() + { + $this->assertNull(Mimetypes::getInstance()->fromExtension('foobar')); + } +} diff --git a/tests/Fixtures/FakeGraphApiForResumableUpload.php b/tests/Fixtures/FakeGraphApiForResumableUpload.php new file mode 100644 index 000000000..29f56446c --- /dev/null +++ b/tests/Fixtures/FakeGraphApiForResumableUpload.php @@ -0,0 +1,125 @@ +respondWith = 'FAIL_ON_START'; + } + + public function failOnTransfer() + { + $this->respondWith = 'FAIL_ON_TRANSFER'; + } + + public function failOnTransferAndUploadNewChunk() + { + $this->respondWith = 'FAIL_ON_TRANSFER_AND_UPLOAD_NEW_CHUNK'; + } + + public function send($url, $method, $body, array $headers, $timeOut) + { + // Could be start, transfer or finish + if (strpos($body, 'transfer') !== false) { + return $this->respondTransfer(); + } elseif (strpos($body, 'finish') !== false) { + return $this->respondFinish(); + } + + return $this->respondStart(); + } + + private function respondStart() + { + if ($this->respondWith == 'FAIL_ON_START') { + return new GraphRawResponse( + "HTTP/1.1 500 OK\r\nFoo: Bar", + '{"error":{"message":"Error validating access token: Session has expired on Monday, ' . + '10-Aug-15 01:00:00 PDT. The current time is Monday, 10-Aug-15 01:14:23 PDT.",' . + '"type":"OAuthException","code":190,"error_subcode":463}}' + ); + } + + return new GraphRawResponse( + "HTTP/1.1 200 OK\r\nFoo: Bar", + '{"video_id":"1337","start_offset":"0","end_offset":"20","upload_session_id":"42"}' + ); + } + + private function respondTransfer() + { + if ($this->respondWith == 'FAIL_ON_TRANSFER') { + return new GraphRawResponse( + "HTTP/1.1 500 OK\r\nFoo: Bar", + '{"error":{"message":"There was a problem uploading your video. Please try uploading it again.",' . + '"type":"FacebookApiException","code":6000,"error_subcode":1363019}}' + ); + } + + if ($this->respondWith == 'FAIL_ON_TRANSFER_AND_UPLOAD_NEW_CHUNK') { + return new GraphRawResponse( + "HTTP/1.1 500 OK\r\nFoo: Bar", + '{"error":{"message":"There was a problem uploading your video. Please try uploading it again.",' . + '"type":"OAuthException","code":6001,"error_subcode":1363037,' . + '"error_data":{"start_offset":40,"end_offset":50}}}' + ); + } + + switch ($this->transferCount) { + case 0: + $data = ['start_offset' => 20, 'end_offset' => 40]; + break; + case 1: + $data = ['start_offset' => 40, 'end_offset' => 50]; + break; + default: + $data = ['start_offset' => 50, 'end_offset' => 50]; + break; + } + + $this->transferCount++; + + return new GraphRawResponse( + "HTTP/1.1 200 OK\r\nFoo: Bar", + json_encode($data) + ); + } + + private function respondFinish() + { + return new GraphRawResponse( + "HTTP/1.1 200 OK\r\nFoo: Bar", + '{"success":true}' + ); + } +} diff --git a/tests/Fixtures/FooBarPseudoRandomStringGenerator.php b/tests/Fixtures/FooBarPseudoRandomStringGenerator.php new file mode 100644 index 000000000..17448b6eb --- /dev/null +++ b/tests/Fixtures/FooBarPseudoRandomStringGenerator.php @@ -0,0 +1,34 @@ +getParams(); + $rawResponse = json_encode([ + 'access_token' => 'foo_access_token_from:' . $params['code'], + ]); + + return new FacebookResponse($request, $rawResponse, 200); + } +} diff --git a/tests/Fixtures/FooUrlDetectionInterface.php b/tests/Fixtures/FooUrlDetectionInterface.php new file mode 100644 index 000000000..8ee70c303 --- /dev/null +++ b/tests/Fixtures/FooUrlDetectionInterface.php @@ -0,0 +1,34 @@ + '\Facebook\Tests\Fixtures\MyFooSubClassGraphNode', + ]; +} diff --git a/tests/Fixtures/MyFooSubClassGraphNode.php b/tests/Fixtures/MyFooSubClassGraphNode.php new file mode 100644 index 000000000..d03b308b8 --- /dev/null +++ b/tests/Fixtures/MyFooSubClassGraphNode.php @@ -0,0 +1,30 @@ + self::ALBUM_NAME, - 'message' => self::ALBUM_DESCRIPTION, - 'value' => 'everyone' - ) - ))->execute()->getGraphObject(); - - $albumId = $response->getProperty('id'); - - $response = ( - new FacebookRequest( - FacebookTestHelper::$testSession, - 'GET', - '/'.$albumId - ))->execute()->getGraphObject(GraphAlbum::className()); - - $this->assertTrue($response instanceof GraphAlbum); - $this->assertEquals($albumId, $response->getId()); - $this->assertTrue($response->getFrom() instanceof \Facebook\GraphUser); - $this->assertTrue($response->canUpload()); - $this->assertEquals(0, $response->getCount()); - $this->assertEquals(self::ALBUM_NAME, $response->getName()); - $this->assertEquals(self::ALBUM_DESCRIPTION, $response->getDescription()); - $this->assertNotNull($response->getLink()); - $this->assertNotNull($response->getPrivacy()); - - $type = array("profile", "mobile", "wall", "normal", "album"); - $this->assertTrue(in_array($response->getType(),$type)); - - date_default_timezone_set('GMT'); - $this->assertTrue($response->getCreatedTime() instanceof DateTime); - $this->assertTrue($response->getUpdatedTime() instanceof DateTime); - } - -} diff --git a/tests/GraphLocationTest.php b/tests/GraphLocationTest.php deleted file mode 100644 index 483b755b4..000000000 --- a/tests/GraphLocationTest.php +++ /dev/null @@ -1,25 +0,0 @@ -execute()->getGraphObject(); - $this->assertTrue($response instanceof GraphObject); - - $location = $response->getProperty('location', GraphLocation::className()); - $this->assertTrue(is_float($location->getLatitude())); - $this->assertTrue(is_float($location->getLongitude())); - } - -} diff --git a/tests/GraphNodes/AbstractGraphNode.php b/tests/GraphNodes/AbstractGraphNode.php new file mode 100644 index 000000000..b4f0e909b --- /dev/null +++ b/tests/GraphNodes/AbstractGraphNode.php @@ -0,0 +1,51 @@ +responseMock = m::mock('\Facebook\FacebookResponse'); + } + + protected function makeFactoryWithData($data) + { + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($data); + + return new GraphNodeFactory($this->responseMock); + } +} diff --git a/tests/GraphNodes/CollectionTest.php b/tests/GraphNodes/CollectionTest.php new file mode 100755 index 000000000..14af47684 --- /dev/null +++ b/tests/GraphNodes/CollectionTest.php @@ -0,0 +1,139 @@ + 'bar']); + + $field = $graphNode->getField('foo'); + $this->assertEquals('bar', $field); + + // @todo v6: Remove this assertion + $property = $graphNode->getProperty('foo'); + $this->assertEquals('bar', $property); + } + + public function testAMissingPropertyWillReturnNull() + { + $graphNode = new Collection(['foo' => 'bar']); + $field = $graphNode->getField('baz'); + + $this->assertNull($field, 'Expected the property to return null.'); + } + + public function testAMissingPropertyWillReturnTheDefault() + { + $graphNode = new Collection(['foo' => 'bar']); + + $field = $graphNode->getField('baz', 'faz'); + $this->assertEquals('faz', $field); + + // @todo v6: Remove this assertion + $property = $graphNode->getProperty('baz', 'faz'); + $this->assertEquals('faz', $property); + } + + public function testFalseDefaultsWillReturnSameType() + { + $graphNode = new Collection(['foo' => 'bar']); + + $field = $graphNode->getField('baz', ''); + $this->assertSame('', $field); + + $field = $graphNode->getField('baz', 0); + $this->assertSame(0, $field); + + $field = $graphNode->getField('baz', false); + $this->assertSame(false, $field); + } + + public function testTheKeysFromTheCollectionCanBeReturned() + { + $graphNode = new Collection([ + 'key1' => 'foo', + 'key2' => 'bar', + 'key3' => 'baz', + ]); + + $fieldNames = $graphNode->getFieldNames(); + $this->assertEquals(['key1', 'key2', 'key3'], $fieldNames); + + // @todo v6: Remove this assertion + $propertyNames = $graphNode->getPropertyNames(); + $this->assertEquals(['key1', 'key2', 'key3'], $propertyNames); + } + + public function testAnArrayCanBeInjectedViaTheConstructor() + { + $collection = new Collection(['foo', 'bar']); + $this->assertEquals(['foo', 'bar'], $collection->asArray()); + } + + public function testACollectionCanBeConvertedToProperJson() + { + $collection = new Collection(['foo', 'bar', 123]); + + $collectionAsString = $collection->asJson(); + + $this->assertEquals('["foo","bar",123]', $collectionAsString); + } + + public function testACollectionCanBeCounted() + { + $collection = new Collection(['foo', 'bar', 'baz']); + + $collectionCount = count($collection); + + $this->assertEquals(3, $collectionCount); + } + + public function testACollectionCanBeAccessedAsAnArray() + { + $collection = new Collection(['foo' => 'bar', 'faz' => 'baz']); + + $this->assertEquals('bar', $collection['foo']); + $this->assertEquals('baz', $collection['faz']); + } + + public function testACollectionCanBeIteratedOver() + { + $collection = new Collection(['foo' => 'bar', 'faz' => 'baz']); + + $this->assertInstanceOf('IteratorAggregate', $collection); + + $newArray = []; + + foreach ($collection as $k => $v) { + $newArray[$k] = $v; + } + + $this->assertEquals(['foo' => 'bar', 'faz' => 'baz'], $newArray); + } +} diff --git a/tests/GraphNodes/GraphAchievementTest.php b/tests/GraphNodes/GraphAchievementTest.php new file mode 100644 index 000000000..5be1140fd --- /dev/null +++ b/tests/GraphNodes/GraphAchievementTest.php @@ -0,0 +1,117 @@ + '1337' + ]; + + $factory = $this->makeFactoryWithData($dataFromGraph); + $graphNode = $factory->makeGraphAchievement(); + + $id = $graphNode->getId(); + + $this->assertEquals($dataFromGraph['id'], $id); + } + + public function testTypeIsAlwaysString() + { + $dataFromGraph = [ + 'id' => '1337' + ]; + + $factory = $this->makeFactoryWithData($dataFromGraph); + $graphNode = $factory->makeGraphAchievement(); + + $type = $graphNode->getType(); + + $this->assertEquals('game.achievement', $type); + } + + public function testNoFeedStoryIsBoolean() + { + $dataFromGraph = [ + 'no_feed_story' => (rand(0, 1) == 1) + ]; + + $factory = $this->makeFactoryWithData($dataFromGraph); + $graphNode = $factory->makeGraphAchievement(); + + $isNoFeedStory = $graphNode->isNoFeedStory(); + + $this->assertTrue(is_bool($isNoFeedStory)); + } + + public function testDatesGetCastToDateTime() + { + $dataFromGraph = [ + 'publish_time' => '2014-07-15T03:54:34+0000' + ]; + + $factory = $this->makeFactoryWithData($dataFromGraph); + $graphNode = $factory->makeGraphAchievement(); + + $publishTime = $graphNode->getPublishTime(); + + $this->assertInstanceOf('DateTime', $publishTime); + } + + public function testFromGetsCastAsGraphUser() + { + $dataFromGraph = [ + 'from' => [ + 'id' => '1337', + 'name' => 'Foo McBar' + ] + ]; + + $factory = $this->makeFactoryWithData($dataFromGraph); + $graphNode = $factory->makeGraphAchievement(); + + $from = $graphNode->getFrom(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphUser', $from); + } + + public function testApplicationGetsCastAsGraphApplication() + { + $dataFromGraph = [ + 'application' => [ + 'id' => '1337' + ] + ]; + + $factory = $this->makeFactoryWithData($dataFromGraph); + $graphNode = $factory->makeGraphAchievement(); + + $app = $graphNode->getApplication(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphApplication', $app); + } +} diff --git a/tests/GraphNodes/GraphAlbumTest.php b/tests/GraphNodes/GraphAlbumTest.php new file mode 100644 index 000000000..0c4eab5ef --- /dev/null +++ b/tests/GraphNodes/GraphAlbumTest.php @@ -0,0 +1,109 @@ +responseMock = m::mock('\\Facebook\\FacebookResponse'); + } + + public function testDatesGetCastToDateTime() + { + $dataFromGraph = [ + 'created_time' => '2014-07-15T03:54:34+0000', + 'updated_time' => '2014-07-12T01:24:09+0000', + 'id' => '123', + 'name' => 'Bar', + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphAlbum(); + + $createdTime = $graphNode->getCreatedTime(); + $updatedTime = $graphNode->getUpdatedTime(); + + $this->assertInstanceOf('DateTime', $createdTime); + $this->assertInstanceOf('DateTime', $updatedTime); + } + + public function testFromGetsCastAsGraphUser() + { + $dataFromGraph = [ + 'id' => '123', + 'from' => [ + 'id' => '1337', + 'name' => 'Foo McBar', + ], + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphAlbum(); + + $from = $graphNode->getFrom(); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphUser', $from); + } + + public function testPlacePropertyWillGetCastAsGraphPageObject() + { + $dataFromGraph = [ + 'id' => '123', + 'name' => 'Foo Album', + 'place' => [ + 'id' => '1', + 'name' => 'For Bar Place', + ] + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphAlbum(); + + $place = $graphNode->getPlace(); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphPage', $place); + } +} diff --git a/tests/GraphNodes/GraphEdgeTest.php b/tests/GraphNodes/GraphEdgeTest.php new file mode 100644 index 000000000..3afaf9cfb --- /dev/null +++ b/tests/GraphNodes/GraphEdgeTest.php @@ -0,0 +1,130 @@ + 'https://graph.facebook.com/v7.12/998899/photos?pretty=0&limit=25&after=foo_after_cursor', + 'previous' => 'https://graph.facebook.com/v7.12/998899/photos?pretty=0&limit=25&before=foo_before_cursor', + ]; + + protected function setUp() + { + $app = new FacebookApp('123', 'foo_app_secret'); + $this->request = new FacebookRequest( + $app, + 'foo_token', + 'GET', + '/me/photos?keep=me', + ['foo' => 'bar'], + 'foo_eTag', + 'v1337' + ); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testNonGetRequestsWillThrow() + { + $this->request->setMethod('POST'); + $graphEdge = new GraphEdge($this->request); + $graphEdge->validateForPagination(); + } + + public function testCanReturnGraphGeneratedPaginationEndpoints() + { + $graphEdge = new GraphEdge( + $this->request, + [], + ['paging' => $this->pagination] + ); + $nextPage = $graphEdge->getPaginationUrl('next'); + $prevPage = $graphEdge->getPaginationUrl('previous'); + + $this->assertEquals('/998899/photos?pretty=0&limit=25&after=foo_after_cursor', $nextPage); + $this->assertEquals('/998899/photos?pretty=0&limit=25&before=foo_before_cursor', $prevPage); + } + + public function testCanInstantiateNewPaginationRequest() + { + $graphEdge = new GraphEdge( + $this->request, + [], + ['paging' => $this->pagination], + '/1234567890/likes' + ); + $nextPage = $graphEdge->getNextPageRequest(); + $prevPage = $graphEdge->getPreviousPageRequest(); + + $this->assertInstanceOf('Facebook\FacebookRequest', $nextPage); + $this->assertInstanceOf('Facebook\FacebookRequest', $prevPage); + $this->assertNotSame($this->request, $nextPage); + $this->assertNotSame($this->request, $prevPage); + $this->assertEquals('/v1337/998899/photos?access_token=foo_token&after=foo_after_cursor&appsecret_proof=857d5f035a894f16b4180f19966e055cdeab92d4d53017b13dccd6d43b6497af&foo=bar&limit=25&pretty=0', $nextPage->getUrl()); + $this->assertEquals('/v1337/998899/photos?access_token=foo_token&appsecret_proof=857d5f035a894f16b4180f19966e055cdeab92d4d53017b13dccd6d43b6497af&before=foo_before_cursor&foo=bar&limit=25&pretty=0', $prevPage->getUrl()); + } + + public function testCanMapOverNodes() + { + $graphEdge = new GraphEdge( + $this->request, + [ + new GraphNode(['name' => 'dummy']), + new GraphNode(['name' => 'dummy']), + ], + ['paging' => $this->pagination], + '/1234567890/likes' + ); + + $graphEdge = $graphEdge->map(function (GraphNode $node) { + $node['name'] = str_replace('dummy', 'foo', $node['name']); + return $node; + }); + + $graphEdgeToCompare = new GraphEdge( + $this->request, + [ + new GraphNode(['name' => 'foo']), + new GraphNode(['name' => 'foo']) + ], + ['paging' => $this->pagination], + '/1234567890/likes' + ); + + $this->assertEquals($graphEdgeToCompare, $graphEdge); + } +} diff --git a/tests/GraphNodes/GraphEventTest.php b/tests/GraphNodes/GraphEventTest.php new file mode 100644 index 000000000..7c6f12708 --- /dev/null +++ b/tests/GraphNodes/GraphEventTest.php @@ -0,0 +1,109 @@ +responseMock = m::mock('\Facebook\FacebookResponse'); + } + + public function testCoverGetsCastAsGraphCoverPhoto() + { + $dataFromGraph = [ + 'cover' => ['id' => '1337'] + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphObject = $factory->makeGraphEvent(); + + $cover = $graphObject->getCover(); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphCoverPhoto', $cover); + } + + public function testPlaceGetsCastAsGraphPage() + { + $dataFromGraph = [ + 'place' => ['id' => '1337'] + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphObject = $factory->makeGraphEvent(); + + $place = $graphObject->getPlace(); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphPage', $place); + } + + public function testPictureGetsCastAsGraphPicture() + { + $dataFromGraph = [ + 'picture' => ['id' => '1337'] + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphObject = $factory->makeGraphEvent(); + + $picture = $graphObject->getPicture(); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphPicture', $picture); + } + + public function testParentGroupGetsCastAsGraphGroup() + { + $dataFromGraph = [ + 'parent_group' => ['id' => '1337'] + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphObject = $factory->makeGraphEvent(); + + $parentGroup = $graphObject->getParentGroup(); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphGroup', $parentGroup); + } +} diff --git a/tests/GraphNodes/GraphGroupTest.php b/tests/GraphNodes/GraphGroupTest.php new file mode 100644 index 000000000..c62d50fb0 --- /dev/null +++ b/tests/GraphNodes/GraphGroupTest.php @@ -0,0 +1,75 @@ +responseMock = m::mock('\Facebook\FacebookResponse'); + } + + public function testCoverGetsCastAsGraphCoverPhoto() + { + $dataFromGraph = [ + 'cover' => ['id' => '1337'] + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphGroup(); + + $cover = $graphNode->getCover(); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphCoverPhoto', $cover); + } + + public function testVenueGetsCastAsGraphLocation() + { + $dataFromGraph = [ + 'venue' => ['id' => '1337'] + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphGroup(); + + $venue = $graphNode->getVenue(); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphLocation', $venue); + } +} diff --git a/tests/GraphNodes/GraphNodeFactoryTest.php b/tests/GraphNodes/GraphNodeFactoryTest.php new file mode 100644 index 000000000..5772c0ad5 --- /dev/null +++ b/tests/GraphNodes/GraphNodeFactoryTest.php @@ -0,0 +1,452 @@ +request = new FacebookRequest( + $app, + 'foo_token', + 'GET', + '/me/photos?keep=me', + ['foo' => 'bar'], + 'foo_eTag', + 'v1337' + ); + } + + public function testAValidGraphNodeResponseWillNotThrow() + { + $data = '{"id":"123","name":"foo"}'; + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $factory->validateResponseCastableAsGraphNode(); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testANonGraphNodeResponseWillThrow() + { + $data = '{"data":[{"id":"123","name":"foo"},{"id":"1337","name":"bar"}]}'; + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $factory->validateResponseCastableAsGraphNode(); + } + + public function testAValidGraphEdgeResponseWillNotThrow() + { + $data = '{"data":[{"id":"123","name":"foo"},{"id":"1337","name":"bar"}]}'; + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $factory->validateResponseCastableAsGraphEdge(); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testANonGraphEdgeResponseWillThrow() + { + $data = '{"id":"123","name":"foo"}'; + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $factory->validateResponseCastableAsGraphEdge(); + } + + public function testOnlyNumericArraysAreCastableAsAGraphEdge() + { + $shouldPassOne = GraphNodeFactory::isCastableAsGraphEdge([]); + $shouldPassTwo = GraphNodeFactory::isCastableAsGraphEdge(['foo', 'bar']); + $shouldFail = GraphNodeFactory::isCastableAsGraphEdge(['faz' => 'baz']); + + $this->assertTrue($shouldPassOne, 'Expected the given array to be castable as a GraphEdge.'); + $this->assertTrue($shouldPassTwo, 'Expected the given array to be castable as a GraphEdge.'); + $this->assertFalse($shouldFail, 'Expected the given array to not be castable as a GraphEdge.'); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testInvalidSubClassesWillThrow() + { + GraphNodeFactory::validateSubclass('FooSubClass'); + } + + public function testValidSubClassesWillNotThrow() + { + GraphNodeFactory::validateSubclass('\Facebook\GraphNodes\GraphNode'); + GraphNodeFactory::validateSubclass('\Facebook\GraphNodes\GraphAlbum'); + GraphNodeFactory::validateSubclass('\Facebook\Tests\Fixtures\MyFooGraphNode'); + } + + public function testCastingAsASubClassObjectWillInstantiateTheSubClass() + { + $data = '{"id":"123","name":"foo"}'; + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $mySubClassObject = $factory->makeGraphNode('\Facebook\Tests\Fixtures\MyFooGraphNode'); + + $this->assertInstanceOf('\Facebook\Tests\Fixtures\MyFooGraphNode', $mySubClassObject); + } + + public function testASubClassMappingWillAutomaticallyInstantiateSubClass() + { + $data = '{"id":"123","name":"Foo Name","foo_object":{"id":"1337","name":"Should be sub classed!"}}'; + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $mySubClassObject = $factory->makeGraphNode('\Facebook\Tests\Fixtures\MyFooGraphNode'); + $fooObject = $mySubClassObject->getField('foo_object'); + + $this->assertInstanceOf('\Facebook\Tests\Fixtures\MyFooGraphNode', $mySubClassObject); + $this->assertInstanceOf('\Facebook\Tests\Fixtures\MyFooSubClassGraphNode', $fooObject); + } + + public function testAnUnknownGraphNodeWillBeCastAsAGenericGraphNode() + { + $data = json_encode([ + 'id' => '123', + 'name' => 'Foo Name', + 'unknown_object' => [ + 'id' => '1337', + 'name' => 'Should be generic!', + ], + ]); + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + + $mySubClassObject = $factory->makeGraphNode('\Facebook\Tests\Fixtures\MyFooGraphNode'); + $unknownObject = $mySubClassObject->getField('unknown_object'); + + $this->assertInstanceOf('\Facebook\Tests\Fixtures\MyFooGraphNode', $mySubClassObject); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphNode', $unknownObject); + $this->assertNotInstanceOf('\Facebook\Tests\Fixtures\MyFooGraphNode', $unknownObject); + } + + public function testAListFromGraphWillBeCastAsAGraphEdge() + { + $data = json_encode([ + 'data' => [ + [ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], + [ + 'id' => '1337', + 'name' => 'Bar McBaz', + 'link' => 'http://facebook/bar', + ], + ], + 'paging' => [ + 'next' => 'http://facebook/next', + 'previous' => 'http://facebook/prev', + ], + ]); + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $graphEdge = $factory->makeGraphEdge(); + $graphData = $graphEdge->asArray(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphEdge', $graphEdge); + $this->assertEquals([ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], $graphData[0]); + $this->assertEquals([ + 'id' => '1337', + 'name' => 'Bar McBaz', + 'link' => 'http://facebook/bar', + ], $graphData[1]); + } + + public function testAGraphNodeWillBeCastAsAGraphNode() + { + $data = json_encode([ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ]); + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $graphNode = $factory->makeGraphNode(); + $graphData = $graphNode->asArray(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphNode', $graphNode); + $this->assertEquals([ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], $graphData); + } + + public function testAGraphNodeWithARootDataKeyWillBeCastAsAGraphNode() + { + $data = json_encode([ + 'data' => [ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], + ]); + + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $graphNode = $factory->makeGraphNode(); + $graphData = $graphNode->asArray(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphNode', $graphNode); + $this->assertEquals([ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], $graphData); + } + + public function testAGraphNodeWithARootDataKeyWillConserveRootKeys() + { + $data = json_encode([ + 'id' => '123', + 'foo' => 'bar', + 'data' => [ + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], + ]); + + $res = new FacebookResponse($this->request, $data); + $factory = new GraphNodeFactory($res); + $graphNode = $factory->makeGraphNode(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphNode', $graphNode); + + $graphData = $graphNode->asArray(); + + $this->assertEquals([ + 'id' => '123', + 'foo' => 'bar', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], $graphData); + } + + public function testAGraphEdgeWillBeCastRecursively() + { + $someUser = [ + 'id' => '123', + 'name' => 'Foo McBar', + ]; + $likesCollection = [ + 'data' => [ + [ + 'id' => '1', + 'name' => 'Sammy Kaye Powers', + 'is_friendly' => true, + ], + [ + 'id' => '2', + 'name' => 'Yassine Guedidi', + 'is_friendly' => true, + ], + [ + 'id' => '3', + 'name' => 'Fosco Marotto', + 'is_friendly' => true, + ], + [ + 'id' => '4', + 'name' => 'Foo McUnfriendly', + 'is_friendly' => false, + ], + ], + 'paging' => [ + 'next' => 'http://facebook/next_likes', + 'previous' => 'http://facebook/prev_likes', + ], + ]; + $commentsCollection = [ + 'data' => [ + [ + 'id' => '42_1', + 'from' => $someUser, + 'message' => 'Foo comment.', + 'created_time' => '2014-07-15T03:54:34+0000', + 'likes' => $likesCollection, + ], + [ + 'id' => '42_2', + 'from' => $someUser, + 'message' => 'Bar comment.', + 'created_time' => '2014-07-15T04:11:24+0000', + 'likes' => $likesCollection, + ], + ], + 'paging' => [ + 'next' => 'http://facebook/next_comments', + 'previous' => 'http://facebook/prev_comments', + ], + ]; + $dataFromGraph = [ + 'data' => [ + [ + 'id' => '1337_1', + 'from' => $someUser, + 'story' => 'Some great foo story.', + 'likes' => $likesCollection, + 'comments' => $commentsCollection, + ], + [ + 'id' => '1337_2', + 'from' => $someUser, + 'to' => [ + 'data' => [$someUser], + ], + 'message' => 'Some great bar message.', + 'likes' => $likesCollection, + 'comments' => $commentsCollection, + ], + ], + 'paging' => [ + 'next' => 'http://facebook/next', + 'previous' => 'http://facebook/prev', + ], + ]; + $data = json_encode($dataFromGraph); + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $graphNode = $factory->makeGraphEdge(); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphEdge', $graphNode); + + // Story + $storyObject = $graphNode[0]; + $this->assertInstanceOf('\Facebook\GraphNodes\GraphNode', $storyObject['from']); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphEdge', $storyObject['likes']); + $this->assertInstanceOf('\Facebook\GraphNodes\GraphEdge', $storyObject['comments']); + + // Story Comments + $storyComments = $storyObject['comments']; + $firstStoryComment = $storyComments[0]; + $this->assertInstanceOf('\Facebook\GraphNodes\GraphNode', $firstStoryComment['from']); + + // Message + $messageObject = $graphNode[1]; + $this->assertInstanceOf('\Facebook\GraphNodes\GraphEdge', $messageObject['to']); + $toUsers = $messageObject['to']; + $this->assertInstanceOf('\Facebook\GraphNodes\GraphNode', $toUsers[0]); + } + + public function testAGraphEdgeWillGenerateTheProperParentGraphEdges() + { + $likesList = [ + 'data' => [ + [ + 'id' => '1', + 'name' => 'Sammy Kaye Powers', + ], + ], + 'paging' => [ + 'cursors' => [ + 'after' => 'like_after_cursor', + 'before' => 'like_before_cursor', + ], + ], + ]; + + $photosList = [ + 'data' => [ + [ + 'id' => '777', + 'name' => 'Foo Photo', + 'likes' => $likesList, + ], + ], + 'paging' => [ + 'cursors' => [ + 'after' => 'photo_after_cursor', + 'before' => 'photo_before_cursor', + ], + ], + ]; + + $data = json_encode([ + 'data' => [ + [ + 'id' => '111', + 'name' => 'Foo McBar', + 'likes' => $likesList, + 'photos' => $photosList, + ], + [ + 'id' => '222', + 'name' => 'Bar McBaz', + 'likes' => $likesList, + 'photos' => $photosList, + ], + ], + 'paging' => [ + 'next' => 'http://facebook/next', + 'previous' => 'http://facebook/prev', + ], + ]); + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphNodeFactory($res); + $graphEdge = $factory->makeGraphEdge(); + $topGraphEdge = $graphEdge->getParentGraphEdge(); + $childGraphEdgeOne = $graphEdge[0]['likes']->getParentGraphEdge(); + $childGraphEdgeTwo = $graphEdge[1]['likes']->getParentGraphEdge(); + $childGraphEdgeThree = $graphEdge[1]['photos']->getParentGraphEdge(); + $childGraphEdgeFour = $graphEdge[1]['photos'][0]['likes']->getParentGraphEdge(); + + $this->assertNull($topGraphEdge); + $this->assertEquals('/111/likes', $childGraphEdgeOne); + $this->assertEquals('/222/likes', $childGraphEdgeTwo); + $this->assertEquals('/222/photos', $childGraphEdgeThree); + $this->assertEquals('/777/likes', $childGraphEdgeFour); + } +} diff --git a/tests/GraphNodes/GraphNodeTest.php b/tests/GraphNodes/GraphNodeTest.php new file mode 100644 index 000000000..67444fae8 --- /dev/null +++ b/tests/GraphNodes/GraphNodeTest.php @@ -0,0 +1,138 @@ +asArray(); + + $this->assertEquals([], $backingData); + } + + public function testAGraphNodeCanInstantiateWithData() + { + $graphNode = new GraphNode(['foo' => 'bar']); + $backingData = $graphNode->asArray(); + + $this->assertEquals(['foo' => 'bar'], $backingData); + } + + public function testDatesThatShouldBeCastAsDateTimeObjectsAreDetected() + { + $graphNode = new GraphNode(); + + // Should pass + $shouldPass = $graphNode->isIso8601DateString('1985-10-26T01:21:00+0000'); + $this->assertTrue($shouldPass, 'Expected the valid ISO 8601 formatted date from Back To The Future to pass.'); + + $shouldPass = $graphNode->isIso8601DateString('1999-12-31'); + $this->assertTrue($shouldPass, 'Expected the valid ISO 8601 formatted date to party like it\'s 1999.'); + + $shouldPass = $graphNode->isIso8601DateString('2009-05-19T14:39Z'); + $this->assertTrue($shouldPass, 'Expected the valid ISO 8601 formatted date to pass.'); + + $shouldPass = $graphNode->isIso8601DateString('2014-W36'); + $this->assertTrue($shouldPass, 'Expected the valid ISO 8601 formatted date to pass.'); + + // Should fail + $shouldFail = $graphNode->isIso8601DateString('2009-05-19T14a39r'); + $this->assertFalse($shouldFail, 'Expected the invalid ISO 8601 format to fail.'); + + $shouldFail = $graphNode->isIso8601DateString('foo_time'); + $this->assertFalse($shouldFail, 'Expected the invalid ISO 8601 format to fail.'); + } + + public function testATimeStampCanBeConvertedToADateTimeObject() + { + $someTimeStampFromGraph = 1405547020; + $graphNode = new GraphNode(); + $dateTime = $graphNode->castToDateTime($someTimeStampFromGraph); + $prettyDate = $dateTime->format(\DateTime::RFC1036); + $timeStamp = $dateTime->getTimestamp(); + + $this->assertInstanceOf('DateTime', $dateTime); + $this->assertEquals('Wed, 16 Jul 14 23:43:40 +0200', $prettyDate); + $this->assertEquals(1405547020, $timeStamp); + } + + public function testAGraphDateStringCanBeConvertedToADateTimeObject() + { + $someDateStringFromGraph = '2014-07-15T03:44:53+0000'; + $graphNode = new GraphNode(); + $dateTime = $graphNode->castToDateTime($someDateStringFromGraph); + $prettyDate = $dateTime->format(\DateTime::RFC1036); + $timeStamp = $dateTime->getTimestamp(); + + $this->assertInstanceOf('DateTime', $dateTime); + $this->assertEquals('Tue, 15 Jul 14 03:44:53 +0000', $prettyDate); + $this->assertEquals(1405395893, $timeStamp); + } + + public function testUncastingAGraphNodeWillUncastTheDateTimeObject() + { + $collectionOne = new GraphNode(['foo', 'bar']); + $collectionTwo = new GraphNode([ + 'id' => '123', + 'date' => new \DateTime('2014-07-15T03:44:53+0000'), + 'some_collection' => $collectionOne, + ]); + + $uncastArray = $collectionTwo->uncastItems(); + + $this->assertEquals([ + 'id' => '123', + 'date' => '2014-07-15T03:44:53+0000', + 'some_collection' => ['foo', 'bar'], + ], $uncastArray); + } + + public function testGettingGraphNodeAsAnArrayWillNotUncastTheDateTimeObject() + { + $collection = new GraphNode([ + 'id' => '123', + 'date' => new \DateTime('2014-07-15T03:44:53+0000'), + ]); + + $collectionAsArray = $collection->asArray(); + + $this->assertInstanceOf('DateTime', $collectionAsArray['date']); + } + + public function testReturningACollectionAsJasonWillSafelyRepresentDateTimes() + { + $collection = new GraphNode([ + 'id' => '123', + 'date' => new \DateTime('2014-07-15T03:44:53+0000'), + ]); + + $collectionAsString = $collection->asJson(); + + $this->assertEquals('{"id":"123","date":"2014-07-15T03:44:53+0000"}', $collectionAsString); + } +} diff --git a/tests/GraphNodes/GraphObjectFactoryTest.php b/tests/GraphNodes/GraphObjectFactoryTest.php new file mode 100644 index 000000000..3ef1d0bc1 --- /dev/null +++ b/tests/GraphNodes/GraphObjectFactoryTest.php @@ -0,0 +1,114 @@ +request = new FacebookRequest( + $app, + 'foo_token', + 'GET', + '/me/photos?keep=me', + ['foo' => 'bar'], + 'foo_eTag', + 'v1337' + ); + } + + public function testAGraphNodeWillBeCastAsAGraphNode() + { + $data = json_encode([ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ]); + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphObjectFactory($res); + $graphObject = $factory->makeGraphObject(); + $graphData = $graphObject->asArray(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphObject', $graphObject); + $this->assertEquals([ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], $graphData); + } + + public function testAListFromGraphWillBeCastAsAGraphEdge() + { + $data = json_encode([ + 'data' => [ + [ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], + [ + 'id' => '1337', + 'name' => 'Bar McBaz', + 'link' => 'http://facebook/bar', + ], + ], + 'paging' => [ + 'next' => 'http://facebook/next', + 'previous' => 'http://facebook/prev', + ], + ]); + $res = new FacebookResponse($this->request, $data); + + $factory = new GraphObjectFactory($res); + $graphList = $factory->makeGraphList(); + $graphData = $graphList->asArray(); + + $this->assertInstanceOf('\Facebook\GraphNodes\GraphList', $graphList); + $this->assertEquals([ + 'id' => '123', + 'name' => 'Foo McBar', + 'link' => 'http://facebook/foo', + ], $graphData[0]); + $this->assertEquals([ + 'id' => '1337', + 'name' => 'Bar McBaz', + 'link' => 'http://facebook/bar', + ], $graphData[1]); + } +} diff --git a/tests/GraphNodes/GraphPageTest.php b/tests/GraphNodes/GraphPageTest.php new file mode 100644 index 000000000..c7ce163c9 --- /dev/null +++ b/tests/GraphNodes/GraphPageTest.php @@ -0,0 +1,95 @@ +responseMock = m::mock('\\Facebook\\FacebookResponse'); + } + + public function testPagePropertiesReturnGraphPageObjects() + { + $dataFromGraph = [ + 'id' => '123', + 'name' => 'Foo Page', + 'best_page' => [ + 'id' => '1', + 'name' => 'Bar Page', + ], + 'global_brand_parent_page' => [ + 'id' => '2', + 'name' => 'Faz Page', + ], + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphPage(); + + $bestPage = $graphNode->getBestPage(); + $globalBrandParentPage = $graphNode->getGlobalBrandParentPage(); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphPage', $bestPage); + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphPage', $globalBrandParentPage); + } + + public function testLocationPropertyWillGetCastAsGraphLocationObject() + { + $dataFromGraph = [ + 'id' => '123', + 'name' => 'Foo Page', + 'location' => [ + 'city' => 'Washington', + 'country' => 'United States', + 'latitude' => 38.881634205431, + 'longitude' => -77.029121075722, + 'state' => 'DC', + ], + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphPage(); + + $location = $graphNode->getLocation(); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphLocation', $location); + } +} diff --git a/tests/GraphNodes/GraphSessionInfoTest.php b/tests/GraphNodes/GraphSessionInfoTest.php new file mode 100644 index 000000000..b2e56faed --- /dev/null +++ b/tests/GraphNodes/GraphSessionInfoTest.php @@ -0,0 +1,62 @@ +responseMock = m::mock('\\Facebook\\FacebookResponse'); + } + + public function testDatesGetCastToDateTime() + { + $dataFromGraph = [ + 'expires_at' => 123, + 'issued_at' => 1337, + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + + $graphNode = $factory->makeGraphSessionInfo(); + + $expires = $graphNode->getExpiresAt(); + $issuedAt = $graphNode->getIssuedAt(); + + $this->assertInstanceOf('DateTime', $expires); + $this->assertInstanceOf('DateTime', $issuedAt); + } +} diff --git a/tests/GraphNodes/GraphUserTest.php b/tests/GraphNodes/GraphUserTest.php new file mode 100644 index 000000000..a3230fa36 --- /dev/null +++ b/tests/GraphNodes/GraphUserTest.php @@ -0,0 +1,204 @@ +responseMock = m::mock('\\Facebook\\FacebookResponse'); + } + + public function testDatesGetCastToDateTime() + { + $dataFromGraph = [ + 'updated_time' => '2016-04-26 13:22:05', + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphUser(); + + $updatedTime = $graphNode->getField('updated_time'); + + $this->assertInstanceOf('DateTime', $updatedTime); + } + + public function testBirthdaysGetCastToBirthday() + { + $dataFromGraph = [ + 'birthday' => '1984/01/01', + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphUser(); + + $birthday = $graphNode->getBirthday(); + + // Test to ensure BC + $this->assertInstanceOf('DateTime', $birthday); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\Birthday', $birthday); + $this->assertTrue($birthday->hasDate()); + $this->assertTrue($birthday->hasYear()); + $this->assertEquals('1984/01/01', $birthday->format('Y/m/d')); + } + + public function testBirthdayCastHandlesDateWithoutYear() + { + $dataFromGraph = [ + 'birthday' => '03/21', + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphUser(); + + $birthday = $graphNode->getBirthday(); + + $this->assertTrue($birthday->hasDate()); + $this->assertFalse($birthday->hasYear()); + $this->assertEquals('03/21', $birthday->format('m/d')); + } + + public function testBirthdayCastHandlesYearWithoutDate() + { + $dataFromGraph = [ + 'birthday' => '1984', + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphUser(); + + $birthday = $graphNode->getBirthday(); + + $this->assertTrue($birthday->hasYear()); + $this->assertFalse($birthday->hasDate()); + $this->assertEquals('1984', $birthday->format('Y')); + } + + public function testPagePropertiesWillGetCastAsGraphPageObjects() + { + $dataFromGraph = [ + 'id' => '123', + 'name' => 'Foo User', + 'hometown' => [ + 'id' => '1', + 'name' => 'Foo Place', + ], + 'location' => [ + 'id' => '2', + 'name' => 'Bar Place', + ], + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphUser(); + + $hometown = $graphNode->getHometown(); + $location = $graphNode->getLocation(); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphPage', $hometown); + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphPage', $location); + } + + public function testUserPropertiesWillGetCastAsGraphUserObjects() + { + $dataFromGraph = [ + 'id' => '123', + 'name' => 'Foo User', + 'significant_other' => [ + 'id' => '1337', + 'name' => 'Bar User', + ], + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphUser(); + + $significantOther = $graphNode->getSignificantOther(); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphUser', $significantOther); + } + + public function testPicturePropertiesWillGetCastAsGraphPictureObjects() + { + $dataFromGraph = [ + 'id' => '123', + 'name' => 'Foo User', + 'picture' => [ + 'is_silhouette' => true, + 'url' => 'http://foo.bar', + 'width' => 200, + 'height' => 200, + ], + ]; + + $this->responseMock + ->shouldReceive('getDecodedBody') + ->once() + ->andReturn($dataFromGraph); + $factory = new GraphNodeFactory($this->responseMock); + $graphNode = $factory->makeGraphUser(); + + $Picture = $graphNode->getPicture(); + + $this->assertInstanceOf('\\Facebook\\GraphNodes\\GraphPicture', $Picture); + $this->assertTrue($Picture->isSilhouette()); + $this->assertEquals(200, $Picture->getWidth()); + $this->assertEquals(200, $Picture->getHeight()); + $this->assertEquals('http://foo.bar', $Picture->getUrl()); + } +} diff --git a/tests/GraphObjectTest.php b/tests/GraphObjectTest.php deleted file mode 100644 index c56babc78..000000000 --- a/tests/GraphObjectTest.php +++ /dev/null @@ -1,97 +0,0 @@ -execute()->getGraphObjectList(); - $this->assertTrue(is_array($response)); - } - - public function testArrayProperties() - { - $backingData = array( - 'id' => 42, - 'friends' => array( - 'data' => array( - array( - 'id' => 1, - 'name' => 'David' - ), - array( - 'id' => 2, - 'name' => 'Fosco' - ) - ), - 'paging' => array( - 'next' => 'nexturl' - ) - ) - ); - $obj = new GraphObject($backingData); - $friends = $obj->getPropertyAsArray('friends'); - $this->assertEquals(2, count($friends)); - $this->assertTrue($friends[0] instanceof GraphObject); - $this->assertTrue($friends[1] instanceof GraphObject); - $this->assertEquals('David', $friends[0]->getProperty('name')); - $this->assertEquals('Fosco', $friends[1]->getProperty('name')); - - $backingData = array( - 'id' => 42, - 'friends' => array( - array( - 'id' => 1, - 'name' => 'Ilya' - ), - array( - 'id' => 2, - 'name' => 'Kevin' - ) - ) - ); - $obj = new GraphObject($backingData); - $friends = $obj->getPropertyAsArray('friends'); - $this->assertEquals(2, count($friends)); - $this->assertTrue($friends[0] instanceof GraphObject); - $this->assertTrue($friends[1] instanceof GraphObject); - $this->assertEquals('Ilya', $friends[0]->getProperty('name')); - $this->assertEquals('Kevin', $friends[1]->getProperty('name')); - - } - - public function testAsList() - { - $backingData = array( - 'data' => array( - array( - 'id' => 1, - 'name' => 'David' - ), - array( - 'id' => 2, - 'name' => 'Fosco' - ) - ) - ); - $enc = json_encode($backingData); - $response = new FacebookResponse(null, json_decode($enc), $enc); - $list = $response->getGraphObjectList(GraphUser::className()); - $this->assertEquals(2, count($list)); - $this->assertTrue($list[0] instanceof GraphObject); - $this->assertTrue($list[1] instanceof GraphObject); - $this->assertEquals('David', $list[0]->getName()); - $this->assertEquals('Fosco', $list[1]->getName()); - } - -} diff --git a/tests/GraphSessionInfoTest.php b/tests/GraphSessionInfoTest.php deleted file mode 100644 index 491cf89f9..000000000 --- a/tests/GraphSessionInfoTest.php +++ /dev/null @@ -1,26 +0,0 @@ - FacebookTestHelper::$testSession->getToken() - ); - $response = (new FacebookRequest( - new FacebookSession(FacebookTestHelper::getAppToken()), - 'GET', - '/debug_token', - $params - ))->execute()->getGraphObject(GraphSessionInfo::className()); - $this->assertTrue($response instanceof GraphSessionInfo); - $this->assertNotNull($response->getAppId()); - $this->assertTrue($response->isValid()); - } - -} diff --git a/tests/GraphUserTest.php b/tests/GraphUserTest.php deleted file mode 100644 index 1ccb71509..000000000 --- a/tests/GraphUserTest.php +++ /dev/null @@ -1,27 +0,0 @@ -execute()->getGraphObject(GraphUser::className()); - - $info = FacebookTestHelper::$testSession->getSessionInfo(); - - $this->assertTrue($response instanceof GraphUser); - $this->assertEquals($info->getId(), $response->getId()); - $this->assertNotNull($response->getName()); - $this->assertNotNull($response->getLastName()); - $this->assertNotNull($response->getLink()); - } - -} diff --git a/tests/Helpers/FacebookCanvasHelperTest.php b/tests/Helpers/FacebookCanvasHelperTest.php new file mode 100644 index 000000000..f03d66fbc --- /dev/null +++ b/tests/Helpers/FacebookCanvasHelperTest.php @@ -0,0 +1,53 @@ +helper = new FacebookCanvasHelper($app, new FacebookClient()); + } + + public function testSignedRequestDataCanBeRetrievedFromPostData() + { + $_POST['signed_request'] = $this->rawSignedRequestAuthorized; + + $rawSignedRequest = $this->helper->getRawSignedRequest(); + + $this->assertEquals($this->rawSignedRequestAuthorized, $rawSignedRequest); + } +} diff --git a/tests/Helpers/FacebookJavaScriptHelperTest.php b/tests/Helpers/FacebookJavaScriptHelperTest.php new file mode 100644 index 000000000..521875885 --- /dev/null +++ b/tests/Helpers/FacebookJavaScriptHelperTest.php @@ -0,0 +1,45 @@ +rawSignedRequestAuthorized; + + $app = new FacebookApp('123', 'foo_app_secret'); + $helper = new FacebookJavaScriptHelper($app, new FacebookClient()); + + $rawSignedRequest = $helper->getRawSignedRequest(); + + $this->assertEquals($this->rawSignedRequestAuthorized, $rawSignedRequest); + } +} diff --git a/tests/Helpers/FacebookPageTabHelperTest.php b/tests/Helpers/FacebookPageTabHelperTest.php new file mode 100644 index 000000000..5c27f488a --- /dev/null +++ b/tests/Helpers/FacebookPageTabHelperTest.php @@ -0,0 +1,46 @@ +rawSignedRequestAuthorized; + + $app = new FacebookApp('123', 'foo_app_secret'); + $helper = new FacebookPageTabHelper($app, new FacebookClient()); + + $this->assertFalse($helper->isAdmin()); + $this->assertEquals('42', $helper->getPageId()); + $this->assertEquals('42', $helper->getPageData('id')); + $this->assertEquals('default', $helper->getPageData('foo', 'default')); + } +} diff --git a/tests/Helpers/FacebookRedirectLoginHelperTest.php b/tests/Helpers/FacebookRedirectLoginHelperTest.php new file mode 100644 index 000000000..be31689a2 --- /dev/null +++ b/tests/Helpers/FacebookRedirectLoginHelperTest.php @@ -0,0 +1,136 @@ +persistentDataHandler = new FacebookMemoryPersistentDataHandler(); + + $app = new FacebookApp('123', 'foo_app_secret'); + $oAuth2Client = new FooRedirectLoginOAuth2Client($app, new FacebookClient(), 'v1337'); + $this->redirectLoginHelper = new FacebookRedirectLoginHelper($oAuth2Client, $this->persistentDataHandler); + } + + public function testLoginURL() + { + $scope = ['foo', 'bar']; + $loginUrl = $this->redirectLoginHelper->getLoginUrl(self::REDIRECT_URL, $scope); + + $expectedUrl = 'https://www.facebook.com/v1337/dialog/oauth?'; + $this->assertTrue(strpos($loginUrl, $expectedUrl) === 0, 'Unexpected base login URL returned from getLoginUrl().'); + + $params = [ + 'client_id' => '123', + 'redirect_uri' => self::REDIRECT_URL, + 'state' => $this->persistentDataHandler->get('state'), + 'sdk' => 'php-sdk-' . Facebook::VERSION, + 'scope' => implode(',', $scope), + ]; + foreach ($params as $key => $value) { + $this->assertContains($key . '=' . urlencode($value), $loginUrl); + } + } + + public function testLogoutURL() + { + $logoutUrl = $this->redirectLoginHelper->getLogoutUrl('foo_token', self::REDIRECT_URL); + $expectedUrl = 'https://www.facebook.com/logout.php?'; + $this->assertTrue(strpos($logoutUrl, $expectedUrl) === 0, 'Unexpected base logout URL returned from getLogoutUrl().'); + + $params = [ + 'next' => self::REDIRECT_URL, + 'access_token' => 'foo_token', + ]; + foreach ($params as $key => $value) { + $this->assertTrue( + strpos($logoutUrl, $key . '=' . urlencode($value)) !== false + ); + } + } + + public function testAnAccessTokenCanBeObtainedFromRedirect() + { + $this->persistentDataHandler->set('state', static::FOO_STATE); + + $_GET['code'] = static::FOO_CODE; + $_GET['enforce_https'] = static::FOO_ENFORCE_HTTPS; + $_GET['state'] = static::FOO_STATE; + + $fullUrl = self::REDIRECT_URL . '?state=' . static::FOO_STATE . '&enforce_https=' . static::FOO_ENFORCE_HTTPS . '&code=' . static::FOO_CODE . '&' . static::FOO_PARAM; + + $accessToken = $this->redirectLoginHelper->getAccessToken($fullUrl); + + // 'code', 'enforce_https' and 'state' should be stripped from the URL + $expectedUrl = self::REDIRECT_URL . '?' . static::FOO_PARAM; + $expectedString = 'foo_token_from_code|' . static::FOO_CODE . '|' . $expectedUrl; + + $this->assertEquals($expectedString, $accessToken->getValue()); + } + + public function testACustomCsprsgCanBeInjected() + { + $app = new FacebookApp('123', 'foo_app_secret'); + $accessTokenClient = new FooRedirectLoginOAuth2Client($app, new FacebookClient(), 'v1337'); + $fooPrsg = new FooPseudoRandomStringGenerator(); + $helper = new FacebookRedirectLoginHelper($accessTokenClient, $this->persistentDataHandler, null, $fooPrsg); + + $loginUrl = $helper->getLoginUrl(self::REDIRECT_URL); + + $this->assertContains('state=csprs123', $loginUrl); + } + + public function testThePseudoRandomStringGeneratorWillAutoDetectCsprsg() + { + $this->assertInstanceOf( + 'Facebook\PseudoRandomString\PseudoRandomStringGeneratorInterface', + $this->redirectLoginHelper->getPseudoRandomStringGenerator() + ); + } +} diff --git a/tests/Helpers/FacebookSignedRequestFromInputHelperTest.php b/tests/Helpers/FacebookSignedRequestFromInputHelperTest.php new file mode 100644 index 000000000..ffaa5e329 --- /dev/null +++ b/tests/Helpers/FacebookSignedRequestFromInputHelperTest.php @@ -0,0 +1,90 @@ +helper = new FooSignedRequestHelper($app, new FooSignedRequestHelperFacebookClient()); + } + + public function testSignedRequestDataCanBeRetrievedFromPostData() + { + $_POST['signed_request'] = 'foo_signed_request'; + + $rawSignedRequest = $this->helper->getRawSignedRequestFromPost(); + + $this->assertEquals('foo_signed_request', $rawSignedRequest); + } + + public function testSignedRequestDataCanBeRetrievedFromCookieData() + { + $_COOKIE['fbsr_123'] = 'foo_signed_request'; + + $rawSignedRequest = $this->helper->getRawSignedRequestFromCookie(); + + $this->assertEquals('foo_signed_request', $rawSignedRequest); + } + + public function testAccessTokenWillBeNullWhenAUserHasNotYetAuthorizedTheApp() + { + $this->helper->instantiateSignedRequest($this->rawSignedRequestUnauthorized); + $accessToken = $this->helper->getAccessToken(); + + $this->assertNull($accessToken); + } + + public function testAnAccessTokenCanBeInstantiatedWhenRedirectReturnsAnAccessToken() + { + $this->helper->instantiateSignedRequest($this->rawSignedRequestAuthorizedWithAccessToken); + $accessToken = $this->helper->getAccessToken(); + + $this->assertInstanceOf('Facebook\Authentication\AccessToken', $accessToken); + $this->assertEquals('foo_token', $accessToken->getValue()); + } + + public function testAnAccessTokenCanBeInstantiatedWhenRedirectReturnsACode() + { + $this->helper->instantiateSignedRequest($this->rawSignedRequestAuthorizedWithCode); + $accessToken = $this->helper->getAccessToken(); + + $this->assertInstanceOf('Facebook\Authentication\AccessToken', $accessToken); + $this->assertEquals('foo_access_token_from:foo_code', $accessToken->getValue()); + } +} diff --git a/tests/Http/GraphRawResponseTest.php b/tests/Http/GraphRawResponseTest.php new file mode 100644 index 000000000..fcb71f480 --- /dev/null +++ b/tests/Http/GraphRawResponseTest.php @@ -0,0 +1,110 @@ + '"9d86b21aa74d74e574bbb35ba13524a52deb96e3"', + 'Content-Type' => 'text/javascript; charset=UTF-8', + 'X-FB-Rev' => '9244768', + 'Date' => 'Mon, 19 May 2014 18:37:17 GMT', + 'X-FB-Debug' => '02QQiffE7JG2rV6i/Agzd0gI2/OOQ2lk5UW0=', + 'Access-Control-Allow-Origin' => '*', + ]; + + protected $jsonFakeHeader = 'x-fb-ads-insights-throttle: {"app_id_util_pct": 0.00,"acc_id_util_pct": 0.00}'; + protected $jsonFakeHeaderAsArray = ['x-fb-ads-insights-throttle' => '{"app_id_util_pct": 0.00,"acc_id_util_pct": 0.00}']; + + public function testCanSetTheHeadersFromAnArray() + { + $myHeaders = [ + 'foo' => 'bar', + 'baz' => 'faz', + ]; + $response = new GraphRawResponse($myHeaders, ''); + $headers = $response->getHeaders(); + + $this->assertEquals($myHeaders, $headers); + } + + public function testCanSetTheHeadersFromAString() + { + $response = new GraphRawResponse($this->fakeRawHeader, ''); + $headers = $response->getHeaders(); + $httpResponseCode = $response->getHttpResponseCode(); + + $this->assertEquals($this->fakeHeadersAsArray, $headers); + $this->assertEquals(200, $httpResponseCode); + } + + public function testWillIgnoreProxyHeaders() + { + $response = new GraphRawResponse($this->fakeRawProxyHeader . $this->fakeRawHeader, ''); + $headers = $response->getHeaders(); + $httpResponseCode = $response->getHttpResponseCode(); + + $this->assertEquals($this->fakeHeadersAsArray, $headers); + $this->assertEquals(200, $httpResponseCode); + } + + public function testCanTransformJsonHeaderValues() + { + $response = new GraphRawResponse($this->jsonFakeHeader, ''); + $headers = $response->getHeaders(); + + $this->assertEquals($this->jsonFakeHeaderAsArray['x-fb-ads-insights-throttle'], $headers['x-fb-ads-insights-throttle']); + } + + public function testHttpResponseCode() + { + // HTTP/1.0 + $headers = str_replace('HTTP/1.1', 'HTTP/1.0', $this->fakeRawHeader); + $response = new GraphRawResponse($headers, ''); + $this->assertEquals(200, $response->getHttpResponseCode()); + + // HTTP/1.1 + $response = new GraphRawResponse($this->fakeRawHeader, ''); + $this->assertEquals(200, $response->getHttpResponseCode()); + + // HTTP/2 + $headers = str_replace('HTTP/1.1', 'HTTP/2', $this->fakeRawHeader); + $response = new GraphRawResponse($headers, ''); + $this->assertEquals(200, $response->getHttpResponseCode()); + } +} diff --git a/tests/Http/RequestBodyMultipartTest.php b/tests/Http/RequestBodyMultipartTest.php new file mode 100644 index 000000000..1a23c469f --- /dev/null +++ b/tests/Http/RequestBodyMultipartTest.php @@ -0,0 +1,111 @@ + 'bar', + 'scawy_vawues' => '@FooBar is a real twitter handle.', + ], [], 'foo_boundary'); + $body = $message->getBody(); + + $expectedBody = "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"scawy_vawues\"\r\n\r\n@FooBar is a real twitter handle.\r\n"; + $expectedBody .= "--foo_boundary--\r\n"; + + $this->assertEquals($expectedBody, $body); + } + + public function testCanProperlyEncodeFilesAndParams() + { + $file = new FacebookFile(__DIR__ . '/../foo.txt'); + $message = new RequestBodyMultipart([ + 'foo' => 'bar', + ], [ + 'foo_file' => $file, + ], 'foo_boundary'); + $body = $message->getBody(); + + $expectedBody = "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"foo_file\"; filename=\"foo.txt\"\r\n"; + $expectedBody .= "Content-Type: text/plain\r\n\r\nThis is a text file used for testing. Let's dance.\r\n"; + $expectedBody .= "--foo_boundary--\r\n"; + + $this->assertEquals($expectedBody, $body); + } + + public function testSupportsMultidimensionalParams() + { + $message = new RequestBodyMultipart([ + 'foo' => 'bar', + 'faz' => [1,2,3], + 'targeting' => [ + 'countries' => 'US,GB', + 'age_min' => 13, + ], + 'call_to_action' => [ + 'type' => 'LEARN_MORE', + 'value' => [ + 'link' => 'http://example.com', + 'sponsorship' => [ + 'image' => 'http://example.com/bar.jpg', + ], + ], + ], + ], [], 'foo_boundary'); + $body = $message->getBody(); + + $expectedBody = "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"faz[0]\"\r\n\r\n1\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"faz[1]\"\r\n\r\n2\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"faz[2]\"\r\n\r\n3\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"targeting[countries]\"\r\n\r\nUS,GB\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"targeting[age_min]\"\r\n\r\n13\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"call_to_action[type]\"\r\n\r\nLEARN_MORE\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"call_to_action[value][link]\"\r\n\r\nhttp://example.com\r\n"; + $expectedBody .= "--foo_boundary\r\n"; + $expectedBody .= "Content-Disposition: form-data; name=\"call_to_action[value][sponsorship][image]\"\r\n\r\nhttp://example.com/bar.jpg\r\n"; + $expectedBody .= "--foo_boundary--\r\n"; + + $this->assertEquals($expectedBody, $body); + } +} diff --git a/tests/Http/RequestUrlEncodedTest.php b/tests/Http/RequestUrlEncodedTest.php new file mode 100644 index 000000000..2b4f67f9b --- /dev/null +++ b/tests/Http/RequestUrlEncodedTest.php @@ -0,0 +1,64 @@ + 'bar', + 'scawy_vawues' => '@FooBar is a real twitter handle.', + ]); + $body = $message->getBody(); + + $this->assertEquals('foo=bar&scawy_vawues=%40FooBar+is+a+real+twitter+handle.', $body); + } + + public function testSupportsMultidimensionalParams() + { + $message = new RequestBodyUrlEncoded([ + 'foo' => 'bar', + 'faz' => [1,2,3], + 'targeting' => [ + 'countries' => 'US,GB', + 'age_min' => 13, + ], + 'call_to_action' => [ + 'type' => 'LEARN_MORE', + 'value' => [ + 'link' => 'http://example.com', + 'sponsorship' => [ + 'image' => 'http://example.com/bar.jpg', + ], + ], + ], + ]); + $body = $message->getBody(); + + $this->assertEquals('foo=bar&faz%5B0%5D=1&faz%5B1%5D=2&faz%5B2%5D=3&targeting%5Bcountries%5D=US%2CGB&targeting%5Bage_min%5D=13&call_to_action%5Btype%5D=LEARN_MORE&call_to_action%5Bvalue%5D%5Blink%5D=http%3A%2F%2Fexample.com&call_to_action%5Bvalue%5D%5Bsponsorship%5D%5Bimage%5D=http%3A%2F%2Fexample.com%2Fbar.jpg', $body); + } +} diff --git a/tests/HttpClients/AbstractTestHttpClient.php b/tests/HttpClients/AbstractTestHttpClient.php index 008ca0909..a87005216 100644 --- a/tests/HttpClients/AbstractTestHttpClient.php +++ b/tests/HttpClients/AbstractTestHttpClient.php @@ -1,15 +1,37 @@ 'HTTP/1.1 200 OK', - 'Etag' => '"9d86b21aa74d74e574bbb35ba13524a52deb96e3"', - 'Content-Type' => 'text/javascript; charset=UTF-8', - 'X-FB-Rev' => '9244768', - 'Pragma' => 'no-cache', - 'Expires' => 'Sat, 01 Jan 2000 00:00:00 GMT', - 'Connection' => 'close', - 'Date' => 'Mon, 19 May 2014 18:37:17 GMT', - 'X-FB-Debug' => '02QQiffE7JG2rV6i/Agzd0gI2/OOQ2lk5UW0=', - 'Content-Length' => '29', - 'Cache-Control' => 'private, no-cache, no-store, must-revalidate', - 'Access-Control-Allow-Origin' => '*', - ); - + protected $fakeRawBody = "{\"id\":\"123\",\"name\":\"Foo Bar\"}"; + protected $fakeHeadersAsArray = [ + 'Etag' => '"9d86b21aa74d74e574bbb35ba13524a52deb96e3"', + 'Content-Type' => 'text/javascript; charset=UTF-8', + 'X-FB-Rev' => '9244768', + 'Pragma' => 'no-cache', + 'Expires' => 'Sat, 01 Jan 2000 00:00:00 GMT', + 'Connection' => 'close', + 'Date' => 'Mon, 19 May 2014 18:37:17 GMT', + 'X-FB-Debug' => '02QQiffE7JG2rV6i/Agzd0gI2/OOQ2lk5UW0=', + 'Content-Length' => '29', + 'Cache-Control' => 'private, no-cache, no-store, must-revalidate', + 'Access-Control-Allow-Origin' => '*', + ]; } diff --git a/tests/HttpClients/FacebookCurlHttpClientTest.php b/tests/HttpClients/FacebookCurlHttpClientTest.php index dae2ed4f7..47cc0274e 100644 --- a/tests/HttpClients/FacebookCurlHttpClientTest.php +++ b/tests/HttpClients/FacebookCurlHttpClientTest.php @@ -1,315 +1,214 @@ markTestSkipped('cURL must be installed to test cURL client handler.'); + } + $this->curlMock = m::mock('Facebook\HttpClients\FacebookCurl'); + $this->curlClient = new FacebookCurlHttpClient($this->curlMock); + } + + public function testCanOpenGetCurlConnection() + { + $this->curlMock + ->shouldReceive('init') + ->once() + ->andReturn(null); + $this->curlMock + ->shouldReceive('setoptArray') + ->with(m::on(function ($arg) { + + // array_diff() will sometimes trigger error on child-arrays + if (['X-Foo-Header: X-Bar'] !== $arg[CURLOPT_HTTPHEADER]) { + return false; + } + unset($arg[CURLOPT_HTTPHEADER]); + + $caInfo = array_diff($arg, [ + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_URL => 'http://foo.com', + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 123, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_SSL_VERIFYPEER => true, + ]); + + if (count($caInfo) !== 1) { + return false; + } + + if (1 !== preg_match('/.+\/certs\/DigiCertHighAssuranceEVRootCA\.pem$/', $caInfo[CURLOPT_CAINFO])) { + return false; + } + + return true; + })) + ->once() + ->andReturn(null); - protected $curlMock; - protected $curlClient; - - const CURL_VERSION_STABLE = 0x072400; - const CURL_VERSION_BUGGY = 0x071400; - - public function setUp() - { - $this->curlMock = m::mock('Facebook\HttpClients\FacebookCurl'); - $this->curlClient = new FacebookCurlHttpClient($this->curlMock); - } - - public function tearDown() - { - m::close(); - (new FacebookCurlHttpClient()); // Resets the static dependency injection - } - - public function testCanOpenGetCurlConnection() - { - $this->curlMock - ->shouldReceive('init') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('setopt_array') - ->with(array( - CURLOPT_URL => 'http://foo.com', - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_TIMEOUT => 60, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - )) - ->once() - ->andReturn(null); - - $this->curlClient->openConnection('http://foo.com', 'GET', array()); - } - - public function testCanOpenGetCurlConnectionWithHeaders() - { - $this->curlMock - ->shouldReceive('init') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('setopt_array') - ->with(array( - CURLOPT_URL => 'http://foo.com', - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_TIMEOUT => 60, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_HTTPHEADER => array( - 'X-foo: bar', - ), - )) - ->once() - ->andReturn(null); - - $this->curlClient->addRequestHeader('X-foo', 'bar'); - $this->curlClient->openConnection('http://foo.com', 'GET', array()); - } - - public function testCanOpenPostCurlConnection() - { - $this->curlMock - ->shouldReceive('init') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('setopt_array') - ->with(array( - CURLOPT_URL => 'http://bar.com', - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_TIMEOUT => 60, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_POSTFIELDS => array( - 'baz' => 'bar', - ), - )) - ->once() - ->andReturn(null); - - $this->curlClient->openConnection('http://bar.com', 'POST', array('baz' => 'bar')); - } - - public function testCanOpenPutCurlConnection() - { - $this->curlMock - ->shouldReceive('init') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('setopt_array') - ->with(array( - CURLOPT_URL => 'http://baz.com', - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_TIMEOUT => 60, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_CUSTOMREQUEST => 'PUT', - CURLOPT_POSTFIELDS => array( - 'baz' => 'bar', - ), - )) - ->once() - ->andReturn(null); - - $this->curlClient->openConnection('http://baz.com', 'PUT', array('baz' => 'bar')); - } - - public function testCanOpenDeleteCurlConnection() - { - $this->curlMock - ->shouldReceive('init') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('setopt_array') - ->with(array( - CURLOPT_URL => 'http://faz.com', - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_TIMEOUT => 60, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_CUSTOMREQUEST => 'DELETE', - CURLOPT_POSTFIELDS => array( - 'baz' => 'bar', - ), - )) - ->once() - ->andReturn(null); - - $this->curlClient->openConnection('http://faz.com', 'DELETE', array('baz' => 'bar')); - } - - public function testCanAddBundledCert() - { - $this->curlMock - ->shouldReceive('setopt') - ->with(CURLOPT_CAINFO, '/.fb_ca_chain_bundle\.crt$/') - ->once() - ->andReturn(null); - - $this->curlClient->addBundledCert(); - } - - public function testCanCloseConnection() - { - $this->curlMock - ->shouldReceive('close') - ->once() - ->andReturn(null); - - $this->curlClient->closeConnection(); - } - - public function testTrySendRequest() - { - $this->curlMock - ->shouldReceive('exec') - ->once() - ->andReturn('foo response'); - $this->curlMock - ->shouldReceive('errno') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('error') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HTTP_CODE) - ->once() - ->andReturn(200); - - $this->curlClient->tryToSendRequest(); - } - - public function testProperlyCompilesRequestHeaders() - { - $headers = $this->curlClient->compileRequestHeaders(); - $expectedHeaders = array(); - $this->assertEquals($expectedHeaders, $headers); - - $this->curlClient->addRequestHeader('X-foo', 'bar'); - $headers = $this->curlClient->compileRequestHeaders(); - $expectedHeaders = array( - 'X-foo: bar', - ); - $this->assertEquals($expectedHeaders, $headers); - - $this->curlClient->addRequestHeader('X-bar', 'baz'); - $headers = $this->curlClient->compileRequestHeaders(); - $expectedHeaders = array( - 'X-foo: bar', - 'X-bar: baz', - ); - $this->assertEquals($expectedHeaders, $headers); - } - - public function testIsolatesTheHeaderAndBody() - { - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HEADER_SIZE) - ->once() - ->andReturn(strlen($this->fakeRawHeader)); - $this->curlMock - ->shouldReceive('version') - ->once() - ->andReturn(array('version_number' => self::CURL_VERSION_STABLE)); - $this->curlMock - ->shouldReceive('exec') - ->once() - ->andReturn($this->fakeRawHeader . $this->fakeRawBody); - - $this->curlClient->sendRequest(); - list($rawHeader, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); - - $this->assertEquals($rawHeader, trim($this->fakeRawHeader)); - $this->assertEquals($rawBody, $this->fakeRawBody); - } - - public function testConvertsRawHeadersToArray() - { - $headers = FacebookCurlHttpClient::headersToArray($this->fakeRawHeader); - - $this->assertEquals($headers, $this->fakeHeadersAsArray); - } - - public function testProperlyHandlesProxyHeaders() - { - $rawHeader = $this->fakeRawProxyHeader . $this->fakeRawHeader; - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HEADER_SIZE) - ->once() - ->andReturn(mb_strlen($rawHeader)); - $this->curlMock - ->shouldReceive('version') - ->once() - ->andReturn(array('version_number' => self::CURL_VERSION_STABLE)); - $this->curlMock - ->shouldReceive('exec') - ->once() - ->andReturn($rawHeader . $this->fakeRawBody); - - $this->curlClient->sendRequest(); - list($rawHeaders, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); - - $this->assertEquals($rawHeaders, trim($rawHeader)); - $this->assertEquals($rawBody, $this->fakeRawBody); - - $headers = FacebookCurlHttpClient::headersToArray($rawHeaders); - - $this->assertEquals($headers, $this->fakeHeadersAsArray); - } - - public function testProperlyHandlesProxyHeadersWithCurlBug() - { - $rawHeader = $this->fakeRawProxyHeader . $this->fakeRawHeader; - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HEADER_SIZE) - ->once() - ->andReturn(mb_strlen($this->fakeRawHeader)); // Mimic bug that doesn't count proxy header - $this->curlMock - ->shouldReceive('version') - ->once() - ->andReturn(array('version_number' => self::CURL_VERSION_BUGGY)); - $this->curlMock - ->shouldReceive('exec') - ->once() - ->andReturn($rawHeader . $this->fakeRawBody); - - $this->curlClient->sendRequest(); - list($rawHeaders, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); - - $this->assertEquals($rawHeaders, trim($rawHeader)); - $this->assertEquals($rawBody, $this->fakeRawBody); - - $headers = FacebookCurlHttpClient::headersToArray($rawHeaders); - - $this->assertEquals($headers, $this->fakeHeadersAsArray); - } - - public function testProperlyHandlesProxyHeadersWithCurlBug2() - { - $rawHeader = $this->fakeRawProxyHeader2 . $this->fakeRawHeader; + $this->curlClient->openConnection('http://foo.com', 'GET', 'foo_body', ['X-Foo-Header' => 'X-Bar'], 123); + } + + public function testCanOpenCurlConnectionWithPostBody() + { + $this->curlMock + ->shouldReceive('init') + ->once() + ->andReturn(null); + $this->curlMock + ->shouldReceive('setoptArray') + ->with(m::on(function ($arg) { + + // array_diff() will sometimes trigger error on child-arrays + if ([] !== $arg[CURLOPT_HTTPHEADER]) { + return false; + } + unset($arg[CURLOPT_HTTPHEADER]); + + $caInfo = array_diff($arg, [ + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_URL => 'http://bar.com', + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 60, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_POSTFIELDS => 'baz=bar', + ]); + + if (count($caInfo) !== 1) { + return false; + } + + if (1 !== preg_match('/.+\/certs\/DigiCertHighAssuranceEVRootCA\.pem$/', $caInfo[CURLOPT_CAINFO])) { + return false; + } + + return true; + })) + ->once() + ->andReturn(null); + + $this->curlClient->openConnection('http://bar.com', 'POST', 'baz=bar', [], 60); + } + + public function testCanCloseConnection() + { + $this->curlMock + ->shouldReceive('close') + ->once() + ->andReturn(null); + + $this->curlClient->closeConnection(); + } + + public function testIsolatesTheHeaderAndBody() + { + $this->curlMock + ->shouldReceive('exec') + ->once() + ->andReturn($this->fakeRawHeader . $this->fakeRawBody); + + $this->curlClient->sendRequest(); + list($rawHeader, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); + + $this->assertEquals($rawHeader, trim($this->fakeRawHeader)); + $this->assertEquals($rawBody, $this->fakeRawBody); + } + + public function testProperlyHandlesProxyHeaders() + { + $rawHeader = $this->fakeRawProxyHeader . $this->fakeRawHeader; + $this->curlMock + ->shouldReceive('exec') + ->once() + ->andReturn($rawHeader . $this->fakeRawBody); + + $this->curlClient->sendRequest(); + list($rawHeaders, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); + + $this->assertEquals($rawHeaders, trim($rawHeader)); + $this->assertEquals($rawBody, $this->fakeRawBody); + } + + public function testProperlyHandlesProxyHeadersWithCurlBug() + { + $rawHeader = $this->fakeRawProxyHeader . $this->fakeRawHeader; $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HEADER_SIZE) + ->shouldReceive('exec') ->once() - ->andReturn(mb_strlen($this->fakeRawHeader)); // Mimic bug that doesn't count proxy header + ->andReturn($rawHeader . $this->fakeRawBody); + + $this->curlClient->sendRequest(); + list($rawHeaders, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); + + $this->assertEquals($rawHeaders, trim($rawHeader)); + $this->assertEquals($rawBody, $this->fakeRawBody); + } + + public function testProperlyHandlesProxyHeadersWithCurlBug2() + { + $rawHeader = $this->fakeRawProxyHeader2 . $this->fakeRawHeader; $this->curlMock - ->shouldReceive('version') + ->shouldReceive('exec') ->once() - ->andReturn(array('version_number' => self::CURL_VERSION_BUGGY)); + ->andReturn($rawHeader . $this->fakeRawBody); + + $this->curlClient->sendRequest(); + list($rawHeaders, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); + + $this->assertEquals($rawHeaders, trim($rawHeader)); + $this->assertEquals($rawBody, $this->fakeRawBody); + } + + public function testProperlyHandlesRedirectHeaders() + { + $rawHeader = $this->fakeRawRedirectHeader . $this->fakeRawHeader; $this->curlMock ->shouldReceive('exec') ->once() @@ -320,120 +219,65 @@ public function testProperlyHandlesProxyHeadersWithCurlBug2() $this->assertEquals($rawHeaders, trim($rawHeader)); $this->assertEquals($rawBody, $this->fakeRawBody); + } + + public function testCanSendNormalRequest() + { + $this->curlMock + ->shouldReceive('init') + ->once() + ->andReturn(null); + $this->curlMock + ->shouldReceive('setoptArray') + ->once() + ->andReturn(null); + $this->curlMock + ->shouldReceive('exec') + ->once() + ->andReturn($this->fakeRawHeader . $this->fakeRawBody); + $this->curlMock + ->shouldReceive('errno') + ->once() + ->andReturn(null); + $this->curlMock + ->shouldReceive('close') + ->once() + ->andReturn(null); + + $response = $this->curlClient->send('http://foo.com/', 'GET', '', [], 60); - $headers = FacebookCurlHttpClient::headersToArray($rawHeaders); - - $this->assertEquals($headers, $this->fakeHeadersAsArray); - } - - public function testProperlyHandlesRedirectHeaders() - { - $rawHeader = $this->fakeRawRedirectHeader . $this->fakeRawHeader; - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HEADER_SIZE) - ->once() - ->andReturn(mb_strlen($rawHeader)); - $this->curlMock - ->shouldReceive('version') - ->once() - ->andReturn(array('version_number' => self::CURL_VERSION_STABLE)); - $this->curlMock - ->shouldReceive('exec') - ->once() - ->andReturn($rawHeader . $this->fakeRawBody); - - $this->curlClient->sendRequest(); - list($rawHeaders, $rawBody) = $this->curlClient->extractResponseHeadersAndBody(); - - $this->assertEquals($rawHeaders, trim($rawHeader)); - $this->assertEquals($rawBody, $this->fakeRawBody); - - $headers = FacebookCurlHttpClient::headersToArray($rawHeaders); - - $this->assertEquals($headers, $this->fakeHeadersAsArray); - } - - public function testCanSendNormalRequest() - { - $this->curlMock - ->shouldReceive('init') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('setopt_array') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('exec') - ->once() - ->andReturn($this->fakeRawHeader . $this->fakeRawBody); - $this->curlMock - ->shouldReceive('errno') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('error') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HTTP_CODE) - ->once() - ->andReturn(200); - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HEADER_SIZE) - ->once() - ->andReturn(mb_strlen($this->fakeRawHeader)); - $this->curlMock - ->shouldReceive('version') - ->once() - ->andReturn(array('version_number' => self::CURL_VERSION_STABLE)); - $this->curlMock - ->shouldReceive('close') - ->once() - ->andReturn(null); - - $responseBody = $this->curlClient->send('http://foo.com/'); - - $this->assertEquals($responseBody, $this->fakeRawBody); - $this->assertEquals($this->curlClient->getResponseHeaders(), $this->fakeHeadersAsArray); - $this->assertEquals(200, $this->curlClient->getResponseHttpStatusCode()); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testThrowsExceptionOnClientError() - { - $this->curlMock - ->shouldReceive('init') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('setopt_array') - ->once() - ->andReturn(null); - $this->curlMock - ->shouldReceive('exec') - ->once() - ->andReturn(false); - $this->curlMock - ->shouldReceive('errno') - ->once() - ->andReturn(123); - $this->curlMock - ->shouldReceive('error') - ->once() - ->andReturn('Foo error'); - $this->curlMock - ->shouldReceive('getinfo') - ->with(CURLINFO_HTTP_CODE) - ->once() - ->andReturn(null); - - $this->curlClient->send('http://foo.com/'); - } + $this->assertInstanceOf('Facebook\Http\GraphRawResponse', $response); + $this->assertEquals($this->fakeRawBody, $response->getBody()); + $this->assertEquals($this->fakeHeadersAsArray, $response->getHeaders()); + $this->assertEquals(200, $response->getHttpResponseCode()); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testThrowsExceptionOnClientError() + { + $this->curlMock + ->shouldReceive('init') + ->once() + ->andReturn(null); + $this->curlMock + ->shouldReceive('setoptArray') + ->once() + ->andReturn(null); + $this->curlMock + ->shouldReceive('exec') + ->once() + ->andReturn(false); + $this->curlMock + ->shouldReceive('errno') + ->once() + ->andReturn(123); + $this->curlMock + ->shouldReceive('error') + ->once() + ->andReturn('Foo error'); + $this->curlClient->send('http://foo.com/', 'GET', '', [], 60); + } } diff --git a/tests/HttpClients/FacebookGuzzleHttpClientTest.php b/tests/HttpClients/FacebookGuzzleHttpClientTest.php index 6905480a8..f14ad96a0 100644 --- a/tests/HttpClients/FacebookGuzzleHttpClientTest.php +++ b/tests/HttpClients/FacebookGuzzleHttpClientTest.php @@ -1,97 +1,143 @@ guzzleMock = m::mock('GuzzleHttp\Client'); + $this->guzzleClient = new FacebookGuzzleHttpClient($this->guzzleMock); + } + + public function testCanSendNormalRequest() + { + $request = new Request('GET', 'http://foo.com'); + + $body = Stream::factory($this->fakeRawBody); + $response = new Response(200, $this->fakeHeadersAsArray, $body); + + $this->guzzleMock + ->shouldReceive('createRequest') + ->once() + ->with('GET', 'http://foo.com/', m::on(function ($arg) { + + // array_diff_assoc() will sometimes trigger error on child-arrays + if (['X-foo' => 'bar'] !== $arg['headers']) { + return false; + } + unset($arg['headers']); + + $caInfo = array_diff_assoc($arg, [ + 'body' => 'foo_body', + 'timeout' => 123, + 'connect_timeout' => 10, + ]); + + if (count($caInfo) !== 1) { + return false; + } + + if (1 !== preg_match('/.+\/certs\/DigiCertHighAssuranceEVRootCA\.pem$/', $caInfo['verify'])) { + return false; + } + + return true; + })) + ->andReturn($request); + $this->guzzleMock + ->shouldReceive('send') + ->once() + ->with($request) + ->andReturn($response); + + $response = $this->guzzleClient->send('http://foo.com/', 'GET', 'foo_body', ['X-foo' => 'bar'], 123); + + $this->assertInstanceOf('Facebook\Http\GraphRawResponse', $response); + $this->assertEquals($this->fakeRawBody, $response->getBody()); + $this->assertEquals($this->fakeHeadersAsArray, $response->getHeaders()); + $this->assertEquals(200, $response->getHttpResponseCode()); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testThrowsExceptionOnClientError() + { + $request = new Request('GET', 'http://foo.com'); + + $this->guzzleMock + ->shouldReceive('createRequest') + ->once() + ->with('GET', 'http://foo.com/', m::on(function ($arg) { + + // array_diff_assoc() will sometimes trigger error on child-arrays + if ([] !== $arg['headers']) { + return false; + } + unset($arg['headers']); + + $caInfo = array_diff_assoc($arg, [ + 'body' => 'foo_body', + 'timeout' => 60, + 'connect_timeout' => 10, + ]); + + if (count($caInfo) !== 1) { + return false; + } + + if (1 !== preg_match('/.+\/certs\/DigiCertHighAssuranceEVRootCA\.pem$/', $caInfo['verify'])) { + return false; + } - protected $guzzleMock; - protected $guzzleClient; - - public function setUp() - { - $this->guzzleMock = m::mock('GuzzleHttp\Client'); - $this->guzzleClient = new FacebookGuzzleHttpClient($this->guzzleMock); - } - - public function tearDown() - { - m::close(); - (new FacebookGuzzleHttpClient()); // Resets the static dependency injection - } - - public function testCanSendNormalRequest() - { - $requestMock = m::mock('GuzzleHttp\Message\RequestInterface'); - $requestMock - ->shouldReceive('setHeader') - ->once() - ->with('X-foo', 'bar') - ->andReturn(null); - - $responseMock = m::mock('GuzzleHttp\Message\ResponseInterface'); - $responseMock - ->shouldReceive('getStatusCode') - ->once() - ->andReturn(200); - $responseMock - ->shouldReceive('getHeaders') - ->once() - ->andReturn($this->fakeHeadersAsArray); - $responseMock - ->shouldReceive('getBody') - ->once() - ->andReturn($this->fakeRawBody); - - $this->guzzleMock - ->shouldReceive('createRequest') - ->once() - ->with('GET', 'http://foo.com/', array()) - ->andReturn($requestMock); - $this->guzzleMock - ->shouldReceive('send') - ->once() - ->with($requestMock) - ->andReturn($responseMock); - - $this->guzzleClient->addRequestHeader('X-foo', 'bar'); - $responseBody = $this->guzzleClient->send('http://foo.com/'); - - $this->assertEquals($responseBody, $this->fakeRawBody); - $this->assertEquals($this->guzzleClient->getResponseHeaders(), $this->fakeHeadersAsArray); - $this->assertEquals(200, $this->guzzleClient->getResponseHttpStatusCode()); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testThrowsExceptionOnClientError() - { - $requestMock = m::mock('GuzzleHttp\Message\RequestInterface'); - $exceptionMock = m::mock( - 'GuzzleHttp\Exception\RequestException', - array( - 'Foo Error', - $requestMock, - null, - m::mock('GuzzleHttp\Exception\AdapterException'), - )); - - $this->guzzleMock - ->shouldReceive('createRequest') - ->once() - ->with('GET', 'http://foo.com/', array()) - ->andReturn($requestMock); - $this->guzzleMock - ->shouldReceive('send') - ->once() - ->with($requestMock) - ->andThrow($exceptionMock); - - $this->guzzleClient->send('http://foo.com/'); - } + return true; + })) + ->andReturn($request); + $this->guzzleMock + ->shouldReceive('send') + ->once() + ->with($request) + ->andThrow(new RequestException('Foo', $request)); + $this->guzzleClient->send('http://foo.com/', 'GET', 'foo_body', [], 60); + } } diff --git a/tests/HttpClients/FacebookStreamHttpClientTest.php b/tests/HttpClients/FacebookStreamHttpClientTest.php index 26dccc264..3749960e1 100644 --- a/tests/HttpClients/FacebookStreamHttpClientTest.php +++ b/tests/HttpClients/FacebookStreamHttpClientTest.php @@ -1,117 +1,134 @@ streamMock = m::mock('Facebook\HttpClients\FacebookStream'); - $this->streamClient = new FacebookStreamHttpClient($this->streamMock); - } - - public function tearDown() - { - m::close(); - (new FacebookStreamHttpClient()); // Resets the static dependency injection - } - - public function testCanCompileHeader() - { - $this->streamClient->addRequestHeader('X-foo', 'bar'); - $this->streamClient->addRequestHeader('X-bar', 'faz'); - $header = $this->streamClient->compileHeader(); - $this->assertEquals("X-foo: bar\r\nX-bar: faz", $header); - } - - public function testCanFormatHeadersToArray() - { - $raw_header_array = explode("\n", trim($this->fakeRawHeader)); - $header_array = FacebookStreamHttpClient::formatHeadersToArray($raw_header_array); - $this->assertEquals($this->fakeHeadersAsArray, $header_array); - } - - public function testCanGetHttpStatusCodeFromResponseHeader() - { - $http_code = FacebookStreamHttpClient::getStatusCodeFromHeader('HTTP/1.1 123 Foo Response'); - $this->assertEquals('123', $http_code); - } - - public function testCanSendNormalRequest() - { - $this->streamMock - ->shouldReceive('streamContextCreate') - ->once() - ->with(\Mockery::on(function($arg) { - if (!isset($arg['http']) || !isset($arg['ssl'])) { - return false; - } - - if ($arg['http'] !== array( - 'method' => 'GET', - 'timeout' => 60, - 'ignore_errors' => true, - 'header' => 'X-foo: bar', - )) { - return false; - } - - if ($arg['ssl']['verify_peer'] !== true) { - return false; - } - - if (false === preg_match('/.fb_ca_chain_bundle\.crt$/', $arg['ssl']['cafile'])) { - return false; - } - - return true; - })) - ->andReturn(null); - $this->streamMock - ->shouldReceive('getResponseHeaders') - ->once() - ->andReturn(explode("\n", trim($this->fakeRawHeader))); - $this->streamMock - ->shouldReceive('fileGetContents') - ->once() - ->with('http://foo.com/') - ->andReturn($this->fakeRawBody); - - $this->streamClient->addRequestHeader('X-foo', 'bar'); - $responseBody = $this->streamClient->send('http://foo.com/'); - - $this->assertEquals($responseBody, $this->fakeRawBody); - $this->assertEquals($this->streamClient->getResponseHeaders(), $this->fakeHeadersAsArray); - $this->assertEquals(200, $this->streamClient->getResponseHttpStatusCode()); - } - - /** - * @expectedException \Facebook\FacebookSDKException - */ - public function testThrowsExceptionOnClientError() - { - $this->streamMock - ->shouldReceive('streamContextCreate') - ->once() - ->andReturn(null); - $this->streamMock - ->shouldReceive('getResponseHeaders') - ->once() - ->andReturn(null); - $this->streamMock - ->shouldReceive('fileGetContents') - ->once() - ->with('http://foo.com/') - ->andReturn(false); - - $this->streamClient->send('http://foo.com/'); - } - + /** + * @var \Facebook\HttpClients\FacebookStream + */ + protected $streamMock; + + /** + * @var FacebookStreamHttpClient + */ + protected $streamClient; + + protected function setUp() + { + $this->streamMock = m::mock('Facebook\HttpClients\FacebookStream'); + $this->streamClient = new FacebookStreamHttpClient($this->streamMock); + } + + public function testCanCompileHeader() + { + $headers = [ + 'X-foo' => 'bar', + 'X-bar' => 'faz', + ]; + $header = $this->streamClient->compileHeader($headers); + $this->assertEquals("X-foo: bar\r\nX-bar: faz", $header); + } + + public function testCanSendNormalRequest() + { + $this->streamMock + ->shouldReceive('streamContextCreate') + ->once() + ->with(m::on(function ($arg) { + if (!isset($arg['http']) || !isset($arg['ssl'])) { + return false; + } + + if ($arg['http'] !== [ + 'method' => 'GET', + 'header' => 'X-foo: bar', + 'content' => 'foo_body', + 'timeout' => 123, + 'ignore_errors' => true, + ] + ) { + return false; + } + + $caInfo = array_diff_assoc($arg['ssl'], [ + 'verify_peer' => true, + 'verify_peer_name' => true, + 'allow_self_signed' => true, + ]); + + if (count($caInfo) !== 1) { + return false; + } + + if (1 !== preg_match('/.+\/certs\/DigiCertHighAssuranceEVRootCA\.pem$/', $caInfo['cafile'])) { + return false; + } + + return true; + })) + ->andReturn(null); + $this->streamMock + ->shouldReceive('getResponseHeaders') + ->once() + ->andReturn(explode("\n", trim($this->fakeRawHeader))); + $this->streamMock + ->shouldReceive('fileGetContents') + ->once() + ->with('http://foo.com/') + ->andReturn($this->fakeRawBody); + + $response = $this->streamClient->send('http://foo.com/', 'GET', 'foo_body', ['X-foo' => 'bar'], 123); + + $this->assertInstanceOf('Facebook\Http\GraphRawResponse', $response); + $this->assertEquals($this->fakeRawBody, $response->getBody()); + $this->assertEquals($this->fakeHeadersAsArray, $response->getHeaders()); + $this->assertEquals(200, $response->getHttpResponseCode()); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testThrowsExceptionOnClientError() + { + $this->streamMock + ->shouldReceive('streamContextCreate') + ->once() + ->andReturn(null); + $this->streamMock + ->shouldReceive('getResponseHeaders') + ->once() + ->andReturn(null); + $this->streamMock + ->shouldReceive('fileGetContents') + ->once() + ->with('http://foo.com/') + ->andReturn(false); + + $this->streamClient->send('http://foo.com/', 'GET', 'foo_body', [], 60); + } } diff --git a/tests/HttpClients/HttpClientsFactoryTest.php b/tests/HttpClients/HttpClientsFactoryTest.php new file mode 100644 index 000000000..4d49489b9 --- /dev/null +++ b/tests/HttpClients/HttpClientsFactoryTest.php @@ -0,0 +1,72 @@ +assertInstanceOf(self::COMMON_INTERFACE, $httpClient); + $this->assertInstanceOf($expected, $httpClient); + } + + /** + * @return array + */ + public function httpClientsProvider() + { + $clients = [ + ['guzzle', self::COMMON_NAMESPACE . 'FacebookGuzzleHttpClient'], + ['stream', self::COMMON_NAMESPACE . 'FacebookStreamHttpClient'], + [new Client(), self::COMMON_NAMESPACE . 'FacebookGuzzleHttpClient'], + [new FacebookGuzzleHttpClient(), self::COMMON_NAMESPACE . 'FacebookGuzzleHttpClient'], + [new FacebookStreamHttpClient(), self::COMMON_NAMESPACE . 'FacebookStreamHttpClient'], + [null, self::COMMON_INTERFACE], + ]; + if (extension_loaded('curl')) { + $clients[] = ['curl', self::COMMON_NAMESPACE . 'FacebookCurlHttpClient']; + $clients[] = [new FacebookCurlHttpClient(), self::COMMON_NAMESPACE . 'FacebookCurlHttpClient']; + } + + return $clients; + } +} diff --git a/tests/PersistentData/FacebookMemoryPersistentDataHandlerTest.php b/tests/PersistentData/FacebookMemoryPersistentDataHandlerTest.php new file mode 100644 index 000000000..89717f831 --- /dev/null +++ b/tests/PersistentData/FacebookMemoryPersistentDataHandlerTest.php @@ -0,0 +1,46 @@ +set('foo', 'bar'); + $value = $handler->get('foo'); + + $this->assertEquals('bar', $value); + } + + public function testGettingAValueThatDoesntExistWillReturnNull() + { + $handler = new FacebookMemoryPersistentDataHandler(); + $value = $handler->get('does_not_exist'); + + $this->assertNull($value); + } +} diff --git a/tests/PersistentData/FacebookSessionPersistentDataHandlerTest.php b/tests/PersistentData/FacebookSessionPersistentDataHandlerTest.php new file mode 100644 index 000000000..752d275de --- /dev/null +++ b/tests/PersistentData/FacebookSessionPersistentDataHandlerTest.php @@ -0,0 +1,62 @@ +set('foo', 'bar'); + + $this->assertEquals('bar', $_SESSION['FBRLH_foo']); + } + + public function testCanGetAValue() + { + $_SESSION['FBRLH_faz'] = 'baz'; + $handler = new FacebookSessionPersistentDataHandler($enableSessionCheck = false); + $value = $handler->get('faz'); + + $this->assertEquals('baz', $value); + } + + public function testGettingAValueThatDoesntExistWillReturnNull() + { + $handler = new FacebookSessionPersistentDataHandler($enableSessionCheck = false); + $value = $handler->get('does_not_exist'); + + $this->assertNull($value); + } +} diff --git a/tests/PersistentData/PersistentDataFactoryTest.php b/tests/PersistentData/PersistentDataFactoryTest.php new file mode 100644 index 000000000..d6206fcdc --- /dev/null +++ b/tests/PersistentData/PersistentDataFactoryTest.php @@ -0,0 +1,68 @@ +assertInstanceOf(self::COMMON_INTERFACE, $persistentDataHandler); + $this->assertInstanceOf($expected, $persistentDataHandler); + } + + /** + * @return array + */ + public function persistentDataHandlerProviders() + { + $handlers = [ + ['memory', self::COMMON_NAMESPACE . 'FacebookMemoryPersistentDataHandler'], + [new FacebookMemoryPersistentDataHandler(), self::COMMON_NAMESPACE . 'FacebookMemoryPersistentDataHandler'], + [new FacebookSessionPersistentDataHandler(false), self::COMMON_NAMESPACE . 'FacebookSessionPersistentDataHandler'], + [null, self::COMMON_INTERFACE], + ]; + + if (session_status() === PHP_SESSION_ACTIVE) { + $handlers[] = ['session', self::COMMON_NAMESPACE . 'FacebookSessionPersistentDataHandler']; + } + + return $handlers; + } +} diff --git a/tests/PseudoRandomString/McryptPseudoRandomStringGeneratorTest.php b/tests/PseudoRandomString/McryptPseudoRandomStringGeneratorTest.php new file mode 100644 index 000000000..f5e033675 --- /dev/null +++ b/tests/PseudoRandomString/McryptPseudoRandomStringGeneratorTest.php @@ -0,0 +1,48 @@ +=')) { + $this->markTestSkipped('Skipping test mcrypt is deprecated from 7.1'); + } + + if (!function_exists('mcrypt_create_iv')) { + $this->markTestSkipped( + 'Mcrypt must be installed to test mcrypt_create_iv().' + ); + } + + $prsg = new McryptPseudoRandomStringGenerator(); + $randomString = $prsg->getPseudoRandomString(10); + + $this->assertEquals(1, preg_match('/^([0-9a-f]+)$/', $randomString)); + $this->assertEquals(10, mb_strlen($randomString)); + } +} diff --git a/tests/PseudoRandomString/OpenSslPseudoRandomStringGeneratorTest.php b/tests/PseudoRandomString/OpenSslPseudoRandomStringGeneratorTest.php new file mode 100644 index 000000000..3ab4edc93 --- /dev/null +++ b/tests/PseudoRandomString/OpenSslPseudoRandomStringGeneratorTest.php @@ -0,0 +1,44 @@ +markTestSkipped( + 'The OpenSSL extension must be enabled to test openssl_random_pseudo_bytes().' + ); + } + + $prsg = new OpenSslPseudoRandomStringGenerator(); + $randomString = $prsg->getPseudoRandomString(10); + + $this->assertEquals(1, preg_match('/^([0-9a-f]+)$/', $randomString)); + $this->assertEquals(10, mb_strlen($randomString)); + } +} diff --git a/tests/PseudoRandomString/PseudoRandomStringFactoryTest.php b/tests/PseudoRandomString/PseudoRandomStringFactoryTest.php new file mode 100644 index 000000000..9dc679e66 --- /dev/null +++ b/tests/PseudoRandomString/PseudoRandomStringFactoryTest.php @@ -0,0 +1,71 @@ +assertInstanceOf(self::COMMON_INTERFACE, $pseudoRandomStringGenerator); + $this->assertInstanceOf($expected, $pseudoRandomStringGenerator); + } + + /** + * @return array + */ + public function csprngProvider() + { + $providers = [ + [null, self::COMMON_INTERFACE], + ]; + if (function_exists('random_bytes')) { + $providers[] = ['random_bytes', self::COMMON_NAMESPACE . 'RandomBytesPseudoRandomStringGenerator']; + } + if (function_exists('mcrypt_create_iv')) { + $providers[] = ['mcrypt', self::COMMON_NAMESPACE . 'McryptPseudoRandomStringGenerator']; + } + if (function_exists('openssl_random_pseudo_bytes')) { + $providers[] = ['openssl', self::COMMON_NAMESPACE . 'OpenSslPseudoRandomStringGenerator']; + } + if (!ini_get('open_basedir') && is_readable('/dev/urandom')) { + $providers[] = ['urandom', self::COMMON_NAMESPACE . 'UrandomPseudoRandomStringGenerator']; + } + + return $providers; + } +} diff --git a/tests/PseudoRandomString/PseudoRandomStringGeneratorTraitTest.php b/tests/PseudoRandomString/PseudoRandomStringGeneratorTraitTest.php new file mode 100644 index 000000000..16165d98c --- /dev/null +++ b/tests/PseudoRandomString/PseudoRandomStringGeneratorTraitTest.php @@ -0,0 +1,47 @@ +validateLength('foo_len'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testALengthThatIsNotAtLeastOneCharacterWillThrow() + { + $prsg = new MyFooBarPseudoRandomStringGenerator(); + $prsg->validateLength(0); + } +} diff --git a/src/Facebook/HttpClients/FacebookHttpable.php b/tests/PseudoRandomString/RandomBytesPseudoRandomStringGeneratorTest.php old mode 100755 new mode 100644 similarity index 55% rename from src/Facebook/HttpClients/FacebookHttpable.php rename to tests/PseudoRandomString/RandomBytesPseudoRandomStringGeneratorTest.php index e3afaf392..61f2a46a2 --- a/src/Facebook/HttpClients/FacebookHttpable.php +++ b/tests/PseudoRandomString/RandomBytesPseudoRandomStringGeneratorTest.php @@ -1,6 +1,6 @@ markTestSkipped( + 'Must have PHP 7 or paragonie/random_compat installed to test random_bytes().' + ); + } - /** - * Sends a request to the server - * - * @param string $url The endpoint to send the request to - * @param string $method The request method - * @param array $parameters The key value pairs to be sent in the body - * - * @return string Raw response from the server - * - * @throws \Facebook\FacebookSDKException - */ - public function send($url, $method = 'GET', $parameters = array()); + $csprng = new RandomBytesPseudoRandomStringGenerator; + $randomString = $csprng->getPseudoRandomString(10); + $this->assertEquals(1, preg_match('/^([0-9a-f]+)$/', $randomString)); + $this->assertEquals(10, strlen($randomString)); + } } diff --git a/tests/PseudoRandomString/UrandomPseudoRandomStringGeneratorTest.php b/tests/PseudoRandomString/UrandomPseudoRandomStringGeneratorTest.php new file mode 100644 index 000000000..e67136fe2 --- /dev/null +++ b/tests/PseudoRandomString/UrandomPseudoRandomStringGeneratorTest.php @@ -0,0 +1,50 @@ +markTestSkipped( + 'Cannot test /dev/urandom generator due to open_basedir constraint.' + ); + } + + if (!is_readable('/dev/urandom')) { + $this->markTestSkipped( + '/dev/urandom not found or is not readable.' + ); + } + + $prsg = new UrandomPseudoRandomStringGenerator(); + $randomString = $prsg->getPseudoRandomString(10); + + $this->assertEquals(1, preg_match('/^([0-9a-f]+)$/', $randomString)); + $this->assertEquals(10, mb_strlen($randomString)); + } +} diff --git a/tests/SignedRequestTest.php b/tests/SignedRequestTest.php new file mode 100644 index 000000000..119f27ce1 --- /dev/null +++ b/tests/SignedRequestTest.php @@ -0,0 +1,139 @@ + 'foo_token', + 'algorithm' => 'HMAC-SHA256', + 'issued_at' => 321, + 'code' => 'foo_code', + 'state' => 'foo_state', + 'user_id' => 123, + 'foo' => 'bar', + ]; + + protected function setUp() + { + $this->app = new FacebookApp('123', 'foo_app_secret'); + } + + public function testAValidSignedRequestCanBeCreated() + { + $sr = new SignedRequest($this->app); + $rawSignedRequest = $sr->make($this->payloadData); + + $srTwo = new SignedRequest($this->app, $rawSignedRequest); + $payload = $srTwo->getPayload(); + + $expectedRawSignedRequest = $this->rawSignature . '.' . $this->rawPayload; + $this->assertEquals($expectedRawSignedRequest, $rawSignedRequest); + $this->assertEquals($this->payloadData, $payload); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testInvalidSignedRequestsWillFailFormattingValidation() + { + new SignedRequest($this->app, 'invalid_signed_request'); + } + + public function testBase64EncodingIsUrlSafe() + { + $sr = new SignedRequest($this->app); + $encodedData = $sr->base64UrlEncode('aijkoprstADIJKLOPQTUVX1256!)]-:;"<>?.|~'); + + $this->assertEquals('YWlqa29wcnN0QURJSktMT1BRVFVWWDEyNTYhKV0tOjsiPD4_Lnx-', $encodedData); + } + + public function testAUrlSafeBase64EncodedStringCanBeDecoded() + { + $sr = new SignedRequest($this->app); + $decodedData = $sr->base64UrlDecode('YWlqa29wcnN0QURJSktMT1BRVFVWWDEyNTYhKV0tOjsiPD4/Lnx+'); + + $this->assertEquals('aijkoprstADIJKLOPQTUVX1256!)]-:;"<>?.|~', $decodedData); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAnImproperlyEncodedSignatureWillThrowAnException() + { + new SignedRequest($this->app, 'foo_sig.' . $this->rawPayload); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testAnImproperlyEncodedPayloadWillThrowAnException() + { + new SignedRequest($this->app, $this->rawSignature . '.foo_payload'); + } + + /** + * @expectedException \Facebook\Exceptions\FacebookSDKException + */ + public function testNonApprovedAlgorithmsWillThrowAnException() + { + $signedRequestData = $this->payloadData; + $signedRequestData['algorithm'] = 'FOO-ALGORITHM'; + + $sr = new SignedRequest($this->app); + $rawSignedRequest = $sr->make($signedRequestData); + + new SignedRequest($this->app, $rawSignedRequest); + } + + public function testAsRawSignedRequestCanBeValidatedAndDecoded() + { + $rawSignedRequest = $this->rawSignature . '.' . $this->rawPayload; + $sr = new SignedRequest($this->app, $rawSignedRequest); + + $this->assertEquals($this->payloadData, $sr->getPayload()); + } + + public function testARawSignedRequestCanBeValidatedAndDecoded() + { + $rawSignedRequest = $this->rawSignature . '.' . $this->rawPayload; + $sr = new SignedRequest($this->app, $rawSignedRequest); + + $this->assertEquals($sr->getPayload(), $this->payloadData); + $this->assertEquals($sr->getRawSignedRequest(), $rawSignedRequest); + $this->assertEquals(123, $sr->getUserId()); + $this->assertTrue($sr->hasOAuthData()); + } +} diff --git a/tests/Url/FacebookUrlDetectionHandlerTest.php b/tests/Url/FacebookUrlDetectionHandlerTest.php new file mode 100644 index 000000000..b623c0524 --- /dev/null +++ b/tests/Url/FacebookUrlDetectionHandlerTest.php @@ -0,0 +1,134 @@ + 'foo.bar', + 'SERVER_PORT' => '80', + 'REQUEST_URI' => '/baz?foo=123', + ]; + + $urlHandler = new FacebookUrlDetectionHandler(); + $currentUri = $urlHandler->getCurrentUrl(); + + $this->assertEquals('http://foo.bar/baz?foo=123', $currentUri); + } + + public function testProperlyGeneratesSecureUrlFromCommonScenario() + { + $_SERVER = [ + 'HTTP_HOST' => 'foo.bar', + 'SERVER_PORT' => '443', + 'REQUEST_URI' => '/baz?foo=123', + ]; + + $urlHandler = new FacebookUrlDetectionHandler(); + $currentUri = $urlHandler->getCurrentUrl(); + + $this->assertEquals('https://foo.bar/baz?foo=123', $currentUri); + } + + public function testProperlyGeneratesUrlFromProxy() + { + $_SERVER = [ + 'HTTP_X_FORWARDED_PORT' => '80', + 'HTTP_X_FORWARDED_PROTO' => 'http', + 'HTTP_HOST' => 'foo.bar', + 'SERVER_PORT' => '80', + 'REQUEST_URI' => '/baz?foo=123', + ]; + + $urlHandler = new FacebookUrlDetectionHandler(); + $currentUri = $urlHandler->getCurrentUrl(); + + $this->assertEquals('http://foo.bar/baz?foo=123', $currentUri); + } + + public function testProperlyGeneratesSecureUrlFromProxy() + { + $_SERVER = [ + 'HTTP_X_FORWARDED_PORT' => '443', + 'HTTP_X_FORWARDED_PROTO' => 'https', + 'HTTP_HOST' => 'foo.bar', + 'SERVER_PORT' => '80', + 'REQUEST_URI' => '/baz?foo=123', + ]; + + $urlHandler = new FacebookUrlDetectionHandler(); + $currentUri = $urlHandler->getCurrentUrl(); + + $this->assertEquals('https://foo.bar/baz?foo=123', $currentUri); + } + + public function testProperlyGeneratesUrlWithCustomPort() + { + $_SERVER = [ + 'HTTP_HOST' => 'foo.bar', + 'SERVER_PORT' => '1337', + 'REQUEST_URI' => '/foo.php', + ]; + + $urlHandler = new FacebookUrlDetectionHandler(); + $currentUri = $urlHandler->getCurrentUrl(); + + $this->assertEquals('http://foo.bar:1337/foo.php', $currentUri); + } + + public function testProperlyGeneratesSecureUrlWithCustomPort() + { + $_SERVER = [ + 'HTTP_HOST' => 'foo.bar', + 'SERVER_PORT' => '1337', + 'REQUEST_URI' => '/foo.php', + 'HTTPS' => 'On', + ]; + + $urlHandler = new FacebookUrlDetectionHandler(); + $currentUri = $urlHandler->getCurrentUrl(); + + $this->assertEquals('https://foo.bar:1337/foo.php', $currentUri); + } + + public function testProperlyGeneratesUrlWithCustomPortFromProxy() + { + $_SERVER = [ + 'HTTP_X_FORWARDED_PORT' => '8888', + 'HTTP_X_FORWARDED_PROTO' => 'http', + 'HTTP_HOST' => 'foo.bar', + 'SERVER_PORT' => '80', + 'REQUEST_URI' => '/foo.php', + ]; + + $urlHandler = new FacebookUrlDetectionHandler(); + $currentUri = $urlHandler->getCurrentUrl(); + + $this->assertEquals('http://foo.bar:8888/foo.php', $currentUri); + } +} diff --git a/tests/Url/FacebookUrlManipulatorTest.php b/tests/Url/FacebookUrlManipulatorTest.php new file mode 100644 index 000000000..d92a2857a --- /dev/null +++ b/tests/Url/FacebookUrlManipulatorTest.php @@ -0,0 +1,217 @@ +assertEquals($expectedCleanUrl, $currentUri); + } + + public function provideUris() + { + return [ + [ + 'http://localhost/something?state=0000&foo=bar&code=abcd', + 'http://localhost/something?foo=bar', + ], + [ + 'https://localhost/something?state=0000&foo=bar&code=abcd', + 'https://localhost/something?foo=bar', + ], + [ + 'http://localhost/something?state=0000&foo=bar&error=abcd&error_reason=abcd&error_description=abcd&error_code=1', + 'http://localhost/something?foo=bar', + ], + [ + 'https://localhost/something?state=0000&foo=bar&error=abcd&error_reason=abcd&error_description=abcd&error_code=1', + 'https://localhost/something?foo=bar', + ], + [ + 'http://localhost/something?state=0000&foo=bar&error=abcd', + 'http://localhost/something?foo=bar', + ], + [ + 'https://localhost/something?state=0000&foo=bar&error=abcd', + 'https://localhost/something?foo=bar', + ], + [ + 'https://localhost:1337/something?state=0000&foo=bar&error=abcd', + 'https://localhost:1337/something?foo=bar', + ], + [ + 'https://localhost:1337/something?state=0000&code=foo', + 'https://localhost:1337/something', + ], + [ + 'https://localhost/something/?state=0000&code=foo&foo=bar', + 'https://localhost/something/?foo=bar', + ], + [ + 'https://localhost/something/?state=0000&code=foo', + 'https://localhost/something/', + ], + ]; + } + + public function testGracefullyHandlesUrlAppending() + { + $params = []; + $url = 'https://www.foo.com/'; + $processed_url = FacebookUrlManipulator::appendParamsToUrl($url, $params); + $this->assertEquals('https://www.foo.com/', $processed_url); + + $params = [ + 'access_token' => 'foo', + ]; + $url = 'https://www.foo.com/'; + $processed_url = FacebookUrlManipulator::appendParamsToUrl($url, $params); + $this->assertEquals('https://www.foo.com/?access_token=foo', $processed_url); + + $params = [ + 'access_token' => 'foo', + 'bar' => 'baz', + ]; + $url = 'https://www.foo.com/?foo=bar'; + $processed_url = FacebookUrlManipulator::appendParamsToUrl($url, $params); + $this->assertEquals('https://www.foo.com/?access_token=foo&bar=baz&foo=bar', $processed_url); + + $params = [ + 'access_token' => 'foo', + ]; + $url = 'https://www.foo.com/?foo=bar&access_token=bar'; + $processed_url = FacebookUrlManipulator::appendParamsToUrl($url, $params); + $this->assertEquals('https://www.foo.com/?access_token=bar&foo=bar', $processed_url); + } + + public function testSlashesAreProperlyPrepended() + { + $slashTestOne = FacebookUrlManipulator::forceSlashPrefix('foo'); + $slashTestTwo = FacebookUrlManipulator::forceSlashPrefix('/foo'); + $slashTestThree = FacebookUrlManipulator::forceSlashPrefix('foo/bar'); + $slashTestFour = FacebookUrlManipulator::forceSlashPrefix('/foo/bar'); + $slashTestFive = FacebookUrlManipulator::forceSlashPrefix(null); + $slashTestSix = FacebookUrlManipulator::forceSlashPrefix(''); + + $this->assertEquals('/foo', $slashTestOne); + $this->assertEquals('/foo', $slashTestTwo); + $this->assertEquals('/foo/bar', $slashTestThree); + $this->assertEquals('/foo/bar', $slashTestFour); + $this->assertEquals(null, $slashTestFive); + $this->assertEquals('', $slashTestSix); + } + + public function testParamsCanBeReturnedAsArray() + { + $paramsOne = FacebookUrlManipulator::getParamsAsArray('/foo'); + $paramsTwo = FacebookUrlManipulator::getParamsAsArray('/foo?one=1&two=2'); + $paramsThree = FacebookUrlManipulator::getParamsAsArray('https://www.foo.com'); + $paramsFour = FacebookUrlManipulator::getParamsAsArray('https://www.foo.com/?'); + $paramsFive = FacebookUrlManipulator::getParamsAsArray('https://www.foo.com/?foo=bar'); + + $this->assertEquals([], $paramsOne); + $this->assertEquals(['one' => '1', 'two' => '2'], $paramsTwo); + $this->assertEquals([], $paramsThree); + $this->assertEquals([], $paramsFour); + $this->assertEquals(['foo' => 'bar'], $paramsFive); + } + + /** + * @dataProvider provideMergableEndpoints + */ + public function testParamsCanBeMergedOntoUrlProperly($urlOne, $urlTwo, $expected) + { + $result = FacebookUrlManipulator::mergeUrlParams($urlOne, $urlTwo); + + $this->assertEquals($result, $expected); + } + + public function provideMergableEndpoints() + { + return [ + [ + 'https://www.foo.com/?foo=ignore_foo&dance=fun', + '/me?foo=keep_foo', + '/me?dance=fun&foo=keep_foo', + ], + [ + 'https://www.bar.com?', + 'https://foo.com?foo=bar', + 'https://foo.com?foo=bar', + ], + [ + 'you', + 'me', + 'me', + ], + [ + '/1234?swing=fun', + '/1337?bar=baz&west=coast', + '/1337?bar=baz&swing=fun&west=coast', + ], + ]; + } + + public function testGraphUrlsCanBeTrimmed() + { + $fullGraphUrl = 'https://graph.facebook.com/'; + $baseGraphUrl = FacebookUrlManipulator::baseGraphUrlEndpoint($fullGraphUrl); + $this->assertEquals('/', $baseGraphUrl); + + $fullGraphUrl = 'https://graph.facebook.com/v1.0/'; + $baseGraphUrl = FacebookUrlManipulator::baseGraphUrlEndpoint($fullGraphUrl); + $this->assertEquals('/', $baseGraphUrl); + + $fullGraphUrl = 'https://graph.facebook.com/me'; + $baseGraphUrl = FacebookUrlManipulator::baseGraphUrlEndpoint($fullGraphUrl); + $this->assertEquals('/me', $baseGraphUrl); + + $fullGraphUrl = 'https://graph.beta.facebook.com/me'; + $baseGraphUrl = FacebookUrlManipulator::baseGraphUrlEndpoint($fullGraphUrl); + $this->assertEquals('/me', $baseGraphUrl); + + $fullGraphUrl = 'https://whatever-they-want.facebook.com/v2.1/me'; + $baseGraphUrl = FacebookUrlManipulator::baseGraphUrlEndpoint($fullGraphUrl); + $this->assertEquals('/me', $baseGraphUrl); + + $fullGraphUrl = 'https://graph.facebook.com/v5.301/1233?foo=bar'; + $baseGraphUrl = FacebookUrlManipulator::baseGraphUrlEndpoint($fullGraphUrl); + $this->assertEquals('/1233?foo=bar', $baseGraphUrl); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 38f0d3d61..49fba281d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,35 +1,31 @@