]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Much more reliable Facebook SSO
authorZach Copley <zach@status.net>
Fri, 5 Nov 2010 06:34:06 +0000 (06:34 +0000)
committerZach Copley <zach@status.net>
Fri, 5 Nov 2010 06:34:06 +0000 (06:34 +0000)
plugins/FacebookSSO/FacebookSSOPlugin.php
plugins/FacebookSSO/actions/facebookfinishlogin.php [new file with mode: 0644]
plugins/FacebookSSO/actions/facebooklogin.php
plugins/FacebookSSO/actions/facebookregister.php [deleted file]
plugins/FacebookSSO/images/login-button.png [new file with mode: 0644]
plugins/FacebookSSO/lib/facebookclient.php [new file with mode: 0644]
plugins/FacebookSSO/lib/facebookqueuehandler.php [new file with mode: 0644]

index b14ef0bade41968e2011a9519598d69d1c274809..a726b2facc46d33cde843b73f649be55755f8c32 100644 (file)
@@ -125,7 +125,7 @@ class FacebookSSOPlugin extends Plugin
             include_once $dir . '/extlib/facebookapi_php5_restlib.php';
             return false;
         case 'FacebookloginAction':
-        case 'FacebookregisterAction':
+        case 'FacebookfinishloginAction':
         case 'FacebookadminpanelAction':
         case 'FacebooksettingsAction':
             include_once $dir . '/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
@@ -146,15 +146,17 @@ class FacebookSSOPlugin extends Plugin
     function needsScripts($action)
     {
         static $needy = array(
-            'FacebookloginAction',
-            'FacebookregisterAction',
+            //'FacebookloginAction',
+            'FacebookfinishloginAction',
             'FacebookadminpanelAction',
             'FacebooksettingsAction'
         );
 
         if (in_array(get_class($action), $needy)) {
+            common_debug("needs scripts!");
             return true;
         } else {
+            common_debug("doesn't need scripts!");
             return false;
         }
     }
@@ -185,8 +187,8 @@ class FacebookSSOPlugin extends Plugin
                 array('action' => 'facebooklogin')
             );
             $m->connect(
-                'main/facebookregister',
-                array('action' => 'facebookregister')
+                'main/facebookfinishlogin',
+                array('action' => 'facebookfinishlogin')
             );
 
             $m->connect(
@@ -298,50 +300,42 @@ class FacebookSSOPlugin extends Plugin
 
     function onStartShowHeader($action)
     {
-        if ($this->needsScripts($action)) {
+        // output <div id="fb-root"></div> as close to <body> as possible
+        $action->element('div', array('id' => 'fb-root'));
+        return true;
+    }
 
-            // output <div id="fb-root"></div> as close to <body> as possible
-            $action->element('div', array('id' => 'fb-root'));
+    function onEndShowScripts($action)
+    {
+        if ($this->needsScripts($action)) {
 
-            $dir = dirname(__FILE__);
+            $action->script('https://connect.facebook.net/en_US/all.js');
 
             $script = <<<ENDOFSCRIPT
-window.fbAsyncInit = function() {
-
-    FB.init({
-      appId   : %s,
-      session : %s,   // don't refetch the session when PHP already has it
-      status  : true, // check login status
-      cookie  : true, // enable cookies to allow the server to access the session
-      xfbml   : true  // parse XFBML
-    });
-
-    // whenever the user logs in, refresh the page
-    FB.Event.subscribe(
-        'auth.login',
-        function() {
-            window.location.reload();
+FB.init({appId: %1\$s, session: %2\$s, status: true, cookie: true, xfbml: true});
+
+$('#facebook_button').bind('click', function(event) {
+
+    event.preventDefault();
+
+    FB.login(function(response) {
+        if (response.session && response.perms) {
+            window.location.href = '%3\$s';
+        } else {
+            // NOP (user cancelled login)
         }
-    );
-};
-
-(function() {
-    var e = document.createElement('script');
-    e.src = document.location.protocol + '//connect.facebook.net/en_US/all.js';
-    e.async = true;
-    document.getElementById('fb-root').appendChild(e);
-}());
+    }, {perms:'read_stream,publish_stream,offline_access,user_status,user_location,user_website'});
+});
 ENDOFSCRIPT;
 
             $action->inlineScript(
                 sprintf($script,
                     json_encode($this->facebook->getAppId()),
-                    json_encode($this->facebook->getSession())
+                    json_encode($this->facebook->getSession()),
+                    common_local_url('facebookfinishlogin')
                 )
             );
         }
-
-        return true;
     }
 
     /*
diff --git a/plugins/FacebookSSO/actions/facebookfinishlogin.php b/plugins/FacebookSSO/actions/facebookfinishlogin.php
new file mode 100644 (file)
index 0000000..16f7cff
--- /dev/null
@@ -0,0 +1,572 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Login or register a local user based on a Facebook user
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+class FacebookfinishloginAction extends Action
+{
+
+    private $facebook = null; // Facebook client
+    private $fbuid    = null; // Facebook user ID
+    private $fbuser   = null; // Facebook user object (JSON)
+
+    function prepare($args) {
+
+        parent::prepare($args);
+
+        $this->facebook = new Facebook(
+            array(
+                'appId'  => common_config('facebook', 'appid'),
+                'secret' => common_config('facebook', 'secret'),
+                'cookie' => true,
+            )
+        );
+
+        // Check for a Facebook user session
+
+        $session = $this->facebook->getSession();
+        $me      = null;
+
+        if ($session) {
+            try {
+                $this->fbuid  = $this->facebook->getUser();
+                $this->fbuser = $this->facebook->api('/me');
+            } catch (FacebookApiException $e) {
+                common_log(LOG_ERROR, $e, __FILE__);
+            }
+        }
+
+        if (!empty($this->fbuser)) {
+
+            // OKAY, all is well... proceed to register
+
+            common_debug("Found a valid Facebook user.", __FILE__);
+        } else {
+
+            // This shouldn't happen in the regular course of things
+
+            list($proxy, $ip) = common_client_ip();
+
+            common_log(
+                LOG_WARNING,
+                    sprintf(
+                        'Failed Facebook authentication attempt, proxy = %s, ip = %s.',
+                         $proxy,
+                         $ip
+                    ),
+                    __FILE__
+            );
+
+            $this->clientError(
+                _m('You must be logged into Facebook to register a local account using Facebook.')
+            );
+        }
+
+        return true;
+    }
+
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if (common_is_real_login()) {
+            
+            // User is already logged in, are her accounts already linked?
+
+            $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
+
+            if (!empty($flink)) {
+
+                // User already has a linked Facebook account and shouldn't be here!
+
+                common_debug(
+                    sprintf(
+                        'There\'s already a local user %d linked with Facebook user %s.',
+                        $flink->user_id,
+                        $this->fbuid
+                    )
+                );
+
+                $this->clientError(
+                    _m('There is already a local account linked with that Facebook account.')
+                );
+
+            } else {
+
+                // Possibly reconnect an existing account
+                
+                $this->connectUser();
+            }
+
+        } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+
+            $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')
+                );
+            }
+        } else {
+
+            $this->tryLogin();
+        }
+    }
+
+    function showPageNotice()
+    {
+        if ($this->error) {
+
+            $this->element('div', array('class' => 'error'), $this->error);
+
+        } else {
+        
+            $this->element(
+                'div', 'instructions',
+                // TRANS: %s is the site name.
+                sprintf(
+                    _m('This is the first time you\'ve logged into %s so we must connect your Facebook to a local account. You can either create a new local account, or connect with an existing local account.'),
+                    common_config('site', 'name')
+                )
+            );
+        }
+    }
+
+    function title()
+    {
+        // TRANS: Page title.
+        return _m('Facebook Setup');
+    }
+
+    function showForm($error=null, $username=null)
+    {
+        $this->error = $error;
+        $this->username = $username;
+
+        $this->showPage();
+    }
+
+    function showPage()
+    {
+        parent::showPage();
+    }
+
+    /**
+     * @fixme much of this duplicates core code, which is very fragile.
+     * Should probably be replaced with an extensible mini version of
+     * the core registration form.
+     */
+    function showContent()
+    {
+        if (!empty($this->message_text)) {
+            $this->element('p', null, $this->message);
+            return;
+        }
+
+        $this->elementStart('form', array('method' => 'post',
+                                          'id' => 'form_settings_facebook_connect',
+                                          'class' => 'form_settings',
+                                          'action' => common_local_url('facebookfinishlogin')));
+        $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options'));
+        // TRANS: Legend.
+        $this->element('legend', null, _m('Connection options'));
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        $this->element('input', array('type' => 'checkbox',
+                                      'id' => 'license',
+                                      'class' => 'checkbox',
+                                      'name' => 'license',
+                                      'value' => 'true'));
+        $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
+        // TRANS: %s is the name of the license used by the user for their status updates.
+        $message = _m('My text and files are available under %s ' .
+                     'except this private data: password, ' .
+                     'email address, IM address, and phone number.');
+        $link = '<a href="' .
+                htmlspecialchars(common_config('license', 'url')) .
+                '">' .
+                htmlspecialchars(common_config('license', 'title')) .
+                '</a>';
+        $this->raw(sprintf(htmlspecialchars($message), $link));
+        $this->elementEnd('label');
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+
+        $this->elementStart('fieldset');
+        $this->hidden('token', common_session_token());
+        $this->element('legend', null,
+                       // TRANS: Legend.
+                       _m('Create new account'));
+        $this->element('p', null,
+                       _m('Create a new user with this nickname.'));
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        // TRANS: Field label.
+        $this->input('newname', _m('New nickname'),
+                     ($this->username) ? $this->username : '',
+                     _m('1-64 lowercase letters or numbers, no punctuation or spaces'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+        // TRANS: Submit button.
+        $this->submit('create', _m('BUTTON','Create'));
+        $this->elementEnd('fieldset');
+
+        $this->elementStart('fieldset');
+        // TRANS: Legend.
+        $this->element('legend', null,
+                       _m('Connect existing account'));
+        $this->element('p', null,
+                       _m('If you already have an account, login with your username and password to connect it to your Facebook.'));
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        // TRANS: Field label.
+        $this->input('nickname', _m('Existing nickname'));
+        $this->elementEnd('li');
+        $this->elementStart('li');
+        $this->password('password', _m('Password'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+        // TRANS: Submit button.
+        $this->submit('connect', _m('BUTTON','Connect'));
+        $this->elementEnd('fieldset');
+
+        $this->elementEnd('fieldset');
+        $this->elementEnd('form');
+    }
+
+    function message($msg)
+    {
+        $this->message_text = $msg;
+        $this->showPage();
+    }
+
+    function createNewUser()
+    {
+        if (common_config('site', 'closed')) {
+            // TRANS: Client error trying to register with registrations not allowed.
+            $this->clientError(_m('Registration not allowed.'));
+            return;
+        }
+
+        $invite = null;
+
+        if (common_config('site', 'inviteonly')) {
+            $code = $_SESSION['invitecode'];
+            if (empty($code)) {
+                // TRANS: Client error trying to register with registrations 'invite only'.
+                $this->clientError(_m('Registration not allowed.'));
+                return;
+            }
+
+            $invite = Invitation::staticGet($code);
+
+            if (empty($invite)) {
+                // TRANS: Client error trying to register with an invalid invitation code.
+                $this->clientError(_m('Not a valid invitation code.'));
+                return;
+            }
+        }
+
+        $nickname = $this->trimmed('newname');
+
+        if (!Validate::string($nickname, array('min_length' => 1,
+                                               'max_length' => 64,
+                                               'format' => NICKNAME_FMT))) {
+            $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.'));
+            return;
+        }
+
+        if (!User::allowed_nickname($nickname)) {
+            $this->showForm(_m('Nickname not allowed.'));
+            return;
+        }
+
+        if (User::staticGet('nickname', $nickname)) {
+            $this->showForm(_m('Nickname already in use. Try another one.'));
+            return;
+        }
+
+        $args = array(
+            'nickname' => $nickname,
+            'fullname' => $this->fbuser['firstname'] . ' ' . $this->fbuser['lastname'],
+            // XXX: Figure out how to get email
+            'homepage' => $this->fbuser['link'],
+            'bio'      => $this->fbuser['about'],
+            'location' => $this->fbuser['location']['name']
+        );
+
+        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;
+        }
+
+        common_set_user($user);
+        common_real_login(true);
+
+        common_log(
+            LOG_INFO,
+            sprintf(
+                'Registered new user %d from Facebook user %s',
+                $user->id,
+                $this->fbuid
+            ),
+            __FILE__
+        );
+
+        common_redirect(
+            common_local_url(
+                'showstream',
+                array('nickname' => $user->nickname)
+            ),
+            303
+        );
+    }
+
+    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('Facebook Connect Plugin - ' .
+                         "Legit user to connect to Facebook: $nickname");
+        }
+
+        $result = $this->flinkUser($user->id, $this->fbuid);
+
+        if (!$result) {
+            $this->serverError(_m('Error connecting user to Facebook.'));
+            return;
+        }
+
+        common_debug('Facebook Connnect Plugin - ' .
+                     "Connected Facebook user $this->fbuid to local user $user->id");
+
+        common_set_user($user);
+        common_real_login(true);
+
+        $this->goHome($user->nickname);
+    }
+
+    function connectUser()
+    {
+        $user = common_current_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 to local user %d',
+                $this->fbuid,
+                $user->id
+            ),
+            __FILE__
+        );
+
+        // Return to Facebook connection settings tab
+        common_redirect(common_local_url('facebookfinishlogin'), 303);
+    }
+
+    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['firstname'] .
+            ' ' . $this->fbuser['lastname']);
+
+        if (!empty($fullname)) {
+            $fullname = $this->nicknamize($fullname);
+            if ($this->isNewNickname($fullname)) {
+                return $fullname;
+            }
+        }
+
+        return null;
+    }
+
+     /**
+      * Given a string, try to make it work as a nickname
+      */
+     function nicknamize($str)
+     {
+         $str = preg_replace('/\W/', '', $str);
+         return strtolower($str);
+     }
+
+    function isNewNickname($str)
+    {
+        if (!Validate::string($str, array('min_length' => 1,
+                                          'max_length' => 64,
+                                          'format' => NICKNAME_FMT))) {
+            return false;
+        }
+        if (!User::allowed_nickname($str)) {
+            return false;
+        }
+        if (User::staticGet('nickname', $str)) {
+            return false;
+        }
+        return true;
+    }
+
+}
index bb30be1af7f99cc8b91d77d074a9e9980c998d92..08c237fe6e190eadf71a35cad3fb425fbb4a8e38 100644 (file)
@@ -40,90 +40,10 @@ class FacebookloginAction extends Action
         parent::handle($args);
 
         if (common_is_real_login()) {
-            
             $this->clientError(_m('Already logged in.'));
-
-        } else {
-
-            $facebook = new Facebook(
-                array(
-                    'appId'  => common_config('facebook', 'appid'),
-                    'secret' => common_config('facebook', 'secret'),
-                    'cookie' => true,
-                )
-            );
-
-            $session = $facebook->getSession();
-            $fbuser  = null;
-
-            if ($session) {
-                try {
-                    $fbuid = $facebook->getUser();
-                    $fbuser  = $facebook->api('/me');
-                } catch (FacebookApiException $e) {
-                    common_log(LOG_ERROR, $e);
-                }
-            }
-
-            if (!empty($fbuser)) {
-                common_debug("Found a valid Facebook user", __FILE__);
-
-                // Check to see if we have a foreign link already
-                $flink = Foreign_link::getByForeignId($fbuid, FACEBOOK_SERVICE);
-
-                if (empty($flink)) {
-
-                    // See if the user would like to register a new local
-                    // account
-                    common_redirect(
-                        common_local_url('facebookregister'),
-                        303
-                    );
-
-                } else {
-
-                    // Log our user in!
-                    $user = $flink->getUser();
-
-                    if (!empty($user)) {
-
-                        common_log(
-                            LOG_INFO,
-                            sprintf(
-                                'Logged in Facebook user %s as user %s (%s)',
-                                $fbuid,
-                                $user->id,
-                                $user->nickname
-                            ),
-                            __FILE__
-                        );
-
-                        common_set_user($user);
-                        common_real_login(true);
-                        $this->goHome($user->nickname);
-                    }
-                }
-
-            }
-        }
-
-        $this->showPage();
-    }
-
-    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)
-            );
+            $this->showPage();
         }
-
-        common_redirect($url, 303);
     }
 
     function getInstructions()
@@ -151,14 +71,45 @@ class FacebookloginAction extends Action
 
         $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(
-            //'show-faces' => 'true',
-            //'max-rows'   => '4',
-            //'width'      => '600',
-            'perms'      => 'user_location,user_website,offline_access,publish_stream'
+            'src' => common_path(
+                'plugins/FacebookSSO/images/login-button.png',
+                true
+            ),
+            'alt'   => 'Login with Facebook',
+            'title' => 'Login with Facebook'
         );
 
-        $this->element('fb:login-button', $attrs);
+        $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');
     }
 
diff --git a/plugins/FacebookSSO/actions/facebookregister.php b/plugins/FacebookSSO/actions/facebookregister.php
deleted file mode 100644 (file)
index e21deff..0000000
+++ /dev/null
@@ -1,548 +0,0 @@
-<?php
-/**
- * StatusNet, the distributed open-source microblogging tool
- *
- * Register a local user and connect it to a Facebook account
- *
- * PHP version 5
- *
- * LICENCE: This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @category  Plugin
- * @package   StatusNet
- * @author    Zach Copley <zach@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link      http://status.net/
- */
-
-if (!defined('STATUSNET')) {
-    exit(1);
-}
-
-class FacebookregisterAction 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') {
-
-            $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')
-                );
-            }
-        } else {
-
-            $this->tryLogin();
-        }
-    }
-
-    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('facebookregister')));
-        $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options'));
-        // TRANS: Legend.
-        $this->element('legend', null, _m('Connection options'));
-        $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
-        $this->element('input', array('type' => 'checkbox',
-                                      'id' => 'license',
-                                      'class' => 'checkbox',
-                                      'name' => 'license',
-                                      'value' => 'true'));
-        $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
-        // TRANS: %s is the name of the license used by the user for their status updates.
-        $message = _m('My text and files are available under %s ' .
-                     'except this private data: password, ' .
-                     'email address, IM address, and phone number.');
-        $link = '<a href="' .
-                htmlspecialchars(common_config('license', 'url')) .
-                '">' .
-                htmlspecialchars(common_config('license', 'title')) .
-                '</a>';
-        $this->raw(sprintf(htmlspecialchars($message), $link));
-        $this->elementEnd('label');
-        $this->elementEnd('li');
-        $this->elementEnd('ul');
-
-        $this->elementStart('fieldset');
-        $this->hidden('token', common_session_token());
-        $this->element('legend', null,
-                       // TRANS: Legend.
-                       _m('Create new account'));
-        $this->element('p', null,
-                       _m('Create a new user with this nickname.'));
-        $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
-        // TRANS: Field label.
-        $this->input('newname', _m('New nickname'),
-                     ($this->username) ? $this->username : '',
-                     _m('1-64 lowercase letters or numbers, no punctuation or spaces'));
-        $this->elementEnd('li');
-        $this->elementEnd('ul');
-        // TRANS: Submit button.
-        $this->submit('create', _m('BUTTON','Create'));
-        $this->elementEnd('fieldset');
-
-        $this->elementStart('fieldset');
-        // TRANS: Legend.
-        $this->element('legend', null,
-                       _m('Connect existing account'));
-        $this->element('p', null,
-                       _m('If you already have an account, login with your username and password to connect it to your Facebook.'));
-        $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
-        // TRANS: Field label.
-        $this->input('nickname', _m('Existing nickname'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->password('password', _m('Password'));
-        $this->elementEnd('li');
-        $this->elementEnd('ul');
-        // TRANS: Submit button.
-        $this->submit('connect', _m('BUTTON','Connect'));
-        $this->elementEnd('fieldset');
-
-        $this->elementEnd('fieldset');
-        $this->elementEnd('form');
-    }
-
-    function message($msg)
-    {
-        $this->message_text = $msg;
-        $this->showPage();
-    }
-
-    function createNewUser()
-    {
-        if (common_config('site', 'closed')) {
-            // TRANS: Client error trying to register with registrations not allowed.
-            $this->clientError(_m('Registration not allowed.'));
-            return;
-        }
-
-        $invite = null;
-
-        if (common_config('site', 'inviteonly')) {
-            $code = $_SESSION['invitecode'];
-            if (empty($code)) {
-                // TRANS: Client error trying to register with registrations 'invite only'.
-                $this->clientError(_m('Registration not allowed.'));
-                return;
-            }
-
-            $invite = Invitation::staticGet($code);
-
-            if (empty($invite)) {
-                // TRANS: Client error trying to register with an invalid invitation code.
-                $this->clientError(_m('Not a valid invitation code.'));
-                return;
-            }
-        }
-
-        $nickname = $this->trimmed('newname');
-
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => NICKNAME_FMT))) {
-            $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.'));
-            return;
-        }
-
-        if (!User::allowed_nickname($nickname)) {
-            $this->showForm(_m('Nickname not allowed.'));
-            return;
-        }
-
-        if (User::staticGet('nickname', $nickname)) {
-            $this->showForm(_m('Nickname already in use. Try another one.'));
-            return;
-        }
-
-        $args = array(
-            'nickname' => $nickname,
-            'fullname' => $this->fbuser['firstname'] . ' ' . $this->fbuser['lastname'],
-            // XXX: Figure out how to get email
-            'homepage' => $this->fbuser['link'],
-            'bio'      => $this->fbuser['about'],
-            'location' => $this->fbuser['location']['name']
-        );
-
-        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;
-        }
-
-        common_set_user($user);
-        common_real_login(true);
-
-        common_log(
-            LOG_INFO,
-            sprintf(
-                'Registered new user %d from Facebook user %s',
-                $user->id,
-                $this->fbuid
-            ),
-            __FILE__
-        );
-
-        common_redirect(
-            common_local_url(
-                'showstream',
-                array('nickname' => $user->nickname)
-            ),
-            303
-        );
-    }
-
-    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('Facebook Connect Plugin - ' .
-                         "Legit user to connect to Facebook: $nickname");
-        }
-
-        $result = $this->flinkUser($user->id, $this->fbuid);
-
-        if (!$result) {
-            $this->serverError(_m('Error connecting user to Facebook.'));
-            return;
-        }
-
-        common_debug('Facebook Connnect Plugin - ' .
-                     "Connected Facebook user $this->fbuid to local user $user->id");
-
-        common_set_user($user);
-        common_real_login(true);
-
-        $this->goHome($user->nickname);
-    }
-
-    function connectUser()
-    {
-        $user = common_current_user();
-
-        $result = $this->flinkUser($user->id, $this->fbuid);
-
-        if (empty($result)) {
-            $this->serverError(_m('Error connecting user to Facebook.'));
-            return;
-        }
-
-        common_debug('Facebook Connect Plugin - ' .
-                     "Connected Facebook user $this->fbuid to local user $user->id");
-
-        // Return to Facebook connection settings tab
-        common_redirect(common_local_url('FBConnectSettings'), 303);
-    }
-
-    function tryLogin()
-    {
-        common_debug('Facebook Connect Plugin - ' .
-                     "Trying login for Facebook user $this->fbuid.");
-
-        $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_CONNECT_SERVICE);
-
-        if (!empty($flink)) {
-            $user = $flink->getUser();
-
-            if (!empty($user)) {
-
-                common_debug('Facebook Connect Plugin - ' .
-                             "Logged in Facebook user $flink->foreign_id as user $user->id ($user->nickname)");
-
-                common_set_user($user);
-                common_real_login(true);
-                $this->goHome($user->nickname);
-            }
-
-        } else {
-
-            common_debug('Facebook Connect Plugin - ' .
-                         "No flink found for fbuid: $this->fbuid - new user");
-
-            $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['firstname'] .
-            ' ' . $this->fbuser['lastname']);
-
-        if (!empty($fullname)) {
-            $fullname = $this->nicknamize($fullname);
-            if ($this->isNewNickname($fullname)) {
-                return $fullname;
-            }
-        }
-
-        return null;
-    }
-
-     /**
-      * Given a string, try to make it work as a nickname
-      */
-     function nicknamize($str)
-     {
-         $str = preg_replace('/\W/', '', $str);
-         return strtolower($str);
-     }
-
-    function isNewNickname($str)
-    {
-        if (!Validate::string($str, array('min_length' => 1,
-                                          'max_length' => 64,
-                                          'format' => NICKNAME_FMT))) {
-            return false;
-        }
-        if (!User::allowed_nickname($str)) {
-            return false;
-        }
-        if (User::staticGet('nickname', $str)) {
-            return false;
-        }
-        return true;
-    }
-
-}
diff --git a/plugins/FacebookSSO/images/login-button.png b/plugins/FacebookSSO/images/login-button.png
new file mode 100644 (file)
index 0000000..4e7766b
Binary files /dev/null and b/plugins/FacebookSSO/images/login-button.png differ
diff --git a/plugins/FacebookSSO/lib/facebookclient.php b/plugins/FacebookSSO/lib/facebookclient.php
new file mode 100644 (file)
index 0000000..a0549a1
--- /dev/null
@@ -0,0 +1,640 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for communicating with Facebook
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   StatusNet
+ * @author    Craig Andrews <candrews@integralblue.com>
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2009-2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Class for communication with Facebook
+ *
+ * @category Plugin
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class Facebookclient
+{
+    protected $facebook      = null; // Facebook Graph client obj
+    protected $flink         = null; // Foreign_link StatusNet -> Facebook
+    protected $notice        = null; // The user's notice
+    protected $user          = null; // Sender of the notice
+    protected $oldRestClient = null; // Old REST API client
+
+    function __constructor($notice)
+    {
+        $this->facebook = self::getFacebook();
+        $this->notice   = $notice;
+
+        $this->flink = Foreign_link::getByUserID(
+            $notice->profile_id,
+            FACEBOOK_SERVICE
+        );
+        
+        $this->user = $this->flink->getUser();
+
+        $this->oldRestClient = self::getOldRestClient();
+    }
+
+    /*
+     * Get and instance of the old REST API client for sending notices from
+     * users with Facebook links that pre-exist the Graph API
+     */
+    static function getOldRestClient()
+    {
+        $apikey = common_config('facebook', 'apikey');
+        $secret = common_config('facebook', 'secret');
+
+        // If there's no app key and secret set in the local config, look
+        // for a global one
+        if (empty($apikey) || empty($secret)) {
+            $apikey = common_config('facebook', 'global_apikey');
+            $secret = common_config('facebook', 'global_secret');
+        }
+
+        return new FacebookRestClient($apikey, $secret, null);
+    }
+
+    /*
+     * 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)
+    {
+        $client = new Facebookclient($notice);
+        $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 (empty($this->flink->credentials)) {
+            $this->sendOldRest();
+        } else {
+
+            // Otherwise we most likely have an access token
+            $this->sendGraph();
+        }
+    }
+
+    /*
+     * Send a notice to Facebook using the Graph API
+     */
+    function sendGraph()
+    {
+        common_debug("Send notice via Graph API", __FILE__);
+    }
+
+    /*
+     * 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()
+    {
+        if (isFacebookBound()) {
+
+            try {
+
+                $canPublish = $this->checkPermission('publish_stream');
+                $canUpdate  = $this->checkPermission('status_update');
+
+                // Post to Facebook
+                if ($notice->hasAttachments() && $canPublish == 1) {
+                    $this->restPublishStream();
+                } elseif ($canUpdate == 1 || $canPublish == 1) {
+                    $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 (FacebookRestClientException $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 ServerExpception("No such permission!");
+        }
+
+        $fbuid = $this->flink->foreign_link;
+
+        common_debug(
+            sprintf(
+                'Checking for %s permission for user %s (%d), fbuid %s',
+                $permission,
+                $this->user->nickname,
+                $this->user->id,
+                $fbuid
+            ),
+            __FILE__
+        );
+
+        // NOTE: $this->oldRestClient->users_hasAppPermission() has been
+        // returning bogus results, so we're using FQL to check for
+        // permissions
+
+        $fql = sprintf(
+            "SELECT %s FROM permissions WHERE uid = %s",
+            $permission,
+            $fbuid
+        );
+
+        $result = $this->oldRestClient->fql_query($fql);
+
+        $hasPermission = 0;
+
+        if (isset($result[0][$permission])) {
+            $canPublish = $result[0][$permission];
+        }
+
+        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;
+
+        }
+    
+    }
+
+    function handleFacebookError($e)
+    {
+        $fbuid  = $this->flink->foreign_id;
+        $code   = $e->getCode();
+        $errmsg = $e->getMessage();
+
+        // 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;
+        }
+    }
+
+    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->oldRestClient->users_setStatus(
+             $this->notice->content,
+             $fbuid,
+             false,
+             true
+        );
+
+        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__
+        );
+    }
+
+    function restPublishStream()
+    {
+        $fbuid = $this->flink->foreign_id;
+
+        common_debug(
+            sprintf(
+                'Attempting to post notice %d as stream item with attachment for '
+                . '%s (%d) fbuid %s',
+                $this->notice->id,
+                $this->user->nickname,
+                $this->user->id,
+                $fbuid
+            ),
+            __FILE__
+        );
+
+        $fbattachment = format_attachments($notice->attachments());
+
+        $this->oldRestClient->stream_publish(
+            $this->notice->content,
+            $fbattachment,
+            null,
+            null,
+            $fbuid
+        );
+
+        common_log(
+            LOG_INFO,
+            sprintf(
+                'Posted notice %d as a stream item with attachment for %s '
+                . '(%d), fbuid %s',
+                $this->notice->id,
+                $this->user->nickname,
+                $this->user->id,
+                $fbuid
+            ),
+            __FILE__
+        );
+        
+    }
+
+    function format_attachments($attachments)
+    {
+        $fbattachment          = array();
+        $fbattachment['media'] = array();
+
+        foreach($attachments as $attachment)
+        {
+            if($enclosure = $attachment->getEnclosure()){
+                $fbmedia = get_fbmedia_for_attachment($enclosure);
+            }else{
+                $fbmedia = get_fbmedia_for_attachment($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 an File objects, returns an associative array suitable for Facebook media
+     */
+    function get_fbmedia_for_attachment($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;
+    }
+
+    function disconnect()
+    {
+        $fbuid = $this->flink->foreign_link;
+
+        common_log(
+            LOG_INFO,
+            sprintf(
+                'Removing Facebook link for %s (%d), fbuid %s',
+                $this->user->nickname,
+                $this->user->id,
+                $fbuid
+            ),
+            __FILE__
+        );
+
+        $result = $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 = $user->getProfile();
+
+        $siteName = common_config('site', 'name');
+
+        common_switch_locale($user->language);
+
+        $subject = sprintf(
+            _m('Your Facebook connection has been removed'),
+            $siteName
+        );
+
+        $msg = <<<BODY
+Hi, %1$s. We're sorry to inform you we are unable to publish your notice to
+Facebook, and have removed the connection between your %2$s account and Facebook.
+
+This may have happened because you have removed permission for %2$s to post on
+your behalf, or perhaps you have deactivated your Facebook account. You can
+reconnect your %s account to Facebook at any time by logging in with Facebook
+again.
+BODY;
+        $body = sprintf(
+            _m($msg),
+            $this->user->nickname,
+            $siteName
+        );
+        
+        common_switch_locale();
+
+        return mail_to_user($this->user, $subject, $body);
+    }
+
+}
diff --git a/plugins/FacebookSSO/lib/facebookqueuehandler.php b/plugins/FacebookSSO/lib/facebookqueuehandler.php
new file mode 100644 (file)
index 0000000..af96d35
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Queuehandler for Facebook transport
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php';
+
+class FacebookQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'facebook';
+    }
+
+    function handle($notice)
+    {
+        if ($this->_isLocal($notice)) {
+            return 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);
+    }
+}