Facebook to email

Facebook to email Git Source Tree

Root/base_facebook.php

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
18if (!function_exists('curl_init')) {
19 throw new Exception('Facebook needs the CURL PHP extension.');
20}
21if (!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 */
30class 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 */
123abstract 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}

Archive Download this file

Branches