diff --git a/.gitignore b/.gitignore index 7053dc17..c2a70fcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ /coverage/ +vendor/ +composer.lock +composer.phar +.DS_Store +.idea/ diff --git a/.travis.yml b/.travis.yml index 2790134e..9c49978d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,5 @@ language: php php: - 5.3 - 5.4 + - 5.5 script: phpunit --stderr --bootstrap tests/bootstrap.php tests/tests.php diff --git a/composer.json b/composer.json index 6ec7c917..38cba016 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,9 @@ "ext-curl": "*", "ext-json": "*" }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, "autoload": { "classmap": ["src"] } diff --git a/examples/example.php b/examples/example.php index cd0b378c..645adf2b 100644 --- a/examples/example.php +++ b/examples/example.php @@ -1,4 +1,9 @@ 'YOUR_APP_ID', - 'secret' => 'YOUR_APP_SECRET', - )); +$facebook = new Facebook(array( + 'appId' => 'YOUR_APP_ID', + 'secret' => 'YOUR_APP_SECRET', +)); - // Get User ID - $user = $facebook->getUser(); +// Get User ID +$user = $facebook->getUser(); +``` To make [API][API] calls: +```php +if ($user) { + try { + // Proceed knowing you have a logged in user who's authenticated. + $user_profile = $facebook->api('/me'); + } catch (FacebookApiException $e) { + error_log($e); + $user = null; + } +} +``` + +You can make api calls by choosing the `HTTP method` and setting optional `parameters`: +```php +$facebook->api('/me/feed/', 'post', array( + 'message' => 'I want to display this message on my wall' +)); +``` - if ($user) { - try { - // Proceed knowing you have a logged in user who's authenticated. - $user_profile = $facebook->api('/me'); - } catch (FacebookApiException $e) { - error_log($e); - $user = null; - } - } Login or logout url will be needed depending on current user state. - - if ($user) { - $logoutUrl = $facebook->getLogoutUrl(); - } else { - $loginUrl = $facebook->getLoginUrl(); - } - -[examples]: http://github.com/facebook/facebook-php-sdk/blob/master/examples/example.php +```php +if ($user) { + $logoutUrl = $facebook->getLogoutUrl(); +} else { + $loginUrl = $facebook->getLoginUrl(); +} +``` + +With Composer: + +- Add the `"facebook/php-sdk": "@stable"` into the `require` section of your `composer.json`. +- Run `composer install`. +- The example will look like + +```php +if (($loader = require_once __DIR__ . '/vendor/autoload.php') == null) { + die('Vendor directory not found, Please run composer install.'); +} + +$facebook = new Facebook(array( + 'appId' => 'YOUR_APP_ID', + 'secret' => 'YOUR_APP_SECRET', +)); + +// Get User ID +$user = $facebook->getUser(); +``` + +[examples]: /examples/example.php [API]: http://developers.facebook.com/docs/api - Tests ----- diff --git a/src/base_facebook.php b/src/base_facebook.php index 82758192..95e18fb1 100644 --- a/src/base_facebook.php +++ b/src/base_facebook.php @@ -31,6 +31,8 @@ class FacebookApiException extends Exception { /** * The result from the API server that represents the exception information. + * + * @var mixed */ protected $result; @@ -42,7 +44,10 @@ class FacebookApiException extends Exception public function __construct($result) { $this->result = $result; - $code = isset($result['error_code']) ? $result['error_code'] : 0; + $code = 0; + if (isset($result['error_code']) && is_int($result['error_code'])) { + $code = $result['error_code']; + } if (isset($result['error_description'])) { // OAuth 2.0 Draft 10 style @@ -120,7 +125,7 @@ abstract class BaseFacebook /** * Version. */ - const VERSION = '3.2.2'; + const VERSION = '3.2.3'; /** * Signed Request Algorithm. @@ -129,6 +134,8 @@ abstract class BaseFacebook /** * Default options for curl. + * + * @var array */ public static $CURL_OPTS = array( CURLOPT_CONNECTTIMEOUT => 10, @@ -140,6 +147,8 @@ abstract class BaseFacebook /** * List of query parameters that get automatically dropped when rebuilding * the current URL. + * + * @var array */ protected static $DROP_QUERY_PARAMS = array( 'code', @@ -149,6 +158,8 @@ abstract class BaseFacebook /** * Maps aliases to Facebook domains. + * + * @var array */ public static $DOMAIN_MAP = array( 'api' => 'https://api.facebook.com/', @@ -182,11 +193,15 @@ abstract class BaseFacebook /** * The data from the signed_request token. + * + * @var string */ protected $signedRequest; /** * A CSRF state variable to assist in the defense against CSRF attacks. + * + * @var string */ protected $state; @@ -212,6 +227,13 @@ abstract class BaseFacebook */ protected $trustForwarded = false; + /** + * Indicates if signed_request is allowed in query parameters. + * + * @var boolean + */ + protected $allowSignedRequest = true; + /** * Initialize a Facebook Application. * @@ -219,6 +241,9 @@ abstract class BaseFacebook * - appId: the application ID * - secret: the application secret * - fileUpload: (optional) boolean indicating if file uploads are enabled + * - allowSignedRequest: (optional) boolean indicating if signed_request is + * allowed in query parameters or POST body. Should be + * false for non-canvas apps. Defaults to true. * * @param array $config The application configuration */ @@ -231,6 +256,10 @@ public function __construct($config) { if (isset($config['trustForwarded']) && $config['trustForwarded']) { $this->trustForwarded = true; } + if (isset($config['allowSignedRequest']) + && !$config['allowSignedRequest']) { + $this->allowSignedRequest = false; + } $state = $this->getPersistentData('state'); if (!empty($state)) { $this->state = $state; @@ -241,6 +270,7 @@ public function __construct($config) { * Set the Application ID. * * @param string $appId The Application ID + * * @return BaseFacebook */ public function setAppId($appId) { @@ -261,8 +291,10 @@ public function getAppId() { * Set the App Secret. * * @param string $apiSecret The App Secret + * * @return BaseFacebook - * @deprecated + * @deprecated Use setAppSecret instead. + * @see setAppSecret() */ public function setApiSecret($apiSecret) { $this->setAppSecret($apiSecret); @@ -273,6 +305,7 @@ public function setApiSecret($apiSecret) { * Set the App Secret. * * @param string $appSecret The App Secret + * * @return BaseFacebook */ public function setAppSecret($appSecret) { @@ -284,7 +317,9 @@ public function setAppSecret($appSecret) { * Get the App Secret. * * @return string the App Secret - * @deprecated + * + * @deprecated Use getAppSecret instead. + * @see getAppSecret() */ public function getApiSecret() { return $this->getAppSecret(); @@ -303,6 +338,7 @@ public function getAppSecret() { * Set the file upload support status. * * @param boolean $fileUploadSupport The file upload support status. + * * @return BaseFacebook */ public function setFileUploadSupport($fileUploadSupport) { @@ -320,11 +356,12 @@ public function getFileUploadSupport() { } /** - * DEPRECATED! Please use getFileUploadSupport instead. - * * Get the file upload support status. * * @return boolean true if and only if the server supports file upload. + * + * @deprecated Use getFileUploadSupport instead. + * @see getFileUploadSupport() */ public function useFileUploadSupport() { return $this->getFileUploadSupport(); @@ -336,6 +373,7 @@ public function useFileUploadSupport() { * to use it. * * @param string $access_token an access token. + * * @return BaseFacebook */ public function setAccessToken($access_token) { @@ -488,9 +526,10 @@ protected function getUserAccessToken() { */ public function getSignedRequest() { if (!$this->signedRequest) { - if (!empty($_REQUEST['signed_request'])) { + if ($this->allowSignedRequest && !empty($_REQUEST['signed_request'])) { $this->signedRequest = $this->parseSignedRequest( - $_REQUEST['signed_request']); + $_REQUEST['signed_request'] + ); } else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) { $this->signedRequest = $this->parseSignedRequest( $_COOKIE[$this->getSignedRequestCookieName()]); @@ -529,6 +568,11 @@ protected function getUserFromAvailableData() { if ($signed_request) { if (array_key_exists('user_id', $signed_request)) { $user = $signed_request['user_id']; + + if($user != $this->getPersistentData('user_id')){ + $this->clearAllPersistentData(); + } + $this->setPersistentData('user_id', $signed_request['user_id']); return $user; } @@ -584,11 +628,15 @@ public function getLoginUrl($params=array()) { return $this->getUrl( 'www', 'dialog/oauth', - array_merge(array( - 'client_id' => $this->getAppId(), - 'redirect_uri' => $currentUrl, // possibly overwritten - 'state' => $this->state), - $params)); + array_merge( + array( + 'client_id' => $this->getAppId(), + 'redirect_uri' => $currentUrl, // possibly overwritten + 'state' => $this->state, + 'sdk' => 'php-sdk-'.self::VERSION + ), + $params + )); } /** @@ -611,31 +659,6 @@ public function getLogoutUrl($params=array()) { ); } - /** - * Get a login status URL to fetch the status from Facebook. - * - * The parameters: - * - ok_session: the URL to go to if a session is found - * - no_session: the URL to go to if the user is not connected - * - no_user: the URL to go to if the user is not signed into facebook - * - * @param array $params Provide custom parameters - * @return string The URL for the logout flow - */ - public function getLoginStatusUrl($params=array()) { - return $this->getUrl( - 'www', - 'extern/login_status.php', - array_merge(array( - 'api_key' => $this->getAppId(), - 'no_session' => $this->getCurrentUrl(), - 'no_user' => $this->getCurrentUrl(), - 'ok_session' => $this->getCurrentUrl(), - 'session_version' => 3, - ), $params) - ); - } - /** * Make an API call. * @@ -664,7 +687,7 @@ protected function getSignedRequestCookieName() { } /** - * Constructs and returns the name of the coookie that potentially contain + * Constructs and returns the name of the cookie that potentially contain * metadata. The cookie is not set by the BaseFacebook class, but it may be * set by the JavaScript SDK. * @@ -683,20 +706,16 @@ protected function getMetadataCookieName() { * code could not be determined. */ protected function getCode() { - if (isset($_REQUEST['code'])) { - if ($this->state !== null && - isset($_REQUEST['state']) && - $this->state === $_REQUEST['state']) { - + if (!isset($_REQUEST['code']) || !isset($_REQUEST['state'])) { + return false; + } + if ($this->state === $_REQUEST['state']) { // CSRF state has done its job, so clear it $this->state = null; $this->clearPersistentData('state'); return $_REQUEST['code']; - } else { - self::errorLog('CSRF state token does not match one provided.'); - return false; - } } + self::errorLog('CSRF state token does not match one provided.'); return false; } @@ -727,7 +746,7 @@ protected function getUserFromAccessToken() { * @return string The application access token, useful for gathering * public information about users and applications. */ - protected function getApplicationAccessToken() { + public function getApplicationAccessToken() { return $this->appId.'|'.$this->appSecret; } @@ -752,6 +771,8 @@ protected function establishCSRFTokenState() { * either logged in to Facebook or has granted an offline access permission. * * @param string $code An authorization code. + * @param string $redirect_uri Optional redirect URI. Default null + * * @return mixed An access token exchanged for the authorization code, or * false if an access token could not be generated. */ @@ -894,9 +915,13 @@ protected function _oauthRequest($url, $params) { $params['access_token'] = $this->getAccessToken(); } + if (isset($params['access_token']) && !isset($params['appsecret_proof'])) { + $params['appsecret_proof'] = $this->getAppSecretProof($params['access_token']); + } + // json_encode all params values that are not strings foreach ($params as $key => $value) { - if (!is_string($value)) { + if (!is_string($value) && !($value instanceof CURLFile)) { $params[$key] = json_encode($value); } } @@ -904,6 +929,19 @@ protected function _oauthRequest($url, $params) { return $this->makeRequest($url, $params); } + /** + * Generate a proof of App Secret + * This is required for all API calls originating from a server + * It is a sha256 hash of the access_token made using the app secret + * + * @param string $access_token The access_token to be hashed (required) + * + * @return string The sha256 hash of the access_token + */ + protected function getAppSecretProof($access_token) { + return hash_hmac('sha256', $access_token, $this->getAppSecret()); + } + /** * Makes an HTTP request. This method can be overridden by subclasses if * developers want to do fancier things or use something other than curl to @@ -941,11 +979,13 @@ protected function makeRequest($url, $params, $ch=null) { curl_setopt_array($ch, $opts); $result = curl_exec($ch); - if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT + $errno = curl_errno($ch); + // CURLE_SSL_CACERT || CURLE_SSL_CACERT_BADFILE + if ($errno == 60 || $errno == 77) { self::errorLog('Invalid or no certificate authority found, '. 'using bundled information'); curl_setopt($ch, CURLOPT_CAINFO, - dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); + dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt'); $result = curl_exec($ch); } @@ -987,16 +1027,25 @@ protected function makeRequest($url, $params, $ch=null) { * Parses a signed_request and validates the signature. * * @param string $signed_request A signed token + * * @return array The payload inside it or null if the sig is wrong */ protected function parseSignedRequest($signed_request) { + + if (!$signed_request || strpos($signed_request, '.') === false) { + self::errorLog('Signed request was invalid!'); + return null; + } + list($encoded_sig, $payload) = explode('.', $signed_request, 2); // decode the data $sig = self::base64UrlDecode($encoded_sig); $data = json_decode(self::base64UrlDecode($payload), true); - if (strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM) { + if (!isset($data['algorithm']) + || strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM + ) { self::errorLog( 'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM); return null; @@ -1005,18 +1054,30 @@ protected function parseSignedRequest($signed_request) { // check sig $expected_sig = hash_hmac('sha256', $payload, $this->getAppSecret(), $raw = true); - if ($sig !== $expected_sig) { + + if (strlen($expected_sig) !== strlen($sig)) { self::errorLog('Bad Signed JSON signature!'); return null; } - return $data; + $result = 0; + for ($i = 0; $i < strlen($expected_sig); $i++) { + $result |= ord($expected_sig[$i]) ^ ord($sig[$i]); + } + + if ($result == 0) { + return $data; + } else { + self::errorLog('Bad Signed JSON signature!'); + return null; + } } /** * Makes a signed_request blob using the given data. * - * @param array The data array. + * @param array $data The data array. + * * @return string The signed request. */ protected function makeSignedRequest($data) { @@ -1036,7 +1097,8 @@ protected function makeSignedRequest($data) { /** * Build the URL for api given parameters. * - * @param $method String the method name. + * @param string $method The method name. + * * @return string The URL for the given parameters */ protected function getApiUrl($method) { @@ -1113,9 +1175,9 @@ protected function getApiUrl($method) { /** * Build the URL for given domain alias, path and parameters. * - * @param $name string The name of the domain - * @param $path string Optional path (without a leading slash) - * @param $params array Optional query parameters + * @param string $name The name of the domain + * @param string $path Optional path (without a leading slash) + * @param array $params Optional query parameters * * @return string The URL for the given parameters */ @@ -1134,13 +1196,26 @@ protected function getUrl($name, $path='', $params=array()) { return $url; } + /** + * Returns the HTTP Host + * + * @return string The HTTP Host + */ protected function getHttpHost() { if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { - return $_SERVER['HTTP_X_FORWARDED_HOST']; + $forwardProxies = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']); + if (!empty($forwardProxies)) { + return $forwardProxies[0]; + } } return $_SERVER['HTTP_HOST']; } + /** + * Returns the HTTP Protocol + * + * @return string The HTTP Protocol + */ protected function getHttpProtocol() { if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { @@ -1162,7 +1237,9 @@ protected function getHttpProtocol() { } /** - * Get the base domain used for the cookie. + * Returns the base domain used for the cookie. + * + * @return string The base domain */ protected function getBaseDomain() { // The base domain is stored in the metadata cookie if not we fallback @@ -1175,8 +1252,6 @@ protected function getBaseDomain() { return $this->getHttpHost(); } - /** - /** * Returns the Current URL, stripping it of known FB parameters that should * not persist. @@ -1223,13 +1298,14 @@ protected function getCurrentUrl() { * params that should be stripped out. * * @param string $param A key or key/value pair within a URL's query (e.g. - * 'foo=a', 'foo=', or 'foo'. + * 'foo=a', 'foo=', or 'foo'. * * @return boolean */ protected function shouldRetainParam($param) { foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { - if (strpos($param, $drop_query_param.'=') === 0) { + if ($param === $drop_query_param || + strpos($param, $drop_query_param.'=') === 0) { return false; } } @@ -1242,7 +1318,7 @@ protected function shouldRetainParam($param) { * because the access token is no longer valid. If that is * the case, then we destroy the session. * - * @param $result array A record storing the error message returned + * @param array $result A record storing the error message returned * by a failed API call. */ protected function throwAPIException($result) { @@ -1291,8 +1367,9 @@ protected static function errorLog($msg) { * _ instead of / * No padded = * - * @param string $input base64UrlEncoded string - * @return string + * @param string $input base64UrlEncoded input + * + * @return string The decoded string */ protected static function base64UrlDecode($input) { return base64_decode(strtr($input, '-_', '+/')); @@ -1304,8 +1381,8 @@ protected static function base64UrlDecode($input) { * - instead of + * _ instead of / * - * @param string $input string - * @return string base64Url encoded string + * @param string $input The input to encode + * @return string The base64Url encoded input, as a string. */ protected static function base64UrlEncode($input) { $str = strtr(base64_encode($input), '+/', '-_'); @@ -1345,7 +1422,7 @@ public function destroySession() { /** * Parses the metadata cookie that our Javascript API set * - * @return an array mapping key to value + * @return array an array mapping key to value */ protected function getMetadataCookie() { $cookie_name = $this->getMetadataCookieName(); @@ -1373,6 +1450,14 @@ protected function getMetadataCookie() { return $metadata; } + /** + * Finds whether the given domain is allowed or not + * + * @param string $big The value to be checked against $small + * @param string $small The input string + * + * @return boolean Returns TRUE if $big matches $small + */ protected static function isAllowedDomain($big, $small) { if ($big === $small) { return true; @@ -1380,6 +1465,14 @@ protected static function isAllowedDomain($big, $small) { return self::endsWith($big, '.'.$small); } + /** + * Checks if $big string ends with $small string + * + * @param string $big The value to be checked against $small + * @param string $small The input string + * + * @return boolean TRUE if $big ends with $small + */ protected static function endsWith($big, $small) { $len = strlen($small); if ($len === 0) { @@ -1423,6 +1516,7 @@ abstract protected function getPersistentData($key, $default = false); * Clear the data with $key from the persistent storage * * @param string $key + * * @return void */ abstract protected function clearPersistentData($key); diff --git a/src/facebook.php b/src/facebook.php index a2238ef6..b6b827dc 100644 --- a/src/facebook.php +++ b/src/facebook.php @@ -23,13 +23,22 @@ */ class Facebook extends BaseFacebook { + /** + * Cookie prefix + */ const FBSS_COOKIE_NAME = 'fbss'; - // We can set this to a high number because the main session - // expiration will trump this. + /** + * We can set this to a high number because the main session + * expiration will trump this. + */ const FBSS_COOKIE_EXPIRE = 31556926; // 1 year - // Stores the shared session ID if one is set. + /** + * Stores the shared session ID if one is set. + * + * @var string + */ protected $sharedSessionID; /** @@ -38,25 +47,45 @@ class Facebook extends BaseFacebook * access token if during the course of execution * we discover them. * - * @param Array $config the application configuration. Additionally + * @param array $config the application configuration. Additionally * accepts "sharedSession" as a boolean to turn on a secondary * cookie for environments with a shared session (that is, your app * shares the domain with other apps). - * @see BaseFacebook::__construct in facebook.php + * + * @see BaseFacebook::__construct */ public function __construct($config) { - if (!session_id()) { + if ((function_exists('session_status') + && session_status() !== PHP_SESSION_ACTIVE) || !session_id()) { session_start(); } parent::__construct($config); if (!empty($config['sharedSession'])) { $this->initSharedSession(); + + // re-load the persisted state, since parent + // attempted to read out of non-shared cookie + $state = $this->getPersistentData('state'); + if (!empty($state)) { + $this->state = $state; + } else { + $this->state = null; + } + } } + /** + * Supported keys for persistent data + * + * @var array + */ protected static $kSupportedKeys = array('state', 'code', 'access_token', 'user_id'); + /** + * Initiates Shared Session + */ protected function initSharedSession() { $cookie_name = $this->getSharedSessionCookieName(); if (isset($_COOKIE[$cookie_name])) { @@ -95,10 +124,16 @@ protected function initSharedSession() { /** * Provides the implementations of the inherited abstract - * methods. The implementation uses PHP sessions to maintain + * methods. The implementation uses PHP sessions to maintain * a store for authorization codes, user ids, CSRF states, and * access tokens. */ + + /** + * {@inheritdoc} + * + * @see BaseFacebook::setPersistentData() + */ protected function setPersistentData($key, $value) { if (!in_array($key, self::$kSupportedKeys)) { self::errorLog('Unsupported key passed to setPersistentData.'); @@ -109,6 +144,11 @@ protected function setPersistentData($key, $value) { $_SESSION[$session_var_name] = $value; } + /** + * {@inheritdoc} + * + * @see BaseFacebook::getPersistentData() + */ protected function getPersistentData($key, $default = false) { if (!in_array($key, self::$kSupportedKeys)) { self::errorLog('Unsupported key passed to getPersistentData.'); @@ -120,6 +160,11 @@ protected function getPersistentData($key, $default = false) { $_SESSION[$session_var_name] : $default; } + /** + * {@inheritdoc} + * + * @see BaseFacebook::clearPersistentData() + */ protected function clearPersistentData($key) { if (!in_array($key, self::$kSupportedKeys)) { self::errorLog('Unsupported key passed to clearPersistentData.'); @@ -127,9 +172,16 @@ protected function clearPersistentData($key) { } $session_var_name = $this->constructSessionVariableName($key); - unset($_SESSION[$session_var_name]); + if (isset($_SESSION[$session_var_name])) { + unset($_SESSION[$session_var_name]); + } } + /** + * {@inheritdoc} + * + * @see BaseFacebook::clearAllPersistentData() + */ protected function clearAllPersistentData() { foreach (self::$kSupportedKeys as $key) { $this->clearPersistentData($key); @@ -139,6 +191,9 @@ protected function clearAllPersistentData() { } } + /** + * Deletes Shared session cookie + */ protected function deleteSharedSessionCookie() { $cookie_name = $this->getSharedSessionCookieName(); unset($_COOKIE[$cookie_name]); @@ -146,10 +201,23 @@ protected function deleteSharedSessionCookie() { setcookie($cookie_name, '', 1, '/', '.'.$base_domain); } + /** + * Returns the Shared session cookie name + * + * @return string The Shared session cookie name + */ protected function getSharedSessionCookieName() { return self::FBSS_COOKIE_NAME . '_' . $this->getAppId(); } + /** + * Constructs and returns the name of the session key. + * + * @see setPersistentData() + * @param string $key The key for which the session variable name to construct. + * + * @return string The name of the session key. + */ protected function constructSessionVariableName($key) { $parts = array('fb', $this->getAppId(), $key); if ($this->sharedSessionID) { diff --git a/tests/tests.php b/tests/tests.php index fcdbe871..fa1d83a7 100644 --- a/tests/tests.php +++ b/tests/tests.php @@ -22,18 +22,20 @@ class PHPSDKTestCase extends PHPUnit_Framework_TestCase { const MIGRATED_APP_ID = '174236045938435'; const MIGRATED_SECRET = '0073dce2d95c4a5c2922d1827ea0cca6'; - const TEST_USER = 499834690; + const TEST_USER = 499834690; + const TEST_USER_2 = 499835484; private static $kExpiredAccessToken = 'AAABrFmeaJjgBAIshbq5ZBqZBICsmveZCZBi6O4w9HSTkFI73VMtmkL9jLuWsZBZC9QMHvJFtSulZAqonZBRIByzGooCZC8DWr0t1M4BL9FARdQwPWPnIqCiFQ'; - private static function kValidSignedRequest() { + private static function kValidSignedRequest($id = self::TEST_USER, $oauth_token = null) { $facebook = new FBPublic(array( 'appId' => self::APP_ID, 'secret' => self::SECRET, )); return $facebook->publicMakeSignedRequest( array( - 'user_id' => self::TEST_USER, + 'user_id' => $id, + 'oauth_token' => $oauth_token ) ); } @@ -321,7 +323,45 @@ public function testGetCodeWithMissingCSRFState() { // intentionally don't set CSRF token at all $this->assertFalse($facebook->publicGetCode(), 'Expect getCode to fail, CSRF state not sent back.'); + } + + public function testPersistentCSRFState() + { + $facebook = new FBCode(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + )); + $facebook->setCSRFStateToken(); + $code = $facebook->getCSRFStateToken(); + $facebook = new FBCode(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + )); + + $this->assertEquals($code, $facebook->publicGetState(), + 'Persisted CSRF state token not loaded correctly'); + } + + public function testPersistentCSRFStateWithSharedSession() + { + $_SERVER['HTTP_HOST'] = 'fbrell.com'; + $facebook = new FBCode(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + 'sharedSession' => true, + )); + $facebook->setCSRFStateToken(); + $code = $facebook->getCSRFStateToken(); + + $facebook = new FBCode(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + 'sharedSession' => true, + )); + + $this->assertEquals($code, $facebook->publicGetState(), + 'Persisted CSRF state token not loaded correctly with shared session'); } public function testGetUserFromSignedRequest() { @@ -335,6 +375,47 @@ public function testGetUserFromSignedRequest() { 'Failed to get user ID from a valid signed request.'); } + public function testDisallowSignedRequest() { + $facebook = new TransientFacebook(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + 'allowSignedRequest' => false + )); + + $_REQUEST['signed_request'] = self::kValidSignedRequest(); + $this->assertEquals(0, $facebook->getUser(), + 'Should not have received valid user from signed_request.'); + } + + + public function testSignedRequestRewrite(){ + $facebook = new FBRewrite(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + )); + + $_REQUEST['signed_request'] = self::kValidSignedRequest(self::TEST_USER, 'Hello sweetie'); + + $this->assertEquals(self::TEST_USER, $facebook->getUser(), + 'Failed to get user ID from a valid signed request.'); + + $this->assertEquals('Hello sweetie', $facebook->getAccessToken(), + 'Failed to get access token from signed request'); + + $facebook->uncache(); + + $_REQUEST['signed_request'] = self::kValidSignedRequest(self::TEST_USER_2, 'spoilers'); + + $this->assertEquals(self::TEST_USER_2, $facebook->getUser(), + 'Failed to get user ID from a valid signed request.'); + + $_REQUEST['signed_request'] = null; + $facebook ->uncacheSignedRequest(); + + $this->assertNotEquals('Hello sweetie', $facebook->getAccessToken(), + 'Failed to clear access token'); + } + public function testGetSignedRequestFromCookie() { $facebook = new FBPublicCookie(array( 'appId' => self::APP_ID, @@ -680,37 +761,6 @@ public function testLogoutURLDefaults() { $this->assertFalse(strpos($facebook->getLogoutUrl(), self::SECRET)); } - public function testLoginStatusURLDefaults() { - $_SERVER['HTTP_HOST'] = 'fbrell.com'; - $_SERVER['REQUEST_URI'] = '/examples'; - $facebook = new TransientFacebook(array( - 'appId' => self::APP_ID, - 'secret' => self::SECRET, - )); - $encodedUrl = rawurlencode('http://fbrell.com/examples'); - $this->assertNotNull(strpos($facebook->getLoginStatusUrl(), $encodedUrl), - 'Expect the current url to exist.'); - } - - public function testLoginStatusURLCustom() { - $_SERVER['HTTP_HOST'] = 'fbrell.com'; - $_SERVER['REQUEST_URI'] = '/examples'; - $facebook = new TransientFacebook(array( - 'appId' => self::APP_ID, - 'secret' => self::SECRET, - )); - $encodedUrl1 = rawurlencode('http://fbrell.com/examples'); - $okUrl = 'http://fbrell.com/here1'; - $encodedUrl2 = rawurlencode($okUrl); - $loginStatusUrl = $facebook->getLoginStatusUrl(array( - 'ok_session' => $okUrl, - )); - $this->assertNotNull(strpos($loginStatusUrl, $encodedUrl1), - 'Expect the current url to exist.'); - $this->assertNotNull(strpos($loginStatusUrl, $encodedUrl2), - 'Expect the custom url to exist.'); - } - public function testNonDefaultPort() { $_SERVER['HTTP_HOST'] = 'fbrell.com:8080'; $_SERVER['REQUEST_URI'] = '/examples'; @@ -761,10 +811,11 @@ public function testSignedToken() { 'appId' => self::APP_ID, 'secret' => self::SECRET )); - $payload = $facebook->publicParseSignedRequest(self::kValidSignedRequest()); + $sr = self::kValidSignedRequest(); + $payload = $facebook->publicParseSignedRequest($sr); $this->assertNotNull($payload, 'Expected token to parse'); $this->assertEquals($facebook->getSignedRequest(), null); - $_REQUEST['signed_request'] = self::kValidSignedRequest(); + $_REQUEST['signed_request'] = $sr; $this->assertEquals($facebook->getSignedRequest(), $payload); } @@ -1276,12 +1327,42 @@ public function testMissingAccessTokenInCodeExchangeIsIgnored() { $this->assertFalse($stub->publicGetAccessTokenFromCode('c', '')); } + public function testAppsecretProofNoParams() { + $fb = new FBRecordMakeRequest(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + )); + $token = $fb->getAccessToken(); + $proof = $fb->publicGetAppSecretProof($token); + $params = array(); + $fb->api('/mattynoce', $params); + $requests = $fb->publicGetRequests(); + $this->assertEquals($proof, $requests[0]['params']['appsecret_proof']); + } + + public function testAppsecretProofWithParams() { + $fb = new FBRecordMakeRequest(array( + 'appId' => self::APP_ID, + 'secret' => self::SECRET, + )); + $proof = 'foo'; + $params = array('appsecret_proof' => $proof); + $fb->api('/mattynoce', $params); + $requests = $fb->publicGetRequests(); + $this->assertEquals($proof, $requests[0]['params']['appsecret_proof']); + } + public function testExceptionConstructorWithErrorCode() { $code = 404; $e = new FacebookApiException(array('error_code' => $code)); $this->assertEquals($code, $e->getCode()); } + public function testExceptionConstructorWithInvalidErrorCode() { + $e = new FacebookApiException(array('error_code' => 'not an int')); + $this->assertEquals(0, $e->getCode()); + } + // this happens often despite the fact that it is useless public function testExceptionTypeFalse() { $e = new FacebookApiException(false); @@ -1862,6 +1943,10 @@ protected function makeRequest($url, $params, $ch=null) { public function publicGetRequests() { return $this->requests; } + + public function publicGetAppSecretProof($access_token) { + return $this->getAppSecretProof($access_token); + } } class FBPublic extends TransientFacebook { @@ -1930,6 +2015,10 @@ public function publicGetCode() { return $this->getCode(); } + public function publicGetState() { + return $this->state; + } + public function setCSRFStateToken() { $this->establishCSRFTokenState(); } @@ -1969,6 +2058,20 @@ public function publicGetMetadataCookieName() { } } +class FBRewrite extends Facebook{ + + public function uncacheSignedRequest(){ + $this->signedRequest = null; + } + + public function uncache() + { + $this->user = null; + $this->signedRequest = null; + $this->accessToken = null; + } +} + class FBPublicGetAccessTokenFromCode extends TransientFacebook { public function publicGetAccessTokenFromCode($code, $redirect_uri = null) {