From: Zach Copley Date: Tue, 16 Nov 2010 02:33:17 +0000 (+0000) Subject: FacebookSSO -> FacebookBridge X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=4f63b5cff613f02ffed7de7a47027d65d723dbd4;p=quix0rs-gnu-social.git FacebookSSO -> FacebookBridge --- diff --git a/plugins/FacebookBridge/FacebookBridgePlugin.php b/plugins/FacebookBridge/FacebookBridgePlugin.php new file mode 100644 index 0000000000..c30ea15440 --- /dev/null +++ b/plugins/FacebookBridge/FacebookBridgePlugin.php @@ -0,0 +1,523 @@ +. + * + * @category Pugin + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +define("FACEBOOK_SERVICE", 2); + +/** + * Main class for Facebook plugin + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class FacebookBridgePlugin extends Plugin +{ + public $appId = null; // Facebook application ID + public $secret = null; // Facebook application secret + public $facebook = null; // Facebook application instance + public $dir = null; // Facebook SSO plugin dir + + /** + * Initializer for this plugin + * + * Gets an instance of the Facebook API client object + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function initialize() + { + $this->facebook = Facebookclient::getFacebook( + $this->appId, + $this->secret + ); + + return true; + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onAutoload($cls) + { + + $dir = dirname(__FILE__); + + //common_debug("class = " . $cls); + + switch ($cls) + { + case 'Facebook': // Facebook PHP SDK + include_once $dir . '/extlib/facebook.php'; + return false; + case 'FacebookloginAction': + case 'FacebookfinishloginAction': + case 'FacebookadminpanelAction': + case 'FacebooksettingsAction': + case 'FacebookdeauthorizeAction': + include_once $dir . '/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'Facebookclient': + case 'FacebookQueueHandler': + include_once $dir . '/lib/' . strtolower($cls) . '.php'; + return false; + case 'Notice_to_item': + include_once $dir . '/classes/' . $cls . '.php'; + return false; + default: + return true; + } + + } + + /** + * Database schema setup + * + * We maintain a table mapping StatusNet notices to Facebook items + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('notice_to_item', Notice_to_item::schemaDef()); + return true; + } + + /* + * Does this $action need the Facebook JavaScripts? + */ + function needsScripts($action) + { + static $needy = array( + 'FacebookloginAction', + 'FacebookfinishloginAction', + 'FacebookadminpanelAction', + 'FacebooksettingsAction' + ); + + if (in_array(get_class($action), $needy)) { + return true; + } else { + return false; + } + } + + /** + * Map URLs to actions + * + * @param Net_URL_Mapper $m path-to-action mapper + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onRouterInitialized($m) + { + // Always add the admin panel route + $m->connect('admin/facebook', array('action' => 'facebookadminpanel')); + + // Only add these routes if an application has been setup on + // Facebook for the plugin to use. + if ($this->hasApplication()) { + + $m->connect( + 'main/facebooklogin', + array('action' => 'facebooklogin') + ); + $m->connect( + 'main/facebookfinishlogin', + array('action' => 'facebookfinishlogin') + ); + $m->connect( + 'settings/facebook', + array('action' => 'facebooksettings') + ); + $m->connect( + 'facebook/deauthorize', + array('action' => 'facebookdeauthorize') + ); + + } + + return true; + } + + /* + * Add a login tab for Facebook, but only if there's a Facebook + * application defined for the plugin to use. + * + * @param Action &action the current action + * + * @return void + */ + function onEndLoginGroupNav(&$action) + { + $action_name = $action->trimmed('action'); + + if ($this->hasApplication()) { + + $action->menuItem( + common_local_url('facebooklogin'), + _m('MENU', 'Facebook'), + // TRANS: Tooltip for menu item "Facebook". + _m('Login or register using Facebook'), + 'facebooklogin' === $action_name + ); + } + + return true; + } + + /** + * Add a Facebook tab to the admin panels + * + * @param Widget $nav Admin panel nav + * + * @return boolean hook value + */ + function onEndAdminPanelNav($nav) + { + if (AdminPanelAction::canAdmin('facebook')) { + + $action_name = $nav->action->trimmed('action'); + + $nav->out->menuItem( + common_local_url('facebookadminpanel'), + // TRANS: Menu item. + _m('MENU','Facebook'), + // TRANS: Tooltip for menu item "Facebook". + _m('Facebook integration configuration'), + $action_name == 'facebookadminpanel', + 'nav_facebook_admin_panel' + ); + } + + return true; + } + + /* + * Add a tab for user-level Facebook settings + * + * @param Action &action the current action + * + * @return void + */ + function onEndConnectSettingsNav(&$action) + { + if ($this->hasApplication()) { + $action_name = $action->trimmed('action'); + + $action->menuItem( + common_local_url('facebooksettings'), + // TRANS: Menu item tab. + _m('MENU','Facebook'), + // TRANS: Tooltip for menu item "Facebook". + _m('Facebook settings'), + $action_name === 'facebooksettings' + ); + } + + return true; + } + + /* + * Is there a Facebook application for the plugin to use? + * + * Checks to see if a Facebook application ID and secret + * have been configured and a valid Facebook API client + * object exists. + * + */ + function hasApplication() + { + if (!empty($this->facebook)) { + + $appId = $this->facebook->getAppId(); + $secret = $this->facebook->getApiSecret(); + + if (!empty($appId) && !empty($secret)) { + return true; + } + + } + + return false; + } + + /* + * Output a Facebook div for the Facebook JavaSsript SDK to use + * + * @param Action $action the current action + * + */ + function onStartShowHeader($action) + { + // output
as close to as possible + $action->element('div', array('id' => 'fb-root')); + return true; + } + + /* + * Load the Facebook JavaScript SDK on pages that need them. + * + * @param Action $action the current action + * + */ + function onEndShowScripts($action) + { + if ($this->needsScripts($action)) { + + $action->script('https://connect.facebook.net/en_US/all.js'); + + $script = <<inlineScript( + sprintf($script, + json_encode($this->facebook->getAppId()), + json_encode($this->facebook->getSession()), + common_local_url('facebookfinishlogin') + ) + ); + } + } + + /* + * Log the user out of Facebook, per the Facebook authentication guide + * + * @param Action action the current action + */ + function onEndLogout($action) + { + if ($this->hasApplication()) { + $session = $this->facebook->getSession(); + $fbuser = null; + $fbuid = null; + + if ($session) { + try { + $fbuid = $this->facebook->getUser(); + $fbuser = $this->facebook->api('/me'); + } catch (FacebookApiException $e) { + common_log(LOG_ERROR, $e, __FILE__); + } + } + + if (!empty($fbuser)) { + + $logoutUrl = $this->facebook->getLogoutUrl( + array('next' => common_local_url('public')) + ); + + common_log( + LOG_INFO, + sprintf( + "Logging user out of Facebook (fbuid = %s)", + $fbuid + ), + __FILE__ + ); + common_debug("LOGOUT URL = $logoutUrl"); + common_redirect($logoutUrl, 303); + } + + } + } + + /* + * Add fbml namespace to our HTML, so Facebook's JavaScript SDK can parse + * and render XFBML tags + * + * @param Action $action the current action + * @param array $attrs array of attributes for the HTML tag + * + * @return nothing + */ + function onStartHtmlElement($action, $attrs) { + + if ($this->needsScripts($action)) { + $attrs = array_merge( + $attrs, + array('xmlns:fb' => 'http://www.facebook.com/2008/fbml') + ); + } + + return true; + } + + /** + * Add a Facebook queue item for each notice + * + * @param Notice $notice the notice + * @param array &$transports the list of transports (queues) + * + * @return boolean hook return + */ + function onStartEnqueueNotice($notice, &$transports) + { + if (self::hasApplication() && $notice->isLocal()) { + array_push($transports, 'facebook'); + } + return true; + } + + /** + * Register Facebook notice queue handler + * + * @param QueueManager $manager + * + * @return boolean hook return + */ + function onEndInitializeQueueManager($manager) + { + if (self::hasApplication()) { + $manager->connect('facebook', 'FacebookQueueHandler'); + } + return true; + } + + /* + * Use SSL for Facebook stuff + * + * @param string $action name + * @param boolean $ssl outval to force SSL + * @return mixed hook return value + */ + function onSensitiveAction($action, &$ssl) + { + $sensitive = array( + 'facebookadminpanel', + 'facebooksettings', + 'facebooklogin', + 'facebookfinishlogin' + ); + + if (in_array($action, $sensitive)) { + $ssl = true; + return false; + } else { + return true; + } + } + + /** + * If a notice gets deleted, remove the Notice_to_item mapping and + * delete the item on Facebook + * + * @param User $user The user doing the deleting + * @param Notice $notice The notice getting deleted + * + * @return boolean hook value + */ + function onStartDeleteOwnNotice(User $user, Notice $notice) + { + $client = new Facebookclient($notice); + $client->streamRemove(); + + return true; + } + + /** + * Notify remote users when their notices get favorited. + * + * @param Profile or User $profile of local user doing the faving + * @param Notice $notice being favored + * @return hook return value + */ + function onEndFavorNotice(Profile $profile, Notice $notice) + { + $client = new Facebookclient($notice); + $client->like(); + + return true; + } + + /** + * Notify remote users when their notices get de-favorited. + * + * @param Profile $profile Profile person doing the de-faving + * @param Notice $notice Notice being favored + * + * @return hook return value + */ + function onEndDisfavorNotice(Profile $profile, Notice $notice) + { + $client = new Facebookclient($notice); + $client->unLike(); + + return true; + } + + /* + * Add version info for this plugin + * + * @param array &$versions plugin version descriptions + */ + function onPluginVersion(&$versions) + { + $versions[] = array( + 'name' => 'Facebook Single-Sign-On', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews, Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:FacebookBridge', + 'rawdescription' => + _m('A plugin for integrating StatusNet with Facebook.') + ); + + return true; + } +} diff --git a/plugins/FacebookBridge/actions/facebookadminpanel.php b/plugins/FacebookBridge/actions/facebookadminpanel.php new file mode 100644 index 0000000000..61b5441848 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebookadminpanel.php @@ -0,0 +1,212 @@ +. + * + * @category Settings + * @package StatusNet + * @author Zach Copley + * @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 + * @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 0000000000..cb816fc54a --- /dev/null +++ b/plugins/FacebookBridge/actions/facebookdeauthorize.php @@ -0,0 +1,214 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +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 = <<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 0000000000..2174c5ad4a --- /dev/null +++ b/plugins/FacebookBridge/actions/facebookfinishlogin.php @@ -0,0 +1,688 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @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 = '' . + htmlspecialchars(common_config('license', 'title')) . + ''; + $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 0000000000..9a230b7241 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebooklogin.php @@ -0,0 +1,122 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +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 0000000000..e511810369 --- /dev/null +++ b/plugins/FacebookBridge/actions/facebooksettings.php @@ -0,0 +1,264 @@ +. + * + * @category Settings + * @package StatusNet + * @author Zach Copley + * @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 + * @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 0000000000..a6a8030342 --- /dev/null +++ b/plugins/FacebookBridge/classes/Notice_to_item.php @@ -0,0 +1,190 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for mapping notices to Facebook stream items + * + * Note that notice_id is unique only within a single database; if you + * want to share this data for some reason, get the notice's URI and use + * that instead, since it's universally unique. + * + * @category Action + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Notice_to_item extends Memcached_DataObject +{ + public $__table = 'notice_to_item'; // table name + public $notice_id; // int(4) primary_key not_null + public $item_id; // varchar(255) not null + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup + * @param mixed $v Value to lookup + * + * @return Notice_to_item object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Notice_to_item', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array( + 'notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'item_id' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL + ); + } + + static function schemaDef() + { + return array( + new ColumnDef('notice_id', 'integer', null, false, 'PRI'), + new ColumnDef('item_id', 'varchar', 255, false, 'UNI'), + new ColumnDef('created', 'datetime', null, false) + ); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + + function keyTypes() + { + return array('notice_id' => 'K', 'item_id' => 'U'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Save a mapping between a notice and a Facebook item + * + * @param integer $notice_id ID of the notice in StatusNet + * @param integer $item_id ID of the stream item on Facebook + * + * @return Notice_to_item new object for this value + */ + + static function saveNew($notice_id, $item_id) + { + $n2i = Notice_to_item::staticGet('notice_id', $notice_id); + + if (!empty($n2i)) { + return $n2i; + } + + $n2i = Notice_to_item::staticGet('item_id', $item_id); + + if (!empty($n2i)) { + return $n2i; + } + + common_debug( + "Mapping notice {$notice_id} to Facebook item {$item_id}", + __FILE__ + ); + + $n2i = new Notice_to_item(); + + $n2i->notice_id = $notice_id; + $n2i->item_id = $item_id; + $n2i->created = common_sql_now(); + + $n2i->insert(); + + return $n2i; + } +} diff --git a/plugins/FacebookBridge/extlib/facebook.php b/plugins/FacebookBridge/extlib/facebook.php new file mode 100644 index 0000000000..d2d2e866bf --- /dev/null +++ b/plugins/FacebookBridge/extlib/facebook.php @@ -0,0 +1,963 @@ + + */ +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 + */ +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 0000000000..b92d7190e9 --- /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 new file mode 100644 index 0000000000..4e7766bcad Binary files /dev/null and b/plugins/FacebookBridge/images/login-button.png differ diff --git a/plugins/FacebookBridge/lib/facebookclient.php b/plugins/FacebookBridge/lib/facebookclient.php new file mode 100644 index 0000000000..33edf5c6b1 --- /dev/null +++ b/plugins/FacebookBridge/lib/facebookclient.php @@ -0,0 +1,1022 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews + * @author Zach Copley + * @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 + * @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('/^\(#(?\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 = <<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 0000000000..1e82ff01b1 --- /dev/null +++ b/plugins/FacebookBridge/lib/facebookqueuehandler.php @@ -0,0 +1,61 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @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); + } +} diff --git a/plugins/FacebookSSO/FacebookBridgePlugin.php b/plugins/FacebookSSO/FacebookBridgePlugin.php deleted file mode 100644 index c30ea15440..0000000000 --- a/plugins/FacebookSSO/FacebookBridgePlugin.php +++ /dev/null @@ -1,523 +0,0 @@ -. - * - * @category Pugin - * @package StatusNet - * @author Zach Copley - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -define("FACEBOOK_SERVICE", 2); - -/** - * Main class for Facebook plugin - * - * @category Plugin - * @package StatusNet - * @author Zach Copley - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ -class FacebookBridgePlugin extends Plugin -{ - public $appId = null; // Facebook application ID - public $secret = null; // Facebook application secret - public $facebook = null; // Facebook application instance - public $dir = null; // Facebook SSO plugin dir - - /** - * Initializer for this plugin - * - * Gets an instance of the Facebook API client object - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function initialize() - { - $this->facebook = Facebookclient::getFacebook( - $this->appId, - $this->secret - ); - - return true; - } - - /** - * Load related modules when needed - * - * @param string $cls Name of the class to be loaded - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function onAutoload($cls) - { - - $dir = dirname(__FILE__); - - //common_debug("class = " . $cls); - - switch ($cls) - { - case 'Facebook': // Facebook PHP SDK - include_once $dir . '/extlib/facebook.php'; - return false; - case 'FacebookloginAction': - case 'FacebookfinishloginAction': - case 'FacebookadminpanelAction': - case 'FacebooksettingsAction': - case 'FacebookdeauthorizeAction': - include_once $dir . '/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; - return false; - case 'Facebookclient': - case 'FacebookQueueHandler': - include_once $dir . '/lib/' . strtolower($cls) . '.php'; - return false; - case 'Notice_to_item': - include_once $dir . '/classes/' . $cls . '.php'; - return false; - default: - return true; - } - - } - - /** - * Database schema setup - * - * We maintain a table mapping StatusNet notices to Facebook items - * - * @see Schema - * @see ColumnDef - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function onCheckSchema() - { - $schema = Schema::get(); - $schema->ensureTable('notice_to_item', Notice_to_item::schemaDef()); - return true; - } - - /* - * Does this $action need the Facebook JavaScripts? - */ - function needsScripts($action) - { - static $needy = array( - 'FacebookloginAction', - 'FacebookfinishloginAction', - 'FacebookadminpanelAction', - 'FacebooksettingsAction' - ); - - if (in_array(get_class($action), $needy)) { - return true; - } else { - return false; - } - } - - /** - * Map URLs to actions - * - * @param Net_URL_Mapper $m path-to-action mapper - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function onRouterInitialized($m) - { - // Always add the admin panel route - $m->connect('admin/facebook', array('action' => 'facebookadminpanel')); - - // Only add these routes if an application has been setup on - // Facebook for the plugin to use. - if ($this->hasApplication()) { - - $m->connect( - 'main/facebooklogin', - array('action' => 'facebooklogin') - ); - $m->connect( - 'main/facebookfinishlogin', - array('action' => 'facebookfinishlogin') - ); - $m->connect( - 'settings/facebook', - array('action' => 'facebooksettings') - ); - $m->connect( - 'facebook/deauthorize', - array('action' => 'facebookdeauthorize') - ); - - } - - return true; - } - - /* - * Add a login tab for Facebook, but only if there's a Facebook - * application defined for the plugin to use. - * - * @param Action &action the current action - * - * @return void - */ - function onEndLoginGroupNav(&$action) - { - $action_name = $action->trimmed('action'); - - if ($this->hasApplication()) { - - $action->menuItem( - common_local_url('facebooklogin'), - _m('MENU', 'Facebook'), - // TRANS: Tooltip for menu item "Facebook". - _m('Login or register using Facebook'), - 'facebooklogin' === $action_name - ); - } - - return true; - } - - /** - * Add a Facebook tab to the admin panels - * - * @param Widget $nav Admin panel nav - * - * @return boolean hook value - */ - function onEndAdminPanelNav($nav) - { - if (AdminPanelAction::canAdmin('facebook')) { - - $action_name = $nav->action->trimmed('action'); - - $nav->out->menuItem( - common_local_url('facebookadminpanel'), - // TRANS: Menu item. - _m('MENU','Facebook'), - // TRANS: Tooltip for menu item "Facebook". - _m('Facebook integration configuration'), - $action_name == 'facebookadminpanel', - 'nav_facebook_admin_panel' - ); - } - - return true; - } - - /* - * Add a tab for user-level Facebook settings - * - * @param Action &action the current action - * - * @return void - */ - function onEndConnectSettingsNav(&$action) - { - if ($this->hasApplication()) { - $action_name = $action->trimmed('action'); - - $action->menuItem( - common_local_url('facebooksettings'), - // TRANS: Menu item tab. - _m('MENU','Facebook'), - // TRANS: Tooltip for menu item "Facebook". - _m('Facebook settings'), - $action_name === 'facebooksettings' - ); - } - - return true; - } - - /* - * Is there a Facebook application for the plugin to use? - * - * Checks to see if a Facebook application ID and secret - * have been configured and a valid Facebook API client - * object exists. - * - */ - function hasApplication() - { - if (!empty($this->facebook)) { - - $appId = $this->facebook->getAppId(); - $secret = $this->facebook->getApiSecret(); - - if (!empty($appId) && !empty($secret)) { - return true; - } - - } - - return false; - } - - /* - * Output a Facebook div for the Facebook JavaSsript SDK to use - * - * @param Action $action the current action - * - */ - function onStartShowHeader($action) - { - // output
as close to as possible - $action->element('div', array('id' => 'fb-root')); - return true; - } - - /* - * Load the Facebook JavaScript SDK on pages that need them. - * - * @param Action $action the current action - * - */ - function onEndShowScripts($action) - { - if ($this->needsScripts($action)) { - - $action->script('https://connect.facebook.net/en_US/all.js'); - - $script = <<inlineScript( - sprintf($script, - json_encode($this->facebook->getAppId()), - json_encode($this->facebook->getSession()), - common_local_url('facebookfinishlogin') - ) - ); - } - } - - /* - * Log the user out of Facebook, per the Facebook authentication guide - * - * @param Action action the current action - */ - function onEndLogout($action) - { - if ($this->hasApplication()) { - $session = $this->facebook->getSession(); - $fbuser = null; - $fbuid = null; - - if ($session) { - try { - $fbuid = $this->facebook->getUser(); - $fbuser = $this->facebook->api('/me'); - } catch (FacebookApiException $e) { - common_log(LOG_ERROR, $e, __FILE__); - } - } - - if (!empty($fbuser)) { - - $logoutUrl = $this->facebook->getLogoutUrl( - array('next' => common_local_url('public')) - ); - - common_log( - LOG_INFO, - sprintf( - "Logging user out of Facebook (fbuid = %s)", - $fbuid - ), - __FILE__ - ); - common_debug("LOGOUT URL = $logoutUrl"); - common_redirect($logoutUrl, 303); - } - - } - } - - /* - * Add fbml namespace to our HTML, so Facebook's JavaScript SDK can parse - * and render XFBML tags - * - * @param Action $action the current action - * @param array $attrs array of attributes for the HTML tag - * - * @return nothing - */ - function onStartHtmlElement($action, $attrs) { - - if ($this->needsScripts($action)) { - $attrs = array_merge( - $attrs, - array('xmlns:fb' => 'http://www.facebook.com/2008/fbml') - ); - } - - return true; - } - - /** - * Add a Facebook queue item for each notice - * - * @param Notice $notice the notice - * @param array &$transports the list of transports (queues) - * - * @return boolean hook return - */ - function onStartEnqueueNotice($notice, &$transports) - { - if (self::hasApplication() && $notice->isLocal()) { - array_push($transports, 'facebook'); - } - return true; - } - - /** - * Register Facebook notice queue handler - * - * @param QueueManager $manager - * - * @return boolean hook return - */ - function onEndInitializeQueueManager($manager) - { - if (self::hasApplication()) { - $manager->connect('facebook', 'FacebookQueueHandler'); - } - return true; - } - - /* - * Use SSL for Facebook stuff - * - * @param string $action name - * @param boolean $ssl outval to force SSL - * @return mixed hook return value - */ - function onSensitiveAction($action, &$ssl) - { - $sensitive = array( - 'facebookadminpanel', - 'facebooksettings', - 'facebooklogin', - 'facebookfinishlogin' - ); - - if (in_array($action, $sensitive)) { - $ssl = true; - return false; - } else { - return true; - } - } - - /** - * If a notice gets deleted, remove the Notice_to_item mapping and - * delete the item on Facebook - * - * @param User $user The user doing the deleting - * @param Notice $notice The notice getting deleted - * - * @return boolean hook value - */ - function onStartDeleteOwnNotice(User $user, Notice $notice) - { - $client = new Facebookclient($notice); - $client->streamRemove(); - - return true; - } - - /** - * Notify remote users when their notices get favorited. - * - * @param Profile or User $profile of local user doing the faving - * @param Notice $notice being favored - * @return hook return value - */ - function onEndFavorNotice(Profile $profile, Notice $notice) - { - $client = new Facebookclient($notice); - $client->like(); - - return true; - } - - /** - * Notify remote users when their notices get de-favorited. - * - * @param Profile $profile Profile person doing the de-faving - * @param Notice $notice Notice being favored - * - * @return hook return value - */ - function onEndDisfavorNotice(Profile $profile, Notice $notice) - { - $client = new Facebookclient($notice); - $client->unLike(); - - return true; - } - - /* - * Add version info for this plugin - * - * @param array &$versions plugin version descriptions - */ - function onPluginVersion(&$versions) - { - $versions[] = array( - 'name' => 'Facebook Single-Sign-On', - 'version' => STATUSNET_VERSION, - 'author' => 'Craig Andrews, Zach Copley', - 'homepage' => 'http://status.net/wiki/Plugin:FacebookBridge', - 'rawdescription' => - _m('A plugin for integrating StatusNet with Facebook.') - ); - - return true; - } -} diff --git a/plugins/FacebookSSO/actions/facebookadminpanel.php b/plugins/FacebookSSO/actions/facebookadminpanel.php deleted file mode 100644 index 61b5441848..0000000000 --- a/plugins/FacebookSSO/actions/facebookadminpanel.php +++ /dev/null @@ -1,212 +0,0 @@ -. - * - * @category Settings - * @package StatusNet - * @author Zach Copley - * @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 - * @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/FacebookSSO/actions/facebookdeauthorize.php b/plugins/FacebookSSO/actions/facebookdeauthorize.php deleted file mode 100644 index cb816fc54a..0000000000 --- a/plugins/FacebookSSO/actions/facebookdeauthorize.php +++ /dev/null @@ -1,214 +0,0 @@ -. - * - * @category Plugin - * @package StatusNet - * @author Zach Copley - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ - -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 = <<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/FacebookSSO/actions/facebookfinishlogin.php b/plugins/FacebookSSO/actions/facebookfinishlogin.php deleted file mode 100644 index 2174c5ad4a..0000000000 --- a/plugins/FacebookSSO/actions/facebookfinishlogin.php +++ /dev/null @@ -1,688 +0,0 @@ -. - * - * @category Plugin - * @package StatusNet - * @author Zach Copley - * @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 = '' . - htmlspecialchars(common_config('license', 'title')) . - ''; - $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/FacebookSSO/actions/facebooklogin.php b/plugins/FacebookSSO/actions/facebooklogin.php deleted file mode 100644 index 9a230b7241..0000000000 --- a/plugins/FacebookSSO/actions/facebooklogin.php +++ /dev/null @@ -1,122 +0,0 @@ -. - * - * @category Plugin - * @package StatusNet - * @author Zach Copley - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ - -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/FacebookSSO/actions/facebooksettings.php b/plugins/FacebookSSO/actions/facebooksettings.php deleted file mode 100644 index e511810369..0000000000 --- a/plugins/FacebookSSO/actions/facebooksettings.php +++ /dev/null @@ -1,264 +0,0 @@ -. - * - * @category Settings - * @package StatusNet - * @author Zach Copley - * @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 - * @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/FacebookSSO/classes/Notice_to_item.php b/plugins/FacebookSSO/classes/Notice_to_item.php deleted file mode 100644 index a6a8030342..0000000000 --- a/plugins/FacebookSSO/classes/Notice_to_item.php +++ /dev/null @@ -1,190 +0,0 @@ - - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2010, StatusNet, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; - -/** - * Data class for mapping notices to Facebook stream items - * - * Note that notice_id is unique only within a single database; if you - * want to share this data for some reason, get the notice's URI and use - * that instead, since it's universally unique. - * - * @category Action - * @package StatusNet - * @author Zach Copley - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * - * @see DB_DataObject - */ - -class Notice_to_item extends Memcached_DataObject -{ - public $__table = 'notice_to_item'; // table name - public $notice_id; // int(4) primary_key not_null - public $item_id; // varchar(255) not null - public $created; // datetime - - /** - * Get an instance by key - * - * This is a utility method to get a single instance with a given key value. - * - * @param string $k Key to use to lookup - * @param mixed $v Value to lookup - * - * @return Notice_to_item object found, or null for no hits - * - */ - - function staticGet($k, $v=null) - { - return Memcached_DataObject::staticGet('Notice_to_item', $k, $v); - } - - /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. - * - * @return array array of column definitions - */ - - function table() - { - return array( - 'notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'item_id' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL - ); - } - - static function schemaDef() - { - return array( - new ColumnDef('notice_id', 'integer', null, false, 'PRI'), - new ColumnDef('item_id', 'varchar', 255, false, 'UNI'), - new ColumnDef('created', 'datetime', null, false) - ); - } - - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has, since it - * won't appear in StatusNet's own keys list. In most cases, this will - * simply reference your keyTypes() function. - * - * @return array list of key field names - */ - - function keys() - { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. This key information is used to store and clear - * cached data, so be sure to list any key that will be used for static - * lookups. - * - * @return array associative array of key definitions, field name to type: - * 'K' for primary key: for compound keys, add an entry for each component; - * 'U' for unique keys: compound keys are not well supported here. - */ - - function keyTypes() - { - return array('notice_id' => 'K', 'item_id' => 'U'); - } - - /** - * Magic formula for non-autoincrementing integer primary keys - * - * If a table has a single integer column as its primary key, DB_DataObject - * assumes that the column is auto-incrementing and makes a sequence table - * to do this incrementation. Since we don't need this for our class, we - * overload this method and return the magic formula that DB_DataObject needs. - * - * @return array magic three-false array that stops auto-incrementing. - */ - - function sequenceKey() - { - return array(false, false, false); - } - - /** - * Save a mapping between a notice and a Facebook item - * - * @param integer $notice_id ID of the notice in StatusNet - * @param integer $item_id ID of the stream item on Facebook - * - * @return Notice_to_item new object for this value - */ - - static function saveNew($notice_id, $item_id) - { - $n2i = Notice_to_item::staticGet('notice_id', $notice_id); - - if (!empty($n2i)) { - return $n2i; - } - - $n2i = Notice_to_item::staticGet('item_id', $item_id); - - if (!empty($n2i)) { - return $n2i; - } - - common_debug( - "Mapping notice {$notice_id} to Facebook item {$item_id}", - __FILE__ - ); - - $n2i = new Notice_to_item(); - - $n2i->notice_id = $notice_id; - $n2i->item_id = $item_id; - $n2i->created = common_sql_now(); - - $n2i->insert(); - - return $n2i; - } -} diff --git a/plugins/FacebookSSO/extlib/facebook.php b/plugins/FacebookSSO/extlib/facebook.php deleted file mode 100644 index d2d2e866bf..0000000000 --- a/plugins/FacebookSSO/extlib/facebook.php +++ /dev/null @@ -1,963 +0,0 @@ - - */ -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 - */ -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/FacebookSSO/extlib/fb_ca_chain_bundle.crt b/plugins/FacebookSSO/extlib/fb_ca_chain_bundle.crt deleted file mode 100644 index b92d7190e9..0000000000 --- a/plugins/FacebookSSO/extlib/fb_ca_chain_bundle.crt +++ /dev/null @@ -1,121 +0,0 @@ ------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/FacebookSSO/images/login-button.png b/plugins/FacebookSSO/images/login-button.png deleted file mode 100644 index 4e7766bcad..0000000000 Binary files a/plugins/FacebookSSO/images/login-button.png and /dev/null differ diff --git a/plugins/FacebookSSO/lib/facebookclient.php b/plugins/FacebookSSO/lib/facebookclient.php deleted file mode 100644 index 33edf5c6b1..0000000000 --- a/plugins/FacebookSSO/lib/facebookclient.php +++ /dev/null @@ -1,1022 +0,0 @@ -. - * - * @category Plugin - * @package StatusNet - * @author Craig Andrews - * @author Zach Copley - * @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 - * @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('/^\(#(?\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 = <<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/FacebookSSO/lib/facebookqueuehandler.php b/plugins/FacebookSSO/lib/facebookqueuehandler.php deleted file mode 100644 index 1e82ff01b1..0000000000 --- a/plugins/FacebookSSO/lib/facebookqueuehandler.php +++ /dev/null @@ -1,61 +0,0 @@ -. - * - * @category Plugin - * @package StatusNet - * @author Zach Copley - * @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); - } -}