Rewrite a substantial portion of the facebook code.
authorMichael J. Rubinsky <mrubinsk@horde.org>
Mon, 23 Feb 2009 02:22:25 +0000 (21:22 -0500)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Mon, 23 Feb 2009 02:23:55 +0000 (21:23 -0500)
In the process, break out the Auth related functionality into it's
own class.

framework/Service_Facebook/lib/Horde/Service/Facebook.php
framework/Service_Facebook/lib/Horde/Service/Facebook/Auth.php [new file with mode: 0644]
framework/Service_Facebook/lib/Horde/Service/Facebook/Request.php
framework/Service_Facebook/package.xml

index 7d5001f..57ac429 100644 (file)
@@ -62,12 +62,8 @@ class Horde_Service_Facebook
      */
     public $secret;
 
-    // Used since we are emulating a FB Desktop Application - since we are not
-    // being used within the context of a FB Canvas.
-    protected  $_app_secret;
-
-    // Flag used internally to help with authentication
-    protected $_verify_sig = false;
+    // The token returned by auth.createToken
+    protected $_token;
 
     // Store the current session_key
     public $session_key;
@@ -85,9 +81,8 @@ class Horde_Service_Facebook
     public $use_ssl_resources = false;
     private $_batchRequest;
 
-    protected $_base_domain;
-    protected $_call_as_apikey;
 
+    protected $_logger;
 
     /**
      *
@@ -107,6 +102,11 @@ class Horde_Service_Facebook
      */
     protected $_context;
 
+
+    // TODO: Implement some kind of instance array for these types of classes...
+    protected $_auth = null; // H_S_Facebook_Auth
+
+
     const API_VALIDATION_ERROR = 1;
     const REST_SERVER_ADDR = 'http://api.facebook.com/restserver.php';
 
@@ -120,6 +120,10 @@ class Horde_Service_Facebook
      *      http_client - required
      *      http_response - required
      *      login_redirect_callback - optional
+     *      logger
+     *      no_resolve - set to true to prevent attempting to obtain a session
+     *                   from an auth_token. Useful if client code wants to
+     *                   handle this.
      *
      * @param session_key
      */
@@ -132,22 +136,22 @@ class Horde_Service_Facebook
             $this->_http = $context['http_client'];
         }
 
-        // Horde_Controller_Request object
+        // Required Horde_Controller_Request object
         if (empty($context['http_request'])) {
             throw new InvalidArgumentException('A http request object is required');
         } else {
             $this->_request = $context['http_request'];
         }
 
+        // Optional Horde_Log_Logger
+        if (!empty($context['logger'])) {
+            $this->_logger = $context['logger'];
+        }
+
+        $this->_logDebug('Initializing Horde_Service_Facebook');
+
         $this->api_key = $api_key;
         $this->secret = $secret;
-        $this->_app_secret = $secret;
-        $this->validate_fb_params();
-        $this->_call_as_apikey = '';
-
-        // Set the default user id for methods that allow the caller to
-        // pass an explicit uid instead of using a session key.
-        $this->user = !empty($this->user) ? $this->user : null;
 
         if (!empty($context['use_ssl'])) {
             $this->useSslResources = true;
@@ -158,138 +162,24 @@ class Horde_Service_Facebook
     }
 
     /**
-     * Validates that the parameters passed in were sent from Facebook. It does so
-     * by validating that the signature matches one that could only be generated
-     * by using your application's secret key.
-     *
-     * Facebook-provided parameters will come from $_POST, $_GET, or $_COOKIE,
-     * in that order. $_POST and $_GET are always more up-to-date than cookies,
-     * so we prefer those if they are available.
-     *
-     * For nitty-gritty details of when each of these is used, check out
-     * http://wiki.developers.facebook.com/index.php/Verifying_The_Signature
-     *
-     * @param bool  resolve_auth_token  convert an auth token into a session
+     * Initialize the object - check to see if we have a valid FB
+     * session, verify the signature etc...
      */
-    public function validate_fb_params($resolve_auth_token = true)
+    public function validateSession()
     {
-        // Prefer $_POST data - but if absent, try $_GET and $_POST with
-        // 'fb_post_sig' since that might be returned by FQL queries.
-        $post = $this->_request->getPostParams();
-        $get = $this->_request->getGetParams();
-
-        $this->fb_params = $this->get_valid_fb_params($post, 48 * 3600, 'fb_sig');
-        if (!$this->fb_params) {
-            $fb_params = $this->get_valid_fb_params($get, 48 * 3600, 'fb_sig');
-            $fb_post_params = $this->get_valid_fb_params($post, 48 * 3600, 'fb_post_sig');
-            $this->fb_params = array_merge($fb_params, $fb_post_params);
-        }
-
-        if ($this->fb_params) {
-            $user = isset($this->fb_params['user']) ? $this->fb_params['user'] : null;
-            $this->_base_domain  = isset($this->fb_params['base_domain']) ? $this->fb_params['base_domain'] : null;
-            if (isset($this->fb_params['session_key'])) {
-                $session_key = $this->fb_params['session_key'];
-            } elseif (isset($this->fb_params['profile_session_key'])) {
-                $session_key = $this->fb_params['profile_session_key'];
-            } else {
-                $session_key = null;
-            }
-            $expires = isset($this->fb_params['expires']) ? $this->fb_params['expires'] : null;
-            $this->set_user($user, $session_key, $expires);
-        } elseif ($cookies = $this->get_valid_fb_params($this->_request->getCookie(), null, $this->api_key)) {
-            $base_domain_cookie = 'base_domain_' . $this->api_key;
-            if ($this->_request->getCookie($base_domain_cookie)) {
-                $this->_base_domain = $this->_request->getCookie($base_domain_cookie);
-            }
-            // use $api_key . '_' as a prefix for the cookies in case there are
-            // multiple facebook clients on the same domain.
-            $expires = isset($cookies['expires']) ? $cookies['expires'] : null;
-            $this->set_user($cookies['user'], $cookies['session_key'], $expires);
-        } elseif ($resolve_auth_token && isset($get['auth_token']) &&
-                  $session = $this->do_get_session($get['auth_token'])) {
-
-            if (isset($session['base_domain'])) {
-                $this->_base_domain = $session['base_domain'];
-            }
-
-            $this->set_user($session['uid'],
-                            $session['session_key'],
-                            $session['expires']);
-        }
-
-        return !empty($this->fb_params);
+        return $this->auth->validateSession(empty($this->_context['no_resolve']));
     }
 
-    /**
-     * Return the session information
-     *
-     * @param $auth_token
-     * @return unknown_type
-     */
-    protected function _do_get_session($auth_token)
+    // Lazy loader
+    public function __get($value)
     {
-        try {
-            return $this->auth_getSession($auth_token);
-        } catch (Horde_Service_Facebook_Exception $e) {
-            // API_EC_PARAM means we don't have a logged in user, otherwise who
-            // knows what it means, so just throw it.
-            if ($e->getCode() != Horde_Service_Facebook_ErrorCodes::API_EC_PARAM) {
-                throw $e;
+        // TODO: Some kind of array/hash to hold valid types - maybe a
+        // factory method to instantiate these?
+        if ($value == 'auth') {
+            if (empty($this->_auth)) {
+                $this->_auth = new Horde_Service_Facebook_Auth($this, $this->_request);
             }
-        }
-    }
-
-    /**
-     * Get authenticated session information for a "desktop" app.
-     *
-     * @param $auth_token
-     * @return unknown_type
-     */
-    public function do_get_session($auth_token)
-    {
-        $this->secret = $this->_app_secret;
-        $this->session_key = null;
-        $session_info = $this->_do_get_session($auth_token);
-        if (!empty($session_info['secret'])) {
-            // store the session secret
-            $this->set_session_secret($session_info['secret']);
-        }
-
-        return $session_info;
-    }
-
-    public function set_session_secret($session_secret)
-    {
-        $this->secret = $session_secret;
-    }
-
-    /**
-     * Invalidate the session currently being used, and clear any state
-     * associated with it.
-     *
-     * TODO: This calls setcookie() so need to ensure we aren't called after
-     * headers are sent or throw an exception.
-     */
-    public function expire_session()
-    {
-        if ($this->auth_expireSession()) {
-            if (!$this->in_fb_canvas() && $this->_request->getCookie($this->api_key . '_user')) {
-                $cookies = array('user', 'session_key', 'expires', 'ss');
-                foreach ($cookies as $name) {
-                    setcookie($this->api_key . '_' . $name, false, time() - 3600);
-                }
-                setcookie($this->api_key, false, time() - 3600);
-            }
-
-            // now, clear the rest of the stored state
-            $this->user = 0;
-            $this->session_key = 0;
-
-            return true;
-        } else {
-
-            return false;
+            return $this->_auth;
         }
     }
 
@@ -320,16 +210,6 @@ class Horde_Service_Facebook
     }
 
     /**
-     * Getter for session user.
-     *
-     * @return string  The FB userid
-     */
-    public function get_loggedin_user()
-    {
-        return $this->user;
-    }
-
-    /**
      * Return the current request's url
      *
      * @return string
@@ -340,49 +220,11 @@ class Horde_Service_Facebook
     }
 
     /**
-     * Ensure the user is logged in and has a current FB session. If not,
-     * attempt to redirect to a FB login page.
-     *
-     * @return void
-     */
-    public function require_login()
-    {
-        if ($this->get_loggedin_user()) {
-            try {
-                // try a session-based API call to ensure that we have the correct
-                // session secret
-                $user = $this->users_getLoggedInUser();
-
-                // now that we have a valid session secret, verify the signature
-                $this->_verify_sig = true;
-                if ($this->validate_fb_params(false)) {
-                    return $user;
-                } else {
-                    // validation failed
-                    return null;
-                }
-            } catch (Horde_Service_Facebook_Exception $ex) {
-                $get = $this->_request->getGetParams();
-                if (isset($get['auth_token'])) {
-                    // if we have an auth_token, use it to establish a session
-                    $session_info = $this->do_get_session($get['auth_token']);
-                    if ($session_info) {
-                      return $session_info['uid'];
-                    }
-                }
-            }
-        }
-
-        // if we get here, we need to redirect the user to log in
-        $this->_redirect($this->_get_login_url(self::_current_url()));
-    }
-
-    /**
      * Helper function to get the appropriate facebook url
      *
      * @return string
      */
-    protected static function _get_facebook_url($subdomain = 'www')
+    public static function get_facebook_url($subdomain = 'www')
     {
         return 'http://' . $subdomain . '.facebook.com';
     }
@@ -392,164 +234,13 @@ class Horde_Service_Facebook
      *
      *  @return string
      */
-    protected function _get_login_url($next)
+    public function get_login_url($next)
     {
-        return self::_get_facebook_url() . '/login.php?v=1.0&api_key='
+        return self::get_facebook_url() . '/login.php?v=1.0&api_key='
             . $this->api_key . ($next ? '&next=' . urlencode($next)  : '');
     }
 
     /**
-     * Set the current session user
-     *
-     * @param string $user  The FB userid
-     * @param string $session_key
-     * @param timestamp $expires
-     *
-     * @return void
-     */
-    public function set_user($user, $session_key, $expires = null)
-    {
-        if (!$this->_request->getCookie($this->api_key . '_user') ||
-            $this->_request->getCookie($this->api_key . '_user') != $user) {
-
-            $this->set_cookies($user, $session_key, $expires);
-        }
-        $this->user = $user;
-        $this->session_key = $session_key;
-        $this->_session_expires = $expires;
-    }
-
-    /**
-     * Set session cookies (Facebook *requires* cookies to be enabled).
-     *
-     * @param string $user  FB userid
-     * @param string $session_key  The current session key
-     * @param timestamp $expires
-     *
-     * @return void
-     */
-    public function set_cookies($user, $session_key, $expires = null)
-    {
-        $cookies = array();
-        $cookies['user'] = $user;
-        $cookies['session_key'] = $session_key;
-        if ($expires != null) {
-            $cookies['expires'] = $expires;
-        }
-        foreach ($cookies as $name => $val) {
-            setcookie($this->api_key . '_' . $name, $val, (int)$expires, '', $this->_base_domain);
-        }
-        $sig = self::generate_sig($cookies, $this->secret);
-        setcookie($this->api_key, $sig, (int)$expires, '', $this->_base_domain);
-        if ($this->_base_domain != null) {
-            $base_domain_cookie = 'base_domain_' . $this->api_key;
-            setcookie($base_domain_cookie, $this->_base_domain, (int)$expires, '', $this->_base_domain);
-        }
-    }
-
-    /**
-     * Get the signed parameters that were sent from Facebook. Validates the set
-     * of parameters against the included signature.
-     *
-     * Since Facebook sends data to your callback URL via unsecured means, the
-     * signature is the only way to make sure that the data actually came from
-     * Facebook. So if an app receives a request at the callback URL, it should
-     * always verify the signature that comes with against your own secret key.
-     * Otherwise, it's possible for someone to spoof a request by
-     * pretending to be someone else, i.e.:
-     *      www.your-callback-url.com/?fb_user=10101
-     *
-     * This is done automatically by verify_fb_params.
-     *
-     * @param array $params       A hash of all external parameters.
-     * @param int $timeout        Number of seconds that the args are good for.
-     * @param string $namespace   Prefix string for the set of parameters we
-     *                            want to verify(fb_sig or fb_post_sig).
-     *
-     * @return array  The subset of parameters containing the given prefix,
-     *                and also matching the signature associated with them or an
-     *                empty array if the signature did not match.
-     */
-    public function get_valid_fb_params($params, $timeout = null, $namespace = 'fb_sig')
-    {
-        $prefix = $namespace . '_';
-        $prefix_len = strlen($prefix);
-        $fb_params = array();
-        if (empty($params)) {
-            return array();
-        }
-
-        foreach ($params as $name => $val) {
-            // pull out only those parameters that match the prefix
-            // note that the signature itself ($params[$namespace]) is not in the list
-            if (strpos($name, $prefix) === 0) {
-                $fb_params[substr($name, $prefix_len)] = $val;
-            }
-        }
-
-        // validate that the request hasn't expired. this is most likely
-        // for params that come from $_COOKIE
-        if ($timeout && (!isset($fb_params['time']) || time() - $fb_params['time'] > $timeout)) {
-          return array();
-        }
-
-        // validate that the params match the signature
-        $signature = isset($params[$namespace]) ? $params[$namespace] : null;
-        if (!$signature || (!$this->_verify_signature($fb_params, $signature))) {
-            return array();
-        }
-
-        return $fb_params;
-    }
-
-    /**
-     * Validates that a given set of parameters match their signature.
-     * Parameters all match a given input prefix, such as "fb_sig".
-     *
-     * @param array $fb_params  An array of all Facebook-sent parameters, not
-     *                          including the signature itself.
-     * @param $expected_sig     The expected result to check against.
-     *
-     * @return boolean
-     */
-    protected function _verify_signature($fb_params, $expected_sig)
-    {
-        // we don't want to verify the signature until we have a valid
-        // session secret
-        if ($this->_verify_sig) {
-            return self::generate_sig($fb_params, $this->secret) == $expected_sig;
-        } else {
-            return true;
-        }
-    }
-
-    /**
-     * Generate a signature using the application secret key.
-     *
-     * The only two entities that know your secret key are you and Facebook,
-     * according to the Terms of Service. Since nobody else can generate
-     * the signature, you can rely on it to verify that the information
-     * came from Facebook.
-     *
-     * @param array $params   An array of all Facebook-sent parameters, NOT
-     *                        INCLUDING the signature itself.
-     * @param string $secret  The application's secret key.
-     *
-     * @return string  Hash to be checked against the FB provided signature.
-     */
-    public static function generate_sig($params_array, $secret)
-    {
-        $str = '';
-        ksort($params_array);
-        foreach ($params_array as $k => $v) {
-            $str .= "$k=$v";
-        }
-        $str .= $secret;
-
-        return md5($str);
-    }
-
-    /**
      * Start a batch operation.
      */
     public function batchBegin()
@@ -578,52 +269,6 @@ class Horde_Service_Facebook
         $this->_batchRequest = null;
     }
 
-    /**
-     * Creates an authentication token to be used as part of the desktop login
-     * flow.  For more information, please see
-     * http://wiki.developers.facebook.com/index.php/Auth.createToken.
-     *
-     * @return string  An authentication token.
-     */
-    public function auth_createToken()
-    {
-        return $this->call_method('facebook.auth.createToken');
-    }
-
-    /**
-     * Returns the session information available after current user logs in.
-     *
-     * @param string $auth_token             the token returned by
-     *                                       auth_createToken or passed back to
-     *                                       your callback_url.
-     *
-     * @return array  An assoc array containing session_key, uid
-     */
-    public function auth_getSession($auth_token)
-    {
-        //Check if we are in batch mode
-        if ($this->_batchRequest === null) {
-            $result = $this->call_method('facebook.auth.getSession', array('auth_token' => $auth_token));
-            $this->session_key = $result['session_key'];
-            if (!empty($result['secret'])) {
-                // desktop apps have a special secret
-                $this->secret = $result['secret'];
-            }
-            return $result;
-        }
-    }
-
-    /**
-     * Expires the session that is currently being used.  If this call is
-     * successful, no further calls to the API (which require a session) can be
-     * made until a valid session is created.
-     *
-     * @return bool  true if session expiration was successful, false otherwise
-     */
-    public function auth_expireSession()
-    {
-        return $this->call_method('facebook.auth.expireSession');
-    }
 
     /**
      * Returns events according to the filters specified.
@@ -1177,7 +822,7 @@ class Horde_Service_Facebook
         return $this->call_upload_method('facebook.video.upload',
             array('title' => $title,
                   'description' => $description),
-            $file, self::_get_facebook_url('api-video') . '/restserver.php');
+            $file, self::get_facebook_url('api-video') . '/restserver.php');
     }
 
     /**
@@ -1429,4 +1074,18 @@ class Horde_Service_Facebook
         return $this->run_http_post_transaction($content_type, $content, $server_addr);
     }
 
+    protected function _logDebug($message)
+    {
+        if (!empty($this->_logger)) {
+            $this->_logger->debug($message);
+        }
+    }
+
+    protected function _logErr($message)
+    {
+        if (!empty($this->_logger)) {
+            $this->_logger->err($message);
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/framework/Service_Facebook/lib/Horde/Service/Facebook/Auth.php b/framework/Service_Facebook/lib/Horde/Service/Facebook/Auth.php
new file mode 100644 (file)
index 0000000..a8f4b97
--- /dev/null
@@ -0,0 +1,421 @@
+<?php
+/**
+ * Horde_Service_Facebook_Auth:: wrap functionality associated with
+ * authenticating to Facebook.
+ *
+ * For now, only provide methods for authenticating that make sense from
+ * within a Horde context.
+ *
+ * Copyright 2009 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @category Horde
+ * @package Horde_Service_Facebook
+ */
+class Horde_Service_Facebook_Auth
+{
+    /**
+     *
+     * @var Horde_Service_Facebook
+     */
+    protected $_facebook;
+
+    /**
+     *
+     * @var string
+     */
+    protected $_base_domain;
+
+    /**
+     * @var string
+     */
+    protected $_sessionKey;
+
+    /**
+     * The current userid
+     *
+     * @var string
+     */
+    protected $_user;
+
+    /**
+     *
+     * @var Horde_Controller_Request
+     */
+    protected $_request;
+
+    /** EXTEND_PERMS constants **/
+    const EXTEND_PERMS_OFFLINE = 'offline_access';
+    const EXTEND_PERMS_STATUSUPDATE = 'status_update';
+    const EXTEND_PERMS_SHAREITEM = 'share_item';
+    const EXTEND_PERMS_UPLOADPHOTO = 'photo_upload';
+\
+
+    /**
+     * TODO: Probably abstract out a Base class for these?
+     * @return unknown_type
+     */
+    public function __construct($facebook, $request, $params = array())
+    {
+        $this->_facebook = $facebook;
+        $this->_request = $request;
+    }
+
+    /**
+     * Returns the URL for a user to obtain the auth token needed to generate an
+     * infinite session for a web app.
+     *
+     * http://www.sitepoint.com/article/developing-facebook-platform/
+     * http://forum.developers.facebook.com/viewtopic.php?id=20223
+     *
+     * This has been replaced by having the user grant "offline access"
+     * to the application using extended permissions.
+     *
+     * @return string
+     */
+    public function getAuthTokenUrl()
+    {
+        return $this->_facebook->get_facebook_url() . '/code_gen.php?v=1.0&api_key='
+            . $this->_facebook->api_key;
+    }
+
+
+    /**
+     * Return the URL needed for approving an extendted permission.
+     *
+     * @param string $perm         An EXTEND_PERMS_* constant
+     * @param string $success_url  URL to redirect to on success
+     * @param strint $cancel_url   URL to redirect to on cancel
+     *
+     * @return string
+     */
+    public function getExtendedPermUrl($perm, $success_url = '', $cancel_url = '')
+    {
+        return $this->_facebook->get_facebook_url() . '/authorize.php?v=1'
+            . '&ext_perm=' . $perm . '&api_key=' . $this->_facebook->api_key
+            . (!empty($success_url) ? '&next=' . urlencode($success_url) : '')
+            . (!empty($cancel_url) ? '&cancel=' . urlencode($cancel_url) : '');
+    }
+
+    /**
+     * Getter
+     *
+     * @return string  The current session key
+     */
+    public function getSessionKey()
+    {
+        return $this->_sessionKey;
+    }
+
+    /**
+     * Getter
+     *
+     * @return string  The current userid
+     */
+    public function getUser()
+    {
+        return $this->_user;
+    }
+
+    /**
+     * Returns the session information available after current user logs in.
+     *
+     * @param string $auth_token             the token
+     *
+     * @return array  An assoc array containing session_key, uid
+     */
+    public function getSession($auth_token)
+    {
+        //Check if we are in batch mode
+        if ($this->_facebook->batchRequest === null) {
+            try {
+                $results = $this->_facebook->call_method(
+                    'facebook.auth.getSession',
+                    array('auth_token' => $auth_token));
+                return $results;
+            } catch (Horde_Service_Facebook_Exception $e) {
+                if ($e->getCode() != Horde_Service_Facebook_ErrorCodes::API_EC_PARAM) {
+                    // API_EC_PARAM means we don't have a logged in user, otherwise who
+                    // knows what it means, so just throw it.
+                    throw $e;
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates an authentication token to be used as part of the desktop login
+     * flow.  For more information, please see
+     * http://wiki.developers.facebook.com/index.php/Auth.createToken.
+     *
+     * @return string  An authentication token.
+     */
+    public function createToken()
+    {
+        return $this->_facebook->call_method('facebook.auth.createToken');
+    }
+
+    /**
+     * Expires the session that is currently being used.  If this call is
+     * successful, no further calls to the API (which require a session) can be
+     * made until a valid session is created.
+     *
+     * @return bool  true if session expiration was successful, false otherwise
+     */
+    private function _expireSession()
+    {
+        return $this->_facebook->call_method('facebook.auth.expireSession');
+    }
+
+    /**
+     * Invalidate the session currently being used, and clear any state
+     * associated with it.
+     */
+    public function expireSession()
+    {
+        if ($this->_expireSession()) {
+            if ($this->_request->getCookie($this->_facebook->api_key . '_user')) {
+                $cookies = array('user', 'session_key', 'expires', 'ss');
+                foreach ($cookies as $name) {
+                    setcookie($this->_facebook->api_key . '_' . $name, false, time() - 3600);
+                }
+                setcookie($this->_facebook->api_key, false, time() - 3600);
+            }
+
+            // now, clear the rest of the stored state
+            $this->setUser(0, 0, time() - 3600);
+            return true;
+        } else {
+
+            return false;
+        }
+    }
+
+    /**
+     * Validates that the parameters passed in were sent from Facebook. It does so
+     * by validating that the signature matches one that could only be generated
+     * by using your application's secret key.
+     *
+     * Valid parameters will cause an active session to be set. Note that this
+     * will only return true if the session is NOT set manually via setUser()
+     * in client code.
+     *
+     * Facebook-provided parameters will come from $_POST, $_GET, or $_COOKIE,
+     * in that order. $_POST and $_GET are always more up-to-date than cookies,
+     * so we prefer those if they are available.
+     *
+     * For nitty-gritty details of when each of these is used, check out
+     * http://wiki.developers.facebook.com/index.php/Verifying_The_Signature
+     *
+     * @param boolean  Resolve_auth_token  convert an auth token into a session
+     * @param boolean  Should we ignore any session data present in cookies?
+     *                 (Needed when we need to make sure we are using a new
+     *                  infinite session when returned)
+     *
+     * @return boolean  Whether or not we were able to obtain the params.
+     */
+    public function validateSession($ignore_cookies = false, $resolve_auth_token = true)
+    {
+        // Prefer $_POST data - but if absent, try $_GET and $_POST with
+        // 'fb_post_sig' since that might be returned by FQL queries.
+        $post = $this->_request->getPostParams();
+        $get = $this->_request->getGetParams();
+
+        // Parse the values
+        $fb_params = $this->_getParams($post, 48 * 3600, 'fb_sig');
+        if (!$fb_params) {
+            $fb_params = $this->_getParams($get, 48 * 3600, 'fb_sig');
+            $fb_post_params = $this->_getParams($post, 48 * 3600, 'fb_post_sig');
+            $fb_params = array_merge($fb_params, $fb_post_params);
+        }
+
+        if ($fb_params) {
+            // If we have valid params, set up the session.
+            $user = isset($fb_params['user']) ? $fb_params['user'] : null;
+            $this->_base_domain  = isset($fb_params['base_domain']) ? $fb_params['base_domain'] : null;
+            if (isset($fb_params['session_key'])) {
+                $sessionKey = $fb_params['session_key'];
+            } elseif (isset($fb_params['profile_session_key'])) {
+                $sessionKey = $fb_params['profile_session_key'];
+            } else {
+                $sessionKey = null;
+            }
+            $expires = isset($fb_params['expires']) ? $fb_params['expires'] : null;
+            $this->setUser($user, $sessionKey, $expires);
+
+        } elseif (!$ignore_cookies &&
+                  $fb_params = $this->_getParams($this->_request->getCookie(), null, $this->_facebook->api_key)) {
+
+            // Nothing yet, try cookies...this is where we will get our values
+            // for an extenral web app accessing FB's API - assuming the session
+            // has already been retrieved previously.
+            $base_domain_cookie = 'base_domain_' . $this->_facebook->api_key;
+            if ($this->_request->getCookie($base_domain_cookie)) {
+                $this->_base_domain = $this->_request->getCookie($base_domain_cookie);
+            }
+            // use $api_key . '_' as a prefix for the cookies in case there are
+            // multiple facebook clients on the same domain.
+            $expires = isset($fb_params['expires']) ? $fb_params['expires'] : null;
+            $this->setUser($fb_params['user'], $fb_params['session_key'], $expires);
+
+        } elseif ($resolve_auth_token && isset($get['auth_token']) &&
+                  $session = $this->getSession($get['auth_token'])) {
+
+            if (isset($session['base_domain'])) {
+                $this->_base_domain = $session['base_domain'];
+            }
+
+            $this->setUser($session['uid'],
+                            $session['session_key'],
+                            $session['expires']);
+
+            return true;
+        }
+
+        return !empty($fb_params);
+    }
+
+    /**
+     * Get the signed parameters that were sent from Facebook. Validates the set
+     * of parameters against the included signature.
+     *
+     * Since Facebook sends data to your callback URL via unsecured means, the
+     * signature is the only way to make sure that the data actually came from
+     * Facebook. So if an app receives a request at the callback URL, it should
+     * always verify the signature that comes with against your own secret key.
+     * Otherwise, it's possible for someone to spoof a request by
+     * pretending to be someone else, i.e.:
+     *      www.your-callback-url.com/?fb_user=10101
+     *
+     * This is done automatically by verify_fb_params.
+     *
+     * @param array $params       A hash of all external parameters.
+     * @param int $timeout        Number of seconds that the args are good for.
+     * @param string $namespace   Prefix string for the set of parameters we
+     *                            want to verify(fb_sig or fb_post_sig).
+     *
+     * @return array  The subset of parameters containing the given prefix,
+     *                and also matching the signature associated with them or an
+     *                empty array if the signature did not match.
+     */
+    protected function _getParams($params, $timeout = null, $namespace = 'fb_sig')
+    {
+        $prefix = $namespace . '_';
+        $prefix_len = strlen($prefix);
+        $fb_params = array();
+        if (empty($params)) {
+            return array();
+        }
+
+        foreach ($params as $name => $val) {
+            // pull out only those parameters that match the prefix
+            // note that the signature itself ($params[$namespace]) is not in the list
+            if (strpos($name, $prefix) === 0) {
+                $fb_params[substr($name, $prefix_len)] = $val;
+            }
+        }
+
+        // validate that the request hasn't expired. this is most likely
+        // for params that come from $_COOKIE
+        if ($timeout && (!isset($fb_params['time']) || time() - $fb_params['time'] > $timeout)) {
+          return array();
+        }
+
+        // validate that the params match the signature
+        $signature = isset($params[$namespace]) ? $params[$namespace] : null;
+        if (!$signature || (!$this->validateSignature($fb_params, $signature))) {
+            return array();
+        }
+
+        return $fb_params;
+    }
+
+    /**
+     * Validates that a given set of parameters match their signature.
+     * Parameters all match a given input prefix, such as "fb_sig".
+     *
+     * @param array $fb_params  An array of all Facebook-sent parameters, not
+     *                          including the signature itself.
+     * @param $expected_sig     The expected result to check against.
+     *
+     * @return boolean
+     */
+    public function validateSignature($fb_params, $expected_sig)
+    {
+        return self::generateSignature($fb_params, $this->_facebook->secret) == $expected_sig;
+    }
+
+    /**
+     * Generate a signature using the application secret key.
+     *
+     * The only two entities that know your secret key are you and Facebook,
+     * according to the Terms of Service. Since nobody else can generate
+     * the signature, you can rely on it to verify that the information
+     * came from Facebook.
+     *
+     * @param array $params   An array of all Facebook-sent parameters, NOT
+     *                        INCLUDING the signature itself.
+     * @param string $secret  The application's secret key.
+     *
+     * @return string  Hash to be checked against the FB provided signature.
+     */
+    public static function generateSignature($params_array, $secret)
+    {
+        $str = '';
+        ksort($params_array);
+        foreach ($params_array as $k => $v) {
+            $str .= "$k=$v";
+        }
+        $str .= $secret;
+
+        return md5($str);
+    }
+    /**
+     * Set session cookies.
+     *
+     * @param string $user  FB userid
+     * @param string $session_key  The current session key
+     * @param timestamp $expires
+     *
+     * @return void
+     */
+    public function setCookies($user, $sessionKey, $expires = null)
+    {
+        $cookies = array();
+        $cookies['user'] = $user;
+        $cookies['session_key'] = $sessionKey;
+        if ($expires != null) {
+            $cookies['expires'] = $expires;
+        }
+        foreach ($cookies as $name => $val) {
+            setcookie($this->_facebook->api_key . '_' . $name, $val, (int)$expires, '', $this->_base_domain);
+        }
+        $sig = self::generateSignature($cookies, $this->_facebook->secret);
+        setcookie($this->_facebook->api_key, $sig, (int)$expires, '', $this->_base_domain);
+        if ($this->_base_domain != null) {
+            $base_domain_cookie = 'base_domain_' . $this->_facebook->api_key;
+            setcookie($base_domain_cookie, $this->_base_domain, (int)$expires, '', $this->_base_domain);
+        }
+    }
+
+    /**
+     * Set the current session user in the object and in a cookie.
+     *
+     * @param string $user  The FB userid
+     * @param string $sessionKey
+     * @param timestamp $expires
+     *
+     * @return void
+     */
+    public function setUser($user, $sessionKey, $expires = null)
+    {
+        if (!$this->_request->getCookie($this->_facebook->api_key . '_user') ||
+            $this->_request->getCookie($this->_facebook->api_key . '_user') != $user) {
+
+            $this->setCookies($user, $sessionKey, $expires);
+        }
+        $this->_user = $user;
+        $this->_sessionKey = $sessionKey;
+        $this->_session_expires = $expires;
+    }
+}
\ No newline at end of file
index 429f3af..2e8174c 100644 (file)
@@ -58,7 +58,7 @@ class Horde_Service_Facebook_Request
         $this->_addStandardParams($method, $params);
         // we need to do this before signing the params
         $this->_convertToCsv($params);
-        $params['sig'] = Horde_Service_Facebook::generate_sig($params, $this->_facebook->secret);
+        $params['sig'] = Horde_Service_Facebook_Auth::generateSignature($params, $this->_facebook->secret);
     }
 
     protected function _addStandardParams($method, &$params)
@@ -66,7 +66,9 @@ class Horde_Service_Facebook_Request
         // We only support JSON
         $params['format'] = 'json';
         $params['method'] = $method;
-        $params['session_key'] = $this->_facebook->session_key;
+        if (empty($params['session_key'])) {
+            $params['session_key'] = $this->_facebook->auth->getSessionKey();
+        }
         $params['api_key'] = $this->_facebook->api_key;
         $params['call_id'] = microtime(true);
         if ($params['call_id'] <= $this->_last_call_id) {
index 5509313..af5c098 100644 (file)
@@ -37,6 +37,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
        <file name="ErrorCodes.php" role="php" />
        <file name="Request.php" role="php" />
        <file name="BatchRequest.php" role="php" />
+       <file name="Auth.php" role="php" />
       </dir> <!-- /lib/Horde/Service/Facebook -->
       <file name="Facebook.php" role="php" />
      </dir> <!-- /lib/Horde/Service -->
@@ -64,6 +65,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
    <install name="lib/Horde/Service/Facebook/ErrorCodes.php" as="Horde/Service/Facebook/ErrorCodes.php" />
    <install name="lib/Horde/Service/Facebook/Request.php" as="Horde/Service/Facebook/Request.php" />
    <install name="lib/Horde/Service/Facebook/BatchRequest.php" as="Horde/Service/Facebook/BatchRequest.php" />
+   <install name="lib/Horde/Service/Facebook/Auth.php" as="Horde/Service/Facebook/Auth.php" />
    <install name="lib/Horde/Service/Facebook.php" as="Horde/Service/Facebook.php" />
    
   </filelist>