├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── tests.yml ├── .php_cs ├── .styleci.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── composer.json ├── phpcs.xml.dist └── src ├── Application.php ├── Authentication ├── AccessToken.php ├── AccessTokenMetadata.php └── OAuth2Client.php ├── BatchRequest.php ├── BatchResponse.php ├── Client.php ├── Exception ├── AuthenticationException.php ├── AuthorizationException.php ├── ClientException.php ├── OtherException.php ├── ResponseException.php ├── ResumableUploadException.php ├── SDKException.php ├── ServerException.php └── ThrottleException.php ├── Facebook.php ├── FileUpload ├── File.php ├── Mimetypes.php ├── ResumableUploader.php ├── TransferChunk.php └── Video.php ├── GraphNode ├── Birthday.php ├── GraphAchievement.php ├── GraphAlbum.php ├── GraphApplication.php ├── GraphCoverPhoto.php ├── GraphEdge.php ├── GraphEvent.php ├── GraphGroup.php ├── GraphLocation.php ├── GraphNode.php ├── GraphNodeFactory.php ├── GraphPage.php ├── GraphPicture.php ├── GraphSessionInfo.php └── GraphUser.php ├── Helper ├── CanvasHelper.php ├── JavaScriptHelper.php ├── PageTabHelper.php ├── RedirectLoginHelper.php └── SignedRequestFromInputHelper.php ├── Http ├── RequestBodyInterface.php ├── RequestBodyMultipart.php └── RequestBodyUrlEncoded.php ├── PersistentData ├── InMemoryPersistentDataHandler.php ├── PersistentDataFactory.php ├── PersistentDataInterface.php └── SessionPersistentDataHandler.php ├── Request.php ├── Response.php ├── SignedRequest.php └── Url ├── UrlDetectionHandler.php ├── UrlDetectionInterface.php └── UrlManipulator.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [joelbutcher] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://paypal.me/joelbutcher"] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Create a report to help us improve 4 | title: "[bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment context** 27 | - SDK version: #.#.# 28 | - Graph version: #.#.# 29 | - PHP version: #.#.# 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What problem does this feature request solve?.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe your proposed solution** 14 | A clear and concise description of what you think should happen. 15 | 16 | **Describe alternatives you've considered** 17 | Where possible, please describe alternative solutions you have considered and subsequently opted against and why they weren’t implemented. 18 | 19 | **Environment context** 20 | - SDK version: #.#.# 21 | - Graph version: #.#.# 22 | - PHP version: #.#.# 23 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | tests: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | php: [7.3, 7.4, 8.0] 17 | 18 | name: PHP ${{ matrix.php }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php }} 28 | extensions: dom, curl 29 | tools: composer:v2 30 | coverage: none 31 | 32 | - name: Install dependencies 33 | run: composer update --prefer-dist --no-interaction --no-progress 34 | 35 | - name: Execute tests 36 | run: vendor/bin/phpunit --verbose 37 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(false) 5 | ->setRules([ 6 | '@Symfony' => false, 7 | 'align_multiline_comment' => true, 8 | 'combine_consecutive_unsets' => true, 9 | // one should use PHPUnit methods to set up expected exception instead of annotations 10 | 'heredoc_to_nowdoc' => true, 11 | 'no_null_property_initialization' => true, 12 | 'no_useless_else' => true, 13 | 'phpdoc_add_missing_param_annotation' => true, 14 | 'phpdoc_order' => true, 15 | 'phpdoc_types_order' => true, 16 | 'phpdoc_align' => true, 17 | 'phpdoc_indent' => true, 18 | 'phpdoc_inline_tag' => true, 19 | 'phpdoc_order' => true, 20 | 'phpdoc_scalar' => true, 21 | 'phpdoc_separation' => true, 22 | 'phpdoc_summary' => true, 23 | 'phpdoc_trim' => true, 24 | 'phpdoc_types' => true, 25 | 'phpdoc_var_without_name' => true, 26 | 'phpdoc_annotation_without_dot' => true, 27 | 'no_empty_comment' => true, 28 | 'no_trailing_whitespace_in_comment' => true, 29 | ]) 30 | ; 31 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: recommended 2 | 3 | finder: 4 | path: 5 | - "src" 6 | - "tests" 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at joel@joelbutcher.co.uk. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Joel Butcher and Facebook, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joelbutcher/facebook-graph-sdk", 3 | "description": "Facebook SDK for PHP", 4 | "keywords": ["facebook", "sdk", "php"], 5 | "type": "library", 6 | "homepage": "https://github.com/joelbutcher/facebook-graph-sdk", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Joel Butcher", 11 | "homepage": "https://github.com/joelbutcher/facebook-graph-sdk/contributors" 12 | } 13 | ], 14 | "config": { 15 | "sort-packages": true, 16 | "allow-plugins": { 17 | "php-http/discovery": true 18 | } 19 | }, 20 | "require": { 21 | "php": "^7.3 || ^8.0", 22 | "guzzlehttp/guzzle": "^7.9", 23 | "php-http/discovery": "^1.0", 24 | "psr/http-client": "^1.0", 25 | "psr/http-factory": "^1.0", 26 | "psr/http-message": "^1.0|^2.0" 27 | }, 28 | "require-dev": { 29 | "guzzlehttp/psr7": "^2.0", 30 | "mockery/mockery": "^1.0", 31 | "phpunit/phpunit": "^9.3", 32 | "symfony/var-dumper": "^7.2" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Facebook\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Facebook\\Tests\\": "tests/" 42 | } 43 | }, 44 | "extra": { 45 | "branch-alias": { 46 | "dev-master": "7.x-dev" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | src/ 4 | tests/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | id = (string) $id; 56 | $this->secret = $secret; 57 | } 58 | 59 | /** 60 | * Returns the app ID. 61 | * 62 | * @return string 63 | */ 64 | public function getId() 65 | { 66 | return $this->id; 67 | } 68 | 69 | /** 70 | * Returns the app secret. 71 | * 72 | * @return string 73 | */ 74 | public function getSecret() 75 | { 76 | return $this->secret; 77 | } 78 | 79 | /** 80 | * Returns an app access token. 81 | * 82 | * @return AccessToken 83 | */ 84 | public function getAccessToken() 85 | { 86 | return new AccessToken($this->id.'|'.$this->secret); 87 | } 88 | 89 | /** 90 | * Serializes the Application entity as a string. 91 | * 92 | * @return string 93 | */ 94 | public function serialize() 95 | { 96 | return implode('|', [$this->id, $this->secret]); 97 | } 98 | 99 | /** 100 | * Unserializes a string as an Application entity. 101 | * 102 | * @param string $serialized 103 | */ 104 | public function unserialize($serialized) 105 | { 106 | list($id, $secret) = explode('|', $serialized); 107 | 108 | $this->__construct($id, $secret); 109 | } 110 | 111 | public function __serialize() 112 | { 113 | return [ 114 | 'id' => $this->id, 115 | 'secret' => $this->secret, 116 | ]; 117 | } 118 | 119 | public function __unserialize(array $data) 120 | { 121 | $this->__construct($data['id'], $data['secret']); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Authentication/AccessToken.php: -------------------------------------------------------------------------------- 1 | value = $accessToken; 51 | if ($expiresAt) { 52 | $this->setExpiresAtFromTimeStamp($expiresAt); 53 | } 54 | } 55 | 56 | /** 57 | * Generate an app secret proof to sign a request to Graph. 58 | * 59 | * @param string $appSecret the app secret 60 | * 61 | * @return string 62 | */ 63 | public function getAppSecretProof($appSecret) 64 | { 65 | return hash_hmac('sha256', $this->value, $appSecret); 66 | } 67 | 68 | /** 69 | * Getter for expiresAt. 70 | * 71 | * @return null|\DateTime 72 | */ 73 | public function getExpiresAt() 74 | { 75 | return $this->expiresAt; 76 | } 77 | 78 | /** 79 | * Determines whether or not this is an app access token. 80 | * 81 | * @return bool 82 | */ 83 | public function isAppAccessToken() 84 | { 85 | return strpos($this->value, '|') !== false; 86 | } 87 | 88 | /** 89 | * Determines whether or not this is a long-lived token. 90 | * 91 | * @return bool 92 | */ 93 | public function isLongLived() 94 | { 95 | if ($this->expiresAt) { 96 | return $this->expiresAt->getTimestamp() > time() + (60 * 60 * 2); 97 | } 98 | 99 | return $this->isAppAccessToken(); 100 | } 101 | 102 | /** 103 | * Checks the expiration of the access token. 104 | * 105 | * @return null|bool 106 | */ 107 | public function isExpired() 108 | { 109 | if ($this->getExpiresAt() instanceof \DateTime) { 110 | return $this->getExpiresAt()->getTimestamp() < time(); 111 | } 112 | 113 | if ($this->isAppAccessToken()) { 114 | return false; 115 | } 116 | 117 | return null; 118 | } 119 | 120 | /** 121 | * Returns the access token as a string. 122 | * 123 | * @return string 124 | */ 125 | public function getValue() 126 | { 127 | return $this->value; 128 | } 129 | 130 | /** 131 | * Returns the access token as a string. 132 | * 133 | * @return string 134 | */ 135 | public function __toString() 136 | { 137 | return $this->getValue(); 138 | } 139 | 140 | /** 141 | * Setter for expires_at. 142 | * 143 | * @param int $timeStamp 144 | */ 145 | protected function setExpiresAtFromTimeStamp($timeStamp) 146 | { 147 | $dt = new \DateTime(); 148 | $dt->setTimestamp($timeStamp); 149 | $this->expiresAt = $dt; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Authentication/AccessTokenMetadata.php: -------------------------------------------------------------------------------- 1 | metadata = $metadata['data']; 62 | 63 | $this->castTimestampsToDateTime(); 64 | } 65 | 66 | /** 67 | * Returns a value from the metadata. 68 | * 69 | * @param string $field the property to retrieve 70 | * @param mixed $default the default to return if the property doesn't exist 71 | * 72 | * @return mixed 73 | */ 74 | public function getField($field, $default = null) 75 | { 76 | if (isset($this->metadata[$field])) { 77 | return $this->metadata[$field]; 78 | } 79 | 80 | return $default; 81 | } 82 | 83 | /** 84 | * Returns a value from a child property in the metadata. 85 | * 86 | * @param string $parentField the parent property 87 | * @param string $field the property to retrieve 88 | * @param mixed $default the default to return if the property doesn't exist 89 | * 90 | * @return mixed 91 | */ 92 | public function getChildProperty($parentField, $field, $default = null) 93 | { 94 | if (!isset($this->metadata[$parentField])) { 95 | return $default; 96 | } 97 | 98 | if (!isset($this->metadata[$parentField][$field])) { 99 | return $default; 100 | } 101 | 102 | return $this->metadata[$parentField][$field]; 103 | } 104 | 105 | /** 106 | * Returns a value from the error metadata. 107 | * 108 | * @param string $field the property to retrieve 109 | * @param mixed $default the default to return if the property doesn't exist 110 | * 111 | * @return mixed 112 | */ 113 | public function getErrorProperty($field, $default = null) 114 | { 115 | return $this->getChildProperty('error', $field, $default); 116 | } 117 | 118 | /** 119 | * Returns a value from the "metadata" metadata. *Brain explodes*. 120 | * 121 | * @param string $field the property to retrieve 122 | * @param mixed $default the default to return if the property doesn't exist 123 | * 124 | * @return mixed 125 | */ 126 | public function getMetadataProperty($field, $default = null) 127 | { 128 | return $this->getChildProperty('metadata', $field, $default); 129 | } 130 | 131 | /** 132 | * The ID of the application this access token is for. 133 | * 134 | * @return null|string 135 | */ 136 | public function getAppId() 137 | { 138 | return $this->getField('app_id'); 139 | } 140 | 141 | /** 142 | * Name of the application this access token is for. 143 | * 144 | * @return null|string 145 | */ 146 | public function getApplication() 147 | { 148 | return $this->getField('application'); 149 | } 150 | 151 | /** 152 | * Any error that a request to the graph api 153 | * would return due to the access token. 154 | * 155 | * @return null|bool 156 | */ 157 | public function isError() 158 | { 159 | return $this->getField('error') !== null; 160 | } 161 | 162 | /** 163 | * The error code for the error. 164 | * 165 | * @return null|int 166 | */ 167 | public function getErrorCode() 168 | { 169 | return $this->getErrorProperty('code'); 170 | } 171 | 172 | /** 173 | * The error message for the error. 174 | * 175 | * @return null|string 176 | */ 177 | public function getErrorMessage() 178 | { 179 | return $this->getErrorProperty('message'); 180 | } 181 | 182 | /** 183 | * The error subcode for the error. 184 | * 185 | * @return null|int 186 | */ 187 | public function getErrorSubcode() 188 | { 189 | return $this->getErrorProperty('subcode'); 190 | } 191 | 192 | /** 193 | * DateTime when this access token expires. 194 | * 195 | * @return null|\DateTime 196 | */ 197 | public function getExpiresAt() 198 | { 199 | return $this->getField('expires_at'); 200 | } 201 | 202 | /** 203 | * Whether the access token is still valid or not. 204 | * 205 | * @return null|bool 206 | */ 207 | public function getIsValid() 208 | { 209 | return $this->getField('is_valid'); 210 | } 211 | 212 | /** 213 | * DateTime when this access token was issued. 214 | * 215 | * Note that the issued_at field is not returned 216 | * for short-lived access tokens. 217 | * 218 | * @see https://developers.facebook.com/docs/facebook-login/access-tokens#debug 219 | * 220 | * @return null|\DateTime 221 | */ 222 | public function getIssuedAt() 223 | { 224 | return $this->getField('issued_at'); 225 | } 226 | 227 | /** 228 | * General metadata associated with the access token. 229 | * Can contain data like 'sso', 'auth_type', 'auth_nonce'. 230 | * 231 | * @return null|array 232 | */ 233 | public function getMetadata() 234 | { 235 | return $this->getField('metadata'); 236 | } 237 | 238 | /** 239 | * The 'sso' child property from the 'metadata' parent property. 240 | * 241 | * @return null|string 242 | */ 243 | public function getSso() 244 | { 245 | return $this->getMetadataProperty('sso'); 246 | } 247 | 248 | /** 249 | * The 'auth_type' child property from the 'metadata' parent property. 250 | * 251 | * @return null|string 252 | */ 253 | public function getAuthType() 254 | { 255 | return $this->getMetadataProperty('auth_type'); 256 | } 257 | 258 | /** 259 | * The 'auth_nonce' child property from the 'metadata' parent property. 260 | * 261 | * @return null|string 262 | */ 263 | public function getAuthNonce() 264 | { 265 | return $this->getMetadataProperty('auth_nonce'); 266 | } 267 | 268 | /** 269 | * For impersonated access tokens, the ID of 270 | * the page this token contains. 271 | * 272 | * @return null|string 273 | */ 274 | public function getProfileId() 275 | { 276 | return $this->getField('profile_id'); 277 | } 278 | 279 | /** 280 | * List of permissions that the user has granted for 281 | * the app in this access token. 282 | * 283 | * @return array 284 | */ 285 | public function getScopes() 286 | { 287 | return $this->getField('scopes'); 288 | } 289 | 290 | /** 291 | * The ID of the user this access token is for. 292 | * 293 | * @return null|string 294 | */ 295 | public function getUserId() 296 | { 297 | return $this->getField('user_id'); 298 | } 299 | 300 | /** 301 | * Ensures the app ID from the access token 302 | * metadata is what we expect. 303 | * 304 | * @param string $appId 305 | * 306 | * @throws SDKException 307 | */ 308 | public function validateAppId($appId) 309 | { 310 | if ($this->getAppId() !== $appId) { 311 | throw new SDKException('Access token metadata contains unexpected app ID.', 401); 312 | } 313 | } 314 | 315 | /** 316 | * Ensures the user ID from the access token 317 | * metadata is what we expect. 318 | * 319 | * @param string $userId 320 | * 321 | * @throws SDKException 322 | */ 323 | public function validateUserId($userId) 324 | { 325 | if ($this->getUserId() !== $userId) { 326 | throw new SDKException('Access token metadata contains unexpected user ID.', 401); 327 | } 328 | } 329 | 330 | /** 331 | * Ensures the access token has not expired yet. 332 | * 333 | * @throws SDKException 334 | */ 335 | public function validateExpiration() 336 | { 337 | if (!$this->getExpiresAt() instanceof \DateTime) { 338 | return; 339 | } 340 | 341 | if ($this->getExpiresAt()->getTimestamp() < time()) { 342 | throw new SDKException('Inspection of access token metadata shows that the access token has expired.', 401); 343 | } 344 | } 345 | 346 | /** 347 | * Converts a unix timestamp into a DateTime entity. 348 | * 349 | * @param int $timestamp 350 | * 351 | * @return \DateTime 352 | */ 353 | private function convertTimestampToDateTime($timestamp) 354 | { 355 | $dt = new \DateTime(); 356 | $dt->setTimestamp($timestamp); 357 | 358 | return $dt; 359 | } 360 | 361 | /** 362 | * Casts the unix timestamps as DateTime entities. 363 | */ 364 | private function castTimestampsToDateTime() 365 | { 366 | foreach (static::$dateProperties as $key) { 367 | if (isset($this->metadata[$key]) && $this->metadata[$key] !== 0) { 368 | $this->metadata[$key] = $this->convertTimestampToDateTime($this->metadata[$key]); 369 | } 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/Authentication/OAuth2Client.php: -------------------------------------------------------------------------------- 1 | app = $app; 77 | $this->client = $client; 78 | $this->graphVersion = $graphVersion; 79 | } 80 | 81 | /** 82 | * Returns the last Request that was sent. 83 | * Useful for debugging and testing. 84 | * 85 | * @return null|Request 86 | */ 87 | public function getLastRequest() 88 | { 89 | return $this->lastRequest; 90 | } 91 | 92 | /** 93 | * Get the metadata associated with the access token. 94 | * 95 | * @param AccessToken|string $accessToken the access token to debug 96 | * 97 | * @return AccessTokenMetadata 98 | */ 99 | public function debugToken($accessToken) 100 | { 101 | $accessToken = $accessToken instanceof AccessToken ? $accessToken->getValue() : $accessToken; 102 | $params = ['input_token' => $accessToken]; 103 | 104 | $this->lastRequest = new Request( 105 | $this->app, 106 | $this->app->getAccessToken(), 107 | 'GET', 108 | '/debug_token', 109 | $params, 110 | null, 111 | $this->graphVersion 112 | ); 113 | $response = $this->client->sendRequest($this->lastRequest); 114 | $metadata = $response->getDecodedBody(); 115 | 116 | return new AccessTokenMetadata($metadata); 117 | } 118 | 119 | /** 120 | * Generates an authorization URL to begin the process of authenticating a user. 121 | * 122 | * @param string $redirectUrl the callback URL to redirect to 123 | * @param string $state the CSPRNG-generated CSRF value 124 | * @param array $scope an array of permissions to request 125 | * @param array $params an array of parameters to generate URL 126 | * @param string $separator the separator to use in http_build_query() 127 | * 128 | * @return string 129 | */ 130 | public function getAuthorizationUrl($redirectUrl, $state, array $scope = [], array $params = [], $separator = '&') 131 | { 132 | $params += [ 133 | 'client_id' => $this->app->getId(), 134 | 'state' => $state, 135 | 'response_type' => 'code', 136 | 'sdk' => 'php-sdk-'.Facebook::VERSION, 137 | 'redirect_uri' => $redirectUrl, 138 | 'scope' => implode(',', $scope), 139 | ]; 140 | 141 | return static::BASE_AUTHORIZATION_URL.'/'.$this->graphVersion.'/dialog/oauth?'.http_build_query($params, '', $separator); 142 | } 143 | 144 | /** 145 | * Get a valid access token from a code. 146 | * 147 | * @param string $code 148 | * @param string $redirectUri 149 | * 150 | * @throws SDKException 151 | * 152 | * @return AccessToken 153 | */ 154 | public function getAccessTokenFromCode($code, $redirectUri = '') 155 | { 156 | $params = [ 157 | 'code' => $code, 158 | 'redirect_uri' => $redirectUri, 159 | ]; 160 | 161 | return $this->requestAnAccessToken($params); 162 | } 163 | 164 | /** 165 | * Exchanges a short-lived access token with a long-lived access token. 166 | * 167 | * @param AccessToken|string $accessToken 168 | * 169 | * @throws SDKException 170 | * 171 | * @return AccessToken 172 | */ 173 | public function getLongLivedAccessToken($accessToken) 174 | { 175 | $accessToken = $accessToken instanceof AccessToken ? $accessToken->getValue() : $accessToken; 176 | $params = [ 177 | 'grant_type' => 'fb_exchange_token', 178 | 'fb_exchange_token' => $accessToken, 179 | ]; 180 | 181 | return $this->requestAnAccessToken($params); 182 | } 183 | 184 | /** 185 | * Get a valid code from an access token. 186 | * 187 | * @param AccessToken|string $accessToken 188 | * @param string $redirectUri 189 | * 190 | * @throws SDKException 191 | * 192 | * @return AccessToken 193 | */ 194 | public function getCodeFromLongLivedAccessToken($accessToken, $redirectUri = '') 195 | { 196 | $params = [ 197 | 'redirect_uri' => $redirectUri, 198 | ]; 199 | 200 | $response = $this->sendRequestWithClientParams('/oauth/client_code', $params, $accessToken); 201 | $data = $response->getDecodedBody(); 202 | 203 | if (!isset($data['code'])) { 204 | throw new SDKException('Code was not returned from Graph.', 401); 205 | } 206 | 207 | return $data['code']; 208 | } 209 | 210 | /** 211 | * Send a request to the OAuth endpoint. 212 | * 213 | * @param array $params 214 | * 215 | * @throws SDKException 216 | * 217 | * @return AccessToken 218 | */ 219 | protected function requestAnAccessToken(array $params) 220 | { 221 | $response = $this->sendRequestWithClientParams('/oauth/access_token', $params); 222 | $data = $response->getDecodedBody(); 223 | 224 | if (!isset($data['access_token'])) { 225 | throw new SDKException('Access token was not returned from Graph.', 401); 226 | } 227 | 228 | // Graph returns two different key names for expiration time 229 | // on the same endpoint. Doh! :/ 230 | $expiresAt = 0; 231 | if (isset($data['expires'])) { 232 | // For exchanging a short lived token with a long lived token. 233 | // The expiration time in seconds will be returned as "expires". 234 | $expiresAt = time() + $data['expires']; 235 | } elseif (isset($data['expires_in'])) { 236 | // For exchanging a code for a short lived access token. 237 | // The expiration time in seconds will be returned as "expires_in". 238 | // See: https://developers.facebook.com/docs/facebook-login/access-tokens#long-via-code 239 | $expiresAt = time() + $data['expires_in']; 240 | } 241 | 242 | return new AccessToken($data['access_token'], $expiresAt); 243 | } 244 | 245 | /** 246 | * Send a request to Graph with an app access token. 247 | * 248 | * @param string $endpoint 249 | * @param array $params 250 | * @param null|AccessToken|string $accessToken 251 | * 252 | * @throws ResponseException 253 | * 254 | * @return Response 255 | */ 256 | protected function sendRequestWithClientParams($endpoint, array $params, $accessToken = null) 257 | { 258 | $params += $this->getClientParams(); 259 | 260 | $accessToken = $accessToken ?: $this->app->getAccessToken(); 261 | 262 | $this->lastRequest = new Request( 263 | $this->app, 264 | $accessToken, 265 | 'GET', 266 | $endpoint, 267 | $params, 268 | null, 269 | $this->graphVersion 270 | ); 271 | 272 | return $this->client->sendRequest($this->lastRequest); 273 | } 274 | 275 | /** 276 | * Returns the client_* params for OAuth requests. 277 | * 278 | * @return array 279 | */ 280 | protected function getClientParams() 281 | { 282 | return [ 283 | 'client_id' => $this->app->getId(), 284 | 'client_secret' => $this->app->getSecret(), 285 | ]; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/BatchRequest.php: -------------------------------------------------------------------------------- 1 | add($requests); 57 | } 58 | 59 | /** 60 | * Adds a new request to the array. 61 | * 62 | * @param array|Request $request 63 | * @param null|array|string $options Array of batch request options e.g. 'name', 'omit_response_on_success'. 64 | * If a string is given, it is the value of the 'name' option. 65 | * 66 | * @throws \InvalidArgumentException 67 | * 68 | * @return BatchRequest 69 | * @return BatchRequest 70 | */ 71 | public function add($request, $options = null) 72 | { 73 | if (is_array($request)) { 74 | foreach ($request as $key => $req) { 75 | $this->add($req, $key); 76 | } 77 | 78 | return $this; 79 | } 80 | 81 | if (!$request instanceof Request) { 82 | throw new \InvalidArgumentException('Argument for add() must be of type array or Request.'); 83 | } 84 | 85 | if (null === $options) { 86 | $options = []; 87 | } elseif (!is_array($options)) { 88 | $options = ['name' => $options]; 89 | } 90 | 91 | $this->addFallbackDefaults($request); 92 | 93 | // File uploads 94 | $attachedFiles = $this->extractFileAttachments($request); 95 | 96 | $name = $options['name'] ?? null; 97 | 98 | unset($options['name']); 99 | 100 | $requestToAdd = [ 101 | 'name' => $name, 102 | 'request' => $request, 103 | 'options' => $options, 104 | 'attached_files' => $attachedFiles, 105 | ]; 106 | 107 | $this->requests[] = $requestToAdd; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Ensures that the Application and access token fall back when missing. 114 | * 115 | * @param Request $request 116 | * 117 | * @throws SDKException 118 | */ 119 | public function addFallbackDefaults(Request $request) 120 | { 121 | if (!$request->getApplication()) { 122 | $app = $this->getApplication(); 123 | if (!$app) { 124 | throw new SDKException('Missing Application on Request and no fallback detected on BatchRequest.'); 125 | } 126 | $request->setApp($app); 127 | } 128 | 129 | if (!$request->getAccessToken()) { 130 | $accessToken = $this->getAccessToken(); 131 | if (!$accessToken) { 132 | throw new SDKException('Missing access token on Request and no fallback detected on BatchRequest.'); 133 | } 134 | $request->setAccessToken($accessToken); 135 | } 136 | } 137 | 138 | /** 139 | * Extracts the files from a request. 140 | * 141 | * @param Request $request 142 | * 143 | * @throws SDKException 144 | * 145 | * @return null|string 146 | */ 147 | public function extractFileAttachments(Request $request) 148 | { 149 | if (!$request->containsFileUploads()) { 150 | return null; 151 | } 152 | 153 | $files = $request->getFiles(); 154 | $fileNames = []; 155 | foreach ($files as $file) { 156 | $fileName = uniqid(); 157 | $this->addFile($fileName, $file); 158 | $fileNames[] = $fileName; 159 | } 160 | 161 | $request->resetFiles(); 162 | 163 | // @TODO Does Graph support multiple uploads on one endpoint? 164 | return implode(',', $fileNames); 165 | } 166 | 167 | /** 168 | * Return the Request entities. 169 | * 170 | * @return array 171 | */ 172 | public function getRequests() 173 | { 174 | return $this->requests; 175 | } 176 | 177 | /** 178 | * Prepares the requests to be sent as a batch request. 179 | */ 180 | public function prepareRequestsForBatch() 181 | { 182 | $this->validateBatchRequestCount(); 183 | 184 | $params = [ 185 | 'batch' => $this->convertRequestsToJson(), 186 | 'include_headers' => true, 187 | ]; 188 | $this->setParams($params); 189 | } 190 | 191 | /** 192 | * Converts the requests into a JSON(P) string. 193 | * 194 | * @return string 195 | */ 196 | public function convertRequestsToJson() 197 | { 198 | $requests = []; 199 | foreach ($this->requests as $request) { 200 | $options = []; 201 | 202 | if (null !== $request['name']) { 203 | $options['name'] = $request['name']; 204 | } 205 | 206 | $options += $request['options']; 207 | 208 | $requests[] = $this->requestEntityToBatchArray($request['request'], $options, $request['attached_files']); 209 | } 210 | 211 | return json_encode($requests); 212 | } 213 | 214 | /** 215 | * Validate the request count before sending them as a batch. 216 | * 217 | * @throws SDKException 218 | */ 219 | public function validateBatchRequestCount() 220 | { 221 | $batchCount = count($this->requests); 222 | if ($batchCount === 0) { 223 | throw new SDKException('There are no batch requests to send.'); 224 | } elseif ($batchCount > 50) { 225 | // Per: https://developers.facebook.com/docs/graph-api/making-multiple-requests#limits 226 | throw new SDKException('You cannot send more than 50 batch requests at a time.'); 227 | } 228 | } 229 | 230 | /** 231 | * Converts a Request entity into an array that is batch-friendly. 232 | * 233 | * @param Request $request the request entity to convert 234 | * @param null|array|string $options Array of batch request options e.g. 'name', 'omit_response_on_success'. 235 | * If a string is given, it is the value of the 'name' option. 236 | * @param null|string $attachedFiles names of files associated with the request 237 | * 238 | * @return array 239 | */ 240 | public function requestEntityToBatchArray(Request $request, $options = null, $attachedFiles = null) 241 | { 242 | if (null === $options) { 243 | $options = []; 244 | } elseif (!is_array($options)) { 245 | $options = ['name' => $options]; 246 | } 247 | 248 | $compiledHeaders = []; 249 | $headers = $request->getHeaders(); 250 | foreach ($headers as $name => $value) { 251 | $compiledHeaders[] = $name.': '.$value; 252 | } 253 | 254 | $batch = [ 255 | 'headers' => $compiledHeaders, 256 | 'method' => $request->getMethod(), 257 | 'relative_url' => $request->getUrl(), 258 | ]; 259 | 260 | // Since file uploads are moved to the root request of a batch request, 261 | // the child requests will always be URL-encoded. 262 | $body = $request->getUrlEncodedBody()->getBody(); 263 | if ($body) { 264 | $batch['body'] = $body; 265 | } 266 | 267 | $batch += $options; 268 | 269 | if (null !== $attachedFiles) { 270 | $batch['attached_files'] = $attachedFiles; 271 | } 272 | 273 | return $batch; 274 | } 275 | 276 | /** 277 | * Get an iterator for the items. 278 | * 279 | * @return ArrayIterator 280 | */ 281 | public function getIterator() 282 | { 283 | return new ArrayIterator($this->requests); 284 | } 285 | 286 | /** 287 | * {@inheritdoc} 288 | */ 289 | public function offsetSet($offset, $value) 290 | { 291 | $this->add($value, $offset); 292 | } 293 | 294 | /** 295 | * {@inheritdoc} 296 | */ 297 | public function offsetExists($offset) 298 | { 299 | return isset($this->requests[$offset]); 300 | } 301 | 302 | /** 303 | * {@inheritdoc} 304 | */ 305 | public function offsetUnset($offset) 306 | { 307 | unset($this->requests[$offset]); 308 | } 309 | 310 | /** 311 | * {@inheritdoc} 312 | */ 313 | public function offsetGet($offset) 314 | { 315 | return $this->requests[$offset] ?? null; 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/BatchResponse.php: -------------------------------------------------------------------------------- 1 | batchRequest = $batchRequest; 51 | 52 | $request = $response->getRequest(); 53 | $body = $response->getBody(); 54 | $httpStatusCode = $response->getHttpStatusCode(); 55 | $headers = $response->getHeaders(); 56 | parent::__construct($request, $body, $httpStatusCode, $headers); 57 | 58 | $responses = $response->getDecodedBody(); 59 | $this->setResponses($responses); 60 | } 61 | 62 | /** 63 | * Returns an array of Response entities. 64 | * 65 | * @return Response[] 66 | */ 67 | public function getResponses() 68 | { 69 | return $this->responses; 70 | } 71 | 72 | /** 73 | * The main batch response will be an array of requests so 74 | * we need to iterate over all the responses. 75 | * 76 | * @param array $responses 77 | */ 78 | public function setResponses(array $responses) 79 | { 80 | $this->responses = []; 81 | 82 | foreach ($responses as $key => $graphResponse) { 83 | $this->addResponse($key, $graphResponse); 84 | } 85 | } 86 | 87 | /** 88 | * Add a response to the list. 89 | * 90 | * @param int $key 91 | * @param null|array $response 92 | */ 93 | public function addResponse($key, $response) 94 | { 95 | $originalRequestName = $this->batchRequest[$key]['name'] ?? $key; 96 | $originalRequest = $this->batchRequest[$key]['request'] ?? null; 97 | 98 | $httpResponseBody = $response['body'] ?? null; 99 | $httpResponseCode = $response['code'] ?? null; 100 | // @TODO With PHP 5.5 support, this becomes array_column($response['headers'], 'value', 'name') 101 | $httpResponseHeaders = isset($response['headers']) ? $this->normalizeBatchHeaders($response['headers']) : []; 102 | 103 | $this->responses[$originalRequestName] = new Response( 104 | $originalRequest, 105 | $httpResponseBody, 106 | $httpResponseCode, 107 | $httpResponseHeaders 108 | ); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function getIterator() 115 | { 116 | return new ArrayIterator($this->responses); 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | */ 122 | public function offsetSet($offset, $value) 123 | { 124 | $this->addResponse($offset, $value); 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | */ 130 | public function offsetExists($offset) 131 | { 132 | return isset($this->responses[$offset]); 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function offsetUnset($offset) 139 | { 140 | unset($this->responses[$offset]); 141 | } 142 | 143 | /** 144 | * {@inheritdoc} 145 | */ 146 | public function offsetGet($offset) 147 | { 148 | return $this->responses[$offset] ?? null; 149 | } 150 | 151 | /** 152 | * Converts the batch header array into a standard format. 153 | * 154 | * @TODO replace with array_column() when PHP 5.5 is supported. 155 | * 156 | * @param array $batchHeaders 157 | * 158 | * @return array 159 | */ 160 | private function normalizeBatchHeaders(array $batchHeaders) 161 | { 162 | $headers = []; 163 | 164 | foreach ($batchHeaders as $header) { 165 | $headers[$header['name']] = $header['value']; 166 | } 167 | 168 | return $headers; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient ?: Psr18ClientDiscovery::find(); 92 | $this->enableBetaMode = $enableBeta; 93 | } 94 | 95 | /** 96 | * Sets the HTTP client handler. 97 | * 98 | * @param ClientInterface $httpClient 99 | */ 100 | public function setClientInterface(ClientInterface $httpClient) 101 | { 102 | $this->httpClient = $httpClient; 103 | } 104 | 105 | /** 106 | * Returns the HTTP client handler. 107 | * 108 | * @return ClientInterface 109 | */ 110 | public function getClientInterface() 111 | { 112 | return $this->httpClient; 113 | } 114 | 115 | /** 116 | * Toggle beta mode. 117 | * 118 | * @param bool $betaMode 119 | */ 120 | public function enableBetaMode($betaMode = true) 121 | { 122 | $this->enableBetaMode = $betaMode; 123 | } 124 | 125 | /** 126 | * Returns the base Graph URL. 127 | * 128 | * @param bool $postToVideoUrl post to the video API if videos are being uploaded 129 | * 130 | * @return string 131 | */ 132 | public function getBaseGraphUrl($postToVideoUrl = false) 133 | { 134 | if ($postToVideoUrl) { 135 | return $this->enableBetaMode ? static::BASE_GRAPH_VIDEO_URL_BETA : static::BASE_GRAPH_VIDEO_URL; 136 | } 137 | 138 | return $this->enableBetaMode ? static::BASE_GRAPH_URL_BETA : static::BASE_GRAPH_URL; 139 | } 140 | 141 | /** 142 | * Prepares the request for sending to the client handler. 143 | * 144 | * @param Request $request 145 | * 146 | * @return array 147 | */ 148 | public function prepareRequestMessage(Request $request) 149 | { 150 | $postToVideoUrl = $request->containsVideoUploads(); 151 | $url = $this->getBaseGraphUrl($postToVideoUrl).$request->getUrl(); 152 | 153 | // If we're sending files they should be sent as multipart/form-data 154 | if ($request->containsFileUploads()) { 155 | $requestBody = $request->getMultipartBody(); 156 | $request->setHeaders([ 157 | 'Content-Type' => 'multipart/form-data; boundary='.$requestBody->getBoundary(), 158 | ]); 159 | } else { 160 | $requestBody = $request->getUrlEncodedBody(); 161 | $request->setHeaders([ 162 | 'Content-Type' => 'application/x-www-form-urlencoded', 163 | ]); 164 | } 165 | 166 | return [ 167 | $url, 168 | $request->getMethod(), 169 | $request->getHeaders(), 170 | $requestBody->getBody(), 171 | ]; 172 | } 173 | 174 | /** 175 | * Makes the request to Graph and returns the result. 176 | * 177 | * @param Request $request 178 | * 179 | * @throws SDKException 180 | * 181 | * @return Response 182 | */ 183 | public function sendRequest(Request $request) 184 | { 185 | if (get_class($request) === 'Facebook\Request') { 186 | $request->validateAccessToken(); 187 | } 188 | 189 | [$url, $method, $headers, $body] = $this->prepareRequestMessage($request); 190 | 191 | $psrRequest = Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $url, $headers, $body) 192 | ->withBody(Psr17FactoryDiscovery::findStreamFactory()->createStream($body)); 193 | 194 | foreach ($headers as $headerKey => $headerValue) { 195 | $psrRequest->withHeader($headerKey, $headerValue); 196 | } 197 | 198 | 199 | $psr7Response = $this->httpClient->sendRequest($psrRequest); 200 | 201 | static::$requestCount++; 202 | 203 | // Prepare headers from associative array to a single string for each header. 204 | $responseHeaders = []; 205 | foreach ($psr7Response->getHeaders() as $name => $values) { 206 | $responseHeaders[] = sprintf('%s: %s', $name, implode(', ', $values)); 207 | } 208 | 209 | $Response = new Response( 210 | $request, 211 | $psr7Response->getBody(), 212 | $psr7Response->getStatusCode(), 213 | $responseHeaders 214 | ); 215 | 216 | if ($Response->isError()) { 217 | throw $Response->getThrownException(); 218 | } 219 | 220 | return $Response; 221 | } 222 | 223 | /** 224 | * Makes a batched request to Graph and returns the result. 225 | * 226 | * @param BatchRequest $request 227 | * 228 | * @throws SDKException 229 | * 230 | * @return BatchResponse 231 | */ 232 | public function sendBatchRequest(BatchRequest $request) 233 | { 234 | $request->prepareRequestsForBatch(); 235 | $Response = $this->sendRequest($request); 236 | 237 | return new BatchResponse($request, $Response); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Exception/AuthenticationException.php: -------------------------------------------------------------------------------- 1 | response = $response; 49 | $this->responseData = $response->getDecodedBody(); 50 | 51 | $errorMessage = $this->get('message', 'Unknown error from Graph.'); 52 | $errorCode = $this->get('code', -1); 53 | 54 | parent::__construct($errorMessage, $errorCode, $previousException); 55 | } 56 | 57 | /** 58 | * A factory for creating the appropriate exception based on the response from Graph. 59 | * 60 | * @param Response $response the response that threw the exception 61 | * 62 | * @return ResponseException 63 | */ 64 | public static function create(Response $response) 65 | { 66 | $data = $response->getDecodedBody(); 67 | 68 | if (!isset($data['error']['code']) && isset($data['code'])) { 69 | $data = ['error' => $data]; 70 | } 71 | 72 | $code = $data['error']['code'] ?? null; 73 | $message = $data['error']['message'] ?? 'Unknown error from Graph.'; 74 | 75 | if (isset($data['error']['error_subcode'])) { 76 | switch ($data['error']['error_subcode']) { 77 | // Other authentication issues 78 | case 458: 79 | case 459: 80 | case 460: 81 | case 463: 82 | case 464: 83 | case 467: 84 | return new static($response, new AuthenticationException($message, $code)); 85 | // Video upload resumable error 86 | case 1363030: 87 | case 1363019: 88 | case 1363037: 89 | case 1363033: 90 | case 1363021: 91 | case 1363041: 92 | return new static($response, new ResumableUploadException($message, $code)); 93 | } 94 | } 95 | 96 | switch ($code) { 97 | // Login status or token expired, revoked, or invalid 98 | case 100: 99 | case 102: 100 | case 190: 101 | return new static($response, new AuthenticationException($message, $code)); 102 | 103 | // Server issue, possible downtime 104 | case 1: 105 | case 2: 106 | return new static($response, new ServerException($message, $code)); 107 | 108 | // API Throttling 109 | case 4: 110 | case 17: 111 | case 32: 112 | case 341: 113 | case 613: 114 | return new static($response, new ThrottleException($message, $code)); 115 | 116 | // Duplicate Post 117 | case 506: 118 | return new static($response, new ClientException($message, $code)); 119 | } 120 | 121 | // Missing Permissions 122 | if ($code == 10 || ($code >= 200 && $code <= 299)) { 123 | return new static($response, new AuthorizationException($message, $code)); 124 | } 125 | 126 | // OAuth authentication error 127 | if (isset($data['error']['type']) && $data['error']['type'] === 'OAuthException') { 128 | return new static($response, new AuthenticationException($message, $code)); 129 | } 130 | 131 | // All others 132 | return new static($response, new OtherException($message, $code)); 133 | } 134 | 135 | /** 136 | * Checks isset and returns that or a default value. 137 | * 138 | * @param string $key 139 | * @param mixed $default 140 | * 141 | * @return mixed 142 | */ 143 | private function get($key, $default = null) 144 | { 145 | if (isset($this->responseData['error'][$key])) { 146 | return $this->responseData['error'][$key]; 147 | } 148 | 149 | return $default; 150 | } 151 | 152 | /** 153 | * Returns the HTTP status code. 154 | * 155 | * @return int 156 | */ 157 | public function getHttpStatusCode() 158 | { 159 | return $this->response->getHttpStatusCode(); 160 | } 161 | 162 | /** 163 | * Returns the sub-error code. 164 | * 165 | * @return int 166 | */ 167 | public function getSubErrorCode() 168 | { 169 | return $this->get('error_subcode', -1); 170 | } 171 | 172 | /** 173 | * Returns the error type. 174 | * 175 | * @return string 176 | */ 177 | public function getErrorType() 178 | { 179 | return $this->get('type', ''); 180 | } 181 | 182 | /** 183 | * Returns the raw response used to create the exception. 184 | * 185 | * @return string 186 | */ 187 | public function getRawResponse() 188 | { 189 | return $this->response->getBody(); 190 | } 191 | 192 | /** 193 | * Returns the decoded response used to create the exception. 194 | * 195 | * @return array 196 | */ 197 | public function getResponseData() 198 | { 199 | return $this->responseData; 200 | } 201 | 202 | /** 203 | * Returns the response entity used to create the exception. 204 | * 205 | * @return Response 206 | */ 207 | public function getResponse() 208 | { 209 | return $this->response; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Exception/ResumableUploadException.php: -------------------------------------------------------------------------------- 1 | path = $filePath; 62 | $this->maxLength = $maxLength; 63 | $this->offset = $offset; 64 | $this->open(); 65 | } 66 | 67 | /** 68 | * Closes the stream when destructed. 69 | */ 70 | public function __destruct() 71 | { 72 | $this->close(); 73 | } 74 | 75 | /** 76 | * Opens a stream for the file. 77 | * 78 | * @throws SDKException 79 | */ 80 | public function open() 81 | { 82 | if (!$this->isRemoteFile($this->path) && !is_readable($this->path)) { 83 | throw new SDKException('Failed to create File entity. Unable to read resource: '.$this->path.'.'); 84 | } 85 | 86 | $this->stream = fopen($this->path, 'r'); 87 | 88 | if (!$this->stream) { 89 | throw new SDKException('Failed to create File entity. Unable to open resource: '.$this->path.'.'); 90 | } 91 | } 92 | 93 | /** 94 | * Stops the file stream. 95 | */ 96 | public function close() 97 | { 98 | if (is_resource($this->stream)) { 99 | fclose($this->stream); 100 | } 101 | } 102 | 103 | /** 104 | * Return the contents of the file. 105 | * 106 | * @return string 107 | */ 108 | public function getContents() 109 | { 110 | return stream_get_contents($this->stream, $this->maxLength, $this->offset); 111 | } 112 | 113 | /** 114 | * Return the name of the file. 115 | * 116 | * @return string 117 | */ 118 | public function getFileName() 119 | { 120 | return basename($this->path); 121 | } 122 | 123 | /** 124 | * Return the path of the file. 125 | * 126 | * @return string 127 | */ 128 | public function getFilePath() 129 | { 130 | return $this->path; 131 | } 132 | 133 | /** 134 | * Return the size of the file. 135 | * 136 | * @return int 137 | */ 138 | public function getSize() 139 | { 140 | return filesize($this->path); 141 | } 142 | 143 | /** 144 | * Return the mimetype of the file. 145 | * 146 | * @return string 147 | */ 148 | public function getMimetype() 149 | { 150 | return Mimetypes::getInstance()->fromFilename($this->path) ?: 'text/plain'; 151 | } 152 | 153 | /** 154 | * Returns true if the path to the file is remote. 155 | * 156 | * @param string $pathToFile 157 | * 158 | * @return bool 159 | */ 160 | protected function isRemoteFile($pathToFile) 161 | { 162 | return preg_match('/^(https?|ftp):\/\/.*/', $pathToFile) === 1; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/FileUpload/ResumableUploader.php: -------------------------------------------------------------------------------- 1 | app = $app; 65 | $this->client = $client; 66 | $this->accessToken = $accessToken; 67 | $this->graphVersion = $graphVersion; 68 | } 69 | 70 | /** 71 | * Upload by chunks - start phase. 72 | * 73 | * @param string $endpoint 74 | * @param File $file 75 | * 76 | * @throws SDKException 77 | * 78 | * @return TransferChunk 79 | */ 80 | public function start($endpoint, File $file) 81 | { 82 | $params = [ 83 | 'upload_phase' => 'start', 84 | 'file_size' => $file->getSize(), 85 | ]; 86 | $response = $this->sendUploadRequest($endpoint, $params); 87 | 88 | return new TransferChunk($file, $response['upload_session_id'], $response['video_id'], $response['start_offset'], $response['end_offset']); 89 | } 90 | 91 | /** 92 | * Upload by chunks - transfer phase. 93 | * 94 | * @param string $endpoint 95 | * @param TransferChunk $chunk 96 | * @param bool $allowToThrow 97 | * 98 | * @throws ResponseException 99 | * 100 | * @return TransferChunk 101 | */ 102 | public function transfer($endpoint, TransferChunk $chunk, $allowToThrow = false) 103 | { 104 | $params = [ 105 | 'upload_phase' => 'transfer', 106 | 'upload_session_id' => $chunk->getUploadSessionId(), 107 | 'start_offset' => $chunk->getStartOffset(), 108 | 'video_file_chunk' => $chunk->getPartialFile(), 109 | ]; 110 | 111 | try { 112 | $response = $this->sendUploadRequest($endpoint, $params); 113 | } catch (ResponseException $e) { 114 | $preException = $e->getPrevious(); 115 | if ($allowToThrow || !$preException instanceof ResumableUploadException) { 116 | throw $e; 117 | } 118 | 119 | // Return the same chunk entity so it can be retried. 120 | return $chunk; 121 | } 122 | 123 | return new TransferChunk($chunk->getFile(), $chunk->getUploadSessionId(), $chunk->getVideoId(), $response['start_offset'], $response['end_offset']); 124 | } 125 | 126 | /** 127 | * Upload by chunks - finish phase. 128 | * 129 | * @param string $endpoint 130 | * @param string $uploadSessionId 131 | * @param array $metadata the metadata associated with the file 132 | * 133 | * @throws SDKException 134 | * 135 | * @return bool 136 | */ 137 | public function finish($endpoint, $uploadSessionId, $metadata = []) 138 | { 139 | $params = array_merge($metadata, [ 140 | 'upload_phase' => 'finish', 141 | 'upload_session_id' => $uploadSessionId, 142 | ]); 143 | $response = $this->sendUploadRequest($endpoint, $params); 144 | 145 | return $response['success']; 146 | } 147 | 148 | /** 149 | * Helper to make a Request and send it. 150 | * 151 | * @param string $endpoint the endpoint to POST to 152 | * @param array $params the params to send with the request 153 | * 154 | * @return array 155 | */ 156 | private function sendUploadRequest($endpoint, $params = []) 157 | { 158 | $request = new Request($this->app, $this->accessToken, 'POST', $endpoint, $params, null, $this->graphVersion); 159 | 160 | return $this->client->sendRequest($request)->getDecodedBody(); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/FileUpload/TransferChunk.php: -------------------------------------------------------------------------------- 1 | file = $file; 63 | $this->uploadSessionId = $uploadSessionId; 64 | $this->videoId = $videoId; 65 | $this->startOffset = $startOffset; 66 | $this->endOffset = $endOffset; 67 | } 68 | 69 | /** 70 | * Return the file entity. 71 | * 72 | * @return File 73 | */ 74 | public function getFile() 75 | { 76 | return $this->file; 77 | } 78 | 79 | /** 80 | * Return a File entity with partial content. 81 | * 82 | * @return File 83 | */ 84 | public function getPartialFile() 85 | { 86 | $maxLength = $this->endOffset - $this->startOffset; 87 | 88 | return new File($this->file->getFilePath(), $maxLength, $this->startOffset); 89 | } 90 | 91 | /** 92 | * Return upload session Id. 93 | * 94 | * @return int 95 | */ 96 | public function getUploadSessionId() 97 | { 98 | return $this->uploadSessionId; 99 | } 100 | 101 | /** 102 | * Check whether is the last chunk. 103 | * 104 | * @return bool 105 | */ 106 | public function isLastChunk() 107 | { 108 | return $this->startOffset === $this->endOffset; 109 | } 110 | 111 | /** 112 | * @return int 113 | */ 114 | public function getStartOffset() 115 | { 116 | return $this->startOffset; 117 | } 118 | 119 | /** 120 | * Get uploaded video Id. 121 | * 122 | * @return int 123 | */ 124 | public function getVideoId() 125 | { 126 | return $this->videoId; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/FileUpload/Video.php: -------------------------------------------------------------------------------- 1 | hasYear = count($parts) === 3 || count($parts) === 1; 59 | $this->hasDate = count($parts) === 3 || count($parts) === 2; 60 | 61 | parent::__construct($date); 62 | } 63 | 64 | /** 65 | * Returns whether date object contains birth day and month. 66 | * 67 | * @return bool 68 | */ 69 | public function hasDate() 70 | { 71 | return $this->hasDate; 72 | } 73 | 74 | /** 75 | * Returns whether date object contains birth year. 76 | * 77 | * @return bool 78 | */ 79 | public function hasYear() 80 | { 81 | return $this->hasYear; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/GraphNode/GraphAchievement.php: -------------------------------------------------------------------------------- 1 | GraphUser::class, 33 | 'application' => GraphApplication::class, 34 | ]; 35 | 36 | /** 37 | * Returns the ID for the achievement. 38 | * 39 | * @return null|string 40 | */ 41 | public function getId() 42 | { 43 | return $this->getField('id'); 44 | } 45 | 46 | /** 47 | * Returns the user who achieved this. 48 | * 49 | * @return null|GraphUser 50 | */ 51 | public function getFrom() 52 | { 53 | return $this->getField('from'); 54 | } 55 | 56 | /** 57 | * Returns the time at which this was achieved. 58 | * 59 | * @return null|\DateTime 60 | */ 61 | public function getPublishTime() 62 | { 63 | return $this->getField('publish_time'); 64 | } 65 | 66 | /** 67 | * Returns the app in which the user achieved this. 68 | * 69 | * @return null|GraphApplication 70 | */ 71 | public function getApplication() 72 | { 73 | return $this->getField('application'); 74 | } 75 | 76 | /** 77 | * Returns information about the achievement type this instance is connected with. 78 | * 79 | * @return null|array 80 | */ 81 | public function getData() 82 | { 83 | return $this->getField('data'); 84 | } 85 | 86 | /** 87 | * Returns the type of achievement. 88 | * 89 | * @see https://developers.facebook.com/docs/graph-api/reference/achievement 90 | * 91 | * @return string 92 | */ 93 | public function getType() 94 | { 95 | return 'game.achievement'; 96 | } 97 | 98 | /** 99 | * Indicates whether gaining the achievement published a feed story for the user. 100 | * 101 | * @return null|bool 102 | */ 103 | public function isNoFeedStory() 104 | { 105 | return $this->getField('no_feed_story'); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/GraphNode/GraphAlbum.php: -------------------------------------------------------------------------------- 1 | GraphUser::class, 33 | 'place' => GraphPage::class, 34 | ]; 35 | 36 | /** 37 | * Returns the ID for the album. 38 | * 39 | * @return null|string 40 | */ 41 | public function getId() 42 | { 43 | return $this->getField('id'); 44 | } 45 | 46 | /** 47 | * Returns whether the viewer can upload photos to this album. 48 | * 49 | * @return null|bool 50 | */ 51 | public function getCanUpload() 52 | { 53 | return $this->getField('can_upload'); 54 | } 55 | 56 | /** 57 | * Returns the number of photos in this album. 58 | * 59 | * @return null|int 60 | */ 61 | public function getCount() 62 | { 63 | return $this->getField('count'); 64 | } 65 | 66 | /** 67 | * Returns the ID of the album's cover photo. 68 | * 69 | * @return null|string 70 | */ 71 | public function getCoverPhoto() 72 | { 73 | return $this->getField('cover_photo'); 74 | } 75 | 76 | /** 77 | * Returns the time the album was initially created. 78 | * 79 | * @return null|\DateTime 80 | */ 81 | public function getCreatedTime() 82 | { 83 | return $this->getField('created_time'); 84 | } 85 | 86 | /** 87 | * Returns the time the album was updated. 88 | * 89 | * @return null|\DateTime 90 | */ 91 | public function getUpdatedTime() 92 | { 93 | return $this->getField('updated_time'); 94 | } 95 | 96 | /** 97 | * Returns the description of the album. 98 | * 99 | * @return null|string 100 | */ 101 | public function getDescription() 102 | { 103 | return $this->getField('description'); 104 | } 105 | 106 | /** 107 | * Returns profile that created the album. 108 | * 109 | * @return null|GraphUser 110 | */ 111 | public function getFrom() 112 | { 113 | return $this->getField('from'); 114 | } 115 | 116 | /** 117 | * Returns profile that created the album. 118 | * 119 | * @return null|GraphPage 120 | */ 121 | public function getPlace() 122 | { 123 | return $this->getField('place'); 124 | } 125 | 126 | /** 127 | * Returns a link to this album on Facebook. 128 | * 129 | * @return null|string 130 | */ 131 | public function getLink() 132 | { 133 | return $this->getField('link'); 134 | } 135 | 136 | /** 137 | * Returns the textual location of the album. 138 | * 139 | * @return null|string 140 | */ 141 | public function getLocation() 142 | { 143 | return $this->getField('location'); 144 | } 145 | 146 | /** 147 | * Returns the title of the album. 148 | * 149 | * @return null|string 150 | */ 151 | public function getName() 152 | { 153 | return $this->getField('name'); 154 | } 155 | 156 | /** 157 | * Returns the privacy settings for the album. 158 | * 159 | * @return null|string 160 | */ 161 | public function getPrivacy() 162 | { 163 | return $this->getField('privacy'); 164 | } 165 | 166 | /** 167 | * Returns the type of the album. 168 | * 169 | * enum{ profile, mobile, wall, normal, album } 170 | * 171 | * @return null|string 172 | */ 173 | public function getType() 174 | { 175 | return $this->getField('type'); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/GraphNode/GraphApplication.php: -------------------------------------------------------------------------------- 1 | getField('id'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/GraphNode/GraphCoverPhoto.php: -------------------------------------------------------------------------------- 1 | getField('id'); 36 | } 37 | 38 | /** 39 | * Returns the source of cover if it exists. 40 | * 41 | * @return null|string 42 | */ 43 | public function getSource() 44 | { 45 | return $this->getField('source'); 46 | } 47 | 48 | /** 49 | * Returns the offset_x of cover if it exists. 50 | * 51 | * @return null|int 52 | */ 53 | public function getOffsetX() 54 | { 55 | return $this->getField('offset_x'); 56 | } 57 | 58 | /** 59 | * Returns the offset_y of cover if it exists. 60 | * 61 | * @return null|int 62 | */ 63 | public function getOffsetY() 64 | { 65 | return $this->getField('offset_y'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/GraphNode/GraphEvent.php: -------------------------------------------------------------------------------- 1 | GraphCoverPhoto::class, 33 | 'place' => GraphPage::class, 34 | 'picture' => GraphPicture::class, 35 | 'parent_group' => GraphGroup::class, 36 | ]; 37 | 38 | /** 39 | * Returns the `id` (The event ID) as string if present. 40 | * 41 | * @return null|string 42 | */ 43 | public function getId() 44 | { 45 | return $this->getField('id'); 46 | } 47 | 48 | /** 49 | * Returns the `cover` (Cover picture) as GraphCoverPhoto if present. 50 | * 51 | * @return null|GraphCoverPhoto 52 | */ 53 | public function getCover() 54 | { 55 | return $this->getField('cover'); 56 | } 57 | 58 | /** 59 | * Returns the `description` (Long-form description) as string if present. 60 | * 61 | * @return null|string 62 | */ 63 | public function getDescription() 64 | { 65 | return $this->getField('description'); 66 | } 67 | 68 | /** 69 | * Returns the `end_time` (End time, if one has been set) as DateTime if present. 70 | * 71 | * @return null|\DateTime 72 | */ 73 | public function getEndTime() 74 | { 75 | return $this->getField('end_time'); 76 | } 77 | 78 | /** 79 | * Returns the `is_date_only` (Whether the event only has a date specified, but no time) as bool if present. 80 | * 81 | * @return null|bool 82 | */ 83 | public function getIsDateOnly() 84 | { 85 | return $this->getField('is_date_only'); 86 | } 87 | 88 | /** 89 | * Returns the `name` (Event name) as string if present. 90 | * 91 | * @return null|string 92 | */ 93 | public function getName() 94 | { 95 | return $this->getField('name'); 96 | } 97 | 98 | /** 99 | * Returns the `owner` (The profile that created the event) as GraphNode if present. 100 | * 101 | * @return null|GraphNode 102 | */ 103 | public function getOwner() 104 | { 105 | return $this->getField('owner'); 106 | } 107 | 108 | /** 109 | * Returns the `parent_group` (The group the event belongs to) as GraphGroup if present. 110 | * 111 | * @return null|GraphGroup 112 | */ 113 | public function getParentGroup() 114 | { 115 | return $this->getField('parent_group'); 116 | } 117 | 118 | /** 119 | * Returns the `place` (Event Place information) as GraphPage if present. 120 | * 121 | * @return null|GraphPage 122 | */ 123 | public function getPlace() 124 | { 125 | return $this->getField('place'); 126 | } 127 | 128 | /** 129 | * Returns the `privacy` (Who can see the event) as string if present. 130 | * 131 | * @return null|string 132 | */ 133 | public function getPrivacy() 134 | { 135 | return $this->getField('privacy'); 136 | } 137 | 138 | /** 139 | * Returns the `start_time` (Start time) as DateTime if present. 140 | * 141 | * @return null|\DateTime 142 | */ 143 | public function getStartTime() 144 | { 145 | return $this->getField('start_time'); 146 | } 147 | 148 | /** 149 | * Returns the `ticket_uri` (The link users can visit to buy a ticket to this event) as string if present. 150 | * 151 | * @return null|string 152 | */ 153 | public function getTicketUri() 154 | { 155 | return $this->getField('ticket_uri'); 156 | } 157 | 158 | /** 159 | * Returns the `timezone` (Timezone) as string if present. 160 | * 161 | * @return null|string 162 | */ 163 | public function getTimezone() 164 | { 165 | return $this->getField('timezone'); 166 | } 167 | 168 | /** 169 | * Returns the `updated_time` (Last update time) as DateTime if present. 170 | * 171 | * @return null|\DateTime 172 | */ 173 | public function getUpdatedTime() 174 | { 175 | return $this->getField('updated_time'); 176 | } 177 | 178 | /** 179 | * Returns the `picture` (Event picture) as GraphPicture if present. 180 | * 181 | * @return null|GraphPicture 182 | */ 183 | public function getPicture() 184 | { 185 | return $this->getField('picture'); 186 | } 187 | 188 | /** 189 | * Returns the `attending_count` (Number of people attending the event) as int if present. 190 | * 191 | * @return null|int 192 | */ 193 | public function getAttendingCount() 194 | { 195 | return $this->getField('attending_count'); 196 | } 197 | 198 | /** 199 | * Returns the `declined_count` (Number of people who declined the event) as int if present. 200 | * 201 | * @return null|int 202 | */ 203 | public function getDeclinedCount() 204 | { 205 | return $this->getField('declined_count'); 206 | } 207 | 208 | /** 209 | * Returns the `maybe_count` (Number of people who maybe going to the event) as int if present. 210 | * 211 | * @return null|int 212 | */ 213 | public function getMaybeCount() 214 | { 215 | return $this->getField('maybe_count'); 216 | } 217 | 218 | /** 219 | * Returns the `noreply_count` (Number of people who did not reply to the event) as int if present. 220 | * 221 | * @return null|int 222 | */ 223 | public function getNoreplyCount() 224 | { 225 | return $this->getField('noreply_count'); 226 | } 227 | 228 | /** 229 | * Returns the `invited_count` (Number of people invited to the event) as int if present. 230 | * 231 | * @return null|int 232 | */ 233 | public function getInvitedCount() 234 | { 235 | return $this->getField('invited_count'); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/GraphNode/GraphGroup.php: -------------------------------------------------------------------------------- 1 | GraphCoverPhoto::class, 33 | 'venue' => GraphLocation::class, 34 | ]; 35 | 36 | /** 37 | * Returns the `id` (The Group ID) as string if present. 38 | * 39 | * @return null|string 40 | */ 41 | public function getId() 42 | { 43 | return $this->getField('id'); 44 | } 45 | 46 | /** 47 | * Returns the `cover` (The cover photo of the Group) as GraphCoverPhoto if present. 48 | * 49 | * @return null|GraphCoverPhoto 50 | */ 51 | public function getCover() 52 | { 53 | return $this->getField('cover'); 54 | } 55 | 56 | /** 57 | * Returns the `description` (A brief description of the Group) as string if present. 58 | * 59 | * @return null|string 60 | */ 61 | public function getDescription() 62 | { 63 | return $this->getField('description'); 64 | } 65 | 66 | /** 67 | * 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. 68 | * 69 | * @return null|string 70 | */ 71 | public function getEmail() 72 | { 73 | return $this->getField('email'); 74 | } 75 | 76 | /** 77 | * Returns the `icon` (The URL for the Group's icon) as string if present. 78 | * 79 | * @return null|string 80 | */ 81 | public function getIcon() 82 | { 83 | return $this->getField('icon'); 84 | } 85 | 86 | /** 87 | * Returns the `link` (The Group's website) as string if present. 88 | * 89 | * @return null|string 90 | */ 91 | public function getLink() 92 | { 93 | return $this->getField('link'); 94 | } 95 | 96 | /** 97 | * Returns the `name` (The name of the Group) as string if present. 98 | * 99 | * @return null|string 100 | */ 101 | public function getName() 102 | { 103 | return $this->getField('name'); 104 | } 105 | 106 | /** 107 | * Returns the `member_request_count` (Number of people asking to join the group.) as int if present. 108 | * 109 | * @return null|int 110 | */ 111 | public function getMemberRequestCount() 112 | { 113 | return $this->getField('member_request_count'); 114 | } 115 | 116 | /** 117 | * Returns the `owner` (The profile that created this Group) as GraphNode if present. 118 | * 119 | * @return null|GraphNode 120 | */ 121 | public function getOwner() 122 | { 123 | return $this->getField('owner'); 124 | } 125 | 126 | /** 127 | * Returns the `parent` (The parent Group of this Group, if it exists) as GraphNode if present. 128 | * 129 | * @return null|GraphNode 130 | */ 131 | public function getParent() 132 | { 133 | return $this->getField('parent'); 134 | } 135 | 136 | /** 137 | * Returns the `privacy` (The privacy setting of the Group) as string if present. 138 | * 139 | * @return null|string 140 | */ 141 | public function getPrivacy() 142 | { 143 | return $this->getField('privacy'); 144 | } 145 | 146 | /** 147 | * 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. 148 | * 149 | * @return null|\DateTime 150 | */ 151 | public function getUpdatedTime() 152 | { 153 | return $this->getField('updated_time'); 154 | } 155 | 156 | /** 157 | * Returns the `venue` (The location for the Group) as GraphLocation if present. 158 | * 159 | * @return null|GraphLocation 160 | */ 161 | public function getVenue() 162 | { 163 | return $this->getField('venue'); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/GraphNode/GraphLocation.php: -------------------------------------------------------------------------------- 1 | getField('street'); 36 | } 37 | 38 | /** 39 | * Returns the city component of the location. 40 | * 41 | * @return null|string 42 | */ 43 | public function getCity() 44 | { 45 | return $this->getField('city'); 46 | } 47 | 48 | /** 49 | * Returns the state component of the location. 50 | * 51 | * @return null|string 52 | */ 53 | public function getState() 54 | { 55 | return $this->getField('state'); 56 | } 57 | 58 | /** 59 | * Returns the country component of the location. 60 | * 61 | * @return null|string 62 | */ 63 | public function getCountry() 64 | { 65 | return $this->getField('country'); 66 | } 67 | 68 | /** 69 | * Returns the zipcode component of the location. 70 | * 71 | * @return null|string 72 | */ 73 | public function getZip() 74 | { 75 | return $this->getField('zip'); 76 | } 77 | 78 | /** 79 | * Returns the latitude component of the location. 80 | * 81 | * @return null|float 82 | */ 83 | public function getLatitude() 84 | { 85 | return $this->getField('latitude'); 86 | } 87 | 88 | /** 89 | * Returns the street component of the location. 90 | * 91 | * @return null|float 92 | */ 93 | public function getLongitude() 94 | { 95 | return $this->getField('longitude'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/GraphNode/GraphNode.php: -------------------------------------------------------------------------------- 1 | fields = $this->castFields($data); 48 | } 49 | 50 | /** 51 | * Gets the value of a field from the Graph node. 52 | * 53 | * @param string $name the field to retrieve 54 | * @param mixed $default the default to return if the field doesn't exist 55 | * 56 | * @return mixed 57 | */ 58 | public function getField($name, $default = null) 59 | { 60 | if (isset($this->fields[$name])) { 61 | return $this->fields[$name]; 62 | } 63 | 64 | return $default; 65 | } 66 | 67 | /** 68 | * Returns a list of all fields set on the object. 69 | * 70 | * @return array 71 | */ 72 | public function getFieldNames() 73 | { 74 | return array_keys($this->fields); 75 | } 76 | 77 | /** 78 | * Get all of the fields in the node. 79 | * 80 | * @return array 81 | */ 82 | public function getFields() 83 | { 84 | return $this->fields; 85 | } 86 | 87 | /** 88 | * Get all fields as a plain array. 89 | * 90 | * @return array 91 | */ 92 | public function asArray() 93 | { 94 | return array_map(function ($value) { 95 | if ($value instanceof GraphNode || $value instanceof GraphEdge) { 96 | return $value->asArray(); 97 | } 98 | 99 | return $value; 100 | }, $this->fields); 101 | } 102 | 103 | /** 104 | * Convert the collection to its string representation. 105 | * 106 | * @return string 107 | */ 108 | public function __toString() 109 | { 110 | return json_encode($this->uncastFields()); 111 | } 112 | 113 | /** 114 | * Getter for $graphNodeMap. 115 | * 116 | * @return array 117 | */ 118 | public static function getNodeMap() 119 | { 120 | return static::$graphNodeMap; 121 | } 122 | 123 | /** 124 | * Iterates over an array and detects the types each node 125 | * should be cast to and returns all the fields as an array. 126 | * 127 | * @TODO Add auto-casting to AccessToken entities. 128 | * 129 | * @param array $data the array to iterate over 130 | * 131 | * @return array 132 | */ 133 | private function castFields(array $data) 134 | { 135 | $fields = []; 136 | 137 | foreach ($data as $k => $v) { 138 | if ($this->shouldCastAsDateTime($k) 139 | && (is_numeric($v) 140 | || $this->isIso8601DateString($v)) 141 | ) { 142 | $fields[$k] = $this->castToDateTime($v); 143 | } elseif ($k === 'birthday') { 144 | $fields[$k] = $this->castToBirthday($v); 145 | } else { 146 | $fields[$k] = $v; 147 | } 148 | } 149 | 150 | return $fields; 151 | } 152 | 153 | /** 154 | * Uncasts any auto-casted datatypes. 155 | * Basically the reverse of castFields(). 156 | * 157 | * @return array 158 | */ 159 | private function uncastFields() 160 | { 161 | $fields = $this->asArray(); 162 | 163 | return array_map(function ($v) { 164 | if ($v instanceof \DateTime) { 165 | return $v->format(\DateTime::ISO8601); 166 | } 167 | 168 | return $v; 169 | }, $fields); 170 | } 171 | 172 | /** 173 | * Determines if a value from Graph should be cast to DateTime. 174 | * 175 | * @param string $key 176 | * 177 | * @return bool 178 | */ 179 | private function shouldCastAsDateTime($key) 180 | { 181 | return in_array($key, [ 182 | 'created_time', 183 | 'updated_time', 184 | 'start_time', 185 | 'stop_time', 186 | 'end_time', 187 | 'backdated_time', 188 | 'issued_at', 189 | 'expires_at', 190 | 'publish_time', 191 | ], true); 192 | } 193 | 194 | /** 195 | * Detects an ISO 8601 formatted string. 196 | * 197 | * @param string $string 198 | * 199 | * @return bool 200 | * 201 | * @see https://developers.facebook.com/docs/graph-api/using-graph-api/#readmodifiers 202 | * @see http://www.cl.cam.ac.uk/~mgk25/iso-time.html 203 | * @see http://en.wikipedia.org/wiki/ISO_8601 204 | */ 205 | private function isIso8601DateString($string) 206 | { 207 | // This insane regex was yoinked from here: 208 | // http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/ 209 | // ...and I'm all like: 210 | // http://thecodinglove.com/post/95378251969/when-code-works-and-i-dont-know-why 211 | $crazyInsaneRegexThatSomehowDetectsIso8601 = '/^([\+-]?\d{4}(?!\d{2}\b))' 212 | .'((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?' 213 | .'|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d' 214 | .'|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])' 215 | .'((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d' 216 | .'([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/'; 217 | 218 | return preg_match($crazyInsaneRegexThatSomehowDetectsIso8601, $string) === 1; 219 | } 220 | 221 | /** 222 | * Casts a date value from Graph to DateTime. 223 | * 224 | * @param int|string $value 225 | * 226 | * @return \DateTime 227 | */ 228 | private function castToDateTime($value) 229 | { 230 | if (is_int($value)) { 231 | $dt = new \DateTime(); 232 | $dt->setTimestamp($value); 233 | } else { 234 | $dt = new \DateTime($value); 235 | } 236 | 237 | return $dt; 238 | } 239 | 240 | /** 241 | * Casts a birthday value from Graph to Birthday. 242 | * 243 | * @param string $value 244 | * 245 | * @return Birthday 246 | */ 247 | private function castToBirthday($value) 248 | { 249 | return new Birthday($value); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/GraphNode/GraphPage.php: -------------------------------------------------------------------------------- 1 | GraphPage::class, 33 | 'global_brand_parent_page' => GraphPage::class, 34 | 'location' => GraphLocation::class, 35 | 'cover' => GraphCoverPhoto::class, 36 | 'picture' => GraphPicture::class, 37 | ]; 38 | 39 | /** 40 | * Returns the ID for the user's page as a string if present. 41 | * 42 | * @return null|string 43 | */ 44 | public function getId() 45 | { 46 | return $this->getField('id'); 47 | } 48 | 49 | /** 50 | * Returns the Category for the user's page as a string if present. 51 | * 52 | * @return null|string 53 | */ 54 | public function getCategory() 55 | { 56 | return $this->getField('category'); 57 | } 58 | 59 | /** 60 | * Returns the Name of the user's page as a string if present. 61 | * 62 | * @return null|string 63 | */ 64 | public function getName() 65 | { 66 | return $this->getField('name'); 67 | } 68 | 69 | /** 70 | * Returns the best available Page on Facebook. 71 | * 72 | * @return null|GraphPage 73 | */ 74 | public function getBestPage() 75 | { 76 | return $this->getField('best_page'); 77 | } 78 | 79 | /** 80 | * Returns the brand's global (parent) Page. 81 | * 82 | * @return null|GraphPage 83 | */ 84 | public function getGlobalBrandParentPage() 85 | { 86 | return $this->getField('global_brand_parent_page'); 87 | } 88 | 89 | /** 90 | * Returns the location of this place. 91 | * 92 | * @return null|GraphLocation 93 | */ 94 | public function getLocation() 95 | { 96 | return $this->getField('location'); 97 | } 98 | 99 | /** 100 | * Returns CoverPhoto of the Page. 101 | * 102 | * @return null|GraphCoverPhoto 103 | */ 104 | public function getCover() 105 | { 106 | return $this->getField('cover'); 107 | } 108 | 109 | /** 110 | * Returns Picture of the Page. 111 | * 112 | * @return null|GraphPicture 113 | */ 114 | public function getPicture() 115 | { 116 | return $this->getField('picture'); 117 | } 118 | 119 | /** 120 | * Returns the page access token for the admin user. 121 | * 122 | * Only available in the `/me/accounts` context. 123 | * 124 | * @return null|string 125 | */ 126 | public function getAccessToken() 127 | { 128 | return $this->getField('access_token'); 129 | } 130 | 131 | /** 132 | * Returns the roles of the page admin user. 133 | * 134 | * Only available in the `/me/accounts` context. 135 | * 136 | * @return null|array 137 | */ 138 | public function getPerms() 139 | { 140 | return $this->getField('perms'); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/GraphNode/GraphPicture.php: -------------------------------------------------------------------------------- 1 | getField('is_silhouette'); 36 | } 37 | 38 | /** 39 | * Returns the url of user picture if it exists. 40 | * 41 | * @return null|string 42 | */ 43 | public function getUrl() 44 | { 45 | return $this->getField('url'); 46 | } 47 | 48 | /** 49 | * Returns the width of user picture if it exists. 50 | * 51 | * @return null|int 52 | */ 53 | public function getWidth() 54 | { 55 | return $this->getField('width'); 56 | } 57 | 58 | /** 59 | * Returns the height of user picture if it exists. 60 | * 61 | * @return null|int 62 | */ 63 | public function getHeight() 64 | { 65 | return $this->getField('height'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/GraphNode/GraphSessionInfo.php: -------------------------------------------------------------------------------- 1 | getField('app_id'); 36 | } 37 | 38 | /** 39 | * Returns the application name the token was issued for. 40 | * 41 | * @return null|string 42 | */ 43 | public function getApplication() 44 | { 45 | return $this->getField('application'); 46 | } 47 | 48 | /** 49 | * Returns the date & time that the token expires. 50 | * 51 | * @return null|\DateTime 52 | */ 53 | public function getExpiresAt() 54 | { 55 | return $this->getField('expires_at'); 56 | } 57 | 58 | /** 59 | * Returns whether the token is valid. 60 | * 61 | * @return bool 62 | */ 63 | public function getIsValid() 64 | { 65 | return $this->getField('is_valid'); 66 | } 67 | 68 | /** 69 | * Returns the date & time the token was issued at. 70 | * 71 | * @return null|\DateTime 72 | */ 73 | public function getIssuedAt() 74 | { 75 | return $this->getField('issued_at'); 76 | } 77 | 78 | /** 79 | * Returns the scope permissions associated with the token. 80 | * 81 | * @return array 82 | */ 83 | public function getScopes() 84 | { 85 | return $this->getField('scopes'); 86 | } 87 | 88 | /** 89 | * Returns the login id of the user associated with the token. 90 | * 91 | * @return null|string 92 | */ 93 | public function getUserId() 94 | { 95 | return $this->getField('user_id'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/GraphNode/GraphUser.php: -------------------------------------------------------------------------------- 1 | GraphPage::class, 33 | 'location' => GraphPage::class, 34 | 'significant_other' => GraphUser::class, 35 | 'picture' => GraphPicture::class, 36 | ]; 37 | 38 | /** 39 | * Returns the ID for the user as a string if present. 40 | * 41 | * @return null|string 42 | */ 43 | public function getId() 44 | { 45 | return $this->getField('id'); 46 | } 47 | 48 | /** 49 | * Returns the name for the user as a string if present. 50 | * 51 | * @return null|string 52 | */ 53 | public function getName() 54 | { 55 | return $this->getField('name'); 56 | } 57 | 58 | /** 59 | * Returns the first name for the user as a string if present. 60 | * 61 | * @return null|string 62 | */ 63 | public function getFirstName() 64 | { 65 | return $this->getField('first_name'); 66 | } 67 | 68 | /** 69 | * Returns the middle name for the user as a string if present. 70 | * 71 | * @return null|string 72 | */ 73 | public function getMiddleName() 74 | { 75 | return $this->getField('middle_name'); 76 | } 77 | 78 | /** 79 | * Returns the last name for the user as a string if present. 80 | * 81 | * @return null|string 82 | */ 83 | public function getLastName() 84 | { 85 | return $this->getField('last_name'); 86 | } 87 | 88 | /** 89 | * Returns the email for the user as a string if present. 90 | * 91 | * @return null|string 92 | */ 93 | public function getEmail() 94 | { 95 | return $this->getField('email'); 96 | } 97 | 98 | /** 99 | * Returns the gender for the user as a string if present. 100 | * 101 | * @return null|string 102 | */ 103 | public function getGender() 104 | { 105 | return $this->getField('gender'); 106 | } 107 | 108 | /** 109 | * Returns the Facebook URL for the user as a string if available. 110 | * 111 | * @return null|string 112 | */ 113 | public function getLink() 114 | { 115 | return $this->getField('link'); 116 | } 117 | 118 | /** 119 | * Returns the users birthday, if available. 120 | * 121 | * @return null|Birthday 122 | */ 123 | public function getBirthday() 124 | { 125 | return $this->getField('birthday'); 126 | } 127 | 128 | /** 129 | * Returns the current location of the user as a GraphPage. 130 | * 131 | * @return null|GraphPage 132 | */ 133 | public function getLocation() 134 | { 135 | return $this->getField('location'); 136 | } 137 | 138 | /** 139 | * Returns the current location of the user as a GraphPage. 140 | * 141 | * @return null|GraphPage 142 | */ 143 | public function getHometown() 144 | { 145 | return $this->getField('hometown'); 146 | } 147 | 148 | /** 149 | * Returns the current location of the user as a GraphUser. 150 | * 151 | * @return null|GraphUser 152 | */ 153 | public function getSignificantOther() 154 | { 155 | return $this->getField('significant_other'); 156 | } 157 | 158 | /** 159 | * Returns the picture of the user as a GraphPicture. 160 | * 161 | * @return null|GraphPicture 162 | */ 163 | public function getPicture() 164 | { 165 | return $this->getField('picture'); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Helper/CanvasHelper.php: -------------------------------------------------------------------------------- 1 | signedRequest ? $this->signedRequest->get('app_data') : null; 36 | } 37 | 38 | /** 39 | * Get raw signed request from POST. 40 | * 41 | * @return null|string 42 | */ 43 | public function getRawSignedRequest() 44 | { 45 | return $this->getRawSignedRequestFromPost() ?: null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Helper/JavaScriptHelper.php: -------------------------------------------------------------------------------- 1 | getRawSignedRequestFromCookie(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Helper/PageTabHelper.php: -------------------------------------------------------------------------------- 1 | signedRequest) { 48 | return; 49 | } 50 | 51 | $this->pageData = $this->signedRequest->get('page'); 52 | } 53 | 54 | /** 55 | * Returns a value from the page data. 56 | * 57 | * @param string $key 58 | * @param null|mixed $default 59 | * 60 | * @return null|mixed 61 | */ 62 | public function getPageData($key, $default = null) 63 | { 64 | if (isset($this->pageData[$key])) { 65 | return $this->pageData[$key]; 66 | } 67 | 68 | return $default; 69 | } 70 | 71 | /** 72 | * Returns true if the user is an admin. 73 | * 74 | * @return bool 75 | */ 76 | public function isAdmin() 77 | { 78 | return $this->getPageData('admin') === true; 79 | } 80 | 81 | /** 82 | * Returns the page id if available. 83 | * 84 | * @return null|string 85 | */ 86 | public function getPageId() 87 | { 88 | return $this->getPageData('id'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Helper/RedirectLoginHelper.php: -------------------------------------------------------------------------------- 1 | oAuth2Client = $oAuth2Client; 65 | $this->persistentDataHandler = $persistentDataHandler ?: new SessionPersistentDataHandler(); 66 | $this->urlDetectionHandler = $urlHandler ?: new UrlDetectionHandler(); 67 | } 68 | 69 | /** 70 | * Returns the persistent data handler. 71 | * 72 | * @return PersistentDataInterface 73 | */ 74 | public function getPersistentDataHandler() 75 | { 76 | return $this->persistentDataHandler; 77 | } 78 | 79 | /** 80 | * Returns the URL detection handler. 81 | * 82 | * @return UrlDetectionInterface 83 | */ 84 | public function getUrlDetectionHandler() 85 | { 86 | return $this->urlDetectionHandler; 87 | } 88 | 89 | /** 90 | * Stores CSRF state and returns a URL to which the user should be sent to in order to continue the login process with Facebook. 91 | * 92 | * @param string $redirectUrl the URL Facebook should redirect users to after login 93 | * @param array $scope list of permissions to request during login 94 | * @param array $params an array of parameters to generate URL 95 | * @param string $separator the separator to use in http_build_query() 96 | * 97 | * @return string 98 | */ 99 | private function makeUrl($redirectUrl, array $scope, array $params = [], $separator = '&') 100 | { 101 | $state = $this->persistentDataHandler->get('state') ?: $this->getPseudoRandomString(); 102 | $this->persistentDataHandler->set('state', $state); 103 | 104 | return $this->oAuth2Client->getAuthorizationUrl($redirectUrl, $state, $scope, $params, $separator); 105 | } 106 | 107 | private function getPseudoRandomString() 108 | { 109 | return bin2hex(random_bytes(static::CSRF_LENGTH)); 110 | } 111 | 112 | /** 113 | * Returns the URL to send the user in order to login to Facebook. 114 | * 115 | * @param string $redirectUrl the URL Facebook should redirect users to after login 116 | * @param array $scope list of permissions to request during login 117 | * @param string $separator the separator to use in http_build_query() 118 | * 119 | * @return string 120 | */ 121 | public function getLoginUrl($redirectUrl, array $scope = [], $separator = '&') 122 | { 123 | return $this->makeUrl($redirectUrl, $scope, [], $separator); 124 | } 125 | 126 | /** 127 | * Returns the URL to send the user in order to log out of Facebook. 128 | * 129 | * @param AccessToken|string $accessToken the access token that will be logged out 130 | * @param string $next the url Facebook should redirect the user to after a successful logout 131 | * @param string $separator the separator to use in http_build_query() 132 | * 133 | * @throws SDKException 134 | * 135 | * @return string 136 | */ 137 | public function getLogoutUrl($accessToken, $next, $separator = '&') 138 | { 139 | if (!$accessToken instanceof AccessToken) { 140 | $accessToken = new AccessToken($accessToken); 141 | } 142 | 143 | if ($accessToken->isAppAccessToken()) { 144 | throw new SDKException('Cannot generate a logout URL with an app access token.', 722); 145 | } 146 | 147 | $params = [ 148 | 'next' => $next, 149 | 'access_token' => $accessToken->getValue(), 150 | ]; 151 | 152 | return 'https://www.facebook.com/logout.php?'.http_build_query($params, '', $separator); 153 | } 154 | 155 | /** 156 | * Returns the URL to send the user in order to login to Facebook with permission(s) to be re-asked. 157 | * 158 | * @param string $redirectUrl the URL Facebook should redirect users to after login 159 | * @param array $scope list of permissions to request during login 160 | * @param string $separator the separator to use in http_build_query() 161 | * 162 | * @return string 163 | */ 164 | public function getReRequestUrl($redirectUrl, array $scope = [], $separator = '&') 165 | { 166 | $params = ['auth_type' => 'rerequest']; 167 | 168 | return $this->makeUrl($redirectUrl, $scope, $params, $separator); 169 | } 170 | 171 | /** 172 | * Returns the URL to send the user in order to login to Facebook with user to be re-authenticated. 173 | * 174 | * @param string $redirectUrl the URL Facebook should redirect users to after login 175 | * @param array $scope list of permissions to request during login 176 | * @param string $separator the separator to use in http_build_query() 177 | * 178 | * @return string 179 | */ 180 | public function getReAuthenticationUrl($redirectUrl, array $scope = [], $separator = '&') 181 | { 182 | $params = ['auth_type' => 'reauthenticate']; 183 | 184 | return $this->makeUrl($redirectUrl, $scope, $params, $separator); 185 | } 186 | 187 | /** 188 | * Takes a valid code from a login redirect, and returns an AccessToken entity. 189 | * 190 | * @param null|string $redirectUrl the redirect URL 191 | * 192 | * @throws SDKException 193 | * 194 | * @return null|AccessToken 195 | */ 196 | public function getAccessToken($redirectUrl = null) 197 | { 198 | if (!$code = $this->getCode()) { 199 | return null; 200 | } 201 | 202 | $this->validateCsrf(); 203 | $this->resetCsrf(); 204 | 205 | $redirectUrl = $redirectUrl ?: $this->urlDetectionHandler->getCurrentUrl(); 206 | // At minimum we need to remove the state param 207 | $redirectUrl = UrlManipulator::removeParamsFromUrl($redirectUrl, ['state']); 208 | 209 | return $this->oAuth2Client->getAccessTokenFromCode($code, $redirectUrl); 210 | } 211 | 212 | /** 213 | * Validate the request against a cross-site request forgery. 214 | * 215 | * @throws SDKException 216 | */ 217 | protected function validateCsrf() 218 | { 219 | $state = $this->getState(); 220 | if (!$state) { 221 | throw new SDKException('Cross-site request forgery validation failed. Required GET param "state" missing.'); 222 | } 223 | $savedState = $this->persistentDataHandler->get('state'); 224 | if (!$savedState) { 225 | throw new SDKException('Cross-site request forgery validation failed. Required param "state" missing from persistent data.'); 226 | } 227 | 228 | if (\hash_equals($savedState, $state)) { 229 | return; 230 | } 231 | 232 | throw new SDKException('Cross-site request forgery validation failed. The "state" param from the URL and session do not match.'); 233 | } 234 | 235 | /** 236 | * Resets the CSRF so that it doesn't get reused. 237 | */ 238 | private function resetCsrf() 239 | { 240 | $this->persistentDataHandler->set('state', null); 241 | } 242 | 243 | /** 244 | * Return the code. 245 | * 246 | * @return null|string 247 | */ 248 | protected function getCode() 249 | { 250 | return $this->getInput('code'); 251 | } 252 | 253 | /** 254 | * Return the state. 255 | * 256 | * @return null|string 257 | */ 258 | protected function getState() 259 | { 260 | return $this->getInput('state'); 261 | } 262 | 263 | /** 264 | * Return the error code. 265 | * 266 | * @return null|string 267 | */ 268 | public function getErrorCode() 269 | { 270 | return $this->getInput('error_code'); 271 | } 272 | 273 | /** 274 | * Returns the error. 275 | * 276 | * @return null|string 277 | */ 278 | public function getError() 279 | { 280 | return $this->getInput('error'); 281 | } 282 | 283 | /** 284 | * Returns the error reason. 285 | * 286 | * @return null|string 287 | */ 288 | public function getErrorReason() 289 | { 290 | return $this->getInput('error_reason'); 291 | } 292 | 293 | /** 294 | * Returns the error description. 295 | * 296 | * @return null|string 297 | */ 298 | public function getErrorDescription() 299 | { 300 | return $this->getInput('error_description'); 301 | } 302 | 303 | /** 304 | * Returns a value from a GET param. 305 | * 306 | * @param string $key 307 | * 308 | * @return null|string 309 | */ 310 | private function getInput($key) 311 | { 312 | return $_GET[$key] ?? null; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/Helper/SignedRequestFromInputHelper.php: -------------------------------------------------------------------------------- 1 | app = $app; 59 | $this->oAuth2Client = new OAuth2Client($this->app, $client, $graphVersion); 60 | 61 | $this->instantiateSignedRequest(); 62 | } 63 | 64 | /** 65 | * Instantiates a new SignedRequest entity. 66 | * 67 | * @param null|string 68 | * @param null|mixed $rawSignedRequest 69 | */ 70 | public function instantiateSignedRequest($rawSignedRequest = null) 71 | { 72 | $rawSignedRequest = $rawSignedRequest ?: $this->getRawSignedRequest(); 73 | 74 | if (!$rawSignedRequest) { 75 | return; 76 | } 77 | 78 | $this->signedRequest = new SignedRequest($this->app, $rawSignedRequest); 79 | } 80 | 81 | /** 82 | * Returns an AccessToken entity from the signed request. 83 | * 84 | * @throws \Facebook\Exception\SDKException 85 | * 86 | * @return null|AccessToken 87 | */ 88 | public function getAccessToken() 89 | { 90 | if ($this->signedRequest && $this->signedRequest->hasOAuthData()) { 91 | $code = $this->signedRequest->get('code'); 92 | $accessToken = $this->signedRequest->get('oauth_token'); 93 | 94 | if ($code && !$accessToken) { 95 | return $this->oAuth2Client->getAccessTokenFromCode($code); 96 | } 97 | 98 | $expiresAt = $this->signedRequest->get('expires', 0); 99 | 100 | return new AccessToken($accessToken, $expiresAt); 101 | } 102 | 103 | return null; 104 | } 105 | 106 | /** 107 | * Returns the SignedRequest entity. 108 | * 109 | * @return null|SignedRequest 110 | */ 111 | public function getSignedRequest() 112 | { 113 | return $this->signedRequest; 114 | } 115 | 116 | /** 117 | * Returns the user_id if available. 118 | * 119 | * @return null|string 120 | */ 121 | public function getUserId() 122 | { 123 | return $this->signedRequest ? $this->signedRequest->getUserId() : null; 124 | } 125 | 126 | /** 127 | * Get raw signed request from input. 128 | * 129 | * @return null|string 130 | */ 131 | abstract public function getRawSignedRequest(); 132 | 133 | /** 134 | * Get raw signed request from POST input. 135 | * 136 | * @return null|string 137 | */ 138 | public function getRawSignedRequestFromPost() 139 | { 140 | if (isset($_POST['signed_request'])) { 141 | return $_POST['signed_request']; 142 | } 143 | 144 | return null; 145 | } 146 | 147 | /** 148 | * Get raw signed request from cookie set from the Javascript SDK. 149 | * 150 | * @return null|string 151 | */ 152 | public function getRawSignedRequestFromCookie() 153 | { 154 | if (isset($_COOKIE['fbsr_'.$this->app->getId()])) { 155 | return $_COOKIE['fbsr_'.$this->app->getId()]; 156 | } 157 | 158 | return null; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Http/RequestBodyInterface.php: -------------------------------------------------------------------------------- 1 | params = $params; 59 | $this->files = $files; 60 | $this->boundary = $boundary ?: uniqid(); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getBody() 67 | { 68 | $body = ''; 69 | 70 | // Compile normal params 71 | $params = $this->getNestedParams($this->params); 72 | foreach ($params as $k => $v) { 73 | $body .= $this->getParamString($k, $v); 74 | } 75 | 76 | // Compile files 77 | foreach ($this->files as $k => $v) { 78 | $body .= $this->getFileString($k, $v); 79 | } 80 | 81 | // Peace out 82 | $body .= "--{$this->boundary}--\r\n"; 83 | 84 | return $body; 85 | } 86 | 87 | /** 88 | * Get the boundary. 89 | * 90 | * @return string 91 | */ 92 | public function getBoundary() 93 | { 94 | return $this->boundary; 95 | } 96 | 97 | /** 98 | * Get the string needed to transfer a file. 99 | * 100 | * @param string $name 101 | * @param File $file 102 | * 103 | * @return string 104 | */ 105 | private function getFileString($name, File $file) 106 | { 107 | return sprintf( 108 | "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"%s\r\n\r\n%s\r\n", 109 | $this->boundary, 110 | $name, 111 | $file->getFileName(), 112 | $this->getFileHeaders($file), 113 | $file->getContents() 114 | ); 115 | } 116 | 117 | /** 118 | * Get the string needed to transfer a POST field. 119 | * 120 | * @param string $name 121 | * @param string $value 122 | * 123 | * @return string 124 | */ 125 | private function getParamString($name, $value) 126 | { 127 | return sprintf( 128 | "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", 129 | $this->boundary, 130 | $name, 131 | $value 132 | ); 133 | } 134 | 135 | /** 136 | * Returns the params as an array of nested params. 137 | * 138 | * @param array $params 139 | * 140 | * @return array 141 | */ 142 | private function getNestedParams(array $params) 143 | { 144 | $query = http_build_query($params, '', '&'); 145 | $params = explode('&', $query); 146 | $result = []; 147 | 148 | foreach ($params as $param) { 149 | list($key, $value) = explode('=', $param, 2); 150 | $result[urldecode($key)] = urldecode($value); 151 | } 152 | 153 | return $result; 154 | } 155 | 156 | /** 157 | * Get the headers needed before transferring the content of a POST file. 158 | * 159 | * @param File $file 160 | * 161 | * @return string 162 | */ 163 | protected function getFileHeaders(File $file) 164 | { 165 | return "\r\nContent-Type: {$file->getMimetype()}"; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Http/RequestBodyUrlEncoded.php: -------------------------------------------------------------------------------- 1 | params = $params; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getBody() 47 | { 48 | return http_build_query($this->params, '', '&'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PersistentData/InMemoryPersistentDataHandler.php: -------------------------------------------------------------------------------- 1 | sessionData[$key] ?? null; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function set($key, $value) 45 | { 46 | $this->sessionData[$key] = $value; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PersistentData/PersistentDataFactory.php: -------------------------------------------------------------------------------- 1 | sessionPrefix.$key])) { 58 | return $_SESSION[$this->sessionPrefix.$key]; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function set($key, $value) 68 | { 69 | $_SESSION[$this->sessionPrefix.$key] = $value; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | request = $request; 73 | $this->body = $body; 74 | $this->httpStatusCode = $httpStatusCode; 75 | $this->headers = $headers; 76 | 77 | $this->decodeBody(); 78 | } 79 | 80 | /** 81 | * Return the original request that returned this response. 82 | * 83 | * @return Request 84 | */ 85 | public function getRequest() 86 | { 87 | return $this->request; 88 | } 89 | 90 | /** 91 | * Return the Application entity used for this response. 92 | * 93 | * @return Application 94 | */ 95 | public function getApplication() 96 | { 97 | return $this->request->getApplication(); 98 | } 99 | 100 | /** 101 | * Return the access token that was used for this response. 102 | * 103 | * @return null|string 104 | */ 105 | public function getAccessToken() 106 | { 107 | return $this->request->getAccessToken(); 108 | } 109 | 110 | /** 111 | * Return the HTTP status code for this response. 112 | * 113 | * @return int 114 | */ 115 | public function getHttpStatusCode() 116 | { 117 | return $this->httpStatusCode; 118 | } 119 | 120 | /** 121 | * Return the HTTP headers for this response. 122 | * 123 | * @return array 124 | */ 125 | public function getHeaders() 126 | { 127 | return $this->headers; 128 | } 129 | 130 | /** 131 | * Return the raw body response. 132 | * 133 | * @return string 134 | */ 135 | public function getBody() 136 | { 137 | return $this->body; 138 | } 139 | 140 | /** 141 | * Return the decoded body response. 142 | * 143 | * @return array 144 | */ 145 | public function getDecodedBody() 146 | { 147 | return $this->decodedBody; 148 | } 149 | 150 | /** 151 | * Get the app secret proof that was used for this response. 152 | * 153 | * @return null|string 154 | */ 155 | public function getAppSecretProof() 156 | { 157 | return $this->request->getAppSecretProof(); 158 | } 159 | 160 | /** 161 | * Get the ETag associated with the response. 162 | * 163 | * @return null|string 164 | */ 165 | public function getETag() 166 | { 167 | return $this->headers['ETag'] ?? null; 168 | } 169 | 170 | /** 171 | * Get the version of Graph that returned this response. 172 | * 173 | * @return null|string 174 | */ 175 | public function getGraphVersion() 176 | { 177 | return $this->headers['Facebook-API-Version'] ?? null; 178 | } 179 | 180 | /** 181 | * Returns true if Graph returned an error message. 182 | * 183 | * @return bool 184 | */ 185 | public function isError() 186 | { 187 | return isset($this->decodedBody['error']); 188 | } 189 | 190 | /** 191 | * Throws the exception. 192 | * 193 | * @throws SDKException 194 | */ 195 | public function throwException() 196 | { 197 | throw $this->thrownException; 198 | } 199 | 200 | /** 201 | * Instantiates an exception to be thrown later. 202 | */ 203 | public function makeException() 204 | { 205 | $this->thrownException = ResponseException::create($this); 206 | } 207 | 208 | /** 209 | * Returns the exception that was thrown for this request. 210 | * 211 | * @return null|ResponseException 212 | */ 213 | public function getThrownException() 214 | { 215 | return $this->thrownException; 216 | } 217 | 218 | /** 219 | * Convert the raw response into an array if possible. 220 | * 221 | * Graph will return 2 types of responses: 222 | * - JSON(P) 223 | * Most responses from Graph are JSON(P) 224 | * - application/x-www-form-urlencoded key/value pairs 225 | * Happens on the `/oauth/access_token` endpoint when exchanging 226 | * a short-lived access token for a long-lived access token 227 | * - And sometimes nothing :/ but that'd be a bug. 228 | */ 229 | public function decodeBody() 230 | { 231 | $this->decodedBody = json_decode($this->body, true); 232 | 233 | if ($this->decodedBody === null) { 234 | $this->decodedBody = []; 235 | parse_str($this->body, $this->decodedBody); 236 | } elseif (is_bool($this->decodedBody)) { 237 | // Backwards compatibility for Graph < 2.1. 238 | // Mimics 2.1 responses. 239 | // @TODO Remove this after Graph 2.0 is no longer supported 240 | $this->decodedBody = ['success' => $this->decodedBody]; 241 | } elseif (is_numeric($this->decodedBody)) { 242 | $this->decodedBody = ['id' => $this->decodedBody]; 243 | } 244 | 245 | if (!is_array($this->decodedBody)) { 246 | $this->decodedBody = []; 247 | } 248 | 249 | if ($this->isError()) { 250 | $this->makeException(); 251 | } 252 | } 253 | 254 | /** 255 | * Instantiate a new GraphNode from response. 256 | * 257 | * @param null|string $subclassName the GraphNode subclass to cast to 258 | * 259 | * @throws SDKException 260 | * 261 | * @return \Facebook\GraphNode\GraphNode 262 | */ 263 | public function getGraphNode($subclassName = null) 264 | { 265 | $factory = new GraphNodeFactory($this); 266 | 267 | return $factory->makeGraphNode($subclassName); 268 | } 269 | 270 | /** 271 | * Convenience method for creating a GraphAlbum collection. 272 | * 273 | * @throws SDKException 274 | * 275 | * @return \Facebook\GraphNode\GraphAlbum 276 | */ 277 | public function getGraphAlbum() 278 | { 279 | $factory = new GraphNodeFactory($this); 280 | 281 | return $factory->makeGraphAlbum(); 282 | } 283 | 284 | /** 285 | * Convenience method for creating a GraphPage collection. 286 | * 287 | * @throws SDKException 288 | * 289 | * @return \Facebook\GraphNode\GraphPage 290 | */ 291 | public function getGraphPage() 292 | { 293 | $factory = new GraphNodeFactory($this); 294 | 295 | return $factory->makeGraphPage(); 296 | } 297 | 298 | /** 299 | * Convenience method for creating a GraphSessionInfo collection. 300 | * 301 | * @throws SDKException 302 | * 303 | * @return \Facebook\GraphNode\GraphSessionInfo 304 | */ 305 | public function getGraphSessionInfo() 306 | { 307 | $factory = new GraphNodeFactory($this); 308 | 309 | return $factory->makeGraphSessionInfo(); 310 | } 311 | 312 | /** 313 | * Convenience method for creating a GraphUser collection. 314 | * 315 | * @throws SDKException 316 | * 317 | * @return \Facebook\GraphNode\GraphUser 318 | */ 319 | public function getGraphUser() 320 | { 321 | $factory = new GraphNodeFactory($this); 322 | 323 | return $factory->makeGraphUser(); 324 | } 325 | 326 | /** 327 | * Convenience method for creating a GraphEvent collection. 328 | * 329 | * @throws SDKException 330 | * 331 | * @return \Facebook\GraphNode\GraphEvent 332 | */ 333 | public function getGraphEvent() 334 | { 335 | $factory = new GraphNodeFactory($this); 336 | 337 | return $factory->makeGraphEvent(); 338 | } 339 | 340 | /** 341 | * Convenience method for creating a GraphGroup collection. 342 | * 343 | * @throws SDKException 344 | * 345 | * @return \Facebook\GraphNode\GraphGroup 346 | */ 347 | public function getGraphGroup() 348 | { 349 | $factory = new GraphNodeFactory($this); 350 | 351 | return $factory->makeGraphGroup(); 352 | } 353 | 354 | /** 355 | * Instantiate a new GraphEdge from response. 356 | * 357 | * @param null|string $subclassName the GraphNode subclass to cast list items to 358 | * @param bool $auto_prefix toggle to auto-prefix the subclass name 359 | * 360 | * @throws SDKException 361 | * 362 | * @return \Facebook\GraphNode\GraphEdge 363 | */ 364 | public function getGraphEdge($subclassName = null, $auto_prefix = true) 365 | { 366 | $factory = new GraphNodeFactory($this); 367 | 368 | return $factory->makeGraphEdge($subclassName, $auto_prefix); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/SignedRequest.php: -------------------------------------------------------------------------------- 1 | app = $App; 54 | 55 | if (!$rawSignedRequest) { 56 | return; 57 | } 58 | 59 | $this->rawSignedRequest = $rawSignedRequest; 60 | 61 | $this->parse(); 62 | } 63 | 64 | /** 65 | * Returns the raw signed request data. 66 | * 67 | * @return null|string 68 | */ 69 | public function getRawSignedRequest() 70 | { 71 | return $this->rawSignedRequest; 72 | } 73 | 74 | /** 75 | * Returns the parsed signed request data. 76 | * 77 | * @return null|array 78 | */ 79 | public function getPayload() 80 | { 81 | return $this->payload; 82 | } 83 | 84 | /** 85 | * Returns a property from the signed request data if available. 86 | * 87 | * @param string $key 88 | * @param null|mixed $default 89 | * 90 | * @return null|mixed 91 | */ 92 | public function get($key, $default = null) 93 | { 94 | if (isset($this->payload[$key])) { 95 | return $this->payload[$key]; 96 | } 97 | 98 | return $default; 99 | } 100 | 101 | /** 102 | * Returns user_id from signed request data if available. 103 | * 104 | * @return null|string 105 | */ 106 | public function getUserId() 107 | { 108 | return $this->get('user_id'); 109 | } 110 | 111 | /** 112 | * Checks for OAuth data in the payload. 113 | * 114 | * @return bool 115 | */ 116 | public function hasOAuthData() 117 | { 118 | return $this->get('oauth_token') || $this->get('code'); 119 | } 120 | 121 | /** 122 | * Creates a signed request from an array of data. 123 | * 124 | * @param array $payload 125 | * 126 | * @return string 127 | */ 128 | public function make(array $payload) 129 | { 130 | $payload['algorithm'] = $payload['algorithm'] ?? 'HMAC-SHA256'; 131 | $payload['issued_at'] = $payload['issued_at'] ?? time(); 132 | $encodedPayload = $this->base64UrlEncode(json_encode($payload)); 133 | 134 | $hashedSig = $this->hashSignature($encodedPayload); 135 | $encodedSig = $this->base64UrlEncode($hashedSig); 136 | 137 | return $encodedSig.'.'.$encodedPayload; 138 | } 139 | 140 | /** 141 | * Validates and decodes a signed request and saves 142 | * the payload to an array. 143 | */ 144 | protected function parse() 145 | { 146 | list($encodedSig, $encodedPayload) = $this->split(); 147 | 148 | // Signature validation 149 | $sig = $this->decodeSignature($encodedSig); 150 | $hashedSig = $this->hashSignature($encodedPayload); 151 | $this->validateSignature($hashedSig, $sig); 152 | 153 | $this->payload = $this->decodePayload($encodedPayload); 154 | 155 | // Payload validation 156 | $this->validateAlgorithm(); 157 | } 158 | 159 | /** 160 | * Splits a raw signed request into signature and payload. 161 | * 162 | * @throws SDKException 163 | * 164 | * @return array 165 | */ 166 | protected function split() 167 | { 168 | if (strpos($this->rawSignedRequest, '.') === false) { 169 | throw new SDKException('Malformed signed request.', 606); 170 | } 171 | 172 | return explode('.', $this->rawSignedRequest, 2); 173 | } 174 | 175 | /** 176 | * Decodes the raw signature from a signed request. 177 | * 178 | * @param string $encodedSig 179 | * 180 | * @throws SDKException 181 | * 182 | * @return string 183 | */ 184 | protected function decodeSignature($encodedSig) 185 | { 186 | $sig = $this->base64UrlDecode($encodedSig); 187 | 188 | if (!$sig) { 189 | throw new SDKException('Signed request has malformed encoded signature data.', 607); 190 | } 191 | 192 | return $sig; 193 | } 194 | 195 | /** 196 | * Decodes the raw payload from a signed request. 197 | * 198 | * @param string $encodedPayload 199 | * 200 | * @throws SDKException 201 | * 202 | * @return array 203 | */ 204 | protected function decodePayload($encodedPayload) 205 | { 206 | $payload = $this->base64UrlDecode($encodedPayload); 207 | 208 | if ($payload) { 209 | $payload = json_decode($payload, true, 512, JSON_BIGINT_AS_STRING); 210 | } 211 | 212 | if (!is_array($payload)) { 213 | throw new SDKException('Signed request has malformed encoded payload data.', 607); 214 | } 215 | 216 | return $payload; 217 | } 218 | 219 | /** 220 | * Validates the algorithm used in a signed request. 221 | * 222 | * @throws SDKException 223 | */ 224 | protected function validateAlgorithm() 225 | { 226 | if ($this->get('algorithm') !== 'HMAC-SHA256') { 227 | throw new SDKException('Signed request is using the wrong algorithm.', 605); 228 | } 229 | } 230 | 231 | /** 232 | * Hashes the signature used in a signed request. 233 | * 234 | * @param string $encodedData 235 | * 236 | * @throws SDKException 237 | * 238 | * @return string 239 | */ 240 | protected function hashSignature($encodedData) 241 | { 242 | $hashedSig = hash_hmac( 243 | 'sha256', 244 | $encodedData, 245 | $this->app->getSecret(), 246 | $raw_output = true 247 | ); 248 | 249 | if (!$hashedSig) { 250 | throw new SDKException('Unable to hash signature from encoded payload data.', 602); 251 | } 252 | 253 | return $hashedSig; 254 | } 255 | 256 | /** 257 | * Validates the signature used in a signed request. 258 | * 259 | * @param string $hashedSig 260 | * @param string $sig 261 | * 262 | * @throws SDKException 263 | */ 264 | protected function validateSignature($hashedSig, $sig) 265 | { 266 | if (\hash_equals($hashedSig, $sig)) { 267 | return; 268 | } 269 | 270 | throw new SDKException('Signed request has an invalid signature.', 602); 271 | } 272 | 273 | /** 274 | * Base64 decoding which replaces characters: 275 | * + instead of - 276 | * / instead of _. 277 | * 278 | * @link http://en.wikipedia.org/wiki/Base64#URL_applications 279 | * 280 | * @param string $input base64 url encoded input 281 | * 282 | * @return string decoded string 283 | */ 284 | public function base64UrlDecode($input) 285 | { 286 | $urlDecodedBase64 = strtr($input, '-_', '+/'); 287 | $this->validateBase64($urlDecodedBase64); 288 | 289 | return base64_decode($urlDecodedBase64); 290 | } 291 | 292 | /** 293 | * Base64 encoding which replaces characters: 294 | * + instead of - 295 | * / instead of _. 296 | * 297 | * @link http://en.wikipedia.org/wiki/Base64#URL_applications 298 | * 299 | * @param string $input string to encode 300 | * 301 | * @return string base64 url encoded input 302 | */ 303 | public function base64UrlEncode($input) 304 | { 305 | return strtr(base64_encode($input), '+/', '-_'); 306 | } 307 | 308 | /** 309 | * Validates a base64 string. 310 | * 311 | * @param string $input base64 value to validate 312 | * 313 | * @throws SDKException 314 | */ 315 | protected function validateBase64($input) 316 | { 317 | if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $input)) { 318 | throw new SDKException('Signed request contains malformed base64 encoding.', 608); 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/Url/UrlDetectionHandler.php: -------------------------------------------------------------------------------- 1 | getHttpScheme().'://'.$this->getHostName().$this->getServerVar('REQUEST_URI'); 34 | } 35 | 36 | /** 37 | * Get the currently active URL scheme. 38 | * 39 | * @return string 40 | */ 41 | protected function getHttpScheme() 42 | { 43 | return $this->isBehindSsl() ? 'https' : 'http'; 44 | } 45 | 46 | /** 47 | * Tries to detect if the server is running behind an SSL. 48 | * 49 | * @return bool 50 | */ 51 | protected function isBehindSsl() 52 | { 53 | // Check for proxy first 54 | $protocol = $this->getHeader('X_FORWARDED_PROTO'); 55 | if ($protocol) { 56 | return $this->protocolWithActiveSsl($protocol); 57 | } 58 | 59 | $protocol = $this->getServerVar('HTTPS'); 60 | if ($protocol) { 61 | return $this->protocolWithActiveSsl($protocol); 62 | } 63 | 64 | return (string) $this->getServerVar('SERVER_PORT') === '443'; 65 | } 66 | 67 | /** 68 | * Detects an active SSL protocol value. 69 | * 70 | * @param string $protocol 71 | * 72 | * @return bool 73 | */ 74 | protected function protocolWithActiveSsl($protocol) 75 | { 76 | $protocol = strtolower((string) $protocol); 77 | 78 | return in_array($protocol, ['on', '1', 'https', 'ssl'], true); 79 | } 80 | 81 | /** 82 | * Tries to detect the host name of the server. 83 | * 84 | * Some elements adapted from 85 | * 86 | * @see https://github.com/symfony/HttpFoundation/blob/master/Request.php 87 | * 88 | * @return string 89 | */ 90 | protected function getHostName() 91 | { 92 | // Check for proxy first 93 | $header = $this->getHeader('X_FORWARDED_HOST'); 94 | if ($header && $this->isValidForwardedHost($header)) { 95 | $elements = explode(',', $header); 96 | $host = $elements[count($elements) - 1]; 97 | } elseif (!$host = $this->getHeader('HOST')) { 98 | if (!$host = $this->getServerVar('SERVER_NAME')) { 99 | $host = $this->getServerVar('SERVER_ADDR'); 100 | } 101 | } 102 | 103 | // trim and remove port number from host 104 | // host is lowercase as per RFC 952/2181 105 | $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); 106 | 107 | // Port number 108 | $scheme = $this->getHttpScheme(); 109 | $port = $this->getCurrentPort(); 110 | $appendPort = ':'.$port; 111 | 112 | // Don't append port number if a normal port. 113 | if (($scheme == 'http' && $port == '80') || ($scheme == 'https' && $port == '443')) { 114 | $appendPort = ''; 115 | } 116 | 117 | return $host.$appendPort; 118 | } 119 | 120 | protected function getCurrentPort() 121 | { 122 | // Check for proxy first 123 | $port = $this->getHeader('X_FORWARDED_PORT'); 124 | if ($port) { 125 | return (string) $port; 126 | } 127 | 128 | $protocol = (string) $this->getHeader('X_FORWARDED_PROTO'); 129 | if ($protocol === 'https') { 130 | return '443'; 131 | } 132 | 133 | return (string) $this->getServerVar('SERVER_PORT'); 134 | } 135 | 136 | /** 137 | * Returns the a value from the $_SERVER super global. 138 | * 139 | * @param string $key 140 | * 141 | * @return string 142 | */ 143 | protected function getServerVar($key) 144 | { 145 | return $_SERVER[$key] ?? ''; 146 | } 147 | 148 | /** 149 | * Gets a value from the HTTP request headers. 150 | * 151 | * @param string $key 152 | * 153 | * @return string 154 | */ 155 | protected function getHeader($key) 156 | { 157 | return $this->getServerVar('HTTP_'.$key); 158 | } 159 | 160 | /** 161 | * Checks if the value in X_FORWARDED_HOST is a valid hostname 162 | * Could prevent unintended redirections. 163 | * 164 | * @param string $header 165 | * 166 | * @return bool 167 | */ 168 | protected function isValidForwardedHost($header) 169 | { 170 | $elements = explode(',', $header); 171 | $host = $elements[count($elements) - 1]; 172 | 173 | return preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $host) //valid chars check 174 | && 0 < strlen($host) && strlen($host) < 254 //overall length check 175 | && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $host); //length of each label 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Url/UrlDetectionInterface.php: -------------------------------------------------------------------------------- 1 | 0) { 51 | $query = '?'.http_build_query($params, '', '&'); 52 | } 53 | } 54 | 55 | $scheme = isset($parts['scheme']) ? $parts['scheme'].'://' : ''; 56 | $host = $parts['host'] ?? ''; 57 | $port = isset($parts['port']) ? ':'.$parts['port'] : ''; 58 | $path = $parts['path'] ?? ''; 59 | $fragment = isset($parts['fragment']) ? '#'.$parts['fragment'] : ''; 60 | 61 | return $scheme.$host.$port.$path.$query.$fragment; 62 | } 63 | 64 | /** 65 | * Gracefully appends params to the URL. 66 | * 67 | * @param string $url the URL that will receive the params 68 | * @param array $newParams the params to append to the URL 69 | * 70 | * @return string 71 | */ 72 | public static function appendParamsToUrl($url, array $newParams = []) 73 | { 74 | if (empty($newParams)) { 75 | return $url; 76 | } 77 | 78 | if (strpos($url, '?') === false) { 79 | return $url.'?'.http_build_query($newParams, '', '&'); 80 | } 81 | 82 | list($path, $query) = explode('?', $url, 2); 83 | $existingParams = []; 84 | parse_str($query, $existingParams); 85 | 86 | // Favor params from the original URL over $newParams 87 | $newParams = array_merge($newParams, $existingParams); 88 | 89 | // Sort for a predicable order 90 | ksort($newParams); 91 | 92 | return $path.'?'.http_build_query($newParams, '', '&'); 93 | } 94 | 95 | /** 96 | * Returns the params from a URL in the form of an array. 97 | * 98 | * @param string $url the URL to parse the params from 99 | * 100 | * @return array 101 | */ 102 | public static function getParamsAsArray($url) 103 | { 104 | $query = parse_url($url, PHP_URL_QUERY); 105 | if (!$query) { 106 | return []; 107 | } 108 | $params = []; 109 | parse_str($query, $params); 110 | 111 | return $params; 112 | } 113 | 114 | /** 115 | * Adds the params of the first URL to the second URL. 116 | * 117 | * Any params that already exist in the second URL will go untouched. 118 | * 119 | * @param string $urlToStealFrom the URL harvest the params from 120 | * @param string $urlToAddTo the URL that will receive the new params 121 | * 122 | * @return string the $urlToAddTo with any new params from $urlToStealFrom 123 | */ 124 | public static function mergeUrlParams($urlToStealFrom, $urlToAddTo) 125 | { 126 | $newParams = static::getParamsAsArray($urlToStealFrom); 127 | // Nothing new to add, return as-is 128 | if (!$newParams) { 129 | return $urlToAddTo; 130 | } 131 | 132 | return static::appendParamsToUrl($urlToAddTo, $newParams); 133 | } 134 | 135 | /** 136 | * Check for a "/" prefix and prepend it if not exists. 137 | * 138 | * @param null|string $string 139 | * 140 | * @return string 141 | */ 142 | public static function forceSlashPrefix($string) 143 | { 144 | if (!$string) { 145 | return ''; 146 | } 147 | 148 | return strpos($string, '/') === 0 ? $string : '/'.$string; 149 | } 150 | 151 | /** 152 | * Trims off the hostname and Graph version from a URL. 153 | * 154 | * @param string $urlToTrim the URL the needs the surgery 155 | * 156 | * @return string the $urlToTrim with the hostname and Graph version removed 157 | */ 158 | public static function baseGraphUrlEndpoint($urlToTrim) 159 | { 160 | return '/'.preg_replace('/^https:\/\/.+\.facebook\.com(\/v.+?)?\//', '', $urlToTrim); 161 | } 162 | } 163 | --------------------------------------------------------------------------------