1 | <?php␊ |
2 | /**␊ |
3 | * Copyright 2011 Facebook, Inc.␊ |
4 | *␊ |
5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may␊ |
6 | * not use this file except in compliance with the License. You may obtain␊ |
7 | * a copy of the License at␊ |
8 | *␊ |
9 | * http://www.apache.org/licenses/LICENSE-2.0␊ |
10 | *␊ |
11 | * Unless required by applicable law or agreed to in writing, software␊ |
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT␊ |
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the␊ |
14 | * License for the specific language governing permissions and limitations␊ |
15 | * under the License.␊ |
16 | */␊ |
17 | ␊ |
18 | if (!function_exists('curl_init')) {␊ |
19 | throw new Exception('Facebook needs the CURL PHP extension.');␊ |
20 | }␊ |
21 | if (!function_exists('json_decode')) {␊ |
22 | throw new Exception('Facebook needs the JSON PHP extension.');␊ |
23 | }␊ |
24 | ␊ |
25 | /**␊ |
26 | * Thrown when an API call returns an exception.␊ |
27 | *␊ |
28 | * @author Naitik Shah <naitik@facebook.com>␊ |
29 | */␊ |
30 | class FacebookApiException extends Exception␊ |
31 | {␊ |
32 | /**␊ |
33 | * The result from the API server that represents the exception information.␊ |
34 | *␊ |
35 | * @var mixed␊ |
36 | */␊ |
37 | protected $result;␊ |
38 | ␊ |
39 | /**␊ |
40 | * Make a new API Exception with the given result.␊ |
41 | *␊ |
42 | * @param array $result The result from the API server␊ |
43 | */␊ |
44 | public function __construct($result) {␊ |
45 | $this->result = $result;␊ |
46 | ␊ |
47 | $code = 0;␊ |
48 | if (isset($result['error_code']) && is_int($result['error_code'])) {␊ |
49 | $code = $result['error_code'];␊ |
50 | }␊ |
51 | ␊ |
52 | if (isset($result['error_description'])) {␊ |
53 | // OAuth 2.0 Draft 10 style␊ |
54 | $msg = $result['error_description'];␊ |
55 | } else if (isset($result['error']) && is_array($result['error'])) {␊ |
56 | // OAuth 2.0 Draft 00 style␊ |
57 | $msg = $result['error']['message'];␊ |
58 | } else if (isset($result['error_msg'])) {␊ |
59 | // Rest server style␊ |
60 | $msg = $result['error_msg'];␊ |
61 | } else {␊ |
62 | $msg = 'Unknown Error. Check getResult()';␊ |
63 | }␊ |
64 | ␊ |
65 | parent::__construct($msg, $code);␊ |
66 | }␊ |
67 | ␊ |
68 | /**␊ |
69 | * Return the associated result object returned by the API server.␊ |
70 | *␊ |
71 | * @return array The result from the API server␊ |
72 | */␊ |
73 | public function getResult() {␊ |
74 | return $this->result;␊ |
75 | }␊ |
76 | ␊ |
77 | /**␊ |
78 | * Returns the associated type for the error. This will default to␊ |
79 | * 'Exception' when a type is not available.␊ |
80 | *␊ |
81 | * @return string␊ |
82 | */␊ |
83 | public function getType() {␊ |
84 | if (isset($this->result['error'])) {␊ |
85 | $error = $this->result['error'];␊ |
86 | if (is_string($error)) {␊ |
87 | // OAuth 2.0 Draft 10 style␊ |
88 | return $error;␊ |
89 | } else if (is_array($error)) {␊ |
90 | // OAuth 2.0 Draft 00 style␊ |
91 | if (isset($error['type'])) {␊ |
92 | return $error['type'];␊ |
93 | }␊ |
94 | }␊ |
95 | }␊ |
96 | ␊ |
97 | return 'Exception';␊ |
98 | }␊ |
99 | ␊ |
100 | /**␊ |
101 | * To make debugging easier.␊ |
102 | *␊ |
103 | * @return string The string representation of the error␊ |
104 | */␊ |
105 | public function __toString() {␊ |
106 | $str = $this->getType() . ': ';␊ |
107 | if ($this->code != 0) {␊ |
108 | $str .= $this->code . ': ';␊ |
109 | }␊ |
110 | return $str . $this->message;␊ |
111 | }␊ |
112 | }␊ |
113 | ␊ |
114 | /**␊ |
115 | * Provides access to the Facebook Platform. This class provides␊ |
116 | * a majority of the functionality needed, but the class is abstract␊ |
117 | * because it is designed to be sub-classed. The subclass must␊ |
118 | * implement the four abstract methods listed at the bottom of␊ |
119 | * the file.␊ |
120 | *␊ |
121 | * @author Naitik Shah <naitik@facebook.com>␊ |
122 | */␊ |
123 | abstract class BaseFacebook␊ |
124 | {␊ |
125 | /**␊ |
126 | * Version.␊ |
127 | */␊ |
128 | const VERSION = '3.2.3';␊ |
129 | ␊ |
130 | /**␊ |
131 | * Signed Request Algorithm.␊ |
132 | */␊ |
133 | const SIGNED_REQUEST_ALGORITHM = 'HMAC-SHA256';␊ |
134 | ␊ |
135 | /**␊ |
136 | * Default options for curl.␊ |
137 | *␊ |
138 | * @var array␊ |
139 | */␊ |
140 | public static $CURL_OPTS = array(␊ |
141 | CURLOPT_CONNECTTIMEOUT => 10,␊ |
142 | CURLOPT_RETURNTRANSFER => true,␊ |
143 | CURLOPT_TIMEOUT => 60,␊ |
144 | CURLOPT_USERAGENT => 'facebook-php-3.2',␊ |
145 | );␊ |
146 | ␊ |
147 | /**␊ |
148 | * List of query parameters that get automatically dropped when rebuilding␊ |
149 | * the current URL.␊ |
150 | *␊ |
151 | * @var array␊ |
152 | */␊ |
153 | protected static $DROP_QUERY_PARAMS = array(␊ |
154 | 'code',␊ |
155 | 'state',␊ |
156 | 'signed_request',␊ |
157 | );␊ |
158 | ␊ |
159 | /**␊ |
160 | * Maps aliases to Facebook domains.␊ |
161 | *␊ |
162 | * @var array␊ |
163 | */␊ |
164 | public static $DOMAIN_MAP = array(␊ |
165 | 'api' => 'https://api.facebook.com/',␊ |
166 | 'api_video' => 'https://api-video.facebook.com/',␊ |
167 | 'api_read' => 'https://api-read.facebook.com/',␊ |
168 | 'graph' => 'https://graph.facebook.com/',␊ |
169 | 'graph_video' => 'https://graph-video.facebook.com/',␊ |
170 | 'www' => 'https://www.facebook.com/',␊ |
171 | );␊ |
172 | ␊ |
173 | /**␊ |
174 | * The Application ID.␊ |
175 | *␊ |
176 | * @var string␊ |
177 | */␊ |
178 | protected $appId;␊ |
179 | ␊ |
180 | /**␊ |
181 | * The Application App Secret.␊ |
182 | *␊ |
183 | * @var string␊ |
184 | */␊ |
185 | protected $appSecret;␊ |
186 | ␊ |
187 | /**␊ |
188 | * The ID of the Facebook user, or 0 if the user is logged out.␊ |
189 | *␊ |
190 | * @var integer␊ |
191 | */␊ |
192 | protected $user;␊ |
193 | ␊ |
194 | /**␊ |
195 | * The data from the signed_request token.␊ |
196 | *␊ |
197 | * @var string␊ |
198 | */␊ |
199 | protected $signedRequest;␊ |
200 | ␊ |
201 | /**␊ |
202 | * A CSRF state variable to assist in the defense against CSRF attacks.␊ |
203 | *␊ |
204 | * @var string␊ |
205 | */␊ |
206 | protected $state;␊ |
207 | ␊ |
208 | /**␊ |
209 | * The OAuth access token received in exchange for a valid authorization␊ |
210 | * code. null means the access token has yet to be determined.␊ |
211 | *␊ |
212 | * @var string␊ |
213 | */␊ |
214 | protected $accessToken = null;␊ |
215 | ␊ |
216 | /**␊ |
217 | * Indicates if the CURL based @ syntax for file uploads is enabled.␊ |
218 | *␊ |
219 | * @var boolean␊ |
220 | */␊ |
221 | protected $fileUploadSupport = false;␊ |
222 | ␊ |
223 | /**␊ |
224 | * Indicates if we trust HTTP_X_FORWARDED_* headers.␊ |
225 | *␊ |
226 | * @var boolean␊ |
227 | */␊ |
228 | protected $trustForwarded = false;␊ |
229 | ␊ |
230 | /**␊ |
231 | * Indicates if signed_request is allowed in query parameters.␊ |
232 | *␊ |
233 | * @var boolean␊ |
234 | */␊ |
235 | protected $allowSignedRequest = true;␊ |
236 | ␊ |
237 | /**␊ |
238 | * Initialize a Facebook Application.␊ |
239 | *␊ |
240 | * The configuration:␊ |
241 | * - appId: the application ID␊ |
242 | * - secret: the application secret␊ |
243 | * - fileUpload: (optional) boolean indicating if file uploads are enabled␊ |
244 | * - allowSignedRequest: (optional) boolean indicating if signed_request is␊ |
245 | * allowed in query parameters or POST body. Should be␊ |
246 | * false for non-canvas apps. Defaults to true.␊ |
247 | *␊ |
248 | * @param array $config The application configuration␊ |
249 | */␊ |
250 | public function __construct($config) {␊ |
251 | $this->setAppId($config['appId']);␊ |
252 | $this->setAppSecret($config['secret']);␊ |
253 | if (isset($config['fileUpload'])) {␊ |
254 | $this->setFileUploadSupport($config['fileUpload']);␊ |
255 | }␊ |
256 | if (isset($config['access_token'])) {␊ |
257 | $this->setAccessToken($config['access_token']);␊ |
258 | }␊ |
259 | if (isset($config['trustForwarded']) && $config['trustForwarded']) {␊ |
260 | $this->trustForwarded = true;␊ |
261 | }␊ |
262 | if (isset($config['allowSignedRequest'])␊ |
263 | && !$config['allowSignedRequest']) {␊ |
264 | $this->allowSignedRequest = false;␊ |
265 | }␊ |
266 | $state = $this->getPersistentData('state');␊ |
267 | if (!empty($state)) {␊ |
268 | $this->state = $state;␊ |
269 | }␊ |
270 | }␊ |
271 | ␊ |
272 | /**␊ |
273 | * Set the Application ID.␊ |
274 | *␊ |
275 | * @param string $appId The Application ID␊ |
276 | *␊ |
277 | * @return BaseFacebook␊ |
278 | */␊ |
279 | public function setAppId($appId) {␊ |
280 | $this->appId = $appId;␊ |
281 | return $this;␊ |
282 | }␊ |
283 | ␊ |
284 | /**␊ |
285 | * Get the Application ID.␊ |
286 | *␊ |
287 | * @return string the Application ID␊ |
288 | */␊ |
289 | public function getAppId() {␊ |
290 | return $this->appId;␊ |
291 | }␊ |
292 | ␊ |
293 | /**␊ |
294 | * Set the App Secret.␊ |
295 | *␊ |
296 | * @param string $apiSecret The App Secret␊ |
297 | *␊ |
298 | * @return BaseFacebook␊ |
299 | * @deprecated Use setAppSecret instead.␊ |
300 | * @see setAppSecret()␊ |
301 | */␊ |
302 | public function setApiSecret($apiSecret) {␊ |
303 | $this->setAppSecret($apiSecret);␊ |
304 | return $this;␊ |
305 | }␊ |
306 | ␊ |
307 | /**␊ |
308 | * Set the App Secret.␊ |
309 | *␊ |
310 | * @param string $appSecret The App Secret␊ |
311 | *␊ |
312 | * @return BaseFacebook␊ |
313 | */␊ |
314 | public function setAppSecret($appSecret) {␊ |
315 | $this->appSecret = $appSecret;␊ |
316 | return $this;␊ |
317 | }␊ |
318 | ␊ |
319 | /**␊ |
320 | * Get the App Secret.␊ |
321 | *␊ |
322 | * @return string the App Secret␊ |
323 | *␊ |
324 | * @deprecated Use getAppSecret instead.␊ |
325 | * @see getAppSecret()␊ |
326 | */␊ |
327 | public function getApiSecret() {␊ |
328 | return $this->getAppSecret();␊ |
329 | }␊ |
330 | ␊ |
331 | /**␊ |
332 | * Get the App Secret.␊ |
333 | *␊ |
334 | * @return string the App Secret␊ |
335 | */␊ |
336 | public function getAppSecret() {␊ |
337 | return $this->appSecret;␊ |
338 | }␊ |
339 | ␊ |
340 | /**␊ |
341 | * Set the file upload support status.␊ |
342 | *␊ |
343 | * @param boolean $fileUploadSupport The file upload support status.␊ |
344 | *␊ |
345 | * @return BaseFacebook␊ |
346 | */␊ |
347 | public function setFileUploadSupport($fileUploadSupport) {␊ |
348 | $this->fileUploadSupport = $fileUploadSupport;␊ |
349 | return $this;␊ |
350 | }␊ |
351 | ␊ |
352 | /**␊ |
353 | * Get the file upload support status.␊ |
354 | *␊ |
355 | * @return boolean true if and only if the server supports file upload.␊ |
356 | */␊ |
357 | public function getFileUploadSupport() {␊ |
358 | return $this->fileUploadSupport;␊ |
359 | }␊ |
360 | ␊ |
361 | /**␊ |
362 | * Get the file upload support status.␊ |
363 | *␊ |
364 | * @return boolean true if and only if the server supports file upload.␊ |
365 | *␊ |
366 | * @deprecated Use getFileUploadSupport instead.␊ |
367 | * @see getFileUploadSupport()␊ |
368 | */␊ |
369 | public function useFileUploadSupport() {␊ |
370 | return $this->getFileUploadSupport();␊ |
371 | }␊ |
372 | ␊ |
373 | /**␊ |
374 | * Sets the access token for api calls. Use this if you get␊ |
375 | * your access token by other means and just want the SDK␊ |
376 | * to use it.␊ |
377 | *␊ |
378 | * @param string $access_token an access token.␊ |
379 | *␊ |
380 | * @return BaseFacebook␊ |
381 | */␊ |
382 | public function setAccessToken($access_token) {␊ |
383 | $this->accessToken = $access_token;␊ |
384 | return $this;␊ |
385 | }␊ |
386 | ␊ |
387 | /**␊ |
388 | * Extend an access token, while removing the short-lived token that might␊ |
389 | * have been generated via client-side flow. Thanks to http://bit.ly/b0Pt0H␊ |
390 | * for the workaround.␊ |
391 | */␊ |
392 | public function setExtendedAccessToken() {␊ |
393 | try {␊ |
394 | // need to circumvent json_decode by calling _oauthRequest␊ |
395 | // directly, since response isn't JSON format.␊ |
396 | $access_token_response = $this->_oauthRequest(␊ |
397 | $this->getUrl('graph', '/oauth/access_token'),␊ |
398 | $params = array(␊ |
399 | 'client_id' => $this->getAppId(),␊ |
400 | 'client_secret' => $this->getAppSecret(),␊ |
401 | 'grant_type' => 'fb_exchange_token',␊ |
402 | 'fb_exchange_token' => $this->getAccessToken(),␊ |
403 | )␊ |
404 | );␊ |
405 | }␊ |
406 | catch (FacebookApiException $e) {␊ |
407 | // most likely that user very recently revoked authorization.␊ |
408 | // In any event, we don't have an access token, so say so.␊ |
409 | return false;␊ |
410 | }␊ |
411 | ␊ |
412 | if (empty($access_token_response)) {␊ |
413 | return false;␊ |
414 | }␊ |
415 | ␊ |
416 | $response_params = array();␊ |
417 | parse_str($access_token_response, $response_params);␊ |
418 | ␊ |
419 | if (!isset($response_params['access_token'])) {␊ |
420 | return false;␊ |
421 | }␊ |
422 | ␊ |
423 | $this->destroySession();␊ |
424 | ␊ |
425 | $this->setPersistentData(␊ |
426 | 'access_token', $response_params['access_token']␊ |
427 | );␊ |
428 | ␊ |
429 | return true;␊ |
430 | }␊ |
431 | ␊ |
432 | /**␊ |
433 | * Determines the access token that should be used for API calls.␊ |
434 | * The first time this is called, $this->accessToken is set equal␊ |
435 | * to either a valid user access token, or it's set to the application␊ |
436 | * access token if a valid user access token wasn't available. Subsequent␊ |
437 | * calls return whatever the first call returned.␊ |
438 | *␊ |
439 | * @return string The access token␊ |
440 | */␊ |
441 | public function getAccessToken() {␊ |
442 | if ($this->accessToken !== null) {␊ |
443 | // we've done this already and cached it. Just return.␊ |
444 | return $this->accessToken;␊ |
445 | }␊ |
446 | ␊ |
447 | // first establish access token to be the application␊ |
448 | // access token, in case we navigate to the /oauth/access_token␊ |
449 | // endpoint, where SOME access token is required.␊ |
450 | $this->setAccessToken($this->getApplicationAccessToken());␊ |
451 | $user_access_token = $this->getUserAccessToken();␊ |
452 | if ($user_access_token) {␊ |
453 | $this->setAccessToken($user_access_token);␊ |
454 | }␊ |
455 | ␊ |
456 | return $this->accessToken;␊ |
457 | }␊ |
458 | ␊ |
459 | /**␊ |
460 | * Determines and returns the user access token, first using␊ |
461 | * the signed request if present, and then falling back on␊ |
462 | * the authorization code if present. The intent is to␊ |
463 | * return a valid user access token, or false if one is determined␊ |
464 | * to not be available.␊ |
465 | *␊ |
466 | * @return string A valid user access token, or false if one␊ |
467 | * could not be determined.␊ |
468 | */␊ |
469 | protected function getUserAccessToken() {␊ |
470 | // first, consider a signed request if it's supplied.␊ |
471 | // if there is a signed request, then it alone determines␊ |
472 | // the access token.␊ |
473 | $signed_request = $this->getSignedRequest();␊ |
474 | if ($signed_request) {␊ |
475 | // apps.facebook.com hands the access_token in the signed_request␊ |
476 | if (array_key_exists('oauth_token', $signed_request)) {␊ |
477 | $access_token = $signed_request['oauth_token'];␊ |
478 | $this->setPersistentData('access_token', $access_token);␊ |
479 | return $access_token;␊ |
480 | }␊ |
481 | ␊ |
482 | // the JS SDK puts a code in with the redirect_uri of ''␊ |
483 | if (array_key_exists('code', $signed_request)) {␊ |
484 | $code = $signed_request['code'];␊ |
485 | if ($code && $code == $this->getPersistentData('code')) {␊ |
486 | // short-circuit if the code we have is the same as the one presented␊ |
487 | return $this->getPersistentData('access_token');␊ |
488 | }␊ |
489 | ␊ |
490 | $access_token = $this->getAccessTokenFromCode($code, '');␊ |
491 | if ($access_token) {␊ |
492 | $this->setPersistentData('code', $code);␊ |
493 | $this->setPersistentData('access_token', $access_token);␊ |
494 | return $access_token;␊ |
495 | }␊ |
496 | }␊ |
497 | ␊ |
498 | // signed request states there's no access token, so anything␊ |
499 | // stored should be cleared.␊ |
500 | $this->clearAllPersistentData();␊ |
501 | return false; // respect the signed request's data, even␊ |
502 | // if there's an authorization code or something else␊ |
503 | }␊ |
504 | ␊ |
505 | $code = $this->getCode();␊ |
506 | if ($code && $code != $this->getPersistentData('code')) {␊ |
507 | $access_token = $this->getAccessTokenFromCode($code);␊ |
508 | if ($access_token) {␊ |
509 | $this->setPersistentData('code', $code);␊ |
510 | $this->setPersistentData('access_token', $access_token);␊ |
511 | return $access_token;␊ |
512 | }␊ |
513 | ␊ |
514 | // code was bogus, so everything based on it should be invalidated.␊ |
515 | $this->clearAllPersistentData();␊ |
516 | return false;␊ |
517 | }␊ |
518 | ␊ |
519 | // as a fallback, just return whatever is in the persistent␊ |
520 | // store, knowing nothing explicit (signed request, authorization␊ |
521 | // code, etc.) was present to shadow it (or we saw a code in $_REQUEST,␊ |
522 | // but it's the same as what's in the persistent store)␊ |
523 | return $this->getPersistentData('access_token');␊ |
524 | }␊ |
525 | ␊ |
526 | /**␊ |
527 | * Retrieve the signed request, either from a request parameter or,␊ |
528 | * if not present, from a cookie.␊ |
529 | *␊ |
530 | * @return string the signed request, if available, or null otherwise.␊ |
531 | */␊ |
532 | public function getSignedRequest() {␊ |
533 | if (!$this->signedRequest) {␊ |
534 | if ($this->allowSignedRequest && !empty($_REQUEST['signed_request'])) {␊ |
535 | $this->signedRequest = $this->parseSignedRequest(␊ |
536 | $_REQUEST['signed_request']␊ |
537 | );␊ |
538 | } else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) {␊ |
539 | $this->signedRequest = $this->parseSignedRequest(␊ |
540 | $_COOKIE[$this->getSignedRequestCookieName()]);␊ |
541 | }␊ |
542 | }␊ |
543 | return $this->signedRequest;␊ |
544 | }␊ |
545 | ␊ |
546 | /**␊ |
547 | * Get the UID of the connected user, or 0␊ |
548 | * if the Facebook user is not connected.␊ |
549 | *␊ |
550 | * @return string the UID if available.␊ |
551 | */␊ |
552 | public function getUser() {␊ |
553 | if ($this->user !== null) {␊ |
554 | // we've already determined this and cached the value.␊ |
555 | return $this->user;␊ |
556 | }␊ |
557 | ␊ |
558 | return $this->user = $this->getUserFromAvailableData();␊ |
559 | }␊ |
560 | ␊ |
561 | /**␊ |
562 | * Determines the connected user by first examining any signed␊ |
563 | * requests, then considering an authorization code, and then␊ |
564 | * falling back to any persistent store storing the user.␊ |
565 | *␊ |
566 | * @return integer The id of the connected Facebook user,␊ |
567 | * or 0 if no such user exists.␊ |
568 | */␊ |
569 | protected function getUserFromAvailableData() {␊ |
570 | // if a signed request is supplied, then it solely determines␊ |
571 | // who the user is.␊ |
572 | $signed_request = $this->getSignedRequest();␊ |
573 | if ($signed_request) {␊ |
574 | if (array_key_exists('user_id', $signed_request)) {␊ |
575 | $user = $signed_request['user_id'];␊ |
576 | ␊ |
577 | if($user != $this->getPersistentData('user_id')){␊ |
578 | $this->clearAllPersistentData();␊ |
579 | }␊ |
580 | ␊ |
581 | $this->setPersistentData('user_id', $signed_request['user_id']);␊ |
582 | return $user;␊ |
583 | }␊ |
584 | ␊ |
585 | // if the signed request didn't present a user id, then invalidate␊ |
586 | // all entries in any persistent store.␊ |
587 | $this->clearAllPersistentData();␊ |
588 | return 0;␊ |
589 | }␊ |
590 | ␊ |
591 | $user = $this->getPersistentData('user_id', $default = 0);␊ |
592 | $persisted_access_token = $this->getPersistentData('access_token');␊ |
593 | ␊ |
594 | // use access_token to fetch user id if we have a user access_token, or if␊ |
595 | // the cached access token has changed.␊ |
596 | $access_token = $this->getAccessToken();␊ |
597 | if ($access_token &&␊ |
598 | $access_token != $this->getApplicationAccessToken() &&␊ |
599 | !($user && $persisted_access_token == $access_token)) {␊ |
600 | $user = $this->getUserFromAccessToken();␊ |
601 | if ($user) {␊ |
602 | $this->setPersistentData('user_id', $user);␊ |
603 | } else {␊ |
604 | $this->clearAllPersistentData();␊ |
605 | }␊ |
606 | }␊ |
607 | ␊ |
608 | return $user;␊ |
609 | }␊ |
610 | ␊ |
611 | /**␊ |
612 | * Get a Login URL for use with redirects. By default, full page redirect is␊ |
613 | * assumed. If you are using the generated URL with a window.open() call in␊ |
614 | * JavaScript, you can pass in display=popup as part of the $params.␊ |
615 | *␊ |
616 | * The parameters:␊ |
617 | * - redirect_uri: the url to go to after a successful login␊ |
618 | * - scope: comma separated list of requested extended perms␊ |
619 | *␊ |
620 | * @param array $params Provide custom parameters␊ |
621 | * @return string The URL for the login flow␊ |
622 | */␊ |
623 | public function getLoginUrl($params=array()) {␊ |
624 | $this->establishCSRFTokenState();␊ |
625 | $currentUrl = $this->getCurrentUrl();␊ |
626 | ␊ |
627 | // if 'scope' is passed as an array, convert to comma separated list␊ |
628 | $scopeParams = isset($params['scope']) ? $params['scope'] : null;␊ |
629 | if ($scopeParams && is_array($scopeParams)) {␊ |
630 | $params['scope'] = implode(',', $scopeParams);␊ |
631 | }␊ |
632 | ␊ |
633 | return $this->getUrl(␊ |
634 | 'www',␊ |
635 | 'dialog/oauth',␊ |
636 | array_merge(␊ |
637 | array(␊ |
638 | 'client_id' => $this->getAppId(),␊ |
639 | 'redirect_uri' => $currentUrl, // possibly overwritten␊ |
640 | 'state' => $this->state,␊ |
641 | 'sdk' => 'php-sdk-'.self::VERSION␊ |
642 | ),␊ |
643 | $params␊ |
644 | ));␊ |
645 | }␊ |
646 | ␊ |
647 | /**␊ |
648 | * Get a Logout URL suitable for use with redirects.␊ |
649 | *␊ |
650 | * The parameters:␊ |
651 | * - next: the url to go to after a successful logout␊ |
652 | *␊ |
653 | * @param array $params Provide custom parameters␊ |
654 | * @return string The URL for the logout flow␊ |
655 | */␊ |
656 | public function getLogoutUrl($params=array()) {␊ |
657 | return $this->getUrl(␊ |
658 | 'www',␊ |
659 | 'logout.php',␊ |
660 | array_merge(array(␊ |
661 | 'next' => $this->getCurrentUrl(),␊ |
662 | 'access_token' => $this->getUserAccessToken(),␊ |
663 | ), $params)␊ |
664 | );␊ |
665 | }␊ |
666 | ␊ |
667 | /**␊ |
668 | * Make an API call.␊ |
669 | *␊ |
670 | * @return mixed The decoded response␊ |
671 | */␊ |
672 | public function api(/* polymorphic */) {␊ |
673 | $args = func_get_args();␊ |
674 | if (is_array($args[0])) {␊ |
675 | return $this->_restserver($args[0]);␊ |
676 | } else {␊ |
677 | return call_user_func_array(array($this, '_graph'), $args);␊ |
678 | }␊ |
679 | }␊ |
680 | ␊ |
681 | /**␊ |
682 | * Constructs and returns the name of the cookie that␊ |
683 | * potentially houses the signed request for the app user.␊ |
684 | * The cookie is not set by the BaseFacebook class, but␊ |
685 | * it may be set by the JavaScript SDK.␊ |
686 | *␊ |
687 | * @return string the name of the cookie that would house␊ |
688 | * the signed request value.␊ |
689 | */␊ |
690 | protected function getSignedRequestCookieName() {␊ |
691 | return 'fbsr_'.$this->getAppId();␊ |
692 | }␊ |
693 | ␊ |
694 | /**␊ |
695 | * Constructs and returns the name of the cookie that potentially contain␊ |
696 | * metadata. The cookie is not set by the BaseFacebook class, but it may be␊ |
697 | * set by the JavaScript SDK.␊ |
698 | *␊ |
699 | * @return string the name of the cookie that would house metadata.␊ |
700 | */␊ |
701 | protected function getMetadataCookieName() {␊ |
702 | return 'fbm_'.$this->getAppId();␊ |
703 | }␊ |
704 | ␊ |
705 | /**␊ |
706 | * Get the authorization code from the query parameters, if it exists,␊ |
707 | * and otherwise return false to signal no authorization code was␊ |
708 | * discoverable.␊ |
709 | *␊ |
710 | * @return mixed The authorization code, or false if the authorization␊ |
711 | * code could not be determined.␊ |
712 | */␊ |
713 | protected function getCode() {␊ |
714 | if (!isset($_REQUEST['code']) || !isset($_REQUEST['state'])) {␊ |
715 | return false;␊ |
716 | }␊ |
717 | if ($this->state === $_REQUEST['state']) {␊ |
718 | // CSRF state has done its job, so clear it␊ |
719 | $this->state = null;␊ |
720 | $this->clearPersistentData('state');␊ |
721 | return $_REQUEST['code'];␊ |
722 | }␊ |
723 | self::errorLog('CSRF state token does not match one provided.');␊ |
724 | ␊ |
725 | return false;␊ |
726 | }␊ |
727 | ␊ |
728 | /**␊ |
729 | * Retrieves the UID with the understanding that␊ |
730 | * $this->accessToken has already been set and is␊ |
731 | * seemingly legitimate. It relies on Facebook's Graph API␊ |
732 | * to retrieve user information and then extract␊ |
733 | * the user ID.␊ |
734 | *␊ |
735 | * @return integer Returns the UID of the Facebook user, or 0␊ |
736 | * if the Facebook user could not be determined.␊ |
737 | */␊ |
738 | protected function getUserFromAccessToken() {␊ |
739 | try {␊ |
740 | $user_info = $this->api('/me');␊ |
741 | return $user_info['id'];␊ |
742 | } catch (FacebookApiException $e) {␊ |
743 | return 0;␊ |
744 | }␊ |
745 | }␊ |
746 | ␊ |
747 | /**␊ |
748 | * Returns the access token that should be used for logged out␊ |
749 | * users when no authorization code is available.␊ |
750 | *␊ |
751 | * @return string The application access token, useful for gathering␊ |
752 | * public information about users and applications.␊ |
753 | */␊ |
754 | public function getApplicationAccessToken() {␊ |
755 | return $this->appId.'|'.$this->appSecret;␊ |
756 | }␊ |
757 | ␊ |
758 | /**␊ |
759 | * Lays down a CSRF state token for this process.␊ |
760 | *␊ |
761 | * @return void␊ |
762 | */␊ |
763 | protected function establishCSRFTokenState() {␊ |
764 | if ($this->state === null) {␊ |
765 | $this->state = md5(uniqid(mt_rand(), true));␊ |
766 | $this->setPersistentData('state', $this->state);␊ |
767 | }␊ |
768 | }␊ |
769 | ␊ |
770 | /**␊ |
771 | * Retrieves an access token for the given authorization code␊ |
772 | * (previously generated from www.facebook.com on behalf of␊ |
773 | * a specific user). The authorization code is sent to graph.facebook.com␊ |
774 | * and a legitimate access token is generated provided the access token␊ |
775 | * and the user for which it was generated all match, and the user is␊ |
776 | * either logged in to Facebook or has granted an offline access permission.␊ |
777 | *␊ |
778 | * @param string $code An authorization code.␊ |
779 | * @param string $redirect_uri Optional redirect URI. Default null␊ |
780 | *␊ |
781 | * @return mixed An access token exchanged for the authorization code, or␊ |
782 | * false if an access token could not be generated.␊ |
783 | */␊ |
784 | protected function getAccessTokenFromCode($code, $redirect_uri = null) {␊ |
785 | if (empty($code)) {␊ |
786 | return false;␊ |
787 | }␊ |
788 | ␊ |
789 | if ($redirect_uri === null) {␊ |
790 | $redirect_uri = $this->getCurrentUrl();␊ |
791 | }␊ |
792 | ␊ |
793 | try {␊ |
794 | // need to circumvent json_decode by calling _oauthRequest␊ |
795 | // directly, since response isn't JSON format.␊ |
796 | $access_token_response =␊ |
797 | $this->_oauthRequest(␊ |
798 | $this->getUrl('graph', '/oauth/access_token'),␊ |
799 | $params = array('client_id' => $this->getAppId(),␊ |
800 | 'client_secret' => $this->getAppSecret(),␊ |
801 | 'redirect_uri' => $redirect_uri,␊ |
802 | 'code' => $code));␊ |
803 | } catch (FacebookApiException $e) {␊ |
804 | // most likely that user very recently revoked authorization.␊ |
805 | // In any event, we don't have an access token, so say so.␊ |
806 | return false;␊ |
807 | }␊ |
808 | ␊ |
809 | if (empty($access_token_response)) {␊ |
810 | return false;␊ |
811 | }␊ |
812 | ␊ |
813 | $response_params = array();␊ |
814 | parse_str($access_token_response, $response_params);␊ |
815 | if (!isset($response_params['access_token'])) {␊ |
816 | return false;␊ |
817 | }␊ |
818 | ␊ |
819 | return $response_params['access_token'];␊ |
820 | }␊ |
821 | ␊ |
822 | /**␊ |
823 | * Invoke the old restserver.php endpoint.␊ |
824 | *␊ |
825 | * @param array $params Method call object␊ |
826 | *␊ |
827 | * @return mixed The decoded response object␊ |
828 | * @throws FacebookApiException␊ |
829 | */␊ |
830 | protected function _restserver($params) {␊ |
831 | // generic application level parameters␊ |
832 | $params['api_key'] = $this->getAppId();␊ |
833 | $params['format'] = 'json-strings';␊ |
834 | ␊ |
835 | $result = json_decode($this->_oauthRequest(␊ |
836 | $this->getApiUrl($params['method']),␊ |
837 | $params␊ |
838 | ), true);␊ |
839 | ␊ |
840 | // results are returned, errors are thrown␊ |
841 | if (is_array($result) && isset($result['error_code'])) {␊ |
842 | $this->throwAPIException($result);␊ |
843 | // @codeCoverageIgnoreStart␊ |
844 | }␊ |
845 | // @codeCoverageIgnoreEnd␊ |
846 | ␊ |
847 | $method = strtolower($params['method']);␊ |
848 | if ($method === 'auth.expiresession' ||␊ |
849 | $method === 'auth.revokeauthorization') {␊ |
850 | $this->destroySession();␊ |
851 | }␊ |
852 | ␊ |
853 | return $result;␊ |
854 | }␊ |
855 | ␊ |
856 | /**␊ |
857 | * Return true if this is video post.␊ |
858 | *␊ |
859 | * @param string $path The path␊ |
860 | * @param string $method The http method (default 'GET')␊ |
861 | *␊ |
862 | * @return boolean true if this is video post␊ |
863 | */␊ |
864 | protected function isVideoPost($path, $method = 'GET') {␊ |
865 | if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) {␊ |
866 | return true;␊ |
867 | }␊ |
868 | return false;␊ |
869 | }␊ |
870 | ␊ |
871 | /**␊ |
872 | * Invoke the Graph API.␊ |
873 | *␊ |
874 | * @param string $path The path (required)␊ |
875 | * @param string $method The http method (default 'GET')␊ |
876 | * @param array $params The query/post data␊ |
877 | *␊ |
878 | * @return mixed The decoded response object␊ |
879 | * @throws FacebookApiException␊ |
880 | */␊ |
881 | protected function _graph($path, $method = 'GET', $params = array()) {␊ |
882 | if (is_array($method) && empty($params)) {␊ |
883 | $params = $method;␊ |
884 | $method = 'GET';␊ |
885 | }␊ |
886 | $params['method'] = $method; // method override as we always do a POST␊ |
887 | ␊ |
888 | if ($this->isVideoPost($path, $method)) {␊ |
889 | $domainKey = 'graph_video';␊ |
890 | } else {␊ |
891 | $domainKey = 'graph';␊ |
892 | }␊ |
893 | ␊ |
894 | $result = json_decode($this->_oauthRequest(␊ |
895 | $this->getUrl($domainKey, $path),␊ |
896 | $params␊ |
897 | ), true);␊ |
898 | ␊ |
899 | // results are returned, errors are thrown␊ |
900 | if (is_array($result) && isset($result['error'])) {␊ |
901 | $this->throwAPIException($result);␊ |
902 | // @codeCoverageIgnoreStart␊ |
903 | }␊ |
904 | // @codeCoverageIgnoreEnd␊ |
905 | ␊ |
906 | return $result;␊ |
907 | }␊ |
908 | ␊ |
909 | /**␊ |
910 | * Make a OAuth Request.␊ |
911 | *␊ |
912 | * @param string $url The path (required)␊ |
913 | * @param array $params The query/post data␊ |
914 | *␊ |
915 | * @return string The decoded response object␊ |
916 | * @throws FacebookApiException␊ |
917 | */␊ |
918 | protected function _oauthRequest($url, $params) {␊ |
919 | if (!isset($params['access_token'])) {␊ |
920 | $params['access_token'] = $this->getAccessToken();␊ |
921 | }␊ |
922 | ␊ |
923 | if (isset($params['access_token']) && !isset($params['appsecret_proof'])) {␊ |
924 | $params['appsecret_proof'] = $this->getAppSecretProof($params['access_token']);␊ |
925 | }␊ |
926 | ␊ |
927 | // json_encode all params values that are not strings␊ |
928 | foreach ($params as $key => $value) {␊ |
929 | if (!is_string($value) && !($value instanceof CURLFile)) {␊ |
930 | $params[$key] = json_encode($value);␊ |
931 | }␊ |
932 | }␊ |
933 | ␊ |
934 | return $this->makeRequest($url, $params);␊ |
935 | }␊ |
936 | ␊ |
937 | /**␊ |
938 | * Generate a proof of App Secret␊ |
939 | * This is required for all API calls originating from a server␊ |
940 | * It is a sha256 hash of the access_token made using the app secret␊ |
941 | *␊ |
942 | * @param string $access_token The access_token to be hashed (required)␊ |
943 | *␊ |
944 | * @return string The sha256 hash of the access_token␊ |
945 | */␊ |
946 | protected function getAppSecretProof($access_token) {␊ |
947 | return hash_hmac('sha256', $access_token, $this->getAppSecret());␊ |
948 | }␊ |
949 | ␊ |
950 | /**␊ |
951 | * Makes an HTTP request. This method can be overridden by subclasses if␊ |
952 | * developers want to do fancier things or use something other than curl to␊ |
953 | * make the request.␊ |
954 | *␊ |
955 | * @param string $url The URL to make the request to␊ |
956 | * @param array $params The parameters to use for the POST body␊ |
957 | * @param CurlHandler $ch Initialized curl handle␊ |
958 | *␊ |
959 | * @return string The response text␊ |
960 | */␊ |
961 | protected function makeRequest($url, $params, $ch=null) {␊ |
962 | if (!$ch) {␊ |
963 | $ch = curl_init();␊ |
964 | }␊ |
965 | ␊ |
966 | $opts = self::$CURL_OPTS;␊ |
967 | if ($this->getFileUploadSupport()) {␊ |
968 | $opts[CURLOPT_POSTFIELDS] = $params;␊ |
969 | } else {␊ |
970 | $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&');␊ |
971 | }␊ |
972 | $opts[CURLOPT_URL] = $url;␊ |
973 | ␊ |
974 | // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait␊ |
975 | // for 2 seconds if the server does not support this header.␊ |
976 | if (isset($opts[CURLOPT_HTTPHEADER])) {␊ |
977 | $existing_headers = $opts[CURLOPT_HTTPHEADER];␊ |
978 | $existing_headers[] = 'Expect:';␊ |
979 | $opts[CURLOPT_HTTPHEADER] = $existing_headers;␊ |
980 | } else {␊ |
981 | $opts[CURLOPT_HTTPHEADER] = array('Expect:');␊ |
982 | }␊ |
983 | ␊ |
984 | curl_setopt_array($ch, $opts);␊ |
985 | $result = curl_exec($ch);␊ |
986 | ␊ |
987 | $errno = curl_errno($ch);␊ |
988 | // CURLE_SSL_CACERT || CURLE_SSL_CACERT_BADFILE␊ |
989 | if ($errno == 60 || $errno == 77) {␊ |
990 | self::errorLog('Invalid or no certificate authority found, '.␊ |
991 | 'using bundled information');␊ |
992 | curl_setopt($ch, CURLOPT_CAINFO,␊ |
993 | dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt');␊ |
994 | $result = curl_exec($ch);␊ |
995 | }␊ |
996 | ␊ |
997 | // With dual stacked DNS responses, it's possible for a server to␊ |
998 | // have IPv6 enabled but not have IPv6 connectivity. If this is␊ |
999 | // the case, curl will try IPv4 first and if that fails, then it will␊ |
1000 | // fall back to IPv6 and the error EHOSTUNREACH is returned by the␊ |
1001 | // operating system.␊ |
1002 | if ($result === false && empty($opts[CURLOPT_IPRESOLVE])) {␊ |
1003 | $matches = array();␊ |
1004 | $regex = '/Failed to connect to ([^:].*): Network is unreachable/';␊ |
1005 | if (preg_match($regex, curl_error($ch), $matches)) {␊ |
1006 | if (strlen(@inet_pton($matches[1])) === 16) {␊ |
1007 | self::errorLog('Invalid IPv6 configuration on server, '.␊ |
1008 | 'Please disable or get native IPv6 on your server.');␊ |
1009 | self::$CURL_OPTS[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;␊ |
1010 | curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);␊ |
1011 | $result = curl_exec($ch);␊ |
1012 | }␊ |
1013 | }␊ |
1014 | }␊ |
1015 | ␊ |
1016 | if ($result === false) {␊ |
1017 | $e = new FacebookApiException(array(␊ |
1018 | 'error_code' => curl_errno($ch),␊ |
1019 | 'error' => array(␊ |
1020 | 'message' => curl_error($ch),␊ |
1021 | 'type' => 'CurlException',␊ |
1022 | ),␊ |
1023 | ));␊ |
1024 | curl_close($ch);␊ |
1025 | throw $e;␊ |
1026 | }␊ |
1027 | curl_close($ch);␊ |
1028 | return $result;␊ |
1029 | }␊ |
1030 | ␊ |
1031 | /**␊ |
1032 | * Parses a signed_request and validates the signature.␊ |
1033 | *␊ |
1034 | * @param string $signed_request A signed token␊ |
1035 | *␊ |
1036 | * @return array The payload inside it or null if the sig is wrong␊ |
1037 | */␊ |
1038 | protected function parseSignedRequest($signed_request) {␊ |
1039 | ␊ |
1040 | if (!$signed_request || strpos($signed_request, '.') === false) {␊ |
1041 | self::errorLog('Signed request was invalid!');␊ |
1042 | return null;␊ |
1043 | }␊ |
1044 | ␊ |
1045 | list($encoded_sig, $payload) = explode('.', $signed_request, 2);␊ |
1046 | ␊ |
1047 | // decode the data␊ |
1048 | $sig = self::base64UrlDecode($encoded_sig);␊ |
1049 | $data = json_decode(self::base64UrlDecode($payload), true);␊ |
1050 | ␊ |
1051 | if (!isset($data['algorithm'])␊ |
1052 | || strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM␊ |
1053 | ) {␊ |
1054 | self::errorLog(␊ |
1055 | 'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM);␊ |
1056 | return null;␊ |
1057 | }␊ |
1058 | ␊ |
1059 | // check sig␊ |
1060 | $expected_sig = hash_hmac('sha256', $payload,␊ |
1061 | $this->getAppSecret(), $raw = true);␊ |
1062 | ␊ |
1063 | if (strlen($expected_sig) !== strlen($sig)) {␊ |
1064 | self::errorLog('Bad Signed JSON signature!');␊ |
1065 | return null;␊ |
1066 | }␊ |
1067 | ␊ |
1068 | $result = 0;␊ |
1069 | for ($i = 0; $i < strlen($expected_sig); $i++) {␊ |
1070 | $result |= ord($expected_sig[$i]) ^ ord($sig[$i]);␊ |
1071 | }␊ |
1072 | ␊ |
1073 | if ($result == 0) {␊ |
1074 | return $data;␊ |
1075 | } else {␊ |
1076 | self::errorLog('Bad Signed JSON signature!');␊ |
1077 | return null;␊ |
1078 | }␊ |
1079 | }␊ |
1080 | ␊ |
1081 | /**␊ |
1082 | * Makes a signed_request blob using the given data.␊ |
1083 | *␊ |
1084 | * @param array $data The data array.␊ |
1085 | *␊ |
1086 | * @return string The signed request.␊ |
1087 | */␊ |
1088 | protected function makeSignedRequest($data) {␊ |
1089 | if (!is_array($data)) {␊ |
1090 | throw new InvalidArgumentException(␊ |
1091 | 'makeSignedRequest expects an array. Got: ' . print_r($data, true));␊ |
1092 | }␊ |
1093 | $data['algorithm'] = self::SIGNED_REQUEST_ALGORITHM;␊ |
1094 | $data['issued_at'] = time();␊ |
1095 | $json = json_encode($data);␊ |
1096 | $b64 = self::base64UrlEncode($json);␊ |
1097 | $raw_sig = hash_hmac('sha256', $b64, $this->getAppSecret(), $raw = true);␊ |
1098 | $sig = self::base64UrlEncode($raw_sig);␊ |
1099 | return $sig.'.'.$b64;␊ |
1100 | }␊ |
1101 | ␊ |
1102 | /**␊ |
1103 | * Build the URL for api given parameters.␊ |
1104 | *␊ |
1105 | * @param string $method The method name.␊ |
1106 | *␊ |
1107 | * @return string The URL for the given parameters␊ |
1108 | */␊ |
1109 | protected function getApiUrl($method) {␊ |
1110 | static $READ_ONLY_CALLS =␊ |
1111 | array('admin.getallocation' => 1,␊ |
1112 | 'admin.getappproperties' => 1,␊ |
1113 | 'admin.getbannedusers' => 1,␊ |
1114 | 'admin.getlivestreamvialink' => 1,␊ |
1115 | 'admin.getmetrics' => 1,␊ |
1116 | 'admin.getrestrictioninfo' => 1,␊ |
1117 | 'application.getpublicinfo' => 1,␊ |
1118 | 'auth.getapppublickey' => 1,␊ |
1119 | 'auth.getsession' => 1,␊ |
1120 | 'auth.getsignedpublicsessiondata' => 1,␊ |
1121 | 'comments.get' => 1,␊ |
1122 | 'connect.getunconnectedfriendscount' => 1,␊ |
1123 | 'dashboard.getactivity' => 1,␊ |
1124 | 'dashboard.getcount' => 1,␊ |
1125 | 'dashboard.getglobalnews' => 1,␊ |
1126 | 'dashboard.getnews' => 1,␊ |
1127 | 'dashboard.multigetcount' => 1,␊ |
1128 | 'dashboard.multigetnews' => 1,␊ |
1129 | 'data.getcookies' => 1,␊ |
1130 | 'events.get' => 1,␊ |
1131 | 'events.getmembers' => 1,␊ |
1132 | 'fbml.getcustomtags' => 1,␊ |
1133 | 'feed.getappfriendstories' => 1,␊ |
1134 | 'feed.getregisteredtemplatebundlebyid' => 1,␊ |
1135 | 'feed.getregisteredtemplatebundles' => 1,␊ |
1136 | 'fql.multiquery' => 1,␊ |
1137 | 'fql.query' => 1,␊ |
1138 | 'friends.arefriends' => 1,␊ |
1139 | 'friends.get' => 1,␊ |
1140 | 'friends.getappusers' => 1,␊ |
1141 | 'friends.getlists' => 1,␊ |
1142 | 'friends.getmutualfriends' => 1,␊ |
1143 | 'gifts.get' => 1,␊ |
1144 | 'groups.get' => 1,␊ |
1145 | 'groups.getmembers' => 1,␊ |
1146 | 'intl.gettranslations' => 1,␊ |
1147 | 'links.get' => 1,␊ |
1148 | 'notes.get' => 1,␊ |
1149 | 'notifications.get' => 1,␊ |
1150 | 'pages.getinfo' => 1,␊ |
1151 | 'pages.isadmin' => 1,␊ |
1152 | 'pages.isappadded' => 1,␊ |
1153 | 'pages.isfan' => 1,␊ |
1154 | 'permissions.checkavailableapiaccess' => 1,␊ |
1155 | 'permissions.checkgrantedapiaccess' => 1,␊ |
1156 | 'photos.get' => 1,␊ |
1157 | 'photos.getalbums' => 1,␊ |
1158 | 'photos.gettags' => 1,␊ |
1159 | 'profile.getinfo' => 1,␊ |
1160 | 'profile.getinfooptions' => 1,␊ |
1161 | 'stream.get' => 1,␊ |
1162 | 'stream.getcomments' => 1,␊ |
1163 | 'stream.getfilters' => 1,␊ |
1164 | 'users.getinfo' => 1,␊ |
1165 | 'users.getloggedinuser' => 1,␊ |
1166 | 'users.getstandardinfo' => 1,␊ |
1167 | 'users.hasapppermission' => 1,␊ |
1168 | 'users.isappuser' => 1,␊ |
1169 | 'users.isverified' => 1,␊ |
1170 | 'video.getuploadlimits' => 1);␊ |
1171 | $name = 'api';␊ |
1172 | if (isset($READ_ONLY_CALLS[strtolower($method)])) {␊ |
1173 | $name = 'api_read';␊ |
1174 | } else if (strtolower($method) == 'video.upload') {␊ |
1175 | $name = 'api_video';␊ |
1176 | }␊ |
1177 | return self::getUrl($name, 'restserver.php');␊ |
1178 | }␊ |
1179 | ␊ |
1180 | /**␊ |
1181 | * Build the URL for given domain alias, path and parameters.␊ |
1182 | *␊ |
1183 | * @param string $name The name of the domain␊ |
1184 | * @param string $path Optional path (without a leading slash)␊ |
1185 | * @param array $params Optional query parameters␊ |
1186 | *␊ |
1187 | * @return string The URL for the given parameters␊ |
1188 | */␊ |
1189 | protected function getUrl($name, $path='', $params=array()) {␊ |
1190 | $url = self::$DOMAIN_MAP[$name];␊ |
1191 | if ($path) {␊ |
1192 | if ($path[0] === '/') {␊ |
1193 | $path = substr($path, 1);␊ |
1194 | }␊ |
1195 | $url .= $path;␊ |
1196 | }␊ |
1197 | if ($params) {␊ |
1198 | $url .= '?' . http_build_query($params, null, '&');␊ |
1199 | }␊ |
1200 | ␊ |
1201 | return $url;␊ |
1202 | }␊ |
1203 | ␊ |
1204 | /**␊ |
1205 | * Returns the HTTP Host␊ |
1206 | *␊ |
1207 | * @return string The HTTP Host␊ |
1208 | */␊ |
1209 | protected function getHttpHost() {␊ |
1210 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) {␊ |
1211 | $forwardProxies = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);␊ |
1212 | if (!empty($forwardProxies)) {␊ |
1213 | return $forwardProxies[0];␊ |
1214 | }␊ |
1215 | }␊ |
1216 | return $_SERVER['HTTP_HOST'];␊ |
1217 | }␊ |
1218 | ␊ |
1219 | /**␊ |
1220 | * Returns the HTTP Protocol␊ |
1221 | *␊ |
1222 | * @return string The HTTP Protocol␊ |
1223 | */␊ |
1224 | protected function getHttpProtocol() {␊ |
1225 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {␊ |
1226 | if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {␊ |
1227 | return 'https';␊ |
1228 | }␊ |
1229 | return 'http';␊ |
1230 | }␊ |
1231 | /*apache + variants specific way of checking for https*/␊ |
1232 | if (isset($_SERVER['HTTPS']) &&␊ |
1233 | ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) {␊ |
1234 | return 'https';␊ |
1235 | }␊ |
1236 | /*nginx way of checking for https*/␊ |
1237 | if (isset($_SERVER['SERVER_PORT']) &&␊ |
1238 | ($_SERVER['SERVER_PORT'] === '443')) {␊ |
1239 | return 'https';␊ |
1240 | }␊ |
1241 | return 'http';␊ |
1242 | }␊ |
1243 | ␊ |
1244 | /**␊ |
1245 | * Returns the base domain used for the cookie.␊ |
1246 | *␊ |
1247 | * @return string The base domain␊ |
1248 | */␊ |
1249 | protected function getBaseDomain() {␊ |
1250 | // The base domain is stored in the metadata cookie if not we fallback␊ |
1251 | // to the current hostname␊ |
1252 | $metadata = $this->getMetadataCookie();␊ |
1253 | if (array_key_exists('base_domain', $metadata) &&␊ |
1254 | !empty($metadata['base_domain'])) {␊ |
1255 | return trim($metadata['base_domain'], '.');␊ |
1256 | }␊ |
1257 | return $this->getHttpHost();␊ |
1258 | }␊ |
1259 | ␊ |
1260 | /**␊ |
1261 | * Returns the Current URL, stripping it of known FB parameters that should␊ |
1262 | * not persist.␊ |
1263 | *␊ |
1264 | * @return string The current URL␊ |
1265 | */␊ |
1266 | protected function getCurrentUrl() {␊ |
1267 | $protocol = $this->getHttpProtocol() . '://';␊ |
1268 | $host = $this->getHttpHost();␊ |
1269 | $currentUrl = $protocol.$host.$_SERVER['REQUEST_URI'];␊ |
1270 | $parts = parse_url($currentUrl);␊ |
1271 | ␊ |
1272 | $query = '';␊ |
1273 | if (!empty($parts['query'])) {␊ |
1274 | // drop known fb params␊ |
1275 | $params = explode('&', $parts['query']);␊ |
1276 | $retained_params = array();␊ |
1277 | foreach ($params as $param) {␊ |
1278 | if ($this->shouldRetainParam($param)) {␊ |
1279 | $retained_params[] = $param;␊ |
1280 | }␊ |
1281 | }␊ |
1282 | ␊ |
1283 | if (!empty($retained_params)) {␊ |
1284 | $query = '?'.implode($retained_params, '&');␊ |
1285 | }␊ |
1286 | }␊ |
1287 | ␊ |
1288 | // use port if non default␊ |
1289 | $port =␊ |
1290 | isset($parts['port']) &&␊ |
1291 | (($protocol === 'http://' && $parts['port'] !== 80) ||␊ |
1292 | ($protocol === 'https://' && $parts['port'] !== 443))␊ |
1293 | ? ':' . $parts['port'] : '';␊ |
1294 | ␊ |
1295 | // rebuild␊ |
1296 | return $protocol . $parts['host'] . $port . $parts['path'] . $query;␊ |
1297 | }␊ |
1298 | ␊ |
1299 | /**␊ |
1300 | * Returns true if and only if the key or key/value pair should␊ |
1301 | * be retained as part of the query string. This amounts to␊ |
1302 | * a brute-force search of the very small list of Facebook-specific␊ |
1303 | * params that should be stripped out.␊ |
1304 | *␊ |
1305 | * @param string $param A key or key/value pair within a URL's query (e.g.␊ |
1306 | * 'foo=a', 'foo=', or 'foo'.␊ |
1307 | *␊ |
1308 | * @return boolean␊ |
1309 | */␊ |
1310 | protected function shouldRetainParam($param) {␊ |
1311 | foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) {␊ |
1312 | if ($param === $drop_query_param ||␊ |
1313 | strpos($param, $drop_query_param.'=') === 0) {␊ |
1314 | return false;␊ |
1315 | }␊ |
1316 | }␊ |
1317 | ␊ |
1318 | return true;␊ |
1319 | }␊ |
1320 | ␊ |
1321 | /**␊ |
1322 | * Analyzes the supplied result to see if it was thrown␊ |
1323 | * because the access token is no longer valid. If that is␊ |
1324 | * the case, then we destroy the session.␊ |
1325 | *␊ |
1326 | * @param array $result A record storing the error message returned␊ |
1327 | * by a failed API call.␊ |
1328 | */␊ |
1329 | protected function throwAPIException($result) {␊ |
1330 | $e = new FacebookApiException($result);␊ |
1331 | switch ($e->getType()) {␊ |
1332 | // OAuth 2.0 Draft 00 style␊ |
1333 | case 'OAuthException':␊ |
1334 | // OAuth 2.0 Draft 10 style␊ |
1335 | case 'invalid_token':␊ |
1336 | // REST server errors are just Exceptions␊ |
1337 | case 'Exception':␊ |
1338 | $message = $e->getMessage();␊ |
1339 | if ((strpos($message, 'Error validating access token') !== false) ||␊ |
1340 | (strpos($message, 'Invalid OAuth access token') !== false) ||␊ |
1341 | (strpos($message, 'An active access token must be used') !== false)␊ |
1342 | ) {␊ |
1343 | $this->destroySession();␊ |
1344 | }␊ |
1345 | break;␊ |
1346 | }␊ |
1347 | ␊ |
1348 | throw $e;␊ |
1349 | }␊ |
1350 | ␊ |
1351 | ␊ |
1352 | /**␊ |
1353 | * Prints to the error log if you aren't in command line mode.␊ |
1354 | *␊ |
1355 | * @param string $msg Log message␊ |
1356 | */␊ |
1357 | protected static function errorLog($msg) {␊ |
1358 | // disable error log if we are running in a CLI environment␊ |
1359 | // @codeCoverageIgnoreStart␊ |
1360 | if (php_sapi_name() != 'cli') {␊ |
1361 | error_log($msg);␊ |
1362 | }␊ |
1363 | // uncomment this if you want to see the errors on the page␊ |
1364 | // print 'error_log: '.$msg."\n";␊ |
1365 | // @codeCoverageIgnoreEnd␊ |
1366 | }␊ |
1367 | ␊ |
1368 | /**␊ |
1369 | * Base64 encoding that doesn't need to be urlencode()ed.␊ |
1370 | * Exactly the same as base64_encode except it uses␊ |
1371 | * - instead of +␊ |
1372 | * _ instead of /␊ |
1373 | * No padded =␊ |
1374 | *␊ |
1375 | * @param string $input base64UrlEncoded input␊ |
1376 | *␊ |
1377 | * @return string The decoded string␊ |
1378 | */␊ |
1379 | protected static function base64UrlDecode($input) {␊ |
1380 | return base64_decode(strtr($input, '-_', '+/'));␊ |
1381 | }␊ |
1382 | ␊ |
1383 | /**␊ |
1384 | * Base64 encoding that doesn't need to be urlencode()ed.␊ |
1385 | * Exactly the same as base64_encode except it uses␊ |
1386 | * - instead of +␊ |
1387 | * _ instead of /␊ |
1388 | *␊ |
1389 | * @param string $input The input to encode␊ |
1390 | * @return string The base64Url encoded input, as a string.␊ |
1391 | */␊ |
1392 | protected static function base64UrlEncode($input) {␊ |
1393 | $str = strtr(base64_encode($input), '+/', '-_');␊ |
1394 | $str = str_replace('=', '', $str);␊ |
1395 | return $str;␊ |
1396 | }␊ |
1397 | ␊ |
1398 | /**␊ |
1399 | * Destroy the current session␊ |
1400 | */␊ |
1401 | public function destroySession() {␊ |
1402 | $this->accessToken = null;␊ |
1403 | $this->signedRequest = null;␊ |
1404 | $this->user = null;␊ |
1405 | $this->clearAllPersistentData();␊ |
1406 | ␊ |
1407 | // Javascript sets a cookie that will be used in getSignedRequest that we␊ |
1408 | // need to clear if we can␊ |
1409 | $cookie_name = $this->getSignedRequestCookieName();␊ |
1410 | if (array_key_exists($cookie_name, $_COOKIE)) {␊ |
1411 | unset($_COOKIE[$cookie_name]);␊ |
1412 | if (!headers_sent()) {␊ |
1413 | $base_domain = $this->getBaseDomain();␊ |
1414 | setcookie($cookie_name, '', 1, '/', '.'.$base_domain);␊ |
1415 | } else {␊ |
1416 | // @codeCoverageIgnoreStart␊ |
1417 | self::errorLog(␊ |
1418 | 'There exists a cookie that we wanted to clear that we couldn\'t '.␊ |
1419 | 'clear because headers was already sent. Make sure to do the first '.␊ |
1420 | 'API call before outputing anything.'␊ |
1421 | );␊ |
1422 | // @codeCoverageIgnoreEnd␊ |
1423 | }␊ |
1424 | }␊ |
1425 | }␊ |
1426 | ␊ |
1427 | /**␊ |
1428 | * Parses the metadata cookie that our Javascript API set␊ |
1429 | *␊ |
1430 | * @return array an array mapping key to value␊ |
1431 | */␊ |
1432 | protected function getMetadataCookie() {␊ |
1433 | $cookie_name = $this->getMetadataCookieName();␊ |
1434 | if (!array_key_exists($cookie_name, $_COOKIE)) {␊ |
1435 | return array();␊ |
1436 | }␊ |
1437 | ␊ |
1438 | // The cookie value can be wrapped in "-characters so remove them␊ |
1439 | $cookie_value = trim($_COOKIE[$cookie_name], '"');␊ |
1440 | ␊ |
1441 | if (empty($cookie_value)) {␊ |
1442 | return array();␊ |
1443 | }␊ |
1444 | ␊ |
1445 | $parts = explode('&', $cookie_value);␊ |
1446 | $metadata = array();␊ |
1447 | foreach ($parts as $part) {␊ |
1448 | $pair = explode('=', $part, 2);␊ |
1449 | if (!empty($pair[0])) {␊ |
1450 | $metadata[urldecode($pair[0])] =␊ |
1451 | (count($pair) > 1) ? urldecode($pair[1]) : '';␊ |
1452 | }␊ |
1453 | }␊ |
1454 | ␊ |
1455 | return $metadata;␊ |
1456 | }␊ |
1457 | ␊ |
1458 | /**␊ |
1459 | * Finds whether the given domain is allowed or not␊ |
1460 | *␊ |
1461 | * @param string $big The value to be checked against $small␊ |
1462 | * @param string $small The input string␊ |
1463 | *␊ |
1464 | * @return boolean Returns TRUE if $big matches $small␊ |
1465 | */␊ |
1466 | protected static function isAllowedDomain($big, $small) {␊ |
1467 | if ($big === $small) {␊ |
1468 | return true;␊ |
1469 | }␊ |
1470 | return self::endsWith($big, '.'.$small);␊ |
1471 | }␊ |
1472 | ␊ |
1473 | /**␊ |
1474 | * Checks if $big string ends with $small string␊ |
1475 | *␊ |
1476 | * @param string $big The value to be checked against $small␊ |
1477 | * @param string $small The input string␊ |
1478 | *␊ |
1479 | * @return boolean TRUE if $big ends with $small␊ |
1480 | */␊ |
1481 | protected static function endsWith($big, $small) {␊ |
1482 | $len = strlen($small);␊ |
1483 | if ($len === 0) {␊ |
1484 | return true;␊ |
1485 | }␊ |
1486 | return substr($big, -$len) === $small;␊ |
1487 | }␊ |
1488 | ␊ |
1489 | /**␊ |
1490 | * Each of the following four methods should be overridden in␊ |
1491 | * a concrete subclass, as they are in the provided Facebook class.␊ |
1492 | * The Facebook class uses PHP sessions to provide a primitive␊ |
1493 | * persistent store, but another subclass--one that you implement--␊ |
1494 | * might use a database, memcache, or an in-memory cache.␊ |
1495 | *␊ |
1496 | * @see Facebook␊ |
1497 | */␊ |
1498 | ␊ |
1499 | /**␊ |
1500 | * Stores the given ($key, $value) pair, so that future calls to␊ |
1501 | * getPersistentData($key) return $value. This call may be in another request.␊ |
1502 | *␊ |
1503 | * @param string $key␊ |
1504 | * @param array $value␊ |
1505 | *␊ |
1506 | * @return void␊ |
1507 | */␊ |
1508 | abstract protected function setPersistentData($key, $value);␊ |
1509 | ␊ |
1510 | /**␊ |
1511 | * Get the data for $key, persisted by BaseFacebook::setPersistentData()␊ |
1512 | *␊ |
1513 | * @param string $key The key of the data to retrieve␊ |
1514 | * @param boolean $default The default value to return if $key is not found␊ |
1515 | *␊ |
1516 | * @return mixed␊ |
1517 | */␊ |
1518 | abstract protected function getPersistentData($key, $default = false);␊ |
1519 | ␊ |
1520 | /**␊ |
1521 | * Clear the data with $key from the persistent storage␊ |
1522 | *␊ |
1523 | * @param string $key␊ |
1524 | *␊ |
1525 | * @return void␊ |
1526 | */␊ |
1527 | abstract protected function clearPersistentData($key);␊ |
1528 | ␊ |
1529 | /**␊ |
1530 | * Clear all data from the persistent storage␊ |
1531 | *␊ |
1532 | * @return void␊ |
1533 | */␊ |
1534 | abstract protected function clearAllPersistentData();␊ |
1535 | }␊ |