diff options
Diffstat (limited to 'plugins/FacebookBridge')
-rw-r--r-- | plugins/FacebookBridge/FacebookBridgePlugin.php | 523 | ||||
-rw-r--r-- | plugins/FacebookBridge/actions/facebookadminpanel.php | 212 | ||||
-rw-r--r-- | plugins/FacebookBridge/actions/facebookdeauthorize.php | 214 | ||||
-rw-r--r-- | plugins/FacebookBridge/actions/facebookfinishlogin.php | 688 | ||||
-rw-r--r-- | plugins/FacebookBridge/actions/facebooklogin.php | 122 | ||||
-rw-r--r-- | plugins/FacebookBridge/actions/facebooksettings.php | 264 | ||||
-rw-r--r-- | plugins/FacebookBridge/classes/Notice_to_item.php | 190 | ||||
-rw-r--r-- | plugins/FacebookBridge/extlib/facebook.php | 963 | ||||
-rw-r--r-- | plugins/FacebookBridge/extlib/fb_ca_chain_bundle.crt | 121 | ||||
-rw-r--r-- | plugins/FacebookBridge/images/login-button.png | bin | 0 -> 1661 bytes | |||
-rw-r--r-- | plugins/FacebookBridge/lib/facebookclient.php | 1022 | ||||
-rw-r--r-- | plugins/FacebookBridge/lib/facebookqueuehandler.php | 61 |
12 files changed, 4380 insertions, 0 deletions
diff --git a/plugins/FacebookBridge/FacebookBridgePlugin.php b/plugins/FacebookBridge/FacebookBridgePlugin.php new file mode 100644 index 000000000..c30ea1544 --- /dev/null +++ b/plugins/FacebookBridge/FacebookBridgePlugin.php @@ -0,0 +1,523 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * A plugin for integrating Facebook with StatusNet. Includes single-sign-on + * and publishing notices to Facebook using Facebook's Graph API. + * + * PHP version 5 + * + * 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 Pugin + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @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 <zach@status.net> + * @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 <div id="fb-root"></div> as close to <body> 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 = <<<ENDOFSCRIPT +FB.init({appId: %1\$s, session: %2\$s, status: true, cookie: true, xfbml: true}); + +$('#facebook_button').bind('click', function(event) { + + event.preventDefault(); + + FB.login(function(response) { + if (response.session && response.perms) { + window.location.href = '%3\$s'; + } else { + // NOP (user cancelled login) + } + }, {perms:'read_stream,publish_stream,offline_access,user_status,user_location,user_website,email'}); +}); +ENDOFSCRIPT; + + $action->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/FacebookBridge/actions/facebookadminpanel.php b/plugins/FacebookBridge/actions/facebookadminpanel.php new file mode 100644 index 000000000..61b544184 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebookadminpanel.php @@ -0,0 +1,212 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Facebook integration administration panel + * + * 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 Settings + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer global Facebook integration settings + * + * @category Admin + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class FacebookadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + function title() + { + return _m('Facebook'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + function getInstructions() + { + return _m('Facebook integration settings'); + } + + /** + * Show the Facebook admin panel form + * + * @return void + */ + function showForm() + { + $form = new FacebookAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + function saveSettings() + { + static $settings = array( + 'facebook' => array('appid', 'secret'), + ); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = $this->trimmed($setting); + } + } + + // This throws an exception on validation errors + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + // appId, key and secret (can't be too long) + + if (mb_strlen($values['facebook']['appid']) > 255) { + $this->clientError( + _m("Invalid Facebook ID. Max length is 255 characters.") + ); + } + + if (mb_strlen($values['facebook']['secret']) > 255) { + $this->clientError( + _m("Invalid Facebook secret. Max length is 255 characters.") + ); + } + } +} + +class FacebookAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + function id() + { + return 'facebookadminpanel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('facebookadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_facebook-application') + ); + $this->out->element('legend', null, _m('Facebook application settings')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->input( + 'appid', + _m('Application ID'), + _m('ID of your Facebook application'), + 'facebook' + ); + $this->unli(); + + $this->li(); + $this->input( + 'secret', + _m('Secret'), + _m('Application secret'), + 'facebook' + ); + $this->unli(); + + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + $this->out->submit('submit', _m('Save'), 'submit', null, _m('Save Facebook settings')); + } +} diff --git a/plugins/FacebookBridge/actions/facebookdeauthorize.php b/plugins/FacebookBridge/actions/facebookdeauthorize.php new file mode 100644 index 000000000..cb816fc54 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebookdeauthorize.php @@ -0,0 +1,214 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * An action that handles deauthorize callbacks from Facebook + * + * PHP version 5 + * + * 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 StatusNet + * @author Zach Copley <zach@status.net> + * @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); +} + +/* + * Action class for handling deauthorize callbacks from Facebook. If the user + * doesn't have a password let her know she'll need to contact the site + * admin to get back into her account (if possible). + */ +class FacebookdeauthorizeAction extends Action +{ + private $facebook; + + /** + * For initializing members of the class. + * + * @param array $args misc. arguments + * + * @return boolean true + */ + function prepare($args) + { + $this->facebook = Facebookclient::getFacebook(); + + return true; + } + + /** + * Handler method + * + * @param array $args is ignored since it's now passed in in prepare() + */ + function handle($args) + { + parent::handle($args); + + $data = $this->facebook->getSignedRequest(); + + if (isset($data['user_id'])) { + + $fbuid = $data['user_id']; + + $flink = Foreign_link::getByForeignID($fbuid, FACEBOOK_SERVICE); + $user = $flink->getUser(); + + // Remove the link to Facebook + $result = $flink->delete(); + + if (!$result) { + common_log_db_error($flink, 'DELETE', __FILE__); + common_log( + LOG_WARNING, + sprintf( + 'Unable to delete Facebook foreign link ' + . 'for %s (%d), fbuid %s', + $user->nickname, + $user->id, + $fbuid + ), + __FILE__ + ); + return; + } + + common_log( + LOG_INFO, + sprintf( + 'Facebook callback: %s (%d), fbuid %s has deauthorized ' + . 'the Facebook application.', + $user->nickname, + $user->id, + $fbuid + ), + __FILE__ + ); + + // Warn the user about being locked out of their account + // if we can. + if (empty($user->password) && !empty($user->email)) { + $this->emailWarn($user); + } else { + common_log( + LOG_WARNING, + sprintf( + '%s (%d), fbuid %d has deauthorized his/her Facebook ' + . 'connection but hasn\'t set a password so s/he ' + . 'is locked out.', + $user->nickname, + $user->id, + $fbuid + ), + __FILE__ + ); + } + + } else { + if (!empty($data)) { + common_log( + LOG_WARNING, + sprintf( + 'Facebook called the deauthorize callback ' + . ' but didn\'t provide a user ID.' + ), + __FILE__ + ); + } else { + // It probably wasn't Facebook that hit this action, + // so redirect to the public timeline + common_redirect(common_local_url('public'), 303); + } + } + } + + /* + * Send the user an email warning that their account has been + * disconnected and he/she has no way to login and must contact + * the site administrator for help. + * + * @param User $user the deauthorizing user + * + */ + function emailWarn($user) + { + $profile = $user->getProfile(); + + $siteName = common_config('site', 'name'); + $siteEmail = common_config('site', 'email'); + + if (empty($siteEmail)) { + common_log( + LOG_WARNING, + "No site email address configured. Please set one." + ); + } + + common_switch_locale($user->language); + + $subject = _m('Contact the %s administrator to retrieve your account'); + + $msg = <<<BODY +Hi %1$s, + +We've noticed you have deauthorized the Facebook connection for your +%2$s account. You have not set a password for your %2$s account yet, so +you will not be able to login. If you wish to continue using your %2$s +account, please contact the site administrator (%3$s) to set a password. + +Sincerely, + +%2$s +BODY; + $body = sprintf( + _m($msg), + $user->nickname, + $siteName, + $siteEmail + ); + + common_switch_locale(); + + if (mail_to_user($user, $subject, $body)) { + common_log( + LOG_INFO, + sprintf( + 'Sent account lockout warning to %s (%d)', + $user->nickname, + $user->id + ), + __FILE__ + ); + } else { + common_log( + LOG_WARNING, + sprintf( + 'Unable to send account lockout warning to %s (%d)', + $user->nickname, + $user->id + ), + __FILE__ + ); + } + } + +}
\ No newline at end of file diff --git a/plugins/FacebookBridge/actions/facebookfinishlogin.php b/plugins/FacebookBridge/actions/facebookfinishlogin.php new file mode 100644 index 000000000..2174c5ad4 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebookfinishlogin.php @@ -0,0 +1,688 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Login or register a local user based on a Facebook user + * + * 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 StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class FacebookfinishloginAction extends Action +{ + private $facebook = null; // Facebook client + private $fbuid = null; // Facebook user ID + private $fbuser = null; // Facebook user object (JSON) + + function prepare($args) { + + parent::prepare($args); + + $this->facebook = new Facebook( + array( + 'appId' => common_config('facebook', 'appid'), + 'secret' => common_config('facebook', 'secret'), + 'cookie' => true, + ) + ); + + // Check for a Facebook user session + + $session = $this->facebook->getSession(); + $me = null; + + if ($session) { + try { + $this->fbuid = $this->facebook->getUser(); + $this->fbuser = $this->facebook->api('/me'); + } catch (FacebookApiException $e) { + common_log(LOG_ERROR, $e, __FILE__); + } + } + + if (!empty($this->fbuser)) { + + // OKAY, all is well... proceed to register + + common_debug("Found a valid Facebook user.", __FILE__); + } else { + + // This shouldn't happen in the regular course of things + + list($proxy, $ip) = common_client_ip(); + + common_log( + LOG_WARNING, + sprintf( + 'Failed Facebook authentication attempt, proxy = %s, ip = %s.', + $proxy, + $ip + ), + __FILE__ + ); + + $this->clientError( + _m('You must be logged into Facebook to register a local account using Facebook.') + ); + } + + return true; + } + + function handle($args) + { + 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); + + if (!empty($flink)) { + + // User already has a linked Facebook account and shouldn't be here! + + common_debug( + sprintf( + 'There\'s already a local user %d linked with Facebook user %s.', + $flink->user_id, + $this->fbuid + ) + ); + + $this->clientError( + _m('There is already a local account linked with that Facebook account.') + ); + + } else { + + // Possibly reconnect an existing account + + $this->connectUser(); + } + + } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost(); + } else { + $this->tryLogin(); + } + } + + function handlePost() + { + $token = $this->trimmed('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('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')) { + + $this->connectNewUser(); + + } else { + + $this->showForm( + _m('An unknown error has occured.'), + $this->trimmed('newname') + ); + } + } + + function showPageNotice() + { + if ($this->error) { + + $this->element('div', array('class' => 'error'), $this->error); + + } else { + + $this->element( + 'div', 'instructions', + // TRANS: %s is the site name. + sprintf( + _m('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 local account, or connect with an existing local account.'), + common_config('site', 'name') + ) + ); + } + } + + function title() + { + // TRANS: Page title. + return _m('Facebook Setup'); + } + + function showForm($error=null, $username=null) + { + $this->error = $error; + $this->username = $username; + + $this->showPage(); + } + + function showPage() + { + parent::showPage(); + } + + /** + * @fixme much of this duplicates core code, which is very fragile. + * Should probably be replaced with an extensible mini version of + * the core registration form. + */ + function showContent() + { + if (!empty($this->message_text)) { + $this->element('p', null, $this->message); + return; + } + + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_facebook_connect', + 'class' => 'form_settings', + 'action' => common_local_url('facebookfinishlogin'))); + $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options')); + // TRANS: Legend. + $this->element('legend', null, _m('Connection options')); + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->element('input', array('type' => 'checkbox', + 'id' => 'license', + 'class' => 'checkbox', + 'name' => 'license', + 'value' => 'true')); + $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license')); + // TRANS: %s is the name of the license used by the user for their status updates. + $message = _m('My text and files are available under %s ' . + 'except this private data: password, ' . + 'email address, IM address, and phone number.'); + $link = '<a href="' . + htmlspecialchars(common_config('license', 'url')) . + '">' . + htmlspecialchars(common_config('license', 'title')) . + '</a>'; + $this->raw(sprintf(htmlspecialchars($message), $link)); + $this->elementEnd('label'); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->elementStart('fieldset'); + $this->hidden('token', common_session_token()); + $this->element('legend', null, + // TRANS: Legend. + _m('Create new account')); + $this->element('p', null, + _m('Create a new user with this nickname.')); + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + // TRANS: Field label. + $this->input('newname', _m('New nickname'), + ($this->username) ? $this->username : '', + _m('1-64 lowercase letters or numbers, no punctuation or spaces')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + // TRANS: Submit button. + $this->submit('create', _m('BUTTON','Create')); + $this->elementEnd('fieldset'); + + $this->elementStart('fieldset'); + // TRANS: Legend. + $this->element('legend', null, + _m('Connect existing account')); + $this->element('p', null, + _m('If you already have an account, login with your username and password to connect it to your Facebook.')); + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + // TRANS: Field label. + $this->input('nickname', _m('Existing nickname')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->password('password', _m('Password')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + // TRANS: Submit button. + $this->submit('connect', _m('BUTTON','Connect')); + $this->elementEnd('fieldset'); + + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } + + function message($msg) + { + $this->message_text = $msg; + $this->showPage(); + } + + function createNewUser() + { + if (common_config('site', 'closed')) { + // TRANS: Client error trying to register with registrations not allowed. + $this->clientError(_m('Registration not allowed.')); + return; + } + + $invite = null; + + if (common_config('site', 'inviteonly')) { + $code = $_SESSION['invitecode']; + if (empty($code)) { + // TRANS: Client error trying to register with registrations 'invite only'. + $this->clientError(_m('Registration not allowed.')); + return; + } + + $invite = Invitation::staticGet($code); + + if (empty($invite)) { + // TRANS: Client error trying to register with an invalid invitation code. + $this->clientError(_m('Not a valid invitation code.')); + return; + } + } + + $nickname = $this->trimmed('newname'); + + if (!Validate::string($nickname, array('min_length' => 1, + 'max_length' => 64, + 'format' => NICKNAME_FMT))) { + $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.')); + return; + } + + if (!User::allowed_nickname($nickname)) { + $this->showForm(_m('Nickname not allowed.')); + return; + } + + if (User::staticGet('nickname', $nickname)) { + $this->showForm(_m('Nickname already in use. Try another one.')); + return; + } + + $args = array( + 'nickname' => $nickname, + 'fullname' => $this->fbuser['first_name'] + . ' ' . $this->fbuser['last_name'], + '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); + $result = $this->flinkUser($user->id, $this->fbuid); + + if (!$result) { + $this->serverError(_m('Error connecting user to Facebook.')); + return; + } + + // Add a Foreign_user record + Facebookclient::addFacebookUser($this->fbuser); + + $this->setAvatar($user); + + common_set_user($user); + common_real_login(true); + + common_log( + LOG_INFO, + sprintf( + 'Registered new user %s (%d) from Facebook user %s, (fbuid %d)', + $user->nickname, + $user->id, + $this->fbuser['name'], + $this->fbuid + ), + __FILE__ + ); + + $this->goHome($user->nickname); + } + + /* + * Attempt to download the user's Facebook picture and create a + * StatusNet avatar for the new user. + */ + function setAvatar($user) + { + $picUrl = sprintf( + 'http://graph.facebook.com/%s/picture?type=large', + $this->fbuid + ); + + // fetch the picture from Facebook + $client = new HTTPClient(); + + // fetch the actual picture + $response = $client->get($picUrl); + + if ($response->isOk()) { + + $finalUrl = $client->getUrl(); + + // 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), + $response->getBody() + ); + + if (!$ok) { + common_log( + LOG_WARNING, + sprintf( + 'Couldn\'t save Facebook avatar %s', + $tmp + ), + __FILE__ + ); + + } 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 picture for ' + . '%s (fbuid %d), filename = %s', + $user->nickname, + $user->id, + $this->fbuser['name'], + $this->fbuid, + $filename + ), + __FILE__ + ); + } + } + } + } + + function connectNewUser() + { + $nickname = $this->trimmed('nickname'); + $password = $this->trimmed('password'); + + if (!common_check_user($nickname, $password)) { + $this->showForm(_m('Invalid username or password.')); + return; + } + + $user = User::staticGet('nickname', $nickname); + + if (!empty($user)) { + common_debug( + sprintf( + 'Found a legit user to connect to Facebook: %s (%d)', + $user->nickname, + $user->id + ), + __FILE__ + ); + } + + $this->tryLinkUser($user); + + common_set_user($user); + common_real_login(true); + + $this->goHome($user->nickname); + } + + 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)) { + $this->serverError(_m('Error connecting user to Facebook.')); + return; + } + + common_debug( + sprintf( + 'Connected Facebook user %s (fbuid %d) to local user %s (%d)', + $this->fbuser['name'], + $this->fbuid, + $user->nickname, + $user->id + ), + __FILE__ + ); + } + + function tryLogin() + { + common_debug( + sprintf( + 'Trying login for Facebook user %s', + $this->fbuid + ), + __FILE__ + ); + + $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE); + + if (!empty($flink)) { + $user = $flink->getUser(); + + if (!empty($user)) { + + common_log( + LOG_INFO, + sprintf( + 'Logged in Facebook user %s as user %d (%s)', + $this->fbuid, + $user->nickname, + $user->id + ), + __FILE__ + ); + + common_set_user($user); + common_real_login(true); + $this->goHome($user->nickname); + } + + } else { + + common_debug( + sprintf( + 'No flink found for fbuid: %s - new user', + $this->fbuid + ), + __FILE__ + ); + + $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; + + // Pull the access token from the Facebook cookies + $flink->credentials = $this->facebook->getAccessToken(); + + $flink->created = common_sql_now(); + + $flink_id = $flink->insert(); + + return $flink_id; + } + + function bestNewNickname() + { + if (!empty($this->fbuser['name'])) { + $nickname = $this->nicknamize($this->fbuser['name']); + if ($this->isNewNickname($nickname)) { + return $nickname; + } + } + + // Try the full name + + $fullname = trim($this->fbuser['first_name'] . + ' ' . $this->fbuser['last_name']); + + 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); + } + + /* + * 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/FacebookBridge/actions/facebooklogin.php b/plugins/FacebookBridge/actions/facebooklogin.php new file mode 100644 index 000000000..9a230b724 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebooklogin.php @@ -0,0 +1,122 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * An action for logging in with Facebook + * + * PHP version 5 + * + * 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 StatusNet + * @author Zach Copley <zach@status.net> + * @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); +} + +class FacebookloginAction extends Action +{ + + function handle($args) + { + parent::handle($args); + + if (common_is_real_login()) { + $this->clientError(_m('Already logged in.')); + } else { + $this->showPage(); + } + } + + function getInstructions() + { + // TRANS: Instructions. + return _m('Login with your Facebook Account'); + } + + function showPageNotice() + { + $instr = $this->getInstructions(); + $output = common_markup_to_html($instr); + $this->elementStart('div', 'instructions'); + $this->raw($output); + $this->elementEnd('div'); + } + + function title() + { + // TRANS: Page title. + return _m('Login with Facebook'); + } + + function showContent() { + + $this->elementStart('fieldset'); + + $facebook = Facebookclient::getFacebook(); + + // Degrade to plain link if JavaScript is not available + $this->elementStart( + 'a', + array( + 'href' => $facebook->getLoginUrl( + array( + 'next' => common_local_url('facebookfinishlogin'), + 'cancel' => common_local_url('facebooklogin') + ) + ), + 'id' => 'facebook_button' + ) + ); + + $attrs = array( + 'src' => common_path( + 'plugins/FacebookBridge/images/login-button.png', + true + ), + 'alt' => 'Login with Facebook', + 'title' => 'Login with Facebook' + ); + + $this->element('img', $attrs); + + $this->elementEnd('a'); + + /* + $this->element('div', array('id' => 'fb-root')); + $this->script( + sprintf( + 'http://connect.facebook.net/en_US/all.js#appId=%s&xfbml=1', + common_config('facebook', 'appid') + ) + ); + $this->element('fb:facepile', array('max-rows' => '2', 'width' =>'300')); + */ + $this->elementEnd('fieldset'); + } + + function showLocalNav() + { + $nav = new LoginGroupNav($this); + $nav->show(); + } +} + diff --git a/plugins/FacebookBridge/actions/facebooksettings.php b/plugins/FacebookBridge/actions/facebooksettings.php new file mode 100644 index 000000000..e51181036 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebooksettings.php @@ -0,0 +1,264 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Settings for Facebook + * + * 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 Settings + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Settings for Facebook + * + * @category Settings + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see SettingsAction + */ + +class FacebooksettingsAction extends ConnectSettingsAction +{ + private $facebook; + private $flink; + private $user; + + function prepare($args) + { + parent::prepare($args); + + $this->facebook = new Facebook( + array( + 'appId' => common_config('facebook', 'appid'), + 'secret' => common_config('facebook', 'secret'), + 'cookie' => true, + ) + ); + + $this->user = common_current_user(); + $this->flink = Foreign_link::getByUserID($this->user->id, FACEBOOK_SERVICE); + + return true; + } + + function handlePost($args) + { + // CSRF protection + + $token = $this->trimmed('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('save')) { + $this->saveSettings(); + } else if ($this->arg('disconnect')) { + $this->disconnect(); + } + } + + function title() + { + // TRANS: Page title for Facebook settings. + return _m('Facebook settings'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _('Facebook settings'); + } + + function showContent() + { + + if (empty($this->flink)) { + + $this->element( + 'p', + 'instructions', + _m('There is no Facebook user connected to this account.') + ); + + $attrs = array( + 'show-faces' => 'true', + 'perms' => 'user_location,user_website,offline_access,publish_stream' + ); + + $this->element('fb:login-button', $attrs); + + + } else { + + $this->elementStart( + 'form', + array( + 'method' => 'post', + 'id' => 'form_settings_facebook', + 'class' => 'form_settings', + 'action' => common_local_url('facebooksettings') + ) + ); + + $this->hidden('token', common_session_token()); + + $this->element('p', 'form_note', _m('Connected Facebook user')); + + $this->elementStart('p', array('class' => 'facebook-user-display')); + + $this->elementStart( + 'fb:profile-pic', + array('uid' => $this->flink->foreign_id, + 'size' => 'small', + 'linked' => 'true', + 'facebook-logo' => 'true') + ); + $this->elementEnd('fb:profile-pic'); + + $this->elementStart( + 'fb:name', + array('uid' => $this->flink->foreign_id, 'useyou' => 'false') + ); + + $this->elementEnd('fb:name'); + + $this->elementEnd('p'); + + $this->elementStart('ul', 'form_data'); + + $this->elementStart('li'); + + $this->checkbox( + 'noticesync', + _m('Publish my notices to Facebook.'), + ($this->flink) ? ($this->flink->noticesync & FOREIGN_NOTICE_SEND) : true + ); + + $this->elementEnd('li'); + + $this->elementStart('li'); + + $this->checkbox( + 'replysync', + _m('Send "@" replies to Facebook.'), + ($this->flink) ? ($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) : true + ); + + $this->elementEnd('li'); + + $this->elementStart('li'); + + // TRANS: Submit button to save synchronisation settings. + $this->submit('save', _m('BUTTON','Save')); + + $this->elementEnd('li'); + + $this->elementEnd('ul'); + + $this->elementStart('fieldset'); + + // TRANS: Legend. + $this->element('legend', null, _m('Disconnect my account from Facebook')); + + if (empty($this->user->password)) { + + $this->elementStart('p', array('class' => 'form_guide')); + // @todo FIXME: Bad i18n. Patchwork message in three parts. + // TRANS: Followed by a link containing text "set a password". + $this->text(_m('Disconnecting your Faceboook ' . + 'would make it impossible to log in! Please ')); + $this->element('a', + array('href' => common_local_url('passwordsettings')), + // TRANS: Preceded by "Please " and followed by " first." + _m('set a password')); + // TRANS: Preceded by "Please set a password". + $this->text(_m(' first.')); + $this->elementEnd('p'); + } else { + + $note = 'Keep your %s account but disconnect from Facebook. ' . + 'You\'ll use your %s password to log in.'; + + $site = common_config('site', 'name'); + + $this->element('p', 'instructions', + sprintf($note, $site, $site)); + + // TRANS: Submit button. + $this->submit('disconnect', _m('BUTTON','Disconnect')); + } + + $this->elementEnd('fieldset'); + + $this->elementEnd('form'); + } + } + + function saveSettings() + { + + $noticesync = $this->boolean('noticesync'); + $replysync = $this->boolean('replysync'); + + $original = clone($this->flink); + $this->flink->set_flags($noticesync, false, $replysync, false); + $result = $this->flink->update($original); + + if ($result === false) { + $this->showForm(_m('There was a problem saving your sync preferences.')); + } else { + // TRANS: Confirmation that synchronisation settings have been saved into the system. + $this->showForm(_m('Sync preferences saved.'), true); + } + } + + function disconnect() + { + $flink = Foreign_link::getByUserID($this->user->id, FACEBOOK_SERVICE); + $result = $flink->delete(); + + if ($result === false) { + common_log_db_error($user, 'DELETE', __FILE__); + $this->serverError(_m('Couldn\'t delete link to Facebook.')); + return; + } + + $this->showForm(_m('You have disconnected from Facebook.'), true); + + } +} + diff --git a/plugins/FacebookBridge/classes/Notice_to_item.php b/plugins/FacebookBridge/classes/Notice_to_item.php new file mode 100644 index 000000000..a6a803034 --- /dev/null +++ b/plugins/FacebookBridge/classes/Notice_to_item.php @@ -0,0 +1,190 @@ +<?php +/** + * Data class for storing notice-to-Facebook-item mappings + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @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 <http://www.gnu.org/licenses/>. + */ + +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 <zach@status.net> + * @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/FacebookBridge/extlib/facebook.php b/plugins/FacebookBridge/extlib/facebook.php new file mode 100644 index 000000000..d2d2e866b --- /dev/null +++ b/plugins/FacebookBridge/extlib/facebook.php @@ -0,0 +1,963 @@ +<?php + +if (!function_exists('curl_init')) { + throw new Exception('Facebook needs the CURL PHP extension.'); +} +if (!function_exists('json_decode')) { + throw new Exception('Facebook needs the JSON PHP extension.'); +} + +/** + * Thrown when an API call returns an exception. + * + * @author Naitik Shah <naitik@facebook.com> + */ +class FacebookApiException extends Exception +{ + /** + * The result from the API server that represents the exception information. + */ + protected $result; + + /** + * Make a new API Exception with the given result. + * + * @param Array $result the result from the API server + */ + public function __construct($result) { + $this->result = $result; + + $code = isset($result['error_code']) ? $result['error_code'] : 0; + + if (isset($result['error_description'])) { + // OAuth 2.0 Draft 10 style + $msg = $result['error_description']; + } else if (isset($result['error']) && is_array($result['error'])) { + // OAuth 2.0 Draft 00 style + $msg = $result['error']['message']; + } else if (isset($result['error_msg'])) { + // Rest server style + $msg = $result['error_msg']; + } else { + $msg = 'Unknown Error. Check getResult()'; + } + + parent::__construct($msg, $code); + } + + /** + * Return the associated result object returned by the API server. + * + * @returns Array the result from the API server + */ + public function getResult() { + return $this->result; + } + + /** + * Returns the associated type for the error. This will default to + * 'Exception' when a type is not available. + * + * @return String + */ + public function getType() { + if (isset($this->result['error'])) { + $error = $this->result['error']; + if (is_string($error)) { + // OAuth 2.0 Draft 10 style + return $error; + } else if (is_array($error)) { + // OAuth 2.0 Draft 00 style + if (isset($error['type'])) { + return $error['type']; + } + } + } + return 'Exception'; + } + + /** + * To make debugging easier. + * + * @returns String the string representation of the error + */ + public function __toString() { + $str = $this->getType() . ': '; + if ($this->code != 0) { + $str .= $this->code . ': '; + } + return $str . $this->message; + } +} + +/** + * Provides access to the Facebook Platform. + * + * @author Naitik Shah <naitik@facebook.com> + */ +class Facebook +{ + /** + * Version. + */ + const VERSION = '2.1.2'; + + /** + * Default options for curl. + */ + public static $CURL_OPTS = array( + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_USERAGENT => 'facebook-php-2.0', + ); + + /** + * List of query parameters that get automatically dropped when rebuilding + * the current URL. + */ + protected static $DROP_QUERY_PARAMS = array( + 'session', + 'signed_request', + ); + + /** + * Maps aliases to Facebook domains. + */ + public static $DOMAIN_MAP = array( + 'api' => 'https://api.facebook.com/', + 'api_read' => 'https://api-read.facebook.com/', + 'graph' => 'https://graph.facebook.com/', + 'www' => 'https://www.facebook.com/', + ); + + /** + * The Application ID. + */ + protected $appId; + + /** + * The Application API Secret. + */ + protected $apiSecret; + + /** + * The active user session, if one is available. + */ + protected $session; + + /** + * The data from the signed_request token. + */ + protected $signedRequest; + + /** + * Indicates that we already loaded the session as best as we could. + */ + protected $sessionLoaded = false; + + /** + * Indicates if Cookie support should be enabled. + */ + protected $cookieSupport = false; + + /** + * Base domain for the Cookie. + */ + protected $baseDomain = ''; + + /** + * Indicates if the CURL based @ syntax for file uploads is enabled. + */ + protected $fileUploadSupport = false; + + /** + * Initialize a Facebook Application. + * + * The configuration: + * - appId: the application ID + * - secret: the application secret + * - cookie: (optional) boolean true to enable cookie support + * - domain: (optional) domain for the cookie + * - fileUpload: (optional) boolean indicating if file uploads are enabled + * + * @param Array $config the application configuration + */ + public function __construct($config) { + $this->setAppId($config['appId']); + $this->setApiSecret($config['secret']); + if (isset($config['cookie'])) { + $this->setCookieSupport($config['cookie']); + } + if (isset($config['domain'])) { + $this->setBaseDomain($config['domain']); + } + if (isset($config['fileUpload'])) { + $this->setFileUploadSupport($config['fileUpload']); + } + } + + /** + * Set the Application ID. + * + * @param String $appId the Application ID + */ + public function setAppId($appId) { + $this->appId = $appId; + return $this; + } + + /** + * Get the Application ID. + * + * @return String the Application ID + */ + public function getAppId() { + return $this->appId; + } + + /** + * Set the API Secret. + * + * @param String $appId the API Secret + */ + public function setApiSecret($apiSecret) { + $this->apiSecret = $apiSecret; + return $this; + } + + /** + * Get the API Secret. + * + * @return String the API Secret + */ + public function getApiSecret() { + return $this->apiSecret; + } + + /** + * Set the Cookie Support status. + * + * @param Boolean $cookieSupport the Cookie Support status + */ + public function setCookieSupport($cookieSupport) { + $this->cookieSupport = $cookieSupport; + return $this; + } + + /** + * Get the Cookie Support status. + * + * @return Boolean the Cookie Support status + */ + public function useCookieSupport() { + return $this->cookieSupport; + } + + /** + * Set the base domain for the Cookie. + * + * @param String $domain the base domain + */ + public function setBaseDomain($domain) { + $this->baseDomain = $domain; + return $this; + } + + /** + * Get the base domain for the Cookie. + * + * @return String the base domain + */ + public function getBaseDomain() { + return $this->baseDomain; + } + + /** + * Set the file upload support status. + * + * @param String $domain the base domain + */ + public function setFileUploadSupport($fileUploadSupport) { + $this->fileUploadSupport = $fileUploadSupport; + return $this; + } + + /** + * Get the file upload support status. + * + * @return String the base domain + */ + public function useFileUploadSupport() { + return $this->fileUploadSupport; + } + + /** + * Get the data from a signed_request token + * + * @return String the base domain + */ + public function getSignedRequest() { + if (!$this->signedRequest) { + if (isset($_REQUEST['signed_request'])) { + $this->signedRequest = $this->parseSignedRequest( + $_REQUEST['signed_request']); + } + } + return $this->signedRequest; + } + + /** + * Set the Session. + * + * @param Array $session the session + * @param Boolean $write_cookie indicate if a cookie should be written. this + * value is ignored if cookie support has been disabled. + */ + public function setSession($session=null, $write_cookie=true) { + $session = $this->validateSessionObject($session); + $this->sessionLoaded = true; + $this->session = $session; + if ($write_cookie) { + $this->setCookieFromSession($session); + } + return $this; + } + + /** + * Get the session object. This will automatically look for a signed session + * sent via the signed_request, Cookie or Query Parameters if needed. + * + * @return Array the session + */ + public function getSession() { + if (!$this->sessionLoaded) { + $session = null; + $write_cookie = true; + + // try loading session from signed_request in $_REQUEST + $signedRequest = $this->getSignedRequest(); + if ($signedRequest) { + // sig is good, use the signedRequest + $session = $this->createSessionFromSignedRequest($signedRequest); + } + + // try loading session from $_REQUEST + if (!$session && isset($_REQUEST['session'])) { + $session = json_decode( + get_magic_quotes_gpc() + ? stripslashes($_REQUEST['session']) + : $_REQUEST['session'], + true + ); + $session = $this->validateSessionObject($session); + } + + // try loading session from cookie if necessary + if (!$session && $this->useCookieSupport()) { + $cookieName = $this->getSessionCookieName(); + if (isset($_COOKIE[$cookieName])) { + $session = array(); + parse_str(trim( + get_magic_quotes_gpc() + ? stripslashes($_COOKIE[$cookieName]) + : $_COOKIE[$cookieName], + '"' + ), $session); + $session = $this->validateSessionObject($session); + // write only if we need to delete a invalid session cookie + $write_cookie = empty($session); + } + } + + $this->setSession($session, $write_cookie); + } + + return $this->session; + } + + /** + * Get the UID from the session. + * + * @return String the UID if available + */ + public function getUser() { + $session = $this->getSession(); + return $session ? $session['uid'] : null; + } + + /** + * Gets a OAuth access token. + * + * @return String the access token + */ + public function getAccessToken() { + $session = $this->getSession(); + // either user session signed, or app signed + if ($session) { + return $session['access_token']; + } else { + return $this->getAppId() .'|'. $this->getApiSecret(); + } + } + + /** + * Get a Login URL for use with redirects. By default, full page redirect is + * assumed. If you are using the generated URL with a window.open() call in + * JavaScript, you can pass in display=popup as part of the $params. + * + * The parameters: + * - next: the url to go to after a successful login + * - cancel_url: the url to go to after the user cancels + * - req_perms: comma separated list of requested extended perms + * - display: can be "page" (default, full page) or "popup" + * + * @param Array $params provide custom parameters + * @return String the URL for the login flow + */ + public function getLoginUrl($params=array()) { + $currentUrl = $this->getCurrentUrl(); + return $this->getUrl( + 'www', + 'login.php', + array_merge(array( + 'api_key' => $this->getAppId(), + 'cancel_url' => $currentUrl, + 'display' => 'page', + 'fbconnect' => 1, + 'next' => $currentUrl, + 'return_session' => 1, + 'session_version' => 3, + 'v' => '1.0', + ), $params) + ); + } + + /** + * Get a Logout URL suitable for use with redirects. + * + * The parameters: + * - next: the url to go to after a successful logout + * + * @param Array $params provide custom parameters + * @return String the URL for the logout flow + */ + public function getLogoutUrl($params=array()) { + return $this->getUrl( + 'www', + 'logout.php', + array_merge(array( + 'next' => $this->getCurrentUrl(), + 'access_token' => $this->getAccessToken(), + ), $params) + ); + } + + /** + * Get a login status URL to fetch the status from facebook. + * + * The parameters: + * - ok_session: the URL to go to if a session is found + * - no_session: the URL to go to if the user is not connected + * - no_user: the URL to go to if the user is not signed into facebook + * + * @param Array $params provide custom parameters + * @return String the URL for the logout flow + */ + public function getLoginStatusUrl($params=array()) { + return $this->getUrl( + 'www', + 'extern/login_status.php', + array_merge(array( + 'api_key' => $this->getAppId(), + 'no_session' => $this->getCurrentUrl(), + 'no_user' => $this->getCurrentUrl(), + 'ok_session' => $this->getCurrentUrl(), + 'session_version' => 3, + ), $params) + ); + } + + /** + * Make an API call. + * + * @param Array $params the API call parameters + * @return the decoded response + */ + public function api(/* polymorphic */) { + $args = func_get_args(); + if (is_array($args[0])) { + return $this->_restserver($args[0]); + } else { + return call_user_func_array(array($this, '_graph'), $args); + } + } + + /** + * Invoke the old restserver.php endpoint. + * + * @param Array $params method call object + * @return the decoded response object + * @throws FacebookApiException + */ + protected function _restserver($params) { + // generic application level parameters + $params['api_key'] = $this->getAppId(); + $params['format'] = 'json-strings'; + + $result = json_decode($this->_oauthRequest( + $this->getApiUrl($params['method']), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error_code'])) { + throw new FacebookApiException($result); + } + return $result; + } + + /** + * Invoke the Graph API. + * + * @param String $path the path (required) + * @param String $method the http method (default 'GET') + * @param Array $params the query/post data + * @return the decoded response object + * @throws FacebookApiException + */ + protected function _graph($path, $method='GET', $params=array()) { + if (is_array($method) && empty($params)) { + $params = $method; + $method = 'GET'; + } + $params['method'] = $method; // method override as we always do a POST + + $result = json_decode($this->_oauthRequest( + $this->getUrl('graph', $path), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error'])) { + $e = new FacebookApiException($result); + switch ($e->getType()) { + // OAuth 2.0 Draft 00 style + case 'OAuthException': + // OAuth 2.0 Draft 10 style + case 'invalid_token': + $this->setSession(null); + } + throw $e; + } + return $result; + } + + /** + * Make a OAuth Request + * + * @param String $path the path (required) + * @param Array $params the query/post data + * @return the decoded response object + * @throws FacebookApiException + */ + protected function _oauthRequest($url, $params) { + if (!isset($params['access_token'])) { + $params['access_token'] = $this->getAccessToken(); + } + + // json_encode all params values that are not strings + foreach ($params as $key => $value) { + if (!is_string($value)) { + $params[$key] = json_encode($value); + } + } + return $this->makeRequest($url, $params); + } + + /** + * Makes an HTTP request. This method can be overriden by subclasses if + * developers want to do fancier things or use something other than curl to + * make the request. + * + * @param String $url the URL to make the request to + * @param Array $params the parameters to use for the POST body + * @param CurlHandler $ch optional initialized curl handle + * @return String the response text + */ + protected function makeRequest($url, $params, $ch=null) { + if (!$ch) { + $ch = curl_init(); + } + + $opts = self::$CURL_OPTS; + if ($this->useFileUploadSupport()) { + $opts[CURLOPT_POSTFIELDS] = $params; + } else { + $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); + } + $opts[CURLOPT_URL] = $url; + + // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait + // for 2 seconds if the server does not support this header. + if (isset($opts[CURLOPT_HTTPHEADER])) { + $existing_headers = $opts[CURLOPT_HTTPHEADER]; + $existing_headers[] = 'Expect:'; + $opts[CURLOPT_HTTPHEADER] = $existing_headers; + } else { + $opts[CURLOPT_HTTPHEADER] = array('Expect:'); + } + + curl_setopt_array($ch, $opts); + $result = curl_exec($ch); + + if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT + self::errorLog('Invalid or no certificate authority found, using bundled information'); + curl_setopt($ch, CURLOPT_CAINFO, + dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); + $result = curl_exec($ch); + } + + if ($result === false) { + $e = new FacebookApiException(array( + 'error_code' => curl_errno($ch), + 'error' => array( + 'message' => curl_error($ch), + 'type' => 'CurlException', + ), + )); + curl_close($ch); + throw $e; + } + curl_close($ch); + return $result; + } + + /** + * The name of the Cookie that contains the session. + * + * @return String the cookie name + */ + protected function getSessionCookieName() { + return 'fbs_' . $this->getAppId(); + } + + /** + * Set a JS Cookie based on the _passed in_ session. It does not use the + * currently stored session -- you need to explicitly pass it in. + * + * @param Array $session the session to use for setting the cookie + */ + protected function setCookieFromSession($session=null) { + if (!$this->useCookieSupport()) { + return; + } + + $cookieName = $this->getSessionCookieName(); + $value = 'deleted'; + $expires = time() - 3600; + $domain = $this->getBaseDomain(); + if ($session) { + $value = '"' . http_build_query($session, null, '&') . '"'; + if (isset($session['base_domain'])) { + $domain = $session['base_domain']; + } + $expires = $session['expires']; + } + + // prepend dot if a domain is found + if ($domain) { + $domain = '.' . $domain; + } + + // if an existing cookie is not set, we dont need to delete it + if ($value == 'deleted' && empty($_COOKIE[$cookieName])) { + return; + } + + if (headers_sent()) { + self::errorLog('Could not set cookie. Headers already sent.'); + + // ignore for code coverage as we will never be able to setcookie in a CLI + // environment + // @codeCoverageIgnoreStart + } else { + setcookie($cookieName, $value, $expires, '/', $domain); + } + // @codeCoverageIgnoreEnd + } + + /** + * Validates a session_version=3 style session object. + * + * @param Array $session the session object + * @return Array the session object if it validates, null otherwise + */ + protected function validateSessionObject($session) { + // make sure some essential fields exist + if (is_array($session) && + isset($session['uid']) && + isset($session['access_token']) && + isset($session['sig'])) { + // validate the signature + $session_without_sig = $session; + unset($session_without_sig['sig']); + $expected_sig = self::generateSignature( + $session_without_sig, + $this->getApiSecret() + ); + if ($session['sig'] != $expected_sig) { + self::errorLog('Got invalid session signature in cookie.'); + $session = null; + } + // check expiry time + } else { + $session = null; + } + return $session; + } + + /** + * Returns something that looks like our JS session object from the + * signed token's data + * + * TODO: Nuke this once the login flow uses OAuth2 + * + * @param Array the output of getSignedRequest + * @return Array Something that will work as a session + */ + protected function createSessionFromSignedRequest($data) { + if (!isset($data['oauth_token'])) { + return null; + } + + $session = array( + 'uid' => $data['user_id'], + 'access_token' => $data['oauth_token'], + 'expires' => $data['expires'], + ); + + // put a real sig, so that validateSignature works + $session['sig'] = self::generateSignature( + $session, + $this->getApiSecret() + ); + + return $session; + } + + /** + * Parses a signed_request and validates the signature. + * Then saves it in $this->signed_data + * + * @param String A signed token + * @param Boolean Should we remove the parts of the payload that + * are used by the algorithm? + * @return Array the payload inside it or null if the sig is wrong + */ + protected function parseSignedRequest($signed_request) { + list($encoded_sig, $payload) = explode('.', $signed_request, 2); + + // decode the data + $sig = self::base64UrlDecode($encoded_sig); + $data = json_decode(self::base64UrlDecode($payload), true); + + if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') { + self::errorLog('Unknown algorithm. Expected HMAC-SHA256'); + return null; + } + + // check sig + $expected_sig = hash_hmac('sha256', $payload, + $this->getApiSecret(), $raw = true); + if ($sig !== $expected_sig) { + self::errorLog('Bad Signed JSON signature!'); + return null; + } + + return $data; + } + + /** + * Build the URL for api given parameters. + * + * @param $method String the method name. + * @return String the URL for the given parameters + */ + protected function getApiUrl($method) { + static $READ_ONLY_CALLS = + array('admin.getallocation' => 1, + 'admin.getappproperties' => 1, + 'admin.getbannedusers' => 1, + 'admin.getlivestreamvialink' => 1, + 'admin.getmetrics' => 1, + 'admin.getrestrictioninfo' => 1, + 'application.getpublicinfo' => 1, + 'auth.getapppublickey' => 1, + 'auth.getsession' => 1, + 'auth.getsignedpublicsessiondata' => 1, + 'comments.get' => 1, + 'connect.getunconnectedfriendscount' => 1, + 'dashboard.getactivity' => 1, + 'dashboard.getcount' => 1, + 'dashboard.getglobalnews' => 1, + 'dashboard.getnews' => 1, + 'dashboard.multigetcount' => 1, + 'dashboard.multigetnews' => 1, + 'data.getcookies' => 1, + 'events.get' => 1, + 'events.getmembers' => 1, + 'fbml.getcustomtags' => 1, + 'feed.getappfriendstories' => 1, + 'feed.getregisteredtemplatebundlebyid' => 1, + 'feed.getregisteredtemplatebundles' => 1, + 'fql.multiquery' => 1, + 'fql.query' => 1, + 'friends.arefriends' => 1, + 'friends.get' => 1, + 'friends.getappusers' => 1, + 'friends.getlists' => 1, + 'friends.getmutualfriends' => 1, + 'gifts.get' => 1, + 'groups.get' => 1, + 'groups.getmembers' => 1, + 'intl.gettranslations' => 1, + 'links.get' => 1, + 'notes.get' => 1, + 'notifications.get' => 1, + 'pages.getinfo' => 1, + 'pages.isadmin' => 1, + 'pages.isappadded' => 1, + 'pages.isfan' => 1, + 'permissions.checkavailableapiaccess' => 1, + 'permissions.checkgrantedapiaccess' => 1, + 'photos.get' => 1, + 'photos.getalbums' => 1, + 'photos.gettags' => 1, + 'profile.getinfo' => 1, + 'profile.getinfooptions' => 1, + 'stream.get' => 1, + 'stream.getcomments' => 1, + 'stream.getfilters' => 1, + 'users.getinfo' => 1, + 'users.getloggedinuser' => 1, + 'users.getstandardinfo' => 1, + 'users.hasapppermission' => 1, + 'users.isappuser' => 1, + 'users.isverified' => 1, + 'video.getuploadlimits' => 1); + $name = 'api'; + if (isset($READ_ONLY_CALLS[strtolower($method)])) { + $name = 'api_read'; + } + return self::getUrl($name, 'restserver.php'); + } + + /** + * Build the URL for given domain alias, path and parameters. + * + * @param $name String the name of the domain + * @param $path String optional path (without a leading slash) + * @param $params Array optional query parameters + * @return String the URL for the given parameters + */ + protected function getUrl($name, $path='', $params=array()) { + $url = self::$DOMAIN_MAP[$name]; + if ($path) { + if ($path[0] === '/') { + $path = substr($path, 1); + } + $url .= $path; + } + if ($params) { + $url .= '?' . http_build_query($params, null, '&'); + } + return $url; + } + + /** + * Returns the Current URL, stripping it of known FB parameters that should + * not persist. + * + * @return String the current URL + */ + protected function getCurrentUrl() { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' + ? 'https://' + : 'http://'; + $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + $parts = parse_url($currentUrl); + + // drop known fb params + $query = ''; + if (!empty($parts['query'])) { + $params = array(); + parse_str($parts['query'], $params); + foreach(self::$DROP_QUERY_PARAMS as $key) { + unset($params[$key]); + } + if (!empty($params)) { + $query = '?' . http_build_query($params, null, '&'); + } + } + + // use port if non default + $port = + isset($parts['port']) && + (($protocol === 'http://' && $parts['port'] !== 80) || + ($protocol === 'https://' && $parts['port'] !== 443)) + ? ':' . $parts['port'] : ''; + + // rebuild + return $protocol . $parts['host'] . $port . $parts['path'] . $query; + } + + /** + * Generate a signature for the given params and secret. + * + * @param Array $params the parameters to sign + * @param String $secret the secret to sign with + * @return String the generated signature + */ + protected static function generateSignature($params, $secret) { + // work with sorted data + ksort($params); + + // generate the base string + $base_string = ''; + foreach($params as $key => $value) { + $base_string .= $key . '=' . $value; + } + $base_string .= $secret; + + return md5($base_string); + } + + /** + * Prints to the error log if you aren't in command line mode. + * + * @param String log message + */ + protected static function errorLog($msg) { + // disable error log if we are running in a CLI environment + // @codeCoverageIgnoreStart + if (php_sapi_name() != 'cli') { + error_log($msg); + } + // uncomment this if you want to see the errors on the page + // print 'error_log: '.$msg."\n"; + // @codeCoverageIgnoreEnd + } + + /** + * Base64 encoding that doesn't need to be urlencode()ed. + * Exactly the same as base64_encode except it uses + * - instead of + + * _ instead of / + * + * @param String base64UrlEncodeded string + */ + protected static function base64UrlDecode($input) { + return base64_decode(strtr($input, '-_', '+/')); + } +} diff --git a/plugins/FacebookBridge/extlib/fb_ca_chain_bundle.crt b/plugins/FacebookBridge/extlib/fb_ca_chain_bundle.crt new file mode 100644 index 000000000..b92d7190e --- /dev/null +++ b/plugins/FacebookBridge/extlib/fb_ca_chain_bundle.crt @@ -0,0 +1,121 @@ +-----BEGIN CERTIFICATE----- +MIIFgjCCBGqgAwIBAgIQDKKbZcnESGaLDuEaVk6fQjANBgkqhkiG9w0BAQUFADBm +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSUwIwYDVQQDExxEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBDQS0zMB4XDTEwMDExMzAwMDAwMFoXDTEzMDQxMTIzNTk1OVowaDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEX +MBUGA1UEChMORmFjZWJvb2ssIEluYy4xFzAVBgNVBAMUDiouZmFjZWJvb2suY29t +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9rzj7QIuLM3sdHu1HcI1VcR3g +b5FExKNV646agxSle1aQ/sJev1mh/u91ynwqd2BQmM0brZ1Hc3QrfYyAaiGGgEkp +xbhezyfeYhAyO0TKAYxPnm2cTjB5HICzk6xEIwFbA7SBJ2fSyW1CFhYZyo3tIBjj +19VjKyBfpRaPkzLmRwIDAQABo4ICrDCCAqgwHwYDVR0jBBgwFoAUUOpzidsp+xCP +nuUBINTeeZlIg/cwHQYDVR0OBBYEFPp+tsFBozkjrHlEnZ9J4cFj2eM0MA4GA1Ud +DwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMF8GA1UdHwRYMFYwKaAnoCWGI2h0dHA6 +Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9jYTMtZmIuY3JsMCmgJ6AlhiNodHRwOi8vY3Js +NC5kaWdpY2VydC5jb20vY2EzLWZiLmNybDCCAcYGA1UdIASCAb0wggG5MIIBtQYL +YIZIAYb9bAEDAAEwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3LmRpZ2ljZXJ0 +LmNvbS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUHAgIwggFWHoIB +UgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkA +YwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEA +bgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMA +UABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkA +IABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwA +aQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8A +cgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMA +ZQAuMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQUF +AAOCAQEACOkTIdxMy11+CKrbGNLBSg5xHaTvu/v1wbyn3dO/mf68pPfJnX6ShPYy +4XM4Vk0x4uaFaU4wAGke+nCKGi5dyg0Esg7nemLNKEJaFAJZ9enxZm334lSCeARy +wlDtxULGOFRyGIZZPmbV2eNq5xdU/g3IuBEhL722mTpAye9FU/J8Wsnw54/gANyO +Gzkewigua8ip8Lbs9Cht399yAfbfhUP1DrAm/xEcnHrzPr3cdCtOyJaM6SRPpRqH +ITK5Nc06tat9lXVosSinT3KqydzxBYua9gCFFiR3x3DgZfvXkC6KDdUlDrNcJUub +a1BHnLLP4mxTHL6faAXYd05IxNn/IA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGVTCCBT2gAwIBAgIQCFH5WYFBRcq94CTiEsnCDjANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA3MDQwMzAwMDAwMFoXDTIyMDQwMzAwMDAwMFowZjEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTElMCMGA1UEAxMcRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +Q0EtMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9hCikQH17+NDdR +CPge+yLtYb4LDXBMUGMmdRW5QYiXtvCgFbsIYOBC6AUpEIc2iihlqO8xB3RtNpcv +KEZmBMcqeSZ6mdWOw21PoF6tvD2Rwll7XjZswFPPAAgyPhBkWBATaccM7pxCUQD5 +BUTuJM56H+2MEb0SqPMV9Bx6MWkBG6fmXcCabH4JnudSREoQOiPkm7YDr6ictFuf +1EutkozOtREqqjcYjbTCuNhcBoz4/yO9NV7UfD5+gw6RlgWYw7If48hl66l7XaAs +zPw82W3tzPpLQ4zJ1LilYRyyQLYoEt+5+F/+07LJ7z20Hkt8HEyZNp496+ynaF4d +32duXvsCAwEAAaOCAvcwggLzMA4GA1UdDwEB/wQEAwIBhjCCAcYGA1UdIASCAb0w +ggG5MIIBtQYLYIZIAYb9bAEDAAIwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3 +LmRpZ2ljZXJ0LmNvbS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUH +AgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQBy +AHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBj +AGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAg +AEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQ +AGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBt +AGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBj +AG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBl +AHIAZQBuAGMAZQAuMA8GA1UdEwEB/wQFMAMBAf8wNAYIKwYBBQUHAQEEKDAmMCQG +CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wgY8GA1UdHwSBhzCB +hDBAoD6gPIY6aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0SGlnaEFz +c3VyYW5jZUVWUm9vdENBLmNybDBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQu +Y29tL0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDAfBgNVHSMEGDAW +gBSxPsNpA/i/RwHUmCYaCALvY2QrwzAdBgNVHQ4EFgQUUOpzidsp+xCPnuUBINTe +eZlIg/cwDQYJKoZIhvcNAQEFBQADggEBAF1PhPGoiNOjsrycbeUpSXfh59bcqdg1 +rslx3OXb3J0kIZCmz7cBHJvUV5eR13UWpRLXuT0uiT05aYrWNTf58SHEW0CtWakv +XzoAKUMncQPkvTAyVab+hA4LmzgZLEN8rEO/dTHlIxxFVbdpCJG1z9fVsV7un5Tk +1nq5GMO41lJjHBC6iy9tXcwFOPRWBW3vnuzoYTYMFEuFFFoMg08iXFnLjIpx2vrF +EIRYzwfu45DC9fkpx1ojcflZtGQriLCnNseaIGHr+k61rmsb5OPs4tk8QUmoIKRU +9ZKNu8BVIASm2LAXFszj0Mi0PeXZhMbT9m5teMl5Q+h6N/9cNUm/ocU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQjCCA6ugAwIBAgIEQoclDjANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEy +MjIxNTI3MjdaFw0xNDA3MjIxNTU3MjdaMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQK +EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNV +BAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGzOVz5vvUu+UtLTKm3+WBP8nNJUm2cSrD +1ZQ0Z6IKHLBfaaZAscS3so/QmKSpQVk609yU1jzbdDikSsxNJYL3SqVTEjju80lt +cZF+Y7arpl/DpIT4T2JRvvjF7Ns4kuMG5QiRDMQoQVX7y1qJFX5x6DW/TXIJPb46 +OFBbdzEbjbPHJEWap6xtABRaBLe6E+tRCphBQSJOZWGHgUFQpnlcid4ZSlfVLuZd +HFMsfpjNGgYWpGhz0DQEE1yhcdNafFXbXmThN4cwVgTlEbQpgBLxeTmIogIRfCdm +t4i3ePLKCqg4qwpkwr9mXZWEwaElHoddGlALIBLMQbtuC1E4uEvLAgMBAAGjggET +MIIBDzASBgNVHRMBAf8ECDAGAQH/AgEBMCcGA1UdJQQgMB4GCCsGAQUFBwMBBggr +BgEFBQcDAgYIKwYBBQUHAwQwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzABhhdo +dHRwOi8vb2NzcC5lbnRydXN0Lm5ldDAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8v +Y3JsLmVudHJ1c3QubmV0L3NlcnZlcjEuY3JsMB0GA1UdDgQWBBSxPsNpA/i/RwHU +mCYaCALvY2QrwzALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7 +UISX8+1i0BowGQYJKoZIhvZ9B0EABAwwChsEVjcuMQMCAIEwDQYJKoZIhvcNAQEF +BQADgYEAUuVY7HCc/9EvhaYzC1rAIo348LtGIiMduEl5Xa24G8tmJnDioD2GU06r +1kjLX/ktCdpdBgXadbjtdrZXTP59uN0AXlsdaTiFufsqVLPvkp5yMnqnuI3E2o6p +NpAkoQSbB6kUCNnXcW26valgOjDLZFOnr241QiwdBAJAAE/rRa8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- diff --git a/plugins/FacebookBridge/images/login-button.png b/plugins/FacebookBridge/images/login-button.png Binary files differnew file mode 100644 index 000000000..4e7766bca --- /dev/null +++ b/plugins/FacebookBridge/images/login-button.png diff --git a/plugins/FacebookBridge/lib/facebookclient.php b/plugins/FacebookBridge/lib/facebookclient.php new file mode 100644 index 000000000..33edf5c6b --- /dev/null +++ b/plugins/FacebookBridge/lib/facebookclient.php @@ -0,0 +1,1022 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class for communicating with Facebook + * + * 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 StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @author Zach Copley <zach@status.net> + * @copyright 2009-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Class for communication with Facebook + * + * @category Plugin + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class Facebookclient +{ + protected $facebook = null; // Facebook Graph client obj + protected $flink = null; // Foreign_link StatusNet -> Facebook + protected $notice = null; // The user's notice + protected $user = null; // Sender of the notice + + function __construct($notice) + { + $this->facebook = self::getFacebook(); + $this->notice = $notice; + + $this->flink = Foreign_link::getByUserID( + $notice->profile_id, + FACEBOOK_SERVICE + ); + + $this->user = $this->flink->getUser(); + } + + /* + * Get an instance of the Facebook Graph SDK object + * + * @param string $appId Application + * @param string $secret Facebook API secret + * + * @return Facebook A Facebook SDK obj + */ + static function getFacebook($appId = null, $secret = null) + { + // Check defaults and configuration for application ID and secret + if (empty($appId)) { + $appId = common_config('facebook', 'appid'); + } + + if (empty($secret)) { + $secret = common_config('facebook', 'secret'); + } + + // If there's no app ID and secret set in the local config, look + // for a global one + if (empty($appId) || empty($secret)) { + $appId = common_config('facebook', 'global_appid'); + $secret = common_config('facebook', 'global_secret'); + } + + return new Facebook( + array( + 'appId' => $appId, + 'secret' => $secret, + 'cookie' => true + ) + ); + } + + /* + * Broadcast a notice to Facebook + * + * @param Notice $notice the notice to send + */ + static function facebookBroadcastNotice($notice) + { + common_debug('Facebook broadcast'); + $client = new Facebookclient($notice); + return $client->sendNotice(); + } + + /* + * Should the notice go to Facebook? + */ + function isFacebookBound() { + + if (empty($this->flink)) { + common_log( + LOG_WARN, + sprintf( + "No Foreign_link to Facebook for the author of notice %d.", + $this->notice->id + ), + __FILE__ + ); + return false; + } + + // Avoid a loop + if ($this->notice->source == 'Facebook') { + common_log( + LOG_INFO, + sprintf( + 'Skipping notice %d because its source is Facebook.', + $this->notice->id + ), + __FILE__ + ); + return false; + } + + // If the user does not want to broadcast to Facebook, move along + if (!($this->flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) { + common_log( + LOG_INFO, + sprintf( + 'Skipping notice %d because user has FOREIGN_NOTICE_SEND bit off.', + $this->notice->id + ), + __FILE__ + ); + return false; + } + + // If it's not a reply, or if the user WANTS to send @-replies, + // then, yeah, it can go to Facebook. + if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $this->notice->content) || + ($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) { + return true; + } + + return false; + } + + /* + * Determine whether we should send this notice using the Graph API or the + * old REST API and then dispatch + */ + function sendNotice() + { + // If there's nothing in the credentials field try to send via + // the Old Rest API + + if ($this->isFacebookBound()) { + common_debug("notice is facebook bound", __FILE__); + if (empty($this->flink->credentials)) { + return $this->sendOldRest(); + } else { + + // Otherwise we most likely have an access token + return $this->sendGraph(); + } + + } else { + common_debug( + sprintf( + "Skipping notice %d - not bound for Facebook", + $this->notice->id, + __FILE__ + ) + ); + } + } + + /* + * Send a notice to Facebook using the Graph API + */ + function sendGraph() + { + try { + + $fbuid = $this->flink->foreign_id; + + common_debug( + sprintf( + "Attempting use Graph API to post notice %d as a stream item for %s (%d), fbuid %s", + $this->notice->id, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + $params = array( + 'access_token' => $this->flink->credentials, + // XXX: Need to worrry about length of the message? + 'message' => $this->notice->content + ); + + $attachments = $this->notice->attachments(); + + if (!empty($attachments)) { + + // We can only send one attachment with the Graph API :( + + $first = array_shift($attachments); + + if (substr($first->mimetype, 0, 6) == 'image/' + || in_array( + $first->mimetype, + array('application/x-shockwave-flash', 'audio/mpeg' ))) { + + $params['picture'] = $first->url; + $params['caption'] = 'Click for full size'; + $params['source'] = $first->url; + } + + } + + $result = $this->facebook->api( + 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); + } + + return true; + } + + /* + * Send a notice to Facebook using the deprecated Old REST API. We need this + * for backwards compatibility. Users who signed up for Facebook bridging + * using the old Facebook Canvas application do not have an OAuth 2.0 + * access token. + */ + function sendOldRest() + { + try { + + $canPublish = $this->checkPermission('publish_stream'); + $canUpdate = $this->checkPermission('status_update'); + + // We prefer to use stream.publish, because it can handle + // attachments and returns the ID of the published item + + if ($canPublish == 1) { + $this->restPublishStream(); + } else if ($canUpdate == 1) { + // as a last resort we can just update the user's "status" + $this->restStatusUpdate(); + } else { + + $msg = 'Not sending notice %d to Facebook because user %s ' + . '(%d), fbuid %s, does not have \'status_update\' ' + . 'or \'publish_stream\' permission.'; + + common_log( + LOG_WARNING, + sprintf( + $msg, + $this->notice->id, + $this->user->nickname, + $this->user->id, + $this->flink->foreign_id + ), + __FILE__ + ); + } + + } catch (FacebookApiException $e) { + return $this->handleFacebookError($e); + } + + return true; + } + + /* + * Query Facebook to to see if a user has permission + * + * + * + * @param $permission the permission to check for - must be either + * public_stream or status_update + * + * @return boolean result + */ + function checkPermission($permission) + { + if (!in_array($permission, array('publish_stream', 'status_update'))) { + throw new ServerException("No such permission!"); + } + + $fbuid = $this->flink->foreign_id; + + common_debug( + sprintf( + 'Checking for %s permission for user %s (%d), fbuid %s', + $permission, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + $hasPermission = $this->facebook->api( + array( + 'method' => 'users.hasAppPermission', + 'ext_perm' => $permission, + 'uid' => $fbuid + ) + ); + + if ($hasPermission == 1) { + + common_debug( + sprintf( + '%s (%d), fbuid %s has %s permission', + $permission, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + return true; + + } else { + + $logMsg = '%s (%d), fbuid $fbuid does NOT have %s permission.' + . 'Facebook returned: %s'; + + common_debug( + sprintf( + $logMsg, + $this->user->nickname, + $this->user->id, + $permission, + $fbuid, + var_export($result, true) + ), + __FILE__ + ); + + return false; + + } + } + + /* + * Handle a Facebook API Exception + * + * @param FacebookApiException $e the exception + * + */ + function handleFacebookError($e) + { + $fbuid = $this->flink->foreign_id; + $errmsg = $e->getMessage(); + $code = $e->getCode(); + + // The Facebook PHP SDK seems to always set the code attribute + // of the Exception to 0; they put the real error code in + // the message. Gar! + if ($code == 0) { + preg_match('/^\(#(?<code>\d+)\)/', $errmsg, $matches); + $code = $matches['code']; + } + + // XXX: Check for any others? + switch($code) { + case 100: // Invalid parameter + $msg = 'Facebook claims notice %d was posted with an invalid ' + . 'parameter (error code 100 - %s) Notice details: ' + . '[nickname=%s, user id=%d, fbuid=%d, content="%s"]. ' + . 'Dequeing.'; + common_log( + LOG_ERR, sprintf( + $msg, + $this->notice->id, + $errmsg, + $this->user->nickname, + $this->user->id, + $fbuid, + $this->notice->content + ), + __FILE__ + ); + return true; + break; + case 200: // Permissions error + case 250: // Updating status requires the extended permission status_update + $this->disconnect(); + return true; // dequeue + break; + case 341: // Feed action request limit reached + $msg = '%s (userid=%d, fbuid=%d) has exceeded his/her limit ' + . 'for posting notices to Facebook today. Dequeuing ' + . 'notice %d'; + common_log( + LOG_INFO, sprintf( + $msg, + $user->nickname, + $user->id, + $fbuid, + $this->notice->id + ), + __FILE__ + ); + // @fixme: We want to rety at a later time when the throttling has expired + // instead of just giving up. + return true; + break; + default: + $msg = 'Facebook returned an error we don\'t know how to deal with ' + . 'when posting notice %d. Error code: %d, error message: "%s"' + . ' Notice details: [nickname=%s, user id=%d, fbuid=%d, ' + . 'notice content="%s"]. Dequeing.'; + common_log( + LOG_ERR, sprintf( + $msg, + $this->notice->id, + $code, + $errmsg, + $this->user->nickname, + $this->user->id, + $fbuid, + $this->notice->content + ), + __FILE__ + ); + return true; // dequeue + break; + } + } + + /* + * Publish a notice to Facebook as a status update + * + * This is the least preferable way to send a notice to Facebook because + * it doesn't support attachments and the API method doesn't return + * the ID of the post on Facebook. + * + */ + function restStatusUpdate() + { + $fbuid = $this->flink->foreign_id; + + common_debug( + sprintf( + "Attempting to post notice %d as a status update for %s (%d), fbuid %s", + $this->notice->id, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + $result = $this->facebook->api( + array( + 'method' => 'users.setStatus', + 'status' => $this->formatMessage(), + 'status_includes_verb' => true, + 'uid' => $fbuid + ) + ); + + 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, + $result // will contain 0, or an error + ); + + throw new FacebookApiException($msg, $result); + } + } + + /* + * Publish a notice to a Facebook user's stream using the old REST API + */ + function restPublishStream() + { + $fbuid = $this->flink->foreign_id; + + common_debug( + sprintf( + 'Attempting to post notice %d as stream item for %s (%d) fbuid %s', + $this->notice->id, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + $fbattachment = $this->formatAttachments(); + + $result = $this->facebook->api( + array( + 'method' => 'stream.publish', + 'message' => $this->formatMessage(), + 'attachment' => $fbattachment, + 'uid' => $fbuid + ) + ); + + 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 + ); + + 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; + } + + /* + * Format attachments for the old REST API stream.publish method + * + * Note: Old REST API supports multiple attachments per post + * + */ + function formatAttachments() + { + $attachments = $this->notice->attachments(); + + $fbattachment = array(); + $fbattachment['media'] = array(); + + foreach($attachments as $attachment) + { + if($enclosure = $attachment->getEnclosure()){ + $fbmedia = $this->getFacebookMedia($enclosure); + }else{ + $fbmedia = $this->getFacebookMedia($attachment); + } + if($fbmedia){ + $fbattachment['media'][]=$fbmedia; + }else{ + $fbattachment['name'] = ($attachment->title ? + $attachment->title : $attachment->url); + $fbattachment['href'] = $attachment->url; + } + } + if(count($fbattachment['media'])>0){ + unset($fbattachment['name']); + unset($fbattachment['href']); + } + return $fbattachment; + } + + /** + * given a File objects, returns an associative array suitable for Facebook media + */ + function getFacebookMedia($attachment) + { + $fbmedia = array(); + + if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) { + $fbmedia['type'] = 'image'; + $fbmedia['src'] = $attachment->url; + $fbmedia['href'] = $attachment->url; + } else if ($attachment->mimetype == 'audio/mpeg') { + $fbmedia['type'] = 'mp3'; + $fbmedia['src'] = $attachment->url; + }else if ($attachment->mimetype == 'application/x-shockwave-flash') { + $fbmedia['type'] = 'flash'; + + // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29 + // says that imgsrc is required... but we have no value to put in it + // $fbmedia['imgsrc']=''; + + $fbmedia['swfsrc'] = $attachment->url; + }else{ + return false; + } + return $fbmedia; + } + + /* + * Disconnect a user from Facebook by deleting his Foreign_link. + * Notifies the user his account has been disconnected by email. + */ + function disconnect() + { + $fbuid = $this->flink->foreign_id; + + common_log( + LOG_INFO, + sprintf( + 'Removing Facebook link for %s (%d), fbuid %s', + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + + $result = $this->flink->delete(); + + if (empty($result)) { + common_log( + LOG_ERR, + sprintf( + 'Could not remove Facebook link for %s (%d), fbuid %s', + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + common_log_db_error($flink, 'DELETE', __FILE__); + } + + // Notify the user that we are removing their Facebook link + + $result = $this->mailFacebookDisconnect(); + + if (!$result) { + + $msg = 'Unable to send email to notify %s (%d), fbuid %s ' + . 'about his/her Facebook link being removed.'; + + common_log( + LOG_WARNING, + sprintf( + $msg, + $this->user->nickname, + $this->user->id, + $fbuid + ), + __FILE__ + ); + } + } + + /** + * Send a mail message to notify a user that her Facebook link + * has been terminated. + * + * @return boolean success flag + */ + function mailFacebookDisconnect() + { + $profile = $this->user->getProfile(); + + $siteName = common_config('site', 'name'); + + common_switch_locale($this->user->language); + + $subject = _m('Your Facebook connection has been removed'); + + $msg = <<<BODY +Hi %1$s, + +We're sorry to inform you we are unable to publish your notice to +Facebook, and have removed the connection between your %2$s account and +Facebook. + +This may have happened because you have removed permission for %2$s +to post on your behalf, or perhaps you have deactivated your Facebook +account. You can reconnect your %s account to Facebook at any time by +logging in with Facebook again. + +Sincerely, + +%2$s +BODY; + $body = sprintf( + _m($msg), + $this->user->nickname, + $siteName + ); + + common_switch_locale(); + + 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__ + ); + } + } + } + +} diff --git a/plugins/FacebookBridge/lib/facebookqueuehandler.php b/plugins/FacebookBridge/lib/facebookqueuehandler.php new file mode 100644 index 000000000..1e82ff01b --- /dev/null +++ b/plugins/FacebookBridge/lib/facebookqueuehandler.php @@ -0,0 +1,61 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Queuehandler for Facebook transport + * + * 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 StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class FacebookQueueHandler extends QueueHandler +{ + function transport() + { + return 'facebook'; + } + + function handle($notice) + { + if ($this->_isLocal($notice)) { + return Facebookclient::facebookBroadcastNotice($notice); + } + return true; + } + + /** + * Determine whether the notice was locally created + * + * @param Notice $notice the notice + * + * @return boolean locality + */ + function _isLocal($notice) + { + return ($notice->is_local == Notice::LOCAL_PUBLIC || + $notice->is_local == Notice::LOCAL_NONPUBLIC); + } +} |