1<?php
2
3if (!function_exists('curl_init')) {
4  throw new Exception('Facebook needs the CURL PHP extension.');
5}
6if (!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 */
15class 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 */
98class 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}
964