]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookSSO/extlib/facebook.php
Merge branch '0.9.x' into facebook-upgrade
[quix0rs-gnu-social.git] / plugins / FacebookSSO / extlib / facebook.php
1 <?php
2
3 if (!function_exists('curl_init')) {
4   throw new Exception('Facebook needs the CURL PHP extension.');
5 }
6 if (!function_exists('json_decode')) {
7   throw new Exception('Facebook needs the JSON PHP extension.');
8 }
9
10 /**
11  * Thrown when an API call returns an exception.
12  *
13  * @author Naitik Shah <naitik@facebook.com>
14  */
15 class FacebookApiException extends Exception
16 {
17   /**
18    * The result from the API server that represents the exception information.
19    */
20   protected $result;
21
22   /**
23    * Make a new API Exception with the given result.
24    *
25    * @param Array $result the result from the API server
26    */
27   public function __construct($result) {
28     $this->result = $result;
29
30     $code = isset($result['error_code']) ? $result['error_code'] : 0;
31
32     if (isset($result['error_description'])) {
33       // OAuth 2.0 Draft 10 style
34       $msg = $result['error_description'];
35     } else if (isset($result['error']) && is_array($result['error'])) {
36       // OAuth 2.0 Draft 00 style
37       $msg = $result['error']['message'];
38     } else if (isset($result['error_msg'])) {
39       // Rest server style
40       $msg = $result['error_msg'];
41     } else {
42       $msg = 'Unknown Error. Check getResult()';
43     }
44
45     parent::__construct($msg, $code);
46   }
47
48   /**
49    * Return the associated result object returned by the API server.
50    *
51    * @returns Array the result from the API server
52    */
53   public function getResult() {
54     return $this->result;
55   }
56
57   /**
58    * Returns the associated type for the error. This will default to
59    * 'Exception' when a type is not available.
60    *
61    * @return String
62    */
63   public function getType() {
64     if (isset($this->result['error'])) {
65       $error = $this->result['error'];
66       if (is_string($error)) {
67         // OAuth 2.0 Draft 10 style
68         return $error;
69       } else if (is_array($error)) {
70         // OAuth 2.0 Draft 00 style
71         if (isset($error['type'])) {
72           return $error['type'];
73         }
74       }
75     }
76     return 'Exception';
77   }
78
79   /**
80    * To make debugging easier.
81    *
82    * @returns String the string representation of the error
83    */
84   public function __toString() {
85     $str = $this->getType() . ': ';
86     if ($this->code != 0) {
87       $str .= $this->code . ': ';
88     }
89     return $str . $this->message;
90   }
91 }
92
93 /**
94  * Provides access to the Facebook Platform.
95  *
96  * @author Naitik Shah <naitik@facebook.com>
97  */
98 class Facebook
99 {
100   /**
101    * Version.
102    */
103   const VERSION = '2.1.2';
104
105   /**
106    * Default options for curl.
107    */
108   public static $CURL_OPTS = array(
109     CURLOPT_CONNECTTIMEOUT => 10,
110     CURLOPT_RETURNTRANSFER => true,
111     CURLOPT_TIMEOUT        => 60,
112     CURLOPT_USERAGENT      => 'facebook-php-2.0',
113   );
114
115   /**
116    * List of query parameters that get automatically dropped when rebuilding
117    * the current URL.
118    */
119   protected static $DROP_QUERY_PARAMS = array(
120     'session',
121     'signed_request',
122   );
123
124   /**
125    * Maps aliases to Facebook domains.
126    */
127   public static $DOMAIN_MAP = array(
128     'api'      => 'https://api.facebook.com/',
129     'api_read' => 'https://api-read.facebook.com/',
130     'graph'    => 'https://graph.facebook.com/',
131     'www'      => 'https://www.facebook.com/',
132   );
133
134   /**
135    * The Application ID.
136    */
137   protected $appId;
138
139   /**
140    * The Application API Secret.
141    */
142   protected $apiSecret;
143
144   /**
145    * The active user session, if one is available.
146    */
147   protected $session;
148
149   /**
150    * The data from the signed_request token.
151    */
152   protected $signedRequest;
153
154   /**
155    * Indicates that we already loaded the session as best as we could.
156    */
157   protected $sessionLoaded = false;
158
159   /**
160    * Indicates if Cookie support should be enabled.
161    */
162   protected $cookieSupport = false;
163
164   /**
165    * Base domain for the Cookie.
166    */
167   protected $baseDomain = '';
168
169   /**
170    * Indicates if the CURL based @ syntax for file uploads is enabled.
171    */
172   protected $fileUploadSupport = false;
173
174   /**
175    * Initialize a Facebook Application.
176    *
177    * The configuration:
178    * - appId: the application ID
179    * - secret: the application secret
180    * - cookie: (optional) boolean true to enable cookie support
181    * - domain: (optional) domain for the cookie
182    * - fileUpload: (optional) boolean indicating if file uploads are enabled
183    *
184    * @param Array $config the application configuration
185    */
186   public function __construct($config) {
187     $this->setAppId($config['appId']);
188     $this->setApiSecret($config['secret']);
189     if (isset($config['cookie'])) {
190       $this->setCookieSupport($config['cookie']);
191     }
192     if (isset($config['domain'])) {
193       $this->setBaseDomain($config['domain']);
194     }
195     if (isset($config['fileUpload'])) {
196       $this->setFileUploadSupport($config['fileUpload']);
197     }
198   }
199
200   /**
201    * Set the Application ID.
202    *
203    * @param String $appId the Application ID
204    */
205   public function setAppId($appId) {
206     $this->appId = $appId;
207     return $this;
208   }
209
210   /**
211    * Get the Application ID.
212    *
213    * @return String the Application ID
214    */
215   public function getAppId() {
216     return $this->appId;
217   }
218
219   /**
220    * Set the API Secret.
221    *
222    * @param String $appId the API Secret
223    */
224   public function setApiSecret($apiSecret) {
225     $this->apiSecret = $apiSecret;
226     return $this;
227   }
228
229   /**
230    * Get the API Secret.
231    *
232    * @return String the API Secret
233    */
234   public function getApiSecret() {
235     return $this->apiSecret;
236   }
237
238   /**
239    * Set the Cookie Support status.
240    *
241    * @param Boolean $cookieSupport the Cookie Support status
242    */
243   public function setCookieSupport($cookieSupport) {
244     $this->cookieSupport = $cookieSupport;
245     return $this;
246   }
247
248   /**
249    * Get the Cookie Support status.
250    *
251    * @return Boolean the Cookie Support status
252    */
253   public function useCookieSupport() {
254     return $this->cookieSupport;
255   }
256
257   /**
258    * Set the base domain for the Cookie.
259    *
260    * @param String $domain the base domain
261    */
262   public function setBaseDomain($domain) {
263     $this->baseDomain = $domain;
264     return $this;
265   }
266
267   /**
268    * Get the base domain for the Cookie.
269    *
270    * @return String the base domain
271    */
272   public function getBaseDomain() {
273     return $this->baseDomain;
274   }
275
276   /**
277    * Set the file upload support status.
278    *
279    * @param String $domain the base domain
280    */
281   public function setFileUploadSupport($fileUploadSupport) {
282     $this->fileUploadSupport = $fileUploadSupport;
283     return $this;
284   }
285
286   /**
287    * Get the file upload support status.
288    *
289    * @return String the base domain
290    */
291   public function useFileUploadSupport() {
292     return $this->fileUploadSupport;
293   }
294
295   /**
296    * Get the data from a signed_request token
297    *
298    * @return String the base domain
299    */
300   public function getSignedRequest() {
301     if (!$this->signedRequest) {
302       if (isset($_REQUEST['signed_request'])) {
303         $this->signedRequest = $this->parseSignedRequest(
304           $_REQUEST['signed_request']);
305       }
306     }
307     return $this->signedRequest;
308   }
309
310   /**
311    * Set the Session.
312    *
313    * @param Array $session the session
314    * @param Boolean $write_cookie indicate if a cookie should be written. this
315    * value is ignored if cookie support has been disabled.
316    */
317   public function setSession($session=null, $write_cookie=true) {
318     $session = $this->validateSessionObject($session);
319     $this->sessionLoaded = true;
320     $this->session = $session;
321     if ($write_cookie) {
322       $this->setCookieFromSession($session);
323     }
324     return $this;
325   }
326
327   /**
328    * Get the session object. This will automatically look for a signed session
329    * sent via the signed_request, Cookie or Query Parameters if needed.
330    *
331    * @return Array the session
332    */
333   public function getSession() {
334     if (!$this->sessionLoaded) {
335       $session = null;
336       $write_cookie = true;
337
338       // try loading session from signed_request in $_REQUEST
339       $signedRequest = $this->getSignedRequest();
340       if ($signedRequest) {
341         // sig is good, use the signedRequest
342         $session = $this->createSessionFromSignedRequest($signedRequest);
343       }
344
345       // try loading session from $_REQUEST
346       if (!$session && isset($_REQUEST['session'])) {
347         $session = json_decode(
348           get_magic_quotes_gpc()
349             ? stripslashes($_REQUEST['session'])
350             : $_REQUEST['session'],
351           true
352         );
353         $session = $this->validateSessionObject($session);
354       }
355
356       // try loading session from cookie if necessary
357       if (!$session && $this->useCookieSupport()) {
358         $cookieName = $this->getSessionCookieName();
359         if (isset($_COOKIE[$cookieName])) {
360           $session = array();
361           parse_str(trim(
362             get_magic_quotes_gpc()
363               ? stripslashes($_COOKIE[$cookieName])
364               : $_COOKIE[$cookieName],
365             '"'
366           ), $session);
367           $session = $this->validateSessionObject($session);
368           // write only if we need to delete a invalid session cookie
369           $write_cookie = empty($session);
370         }
371       }
372
373       $this->setSession($session, $write_cookie);
374     }
375
376     return $this->session;
377   }
378
379   /**
380    * Get the UID from the session.
381    *
382    * @return String the UID if available
383    */
384   public function getUser() {
385     $session = $this->getSession();
386     return $session ? $session['uid'] : null;
387   }
388
389   /**
390    * Gets a OAuth access token.
391    *
392    * @return String the access token
393    */
394   public function getAccessToken() {
395     $session = $this->getSession();
396     // either user session signed, or app signed
397     if ($session) {
398       return $session['access_token'];
399     } else {
400       return $this->getAppId() .'|'. $this->getApiSecret();
401     }
402   }
403
404   /**
405    * Get a Login URL for use with redirects. By default, full page redirect is
406    * assumed. If you are using the generated URL with a window.open() call in
407    * JavaScript, you can pass in display=popup as part of the $params.
408    *
409    * The parameters:
410    * - next: the url to go to after a successful login
411    * - cancel_url: the url to go to after the user cancels
412    * - req_perms: comma separated list of requested extended perms
413    * - display: can be "page" (default, full page) or "popup"
414    *
415    * @param Array $params provide custom parameters
416    * @return String the URL for the login flow
417    */
418   public function getLoginUrl($params=array()) {
419     $currentUrl = $this->getCurrentUrl();
420     return $this->getUrl(
421       'www',
422       'login.php',
423       array_merge(array(
424         'api_key'         => $this->getAppId(),
425         'cancel_url'      => $currentUrl,
426         'display'         => 'page',
427         'fbconnect'       => 1,
428         'next'            => $currentUrl,
429         'return_session'  => 1,
430         'session_version' => 3,
431         'v'               => '1.0',
432       ), $params)
433     );
434   }
435
436   /**
437    * Get a Logout URL suitable for use with redirects.
438    *
439    * The parameters:
440    * - next: the url to go to after a successful logout
441    *
442    * @param Array $params provide custom parameters
443    * @return String the URL for the logout flow
444    */
445   public function getLogoutUrl($params=array()) {
446     return $this->getUrl(
447       'www',
448       'logout.php',
449       array_merge(array(
450         'next'         => $this->getCurrentUrl(),
451         'access_token' => $this->getAccessToken(),
452       ), $params)
453     );
454   }
455
456   /**
457    * Get a login status URL to fetch the status from facebook.
458    *
459    * The parameters:
460    * - ok_session: the URL to go to if a session is found
461    * - no_session: the URL to go to if the user is not connected
462    * - no_user: the URL to go to if the user is not signed into facebook
463    *
464    * @param Array $params provide custom parameters
465    * @return String the URL for the logout flow
466    */
467   public function getLoginStatusUrl($params=array()) {
468     return $this->getUrl(
469       'www',
470       'extern/login_status.php',
471       array_merge(array(
472         'api_key'         => $this->getAppId(),
473         'no_session'      => $this->getCurrentUrl(),
474         'no_user'         => $this->getCurrentUrl(),
475         'ok_session'      => $this->getCurrentUrl(),
476         'session_version' => 3,
477       ), $params)
478     );
479   }
480
481   /**
482    * Make an API call.
483    *
484    * @param Array $params the API call parameters
485    * @return the decoded response
486    */
487   public function api(/* polymorphic */) {
488     $args = func_get_args();
489     if (is_array($args[0])) {
490       return $this->_restserver($args[0]);
491     } else {
492       return call_user_func_array(array($this, '_graph'), $args);
493     }
494   }
495
496   /**
497    * Invoke the old restserver.php endpoint.
498    *
499    * @param Array $params method call object
500    * @return the decoded response object
501    * @throws FacebookApiException
502    */
503   protected function _restserver($params) {
504     // generic application level parameters
505     $params['api_key'] = $this->getAppId();
506     $params['format'] = 'json-strings';
507
508     $result = json_decode($this->_oauthRequest(
509       $this->getApiUrl($params['method']),
510       $params
511     ), true);
512
513     // results are returned, errors are thrown
514     if (is_array($result) && isset($result['error_code'])) {
515       throw new FacebookApiException($result);
516     }
517     return $result;
518   }
519
520   /**
521    * Invoke the Graph API.
522    *
523    * @param String $path the path (required)
524    * @param String $method the http method (default 'GET')
525    * @param Array $params the query/post data
526    * @return the decoded response object
527    * @throws FacebookApiException
528    */
529   protected function _graph($path, $method='GET', $params=array()) {
530     if (is_array($method) && empty($params)) {
531       $params = $method;
532       $method = 'GET';
533     }
534     $params['method'] = $method; // method override as we always do a POST
535
536     $result = json_decode($this->_oauthRequest(
537       $this->getUrl('graph', $path),
538       $params
539     ), true);
540
541     // results are returned, errors are thrown
542     if (is_array($result) && isset($result['error'])) {
543       $e = new FacebookApiException($result);
544       switch ($e->getType()) {
545         // OAuth 2.0 Draft 00 style
546         case 'OAuthException':
547         // OAuth 2.0 Draft 10 style
548         case 'invalid_token':
549           $this->setSession(null);
550       }
551       throw $e;
552     }
553     return $result;
554   }
555
556   /**
557    * Make a OAuth Request
558    *
559    * @param String $path the path (required)
560    * @param Array $params the query/post data
561    * @return the decoded response object
562    * @throws FacebookApiException
563    */
564   protected function _oauthRequest($url, $params) {
565     if (!isset($params['access_token'])) {
566       $params['access_token'] = $this->getAccessToken();
567     }
568
569     // json_encode all params values that are not strings
570     foreach ($params as $key => $value) {
571       if (!is_string($value)) {
572         $params[$key] = json_encode($value);
573       }
574     }
575     return $this->makeRequest($url, $params);
576   }
577
578   /**
579    * Makes an HTTP request. This method can be overriden by subclasses if
580    * developers want to do fancier things or use something other than curl to
581    * make the request.
582    *
583    * @param String $url the URL to make the request to
584    * @param Array $params the parameters to use for the POST body
585    * @param CurlHandler $ch optional initialized curl handle
586    * @return String the response text
587    */
588   protected function makeRequest($url, $params, $ch=null) {
589     if (!$ch) {
590       $ch = curl_init();
591     }
592
593     $opts = self::$CURL_OPTS;
594     if ($this->useFileUploadSupport()) {
595       $opts[CURLOPT_POSTFIELDS] = $params;
596     } else {
597       $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&');
598     }
599     $opts[CURLOPT_URL] = $url;
600
601     // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait
602     // for 2 seconds if the server does not support this header.
603     if (isset($opts[CURLOPT_HTTPHEADER])) {
604       $existing_headers = $opts[CURLOPT_HTTPHEADER];
605       $existing_headers[] = 'Expect:';
606       $opts[CURLOPT_HTTPHEADER] = $existing_headers;
607     } else {
608       $opts[CURLOPT_HTTPHEADER] = array('Expect:');
609     }
610
611     curl_setopt_array($ch, $opts);
612     $result = curl_exec($ch);
613
614     if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
615       self::errorLog('Invalid or no certificate authority found, using bundled information');
616       curl_setopt($ch, CURLOPT_CAINFO,
617                   dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
618       $result = curl_exec($ch);
619     }
620
621     if ($result === false) {
622       $e = new FacebookApiException(array(
623         'error_code' => curl_errno($ch),
624         'error'      => array(
625           'message' => curl_error($ch),
626           'type'    => 'CurlException',
627         ),
628       ));
629       curl_close($ch);
630       throw $e;
631     }
632     curl_close($ch);
633     return $result;
634   }
635
636   /**
637    * The name of the Cookie that contains the session.
638    *
639    * @return String the cookie name
640    */
641   protected function getSessionCookieName() {
642     return 'fbs_' . $this->getAppId();
643   }
644
645   /**
646    * Set a JS Cookie based on the _passed in_ session. It does not use the
647    * currently stored session -- you need to explicitly pass it in.
648    *
649    * @param Array $session the session to use for setting the cookie
650    */
651   protected function setCookieFromSession($session=null) {
652     if (!$this->useCookieSupport()) {
653       return;
654     }
655
656     $cookieName = $this->getSessionCookieName();
657     $value = 'deleted';
658     $expires = time() - 3600;
659     $domain = $this->getBaseDomain();
660     if ($session) {
661       $value = '"' . http_build_query($session, null, '&') . '"';
662       if (isset($session['base_domain'])) {
663         $domain = $session['base_domain'];
664       }
665       $expires = $session['expires'];
666     }
667
668     // prepend dot if a domain is found
669     if ($domain) {
670       $domain = '.' . $domain;
671     }
672
673     // if an existing cookie is not set, we dont need to delete it
674     if ($value == 'deleted' && empty($_COOKIE[$cookieName])) {
675       return;
676     }
677
678     if (headers_sent()) {
679       self::errorLog('Could not set cookie. Headers already sent.');
680
681     // ignore for code coverage as we will never be able to setcookie in a CLI
682     // environment
683     // @codeCoverageIgnoreStart
684     } else {
685       setcookie($cookieName, $value, $expires, '/', $domain);
686     }
687     // @codeCoverageIgnoreEnd
688   }
689
690   /**
691    * Validates a session_version=3 style session object.
692    *
693    * @param Array $session the session object
694    * @return Array the session object if it validates, null otherwise
695    */
696   protected function validateSessionObject($session) {
697     // make sure some essential fields exist
698     if (is_array($session) &&
699         isset($session['uid']) &&
700         isset($session['access_token']) &&
701         isset($session['sig'])) {
702       // validate the signature
703       $session_without_sig = $session;
704       unset($session_without_sig['sig']);
705       $expected_sig = self::generateSignature(
706         $session_without_sig,
707         $this->getApiSecret()
708       );
709       if ($session['sig'] != $expected_sig) {
710         self::errorLog('Got invalid session signature in cookie.');
711         $session = null;
712       }
713       // check expiry time
714     } else {
715       $session = null;
716     }
717     return $session;
718   }
719
720   /**
721    * Returns something that looks like our JS session object from the
722    * signed token's data
723    *
724    * TODO: Nuke this once the login flow uses OAuth2
725    *
726    * @param Array the output of getSignedRequest
727    * @return Array Something that will work as a session
728    */
729   protected function createSessionFromSignedRequest($data) {
730     if (!isset($data['oauth_token'])) {
731       return null;
732     }
733
734     $session = array(
735       'uid'          => $data['user_id'],
736       'access_token' => $data['oauth_token'],
737       'expires'      => $data['expires'],
738     );
739
740     // put a real sig, so that validateSignature works
741     $session['sig'] = self::generateSignature(
742       $session,
743       $this->getApiSecret()
744     );
745
746     return $session;
747   }
748
749   /**
750    * Parses a signed_request and validates the signature.
751    * Then saves it in $this->signed_data
752    *
753    * @param String A signed token
754    * @param Boolean Should we remove the parts of the payload that
755    *                are used by the algorithm?
756    * @return Array the payload inside it or null if the sig is wrong
757    */
758   protected function parseSignedRequest($signed_request) {
759     list($encoded_sig, $payload) = explode('.', $signed_request, 2);
760
761     // decode the data
762     $sig = self::base64UrlDecode($encoded_sig);
763     $data = json_decode(self::base64UrlDecode($payload), true);
764
765     if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
766       self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
767       return null;
768     }
769
770     // check sig
771     $expected_sig = hash_hmac('sha256', $payload,
772                               $this->getApiSecret(), $raw = true);
773     if ($sig !== $expected_sig) {
774       self::errorLog('Bad Signed JSON signature!');
775       return null;
776     }
777
778     return $data;
779   }
780
781   /**
782    * Build the URL for api given parameters.
783    *
784    * @param $method String the method name.
785    * @return String the URL for the given parameters
786    */
787   protected function getApiUrl($method) {
788     static $READ_ONLY_CALLS =
789       array('admin.getallocation' => 1,
790             'admin.getappproperties' => 1,
791             'admin.getbannedusers' => 1,
792             'admin.getlivestreamvialink' => 1,
793             'admin.getmetrics' => 1,
794             'admin.getrestrictioninfo' => 1,
795             'application.getpublicinfo' => 1,
796             'auth.getapppublickey' => 1,
797             'auth.getsession' => 1,
798             'auth.getsignedpublicsessiondata' => 1,
799             'comments.get' => 1,
800             'connect.getunconnectedfriendscount' => 1,
801             'dashboard.getactivity' => 1,
802             'dashboard.getcount' => 1,
803             'dashboard.getglobalnews' => 1,
804             'dashboard.getnews' => 1,
805             'dashboard.multigetcount' => 1,
806             'dashboard.multigetnews' => 1,
807             'data.getcookies' => 1,
808             'events.get' => 1,
809             'events.getmembers' => 1,
810             'fbml.getcustomtags' => 1,
811             'feed.getappfriendstories' => 1,
812             'feed.getregisteredtemplatebundlebyid' => 1,
813             'feed.getregisteredtemplatebundles' => 1,
814             'fql.multiquery' => 1,
815             'fql.query' => 1,
816             'friends.arefriends' => 1,
817             'friends.get' => 1,
818             'friends.getappusers' => 1,
819             'friends.getlists' => 1,
820             'friends.getmutualfriends' => 1,
821             'gifts.get' => 1,
822             'groups.get' => 1,
823             'groups.getmembers' => 1,
824             'intl.gettranslations' => 1,
825             'links.get' => 1,
826             'notes.get' => 1,
827             'notifications.get' => 1,
828             'pages.getinfo' => 1,
829             'pages.isadmin' => 1,
830             'pages.isappadded' => 1,
831             'pages.isfan' => 1,
832             'permissions.checkavailableapiaccess' => 1,
833             'permissions.checkgrantedapiaccess' => 1,
834             'photos.get' => 1,
835             'photos.getalbums' => 1,
836             'photos.gettags' => 1,
837             'profile.getinfo' => 1,
838             'profile.getinfooptions' => 1,
839             'stream.get' => 1,
840             'stream.getcomments' => 1,
841             'stream.getfilters' => 1,
842             'users.getinfo' => 1,
843             'users.getloggedinuser' => 1,
844             'users.getstandardinfo' => 1,
845             'users.hasapppermission' => 1,
846             'users.isappuser' => 1,
847             'users.isverified' => 1,
848             'video.getuploadlimits' => 1);
849     $name = 'api';
850     if (isset($READ_ONLY_CALLS[strtolower($method)])) {
851       $name = 'api_read';
852     }
853     return self::getUrl($name, 'restserver.php');
854   }
855
856   /**
857    * Build the URL for given domain alias, path and parameters.
858    *
859    * @param $name String the name of the domain
860    * @param $path String optional path (without a leading slash)
861    * @param $params Array optional query parameters
862    * @return String the URL for the given parameters
863    */
864   protected function getUrl($name, $path='', $params=array()) {
865     $url = self::$DOMAIN_MAP[$name];
866     if ($path) {
867       if ($path[0] === '/') {
868         $path = substr($path, 1);
869       }
870       $url .= $path;
871     }
872     if ($params) {
873       $url .= '?' . http_build_query($params, null, '&');
874     }
875     return $url;
876   }
877
878   /**
879    * Returns the Current URL, stripping it of known FB parameters that should
880    * not persist.
881    *
882    * @return String the current URL
883    */
884   protected function getCurrentUrl() {
885     $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'
886       ? 'https://'
887       : 'http://';
888     $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
889     $parts = parse_url($currentUrl);
890
891     // drop known fb params
892     $query = '';
893     if (!empty($parts['query'])) {
894       $params = array();
895       parse_str($parts['query'], $params);
896       foreach(self::$DROP_QUERY_PARAMS as $key) {
897         unset($params[$key]);
898       }
899       if (!empty($params)) {
900         $query = '?' . http_build_query($params, null, '&');
901       }
902     }
903
904     // use port if non default
905     $port =
906       isset($parts['port']) &&
907       (($protocol === 'http://' && $parts['port'] !== 80) ||
908        ($protocol === 'https://' && $parts['port'] !== 443))
909       ? ':' . $parts['port'] : '';
910
911     // rebuild
912     return $protocol . $parts['host'] . $port . $parts['path'] . $query;
913   }
914
915   /**
916    * Generate a signature for the given params and secret.
917    *
918    * @param Array $params the parameters to sign
919    * @param String $secret the secret to sign with
920    * @return String the generated signature
921    */
922   protected static function generateSignature($params, $secret) {
923     // work with sorted data
924     ksort($params);
925
926     // generate the base string
927     $base_string = '';
928     foreach($params as $key => $value) {
929       $base_string .= $key . '=' . $value;
930     }
931     $base_string .= $secret;
932
933     return md5($base_string);
934   }
935
936   /**
937    * Prints to the error log if you aren't in command line mode.
938    *
939    * @param String log message
940    */
941   protected static function errorLog($msg) {
942     // disable error log if we are running in a CLI environment
943     // @codeCoverageIgnoreStart
944     if (php_sapi_name() != 'cli') {
945       error_log($msg);
946     }
947     // uncomment this if you want to see the errors on the page
948     // print 'error_log: '.$msg."\n";
949     // @codeCoverageIgnoreEnd
950   }
951
952   /**
953    * Base64 encoding that doesn't need to be urlencode()ed.
954    * Exactly the same as base64_encode except it uses
955    *   - instead of +
956    *   _ instead of /
957    *
958    * @param String base64UrlEncodeded string
959    */
960   protected static function base64UrlDecode($input) {
961     return base64_decode(strtr($input, '-_', '+/'));
962   }
963 }