]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookBridge/extlib/base_facebook.php
Snapshot of the Transifex translation project - October 2015
[quix0rs-gnu-social.git] / plugins / FacebookBridge / extlib / 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
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   protected $result;
36
37   /**
38    * Make a new API Exception with the given result.
39    *
40    * @param array $result The result from the API server
41    */
42   public function __construct($result) {
43     $this->result = $result;
44
45     $code = isset($result['error_code']) ? $result['error_code'] : 0;
46
47     if (isset($result['error_description'])) {
48       // OAuth 2.0 Draft 10 style
49       $msg = $result['error_description'];
50     } else if (isset($result['error']) && is_array($result['error'])) {
51       // OAuth 2.0 Draft 00 style
52       $msg = $result['error']['message'];
53     } else if (isset($result['error_msg'])) {
54       // Rest server style
55       $msg = $result['error_msg'];
56     } else {
57       $msg = 'Unknown Error. Check getResult()';
58     }
59
60     parent::__construct($msg, $code);
61   }
62
63   /**
64    * Return the associated result object returned by the API server.
65    *
66    * @return array The result from the API server
67    */
68   public function getResult() {
69     return $this->result;
70   }
71
72   /**
73    * Returns the associated type for the error. This will default to
74    * 'Exception' when a type is not available.
75    *
76    * @return string
77    */
78   public function getType() {
79     if (isset($this->result['error'])) {
80       $error = $this->result['error'];
81       if (is_string($error)) {
82         // OAuth 2.0 Draft 10 style
83         return $error;
84       } else if (is_array($error)) {
85         // OAuth 2.0 Draft 00 style
86         if (isset($error['type'])) {
87           return $error['type'];
88         }
89       }
90     }
91
92     return 'Exception';
93   }
94
95   /**
96    * To make debugging easier.
97    *
98    * @return string The string representation of the error
99    */
100   public function __toString() {
101     $str = $this->getType() . ': ';
102     if ($this->code != 0) {
103       $str .= $this->code . ': ';
104     }
105     return $str . $this->message;
106   }
107 }
108
109 /**
110  * Provides access to the Facebook Platform.  This class provides
111  * a majority of the functionality needed, but the class is abstract
112  * because it is designed to be sub-classed.  The subclass must
113  * implement the four abstract methods listed at the bottom of
114  * the file.
115  *
116  * @author Naitik Shah <naitik@facebook.com>
117  */
118 abstract class BaseFacebook
119 {
120   /**
121    * Version.
122    */
123   const VERSION = '3.1.1';
124
125   /**
126    * Default options for curl.
127    */
128   public static $CURL_OPTS = array(
129     CURLOPT_CONNECTTIMEOUT => 10,
130     CURLOPT_RETURNTRANSFER => true,
131     CURLOPT_TIMEOUT        => 60,
132     CURLOPT_USERAGENT      => 'facebook-php-3.1',
133   );
134
135   /**
136    * List of query parameters that get automatically dropped when rebuilding
137    * the current URL.
138    */
139   protected static $DROP_QUERY_PARAMS = array(
140     'code',
141     'state',
142     'signed_request',
143   );
144
145   /**
146    * Maps aliases to Facebook domains.
147    */
148   public static $DOMAIN_MAP = array(
149     'api'       => 'https://api.facebook.com/',
150     'api_video' => 'https://api-video.facebook.com/',
151     'api_read'  => 'https://api-read.facebook.com/',
152     'graph'     => 'https://graph.facebook.com/',
153     'www'       => 'https://www.facebook.com/',
154   );
155
156   /**
157    * The Application ID.
158    *
159    * @var string
160    */
161   protected $appId;
162
163   /**
164    * The Application API Secret.
165    *
166    * @var string
167    */
168   protected $apiSecret;
169
170   /**
171    * The ID of the Facebook user, or 0 if the user is logged out.
172    *
173    * @var integer
174    */
175   protected $user;
176
177   /**
178    * The data from the signed_request token.
179    */
180   protected $signedRequest;
181
182   /**
183    * A CSRF state variable to assist in the defense against CSRF attacks.
184    */
185   protected $state;
186
187   /**
188    * The OAuth access token received in exchange for a valid authorization
189    * code.  null means the access token has yet to be determined.
190    *
191    * @var string
192    */
193   protected $accessToken = null;
194
195   /**
196    * Indicates if the CURL based @ syntax for file uploads is enabled.
197    *
198    * @var boolean
199    */
200   protected $fileUploadSupport = false;
201
202   /**
203    * Initialize a Facebook Application.
204    *
205    * The configuration:
206    * - appId: the application ID
207    * - secret: the application secret
208    * - fileUpload: (optional) boolean indicating if file uploads are enabled
209    *
210    * @param array $config The application configuration
211    */
212   public function __construct($config) {
213     $this->setAppId($config['appId']);
214     $this->setApiSecret($config['secret']);
215     if (isset($config['fileUpload'])) {
216       $this->setFileUploadSupport($config['fileUpload']);
217     }
218
219     $state = $this->getPersistentData('state');
220     if (!empty($state)) {
221       $this->state = $this->getPersistentData('state');
222     }
223   }
224
225   /**
226    * Set the Application ID.
227    *
228    * @param string $appId The Application ID
229    * @return BaseFacebook
230    */
231   public function setAppId($appId) {
232     $this->appId = $appId;
233     return $this;
234   }
235
236   /**
237    * Get the Application ID.
238    *
239    * @return string the Application ID
240    */
241   public function getAppId() {
242     return $this->appId;
243   }
244
245   /**
246    * Set the API Secret.
247    *
248    * @param string $apiSecret The API Secret
249    * @return BaseFacebook
250    */
251   public function setApiSecret($apiSecret) {
252     $this->apiSecret = $apiSecret;
253     return $this;
254   }
255
256   /**
257    * Get the API Secret.
258    *
259    * @return string the API Secret
260    */
261   public function getApiSecret() {
262     return $this->apiSecret;
263   }
264
265   /**
266    * Set the file upload support status.
267    *
268    * @param boolean $fileUploadSupport The file upload support status.
269    * @return BaseFacebook
270    */
271   public function setFileUploadSupport($fileUploadSupport) {
272     $this->fileUploadSupport = $fileUploadSupport;
273     return $this;
274   }
275
276   /**
277    * Get the file upload support status.
278    *
279    * @return boolean true if and only if the server supports file upload.
280    */
281   public function useFileUploadSupport() {
282     return $this->fileUploadSupport;
283   }
284
285   /**
286    * Sets the access token for api calls.  Use this if you get
287    * your access token by other means and just want the SDK
288    * to use it.
289    *
290    * @param string $access_token an access token.
291    * @return BaseFacebook
292    */
293   public function setAccessToken($access_token) {
294     $this->accessToken = $access_token;
295     return $this;
296   }
297
298   /**
299    * Determines the access token that should be used for API calls.
300    * The first time this is called, $this->accessToken is set equal
301    * to either a valid user access token, or it's set to the application
302    * access token if a valid user access token wasn't available.  Subsequent
303    * calls return whatever the first call returned.
304    *
305    * @return string The access token
306    */
307   public function getAccessToken() {
308     if ($this->accessToken !== null) {
309       // we've done this already and cached it.  Just return.
310       return $this->accessToken;
311     }
312
313     // first establish access token to be the application
314     // access token, in case we navigate to the /oauth/access_token
315     // endpoint, where SOME access token is required.
316     $this->setAccessToken($this->getApplicationAccessToken());
317     if ($user_access_token = $this->getUserAccessToken()) {
318       $this->setAccessToken($user_access_token);
319     }
320
321     return $this->accessToken;
322   }
323
324   /**
325    * Determines and returns the user access token, first using
326    * the signed request if present, and then falling back on
327    * the authorization code if present.  The intent is to
328    * return a valid user access token, or false if one is determined
329    * to not be available.
330    *
331    * @return string A valid user access token, or false if one
332    *                could not be determined.
333    */
334   protected function getUserAccessToken() {
335     // first, consider a signed request if it's supplied.
336     // if there is a signed request, then it alone determines
337     // the access token.
338     $signed_request = $this->getSignedRequest();
339     if ($signed_request) {
340       // apps.facebook.com hands the access_token in the signed_request
341       if (array_key_exists('oauth_token', $signed_request)) {
342         $access_token = $signed_request['oauth_token'];
343         $this->setPersistentData('access_token', $access_token);
344         return $access_token;
345       }
346
347       // the JS SDK puts a code in with the redirect_uri of ''
348       if (array_key_exists('code', $signed_request)) {
349         $code = $signed_request['code'];
350         $access_token = $this->getAccessTokenFromCode($code, '');
351         if ($access_token) {
352           $this->setPersistentData('code', $code);
353           $this->setPersistentData('access_token', $access_token);
354           return $access_token;
355         }
356       }
357
358       // signed request states there's no access token, so anything
359       // stored should be cleared.
360       $this->clearAllPersistentData();
361       return false; // respect the signed request's data, even
362                     // if there's an authorization code or something else
363     }
364
365     $code = $this->getCode();
366     if ($code && $code != $this->getPersistentData('code')) {
367       $access_token = $this->getAccessTokenFromCode($code);
368       if ($access_token) {
369         $this->setPersistentData('code', $code);
370         $this->setPersistentData('access_token', $access_token);
371         return $access_token;
372       }
373
374       // code was bogus, so everything based on it should be invalidated.
375       $this->clearAllPersistentData();
376       return false;
377     }
378
379     // as a fallback, just return whatever is in the persistent
380     // store, knowing nothing explicit (signed request, authorization
381     // code, etc.) was present to shadow it (or we saw a code in $_REQUEST,
382     // but it's the same as what's in the persistent store)
383     return $this->getPersistentData('access_token');
384   }
385
386   /**
387    * Retrieve the signed request, either from a request parameter or,
388    * if not present, from a cookie.
389    *
390    * @return string the signed request, if available, or null otherwise.
391    */
392   public function getSignedRequest() {
393     if (!$this->signedRequest) {
394       if (isset($_REQUEST['signed_request'])) {
395         $this->signedRequest = $this->parseSignedRequest(
396           $_REQUEST['signed_request']);
397       } else if (isset($_COOKIE[$this->getSignedRequestCookieName()])) {
398         $this->signedRequest = $this->parseSignedRequest(
399           $_COOKIE[$this->getSignedRequestCookieName()]);
400       }
401     }
402     return $this->signedRequest;
403   }
404
405   /**
406    * Get the UID of the connected user, or 0
407    * if the Facebook user is not connected.
408    *
409    * @return string the UID if available.
410    */
411   public function getUser() {
412     if ($this->user !== null) {
413       // we've already determined this and cached the value.
414       return $this->user;
415     }
416
417     return $this->user = $this->getUserFromAvailableData();
418   }
419
420   /**
421    * Determines the connected user by first examining any signed
422    * requests, then considering an authorization code, and then
423    * falling back to any persistent store storing the user.
424    *
425    * @return integer The id of the connected Facebook user,
426    *                 or 0 if no such user exists.
427    */
428   protected function getUserFromAvailableData() {
429     // if a signed request is supplied, then it solely determines
430     // who the user is.
431     $signed_request = $this->getSignedRequest();
432     if ($signed_request) {
433       if (array_key_exists('user_id', $signed_request)) {
434         $user = $signed_request['user_id'];
435         $this->setPersistentData('user_id', $signed_request['user_id']);
436         return $user;
437       }
438
439       // if the signed request didn't present a user id, then invalidate
440       // all entries in any persistent store.
441       $this->clearAllPersistentData();
442       return 0;
443     }
444
445     $user = $this->getPersistentData('user_id', $default = 0);
446     $persisted_access_token = $this->getPersistentData('access_token');
447
448     // use access_token to fetch user id if we have a user access_token, or if
449     // the cached access token has changed.
450     $access_token = $this->getAccessToken();
451     if ($access_token &&
452         $access_token != $this->getApplicationAccessToken() &&
453         !($user && $persisted_access_token == $access_token)) {
454       $user = $this->getUserFromAccessToken();
455       if ($user) {
456         $this->setPersistentData('user_id', $user);
457       } else {
458         $this->clearAllPersistentData();
459       }
460     }
461
462     return $user;
463   }
464
465   /**
466    * Get a Login URL for use with redirects. By default, full page redirect is
467    * assumed. If you are using the generated URL with a window.open() call in
468    * JavaScript, you can pass in display=popup as part of the $params.
469    *
470    * The parameters:
471    * - redirect_uri: the url to go to after a successful login
472    * - scope: comma separated list of requested extended perms
473    *
474    * @param array $params Provide custom parameters
475    * @return string The URL for the login flow
476    */
477   public function getLoginUrl($params=array()) {
478     $this->establishCSRFTokenState();
479     $currentUrl = $this->getCurrentUrl();
480
481     // if 'scope' is passed as an array, convert to comma separated list
482     $scopeParams = isset($params['scope']) ? $params['scope'] : null;
483     if ($scopeParams && is_array($scopeParams)) {
484       $params['scope'] = implode(',', $scopeParams);
485     }
486
487     return $this->getUrl(
488       'www',
489       'dialog/oauth',
490       array_merge(array(
491                     'client_id' => $this->getAppId(),
492                     'redirect_uri' => $currentUrl, // possibly overwritten
493                     'state' => $this->state),
494                   $params));
495   }
496
497   /**
498    * Get a Logout URL suitable for use with redirects.
499    *
500    * The parameters:
501    * - next: the url to go to after a successful logout
502    *
503    * @param array $params Provide custom parameters
504    * @return string The URL for the logout flow
505    */
506   public function getLogoutUrl($params=array()) {
507     return $this->getUrl(
508       'www',
509       'logout.php',
510       array_merge(array(
511         'next' => $this->getCurrentUrl(),
512         'access_token' => $this->getAccessToken(),
513       ), $params)
514     );
515   }
516
517   /**
518    * Get a login status URL to fetch the status from Facebook.
519    *
520    * The parameters:
521    * - ok_session: the URL to go to if a session is found
522    * - no_session: the URL to go to if the user is not connected
523    * - no_user: the URL to go to if the user is not signed into facebook
524    *
525    * @param array $params Provide custom parameters
526    * @return string The URL for the logout flow
527    */
528   public function getLoginStatusUrl($params=array()) {
529     return $this->getUrl(
530       'www',
531       'extern/login_status.php',
532       array_merge(array(
533         'api_key' => $this->getAppId(),
534         'no_session' => $this->getCurrentUrl(),
535         'no_user' => $this->getCurrentUrl(),
536         'ok_session' => $this->getCurrentUrl(),
537         'session_version' => 3,
538       ), $params)
539     );
540   }
541
542   /**
543    * Make an API call.
544    *
545    * @return mixed The decoded response
546    */
547   public function api(/* polymorphic */) {
548     $args = func_get_args();
549     if (is_array($args[0])) {
550       return $this->_restserver($args[0]);
551     } else {
552       return call_user_func_array(array($this, '_graph'), $args);
553     }
554   }
555
556   /**
557    * Constructs and returns the name of the cookie that
558    * potentially houses the signed request for the app user.
559    * The cookie is not set by the BaseFacebook class, but
560    * it may be set by the JavaScript SDK.
561    *
562    * @return string the name of the cookie that would house
563    *         the signed request value.
564    */
565   protected function getSignedRequestCookieName() {
566     return 'fbsr_'.$this->getAppId();
567   }
568
569   /**
570    * Get the authorization code from the query parameters, if it exists,
571    * and otherwise return false to signal no authorization code was
572    * discoverable.
573    *
574    * @return mixed The authorization code, or false if the authorization
575    *               code could not be determined.
576    */
577   protected function getCode() {
578     if (isset($_REQUEST['code'])) {
579       if ($this->state !== null &&
580           isset($_REQUEST['state']) &&
581           $this->state === $_REQUEST['state']) {
582
583         // CSRF state has done its job, so clear it
584         $this->state = null;
585         $this->clearPersistentData('state');
586         return $_REQUEST['code'];
587       } else {
588         self::errorLog('CSRF state token does not match one provided.');
589         return false;
590       }
591     }
592
593     return false;
594   }
595
596   /**
597    * Retrieves the UID with the understanding that
598    * $this->accessToken has already been set and is
599    * seemingly legitimate.  It relies on Facebook's Graph API
600    * to retrieve user information and then extract
601    * the user ID.
602    *
603    * @return integer Returns the UID of the Facebook user, or 0
604    *                 if the Facebook user could not be determined.
605    */
606   protected function getUserFromAccessToken() {
607     try {
608       $user_info = $this->api('/me');
609       return $user_info['id'];
610     } catch (FacebookApiException $e) {
611       return 0;
612     }
613   }
614
615   /**
616    * Returns the access token that should be used for logged out
617    * users when no authorization code is available.
618    *
619    * @return string The application access token, useful for gathering
620    *                public information about users and applications.
621    */
622   protected function getApplicationAccessToken() {
623     return $this->appId.'|'.$this->apiSecret;
624   }
625
626   /**
627    * Lays down a CSRF state token for this process.
628    *
629    * @return void
630    */
631   protected function establishCSRFTokenState() {
632     if ($this->state === null) {
633       $this->state = md5(uniqid(mt_rand(), true));
634       $this->setPersistentData('state', $this->state);
635     }
636   }
637
638   /**
639    * Retrieves an access token for the given authorization code
640    * (previously generated from www.facebook.com on behalf of
641    * a specific user).  The authorization code is sent to graph.facebook.com
642    * and a legitimate access token is generated provided the access token
643    * and the user for which it was generated all match, and the user is
644    * either logged in to Facebook or has granted an offline access permission.
645    *
646    * @param string $code An authorization code.
647    * @return mixed An access token exchanged for the authorization code, or
648    *               false if an access token could not be generated.
649    */
650   protected function getAccessTokenFromCode($code, $redirect_uri = null) {
651     if (empty($code)) {
652       return false;
653     }
654
655     if ($redirect_uri === null) {
656       $redirect_uri = $this->getCurrentUrl();
657     }
658
659     try {
660       // need to circumvent json_decode by calling _oauthRequest
661       // directly, since response isn't JSON format.
662       $access_token_response =
663         $this->_oauthRequest(
664           $this->getUrl('graph', '/oauth/access_token'),
665           $params = array('client_id' => $this->getAppId(),
666                           'client_secret' => $this->getApiSecret(),
667                           'redirect_uri' => $redirect_uri,
668                           'code' => $code));
669     } catch (FacebookApiException $e) {
670       // most likely that user very recently revoked authorization.
671       // In any event, we don't have an access token, so say so.
672       return false;
673     }
674
675     if (empty($access_token_response)) {
676       return false;
677     }
678
679     $response_params = array();
680     parse_str($access_token_response, $response_params);
681     if (!isset($response_params['access_token'])) {
682       return false;
683     }
684
685     return $response_params['access_token'];
686   }
687
688   /**
689    * Invoke the old restserver.php endpoint.
690    *
691    * @param array $params Method call object
692    *
693    * @return mixed The decoded response object
694    * @throws FacebookApiException
695    */
696   protected function _restserver($params) {
697     // generic application level parameters
698     $params['api_key'] = $this->getAppId();
699     $params['format'] = 'json-strings';
700
701     $result = json_decode($this->_oauthRequest(
702       $this->getApiUrl($params['method']),
703       $params
704     ), true);
705
706     // results are returned, errors are thrown
707     if (is_array($result) && isset($result['error_code'])) {
708       throw new FacebookApiException($result);
709     }
710
711     return $result;
712   }
713
714   /**
715    * Invoke the Graph API.
716    *
717    * @param string $path The path (required)
718    * @param string $method The http method (default 'GET')
719    * @param array $params The query/post data
720    *
721    * @return mixed The decoded response object
722    * @throws FacebookApiException
723    */
724   protected function _graph($path, $method = 'GET', $params = array()) {
725     if (is_array($method) && empty($params)) {
726       $params = $method;
727       $method = 'GET';
728     }
729     $params['method'] = $method; // method override as we always do a POST
730
731     $result = json_decode($this->_oauthRequest(
732       $this->getUrl('graph', $path),
733       $params
734     ), true);
735
736     // results are returned, errors are thrown
737     if (is_array($result) && isset($result['error'])) {
738       $this->throwAPIException($result);
739     }
740
741     return $result;
742   }
743
744   /**
745    * Make a OAuth Request.
746    *
747    * @param string $url The path (required)
748    * @param array $params The query/post data
749    *
750    * @return string The decoded response object
751    * @throws FacebookApiException
752    */
753   protected function _oauthRequest($url, $params) {
754     if (!isset($params['access_token'])) {
755       $params['access_token'] = $this->getAccessToken();
756     }
757
758     // json_encode all params values that are not strings
759     foreach ($params as $key => $value) {
760       if (!is_string($value)) {
761         $params[$key] = json_encode($value);
762       }
763     }
764
765     return $this->makeRequest($url, $params);
766   }
767
768   /**
769    * Makes an HTTP request. This method can be overridden by subclasses if
770    * developers want to do fancier things or use something other than curl to
771    * make the request.
772    *
773    * @param string $url The URL to make the request to
774    * @param array $params The parameters to use for the POST body
775    * @param CurlHandler $ch Initialized curl handle
776    *
777    * @return string The response text
778    */
779   protected function makeRequest($url, $params, $ch=null) {
780     if (!$ch) {
781       $ch = curl_init();
782     }
783
784     $opts = self::$CURL_OPTS;
785     if ($this->useFileUploadSupport()) {
786       $opts[CURLOPT_POSTFIELDS] = $params;
787     } else {
788       $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&');
789     }
790     $opts[CURLOPT_URL] = $url;
791
792     // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait
793     // for 2 seconds if the server does not support this header.
794     if (isset($opts[CURLOPT_HTTPHEADER])) {
795       $existing_headers = $opts[CURLOPT_HTTPHEADER];
796       $existing_headers[] = 'Expect:';
797       $opts[CURLOPT_HTTPHEADER] = $existing_headers;
798     } else {
799       $opts[CURLOPT_HTTPHEADER] = array('Expect:');
800     }
801
802     curl_setopt_array($ch, $opts);
803     $result = curl_exec($ch);
804
805     if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
806       self::errorLog('Invalid or no certificate authority found, '.
807                      'using bundled information');
808       curl_setopt($ch, CURLOPT_CAINFO,
809                   dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
810       $result = curl_exec($ch);
811     }
812
813     if ($result === false) {
814       $e = new FacebookApiException(array(
815         'error_code' => curl_errno($ch),
816         'error' => array(
817         'message' => curl_error($ch),
818         'type' => 'CurlException',
819         ),
820       ));
821       curl_close($ch);
822       throw $e;
823     }
824     curl_close($ch);
825     return $result;
826   }
827
828   /**
829    * Parses a signed_request and validates the signature.
830    *
831    * @param string $signed_request A signed token
832    * @return array The payload inside it or null if the sig is wrong
833    */
834   protected function parseSignedRequest($signed_request) {
835     list($encoded_sig, $payload) = explode('.', $signed_request, 2);
836
837     // decode the data
838     $sig = self::base64UrlDecode($encoded_sig);
839     $data = json_decode(self::base64UrlDecode($payload), true);
840
841     if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
842       self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
843       return null;
844     }
845
846     // check sig
847     $expected_sig = hash_hmac('sha256', $payload,
848                               $this->getApiSecret(), $raw = true);
849     if ($sig !== $expected_sig) {
850       self::errorLog('Bad Signed JSON signature!');
851       return null;
852     }
853
854     return $data;
855   }
856
857   /**
858    * Build the URL for api given parameters.
859    *
860    * @param $method String the method name.
861    * @return string The URL for the given parameters
862    */
863   protected function getApiUrl($method) {
864     static $READ_ONLY_CALLS =
865       array('admin.getallocation' => 1,
866             'admin.getappproperties' => 1,
867             'admin.getbannedusers' => 1,
868             'admin.getlivestreamvialink' => 1,
869             'admin.getmetrics' => 1,
870             'admin.getrestrictioninfo' => 1,
871             'application.getpublicinfo' => 1,
872             'auth.getapppublickey' => 1,
873             'auth.getsession' => 1,
874             'auth.getsignedpublicsessiondata' => 1,
875             'comments.get' => 1,
876             'connect.getunconnectedfriendscount' => 1,
877             'dashboard.getactivity' => 1,
878             'dashboard.getcount' => 1,
879             'dashboard.getglobalnews' => 1,
880             'dashboard.getnews' => 1,
881             'dashboard.multigetcount' => 1,
882             'dashboard.multigetnews' => 1,
883             'data.getcookies' => 1,
884             'events.get' => 1,
885             'events.getmembers' => 1,
886             'fbml.getcustomtags' => 1,
887             'feed.getappfriendstories' => 1,
888             'feed.getregisteredtemplatebundlebyid' => 1,
889             'feed.getregisteredtemplatebundles' => 1,
890             'fql.multiquery' => 1,
891             'fql.query' => 1,
892             'friends.arefriends' => 1,
893             'friends.get' => 1,
894             'friends.getappusers' => 1,
895             'friends.getlists' => 1,
896             'friends.getmutualfriends' => 1,
897             'gifts.get' => 1,
898             'groups.get' => 1,
899             'groups.getmembers' => 1,
900             'intl.gettranslations' => 1,
901             'links.get' => 1,
902             'notes.get' => 1,
903             'notifications.get' => 1,
904             'pages.getinfo' => 1,
905             'pages.isadmin' => 1,
906             'pages.isappadded' => 1,
907             'pages.isfan' => 1,
908             'permissions.checkavailableapiaccess' => 1,
909             'permissions.checkgrantedapiaccess' => 1,
910             'photos.get' => 1,
911             'photos.getalbums' => 1,
912             'photos.gettags' => 1,
913             'profile.getinfo' => 1,
914             'profile.getinfooptions' => 1,
915             'stream.get' => 1,
916             'stream.getcomments' => 1,
917             'stream.getfilters' => 1,
918             'users.getinfo' => 1,
919             'users.getloggedinuser' => 1,
920             'users.getstandardinfo' => 1,
921             'users.hasapppermission' => 1,
922             'users.isappuser' => 1,
923             'users.isverified' => 1,
924             'video.getuploadlimits' => 1);
925     $name = 'api';
926     if (isset($READ_ONLY_CALLS[strtolower($method)])) {
927       $name = 'api_read';
928     } else if (strtolower($method) == 'video.upload') {
929       $name = 'api_video';
930     }
931     return self::getUrl($name, 'restserver.php');
932   }
933
934   /**
935    * Build the URL for given domain alias, path and parameters.
936    *
937    * @param $name string The name of the domain
938    * @param $path string Optional path (without a leading slash)
939    * @param $params array Optional query parameters
940    *
941    * @return string The URL for the given parameters
942    */
943   protected function getUrl($name, $path='', $params=array()) {
944     $url = self::$DOMAIN_MAP[$name];
945     if ($path) {
946       if ($path[0] === '/') {
947         $path = substr($path, 1);
948       }
949       $url .= $path;
950     }
951     if ($params) {
952       $url .= '?' . http_build_query($params, null, '&');
953     }
954
955     return $url;
956   }
957
958   /**
959    * Returns the Current URL, stripping it of known FB parameters that should
960    * not persist.
961    *
962    * @return string The current URL
963    */
964   protected function getCurrentUrl() {
965     if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1)
966       || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'
967     ) {
968       $protocol = 'https://';
969     }
970     else {
971       $protocol = 'http://';
972     }
973     $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
974     $parts = parse_url($currentUrl);
975
976     $query = '';
977     if (!empty($parts['query'])) {
978       // drop known fb params
979       $params = explode('&', $parts['query']);
980       $retained_params = array();
981       foreach ($params as $param) {
982         if ($this->shouldRetainParam($param)) {
983           $retained_params[] = $param;
984         }
985       }
986
987       if (!empty($retained_params)) {
988         $query = '?'.implode($retained_params, '&');
989       }
990     }
991
992     // use port if non default
993     $port =
994       isset($parts['port']) &&
995       (($protocol === 'http://' && $parts['port'] !== 80) ||
996        ($protocol === 'https://' && $parts['port'] !== 443))
997       ? ':' . $parts['port'] : '';
998
999     // rebuild
1000     return $protocol . $parts['host'] . $port . $parts['path'] . $query;
1001   }
1002
1003   /**
1004    * Returns true if and only if the key or key/value pair should
1005    * be retained as part of the query string.  This amounts to
1006    * a brute-force search of the very small list of Facebook-specific
1007    * params that should be stripped out.
1008    *
1009    * @param string $param A key or key/value pair within a URL's query (e.g.
1010    *                     'foo=a', 'foo=', or 'foo'.
1011    *
1012    * @return boolean
1013    */
1014   protected function shouldRetainParam($param) {
1015     foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) {
1016       if (strpos($param, $drop_query_param.'=') === 0) {
1017         return false;
1018       }
1019     }
1020
1021     return true;
1022   }
1023
1024   /**
1025    * Analyzes the supplied result to see if it was thrown
1026    * because the access token is no longer valid.  If that is
1027    * the case, then the persistent store is cleared.
1028    *
1029    * @param $result array A record storing the error message returned
1030    *                      by a failed API call.
1031    */
1032   protected function throwAPIException($result) {
1033     $e = new FacebookApiException($result);
1034     switch ($e->getType()) {
1035       // OAuth 2.0 Draft 00 style
1036       case 'OAuthException':
1037         // OAuth 2.0 Draft 10 style
1038       case 'invalid_token':
1039         $message = $e->getMessage();
1040       if ((strpos($message, 'Error validating access token') !== false) ||
1041           (strpos($message, 'Invalid OAuth access token') !== false)) {
1042         $this->setAccessToken(null);
1043         $this->user = 0;
1044         $this->clearAllPersistentData();
1045       }
1046     }
1047
1048     throw $e;
1049   }
1050
1051
1052   /**
1053    * Prints to the error log if you aren't in command line mode.
1054    *
1055    * @param string $msg Log message
1056    */
1057   protected static function errorLog($msg) {
1058     // disable error log if we are running in a CLI environment
1059     // @codeCoverageIgnoreStart
1060     if (php_sapi_name() != 'cli') {
1061       error_log($msg);
1062     }
1063     // uncomment this if you want to see the errors on the page
1064     // print 'error_log: '.$msg."\n";
1065     // @codeCoverageIgnoreEnd
1066   }
1067
1068   /**
1069    * Base64 encoding that doesn't need to be urlencode()ed.
1070    * Exactly the same as base64_encode except it uses
1071    *   - instead of +
1072    *   _ instead of /
1073    *
1074    * @param string $input base64UrlEncoded string
1075    * @return string
1076    */
1077   protected static function base64UrlDecode($input) {
1078     return base64_decode(strtr($input, '-_', '+/'));
1079   }
1080
1081   /**
1082    * Each of the following four methods should be overridden in
1083    * a concrete subclass, as they are in the provided Facebook class.
1084    * The Facebook class uses PHP sessions to provide a primitive
1085    * persistent store, but another subclass--one that you implement--
1086    * might use a database, memcache, or an in-memory cache.
1087    *
1088    * @see Facebook
1089    */
1090
1091   /**
1092    * Stores the given ($key, $value) pair, so that future calls to
1093    * getPersistentData($key) return $value. This call may be in another request.
1094    *
1095    * @param string $key
1096    * @param array $value
1097    *
1098    * @return void
1099    */
1100   abstract protected function setPersistentData($key, $value);
1101
1102   /**
1103    * Get the data for $key, persisted by BaseFacebook::setPersistentData()
1104    *
1105    * @param string $key The key of the data to retrieve
1106    * @param boolean $default The default value to return if $key is not found
1107    *
1108    * @return mixed
1109    */
1110   abstract protected function getPersistentData($key, $default = false);
1111
1112   /**
1113    * Clear the data with $key from the persistent storage
1114    *
1115    * @param string $key
1116    * @return void
1117    */
1118   abstract protected function clearPersistentData($key);
1119
1120   /**
1121    * Clear all data from the persistent storage
1122    *
1123    * @return void
1124    */
1125   abstract protected function clearAllPersistentData();
1126 }