diff options
author | Robin Millette <millette@controlyourself.ca> | 2009-05-17 00:05:53 -0400 |
---|---|---|
committer | Robin Millette <millette@controlyourself.ca> | 2009-05-17 00:05:53 -0400 |
commit | a325144fa0ba769de1cdb554cac04e0116912b66 (patch) | |
tree | b36b33c91330cf80ffcfc090ef52be4df03c63e3 | |
parent | f3ea79a12a7bdf0d4efcd7eb706d25ae81b7fad9 (diff) | |
parent | 811cef10083f8b7824b2fd8140f940bf13b54c8d (diff) |
Merge branch '0.8.x' of git@gitorious.org:~evan/laconica/dev into 0.8.x
-rw-r--r-- | EVENTS.txt | 6 | ||||
-rw-r--r-- | actions/logout.php | 16 | ||||
-rw-r--r-- | config.php.sample | 4 | ||||
-rw-r--r-- | extlib/facebook/facebook.php | 147 | ||||
-rw-r--r-- | extlib/facebook/facebook_desktop.php | 2 | ||||
-rwxr-xr-x[-rw-r--r--] | extlib/facebook/facebookapi_php5_restlib.php | 1026 | ||||
-rw-r--r-- | lib/facebookutil.php | 15 | ||||
-rw-r--r-- | plugins/FBConnect/FBConnectLogin.php | 372 | ||||
-rw-r--r-- | plugins/FBConnect/FBConnectPlugin.php | 278 | ||||
-rw-r--r-- | plugins/FBConnect/xd_receiver.htm | 10 |
10 files changed, 1717 insertions, 159 deletions
diff --git a/EVENTS.txt b/EVENTS.txt index 5edf59245..d689f0be6 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -103,3 +103,9 @@ EndPublicGroupNav: At the end of the public group nav menu RouterInitialized: After the router instance has been initialized - $m: the Net_URL_Mapper that has just been set up +StartLogout: Before logging out +- $action: the logout action + +EndLogout: After logging out +- $action: the logout action + diff --git a/actions/logout.php b/actions/logout.php index 9f3bfe247..c34b10987 100644 --- a/actions/logout.php +++ b/actions/logout.php @@ -70,10 +70,20 @@ class LogoutAction extends Action if (!common_logged_in()) { $this->clientError(_('Not logged in.')); } else { - common_set_user(null); - common_real_login(false); // not logged in - common_forgetme(); // don't log back in! + if (Event::handle('StartLogout', array($this))) { + $this->logout(); + } + Event::handle('EndLogout', array($this)); + common_redirect(common_local_url('public'), 303); } } + + function logout() + { + common_set_user(null); + common_real_login(false); // not logged in + common_forgetme(); // don't log back in! + } + } diff --git a/config.php.sample b/config.php.sample index 6d6a9b533..0e6bf0e5f 100644 --- a/config.php.sample +++ b/config.php.sample @@ -172,6 +172,10 @@ $config['sphinx']['port'] = 3312; #$config['facebook']['apikey'] = 'APIKEY'; #$config['facebook']['secret'] = 'SECRET'; +# Facebook Connect plugin (Needs valid APIKEY above) +#require_once(INSTALLDIR.'/plugins/FBConnect/FBConnectPlugin.php'); +#$fbc = new FBConnectPlugin(); + # Add Google Analytics # require_once('plugins/GoogleAnalyticsPlugin.php'); # $ga = new GoogleAnalyticsPlugin('your secret code'); diff --git a/extlib/facebook/facebook.php b/extlib/facebook/facebook.php index 35de6be50..fee1dd086 100644 --- a/extlib/facebook/facebook.php +++ b/extlib/facebook/facebook.php @@ -1,5 +1,5 @@ <?php -// Copyright 2004-2008 Facebook. All Rights Reserved. +// Copyright 2004-2009 Facebook. All Rights Reserved. // // +---------------------------------------------------------------------------+ // | Facebook Platform PHP5 client | @@ -30,13 +30,12 @@ // +---------------------------------------------------------------------------+ // | For help with this library, contact developers-help@facebook.com | // +---------------------------------------------------------------------------+ -// + include_once 'facebookapi_php5_restlib.php'; define('FACEBOOK_API_VALIDATION_ERROR', 1); class Facebook { public $api_client; - public $api_key; public $secret; public $generate_session_secret; @@ -213,28 +212,55 @@ class Facebook { } } - // Invalidate the session currently being used, and clear any state associated with it + // Invalidate the session currently being used, and clear any state associated + // with it. Note that the user will still remain logged into Facebook. public function expire_session() { if ($this->api_client->auth_expireSession()) { - if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) { - $cookies = array('user', 'session_key', 'expires', 'ss'); - foreach ($cookies as $name) { - setcookie($this->api_key . '_' . $name, false, time() - 3600); - unset($_COOKIE[$this->api_key . '_' . $name]); - } - setcookie($this->api_key, false, time() - 3600); - unset($_COOKIE[$this->api_key]); - } - - // now, clear the rest of the stored state - $this->user = 0; - $this->api_client->session_key = 0; + $this->clear_cookie_state(); return true; } else { return false; } } + /** Logs the user out of all temporary application sessions as well as their + * Facebook session. Note this will only work if the user has a valid current + * session with the application. + * + * @param string $next URL to redirect to upon logging out + * + */ + public function logout($next) { + $logout_url = $this->get_logout_url($next); + + // Clear any stored state + $this->clear_cookie_state(); + + $this->redirect($logout_url); + } + + /** + * Clears any persistent state stored about the user, including + * cookies and information related to the current session in the + * client. + * + */ + public function clear_cookie_state() { + if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) { + $cookies = array('user', 'session_key', 'expires', 'ss'); + foreach ($cookies as $name) { + setcookie($this->api_key . '_' . $name, false, time() - 3600); + unset($_COOKIE[$this->api_key . '_' . $name]); + } + setcookie($this->api_key, false, time() - 3600); + unset($_COOKIE[$this->api_key]); + } + + // now, clear the rest of the stored state + $this->user = 0; + $this->api_client->session_key = 0; + } + public function redirect($url) { if ($this->in_fb_canvas()) { echo '<fb:redirect url="' . $url . '"/>'; @@ -249,7 +275,8 @@ class Facebook { } public function in_frame() { - return isset($this->fb_params['in_canvas']) || isset($this->fb_params['in_iframe']); + return isset($this->fb_params['in_canvas']) + || isset($this->fb_params['in_iframe']); } public function in_fb_canvas() { return isset($this->fb_params['in_canvas']); @@ -296,14 +323,42 @@ class Facebook { } public function get_add_url($next=null) { - return self::get_facebook_url().'/add.php?api_key='.$this->api_key . - ($next ? '&next=' . urlencode($next) : ''); + $page = self::get_facebook_url().'/add.php'; + $params = array('api_key' => $this->api_key); + + if ($next) { + $params['next'] = $next; + } + + return $page . '?' . http_build_query($params); } public function get_login_url($next, $canvas) { - return self::get_facebook_url().'/login.php?v=1.0&api_key=' . $this->api_key . - ($next ? '&next=' . urlencode($next) : '') . - ($canvas ? '&canvas' : ''); + $page = self::get_facebook_url().'/login.php'; + $params = array('api_key' => $this->api_key, + 'v' => '1.0'); + + if ($next) { + $params['next'] = $next; + } + if ($canvas) { + $params['canvas'] = '1'; + } + + return $page . '?' . http_build_query($params); + } + + public function get_logout_url($next) { + $page = self::get_facebook_url().'/logout.php'; + $params = array('app_key' => $this->api_key, + 'session_key' => $this->api_client->session_key); + + if ($next) { + $params['connect_next'] = 1; + $params['next'] = $next; + } + + return $page . '?' . http_build_query($params); } public function set_user($user, $session_key, $expires=null, $session_secret=null) { @@ -410,7 +465,20 @@ class Facebook { return $fb_params; } - /* + /** + * Validates the account that a user was trying to set up an + * independent account through Facebook Connect. + * + * @param user The user attempting to set up an independent account. + * @param hash The hash passed to the reclamation URL used. + * @return bool True if the user is the one that selected the + * reclamation link. + */ + public function verify_account_reclamation($user, $hash) { + return $hash == md5($user . $this->secret); + } + + /** * Validates that a given set of parameters match their signature. * Parameters all match a given input prefix, such as "fb_sig". * @@ -422,6 +490,37 @@ class Facebook { return self::generate_sig($fb_params, $this->secret) == $expected_sig; } + /** + * Validate the given signed public session data structure with + * public key of the app that + * the session proof belongs to. + * + * @param $signed_data the session info that is passed by another app + * @param string $public_key Optional public key of the app. If this + * is not passed, function will make an API call to get it. + * return true if the session proof passed verification. + */ + public function verify_signed_public_session_data($signed_data, + $public_key = null) { + + // If public key is not already provided, we need to get it through API + if (!$public_key) { + $public_key = $this->api_client->auth_getAppPublicKey( + $signed_data['api_key']); + } + + // Create data to verify + $data_to_serialize = $signed_data; + unset($data_to_serialize['sig']); + $serialized_data = implode('_', $data_to_serialize); + + // Decode signature + $signature = base64_decode($signed_data['sig']); + $result = openssl_verify($serialized_data, $signature, $public_key, + OPENSSL_ALGO_SHA1); + return $result == 1; + } + /* * Generate a signature using the application secret key. * diff --git a/extlib/facebook/facebook_desktop.php b/extlib/facebook/facebook_desktop.php index 90cdf66bd..e79a2ca34 100644 --- a/extlib/facebook/facebook_desktop.php +++ b/extlib/facebook/facebook_desktop.php @@ -1,5 +1,5 @@ <?php -// Copyright 2004-2008 Facebook. All Rights Reserved. +// Copyright 2004-2009 Facebook. All Rights Reserved. // // +---------------------------------------------------------------------------+ // | Facebook Platform PHP5 client | diff --git a/extlib/facebook/facebookapi_php5_restlib.php b/extlib/facebook/facebookapi_php5_restlib.php index 389f40a9d..3fec06e8a 100644..100755 --- a/extlib/facebook/facebookapi_php5_restlib.php +++ b/extlib/facebook/facebookapi_php5_restlib.php @@ -1,9 +1,10 @@ <?php +// Copyright 2004-2009 Facebook. All Rights Reserved. // // +---------------------------------------------------------------------------+ // | Facebook Platform PHP5 client | // +---------------------------------------------------------------------------+ -// | Copyright (c) 2007-2008 Facebook, Inc. | +// | Copyright (c) 2007-2009 Facebook, Inc. | // | All rights reserved. | // | | // | Redistribution and use in source and binary forms, with or without | @@ -32,6 +33,7 @@ // include_once 'jsonwrapper/jsonwrapper.php'; + class FacebookRestClient { public $secret; public $session_key; @@ -50,7 +52,9 @@ class FacebookRestClient { public $canvas_user; public $batch_mode; private $batch_queue; + private $pending_batch; private $call_as_apikey; + private $use_curl_if_available; const BATCH_MODE_DEFAULT = 0; const BATCH_MODE_SERVER_PARALLEL = 0; @@ -70,7 +74,8 @@ class FacebookRestClient { $this->batch_mode = FacebookRestClient::BATCH_MODE_DEFAULT; $this->last_call_id = 0; $this->call_as_apikey = ''; - $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php'; + $this->use_curl_if_available = true; + $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php'; if (!empty($GLOBALS['facebook_config']['debug'])) { $this->cur_id = 0; @@ -123,39 +128,61 @@ function toggleDisplay(id, type) { } /** + * Normally, if the cURL library/PHP extension is available, it is used for + * HTTP transactions. This allows that behavior to be overridden, falling + * back to a vanilla-PHP implementation even if cURL is installed. + * + * @param $use_curl_if_available bool whether or not to use cURL if available + */ + public function set_use_curl_if_available($use_curl_if_available) { + $this->use_curl_if_available = $use_curl_if_available; + } + + /** * Start a batch operation. */ public function begin_batch() { - if($this->batch_queue !== null) { + if ($this->pending_batch()) { $code = FacebookAPIErrorCodes::API_EC_BATCH_ALREADY_STARTED; - throw new FacebookRestClientException($code, - FacebookAPIErrorCodes::$api_error_descriptions[$code]); + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); } $this->batch_queue = array(); + $this->pending_batch = true; } /* * End current batch operation */ public function end_batch() { - if($this->batch_queue === null) { + if (!$this->pending_batch()) { $code = FacebookAPIErrorCodes::API_EC_BATCH_NOT_STARTED; - throw new FacebookRestClientException($code, - FacebookAPIErrorCodes::$api_error_descriptions[$code]); + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); } - $this->execute_server_side_batch(); + $this->pending_batch = false; + $this->execute_server_side_batch(); $this->batch_queue = null; } + /** + * are we currently queueing up calls for a batch? + */ + public function pending_batch() { + return $this->pending_batch; + } + private function execute_server_side_batch() { $item_count = count($this->batch_queue); $method_feed = array(); foreach($this->batch_queue as $batch_item) { - $method_feed[] = $this->create_post_string($batch_item['m'], - $batch_item['p']); + $method = $batch_item['m']; + $params = $batch_item['p']; + $this->finalize_params($method, $params); + $method_feed[] = $this->create_post_string($method, $params); } $method_feed_json = json_encode($method_feed); @@ -202,6 +229,18 @@ function toggleDisplay(id, type) { $this->call_as_apikey = ''; } + + /* + * If a page is loaded via HTTPS, then all images and static + * resources need to be printed with HTTPS urls to avoid + * mixed content warnings. If your page loads with an HTTPS + * url, then call set_use_ssl_resources to retrieve the correct + * urls. + */ + public function set_use_ssl_resources($is_ssl = true) { + $this->use_ssl_resources = $is_ssl; + } + /** * Returns public information for an application (as shown in the application * directory) by either application ID, API key, or canvas page name. @@ -231,7 +270,7 @@ function toggleDisplay(id, type) { * @return string An authentication token. */ public function auth_createToken() { - return $this->call_method('facebook.auth.createToken', array()); + return $this->call_method('facebook.auth.createToken'); } /** @@ -246,8 +285,7 @@ function toggleDisplay(id, type) { * @return array An assoc array containing session_key, uid */ public function auth_getSession($auth_token, $generate_session_secret=false) { - //Check if we are in batch mode - if($this->batch_queue === null) { + if (!$this->pending_batch()) { $result = $this->call_method('facebook.auth.getSession', array('auth_token' => $auth_token, 'generate_session_secret' => $generate_session_secret)); @@ -271,7 +309,7 @@ function toggleDisplay(id, type) { * API_EC_PARAM_UNKNOWN */ public function auth_promoteSession() { - return $this->call_method('facebook.auth.promoteSession', array()); + return $this->call_method('facebook.auth.promoteSession'); } /** @@ -282,7 +320,20 @@ function toggleDisplay(id, type) { * @return bool true if session expiration was successful, false otherwise */ public function auth_expireSession() { - return $this->call_method('facebook.auth.expireSession', array()); + return $this->call_method('facebook.auth.expireSession'); + } + + /** + * Revokes the given extended permission that the user granted at some + * prior time (for instance, offline_access or email). If no user is + * provided, it will be revoked for the user of the current session. + * + * @param string $perm The permission to revoke + * @param int $uid The user for whom to revoke the permission. + */ + public function auth_revokeExtendedPermission($perm, $uid=null) { + return $this->call_method('facebook.auth.revokeExtendedPermission', + array('perm' => $perm, 'uid' => $uid)); } /** @@ -303,6 +354,30 @@ function toggleDisplay(id, type) { } /** + * Get public key that is needed to verify digital signature + * an app may pass to other apps. The public key is only used by + * other apps for verification purposes. + * @param string API key of an app + * @return string The public key for the app. + */ + public function auth_getAppPublicKey($target_app_key) { + return $this->call_method('facebook.auth.getAppPublicKey', + array('target_app_key' => $target_app_key)); + } + + /** + * Get a structure that can be passed to another app + * as proof of session. The other app can verify it using public + * key of this app. + * + * @return signed public session data structure. + */ + public function auth_getSignedPublicSessionData() { + return $this->call_method('facebook.auth.getSignedPublicSessionData', + array()); + } + + /** * Returns the number of unconnected friends that exist in this application. * This number is determined based on the accounts registered through * connect.registerUsers() (see below). @@ -363,8 +438,9 @@ function toggleDisplay(id, type) { * * @param int $uid (Optional) User associated with events. A null * parameter will default to the session user. - * @param array $eids (Optional) Filter by these event ids. A null - * parameter will get all events for the user. + * @param array/string $eids (Optional) Filter by these event + * ids. A null parameter will get all events for + * the user. (A csv list will work but is deprecated) * @param int $start_time (Optional) Filter with this unix time as lower * bound. A null or zero parameter indicates no * lower bound. @@ -718,12 +794,15 @@ function toggleDisplay(id, type) { * @param string $body_general (Optional) Additional markup that extends * the body of a short story. * @param int $story_size (Optional) A story size (see above) + * @param string $user_message (Optional) A user message for a short + * story. * * @return bool true on success */ public function &feed_publishUserAction( $template_bundle_id, $template_data, $target_ids='', $body_general='', - $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE) { + $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE, + $user_message='') { if (is_array($template_data)) { $template_data = json_encode($template_data); @@ -739,7 +818,107 @@ function toggleDisplay(id, type) { 'template_data' => $template_data, 'target_ids' => $target_ids, 'body_general' => $body_general, - 'story_size' => $story_size)); + 'story_size' => $story_size, + 'user_message' => $user_message)); + } + + + /** + * Publish a post to the user's stream. + * + * @param $message the user's message + * @param $attachment the post's attachment (optional) + * @param $action links the post's action links (optional) + * @param $target_id the user on whose wall the post will be posted + * (optional) + * @param $uid the actor (defaults to session user) + * @return string the post id + */ + public function stream_publish( + $message, $attachment = null, $action_links = null, $target_id = null, + $uid = null) { + + return $this->call_method( + 'facebook.stream.publish', + array('message' => $message, + 'attachment' => $attachment, + 'action_links' => $action_links, + 'target_id' => $target_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Remove a post from the user's stream. + * Currently, you may only remove stories you application created. + * + * @param $post_id the post id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_remove($post_id, $uid = null) { + return $this->call_method( + 'facebook.stream.remove', + array('post_id' => $post_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Add a comment to a stream post + * + * @param $post_id the post id + * @param $comment the comment text + * @param $uid the actor (defaults to session user) + * @return string the id of the created comment + */ + public function stream_addComment($post_id, $comment, $uid = null) { + return $this->call_method( + 'facebook.stream.addComment', + array('post_id' => $post_id, + 'comment' => $comment, + 'uid' => $this->get_uid($uid))); + } + + + /** + * Remove a comment from a stream post + * + * @param $comment_id the comment id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_removeComment($comment_id, $uid = null) { + return $this->call_method( + 'facebook.stream.removeComment', + array('comment_id' => $comment_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Add a like to a stream post + * + * @param $post_id the post id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_addLike($post_id, $uid = null) { + return $this->call_method( + 'facebook.stream.addLike', + array('post_id' => $post_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Remove a like from a stream post + * + * @param $post_id the post id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_removeLike($post_id, $uid = null) { + return $this->call_method( + 'facebook.stream.removeLike', + array('post_id' => $post_id, + 'uid' => $this->get_uid($uid))); } /** @@ -750,7 +929,7 @@ function toggleDisplay(id, type) { * @return array An array of feed story objects. */ public function &feed_getAppFriendStories() { - return $this->call_method('facebook.feed.getAppFriendStories', array()); + return $this->call_method('facebook.feed.getAppFriendStories'); } /** @@ -771,33 +950,42 @@ function toggleDisplay(id, type) { * Returns whether or not pairs of users are friends. * Note that the Facebook friend relationship is symmetric. * - * @param array $uids1 array of ids (id_1, id_2,...) of some length X - * @param array $uids2 array of ids (id_A, id_B,...) of SAME length X + * @param array/string $uids1 list of ids (id_1, id_2,...) + * of some length X (csv is deprecated) + * @param array/string $uids2 list of ids (id_A, id_B,...) + * of SAME length X (csv is deprecated) * * @return array An array with uid1, uid2, and bool if friends, e.g.: * array(0 => array('uid1' => id_1, 'uid2' => id_A, 'are_friends' => 1), * 1 => array('uid1' => id_2, 'uid2' => id_B, 'are_friends' => 0) * ...) + * @error + * API_EC_PARAM_USER_ID_LIST */ public function &friends_areFriends($uids1, $uids2) { return $this->call_method('facebook.friends.areFriends', - array('uids1' => $uids1, 'uids2' => $uids2)); + array('uids1' => $uids1, + 'uids2' => $uids2)); } /** * Returns the friends of the current session user. * * @param int $flid (Optional) Only return friends on this friend list. + * @param int $uid (Optional) Return friends for this user. * * @return array An array of friends */ - public function &friends_get($flid=null) { + public function &friends_get($flid=null, $uid = null) { if (isset($this->friends_list)) { return $this->friends_list; } $params = array(); - if (isset($this->canvas_user)) { - $params['uid'] = $this->canvas_user; + if (!$uid && isset($this->canvas_user)) { + $uid = $this->canvas_user; + } + if ($uid) { + $params['uid'] = $uid; } if ($flid) { $params['flid'] = $flid; @@ -812,7 +1000,7 @@ function toggleDisplay(id, type) { * @return array An array of friend list objects */ public function &friends_getLists() { - return $this->call_method('facebook.friends.getLists', array()); + return $this->call_method('facebook.friends.getLists'); } /** @@ -822,7 +1010,7 @@ function toggleDisplay(id, type) { * @return array An array of friends also using the app */ public function &friends_getAppUsers() { - return $this->call_method('facebook.friends.getAppUsers', array()); + return $this->call_method('facebook.friends.getAppUsers'); } /** @@ -830,8 +1018,9 @@ function toggleDisplay(id, type) { * * @param int $uid (Optional) User associated with groups. A null * parameter will default to the session user. - * @param array $gids (Optional) Group ids to query. A null parameter will - * get all groups for the user. + * @param array/string $gids (Optional) Array of group ids to query. A null + * parameter will get all groups for the user. + * (csv is deprecated) * * @return array An array of group objects */ @@ -890,6 +1079,40 @@ function toggleDisplay(id, type) { } /** + * Retrieves links posted by the given user. + * + * @param int $uid The user whose links you wish to retrieve + * @param int $limit The maximimum number of links to retrieve + * @param array $link_ids (Optional) Array of specific link + * IDs to retrieve by this user + * + * @return array An array of links. + */ + public function &links_get($uid, $limit, $link_ids = null) { + return $this->call_method('links.get', + array('uid' => $uid, + 'limit' => $limit, + 'link_ids' => $link_ids)); + } + + /** + * Posts a link on Facebook. + * + * @param string $url URL/link you wish to post + * @param string $comment (Optional) A comment about this link + * @param int $uid (Optional) User ID that is posting this link; + * defaults to current session user + * + * @return bool + */ + public function &links_post($url, $comment='', $uid = null) { + return $this->call_method('links.post', + array('uid' => $uid, + 'url' => $url, + 'comment' => $comment)); + } + + /** * Permissions API */ @@ -946,6 +1169,78 @@ function toggleDisplay(id, type) { } /** + * Creates a note with the specified title and content. + * + * @param string $title Title of the note. + * @param string $content Content of the note. + * @param int $uid (Optional) The user for whom you are creating a + * note; defaults to current session user + * + * @return int The ID of the note that was just created. + */ + public function ¬es_create($title, $content, $uid = null) { + return $this->call_method('notes.create', + array('uid' => $uid, + 'title' => $title, + 'content' => $content)); + } + + /** + * Deletes the specified note. + * + * @param int $note_id ID of the note you wish to delete + * @param int $uid (Optional) Owner of the note you wish to delete; + * defaults to current session user + * + * @return bool + */ + public function ¬es_delete($note_id, $uid = null) { + return $this->call_method('notes.delete', + array('uid' => $uid, + 'note_id' => $note_id)); + } + + /** + * Edits a note, replacing its title and contents with the title + * and contents specified. + * + * @param int $note_id ID of the note you wish to edit + * @param string $title Replacement title for the note + * @param string $content Replacement content for the note + * @param int $uid (Optional) Owner of the note you wish to edit; + * defaults to current session user + * + * @return bool + */ + public function ¬es_edit($note_id, $title, $content, $uid = null) { + return $this->call_method('notes.edit', + array('uid' => $uid, + 'note_id' => $note_id, + 'title' => $title, + 'content' => $content)); + } + + /** + * Retrieves all notes by a user. If note_ids are specified, + * retrieves only those specific notes by that user. + * + * @param int $uid User whose notes you wish to retrieve + * @param array $note_ids (Optional) List of specific note + * IDs by this user to retrieve + * + * @return array A list of all of the given user's notes, or an empty list + * if the viewer lacks permissions or if there are no visible + * notes. + */ + public function ¬es_get($uid, $note_ids = null) { + + return $this->call_method('notes.get', + array('uid' => $uid, + 'note_ids' => $note_ids)); + } + + + /** * Returns the outstanding notifications for the session user. * * @return array An assoc array of notification count objects for @@ -954,13 +1249,15 @@ function toggleDisplay(id, type) { * and an eid list of 'event_invites' */ public function ¬ifications_get() { - return $this->call_method('facebook.notifications.get', array()); + return $this->call_method('facebook.notifications.get'); } /** * Sends a notification to the specified users. * * @return A comma separated list of successful recipients + * @error + * API_EC_PARAM_USER_ID_LIST */ public function ¬ifications_send($to_ids, $notification, $type) { return $this->call_method('facebook.notifications.send', @@ -972,12 +1269,14 @@ function toggleDisplay(id, type) { /** * Sends an email to the specified user of the application. * - * @param array $recipients id of the recipients + * @param array/string $recipients array of ids of the recipients (csv is deprecated) * @param string $subject subject of the email * @param string $text (plain text) body of the email * @param string $fbml fbml markup for an html version of the email * * @return string A comma separated list of successful recipients + * @error + * API_EC_PARAM_USER_ID_LIST */ public function ¬ifications_sendEmail($recipients, $subject, @@ -993,9 +1292,9 @@ function toggleDisplay(id, type) { /** * Returns the requested info fields for the requested set of pages. * - * @param array $page_ids an array of page ids - * @param array $fields an array of strings describing the info fields - * desired + * @param array/string $page_ids an array of page ids (csv is deprecated) + * @param array/string $fields an array of strings describing the + * info fields desired (csv is deprecated) * @param int $uid (Optional) limit results to pages of which this * user is a fan. * @param string type limits results to a particular type of page. @@ -1090,7 +1389,7 @@ function toggleDisplay(id, type) { 'tag_text' => $tag_text, 'x' => $x, 'y' => $y, - 'tags' => json_encode($tags), + 'tags' => (is_array($tags)) ? json_encode($tags) : null, 'owner_uid' => $this->get_uid($owner_uid))); } @@ -1128,7 +1427,8 @@ function toggleDisplay(id, type) { * @param int $subj_id (Optional) Filter by uid of user tagged in the photos. * @param int $aid (Optional) Filter by an album, as returned by * photos_getAlbums. - * @param array $pids (Optional) Restrict to a list of pids + * @param array/string $pids (Optional) Restrict to an array of pids + * (csv is deprecated) * * Note that at least one of these parameters needs to be specified, or an * error is returned. @@ -1143,9 +1443,10 @@ function toggleDisplay(id, type) { /** * Returns the albums created by the given user. * - * @param int $uid (Optional) The uid of the user whose albums you want. - * A null will return the albums of the session user. - * @param array $aids (Optional) A list of aids to restrict the query. + * @param int $uid (Optional) The uid of the user whose albums you want. + * A null will return the albums of the session user. + * @param string $aids (Optional) An array of aids to restrict + * the query. (csv is deprecated) * * Note that at least one of the (uid, aids) parameters must be specified. * @@ -1172,16 +1473,66 @@ function toggleDisplay(id, type) { } /** + * Uploads a photo. + * + * @param string $file The location of the photo on the local filesystem. + * @param int $aid (Optional) The album into which to upload the + * photo. + * @param string $caption (Optional) A caption for the photo. + * @param int uid (Optional) The user ID of the user whose photo you + * are uploading + * + * @return array An array of user objects + */ + public function photos_upload($file, $aid=null, $caption=null, $uid=null) { + return $this->call_upload_method('facebook.photos.upload', + array('aid' => $aid, + 'caption' => $caption, + 'uid' => $uid), + $file); + } + + + /** + * Uploads a video. + * + * @param string $file The location of the video on the local filesystem. + * @param string $title (Optional) A title for the video. Titles over 65 characters in length will be truncated. + * @param string $description (Optional) A description for the video. + * + * @return array An array with the video's ID, title, description, and a link to view it on Facebook. + */ + public function video_upload($file, $title=null, $description=null) { + return $this->call_upload_method('facebook.video.upload', + array('title' => $title, + 'description' => $description), + $file, + Facebook::get_facebook_url('api-video') . '/restserver.php'); + } + + /** + * Returns an array with the video limitations imposed on the current session's + * associated user. Maximum length is measured in seconds; maximum size is + * measured in bytes. + * + * @return array Array with "length" and "size" keys + */ + public function &video_getUploadLimits() { + return $this->call_method('facebook.video.getUploadLimits'); + } + + /** * Returns the requested info fields for the requested set of users. * - * @param array $uids An array of user ids - * @param array $fields An array of info field names desired + * @param array/string $uids An array of user ids (csv is deprecated) + * @param array/string $fields An array of info field names desired (csv is deprecated) * * @return array An array of user objects */ public function &users_getInfo($uids, $fields) { return $this->call_method('facebook.users.getInfo', - array('uids' => $uids, 'fields' => $fields)); + array('uids' => $uids, + 'fields' => $fields)); } /** @@ -1194,14 +1545,15 @@ function toggleDisplay(id, type) { * users, use users.getInfo instead, so that proper privacy rules will be * applied. * - * @param array $uids An array of user ids - * @param array $fields An array of info field names desired + * @param array/string $uids An array of user ids (csv is deprecated) + * @param array/string $fields An array of info field names desired (csv is deprecated) * * @return array An array of user objects */ public function &users_getStandardInfo($uids, $fields) { return $this->call_method('facebook.users.getStandardInfo', - array('uids' => $uids, 'fields' => $fields)); + array('uids' => $uids, + 'fields' => $fields)); } /** @@ -1210,7 +1562,7 @@ function toggleDisplay(id, type) { * @return integer User id */ public function &users_getLoggedInUser() { - return $this->call_method('facebook.users.getLoggedInUser', array()); + return $this->call_method('facebook.users.getLoggedInUser'); } /** @@ -1239,6 +1591,17 @@ function toggleDisplay(id, type) { } /** + * Returns whether or not the user corresponding to the current + * session object is verified by Facebook. See the documentation + * for Users.isVerified for details. + * + * @return boolean true if the user is verified + */ + public function &users_isVerified() { + return $this->call_method('facebook.users.isVerified'); + } + + /** * Sets the users' current status message. Message does NOT contain the * word "is" , so make sure to include a verb. * @@ -1269,6 +1632,69 @@ function toggleDisplay(id, type) { } /** + * Gets the stream on behalf of a user using a set of users. This + * call will return the latest $limit queries between $start_time + * and $end_time. + * + * @param int $viewer_id user making the call (def: session) + * @param array $source_ids users/pages to look at (def: all connections) + * @param int $start_time start time to look for stories (def: 1 day ago) + * @param int $end_time end time to look for stories (def: now) + * @param int $limit number of stories to attempt to fetch (def: 30) + * @param string $filter_key key returned by stream.getFilters to fetch + * + * @return array( + * 'posts' => array of posts, + * 'profiles' => array of profile metadata of users/pages in posts + * 'albums' => array of album metadata in posts + * ) + */ + public function &stream_get($viewer_id = null, + $source_ids = null, + $start_time = 0, + $end_time = 0, + $limit = 30, + $filter_key = '') { + $args = array( + 'viewer_id' => $viewer_id, + 'source_ids' => $source_ids, + 'start_time' => $start_time, + 'end_time' => $end_time, + 'limit' => $limit, + 'filter_key' => $filter_key); + return $this->call_method('facebook.stream.get', $args); + } + + /** + * Gets the filters (with relevant filter keys for stream.get) for a + * particular user. These filters are typical things like news feed, + * friend lists, networks. They can be used to filter the stream + * without complex queries to determine which ids belong in which groups. + * + * @param int $uid user to get filters for + * + * @return array of stream filter objects + */ + public function &stream_getFilters($uid = null) { + $args = array('uid' => $uid); + return $this->call_method('facebook.stream.getFilters', $args); + } + + /** + * Gets the full comments given a post_id from stream.get or the + * stream FQL table. Initially, only a set of preview comments are + * returned because some posts can have many comments. + * + * @param string $post_id id of the post to get comments for + * + * @return array of comment objects + */ + public function &stream_getComments($post_id) { + $args = array('post_id' => $post_id); + return $this->call_method('facebook.stream.getComments', $args); + } + + /** * Sets the FBML for the profile of the user attached to this session. * * @param string $markup The FBML that describes the profile @@ -1690,7 +2116,7 @@ function toggleDisplay(id, type) { * API_EC_DATA_UNKNOWN_ERROR */ public function &data_getObjectTypes() { - return $this->call_method('facebook.data.getObjectTypes', array()); + return $this->call_method('facebook.data.getObjectTypes'); } /** @@ -2315,12 +2741,14 @@ function toggleDisplay(id, type) { * * @param string $integration_point_name Name of an integration point * (see developer wiki for list). + * @param int $uid Specific user to check the limit. * * @return int Integration point allocation value */ - public function &admin_getAllocation($integration_point_name) { + public function &admin_getAllocation($integration_point_name, $uid=null) { return $this->call_method('facebook.admin.getAllocation', - array('integration_point_name' => $integration_point_name)); + array('integration_point_name' => $integration_point_name, + 'uid' => $uid)); } /** @@ -2376,28 +2804,75 @@ function toggleDisplay(id, type) { */ public function admin_getRestrictionInfo() { return json_decode( - $this->call_method('admin.getRestrictionInfo', array()), + $this->call_method('admin.getRestrictionInfo'), true); } + + /** + * Bans a list of users from the app. Banned users can't + * access the app's canvas page and forums. + * + * @param array $uids an array of user ids + * @return bool true on success + */ + public function admin_banUsers($uids) { + return $this->call_method( + 'admin.banUsers', array('uids' => json_encode($uids))); + } + + /** + * Unban users that have been previously banned with + * admin_banUsers(). + * + * @param array $uids an array of user ids + * @return bool true on success + */ + public function admin_unbanUsers($uids) { + return $this->call_method( + 'admin.unbanUsers', array('uids' => json_encode($uids))); + } + + /** + * Gets the list of users that have been banned from the application. + * $uids is an optional parameter that filters the result with the list + * of provided user ids. If $uids is provided, + * only banned user ids that are contained in $uids are returned. + * + * @param array $uids an array of user ids to filter by + * @return bool true on success + */ + + public function admin_getBannedUsers($uids = null) { + return $this->call_method( + 'admin.getBannedUsers', + array('uids' => $uids ? json_encode($uids) : null)); + } + /* UTILITY FUNCTIONS */ /** - * Calls the specified method with the specified parameters. + * Calls the specified normal POST method with the specified parameters. * * @param string $method Name of the Facebook method to invoke * @param array $params A map of param names => param values * - * @return mixed Result of method call + * @return mixed Result of method call; this returns a reference to support + * 'delayed returns' when in a batch context. + * See: http://wiki.developers.facebook.com/index.php/Using_batching_API */ - public function & call_method($method, $params) { - //Check if we are in batch mode - if($this->batch_queue === null) { + public function &call_method($method, $params = array()) { + if (!$this->pending_batch()) { if ($this->call_as_apikey) { $params['call_as_apikey'] = $this->call_as_apikey; } - $xml = $this->post_request($method, $params); - $result = $this->convert_xml_to_result($xml, $method, $params); + $data = $this->post_request($method, $params); + if (empty($params['format']) || strtolower($params['format']) != 'json') { + $result = $this->convert_xml_to_result($data, $method, $params); + } + else { + $result = json_decode($data, true); + } if (is_array($result) && isset($result['error_code'])) { throw new FacebookRestClientException($result['error_msg'], @@ -2413,11 +2888,46 @@ function toggleDisplay(id, type) { return $result; } - private function convert_xml_to_result($xml, $method, $params) { + /** + * Calls the specified file-upload POST method with the specified parameters + * + * @param string $method Name of the Facebook method to invoke + * @param array $params A map of param names => param values + * @param string $file A path to the file to upload (required) + * + * @return array A dictionary representing the response. + */ + public function call_upload_method($method, $params, $file, $server_addr = null) { + if (!$this->pending_batch()) { + if (!file_exists($file)) { + $code = + FacebookAPIErrorCodes::API_EC_PARAM; + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); + } + + $xml = $this->post_upload_request($method, $params, $file, $server_addr); + $result = $this->convert_xml_to_result($xml, $method, $params); + + if (is_array($result) && isset($result['error_code'])) { + throw new FacebookRestClientException($result['error_msg'], + $result['error_code']); + } + } + else { + $code = + FacebookAPIErrorCodes::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE; + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); + } + + return $result; + } + + protected function convert_xml_to_result($xml, $method, $params) { $sxml = simplexml_load_string($xml); $result = self::convert_simplexml_to_array($sxml); - if (!empty($GLOBALS['facebook_config']['debug'])) { // output the raw xml and its corresponding php object, for debugging: print '<div style="margin: 10px 30px; padding: 5px; border: 2px solid black; background: gray; color: white; font-size: 12px; font-weight: bold;">'; @@ -2436,7 +2946,25 @@ function toggleDisplay(id, type) { return $result; } - private function create_post_string($method, $params) { + private function finalize_params($method, &$params) { + $this->add_standard_params($method, $params); + // we need to do this before signing the params + $this->convert_array_values_to_json($params); + $params['sig'] = Facebook::generate_sig($params, $this->secret); + } + + private function convert_array_values_to_json(&$params) { + foreach ($params as $key => &$val) { + if (is_array($val)) { + $val = json_encode($val); + } + } + } + + private function add_standard_params($method, &$params) { + if ($this->call_as_apikey) { + $params['call_as_apikey'] = $this->call_as_apikey; + } $params['method'] = $method; $params['session_key'] = $this->session_key; $params['api_key'] = $this->api_key; @@ -2448,50 +2976,118 @@ function toggleDisplay(id, type) { if (!isset($params['v'])) { $params['v'] = '1.0'; } + if (isset($this->use_ssl_resources) && + $this->use_ssl_resources) { + $params['return_ssl_resources'] = true; + } + } + + private function create_post_string($method, $params) { $post_params = array(); foreach ($params as $key => &$val) { - if (is_array($val)) $val = implode(',', $val); $post_params[] = $key.'='.urlencode($val); } - $secret = $this->secret; - $post_params[] = 'sig='.Facebook::generate_sig($params, $secret); return implode('&', $post_params); } - public function post_request($method, $params) { + private function run_multipart_http_transaction($method, $params, $file, $server_addr) { - $post_string = $this->create_post_string($method, $params); + // the format of this message is specified in RFC1867/RFC1341. + // we add twenty pseudo-random digits to the end of the boundary string. + $boundary = '--------------------------FbMuLtIpArT' . + sprintf("%010d", mt_rand()) . + sprintf("%010d", mt_rand()); + $content_type = 'multipart/form-data; boundary=' . $boundary; + // within the message, we prepend two extra hyphens. + $delimiter = '--' . $boundary; + $close_delimiter = $delimiter . '--'; + $content_lines = array(); + foreach ($params as $key => &$val) { + $content_lines[] = $delimiter; + $content_lines[] = 'Content-Disposition: form-data; name="' . $key . '"'; + $content_lines[] = ''; + $content_lines[] = $val; + } + // now add the file data + $content_lines[] = $delimiter; + $content_lines[] = + 'Content-Disposition: form-data; filename="' . $file . '"'; + $content_lines[] = 'Content-Type: application/octet-stream'; + $content_lines[] = ''; + $content_lines[] = file_get_contents($file); + $content_lines[] = $close_delimiter; + $content_lines[] = ''; + $content = implode("\r\n", $content_lines); + return $this->run_http_post_transaction($content_type, $content, $server_addr); + } - if (function_exists('curl_init')) { - // Use CURL if installed... + public function post_request($method, $params) { + $this->finalize_params($method, $params); + $post_string = $this->create_post_string($method, $params); + if ($this->use_curl_if_available && function_exists('curl_init')) { $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion(); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->server_addr); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_USERAGENT, $useragent); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); $result = curl_exec($ch); curl_close($ch); } else { - // Non-CURL based version... $content_type = 'application/x-www-form-urlencoded'; - $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) '.phpversion(); - $context = - array('http' => + $content = $post_string; + $result = $this->run_http_post_transaction($content_type, + $content, + $this->server_addr); + } + return $result; + } + + private function post_upload_request($method, $params, $file, $server_addr = null) { + $server_addr = $server_addr ? $server_addr : $this->server_addr; + $this->finalize_params($method, $params); + if ($this->use_curl_if_available && function_exists('curl_init')) { + // prepending '@' causes cURL to upload the file; the key is ignored. + $params['_file'] = '@' . $file; + $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion(); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $server_addr); + // this has to come before the POSTFIELDS set! + curl_setopt($ch, CURLOPT_POST, 1 ); + // passing an array gets curl to use the multipart/form-data content type + curl_setopt($ch, CURLOPT_POSTFIELDS, $params); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_USERAGENT, $useragent); + $result = curl_exec($ch); + curl_close($ch); + } else { + $result = $this->run_multipart_http_transaction($method, $params, $file, $server_addr); + } + return $result; + } + + private function run_http_post_transaction($content_type, $content, $server_addr) { + + $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) ' . phpversion(); + $content_length = strlen($content); + $context = + array('http' => array('method' => 'POST', - 'header' => 'Content-type: '.$content_type."\r\n". - 'User-Agent: '.$user_agent."\r\n". - 'Content-length: ' . strlen($post_string), - 'content' => $post_string)); - $contextid=stream_context_create($context); - $sock=fopen($this->server_addr, 'r', false, $contextid); - if ($sock) { - $result=''; - while (!feof($sock)) - $result.=fgets($sock, 4096); - - fclose($sock); + 'user_agent' => $user_agent, + 'header' => 'Content-Type: ' . $content_type . "\r\n" . + 'Content-Length: ' . $content_length, + 'content' => $content)); + $context_id = stream_context_create($context); + $sock = fopen($server_addr, 'r', false, $context_id); + + $result = ''; + if ($sock) { + while (!feof($sock)) { + $result .= fgets($sock, 4096); } + fclose($sock); } return $result; } @@ -2541,6 +3137,14 @@ class FacebookAPIErrorCodes { const API_EC_METHOD = 3; const API_EC_TOO_MANY_CALLS = 4; const API_EC_BAD_IP = 5; + const API_EC_HOST_API = 6; + const API_EC_HOST_UP = 7; + const API_EC_SECURE = 8; + const API_EC_RATE = 9; + const API_EC_PERMISSION_DENIED = 10; + const API_EC_DEPRECATED = 11; + const API_EC_VERSION = 12; + const API_EC_INTERNAL_FQL_ERROR = 13; /* * PARAMETER ERRORS @@ -2550,27 +3154,121 @@ class FacebookAPIErrorCodes { const API_EC_PARAM_SESSION_KEY = 102; const API_EC_PARAM_CALL_ID = 103; const API_EC_PARAM_SIGNATURE = 104; + const API_EC_PARAM_TOO_MANY = 105; const API_EC_PARAM_USER_ID = 110; const API_EC_PARAM_USER_FIELD = 111; const API_EC_PARAM_SOCIAL_FIELD = 112; + const API_EC_PARAM_EMAIL = 113; + const API_EC_PARAM_USER_ID_LIST = 114; + const API_EC_PARAM_FIELD_LIST = 115; const API_EC_PARAM_ALBUM_ID = 120; + const API_EC_PARAM_PHOTO_ID = 121; + const API_EC_PARAM_FEED_PRIORITY = 130; + const API_EC_PARAM_CATEGORY = 140; + const API_EC_PARAM_SUBCATEGORY = 141; + const API_EC_PARAM_TITLE = 142; + const API_EC_PARAM_DESCRIPTION = 143; + const API_EC_PARAM_BAD_JSON = 144; const API_EC_PARAM_BAD_EID = 150; const API_EC_PARAM_UNKNOWN_CITY = 151; + const API_EC_PARAM_BAD_PAGE_TYPE = 152; /* * USER PERMISSIONS ERRORS */ const API_EC_PERMISSION = 200; const API_EC_PERMISSION_USER = 210; + const API_EC_PERMISSION_NO_DEVELOPERS = 211; const API_EC_PERMISSION_ALBUM = 220; const API_EC_PERMISSION_PHOTO = 221; + const API_EC_PERMISSION_MESSAGE = 230; + const API_EC_PERMISSION_OTHER_USER = 240; + const API_EC_PERMISSION_STATUS_UPDATE = 250; + const API_EC_PERMISSION_PHOTO_UPLOAD = 260; + const API_EC_PERMISSION_VIDEO_UPLOAD = 261; + const API_EC_PERMISSION_SMS = 270; + const API_EC_PERMISSION_CREATE_LISTING = 280; + const API_EC_PERMISSION_CREATE_NOTE = 281; + const API_EC_PERMISSION_SHARE_ITEM = 282; const API_EC_PERMISSION_EVENT = 290; + const API_EC_PERMISSION_LARGE_FBML_TEMPLATE = 291; + const API_EC_PERMISSION_LIVEMESSAGE = 292; const API_EC_PERMISSION_RSVP_EVENT = 299; - const FQL_EC_PARSER = 601; + /* + * DATA EDIT ERRORS + */ + const API_EC_EDIT = 300; + const API_EC_EDIT_USER_DATA = 310; + const API_EC_EDIT_PHOTO = 320; + const API_EC_EDIT_ALBUM_SIZE = 321; + const API_EC_EDIT_PHOTO_TAG_SUBJECT = 322; + const API_EC_EDIT_PHOTO_TAG_PHOTO = 323; + const API_EC_EDIT_PHOTO_FILE = 324; + const API_EC_EDIT_PHOTO_PENDING_LIMIT = 325; + const API_EC_EDIT_PHOTO_TAG_LIMIT = 326; + const API_EC_EDIT_ALBUM_REORDER_PHOTO_NOT_IN_ALBUM = 327; + const API_EC_EDIT_ALBUM_REORDER_TOO_FEW_PHOTOS = 328; + + const API_EC_MALFORMED_MARKUP = 329; + const API_EC_EDIT_MARKUP = 330; + + const API_EC_EDIT_FEED_TOO_MANY_USER_CALLS = 340; + const API_EC_EDIT_FEED_TOO_MANY_USER_ACTION_CALLS = 341; + const API_EC_EDIT_FEED_TITLE_LINK = 342; + const API_EC_EDIT_FEED_TITLE_LENGTH = 343; + const API_EC_EDIT_FEED_TITLE_NAME = 344; + const API_EC_EDIT_FEED_TITLE_BLANK = 345; + const API_EC_EDIT_FEED_BODY_LENGTH = 346; + const API_EC_EDIT_FEED_PHOTO_SRC = 347; + const API_EC_EDIT_FEED_PHOTO_LINK = 348; + + const API_EC_EDIT_VIDEO_SIZE = 350; + const API_EC_EDIT_VIDEO_INVALID_FILE = 351; + const API_EC_EDIT_VIDEO_INVALID_TYPE = 352; + const API_EC_EDIT_VIDEO_FILE = 353; + + const API_EC_EDIT_FEED_TITLE_ARRAY = 360; + const API_EC_EDIT_FEED_TITLE_PARAMS = 361; + const API_EC_EDIT_FEED_BODY_ARRAY = 362; + const API_EC_EDIT_FEED_BODY_PARAMS = 363; + const API_EC_EDIT_FEED_PHOTO = 364; + const API_EC_EDIT_FEED_TEMPLATE = 365; + const API_EC_EDIT_FEED_TARGET = 366; + const API_EC_EDIT_FEED_MARKUP = 367; + + /** + * SESSION ERRORS + */ + const API_EC_SESSION_TIMED_OUT = 450; + const API_EC_SESSION_METHOD = 451; + const API_EC_SESSION_INVALID = 452; + const API_EC_SESSION_REQUIRED = 453; + const API_EC_SESSION_REQUIRED_FOR_SECRET = 454; + const API_EC_SESSION_CANNOT_USE_SESSION_SECRET = 455; + + + /** + * FQL ERRORS + */ + const FQL_EC_UNKNOWN_ERROR = 600; + const FQL_EC_PARSER = 601; // backwards compatibility + const FQL_EC_PARSER_ERROR = 601; const FQL_EC_UNKNOWN_FIELD = 602; const FQL_EC_UNKNOWN_TABLE = 603; - const FQL_EC_NOT_INDEXABLE = 604; + const FQL_EC_NOT_INDEXABLE = 604; // backwards compatibility + const FQL_EC_NO_INDEX = 604; + const FQL_EC_UNKNOWN_FUNCTION = 605; + const FQL_EC_INVALID_PARAM = 606; + const FQL_EC_INVALID_FIELD = 607; + const FQL_EC_INVALID_SESSION = 608; + const FQL_EC_UNSUPPORTED_APP_TYPE = 609; + const FQL_EC_SESSION_SECRET_NOT_ALLOWED = 610; + const FQL_EC_DEPRECATED_TABLE = 611; + const FQL_EC_EXTENDED_PERMISSION = 612; + const FQL_EC_RATE_LIMIT_EXCEEDED = 613; + + const API_EC_REF_SET_FAILED = 700; /** * DATA STORE API ERRORS @@ -2581,52 +3279,122 @@ class FacebookAPIErrorCodes { const API_EC_DATA_OBJECT_NOT_FOUND = 803; const API_EC_DATA_OBJECT_ALREADY_EXISTS = 804; const API_EC_DATA_DATABASE_ERROR = 805; + const API_EC_DATA_CREATE_TEMPLATE_ERROR = 806; + const API_EC_DATA_TEMPLATE_EXISTS_ERROR = 807; + const API_EC_DATA_TEMPLATE_HANDLE_TOO_LONG = 808; + const API_EC_DATA_TEMPLATE_HANDLE_ALREADY_IN_USE = 809; + const API_EC_DATA_TOO_MANY_TEMPLATE_BUNDLES = 810; + const API_EC_DATA_MALFORMED_ACTION_LINK = 811; + const API_EC_DATA_TEMPLATE_USES_RESERVED_TOKEN = 812; /* - * Batch ERROR + * APPLICATION INFO ERRORS */ - const API_EC_BATCH_ALREADY_STARTED = 900; - const API_EC_BATCH_NOT_STARTED = 901; - const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 902; + const API_EC_NO_SUCH_APP = 900; + /* + * BATCH ERRORS + */ + const API_EC_BATCH_TOO_MANY_ITEMS = 950; + const API_EC_BATCH_ALREADY_STARTED = 951; + const API_EC_BATCH_NOT_STARTED = 952; + const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 953; + + /* + * EVENT API ERRORS + */ + const API_EC_EVENT_INVALID_TIME = 1000; + + /* + * INFO BOX ERRORS + */ + const API_EC_INFO_NO_INFORMATION = 1050; + const API_EC_INFO_SET_FAILED = 1051; + + /* + * LIVEMESSAGE API ERRORS + */ + const API_EC_LIVEMESSAGE_SEND_FAILED = 1100; + const API_EC_LIVEMESSAGE_EVENT_NAME_TOO_LONG = 1101; + const API_EC_LIVEMESSAGE_MESSAGE_TOO_LONG = 1102; + + /* + * CONNECT SESSION ERRORS + */ + const API_EC_CONNECT_FEED_DISABLED = 1300; + + /* + * Platform tag bundles errors + */ + const API_EC_TAG_BUNDLE_QUOTA = 1400; + + /* + * SHARE + */ + const API_EC_SHARE_BAD_URL = 1500; + + /* + * NOTES + */ + const API_EC_NOTE_CANNOT_MODIFY = 1600; + + /* + * COMMENTS + */ + const API_EC_COMMENTS_UNKNOWN = 1700; + const API_EC_COMMENTS_POST_TOO_LONG = 1701; + const API_EC_COMMENTS_DB_DOWN = 1702; + const API_EC_COMMENTS_INVALID_XID = 1703; + const API_EC_COMMENTS_INVALID_UID = 1704; + const API_EC_COMMENTS_INVALID_POST = 1705; + + /** + * This array is no longer maintained; to view the description of an error + * code, please look at the message element of the API response or visit + * the developer wiki at http://wiki.developers.facebook.com/. + */ public static $api_error_descriptions = array( - API_EC_SUCCESS => 'Success', - API_EC_UNKNOWN => 'An unknown error occurred', - API_EC_SERVICE => 'Service temporarily unavailable', - API_EC_METHOD => 'Unknown method', - API_EC_TOO_MANY_CALLS => 'Application request limit reached', - API_EC_BAD_IP => 'Unauthorized source IP address', - API_EC_PARAM => 'Invalid parameter', - API_EC_PARAM_API_KEY => 'Invalid API key', - API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid', - API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous', - API_EC_PARAM_SIGNATURE => 'Incorrect signature', - API_EC_PARAM_USER_ID => 'Invalid user id', - API_EC_PARAM_USER_FIELD => 'Invalid user info field', - API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field', - API_EC_PARAM_ALBUM_ID => 'Invalid album id', - API_EC_PARAM_BAD_EID => 'Invalid eid', - API_EC_PARAM_UNKNOWN_CITY => 'Unknown city', - API_EC_PERMISSION => 'Permissions error', - API_EC_PERMISSION_USER => 'User not visible', - API_EC_PERMISSION_ALBUM => 'Album not visible', - API_EC_PERMISSION_PHOTO => 'Photo not visible', - API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event', - API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event', - FQL_EC_PARSER => 'FQL: Parser Error', - FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field', - FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table', - FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable', - FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function', - FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in', - API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error', - API_EC_DATA_INVALID_OPERATION => 'Invalid operation', - API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded', - API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found', - API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists', - API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again', - API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first', - API_EC_BATCH_NOT_STARTED => 'end_batch called before start_batch', - API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode', + self::API_EC_SUCCESS => 'Success', + self::API_EC_UNKNOWN => 'An unknown error occurred', + self::API_EC_SERVICE => 'Service temporarily unavailable', + self::API_EC_METHOD => 'Unknown method', + self::API_EC_TOO_MANY_CALLS => 'Application request limit reached', + self::API_EC_BAD_IP => 'Unauthorized source IP address', + self::API_EC_PARAM => 'Invalid parameter', + self::API_EC_PARAM_API_KEY => 'Invalid API key', + self::API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid', + self::API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous', + self::API_EC_PARAM_SIGNATURE => 'Incorrect signature', + self::API_EC_PARAM_USER_ID => 'Invalid user id', + self::API_EC_PARAM_USER_FIELD => 'Invalid user info field', + self::API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field', + self::API_EC_PARAM_USER_ID_LIST => 'Invalid user id list', + self::API_EC_PARAM_FIELD_LIST => 'Invalid field list', + self::API_EC_PARAM_ALBUM_ID => 'Invalid album id', + self::API_EC_PARAM_BAD_EID => 'Invalid eid', + self::API_EC_PARAM_UNKNOWN_CITY => 'Unknown city', + self::API_EC_PERMISSION => 'Permissions error', + self::API_EC_PERMISSION_USER => 'User not visible', + self::API_EC_PERMISSION_NO_DEVELOPERS => 'Application has no developers', + self::API_EC_PERMISSION_ALBUM => 'Album not visible', + self::API_EC_PERMISSION_PHOTO => 'Photo not visible', + self::API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event', + self::API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event', + self::API_EC_EDIT_ALBUM_SIZE => 'Album is full', + self::FQL_EC_PARSER => 'FQL: Parser Error', + self::FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field', + self::FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table', + self::FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable', + self::FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function', + self::FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in', + self::API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error', + self::API_EC_DATA_INVALID_OPERATION => 'Invalid operation', + self::API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded', + self::API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found', + self::API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists', + self::API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again', + self::API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first', + self::API_EC_BATCH_NOT_STARTED => 'end_batch called before begin_batch', + self::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode' ); } diff --git a/lib/facebookutil.php b/lib/facebookutil.php index ec3987273..242d2e06f 100644 --- a/lib/facebookutil.php +++ b/lib/facebookutil.php @@ -27,9 +27,21 @@ define("FACEBOOK_PROMPTED_UPDATE_PREF", 2); function getFacebook() { + static $facebook = null; + $apikey = common_config('facebook', 'apikey'); $secret = common_config('facebook', 'secret'); - return new Facebook($apikey, $secret); + + if ($facebook === null) { + $facebook = new Facebook($apikey, $secret); + } + + if (!$facebook) { + common_log(LOG_ERR, 'Could not make new Facebook client obj!', + __FILE__); + } + + return $facebook; } function updateProfileBox($facebook, $flink, $notice) { @@ -92,7 +104,6 @@ function isFacebookBound($notice, $flink) { } - function facebookBroadcastNotice($notice) { $facebook = getFacebook(); diff --git a/plugins/FBConnect/FBConnectLogin.php b/plugins/FBConnect/FBConnectLogin.php new file mode 100644 index 000000000..a544352f3 --- /dev/null +++ b/plugins/FBConnect/FBConnectLogin.php @@ -0,0 +1,372 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Plugin to enable Facebook Connect + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +require_once INSTALLDIR . '/plugins/FBConnect/FBConnectLogin.php'; +require_once INSTALLDIR . '/lib/facebookutil.php'; + +class FBConnectloginAction extends Action +{ + + var $fbuid = null; + var $fb_fields = null; + + function prepare($args) { + parent::prepare($args); + + $this->fbuid = getFacebook()->get_loggedin_user(); + $this->fb_fields = $this->getFacebookFields($this->fbuid, + array('first_name', 'last_name', 'name')); + + return true; + } + + function handle($args) + { + parent::handle($args); + + if (common_is_real_login()) { + $this->clientError(_('Already logged in.')); + } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->showForm(_('There was a problem with your session token. Try again, please.')); + return; + } + if ($this->arg('create')) { + if (!$this->boolean('license')) { + $this->showForm(_('You can\'t register if you don\'t agree to the license.'), + $this->trimmed('newname')); + return; + } + $this->createNewUser(); + } else if ($this->arg('connect')) { + $this->connectUser(); + } else { + common_debug(print_r($this->args, true), __FILE__); + $this->showForm(_('Something weird happened.'), + $this->trimmed('newname')); + } + } else { + $this->tryLogin(); + } + } + + function showPageNotice() + { + if ($this->error) { + $this->element('div', array('class' => 'error'), $this->error); + } else { + $this->element('div', 'instructions', + sprintf(_('This is the first time you\'ve logged into %s so we must connect your Facebook to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name'))); + } + } + + function title() + { + return _('Facebook Account Setup'); + } + + function showForm($error=null, $username=null) + { + $this->error = $error; + $this->username = $username; + + $this->showPage(); + } + + function showPage() + { + parent::showPage(); + } + + function showContent() + { + if (!empty($this->message_text)) { + $this->element('p', null, $this->message); + return; + } + + $this->elementStart('form', array('method' => 'post', + 'id' => 'account_connect', + 'action' => common_local_url('fbconnectlogin'))); + $this->hidden('token', common_session_token()); + $this->element('h2', null, + _('Create new account')); + $this->element('p', null, + _('Create a new user with this nickname.')); + $this->input('newname', _('New nickname'), + ($this->username) ? $this->username : '', + _('1-64 lowercase letters or numbers, no punctuation or spaces')); + $this->elementStart('p'); + $this->element('input', array('type' => 'checkbox', + 'id' => 'license', + 'name' => 'license', + 'value' => 'true')); + $this->text(_('My text and files are available under ')); + $this->element('a', array('href' => common_config('license', 'url')), + common_config('license', 'title')); + $this->text(_(' except this private data: password, email address, IM address, phone number.')); + $this->elementEnd('p'); + $this->submit('create', _('Create')); + $this->element('h2', null, + _('Connect existing account')); + $this->element('p', null, + _('If you already have an account, login with your username and password to connect it to your Facebook.')); + $this->input('nickname', _('Existing nickname')); + $this->password('password', _('Password')); + $this->submit('connect', _('Connect')); + $this->elementEnd('form'); + } + + function message($msg) + { + $this->message_text = $msg; + $this->showPage(); + } + + function createNewUser() + { + + if (common_config('site', 'closed')) { + $this->clientError(_('Registration not allowed.')); + return; + } + + $invite = null; + + if (common_config('site', 'inviteonly')) { + $code = $_SESSION['invitecode']; + if (empty($code)) { + $this->clientError(_('Registration not allowed.')); + return; + } + + $invite = Invitation::staticGet($code); + + if (empty($invite)) { + $this->clientError(_('Not a valid invitation code.')); + return; + } + } + + $nickname = $this->trimmed('newname'); + + if (!Validate::string($nickname, array('min_length' => 1, + 'max_length' => 64, + 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { + $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.')); + return; + } + + if (!User::allowed_nickname($nickname)) { + $this->showForm(_('Nickname not allowed.')); + return; + } + + if (User::staticGet('nickname', $nickname)) { + $this->showForm(_('Nickname already in use. Try another one.')); + return; + } + + $fullname = trim($this->fb_fields['firstname'] . + ' ' . $this->fb_fields['lastname']); + + $args = array('nickname' => $nickname, 'fullname' => $fullname); + + if (!empty($invite)) { + $args['code'] = $invite->code; + } + + $user = User::register($args); + + $result = $this->flinkUser($user->id, $this->fbuid); + + if (!$result) { + $this->serverError(_('Error connecting user to Facebook.')); + return; + } + + common_set_user($user); + common_real_login(true); + + common_debug("Registered new user $user->id from Facebook user $this->fbuid"); + + common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)), + 303); + } + + function connectUser() + { + $nickname = $this->trimmed('nickname'); + $password = $this->trimmed('password'); + + if (!common_check_user($nickname, $password)) { + $this->showForm(_('Invalid username or password.')); + return; + } + + $user = User::staticGet('nickname', $nickname); + + if ($user) { + common_debug("Legit user to connect to Facebook: $nickname"); + } + + $result = $this->flinkUser($user->id, $this->fbuid); + + if (!$result) { + $this->serverError(_('Error connecting user to Facebook.')); + return; + } + + common_debug("Connected Facebook user $this->fbuid to local user $user->id"); + + common_set_user($user); + common_real_login(true); + + $this->goHome($user->nickname); + } + + function tryLogin() + { + $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE); + + if ($flink) { + $user = $flink->getUser(); + + if ($user) { + + common_debug("Logged in Facebook user $flink->foreign_id as user $user->id"); + + common_set_user($user); + common_real_login(true); + $this->goHome($user->nickname); + } + + } else { + $this->showForm(null, $this->bestNewNickname()); + } + } + + function goHome($nickname) + { + $url = common_get_returnto(); + if ($url) { + // We don't have to return to it again + common_set_returnto(null); + } else { + $url = common_local_url('all', + array('nickname' => + $nickname)); + } + common_redirect($url, 303); + } + + function flinkUser($user_id, $fbuid) + { + $flink = new Foreign_link(); + $flink->user_id = $user_id; + $flink->foreign_id = $fbuid; + $flink->service = FACEBOOK_SERVICE; + $flink->created = common_sql_now(); + + $flink_id = $flink->insert(); + + return $flink_id; + } + + function bestNewNickname() + { + + common_debug("bestNewNickname()"); + common_debug(print_r($this->fb_fields, true)); + + if (!empty($this->fb_fields['name'])) { + $nickname = $this->nicknamize($this->fb_fields['name']); + if ($this->isNewNickname($nickname)) { + return $nickname; + } + } + + // Try the full name + + $fullname = trim($this->fb_fields['firstname'] . + ' ' . $this->fb_fields['lastname']); + + if (!empty($fullname)) { + $fullname = $this->nicknamize($fullname); + if ($this->isNewNickname($fullname)) { + return $fullname; + } + } + + return null; + } + + // Given a string, try to make it work as a nickname + + function nicknamize($str) + { + $str = preg_replace('/\W/', '', $str); + return strtolower($str); + } + + function isNewNickname($str) + { + if (!Validate::string($str, array('min_length' => 1, + 'max_length' => 64, + 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { + return false; + } + if (!User::allowed_nickname($str)) { + return false; + } + if (User::staticGet('nickname', $str)) { + return false; + } + return true; + } + + // XXX: Consider moving this to lib/facebookutil.php + function getFacebookFields($fb_uid, $fields) { + try { + $infos = getFacebook()->api_client->users_getInfo($fb_uid, $fields); + + if (empty($infos)) { + return null; + } + return reset($infos); + + } catch (Exception $e) { + error_log("Failure in the api when requesting " . join(",", $fields) + ." on uid " . $fb_uid . " : ". $e->getMessage()); + return null; + } + } + +} diff --git a/plugins/FBConnect/FBConnectPlugin.php b/plugins/FBConnect/FBConnectPlugin.php new file mode 100644 index 000000000..30532e5ec --- /dev/null +++ b/plugins/FBConnect/FBConnectPlugin.php @@ -0,0 +1,278 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Plugin to enable Facebook Connect + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR . '/plugins/FBConnect/FBConnectLogin.php'; +require_once INSTALLDIR . '/lib/facebookutil.php'; + +/** + * Plugin to enable Facebook Connect + * + * @category Plugin + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class FBConnectPlugin extends Plugin +{ + + function __construct() + { + parent::__construct(); + } + + // Hook in new actions + function onRouterInitialized(&$m) { + $m->connect('main/facebookconnect', array('action' => 'fbconnectlogin')); + } + + // Add in xmlns:fb + function onStartShowHTML($action) + { + + // XXX: This is probably a bad place to do general processing + // so maybe I need to make some new events? Maybe in + // Action::prepare? + + $name = get_class($action); + + common_debug("action: $name"); + + // Avoid a redirect loop + if ($name != 'FBConnectloginAction') { + + $this->checkFacebookUser($action); + + } + + $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ? + $_SERVER['HTTP_ACCEPT'] : null; + + // XXX: allow content negotiation for RDF, RSS, or XRDS + + $cp = common_accept_to_prefs($httpaccept); + $sp = common_accept_to_prefs(PAGE_TYPE_PREFS); + + $type = common_negotiate_type($cp, $sp); + + if (!$type) { + throw new ClientException(_('This page is not available in a '. + 'media type you accept'), 406); + } + + + header('Content-Type: '.$type); + + $action->extraHeaders(); + + $action->startXML('html', + '-//W3C//DTD XHTML 1.0 Strict//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'); + + $language = $action->getLanguage(); + + $action->elementStart('html', array('xmlns' => 'http://www.w3.org/1999/xhtml', + 'xmlns:fb' => 'http://www.facebook.com/2008/fbml', + 'xml:lang' => $language, + 'lang' => $language)); + + return false; + + } + + function onEndShowLaconicaScripts($action) + { + + $action->element('script', + array('type' => 'text/javascript', + 'src' => 'http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php'), + ' '); + + $apikey = common_config('facebook', 'apikey'); + $plugin_path = common_path('plugins/FBConnect'); + + $login_url = common_get_returnto() || common_local_url('public'); + + $html = sprintf('<script type="text/javascript">FB.init("%s", "%s/xd_receiver.htm"); + + function refresh_page() { + window.location = "%s"; + } + + </script>', $apikey, $plugin_path, $login_url); + + + $action->raw($html); + } + + function onStartPrimaryNav($action) + { + $user = common_current_user(); + + if ($user) { + $action->menuItem(common_local_url('all', array('nickname' => $user->nickname)), + _('Home'), _('Personal profile and friends timeline'), false, 'nav_home'); + $action->menuItem(common_local_url('profilesettings'), + _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account'); + if (common_config('xmpp', 'enabled')) { + $action->menuItem(common_local_url('imsettings'), + _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect'); + } else { + $action->menuItem(common_local_url('smssettings'), + _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect'); + } + $action->menuItem(common_local_url('invite'), + _('Invite'), + sprintf(_('Invite friends and colleagues to join you on %s'), + common_config('site', 'name')), + false, 'nav_invitecontact'); + + // Need to override the Logout link to make it do FB stuff + + $logout_url = common_local_url('logout'); + $title = _('Logout from the site'); + $text = _('Logout'); + + $html = sprintf('<li id="nav_logout"><a href="%s" title="%s" ' . + 'onclick="FB.Connect.logoutAndRedirect(\'%s\')">%s</a></li>', + $logout_url, $title, $logout_url, $text); + + $action->raw($html); + + } + else { + if (!common_config('site', 'closed')) { + $action->menuItem(common_local_url('register'), + _('Register'), _('Create an account'), false, 'nav_register'); + } + $action->menuItem(common_local_url('openidlogin'), + _('OpenID'), _('Login with OpenID'), false, 'nav_openid'); + $action->menuItem(common_local_url('login'), + _('Login'), _('Login to the site'), false, 'nav_login'); + } + + $action->menuItem(common_local_url('doc', array('title' => 'help')), + _('Help'), _('Help me!'), false, 'nav_help'); + $action->menuItem(common_local_url('peoplesearch'), + _('Search'), _('Search for people or text'), false, 'nav_search'); + + // Tack on "Connect with Facebook" button + + // XXX: Maybe this looks bad and should not go here. Where should it go? + + if (!$user) { + $action->elementStart('li'); + $action->element('fb:login-button', array('onlogin' => 'refresh_page()', + 'length' => 'long')); + $action->elementEnd('li'); + } + + return false; + } + + function checkFacebookUser() { + + try { + + $facebook = getFacebook(); + $fbuid = $facebook->get_loggedin_user(); + $user = common_current_user(); + + // If you're a Facebook user and you're logged in do nothing + + // If you're a Facebook user and you're not logged in + // redirect to Facebook connect login page because that means you have clicked + // the 'connect with Facebook' button and have cookies + + if ($fbuid > 0) { + + if ($facebook->api_client->users_isAppUser($fbuid) || + $facebook->api_client->added) { + + // user should be connected... + + common_debug("Facebook user found: $fbuid"); + + if ($user) { + common_debug("Facebook user is logged in."); + return; + + } else { + common_debug("Facebook user is NOT logged in."); + common_redirect(common_local_url('fbconnectlogin'), 303); + } + + } else { + common_debug("No Facebook connect user found."); + } + } + + } catch (Exception $e) { + common_debug('Expired FB session.'); + } + + } + + function onStartLogout($action) + { + common_debug("onEndLogout()"); + + common_set_user(null); + common_real_login(false); // not logged in + common_forgetme(); // don't log back in! + + try { + + $facebook = getFacebook(); + $fbuid = $facebook->get_loggedin_user(); + + // XXX: ARGGGH this doesn't work right! + + if ($fbuid) { + $facebook->expire_session(); + $facebook->logout(common_local_url('public')); + } + + } catch (Exception $e) { + common_debug('Problem expiring FB session'); + } + + common_debug("logged out."); + + return false; + } + +} + + diff --git a/plugins/FBConnect/xd_receiver.htm b/plugins/FBConnect/xd_receiver.htm new file mode 100644 index 000000000..43fb2c4e4 --- /dev/null +++ b/plugins/FBConnect/xd_receiver.htm @@ -0,0 +1,10 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" > +<head> + <title>cross domain receiver page</title> +</head> +<body> + <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.debug.js" type="text/javascript"></script> +</body> +</html> |