From: Zach Copley Date: Tue, 16 Nov 2010 02:30:08 +0000 (+0000) Subject: - Map notices to Facebook stream items X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=ca4c0a160122d20f95877102f9712aee45c7afb8;p=quix0rs-gnu-social.git - Map notices to Facebook stream items - rename plugin FacebookBridgePlugin - delete/like/unlike notices across the bridge --- diff --git a/plugins/FacebookSSO/FacebookBridgePlugin.php b/plugins/FacebookSSO/FacebookBridgePlugin.php new file mode 100644 index 0000000000..c30ea15440 --- /dev/null +++ b/plugins/FacebookSSO/FacebookBridgePlugin.php @@ -0,0 +1,523 @@ +. + * + * @category Pugin + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +define("FACEBOOK_SERVICE", 2); + +/** + * Main class for Facebook plugin + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class FacebookBridgePlugin extends Plugin +{ + public $appId = null; // Facebook application ID + public $secret = null; // Facebook application secret + public $facebook = null; // Facebook application instance + public $dir = null; // Facebook SSO plugin dir + + /** + * Initializer for this plugin + * + * Gets an instance of the Facebook API client object + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function initialize() + { + $this->facebook = Facebookclient::getFacebook( + $this->appId, + $this->secret + ); + + return true; + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onAutoload($cls) + { + + $dir = dirname(__FILE__); + + //common_debug("class = " . $cls); + + switch ($cls) + { + case 'Facebook': // Facebook PHP SDK + include_once $dir . '/extlib/facebook.php'; + return false; + case 'FacebookloginAction': + case 'FacebookfinishloginAction': + case 'FacebookadminpanelAction': + case 'FacebooksettingsAction': + case 'FacebookdeauthorizeAction': + include_once $dir . '/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'Facebookclient': + case 'FacebookQueueHandler': + include_once $dir . '/lib/' . strtolower($cls) . '.php'; + return false; + case 'Notice_to_item': + include_once $dir . '/classes/' . $cls . '.php'; + return false; + default: + return true; + } + + } + + /** + * Database schema setup + * + * We maintain a table mapping StatusNet notices to Facebook items + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('notice_to_item', Notice_to_item::schemaDef()); + return true; + } + + /* + * Does this $action need the Facebook JavaScripts? + */ + function needsScripts($action) + { + static $needy = array( + 'FacebookloginAction', + 'FacebookfinishloginAction', + 'FacebookadminpanelAction', + 'FacebooksettingsAction' + ); + + if (in_array(get_class($action), $needy)) { + return true; + } else { + return false; + } + } + + /** + * Map URLs to actions + * + * @param Net_URL_Mapper $m path-to-action mapper + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onRouterInitialized($m) + { + // Always add the admin panel route + $m->connect('admin/facebook', array('action' => 'facebookadminpanel')); + + // Only add these routes if an application has been setup on + // Facebook for the plugin to use. + if ($this->hasApplication()) { + + $m->connect( + 'main/facebooklogin', + array('action' => 'facebooklogin') + ); + $m->connect( + 'main/facebookfinishlogin', + array('action' => 'facebookfinishlogin') + ); + $m->connect( + 'settings/facebook', + array('action' => 'facebooksettings') + ); + $m->connect( + 'facebook/deauthorize', + array('action' => 'facebookdeauthorize') + ); + + } + + return true; + } + + /* + * Add a login tab for Facebook, but only if there's a Facebook + * application defined for the plugin to use. + * + * @param Action &action the current action + * + * @return void + */ + function onEndLoginGroupNav(&$action) + { + $action_name = $action->trimmed('action'); + + if ($this->hasApplication()) { + + $action->menuItem( + common_local_url('facebooklogin'), + _m('MENU', 'Facebook'), + // TRANS: Tooltip for menu item "Facebook". + _m('Login or register using Facebook'), + 'facebooklogin' === $action_name + ); + } + + return true; + } + + /** + * Add a Facebook tab to the admin panels + * + * @param Widget $nav Admin panel nav + * + * @return boolean hook value + */ + function onEndAdminPanelNav($nav) + { + if (AdminPanelAction::canAdmin('facebook')) { + + $action_name = $nav->action->trimmed('action'); + + $nav->out->menuItem( + common_local_url('facebookadminpanel'), + // TRANS: Menu item. + _m('MENU','Facebook'), + // TRANS: Tooltip for menu item "Facebook". + _m('Facebook integration configuration'), + $action_name == 'facebookadminpanel', + 'nav_facebook_admin_panel' + ); + } + + return true; + } + + /* + * Add a tab for user-level Facebook settings + * + * @param Action &action the current action + * + * @return void + */ + function onEndConnectSettingsNav(&$action) + { + if ($this->hasApplication()) { + $action_name = $action->trimmed('action'); + + $action->menuItem( + common_local_url('facebooksettings'), + // TRANS: Menu item tab. + _m('MENU','Facebook'), + // TRANS: Tooltip for menu item "Facebook". + _m('Facebook settings'), + $action_name === 'facebooksettings' + ); + } + + return true; + } + + /* + * Is there a Facebook application for the plugin to use? + * + * Checks to see if a Facebook application ID and secret + * have been configured and a valid Facebook API client + * object exists. + * + */ + function hasApplication() + { + if (!empty($this->facebook)) { + + $appId = $this->facebook->getAppId(); + $secret = $this->facebook->getApiSecret(); + + if (!empty($appId) && !empty($secret)) { + return true; + } + + } + + return false; + } + + /* + * Output a Facebook div for the Facebook JavaSsript SDK to use + * + * @param Action $action the current action + * + */ + function onStartShowHeader($action) + { + // output
as close to as possible + $action->element('div', array('id' => 'fb-root')); + return true; + } + + /* + * Load the Facebook JavaScript SDK on pages that need them. + * + * @param Action $action the current action + * + */ + function onEndShowScripts($action) + { + if ($this->needsScripts($action)) { + + $action->script('https://connect.facebook.net/en_US/all.js'); + + $script = <<inlineScript( + sprintf($script, + json_encode($this->facebook->getAppId()), + json_encode($this->facebook->getSession()), + common_local_url('facebookfinishlogin') + ) + ); + } + } + + /* + * Log the user out of Facebook, per the Facebook authentication guide + * + * @param Action action the current action + */ + function onEndLogout($action) + { + if ($this->hasApplication()) { + $session = $this->facebook->getSession(); + $fbuser = null; + $fbuid = null; + + if ($session) { + try { + $fbuid = $this->facebook->getUser(); + $fbuser = $this->facebook->api('/me'); + } catch (FacebookApiException $e) { + common_log(LOG_ERROR, $e, __FILE__); + } + } + + if (!empty($fbuser)) { + + $logoutUrl = $this->facebook->getLogoutUrl( + array('next' => common_local_url('public')) + ); + + common_log( + LOG_INFO, + sprintf( + "Logging user out of Facebook (fbuid = %s)", + $fbuid + ), + __FILE__ + ); + common_debug("LOGOUT URL = $logoutUrl"); + common_redirect($logoutUrl, 303); + } + + } + } + + /* + * Add fbml namespace to our HTML, so Facebook's JavaScript SDK can parse + * and render XFBML tags + * + * @param Action $action the current action + * @param array $attrs array of attributes for the HTML tag + * + * @return nothing + */ + function onStartHtmlElement($action, $attrs) { + + if ($this->needsScripts($action)) { + $attrs = array_merge( + $attrs, + array('xmlns:fb' => 'http://www.facebook.com/2008/fbml') + ); + } + + return true; + } + + /** + * Add a Facebook queue item for each notice + * + * @param Notice $notice the notice + * @param array &$transports the list of transports (queues) + * + * @return boolean hook return + */ + function onStartEnqueueNotice($notice, &$transports) + { + if (self::hasApplication() && $notice->isLocal()) { + array_push($transports, 'facebook'); + } + return true; + } + + /** + * Register Facebook notice queue handler + * + * @param QueueManager $manager + * + * @return boolean hook return + */ + function onEndInitializeQueueManager($manager) + { + if (self::hasApplication()) { + $manager->connect('facebook', 'FacebookQueueHandler'); + } + return true; + } + + /* + * Use SSL for Facebook stuff + * + * @param string $action name + * @param boolean $ssl outval to force SSL + * @return mixed hook return value + */ + function onSensitiveAction($action, &$ssl) + { + $sensitive = array( + 'facebookadminpanel', + 'facebooksettings', + 'facebooklogin', + 'facebookfinishlogin' + ); + + if (in_array($action, $sensitive)) { + $ssl = true; + return false; + } else { + return true; + } + } + + /** + * If a notice gets deleted, remove the Notice_to_item mapping and + * delete the item on Facebook + * + * @param User $user The user doing the deleting + * @param Notice $notice The notice getting deleted + * + * @return boolean hook value + */ + function onStartDeleteOwnNotice(User $user, Notice $notice) + { + $client = new Facebookclient($notice); + $client->streamRemove(); + + return true; + } + + /** + * Notify remote users when their notices get favorited. + * + * @param Profile or User $profile of local user doing the faving + * @param Notice $notice being favored + * @return hook return value + */ + function onEndFavorNotice(Profile $profile, Notice $notice) + { + $client = new Facebookclient($notice); + $client->like(); + + return true; + } + + /** + * Notify remote users when their notices get de-favorited. + * + * @param Profile $profile Profile person doing the de-faving + * @param Notice $notice Notice being favored + * + * @return hook return value + */ + function onEndDisfavorNotice(Profile $profile, Notice $notice) + { + $client = new Facebookclient($notice); + $client->unLike(); + + return true; + } + + /* + * Add version info for this plugin + * + * @param array &$versions plugin version descriptions + */ + function onPluginVersion(&$versions) + { + $versions[] = array( + 'name' => 'Facebook Single-Sign-On', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews, Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:FacebookBridge', + 'rawdescription' => + _m('A plugin for integrating StatusNet with Facebook.') + ); + + return true; + } +} diff --git a/plugins/FacebookSSO/FacebookSSOPlugin.php b/plugins/FacebookSSO/FacebookSSOPlugin.php deleted file mode 100644 index 19d61211d8..0000000000 --- a/plugins/FacebookSSO/FacebookSSOPlugin.php +++ /dev/null @@ -1,457 +0,0 @@ -. - * - * @category Pugin - * @package StatusNet - * @author Zach Copley - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -define("FACEBOOK_SERVICE", 2); - -/** - * Main class for Facebook plugin - * - * @category Plugin - * @package StatusNet - * @author Zach Copley - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ -class FacebookSSOPlugin extends Plugin -{ - public $appId = null; // Facebook application ID - public $apikey = null; // Facebook API key (for deprecated "Old REST API") - public $secret = null; // Facebook application secret - public $facebook = null; // Facebook application instance - public $dir = null; // Facebook SSO plugin dir - - /** - * Initializer for this plugin - * - * Gets an instance of the Facebook API client object - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function initialize() - { - $this->facebook = Facebookclient::getFacebook( - $this->appId, - $this->apikey, - $this->secret - ); - - return true; - } - - /** - * Load related modules when needed - * - * @param string $cls Name of the class to be loaded - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function onAutoload($cls) - { - - $dir = dirname(__FILE__); - - //common_debug("class = " . $cls); - - switch ($cls) - { - case 'Facebook': // Facebook PHP SDK - include_once $dir . '/extlib/facebook.php'; - return false; - case 'FacebookloginAction': - case 'FacebookfinishloginAction': - case 'FacebookadminpanelAction': - case 'FacebooksettingsAction': - case 'FacebookdeauthorizeAction': - include_once $dir . '/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; - return false; - case 'Facebookclient': - case 'FacebookQueueHandler': - include_once $dir . '/lib/' . strtolower($cls) . '.php'; - return false; - default: - return true; - } - - } - - /* - * Does this $action need the Facebook JavaScripts? - */ - function needsScripts($action) - { - static $needy = array( - 'FacebookloginAction', - 'FacebookfinishloginAction', - 'FacebookadminpanelAction', - 'FacebooksettingsAction' - ); - - if (in_array(get_class($action), $needy)) { - return true; - } else { - return false; - } - } - - /** - * Map URLs to actions - * - * @param Net_URL_Mapper $m path-to-action mapper - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function onRouterInitialized($m) - { - // Always add the admin panel route - $m->connect('admin/facebook', array('action' => 'facebookadminpanel')); - - // Only add these routes if an application has been setup on - // Facebook for the plugin to use. - if ($this->hasApplication()) { - - $m->connect( - 'main/facebooklogin', - array('action' => 'facebooklogin') - ); - $m->connect( - 'main/facebookfinishlogin', - array('action' => 'facebookfinishlogin') - ); - $m->connect( - 'settings/facebook', - array('action' => 'facebooksettings') - ); - $m->connect( - 'facebook/deauthorize', - array('action' => 'facebookdeauthorize') - ); - - } - - return true; - } - - /* - * Add a login tab for Facebook, but only if there's a Facebook - * application defined for the plugin to use. - * - * @param Action &action the current action - * - * @return void - */ - function onEndLoginGroupNav(&$action) - { - $action_name = $action->trimmed('action'); - - if ($this->hasApplication()) { - - $action->menuItem( - common_local_url('facebooklogin'), - _m('MENU', 'Facebook'), - // TRANS: Tooltip for menu item "Facebook". - _m('Login or register using Facebook'), - 'facebooklogin' === $action_name - ); - } - - return true; - } - - /** - * Add a Facebook tab to the admin panels - * - * @param Widget $nav Admin panel nav - * - * @return boolean hook value - */ - function onEndAdminPanelNav($nav) - { - if (AdminPanelAction::canAdmin('facebook')) { - - $action_name = $nav->action->trimmed('action'); - - $nav->out->menuItem( - common_local_url('facebookadminpanel'), - // TRANS: Menu item. - _m('MENU','Facebook'), - // TRANS: Tooltip for menu item "Facebook". - _m('Facebook integration configuration'), - $action_name == 'facebookadminpanel', - 'nav_facebook_admin_panel' - ); - } - - return true; - } - - /* - * Add a tab for user-level Facebook settings - * - * @param Action &action the current action - * - * @return void - */ - function onEndConnectSettingsNav(&$action) - { - if ($this->hasApplication()) { - $action_name = $action->trimmed('action'); - - $action->menuItem( - common_local_url('facebooksettings'), - // TRANS: Menu item tab. - _m('MENU','Facebook'), - // TRANS: Tooltip for menu item "Facebook". - _m('Facebook settings'), - $action_name === 'facebooksettings' - ); - } - - return true; - } - - /* - * Is there a Facebook application for the plugin to use? - * - * Checks to see if a Facebook application ID and secret - * have been configured and a valid Facebook API client - * object exists. - * - */ - function hasApplication() - { - if (!empty($this->facebook)) { - - $appId = $this->facebook->getAppId(); - $secret = $this->facebook->getApiSecret(); - - if (!empty($appId) && !empty($secret)) { - return true; - } - - } - - return false; - } - - /* - * Output a Facebook div for the Facebook JavaSsript SDK to use - * - * @param Action $action the current action - * - */ - function onStartShowHeader($action) - { - // output
as close to as possible - $action->element('div', array('id' => 'fb-root')); - return true; - } - - /* - * Load the Facebook JavaScript SDK on pages that need them. - * - * @param Action $action the current action - * - */ - function onEndShowScripts($action) - { - if ($this->needsScripts($action)) { - - $action->script('https://connect.facebook.net/en_US/all.js'); - - $script = <<inlineScript( - sprintf($script, - json_encode($this->facebook->getAppId()), - json_encode($this->facebook->getSession()), - common_local_url('facebookfinishlogin') - ) - ); - } - } - - /* - * Log the user out of Facebook, per the Facebook authentication guide - * - * @param Action action the current action - */ - function onEndLogout($action) - { - if ($this->hasApplication()) { - $session = $this->facebook->getSession(); - $fbuser = null; - $fbuid = null; - - if ($session) { - try { - $fbuid = $this->facebook->getUser(); - $fbuser = $this->facebook->api('/me'); - } catch (FacebookApiException $e) { - common_log(LOG_ERROR, $e, __FILE__); - } - } - - if (!empty($fbuser)) { - - $logoutUrl = $this->facebook->getLogoutUrl( - array('next' => common_local_url('public')) - ); - - common_log( - LOG_INFO, - sprintf( - "Logging user out of Facebook (fbuid = %s)", - $fbuid - ), - __FILE__ - ); - common_debug("LOGOUT URL = $logoutUrl"); - common_redirect($logoutUrl, 303); - } - - } - } - - /* - * Add fbml namespace to our HTML, so Facebook's JavaScript SDK can parse - * and render XFBML tags - * - * @param Action $action the current action - * @param array $attrs array of attributes for the HTML tag - * - * @return nothing - */ - function onStartHtmlElement($action, $attrs) { - - if ($this->needsScripts($action)) { - $attrs = array_merge( - $attrs, - array('xmlns:fb' => 'http://www.facebook.com/2008/fbml') - ); - } - - return true; - } - - /** - * Add a Facebook queue item for each notice - * - * @param Notice $notice the notice - * @param array &$transports the list of transports (queues) - * - * @return boolean hook return - */ - function onStartEnqueueNotice($notice, &$transports) - { - if (self::hasApplication() && $notice->isLocal()) { - array_push($transports, 'facebook'); - } - return true; - } - - /** - * Register Facebook notice queue handler - * - * @param QueueManager $manager - * - * @return boolean hook return - */ - function onEndInitializeQueueManager($manager) - { - if (self::hasApplication()) { - $manager->connect('facebook', 'FacebookQueueHandler'); - } - return true; - } - - /* - * Use SSL for Facebook stuff - * - * @param string $action name - * @param boolean $ssl outval to force SSL - * @return mixed hook return value - */ - function onSensitiveAction($action, &$ssl) - { - $sensitive = array( - 'facebookadminpanel', - 'facebooksettings', - 'facebooklogin', - 'facebookfinishlogin' - ); - - if (in_array($action, $sensitive)) { - $ssl = true; - return false; - } else { - return true; - } - } - - /* - * Add version info for this plugin - * - * @param array &$versions plugin version descriptions - */ - function onPluginVersion(&$versions) - { - $versions[] = array( - 'name' => 'Facebook Single-Sign-On', - 'version' => STATUSNET_VERSION, - 'author' => 'Craig Andrews, Zach Copley', - 'homepage' => 'http://status.net/wiki/Plugin:FacebookSSO', - 'rawdescription' => - _m('A plugin for integrating StatusNet with Facebook.') - ); - - return true; - } -} diff --git a/plugins/FacebookSSO/actions/facebookdeauthorize.php b/plugins/FacebookSSO/actions/facebookdeauthorize.php index fb4afa13bc..cb816fc54a 100644 --- a/plugins/FacebookSSO/actions/facebookdeauthorize.php +++ b/plugins/FacebookSSO/actions/facebookdeauthorize.php @@ -112,7 +112,7 @@ class FacebookdeauthorizeAction extends Action common_log( LOG_WARNING, sprintf( - '%s (%d), fbuid $s has deauthorized his/her Facebook ' + '%s (%d), fbuid %d has deauthorized his/her Facebook ' . 'connection but hasn\'t set a password so s/he ' . 'is locked out.', $user->nickname, @@ -135,8 +135,8 @@ class FacebookdeauthorizeAction extends Action ); } else { // It probably wasn't Facebook that hit this action, - // so redirect to the login page - common_redirect(common_local_url('login'), 303); + // so redirect to the public timeline + common_redirect(common_local_url('public'), 303); } } } diff --git a/plugins/FacebookSSO/actions/facebookfinishlogin.php b/plugins/FacebookSSO/actions/facebookfinishlogin.php index e61f351547..2174c5ad4a 100644 --- a/plugins/FacebookSSO/actions/facebookfinishlogin.php +++ b/plugins/FacebookSSO/actions/facebookfinishlogin.php @@ -97,7 +97,7 @@ class FacebookfinishloginAction extends Action parent::handle($args); if (common_is_real_login()) { - + // User is already logged in, are her accounts already linked? $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE); @@ -121,48 +121,52 @@ class FacebookfinishloginAction extends Action } else { // Possibly reconnect an existing account - + $this->connectUser(); } } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost(); + } else { + $this->tryLogin(); + } + } - $token = $this->trimmed('token'); + function handlePost() + { + $token = $this->trimmed('token'); - if (!$token || $token != common_session_token()) { + if (!$token || $token != common_session_token()) { + $this->showForm( + _m('There was a problem with your session token. Try again, please.') + ); + return; + } + + if ($this->arg('create')) { + + if (!$this->boolean('license')) { $this->showForm( - _m('There was a problem with your session token. Try again, please.')); + _m('You can\'t register if you don\'t agree to the license.'), + $this->trimmed('newname') + ); return; } - if ($this->arg('create')) { - - if (!$this->boolean('license')) { - $this->showForm( - _m('You can\'t register if you don\'t agree to the license.'), - $this->trimmed('newname') - ); - return; - } - - // We has a valid Facebook session and the Facebook user has - // agreed to the SN license, so create a new user - $this->createNewUser(); - - } else if ($this->arg('connect')) { + // We has a valid Facebook session and the Facebook user has + // agreed to the SN license, so create a new user + $this->createNewUser(); - $this->connectNewUser(); + } else if ($this->arg('connect')) { - } else { + $this->connectNewUser(); - $this->showForm( - _m('An unknown error has occured.'), - $this->trimmed('newname') - ); - } } else { - $this->tryLogin(); + $this->showForm( + _m('An unknown error has occured.'), + $this->trimmed('newname') + ); } } @@ -173,7 +177,7 @@ class FacebookfinishloginAction extends Action $this->element('div', array('class' => 'error'), $this->error); } else { - + $this->element( 'div', 'instructions', // TRANS: %s is the site name. @@ -343,19 +347,23 @@ class FacebookfinishloginAction extends Action 'nickname' => $nickname, 'fullname' => $this->fbuser['first_name'] . ' ' . $this->fbuser['last_name'], - 'email' => $this->fbuser['email'], - 'email_confirmed' => true, 'homepage' => $this->fbuser['website'], 'bio' => $this->fbuser['about'], 'location' => $this->fbuser['location']['name'] ); + // It's possible that the email address is already in our + // DB. It's a unique key, so we need to check + if ($this->isNewEmail($this->fbuser['email'])) { + $args['email'] = $this->fbuser['email']; + $args['email_confirmed'] = true; + } + if (!empty($invite)) { $args['code'] = $invite->code; } - $user = User::register($args); - + $user = User::register($args); $result = $this->flinkUser($user->id, $this->fbuid); if (!$result) { @@ -363,6 +371,9 @@ class FacebookfinishloginAction extends Action return; } + // Add a Foreign_user record + Facebookclient::addFacebookUser($this->fbuser); + $this->setAvatar($user); common_set_user($user); @@ -371,20 +382,16 @@ class FacebookfinishloginAction extends Action common_log( LOG_INFO, sprintf( - 'Registered new user %d from Facebook user %s', + 'Registered new user %s (%d) from Facebook user %s, (fbuid %d)', + $user->nickname, $user->id, + $this->fbuser['name'], $this->fbuid ), __FILE__ ); - common_redirect( - common_local_url( - 'showstream', - array('nickname' => $user->nickname) - ), - 303 - ); + $this->goHome($user->nickname); } /* @@ -401,17 +408,19 @@ class FacebookfinishloginAction extends Action // fetch the picture from Facebook $client = new HTTPClient(); - common_debug("status = $status - " . $finalUrl , __FILE__); - // fetch the actual picture $response = $client->get($picUrl); if ($response->isOk()) { $finalUrl = $client->getUrl(); - $filename = 'facebook-' . substr(strrchr($finalUrl, '/'), 1 ); - common_debug("Filename = " . $filename, __FILE__); + // Make sure the filename is unique becuase it's possible for a user + // to deauthorize our app, and then come back in as a new user but + // have the same Facebook picture (avatar URLs have a unique index + // and their URLs are based on the filenames). + $filename = 'facebook-' . common_good_rand(4) . '-' + . substr(strrchr($finalUrl, '/'), 1); $ok = file_put_contents( Avatar::path($filename), @@ -430,17 +439,20 @@ class FacebookfinishloginAction extends Action } else { + // save it as an avatar $profile = $user->getProfile(); if ($profile->setOriginal($filename)) { common_log( LOG_INFO, sprintf( - 'Saved avatar for %s (%d) from Facebook profile %s, filename = %s', + 'Saved avatar for %s (%d) from Facebook picture for ' + . '%s (fbuid %d), filename = %s', $user->nickname, $user->id, + $this->fbuser['name'], $this->fbuid, - $picture + $filename ), __FILE__ ); @@ -462,19 +474,17 @@ class FacebookfinishloginAction extends Action $user = User::staticGet('nickname', $nickname); if (!empty($user)) { - common_debug('Facebook Connect Plugin - ' . - "Legit user to connect to Facebook: $nickname"); - } - - $result = $this->flinkUser($user->id, $this->fbuid); - - if (!$result) { - $this->serverError(_m('Error connecting user to Facebook.')); - return; + common_debug( + sprintf( + 'Found a legit user to connect to Facebook: %s (%d)', + $user->nickname, + $user->id + ), + __FILE__ + ); } - common_debug('Facebook Connnect Plugin - ' . - "Connected Facebook user $this->fbuid to local user $user->id"); + $this->tryLinkUser($user); common_set_user($user); common_real_login(true); @@ -485,7 +495,12 @@ class FacebookfinishloginAction extends Action function connectUser() { $user = common_current_user(); + $this->tryLinkUser($user); + common_redirect(common_local_url('facebookfinishlogin'), 303); + } + function tryLinkUser($user) + { $result = $this->flinkUser($user->id, $this->fbuid); if (empty($result)) { @@ -495,14 +510,14 @@ class FacebookfinishloginAction extends Action common_debug( sprintf( - 'Connected Facebook user %s to local user %d', + 'Connected Facebook user %s (fbuid %d) to local user %s (%d)', + $this->fbuser['name'], $this->fbuid, + $user->nickname, $user->id ), __FILE__ ); - - common_redirect(common_local_url('facebookfinishlogin'), 303); } function tryLogin() @@ -573,7 +588,7 @@ class FacebookfinishloginAction extends Action $flink->user_id = $user_id; $flink->foreign_id = $fbuid; $flink->service = FACEBOOK_SERVICE; - + // Pull the access token from the Facebook cookies $flink->credentials = $this->facebook->getAccessToken(); @@ -595,8 +610,8 @@ class FacebookfinishloginAction extends Action // Try the full name - $fullname = trim($this->fbuser['firstname'] . - ' ' . $this->fbuser['lastname']); + $fullname = trim($this->fbuser['first_name'] . + ' ' . $this->fbuser['last_name']); if (!empty($fullname)) { $fullname = $this->nicknamize($fullname); @@ -617,20 +632,57 @@ class FacebookfinishloginAction extends Action return strtolower($str); } - function isNewNickname($str) - { - if (!Validate::string($str, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { + /* + * Is the desired nickname already taken? + * + * @return boolean result + */ + function isNewNickname($str) + { + if ( + !Validate::string( + $str, + array( + 'min_length' => 1, + 'max_length' => 64, + 'format' => NICKNAME_FMT + ) + ) + ) { return false; } + if (!User::allowed_nickname($str)) { return false; } + if (User::staticGet('nickname', $str)) { return false; } + return true; } + /* + * Do we already have a user record with this email? + * (emails have to be unique but they can change) + * + * @param string $email the email address to check + * + * @return boolean result + */ + function isNewEmail($email) + { + // we shouldn't have to validate the format + $result = User::staticGet('email', $email); + + if (empty($result)) { + common_debug("XXXXXXXXXXXXXXXXXX We've never seen this email before!!!"); + return true; + } + common_debug("XXXXXXXXXXXXXXXXXX dupe email address!!!!"); + + return false; + } + } diff --git a/plugins/FacebookSSO/actions/facebooklogin.php b/plugins/FacebookSSO/actions/facebooklogin.php index 08c237fe6e..9a230b7241 100644 --- a/plugins/FacebookSSO/actions/facebooklogin.php +++ b/plugins/FacebookSSO/actions/facebooklogin.php @@ -89,7 +89,7 @@ class FacebookloginAction extends Action $attrs = array( 'src' => common_path( - 'plugins/FacebookSSO/images/login-button.png', + 'plugins/FacebookBridge/images/login-button.png', true ), 'alt' => 'Login with Facebook', diff --git a/plugins/FacebookSSO/classes/Notice_to_item.php b/plugins/FacebookSSO/classes/Notice_to_item.php new file mode 100644 index 0000000000..a6a8030342 --- /dev/null +++ b/plugins/FacebookSSO/classes/Notice_to_item.php @@ -0,0 +1,190 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * 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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for mapping notices to Facebook stream items + * + * Note that notice_id is unique only within a single database; if you + * want to share this data for some reason, get the notice's URI and use + * that instead, since it's universally unique. + * + * @category Action + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Notice_to_item extends Memcached_DataObject +{ + public $__table = 'notice_to_item'; // table name + public $notice_id; // int(4) primary_key not_null + public $item_id; // varchar(255) not null + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup + * @param mixed $v Value to lookup + * + * @return Notice_to_item object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Notice_to_item', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array( + 'notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'item_id' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL + ); + } + + static function schemaDef() + { + return array( + new ColumnDef('notice_id', 'integer', null, false, 'PRI'), + new ColumnDef('item_id', 'varchar', 255, false, 'UNI'), + new ColumnDef('created', 'datetime', null, false) + ); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + + function keyTypes() + { + return array('notice_id' => 'K', 'item_id' => 'U'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Save a mapping between a notice and a Facebook item + * + * @param integer $notice_id ID of the notice in StatusNet + * @param integer $item_id ID of the stream item on Facebook + * + * @return Notice_to_item new object for this value + */ + + static function saveNew($notice_id, $item_id) + { + $n2i = Notice_to_item::staticGet('notice_id', $notice_id); + + if (!empty($n2i)) { + return $n2i; + } + + $n2i = Notice_to_item::staticGet('item_id', $item_id); + + if (!empty($n2i)) { + return $n2i; + } + + common_debug( + "Mapping notice {$notice_id} to Facebook item {$item_id}", + __FILE__ + ); + + $n2i = new Notice_to_item(); + + $n2i->notice_id = $notice_id; + $n2i->item_id = $item_id; + $n2i->created = common_sql_now(); + + $n2i->insert(); + + return $n2i; + } +} diff --git a/plugins/FacebookSSO/lib/facebookclient.php b/plugins/FacebookSSO/lib/facebookclient.php index cf00b55e3a..33edf5c6b1 100644 --- a/plugins/FacebookSSO/lib/facebookclient.php +++ b/plugins/FacebookSSO/lib/facebookclient.php @@ -173,11 +173,11 @@ class Facebookclient if ($this->isFacebookBound()) { common_debug("notice is facebook bound", __FILE__); if (empty($this->flink->credentials)) { - $this->sendOldRest(); + return $this->sendOldRest(); } else { // Otherwise we most likely have an access token - $this->sendGraph(); + return $this->sendGraph(); } } else { @@ -213,6 +213,7 @@ class Facebookclient $params = array( 'access_token' => $this->flink->credentials, + // XXX: Need to worrry about length of the message? 'message' => $this->notice->content ); @@ -220,7 +221,7 @@ class Facebookclient if (!empty($attachments)) { - // We can only send one attachment with the Graph API + // We can only send one attachment with the Graph API :( $first = array_shift($attachments); @@ -240,6 +241,21 @@ class Facebookclient sprintf('/%s/feed', $fbuid), 'post', $params ); + // Save a mapping + Notice_to_item::saveNew($this->notice->id, $result['id']); + + common_log( + LOG_INFO, + sprintf( + "Posted notice %d as a stream item for %s (%d), fbuid %s", + $this->notice->id, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + } catch (FacebookApiException $e) { return $this->handleFacebookError($e); } @@ -481,24 +497,42 @@ class Facebookclient $result = $this->facebook->api( array( 'method' => 'users.setStatus', - 'status' => $this->notice->content, + 'status' => $this->formatMessage(), 'status_includes_verb' => true, 'uid' => $fbuid ) ); - common_log( - LOG_INFO, - sprintf( - "Posted notice %s as a status update for %s (%d), fbuid %s", + if ($result == 1) { // 1 is success + + common_log( + LOG_INFO, + sprintf( + "Posted notice %s as a status update for %s (%d), fbuid %s", + $this->notice->id, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + // There is no item ID returned for status update so we can't + // save a Notice_to_item mapping + + } else { + + $msg = sprintf( + "Error posting notice %s as a status update for %s (%d), fbuid %s - error code: %s", $this->notice->id, $this->user->nickname, $this->user->id, - $fbuid - ), - __FILE__ - ); + $fbuid, + $result // will contain 0, or an error + ); + throw new FacebookApiException($msg, $result); + } } /* @@ -524,25 +558,66 @@ class Facebookclient $result = $this->facebook->api( array( 'method' => 'stream.publish', - 'message' => $this->notice->content, + 'message' => $this->formatMessage(), 'attachment' => $fbattachment, 'uid' => $fbuid ) ); - common_log( - LOG_INFO, - sprintf( - 'Posted notice %d as a %s for %s (%d), fbuid %s', + if (!empty($result)) { // result will contain the item ID + + // Save a mapping + Notice_to_item::saveNew($this->notice->id, $result); + + common_log( + LOG_INFO, + sprintf( + 'Posted notice %d as a %s for %s (%d), fbuid %s', + $this->notice->id, + empty($fbattachment) ? 'stream item' : 'stream item with attachment', + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + } else { + + $msg = sprintf( + 'Could not post notice %d as a %s for %s (%d), fbuid %s - error code: %s', $this->notice->id, empty($fbattachment) ? 'stream item' : 'stream item with attachment', $this->user->nickname, $this->user->id, + $result, // result will contain an error code $fbuid - ), - __FILE__ - ); + ); + + throw new FacebookApiException($msg, $result); + } + } + /* + * Format the text message of a stream item so it's appropriate for + * sending to Facebook. If the notice is too long, truncate it, and + * add a linkback to the original notice at the end. + * + * @return String $txt the formated message + */ + function formatMessage() + { + // Start with the plaintext source of this notice... + $txt = $this->notice->content; + + // Facebook has a 420-char hardcoded max. + if (mb_strlen($statustxt) > 420) { + $noticeUrl = common_shorten_url($this->notice->uri); + $urlLen = mb_strlen($noticeUrl); + $txt = mb_substr($statustxt, 0, 420 - ($urlLen + 3)) . ' … ' . $noticeUrl; + } + + return $txt; } /* @@ -708,4 +783,240 @@ BODY; return mail_to_user($this->user, $subject, $body); } + /* + * Check to see if we have a mapping to a copy of this notice + * on Facebook + * + * @param Notice $notice the notice to check + * + * @return mixed null if it can't find one, or the id of the Facebook + * stream item + */ + static function facebookStatusId($notice) + { + $n2i = Notice_to_item::staticGet('notice_id', $notice->id); + + if (empty($n2i)) { + return null; + } else { + return $n2i->item_id; + } + } + + /* + * Save a Foreign_user record of a Facebook user + * + * @param object $fbuser a Facebook Graph API user obj + * See: http://developers.facebook.com/docs/reference/api/user + * @return mixed $result Id or key + * + */ + static function addFacebookUser($fbuser) + { + // remove any existing, possibly outdated, record + $luser = Foreign_user::getForeignUser($fbuser['id'], FACEBOOK_SERVICE); + + if (!empty($luser)) { + + $result = $luser->delete(); + + if ($result != false) { + common_log( + LOG_INFO, + sprintf( + 'Removed old Facebook user: %s, fbuid %d', + $fbuid['name'], + $fbuid['id'] + ), + __FILE__ + ); + } + } + + $fuser = new Foreign_user(); + + $fuser->nickname = $fbuser['name']; + $fuser->uri = $fbuser['link']; + $fuser->id = $fbuser['id']; + $fuser->service = FACEBOOK_SERVICE; + $fuser->created = common_sql_now(); + + $result = $fuser->insert(); + + if (empty($result)) { + common_log( + LOG_WARNING, + sprintf( + 'Failed to add new Facebook user: %s, fbuid %d', + $fbuser['name'], + $fbuser['id'] + ), + __FILE__ + ); + + common_log_db_error($fuser, 'INSERT', __FILE__); + } else { + common_log( + LOG_INFO, + sprintf( + 'Added new Facebook user: %s, fbuid %d', + $fbuser['name'], + $fbuser['id'] + ), + __FILE__ + ); + } + + return $result; + } + + /* + * Remove an item from a Facebook user's feed if we have a mapping + * for it. + */ + function streamRemove() + { + $n2i = Notice_to_item::staticGet('notice_id', $this->notice->id); + + if (!empty($this->flink) && !empty($n2i)) { + + $result = $this->facebook->api( + array( + 'method' => 'stream.remove', + 'post_id' => $n2i->item_id, + 'uid' => $this->flink->foreign_id + ) + ); + + if (!empty($result) && result == true) { + + common_log( + LOG_INFO, + sprintf( + 'Deleted Facebook item: %s for %s (%d), fbuid %d', + $n2i->item_id, + $this->user->nickname, + $this->user->id, + $this->flink->foreign_id + ), + __FILE__ + ); + + $n2i->delete(); + + } else { + + common_log( + LOG_WARNING, + sprintf( + 'Could not deleted Facebook item: %s for %s (%d), fbuid %d', + $n2i->item_id, + $this->user->nickname, + $this->user->id, + $this->flink->foreign_id + ), + __FILE__ + ); + } + } + } + + /* + * Like an item in a Facebook user's feed if we have a mapping + * for it. + */ + function like() + { + $n2i = Notice_to_item::staticGet('notice_id', $this->notice->id); + + if (!empty($this->flink) && !empty($n2i)) { + + $result = $this->facebook->api( + array( + 'method' => 'stream.addlike', + 'post_id' => $n2i->item_id, + 'uid' => $this->flink->foreign_id + ) + ); + + if (!empty($result) && result == true) { + + common_log( + LOG_INFO, + sprintf( + 'Added like for item: %s for %s (%d), fbuid %d', + $n2i->item_id, + $this->user->nickname, + $this->user->id, + $this->flink->foreign_id + ), + __FILE__ + ); + + } else { + + common_log( + LOG_WARNING, + sprintf( + 'Could not like Facebook item: %s for %s (%d), fbuid %d', + $n2i->item_id, + $this->user->nickname, + $this->user->id, + $this->flink->foreign_id + ), + __FILE__ + ); + } + } + } + + /* + * Unlike an item in a Facebook user's feed if we have a mapping + * for it. + */ + function unLike() + { + $n2i = Notice_to_item::staticGet('notice_id', $this->notice->id); + + if (!empty($this->flink) && !empty($n2i)) { + + $result = $this->facebook->api( + array( + 'method' => 'stream.removeLike', + 'post_id' => $n2i->item_id, + 'uid' => $this->flink->foreign_id + ) + ); + + if (!empty($result) && result == true) { + + common_log( + LOG_INFO, + sprintf( + 'Removed like for item: %s for %s (%d), fbuid %d', + $n2i->item_id, + $this->user->nickname, + $this->user->id, + $this->flink->foreign_id + ), + __FILE__ + ); + + } else { + + common_log( + LOG_WARNING, + sprintf( + 'Could not remove like for Facebook item: %s for %s (%d), fbuid %d', + $n2i->item_id, + $this->user->nickname, + $this->user->id, + $this->flink->foreign_id + ), + __FILE__ + ); + } + } + } + }