├── README.md ├── config └── facebook.php ├── controllers └── welcome.php ├── libraries ├── base_facebook.php ├── facebook.php └── fb_ca_chain_bundle.crt └── views └── login.php /README.md: -------------------------------------------------------------------------------- 1 | Facebook-PHP-CodeIgniter (DEPRECATED) 2 | ======================== 3 | 4 | > This repo is based on Facebook PHP SDK (v.3.2.3) Link: https://github.com/facebook/facebook-php-sdk 5 | > Facebook PHP SDK (v.3.2.3) has been deprecated. I'll update this to latest SDK as soon as possible. 6 | 7 | ## Demo 8 | > http://puneetk.com/facebook-php-codeigniter 9 | 10 | ## Usage 11 | > The [config file][CONFIG] is a good place to start. All you need to do is enter application id and secret. 12 | 13 | $config['appId'] = 'APP_ID_HERE'; 14 | $config['secret'] = 'SECRET_HERE'; 15 | 16 | 17 | ## Author 18 | **Puneet Kalra** 19 | 20 | - [Twitter](https://twitter.com/puneetkay) 21 | - [Github](https://github.com/puneetkay) 22 | - [Personal page](http://puneetk.com/) 23 | 24 | ## Contributors 25 | 26 | - [Danielgweb](https://github.com/danielgweb) 27 | 28 | 29 | ## License 30 | > Copyright (C) 2013 Puneet Kalra (hello@puneetk.com) 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 33 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 34 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 35 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 36 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 37 | of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 40 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 41 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 42 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 43 | IN THE SOFTWARE. 44 | 45 | [CONFIG]: https://github.com/puneetkay/Facebook-PHP-CodeIgniter/blob/master/config/facebook.php 46 | -------------------------------------------------------------------------------- /config/facebook.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /controllers/welcome.php: -------------------------------------------------------------------------------- 1 | load->helper('url'); 10 | } 11 | 12 | public function login(){ 13 | 14 | $this->load->library('facebook'); // Automatically picks appId and secret from config 15 | // OR 16 | // You can pass different one like this 17 | //$this->load->library('facebook', array( 18 | // 'appId' => 'APP_ID', 19 | // 'secret' => 'SECRET', 20 | // )); 21 | 22 | $user = $this->facebook->getUser(); 23 | 24 | if ($user) { 25 | try { 26 | $data['user_profile'] = $this->facebook->api('/me'); 27 | } catch (FacebookApiException $e) { 28 | $user = null; 29 | } 30 | }else { 31 | // Solves first time login issue. (Issue: #10) 32 | //$this->facebook->destroySession(); 33 | } 34 | 35 | if ($user) { 36 | 37 | $data['logout_url'] = site_url('welcome/logout'); // Logs off application 38 | // OR 39 | // Logs off FB! 40 | // $data['logout_url'] = $this->facebook->getLogoutUrl(); 41 | 42 | } else { 43 | $data['login_url'] = $this->facebook->getLoginUrl(array( 44 | 'redirect_uri' => site_url('welcome/login'), 45 | 'scope' => array("email") // permissions here 46 | )); 47 | } 48 | $this->load->view('login',$data); 49 | 50 | } 51 | 52 | public function logout(){ 53 | 54 | $this->load->library('facebook'); 55 | 56 | // Logs off session from website 57 | $this->facebook->destroySession(); 58 | // Make sure you destory website session as well. 59 | 60 | redirect('welcome/login'); 61 | } 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /libraries/base_facebook.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class FacebookApiException extends Exception 31 | { 32 | /** 33 | * The result from the API server that represents the exception information. 34 | * 35 | * @var mixed 36 | */ 37 | protected $result; 38 | 39 | /** 40 | * Make a new API Exception with the given result. 41 | * 42 | * @param array $result The result from the API server 43 | */ 44 | public function __construct($result) { 45 | $this->result = $result; 46 | 47 | $code = 0; 48 | if (isset($result['error_code']) && is_int($result['error_code'])) { 49 | $code = $result['error_code']; 50 | } 51 | 52 | if (isset($result['error_description'])) { 53 | // OAuth 2.0 Draft 10 style 54 | $msg = $result['error_description']; 55 | } else if (isset($result['error']) && is_array($result['error'])) { 56 | // OAuth 2.0 Draft 00 style 57 | $msg = $result['error']['message']; 58 | } else if (isset($result['error_msg'])) { 59 | // Rest server style 60 | $msg = $result['error_msg']; 61 | } else { 62 | $msg = 'Unknown Error. Check getResult()'; 63 | } 64 | 65 | parent::__construct($msg, $code); 66 | } 67 | 68 | /** 69 | * Return the associated result object returned by the API server. 70 | * 71 | * @return array The result from the API server 72 | */ 73 | public function getResult() { 74 | return $this->result; 75 | } 76 | 77 | /** 78 | * Returns the associated type for the error. This will default to 79 | * 'Exception' when a type is not available. 80 | * 81 | * @return string 82 | */ 83 | public function getType() { 84 | if (isset($this->result['error'])) { 85 | $error = $this->result['error']; 86 | if (is_string($error)) { 87 | // OAuth 2.0 Draft 10 style 88 | return $error; 89 | } else if (is_array($error)) { 90 | // OAuth 2.0 Draft 00 style 91 | if (isset($error['type'])) { 92 | return $error['type']; 93 | } 94 | } 95 | } 96 | 97 | return 'Exception'; 98 | } 99 | 100 | /** 101 | * To make debugging easier. 102 | * 103 | * @return string The string representation of the error 104 | */ 105 | public function __toString() { 106 | $str = $this->getType() . ': '; 107 | if ($this->code != 0) { 108 | $str .= $this->code . ': '; 109 | } 110 | return $str . $this->message; 111 | } 112 | } 113 | 114 | /** 115 | * Provides access to the Facebook Platform. This class provides 116 | * a majority of the functionality needed, but the class is abstract 117 | * because it is designed to be sub-classed. The subclass must 118 | * implement the four abstract methods listed at the bottom of 119 | * the file. 120 | * 121 | * @author Naitik Shah 122 | */ 123 | abstract class BaseFacebook 124 | { 125 | /** 126 | * Version. 127 | */ 128 | const VERSION = '3.2.3'; 129 | 130 | /** 131 | * Signed Request Algorithm. 132 | */ 133 | const SIGNED_REQUEST_ALGORITHM = 'HMAC-SHA256'; 134 | 135 | /** 136 | * Default options for curl. 137 | * 138 | * @var array 139 | */ 140 | public static $CURL_OPTS = array( 141 | CURLOPT_CONNECTTIMEOUT => 10, 142 | CURLOPT_RETURNTRANSFER => true, 143 | CURLOPT_TIMEOUT => 60, 144 | CURLOPT_USERAGENT => 'facebook-php-3.2', 145 | ); 146 | 147 | /** 148 | * List of query parameters that get automatically dropped when rebuilding 149 | * the current URL. 150 | * 151 | * @var array 152 | */ 153 | protected static $DROP_QUERY_PARAMS = array( 154 | 'code', 155 | 'state', 156 | 'signed_request', 157 | ); 158 | 159 | /** 160 | * Maps aliases to Facebook domains. 161 | * 162 | * @var array 163 | */ 164 | public static $DOMAIN_MAP = array( 165 | 'api' => 'https://api.facebook.com/', 166 | 'api_video' => 'https://api-video.facebook.com/', 167 | 'api_read' => 'https://api-read.facebook.com/', 168 | 'graph' => 'https://graph.facebook.com/', 169 | 'graph_video' => 'https://graph-video.facebook.com/', 170 | 'www' => 'https://www.facebook.com/', 171 | ); 172 | 173 | /** 174 | * The Application ID. 175 | * 176 | * @var string 177 | */ 178 | protected $appId; 179 | 180 | /** 181 | * The Application App Secret. 182 | * 183 | * @var string 184 | */ 185 | protected $appSecret; 186 | 187 | /** 188 | * The ID of the Facebook user, or 0 if the user is logged out. 189 | * 190 | * @var integer 191 | */ 192 | protected $user; 193 | 194 | /** 195 | * The data from the signed_request token. 196 | * 197 | * @var string 198 | */ 199 | protected $signedRequest; 200 | 201 | /** 202 | * A CSRF state variable to assist in the defense against CSRF attacks. 203 | * 204 | * @var string 205 | */ 206 | protected $state; 207 | 208 | /** 209 | * The OAuth access token received in exchange for a valid authorization 210 | * code. null means the access token has yet to be determined. 211 | * 212 | * @var string 213 | */ 214 | protected $accessToken = null; 215 | 216 | /** 217 | * Indicates if the CURL based @ syntax for file uploads is enabled. 218 | * 219 | * @var boolean 220 | */ 221 | protected $fileUploadSupport = false; 222 | 223 | /** 224 | * Indicates if we trust HTTP_X_FORWARDED_* headers. 225 | * 226 | * @var boolean 227 | */ 228 | protected $trustForwarded = false; 229 | 230 | /** 231 | * Indicates if signed_request is allowed in query parameters. 232 | * 233 | * @var boolean 234 | */ 235 | protected $allowSignedRequest = true; 236 | 237 | /** 238 | * Initialize a Facebook Application. 239 | * 240 | * The configuration: 241 | * - appId: the application ID 242 | * - secret: the application secret 243 | * - fileUpload: (optional) boolean indicating if file uploads are enabled 244 | * - allowSignedRequest: (optional) boolean indicating if signed_request is 245 | * allowed in query parameters or POST body. Should be 246 | * false for non-canvas apps. Defaults to true. 247 | * 248 | * @param array $config The application configuration 249 | */ 250 | public function __construct($config) { 251 | $this->setAppId($config['appId']); 252 | $this->setAppSecret($config['secret']); 253 | if (isset($config['fileUpload'])) { 254 | $this->setFileUploadSupport($config['fileUpload']); 255 | } 256 | if (isset($config['trustForwarded']) && $config['trustForwarded']) { 257 | $this->trustForwarded = true; 258 | } 259 | if (isset($config['allowSignedRequest']) 260 | && !$config['allowSignedRequest']) { 261 | $this->allowSignedRequest = false; 262 | } 263 | $state = $this->getPersistentData('state'); 264 | if (!empty($state)) { 265 | $this->state = $state; 266 | } 267 | } 268 | 269 | /** 270 | * Set the Application ID. 271 | * 272 | * @param string $appId The Application ID 273 | * 274 | * @return BaseFacebook 275 | */ 276 | public function setAppId($appId) { 277 | $this->appId = $appId; 278 | return $this; 279 | } 280 | 281 | /** 282 | * Get the Application ID. 283 | * 284 | * @return string the Application ID 285 | */ 286 | public function getAppId() { 287 | return $this->appId; 288 | } 289 | 290 | /** 291 | * Set the App Secret. 292 | * 293 | * @param string $apiSecret The App Secret 294 | * 295 | * @return BaseFacebook 296 | * @deprecated Use setAppSecret instead. 297 | * @see setAppSecret() 298 | */ 299 | public function setApiSecret($apiSecret) { 300 | $this->setAppSecret($apiSecret); 301 | return $this; 302 | } 303 | 304 | /** 305 | * Set the App Secret. 306 | * 307 | * @param string $appSecret The App Secret 308 | * 309 | * @return BaseFacebook 310 | */ 311 | public function setAppSecret($appSecret) { 312 | $this->appSecret = $appSecret; 313 | return $this; 314 | } 315 | 316 | /** 317 | * Get the App Secret. 318 | * 319 | * @return string the App Secret 320 | * 321 | * @deprecated Use getAppSecret instead. 322 | * @see getAppSecret() 323 | */ 324 | public function getApiSecret() { 325 | return $this->getAppSecret(); 326 | } 327 | 328 | /** 329 | * Get the App Secret. 330 | * 331 | * @return string the App Secret 332 | */ 333 | public function getAppSecret() { 334 | return $this->appSecret; 335 | } 336 | 337 | /** 338 | * Set the file upload support status. 339 | * 340 | * @param boolean $fileUploadSupport The file upload support status. 341 | * 342 | * @return BaseFacebook 343 | */ 344 | public function setFileUploadSupport($fileUploadSupport) { 345 | $this->fileUploadSupport = $fileUploadSupport; 346 | return $this; 347 | } 348 | 349 | /** 350 | * Get the file upload support status. 351 | * 352 | * @return boolean true if and only if the server supports file upload. 353 | */ 354 | public function getFileUploadSupport() { 355 | return $this->fileUploadSupport; 356 | } 357 | 358 | /** 359 | * Get the file upload support status. 360 | * 361 | * @return boolean true if and only if the server supports file upload. 362 | * 363 | * @deprecated Use getFileUploadSupport instead. 364 | * @see getFileUploadSupport() 365 | */ 366 | public function useFileUploadSupport() { 367 | return $this->getFileUploadSupport(); 368 | } 369 | 370 | /** 371 | * Sets the access token for api calls. Use this if you get 372 | * your access token by other means and just want the SDK 373 | * to use it. 374 | * 375 | * @param string $access_token an access token. 376 | * 377 | * @return BaseFacebook 378 | */ 379 | public function setAccessToken($access_token) { 380 | $this->accessToken = $access_token; 381 | return $this; 382 | } 383 | 384 | /** 385 | * Extend an access token, while removing the short-lived token that might 386 | * have been generated via client-side flow. Thanks to http://bit.ly/b0Pt0H 387 | * for the workaround. 388 | */ 389 | public function setExtendedAccessToken() { 390 | try { 391 | // need to circumvent json_decode by calling _oauthRequest 392 | // directly, since response isn't JSON format. 393 | $access_token_response = $this->_oauthRequest( 394 | $this->getUrl('graph', '/oauth/access_token'), 395 | $params = array( 396 | 'client_id' => $this->getAppId(), 397 | 'client_secret' => $this->getAppSecret(), 398 | 'grant_type' => 'fb_exchange_token', 399 | 'fb_exchange_token' => $this->getAccessToken(), 400 | ) 401 | ); 402 | } 403 | catch (FacebookApiException $e) { 404 | // most likely that user very recently revoked authorization. 405 | // In any event, we don't have an access token, so say so. 406 | return false; 407 | } 408 | 409 | if (empty($access_token_response)) { 410 | return false; 411 | } 412 | 413 | $response_params = array(); 414 | parse_str($access_token_response, $response_params); 415 | 416 | if (!isset($response_params['access_token'])) { 417 | return false; 418 | } 419 | 420 | $this->destroySession(); 421 | 422 | $this->setPersistentData( 423 | 'access_token', $response_params['access_token'] 424 | ); 425 | } 426 | 427 | /** 428 | * Determines the access token that should be used for API calls. 429 | * The first time this is called, $this->accessToken is set equal 430 | * to either a valid user access token, or it's set to the application 431 | * access token if a valid user access token wasn't available. Subsequent 432 | * calls return whatever the first call returned. 433 | * 434 | * @return string The access token 435 | */ 436 | public function getAccessToken() { 437 | if ($this->accessToken !== null) { 438 | // we've done this already and cached it. Just return. 439 | return $this->accessToken; 440 | } 441 | 442 | // first establish access token to be the application 443 | // access token, in case we navigate to the /oauth/access_token 444 | // endpoint, where SOME access token is required. 445 | $this->setAccessToken($this->getApplicationAccessToken()); 446 | $user_access_token = $this->getUserAccessToken(); 447 | if ($user_access_token) { 448 | $this->setAccessToken($user_access_token); 449 | } 450 | 451 | return $this->accessToken; 452 | } 453 | 454 | /** 455 | * Determines and returns the user access token, first using 456 | * the signed request if present, and then falling back on 457 | * the authorization code if present. The intent is to 458 | * return a valid user access token, or false if one is determined 459 | * to not be available. 460 | * 461 | * @return string A valid user access token, or false if one 462 | * could not be determined. 463 | */ 464 | protected function getUserAccessToken() { 465 | // first, consider a signed request if it's supplied. 466 | // if there is a signed request, then it alone determines 467 | // the access token. 468 | $signed_request = $this->getSignedRequest(); 469 | if ($signed_request) { 470 | // apps.facebook.com hands the access_token in the signed_request 471 | if (array_key_exists('oauth_token', $signed_request)) { 472 | $access_token = $signed_request['oauth_token']; 473 | $this->setPersistentData('access_token', $access_token); 474 | return $access_token; 475 | } 476 | 477 | // the JS SDK puts a code in with the redirect_uri of '' 478 | if (array_key_exists('code', $signed_request)) { 479 | $code = $signed_request['code']; 480 | if ($code && $code == $this->getPersistentData('code')) { 481 | // short-circuit if the code we have is the same as the one presented 482 | return $this->getPersistentData('access_token'); 483 | } 484 | 485 | $access_token = $this->getAccessTokenFromCode($code, ''); 486 | if ($access_token) { 487 | $this->setPersistentData('code', $code); 488 | $this->setPersistentData('access_token', $access_token); 489 | return $access_token; 490 | } 491 | } 492 | 493 | // signed request states there's no access token, so anything 494 | // stored should be cleared. 495 | $this->clearAllPersistentData(); 496 | return false; // respect the signed request's data, even 497 | // if there's an authorization code or something else 498 | } 499 | 500 | $code = $this->getCode(); 501 | if ($code && $code != $this->getPersistentData('code')) { 502 | $access_token = $this->getAccessTokenFromCode($code); 503 | if ($access_token) { 504 | $this->setPersistentData('code', $code); 505 | $this->setPersistentData('access_token', $access_token); 506 | return $access_token; 507 | } 508 | 509 | // code was bogus, so everything based on it should be invalidated. 510 | $this->clearAllPersistentData(); 511 | return false; 512 | } 513 | 514 | // as a fallback, just return whatever is in the persistent 515 | // store, knowing nothing explicit (signed request, authorization 516 | // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, 517 | // but it's the same as what's in the persistent store) 518 | return $this->getPersistentData('access_token'); 519 | } 520 | 521 | /** 522 | * Retrieve the signed request, either from a request parameter or, 523 | * if not present, from a cookie. 524 | * 525 | * @return string the signed request, if available, or null otherwise. 526 | */ 527 | public function getSignedRequest() { 528 | if (!$this->signedRequest) { 529 | if ($this->allowSignedRequest && !empty($_REQUEST['signed_request'])) { 530 | $this->signedRequest = $this->parseSignedRequest( 531 | $_REQUEST['signed_request'] 532 | ); 533 | } else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) { 534 | $this->signedRequest = $this->parseSignedRequest( 535 | $_COOKIE[$this->getSignedRequestCookieName()]); 536 | } 537 | } 538 | return $this->signedRequest; 539 | } 540 | 541 | /** 542 | * Get the UID of the connected user, or 0 543 | * if the Facebook user is not connected. 544 | * 545 | * @return string the UID if available. 546 | */ 547 | public function getUser() { 548 | if ($this->user !== null) { 549 | // we've already determined this and cached the value. 550 | return $this->user; 551 | } 552 | 553 | return $this->user = $this->getUserFromAvailableData(); 554 | } 555 | 556 | /** 557 | * Determines the connected user by first examining any signed 558 | * requests, then considering an authorization code, and then 559 | * falling back to any persistent store storing the user. 560 | * 561 | * @return integer The id of the connected Facebook user, 562 | * or 0 if no such user exists. 563 | */ 564 | protected function getUserFromAvailableData() { 565 | // if a signed request is supplied, then it solely determines 566 | // who the user is. 567 | $signed_request = $this->getSignedRequest(); 568 | if ($signed_request) { 569 | if (array_key_exists('user_id', $signed_request)) { 570 | $user = $signed_request['user_id']; 571 | 572 | if($user != $this->getPersistentData('user_id')){ 573 | $this->clearAllPersistentData(); 574 | } 575 | 576 | $this->setPersistentData('user_id', $signed_request['user_id']); 577 | return $user; 578 | } 579 | 580 | // if the signed request didn't present a user id, then invalidate 581 | // all entries in any persistent store. 582 | $this->clearAllPersistentData(); 583 | return 0; 584 | } 585 | 586 | $user = $this->getPersistentData('user_id', $default = 0); 587 | $persisted_access_token = $this->getPersistentData('access_token'); 588 | 589 | // use access_token to fetch user id if we have a user access_token, or if 590 | // the cached access token has changed. 591 | $access_token = $this->getAccessToken(); 592 | if ($access_token && 593 | $access_token != $this->getApplicationAccessToken() && 594 | !($user && $persisted_access_token == $access_token)) { 595 | $user = $this->getUserFromAccessToken(); 596 | if ($user) { 597 | $this->setPersistentData('user_id', $user); 598 | } else { 599 | $this->clearAllPersistentData(); 600 | } 601 | } 602 | 603 | return $user; 604 | } 605 | 606 | /** 607 | * Get a Login URL for use with redirects. By default, full page redirect is 608 | * assumed. If you are using the generated URL with a window.open() call in 609 | * JavaScript, you can pass in display=popup as part of the $params. 610 | * 611 | * The parameters: 612 | * - redirect_uri: the url to go to after a successful login 613 | * - scope: comma separated list of requested extended perms 614 | * 615 | * @param array $params Provide custom parameters 616 | * @return string The URL for the login flow 617 | */ 618 | public function getLoginUrl($params=array()) { 619 | $this->establishCSRFTokenState(); 620 | $currentUrl = $this->getCurrentUrl(); 621 | 622 | // if 'scope' is passed as an array, convert to comma separated list 623 | $scopeParams = isset($params['scope']) ? $params['scope'] : null; 624 | if ($scopeParams && is_array($scopeParams)) { 625 | $params['scope'] = implode(',', $scopeParams); 626 | } 627 | 628 | return $this->getUrl( 629 | 'www', 630 | 'dialog/oauth', 631 | array_merge( 632 | array( 633 | 'client_id' => $this->getAppId(), 634 | 'redirect_uri' => $currentUrl, // possibly overwritten 635 | 'state' => $this->state, 636 | 'sdk' => 'php-sdk-'.self::VERSION 637 | ), 638 | $params 639 | )); 640 | } 641 | 642 | /** 643 | * Get a Logout URL suitable for use with redirects. 644 | * 645 | * The parameters: 646 | * - next: the url to go to after a successful logout 647 | * 648 | * @param array $params Provide custom parameters 649 | * @return string The URL for the logout flow 650 | */ 651 | public function getLogoutUrl($params=array()) { 652 | return $this->getUrl( 653 | 'www', 654 | 'logout.php', 655 | array_merge(array( 656 | 'next' => $this->getCurrentUrl(), 657 | 'access_token' => $this->getUserAccessToken(), 658 | ), $params) 659 | ); 660 | } 661 | 662 | /** 663 | * Get a login status URL to fetch the status from Facebook. 664 | * 665 | * @param array $params Provide custom parameters 666 | * @return string The URL for the logout flow 667 | */ 668 | public function getLoginStatusUrl($params=array()) { 669 | return $this->getLoginUrl( 670 | array_merge(array( 671 | 'response_type' => 'code', 672 | 'display' => 'none', 673 | ), $params) 674 | ); 675 | } 676 | 677 | /** 678 | * Make an API call. 679 | * 680 | * @return mixed The decoded response 681 | */ 682 | public function api(/* polymorphic */) { 683 | $args = func_get_args(); 684 | if (is_array($args[0])) { 685 | return $this->_restserver($args[0]); 686 | } else { 687 | return call_user_func_array(array($this, '_graph'), $args); 688 | } 689 | } 690 | 691 | /** 692 | * Constructs and returns the name of the cookie that 693 | * potentially houses the signed request for the app user. 694 | * The cookie is not set by the BaseFacebook class, but 695 | * it may be set by the JavaScript SDK. 696 | * 697 | * @return string the name of the cookie that would house 698 | * the signed request value. 699 | */ 700 | protected function getSignedRequestCookieName() { 701 | return 'fbsr_'.$this->getAppId(); 702 | } 703 | 704 | /** 705 | * Constructs and returns the name of the cookie that potentially contain 706 | * metadata. The cookie is not set by the BaseFacebook class, but it may be 707 | * set by the JavaScript SDK. 708 | * 709 | * @return string the name of the cookie that would house metadata. 710 | */ 711 | protected function getMetadataCookieName() { 712 | return 'fbm_'.$this->getAppId(); 713 | } 714 | 715 | /** 716 | * Get the authorization code from the query parameters, if it exists, 717 | * and otherwise return false to signal no authorization code was 718 | * discoverable. 719 | * 720 | * @return mixed The authorization code, or false if the authorization 721 | * code could not be determined. 722 | */ 723 | protected function getCode() { 724 | if (!isset($_REQUEST['code']) || !isset($_REQUEST['state'])) { 725 | return false; 726 | } 727 | if ($this->state === $_REQUEST['state']) { 728 | // CSRF state has done its job, so clear it 729 | $this->state = null; 730 | $this->clearPersistentData('state'); 731 | return $_REQUEST['code']; 732 | } 733 | self::errorLog('CSRF state token does not match one provided.'); 734 | 735 | return false; 736 | } 737 | 738 | /** 739 | * Retrieves the UID with the understanding that 740 | * $this->accessToken has already been set and is 741 | * seemingly legitimate. It relies on Facebook's Graph API 742 | * to retrieve user information and then extract 743 | * the user ID. 744 | * 745 | * @return integer Returns the UID of the Facebook user, or 0 746 | * if the Facebook user could not be determined. 747 | */ 748 | protected function getUserFromAccessToken() { 749 | try { 750 | $user_info = $this->api('/me'); 751 | return $user_info['id']; 752 | } catch (FacebookApiException $e) { 753 | return 0; 754 | } 755 | } 756 | 757 | /** 758 | * Returns the access token that should be used for logged out 759 | * users when no authorization code is available. 760 | * 761 | * @return string The application access token, useful for gathering 762 | * public information about users and applications. 763 | */ 764 | public function getApplicationAccessToken() { 765 | return $this->appId.'|'.$this->appSecret; 766 | } 767 | 768 | /** 769 | * Lays down a CSRF state token for this process. 770 | * 771 | * @return void 772 | */ 773 | protected function establishCSRFTokenState() { 774 | if ($this->state === null) { 775 | $this->state = md5(uniqid(mt_rand(), true)); 776 | $this->setPersistentData('state', $this->state); 777 | } 778 | } 779 | 780 | /** 781 | * Retrieves an access token for the given authorization code 782 | * (previously generated from www.facebook.com on behalf of 783 | * a specific user). The authorization code is sent to graph.facebook.com 784 | * and a legitimate access token is generated provided the access token 785 | * and the user for which it was generated all match, and the user is 786 | * either logged in to Facebook or has granted an offline access permission. 787 | * 788 | * @param string $code An authorization code. 789 | * @param string $redirect_uri Optional redirect URI. Default null 790 | * 791 | * @return mixed An access token exchanged for the authorization code, or 792 | * false if an access token could not be generated. 793 | */ 794 | protected function getAccessTokenFromCode($code, $redirect_uri = null) { 795 | if (empty($code)) { 796 | return false; 797 | } 798 | 799 | if ($redirect_uri === null) { 800 | $redirect_uri = $this->getCurrentUrl(); 801 | } 802 | 803 | try { 804 | // need to circumvent json_decode by calling _oauthRequest 805 | // directly, since response isn't JSON format. 806 | $access_token_response = 807 | $this->_oauthRequest( 808 | $this->getUrl('graph', '/oauth/access_token'), 809 | $params = array('client_id' => $this->getAppId(), 810 | 'client_secret' => $this->getAppSecret(), 811 | 'redirect_uri' => $redirect_uri, 812 | 'code' => $code)); 813 | } catch (FacebookApiException $e) { 814 | // most likely that user very recently revoked authorization. 815 | // In any event, we don't have an access token, so say so. 816 | return false; 817 | } 818 | 819 | if (empty($access_token_response)) { 820 | return false; 821 | } 822 | 823 | $response_params = array(); 824 | parse_str($access_token_response, $response_params); 825 | if (!isset($response_params['access_token'])) { 826 | return false; 827 | } 828 | 829 | return $response_params['access_token']; 830 | } 831 | 832 | /** 833 | * Invoke the old restserver.php endpoint. 834 | * 835 | * @param array $params Method call object 836 | * 837 | * @return mixed The decoded response object 838 | * @throws FacebookApiException 839 | */ 840 | protected function _restserver($params) { 841 | // generic application level parameters 842 | $params['api_key'] = $this->getAppId(); 843 | $params['format'] = 'json-strings'; 844 | 845 | $result = json_decode($this->_oauthRequest( 846 | $this->getApiUrl($params['method']), 847 | $params 848 | ), true); 849 | 850 | // results are returned, errors are thrown 851 | if (is_array($result) && isset($result['error_code'])) { 852 | $this->throwAPIException($result); 853 | // @codeCoverageIgnoreStart 854 | } 855 | // @codeCoverageIgnoreEnd 856 | 857 | $method = strtolower($params['method']); 858 | if ($method === 'auth.expiresession' || 859 | $method === 'auth.revokeauthorization') { 860 | $this->destroySession(); 861 | } 862 | 863 | return $result; 864 | } 865 | 866 | /** 867 | * Return true if this is video post. 868 | * 869 | * @param string $path The path 870 | * @param string $method The http method (default 'GET') 871 | * 872 | * @return boolean true if this is video post 873 | */ 874 | protected function isVideoPost($path, $method = 'GET') { 875 | if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) { 876 | return true; 877 | } 878 | return false; 879 | } 880 | 881 | /** 882 | * Invoke the Graph API. 883 | * 884 | * @param string $path The path (required) 885 | * @param string $method The http method (default 'GET') 886 | * @param array $params The query/post data 887 | * 888 | * @return mixed The decoded response object 889 | * @throws FacebookApiException 890 | */ 891 | protected function _graph($path, $method = 'GET', $params = array()) { 892 | if (is_array($method) && empty($params)) { 893 | $params = $method; 894 | $method = 'GET'; 895 | } 896 | $params['method'] = $method; // method override as we always do a POST 897 | 898 | if ($this->isVideoPost($path, $method)) { 899 | $domainKey = 'graph_video'; 900 | } else { 901 | $domainKey = 'graph'; 902 | } 903 | 904 | $result = json_decode($this->_oauthRequest( 905 | $this->getUrl($domainKey, $path), 906 | $params 907 | ), true); 908 | 909 | // results are returned, errors are thrown 910 | if (is_array($result) && isset($result['error'])) { 911 | $this->throwAPIException($result); 912 | // @codeCoverageIgnoreStart 913 | } 914 | // @codeCoverageIgnoreEnd 915 | 916 | return $result; 917 | } 918 | 919 | /** 920 | * Make a OAuth Request. 921 | * 922 | * @param string $url The path (required) 923 | * @param array $params The query/post data 924 | * 925 | * @return string The decoded response object 926 | * @throws FacebookApiException 927 | */ 928 | protected function _oauthRequest($url, $params) { 929 | if (!isset($params['access_token'])) { 930 | $params['access_token'] = $this->getAccessToken(); 931 | } 932 | 933 | if (isset($params['access_token']) && !isset($params['appsecret_proof'])) { 934 | $params['appsecret_proof'] = $this->getAppSecretProof($params['access_token']); 935 | } 936 | 937 | // json_encode all params values that are not strings 938 | foreach ($params as $key => $value) { 939 | if (!is_string($value) && !($value instanceof CURLFile)) { 940 | $params[$key] = json_encode($value); 941 | } 942 | } 943 | 944 | return $this->makeRequest($url, $params); 945 | } 946 | 947 | /** 948 | * Generate a proof of App Secret 949 | * This is required for all API calls originating from a server 950 | * It is a sha256 hash of the access_token made using the app secret 951 | * 952 | * @param string $access_token The access_token to be hashed (required) 953 | * 954 | * @return string The sha256 hash of the access_token 955 | */ 956 | protected function getAppSecretProof($access_token) { 957 | return hash_hmac('sha256', $access_token, $this->getAppSecret()); 958 | } 959 | 960 | /** 961 | * Makes an HTTP request. This method can be overridden by subclasses if 962 | * developers want to do fancier things or use something other than curl to 963 | * make the request. 964 | * 965 | * @param string $url The URL to make the request to 966 | * @param array $params The parameters to use for the POST body 967 | * @param CurlHandler $ch Initialized curl handle 968 | * 969 | * @return string The response text 970 | */ 971 | protected function makeRequest($url, $params, $ch=null) { 972 | if (!$ch) { 973 | $ch = curl_init(); 974 | } 975 | 976 | $opts = self::$CURL_OPTS; 977 | if ($this->getFileUploadSupport()) { 978 | $opts[CURLOPT_POSTFIELDS] = $params; 979 | } else { 980 | $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); 981 | } 982 | $opts[CURLOPT_URL] = $url; 983 | 984 | // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait 985 | // for 2 seconds if the server does not support this header. 986 | if (isset($opts[CURLOPT_HTTPHEADER])) { 987 | $existing_headers = $opts[CURLOPT_HTTPHEADER]; 988 | $existing_headers[] = 'Expect:'; 989 | $opts[CURLOPT_HTTPHEADER] = $existing_headers; 990 | } else { 991 | $opts[CURLOPT_HTTPHEADER] = array('Expect:'); 992 | } 993 | 994 | curl_setopt_array($ch, $opts); 995 | $result = curl_exec($ch); 996 | 997 | $errno = curl_errno($ch); 998 | // CURLE_SSL_CACERT || CURLE_SSL_CACERT_BADFILE 999 | if ($errno == 60 || $errno == 77) { 1000 | self::errorLog('Invalid or no certificate authority found, '. 1001 | 'using bundled information'); 1002 | curl_setopt($ch, CURLOPT_CAINFO, 1003 | dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt'); 1004 | $result = curl_exec($ch); 1005 | } 1006 | 1007 | // With dual stacked DNS responses, it's possible for a server to 1008 | // have IPv6 enabled but not have IPv6 connectivity. If this is 1009 | // the case, curl will try IPv4 first and if that fails, then it will 1010 | // fall back to IPv6 and the error EHOSTUNREACH is returned by the 1011 | // operating system. 1012 | if ($result === false && empty($opts[CURLOPT_IPRESOLVE])) { 1013 | $matches = array(); 1014 | $regex = '/Failed to connect to ([^:].*): Network is unreachable/'; 1015 | if (preg_match($regex, curl_error($ch), $matches)) { 1016 | if (strlen(@inet_pton($matches[1])) === 16) { 1017 | self::errorLog('Invalid IPv6 configuration on server, '. 1018 | 'Please disable or get native IPv6 on your server.'); 1019 | self::$CURL_OPTS[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; 1020 | curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); 1021 | $result = curl_exec($ch); 1022 | } 1023 | } 1024 | } 1025 | 1026 | if ($result === false) { 1027 | $e = new FacebookApiException(array( 1028 | 'error_code' => curl_errno($ch), 1029 | 'error' => array( 1030 | 'message' => curl_error($ch), 1031 | 'type' => 'CurlException', 1032 | ), 1033 | )); 1034 | curl_close($ch); 1035 | throw $e; 1036 | } 1037 | curl_close($ch); 1038 | return $result; 1039 | } 1040 | 1041 | /** 1042 | * Parses a signed_request and validates the signature. 1043 | * 1044 | * @param string $signed_request A signed token 1045 | * 1046 | * @return array The payload inside it or null if the sig is wrong 1047 | */ 1048 | protected function parseSignedRequest($signed_request) { 1049 | 1050 | if (!$signed_request || strpos($signed_request, '.') === false) { 1051 | self::errorLog('Signed request was invalid!'); 1052 | return null; 1053 | } 1054 | 1055 | list($encoded_sig, $payload) = explode('.', $signed_request, 2); 1056 | 1057 | // decode the data 1058 | $sig = self::base64UrlDecode($encoded_sig); 1059 | $data = json_decode(self::base64UrlDecode($payload), true); 1060 | 1061 | if (!isset($data['algorithm']) 1062 | || strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM 1063 | ) { 1064 | self::errorLog( 1065 | 'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM); 1066 | return null; 1067 | } 1068 | 1069 | // check sig 1070 | $expected_sig = hash_hmac('sha256', $payload, 1071 | $this->getAppSecret(), $raw = true); 1072 | 1073 | if (strlen($expected_sig) !== strlen($sig)) { 1074 | self::errorLog('Bad Signed JSON signature!'); 1075 | return null; 1076 | } 1077 | 1078 | $result = 0; 1079 | for ($i = 0; $i < strlen($expected_sig); $i++) { 1080 | $result |= ord($expected_sig[$i]) ^ ord($sig[$i]); 1081 | } 1082 | 1083 | if ($result == 0) { 1084 | return $data; 1085 | } else { 1086 | self::errorLog('Bad Signed JSON signature!'); 1087 | return null; 1088 | } 1089 | } 1090 | 1091 | /** 1092 | * Makes a signed_request blob using the given data. 1093 | * 1094 | * @param array $data The data array. 1095 | * 1096 | * @return string The signed request. 1097 | */ 1098 | protected function makeSignedRequest($data) { 1099 | if (!is_array($data)) { 1100 | throw new InvalidArgumentException( 1101 | 'makeSignedRequest expects an array. Got: ' . print_r($data, true)); 1102 | } 1103 | $data['algorithm'] = self::SIGNED_REQUEST_ALGORITHM; 1104 | $data['issued_at'] = time(); 1105 | $json = json_encode($data); 1106 | $b64 = self::base64UrlEncode($json); 1107 | $raw_sig = hash_hmac('sha256', $b64, $this->getAppSecret(), $raw = true); 1108 | $sig = self::base64UrlEncode($raw_sig); 1109 | return $sig.'.'.$b64; 1110 | } 1111 | 1112 | /** 1113 | * Build the URL for api given parameters. 1114 | * 1115 | * @param string $method The method name. 1116 | * 1117 | * @return string The URL for the given parameters 1118 | */ 1119 | protected function getApiUrl($method) { 1120 | static $READ_ONLY_CALLS = 1121 | array('admin.getallocation' => 1, 1122 | 'admin.getappproperties' => 1, 1123 | 'admin.getbannedusers' => 1, 1124 | 'admin.getlivestreamvialink' => 1, 1125 | 'admin.getmetrics' => 1, 1126 | 'admin.getrestrictioninfo' => 1, 1127 | 'application.getpublicinfo' => 1, 1128 | 'auth.getapppublickey' => 1, 1129 | 'auth.getsession' => 1, 1130 | 'auth.getsignedpublicsessiondata' => 1, 1131 | 'comments.get' => 1, 1132 | 'connect.getunconnectedfriendscount' => 1, 1133 | 'dashboard.getactivity' => 1, 1134 | 'dashboard.getcount' => 1, 1135 | 'dashboard.getglobalnews' => 1, 1136 | 'dashboard.getnews' => 1, 1137 | 'dashboard.multigetcount' => 1, 1138 | 'dashboard.multigetnews' => 1, 1139 | 'data.getcookies' => 1, 1140 | 'events.get' => 1, 1141 | 'events.getmembers' => 1, 1142 | 'fbml.getcustomtags' => 1, 1143 | 'feed.getappfriendstories' => 1, 1144 | 'feed.getregisteredtemplatebundlebyid' => 1, 1145 | 'feed.getregisteredtemplatebundles' => 1, 1146 | 'fql.multiquery' => 1, 1147 | 'fql.query' => 1, 1148 | 'friends.arefriends' => 1, 1149 | 'friends.get' => 1, 1150 | 'friends.getappusers' => 1, 1151 | 'friends.getlists' => 1, 1152 | 'friends.getmutualfriends' => 1, 1153 | 'gifts.get' => 1, 1154 | 'groups.get' => 1, 1155 | 'groups.getmembers' => 1, 1156 | 'intl.gettranslations' => 1, 1157 | 'links.get' => 1, 1158 | 'notes.get' => 1, 1159 | 'notifications.get' => 1, 1160 | 'pages.getinfo' => 1, 1161 | 'pages.isadmin' => 1, 1162 | 'pages.isappadded' => 1, 1163 | 'pages.isfan' => 1, 1164 | 'permissions.checkavailableapiaccess' => 1, 1165 | 'permissions.checkgrantedapiaccess' => 1, 1166 | 'photos.get' => 1, 1167 | 'photos.getalbums' => 1, 1168 | 'photos.gettags' => 1, 1169 | 'profile.getinfo' => 1, 1170 | 'profile.getinfooptions' => 1, 1171 | 'stream.get' => 1, 1172 | 'stream.getcomments' => 1, 1173 | 'stream.getfilters' => 1, 1174 | 'users.getinfo' => 1, 1175 | 'users.getloggedinuser' => 1, 1176 | 'users.getstandardinfo' => 1, 1177 | 'users.hasapppermission' => 1, 1178 | 'users.isappuser' => 1, 1179 | 'users.isverified' => 1, 1180 | 'video.getuploadlimits' => 1); 1181 | $name = 'api'; 1182 | if (isset($READ_ONLY_CALLS[strtolower($method)])) { 1183 | $name = 'api_read'; 1184 | } else if (strtolower($method) == 'video.upload') { 1185 | $name = 'api_video'; 1186 | } 1187 | return self::getUrl($name, 'restserver.php'); 1188 | } 1189 | 1190 | /** 1191 | * Build the URL for given domain alias, path and parameters. 1192 | * 1193 | * @param string $name The name of the domain 1194 | * @param string $path Optional path (without a leading slash) 1195 | * @param array $params Optional query parameters 1196 | * 1197 | * @return string The URL for the given parameters 1198 | */ 1199 | protected function getUrl($name, $path='', $params=array()) { 1200 | $url = self::$DOMAIN_MAP[$name]; 1201 | if ($path) { 1202 | if ($path[0] === '/') { 1203 | $path = substr($path, 1); 1204 | } 1205 | $url .= $path; 1206 | } 1207 | if ($params) { 1208 | $url .= '?' . http_build_query($params, null, '&'); 1209 | } 1210 | 1211 | return $url; 1212 | } 1213 | 1214 | /** 1215 | * Returns the HTTP Host 1216 | * 1217 | * @return string The HTTP Host 1218 | */ 1219 | protected function getHttpHost() { 1220 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { 1221 | $forwardProxies = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']); 1222 | if (!empty($forwardProxies)) { 1223 | return $forwardProxies[0]; 1224 | } 1225 | } 1226 | return $_SERVER['HTTP_HOST']; 1227 | } 1228 | 1229 | /** 1230 | * Returns the HTTP Protocol 1231 | * 1232 | * @return string The HTTP Protocol 1233 | */ 1234 | protected function getHttpProtocol() { 1235 | if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 1236 | if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { 1237 | return 'https'; 1238 | } 1239 | return 'http'; 1240 | } 1241 | /*apache + variants specific way of checking for https*/ 1242 | if (isset($_SERVER['HTTPS']) && 1243 | ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) { 1244 | return 'https'; 1245 | } 1246 | /*nginx way of checking for https*/ 1247 | if (isset($_SERVER['SERVER_PORT']) && 1248 | ($_SERVER['SERVER_PORT'] === '443')) { 1249 | return 'https'; 1250 | } 1251 | return 'http'; 1252 | } 1253 | 1254 | /** 1255 | * Returns the base domain used for the cookie. 1256 | * 1257 | * @return string The base domain 1258 | */ 1259 | protected function getBaseDomain() { 1260 | // The base domain is stored in the metadata cookie if not we fallback 1261 | // to the current hostname 1262 | $metadata = $this->getMetadataCookie(); 1263 | if (array_key_exists('base_domain', $metadata) && 1264 | !empty($metadata['base_domain'])) { 1265 | return trim($metadata['base_domain'], '.'); 1266 | } 1267 | return $this->getHttpHost(); 1268 | } 1269 | 1270 | /** 1271 | * Returns the Current URL, stripping it of known FB parameters that should 1272 | * not persist. 1273 | * 1274 | * @return string The current URL 1275 | */ 1276 | protected function getCurrentUrl() { 1277 | $protocol = $this->getHttpProtocol() . '://'; 1278 | $host = $this->getHttpHost(); 1279 | $currentUrl = $protocol.$host.$_SERVER['REQUEST_URI']; 1280 | $parts = parse_url($currentUrl); 1281 | 1282 | $query = ''; 1283 | if (!empty($parts['query'])) { 1284 | // drop known fb params 1285 | $params = explode('&', $parts['query']); 1286 | $retained_params = array(); 1287 | foreach ($params as $param) { 1288 | if ($this->shouldRetainParam($param)) { 1289 | $retained_params[] = $param; 1290 | } 1291 | } 1292 | 1293 | if (!empty($retained_params)) { 1294 | $query = '?'.implode($retained_params, '&'); 1295 | } 1296 | } 1297 | 1298 | // use port if non default 1299 | $port = 1300 | isset($parts['port']) && 1301 | (($protocol === 'http://' && $parts['port'] !== 80) || 1302 | ($protocol === 'https://' && $parts['port'] !== 443)) 1303 | ? ':' . $parts['port'] : ''; 1304 | 1305 | // rebuild 1306 | return $protocol . $parts['host'] . $port . $parts['path'] . $query; 1307 | } 1308 | 1309 | /** 1310 | * Returns true if and only if the key or key/value pair should 1311 | * be retained as part of the query string. This amounts to 1312 | * a brute-force search of the very small list of Facebook-specific 1313 | * params that should be stripped out. 1314 | * 1315 | * @param string $param A key or key/value pair within a URL's query (e.g. 1316 | * 'foo=a', 'foo=', or 'foo'. 1317 | * 1318 | * @return boolean 1319 | */ 1320 | protected function shouldRetainParam($param) { 1321 | foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { 1322 | if ($param === $drop_query_param || 1323 | strpos($param, $drop_query_param.'=') === 0) { 1324 | return false; 1325 | } 1326 | } 1327 | 1328 | return true; 1329 | } 1330 | 1331 | /** 1332 | * Analyzes the supplied result to see if it was thrown 1333 | * because the access token is no longer valid. If that is 1334 | * the case, then we destroy the session. 1335 | * 1336 | * @param array $result A record storing the error message returned 1337 | * by a failed API call. 1338 | */ 1339 | protected function throwAPIException($result) { 1340 | $e = new FacebookApiException($result); 1341 | switch ($e->getType()) { 1342 | // OAuth 2.0 Draft 00 style 1343 | case 'OAuthException': 1344 | // OAuth 2.0 Draft 10 style 1345 | case 'invalid_token': 1346 | // REST server errors are just Exceptions 1347 | case 'Exception': 1348 | $message = $e->getMessage(); 1349 | if ((strpos($message, 'Error validating access token') !== false) || 1350 | (strpos($message, 'Invalid OAuth access token') !== false) || 1351 | (strpos($message, 'An active access token must be used') !== false) 1352 | ) { 1353 | $this->destroySession(); 1354 | } 1355 | break; 1356 | } 1357 | 1358 | throw $e; 1359 | } 1360 | 1361 | 1362 | /** 1363 | * Prints to the error log if you aren't in command line mode. 1364 | * 1365 | * @param string $msg Log message 1366 | */ 1367 | protected static function errorLog($msg) { 1368 | // disable error log if we are running in a CLI environment 1369 | // @codeCoverageIgnoreStart 1370 | if (php_sapi_name() != 'cli') { 1371 | error_log($msg); 1372 | } 1373 | // uncomment this if you want to see the errors on the page 1374 | // print 'error_log: '.$msg."\n"; 1375 | // @codeCoverageIgnoreEnd 1376 | } 1377 | 1378 | /** 1379 | * Base64 encoding that doesn't need to be urlencode()ed. 1380 | * Exactly the same as base64_encode except it uses 1381 | * - instead of + 1382 | * _ instead of / 1383 | * No padded = 1384 | * 1385 | * @param string $input base64UrlEncoded input 1386 | * 1387 | * @return string The decoded string 1388 | */ 1389 | protected static function base64UrlDecode($input) { 1390 | return base64_decode(strtr($input, '-_', '+/')); 1391 | } 1392 | 1393 | /** 1394 | * Base64 encoding that doesn't need to be urlencode()ed. 1395 | * Exactly the same as base64_encode except it uses 1396 | * - instead of + 1397 | * _ instead of / 1398 | * 1399 | * @param string $input The input to encode 1400 | * @return string The base64Url encoded input, as a string. 1401 | */ 1402 | protected static function base64UrlEncode($input) { 1403 | $str = strtr(base64_encode($input), '+/', '-_'); 1404 | $str = str_replace('=', '', $str); 1405 | return $str; 1406 | } 1407 | 1408 | /** 1409 | * Destroy the current session 1410 | */ 1411 | public function destroySession() { 1412 | $this->accessToken = null; 1413 | $this->signedRequest = null; 1414 | $this->user = null; 1415 | $this->clearAllPersistentData(); 1416 | 1417 | // Javascript sets a cookie that will be used in getSignedRequest that we 1418 | // need to clear if we can 1419 | $cookie_name = $this->getSignedRequestCookieName(); 1420 | if (array_key_exists($cookie_name, $_COOKIE)) { 1421 | unset($_COOKIE[$cookie_name]); 1422 | if (!headers_sent()) { 1423 | $base_domain = $this->getBaseDomain(); 1424 | setcookie($cookie_name, '', 1, '/', '.'.$base_domain); 1425 | } else { 1426 | // @codeCoverageIgnoreStart 1427 | self::errorLog( 1428 | 'There exists a cookie that we wanted to clear that we couldn\'t '. 1429 | 'clear because headers was already sent. Make sure to do the first '. 1430 | 'API call before outputing anything.' 1431 | ); 1432 | // @codeCoverageIgnoreEnd 1433 | } 1434 | } 1435 | } 1436 | 1437 | /** 1438 | * Parses the metadata cookie that our Javascript API set 1439 | * 1440 | * @return array an array mapping key to value 1441 | */ 1442 | protected function getMetadataCookie() { 1443 | $cookie_name = $this->getMetadataCookieName(); 1444 | if (!array_key_exists($cookie_name, $_COOKIE)) { 1445 | return array(); 1446 | } 1447 | 1448 | // The cookie value can be wrapped in "-characters so remove them 1449 | $cookie_value = trim($_COOKIE[$cookie_name], '"'); 1450 | 1451 | if (empty($cookie_value)) { 1452 | return array(); 1453 | } 1454 | 1455 | $parts = explode('&', $cookie_value); 1456 | $metadata = array(); 1457 | foreach ($parts as $part) { 1458 | $pair = explode('=', $part, 2); 1459 | if (!empty($pair[0])) { 1460 | $metadata[urldecode($pair[0])] = 1461 | (count($pair) > 1) ? urldecode($pair[1]) : ''; 1462 | } 1463 | } 1464 | 1465 | return $metadata; 1466 | } 1467 | 1468 | /** 1469 | * Finds whether the given domain is allowed or not 1470 | * 1471 | * @param string $big The value to be checked against $small 1472 | * @param string $small The input string 1473 | * 1474 | * @return boolean Returns TRUE if $big matches $small 1475 | */ 1476 | protected static function isAllowedDomain($big, $small) { 1477 | if ($big === $small) { 1478 | return true; 1479 | } 1480 | return self::endsWith($big, '.'.$small); 1481 | } 1482 | 1483 | /** 1484 | * Checks if $big string ends with $small string 1485 | * 1486 | * @param string $big The value to be checked against $small 1487 | * @param string $small The input string 1488 | * 1489 | * @return boolean TRUE if $big ends with $small 1490 | */ 1491 | protected static function endsWith($big, $small) { 1492 | $len = strlen($small); 1493 | if ($len === 0) { 1494 | return true; 1495 | } 1496 | return substr($big, -$len) === $small; 1497 | } 1498 | 1499 | /** 1500 | * Each of the following four methods should be overridden in 1501 | * a concrete subclass, as they are in the provided Facebook class. 1502 | * The Facebook class uses PHP sessions to provide a primitive 1503 | * persistent store, but another subclass--one that you implement-- 1504 | * might use a database, memcache, or an in-memory cache. 1505 | * 1506 | * @see Facebook 1507 | */ 1508 | 1509 | /** 1510 | * Stores the given ($key, $value) pair, so that future calls to 1511 | * getPersistentData($key) return $value. This call may be in another request. 1512 | * 1513 | * @param string $key 1514 | * @param array $value 1515 | * 1516 | * @return void 1517 | */ 1518 | abstract protected function setPersistentData($key, $value); 1519 | 1520 | /** 1521 | * Get the data for $key, persisted by BaseFacebook::setPersistentData() 1522 | * 1523 | * @param string $key The key of the data to retrieve 1524 | * @param boolean $default The default value to return if $key is not found 1525 | * 1526 | * @return mixed 1527 | */ 1528 | abstract protected function getPersistentData($key, $default = false); 1529 | 1530 | /** 1531 | * Clear the data with $key from the persistent storage 1532 | * 1533 | * @param string $key 1534 | * 1535 | * @return void 1536 | */ 1537 | abstract protected function clearPersistentData($key); 1538 | 1539 | /** 1540 | * Clear all data from the persistent storage 1541 | * 1542 | * @return void 1543 | */ 1544 | abstract protected function clearAllPersistentData(); 1545 | } -------------------------------------------------------------------------------- /libraries/facebook.php: -------------------------------------------------------------------------------- 1 | _ci =& get_instance(); 67 | $this->_ci->load->config('facebook'); 68 | $config = array( 69 | 'appId' => $this->_ci->config->item('appId'), 70 | 'secret' => $this->_ci->config->item('secret'), 71 | ); 72 | } 73 | 74 | if( !isset($config['appId']) || !isset($config['secret']) ){ 75 | $this->_ci =& get_instance(); 76 | $this->_ci->load->config('facebook'); 77 | $config['appId'] = $this->_ci->config->item('appId'); 78 | $config['secret'] = $this->_ci->config->item('secret'); 79 | } 80 | 81 | parent::__construct($config); 82 | if (!empty($config['sharedSession'])) { 83 | $this->initSharedSession(); 84 | 85 | // re-load the persisted state, since parent 86 | // attempted to read out of non-shared cookie 87 | $state = $this->getPersistentData('state'); 88 | if (!empty($state)) { 89 | $this->state = $state; 90 | } else { 91 | $this->state = null; 92 | } 93 | 94 | } 95 | } 96 | 97 | /** 98 | * Supported keys for persistent data 99 | * 100 | * @var array 101 | */ 102 | protected static $kSupportedKeys = 103 | array('state', 'code', 'access_token', 'user_id'); 104 | 105 | /** 106 | * Initiates Shared Session 107 | */ 108 | protected function initSharedSession() { 109 | $cookie_name = $this->getSharedSessionCookieName(); 110 | if (isset($_COOKIE[$cookie_name])) { 111 | $data = $this->parseSignedRequest($_COOKIE[$cookie_name]); 112 | if ($data && !empty($data['domain']) && 113 | self::isAllowedDomain($this->getHttpHost(), $data['domain'])) { 114 | // good case 115 | $this->sharedSessionID = $data['id']; 116 | return; 117 | } 118 | // ignoring potentially unreachable data 119 | } 120 | // evil/corrupt/missing case 121 | $base_domain = $this->getBaseDomain(); 122 | $this->sharedSessionID = md5(uniqid(mt_rand(), true)); 123 | $cookie_value = $this->makeSignedRequest( 124 | array( 125 | 'domain' => $base_domain, 126 | 'id' => $this->sharedSessionID, 127 | ) 128 | ); 129 | $_COOKIE[$cookie_name] = $cookie_value; 130 | if (!headers_sent()) { 131 | $expire = time() + self::FBSS_COOKIE_EXPIRE; 132 | setcookie($cookie_name, $cookie_value, $expire, '/', '.'.$base_domain); 133 | } else { 134 | // @codeCoverageIgnoreStart 135 | self::errorLog( 136 | 'Shared session ID cookie could not be set! You must ensure you '. 137 | 'create the Facebook instance before headers have been sent. This '. 138 | 'will cause authentication issues after the first request.' 139 | ); 140 | // @codeCoverageIgnoreEnd 141 | } 142 | } 143 | 144 | /** 145 | * Provides the implementations of the inherited abstract 146 | * methods. The implementation uses PHP sessions to maintain 147 | * a store for authorization codes, user ids, CSRF states, and 148 | * access tokens. 149 | */ 150 | 151 | /** 152 | * {@inheritdoc} 153 | * 154 | * @see BaseFacebook::setPersistentData() 155 | */ 156 | protected function setPersistentData($key, $value) { 157 | if (!in_array($key, self::$kSupportedKeys)) { 158 | self::errorLog('Unsupported key passed to setPersistentData.'); 159 | return; 160 | } 161 | 162 | $session_var_name = $this->constructSessionVariableName($key); 163 | $_SESSION[$session_var_name] = $value; 164 | } 165 | 166 | /** 167 | * {@inheritdoc} 168 | * 169 | * @see BaseFacebook::getPersistentData() 170 | */ 171 | protected function getPersistentData($key, $default = false) { 172 | if (!in_array($key, self::$kSupportedKeys)) { 173 | self::errorLog('Unsupported key passed to getPersistentData.'); 174 | return $default; 175 | } 176 | 177 | $session_var_name = $this->constructSessionVariableName($key); 178 | return isset($_SESSION[$session_var_name]) ? 179 | $_SESSION[$session_var_name] : $default; 180 | } 181 | 182 | /** 183 | * {@inheritdoc} 184 | * 185 | * @see BaseFacebook::clearPersistentData() 186 | */ 187 | protected function clearPersistentData($key) { 188 | if (!in_array($key, self::$kSupportedKeys)) { 189 | self::errorLog('Unsupported key passed to clearPersistentData.'); 190 | return; 191 | } 192 | 193 | $session_var_name = $this->constructSessionVariableName($key); 194 | if (isset($_SESSION[$session_var_name])) { 195 | unset($_SESSION[$session_var_name]); 196 | } 197 | } 198 | 199 | /** 200 | * {@inheritdoc} 201 | * 202 | * @see BaseFacebook::clearAllPersistentData() 203 | */ 204 | protected function clearAllPersistentData() { 205 | foreach (self::$kSupportedKeys as $key) { 206 | $this->clearPersistentData($key); 207 | } 208 | if ($this->sharedSessionID) { 209 | $this->deleteSharedSessionCookie(); 210 | } 211 | } 212 | 213 | /** 214 | * Deletes Shared session cookie 215 | */ 216 | protected function deleteSharedSessionCookie() { 217 | $cookie_name = $this->getSharedSessionCookieName(); 218 | unset($_COOKIE[$cookie_name]); 219 | $base_domain = $this->getBaseDomain(); 220 | setcookie($cookie_name, '', 1, '/', '.'.$base_domain); 221 | } 222 | 223 | /** 224 | * Returns the Shared session cookie name 225 | * 226 | * @return string The Shared session cookie name 227 | */ 228 | protected function getSharedSessionCookieName() { 229 | return self::FBSS_COOKIE_NAME . '_' . $this->getAppId(); 230 | } 231 | 232 | /** 233 | * Constructs and returns the name of the session key. 234 | * 235 | * @see setPersistentData() 236 | * @param string $key The key for which the session variable name to construct. 237 | * 238 | * @return string The name of the session key. 239 | */ 240 | protected function constructSessionVariableName($key) { 241 | $parts = array('fb', $this->getAppId(), $key); 242 | if ($this->sharedSessionID) { 243 | array_unshift($parts, $this->sharedSessionID); 244 | } 245 | return implode('_', $parts); 246 | } 247 | } -------------------------------------------------------------------------------- /views/login.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Login with Facebook | Puneet Kalra 4 | 5 | 6 | 34 | 35 | 36 | 37 |
38 | 39 | 59 | 60 |
61 |
62 | 63 | 64 | 65 |
66 | 73 | 74 |
75 |
76 | 77 |
78 | 79 | 80 | --------------------------------------------------------------------------------