]> git.mxchange.org Git - friendica.git/blob - library/facebook.php
Merge pull request #3179 from tobiasd/20170222-docquickstarten
[friendica.git] / library / 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     if ($result === false) {
614       $e = new FacebookApiException(array(
615         'error_code' => curl_errno($ch),
616         'error'      => array(
617           'message' => curl_error($ch),
618           'type'    => 'CurlException',
619         ),
620       ));
621       curl_close($ch);
622       throw $e;
623     }
624     curl_close($ch);
625     return $result;
626   }
627
628   /**
629    * The name of the Cookie that contains the session.
630    *
631    * @return String the cookie name
632    */
633   protected function getSessionCookieName() {
634     return 'fbs_' . $this->getAppId();
635   }
636
637   /**
638    * Set a JS Cookie based on the _passed in_ session. It does not use the
639    * currently stored session -- you need to explicitly pass it in.
640    *
641    * @param Array $session the session to use for setting the cookie
642    */
643   protected function setCookieFromSession($session=null) {
644     if (!$this->useCookieSupport()) {
645       return;
646     }
647
648     $cookieName = $this->getSessionCookieName();
649     $value = 'deleted';
650     $expires = time() - 3600;
651     $domain = $this->getBaseDomain();
652     if ($session) {
653       $value = '"' . http_build_query($session, null, '&') . '"';
654       if (isset($session['base_domain'])) {
655         $domain = $session['base_domain'];
656       }
657       $expires = $session['expires'];
658     }
659
660     // prepend dot if a domain is found
661     if ($domain) {
662       $domain = '.' . $domain;
663     }
664
665     // if an existing cookie is not set, we dont need to delete it
666     if ($value == 'deleted' && empty($_COOKIE[$cookieName])) {
667       return;
668     }
669
670     if (headers_sent()) {
671       self::errorLog('Could not set cookie. Headers already sent.');
672
673     // ignore for code coverage as we will never be able to setcookie in a CLI
674     // environment
675     // @codeCoverageIgnoreStart
676     } else {
677       setcookie($cookieName, $value, $expires, '/', $domain);
678     }
679     // @codeCoverageIgnoreEnd
680   }
681
682   /**
683    * Validates a session_version=3 style session object.
684    *
685    * @param Array $session the session object
686    * @return Array the session object if it validates, null otherwise
687    */
688   protected function validateSessionObject($session) {
689     // make sure some essential fields exist
690     if (is_array($session) &&
691         isset($session['uid']) &&
692         isset($session['access_token']) &&
693         isset($session['sig'])) {
694       // validate the signature
695       $session_without_sig = $session;
696       unset($session_without_sig['sig']);
697       $expected_sig = self::generateSignature(
698         $session_without_sig,
699         $this->getApiSecret()
700       );
701       if ($session['sig'] != $expected_sig) {
702         self::errorLog('Got invalid session signature in cookie.');
703         $session = null;
704       }
705       // check expiry time
706     } else {
707       $session = null;
708     }
709     return $session;
710   }
711
712   /**
713    * Returns something that looks like our JS session object from the
714    * signed token's data
715    *
716    * TODO: Nuke this once the login flow uses OAuth2
717    *
718    * @param Array the output of getSignedRequest
719    * @return Array Something that will work as a session
720    */
721   protected function createSessionFromSignedRequest($data) {
722     if (!isset($data['oauth_token'])) {
723       return null;
724     }
725
726     $session = array(
727       'uid'          => $data['user_id'],
728       'access_token' => $data['oauth_token'],
729       'expires'      => $data['expires'],
730     );
731
732     // put a real sig, so that validateSignature works
733     $session['sig'] = self::generateSignature(
734       $session,
735       $this->getApiSecret()
736     );
737
738     return $session;
739   }
740
741   /**
742    * Parses a signed_request and validates the signature.
743    * Then saves it in $this->signed_data
744    *
745    * @param String A signed token
746    * @param Boolean Should we remove the parts of the payload that
747    *                are used by the algorithm?
748    * @return Array the payload inside it or null if the sig is wrong
749    */
750   protected function parseSignedRequest($signed_request) {
751     list($encoded_sig, $payload) = explode('.', $signed_request, 2);
752
753     // decode the data
754     $sig = self::base64UrlDecode($encoded_sig);
755     $data = json_decode(self::base64UrlDecode($payload), true);
756
757     if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
758       self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
759       return null;
760     }
761
762     // check sig
763     $expected_sig = hash_hmac('sha256', $payload,
764                               $this->getApiSecret(), $raw = true);
765     if ($sig !== $expected_sig) {
766       self::errorLog('Bad Signed JSON signature!');
767       return null;
768     }
769
770     return $data;
771   }
772
773   /**
774    * Build the URL for api given parameters.
775    *
776    * @param $method String the method name.
777    * @return String the URL for the given parameters
778    */
779   protected function getApiUrl($method) {
780     static $READ_ONLY_CALLS =
781       array('admin.getallocation' => 1,
782             'admin.getappproperties' => 1,
783             'admin.getbannedusers' => 1,
784             'admin.getlivestreamvialink' => 1,
785             'admin.getmetrics' => 1,
786             'admin.getrestrictioninfo' => 1,
787             'application.getpublicinfo' => 1,
788             'auth.getapppublickey' => 1,
789             'auth.getsession' => 1,
790             'auth.getsignedpublicsessiondata' => 1,
791             'comments.get' => 1,
792             'connect.getunconnectedfriendscount' => 1,
793             'dashboard.getactivity' => 1,
794             'dashboard.getcount' => 1,
795             'dashboard.getglobalnews' => 1,
796             'dashboard.getnews' => 1,
797             'dashboard.multigetcount' => 1,
798             'dashboard.multigetnews' => 1,
799             'data.getcookies' => 1,
800             'events.get' => 1,
801             'events.getmembers' => 1,
802             'fbml.getcustomtags' => 1,
803             'feed.getappfriendstories' => 1,
804             'feed.getregisteredtemplatebundlebyid' => 1,
805             'feed.getregisteredtemplatebundles' => 1,
806             'fql.multiquery' => 1,
807             'fql.query' => 1,
808             'friends.arefriends' => 1,
809             'friends.get' => 1,
810             'friends.getappusers' => 1,
811             'friends.getlists' => 1,
812             'friends.getmutualfriends' => 1,
813             'gifts.get' => 1,
814             'groups.get' => 1,
815             'groups.getmembers' => 1,
816             'intl.gettranslations' => 1,
817             'links.get' => 1,
818             'notes.get' => 1,
819             'notifications.get' => 1,
820             'pages.getinfo' => 1,
821             'pages.isadmin' => 1,
822             'pages.isappadded' => 1,
823             'pages.isfan' => 1,
824             'permissions.checkavailableapiaccess' => 1,
825             'permissions.checkgrantedapiaccess' => 1,
826             'photos.get' => 1,
827             'photos.getalbums' => 1,
828             'photos.gettags' => 1,
829             'profile.getinfo' => 1,
830             'profile.getinfooptions' => 1,
831             'stream.get' => 1,
832             'stream.getcomments' => 1,
833             'stream.getfilters' => 1,
834             'users.getinfo' => 1,
835             'users.getloggedinuser' => 1,
836             'users.getstandardinfo' => 1,
837             'users.hasapppermission' => 1,
838             'users.isappuser' => 1,
839             'users.isverified' => 1,
840             'video.getuploadlimits' => 1);
841     $name = 'api';
842     if (isset($READ_ONLY_CALLS[strtolower($method)])) {
843       $name = 'api_read';
844     }
845     return self::getUrl($name, 'restserver.php');
846   }
847
848   /**
849    * Build the URL for given domain alias, path and parameters.
850    *
851    * @param $name String the name of the domain
852    * @param $path String optional path (without a leading slash)
853    * @param $params Array optional query parameters
854    * @return String the URL for the given parameters
855    */
856   protected function getUrl($name, $path='', $params=array()) {
857     $url = self::$DOMAIN_MAP[$name];
858     if ($path) {
859       if ($path[0] === '/') {
860         $path = substr($path, 1);
861       }
862       $url .= $path;
863     }
864     if ($params) {
865       $url .= '?' . http_build_query($params, null, '&');
866     }
867     return $url;
868   }
869
870   /**
871    * Returns the Current URL, stripping it of known FB parameters that should
872    * not persist.
873    *
874    * @return String the current URL
875    */
876   protected function getCurrentUrl() {
877     $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'
878       ? 'https://'
879       : 'http://';
880     $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
881     $parts = parse_url($currentUrl);
882
883     // drop known fb params
884     $query = '';
885     if (!empty($parts['query'])) {
886       $params = array();
887       parse_str($parts['query'], $params);
888       foreach(self::$DROP_QUERY_PARAMS as $key) {
889         unset($params[$key]);
890       }
891       if (!empty($params)) {
892         $query = '?' . http_build_query($params, null, '&');
893       }
894     }
895
896     // use port if non default
897     $port =
898       isset($parts['port']) &&
899       (($protocol === 'http://' && $parts['port'] !== 80) ||
900        ($protocol === 'https://' && $parts['port'] !== 443))
901       ? ':' . $parts['port'] : '';
902
903     // rebuild
904     return $protocol . $parts['host'] . $port . $parts['path'] . $query;
905   }
906
907   /**
908    * Generate a signature for the given params and secret.
909    *
910    * @param Array $params the parameters to sign
911    * @param String $secret the secret to sign with
912    * @return String the generated signature
913    */
914   protected static function generateSignature($params, $secret) {
915     // work with sorted data
916     ksort($params);
917
918     // generate the base string
919     $base_string = '';
920     foreach($params as $key => $value) {
921       $base_string .= $key . '=' . $value;
922     }
923     $base_string .= $secret;
924
925     return md5($base_string);
926   }
927
928   /**
929    * Prints to the error log if you aren't in command line mode.
930    *
931    * @param String log message
932    */
933   protected static function errorLog($msg) {
934     // disable error log if we are running in a CLI environment
935     // @codeCoverageIgnoreStart
936     if (php_sapi_name() != 'cli') {
937       error_log($msg);
938     }
939     // uncomment this if you want to see the errors on the page
940     // print 'error_log: '.$msg."\n";
941     // @codeCoverageIgnoreEnd
942   }
943
944   /**
945    * Base64 encoding that doesn't need to be urlencode()ed.
946    * Exactly the same as base64_encode except it uses
947    *   - instead of +
948    *   _ instead of /
949    *
950    * @param String base64UrlEncodeded string
951    */
952   protected static function base64UrlDecode($input) {
953     return base64_decode(strtr($input, '-_', '+/'));
954   }
955 }