We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

twitterauthorization.php 20.4 KB
Newer Older
1 2
<?php
/**
3
 * StatusNet, the distributed open-source microblogging tool
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 *
 * Class for doing OAuth authentication against Twitter
 *
 * 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/>.
 *
Zach Copley's avatar
Zach Copley committed
22
 * @category  Plugin
23
 * @package   StatusNet
Julien C's avatar
Julien C committed
24
 * @author    Zach Copley <zach@status.net>
Zach Copley's avatar
Zach Copley committed
25 26
 * @author    Julien C <chaumond@gmail.com>
 * @copyright 2009-2010 StatusNet, Inc.
27
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
28
 * @link      http://status.net/
29 30
 */

31
if (!defined('STATUSNET') && !defined('LACONICA')) {
32 33 34
    exit(1);
}

35 36
require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';

37 38 39
/**
 * Class for doing OAuth authentication against Twitter
 *
40
 * Peforms the OAuth "dance" between StatusNet and Twitter -- requests a token,
41
 * authorizes it, and exchanges it for an access token.  It also creates a link
42
 * (Foreign_link) between the StatusNet user and Twitter user and stores the
43 44
 * access token and secret in the link.
 *
Zach Copley's avatar
Zach Copley committed
45
 * @category Plugin
46 47
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
Zach Copley's avatar
Zach Copley committed
48
 * @author   Julien C <chaumond@gmail.com>
49
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
50
 * @link     http://status.net/
51 52
 *
 */
53 54
class TwitterauthorizationAction extends Action
{
55 56
    var $twuid        = null;
    var $tw_fields    = null;
Julien C's avatar
Julien C committed
57
    var $access_token = null;
58
    var $signin       = null;
59
    var $verifier     = null;
60

61 62 63 64 65 66 67
    /**
     * Initialize class members. Looks for 'oauth_token' parameter.
     *
     * @param array $args misc. arguments
     *
     * @return boolean true
     */
68 69 70 71
    function prepare($args)
    {
        parent::prepare($args);

72
        $this->signin      = $this->boolean('signin');
73
        $this->oauth_token = $this->arg('oauth_token');
74
        $this->verifier    = $this->arg('oauth_verifier');
75 76 77 78

        return true;
    }

79 80 81 82 83 84 85
    /**
     * Handler method
     *
     * @param array $args is ignored since it's now passed in in prepare()
     *
     * @return nothing
     */
86 87 88
    function handle($args)
    {
        parent::handle($args);
89

Julien C's avatar
Julien C committed
90 91 92
        if (common_logged_in()) {
            $user  = common_current_user();
            $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
93

94 95 96
            // If there's already a foreign link record and a foreign user
            // it means the accounts are already linked, and this is unecessary.
            // So go back.
97

Julien C's avatar
Julien C committed
98
            if (isset($flink)) {
99 100 101 102
                $fuser = $flink->getForeignUser();
                if (!empty($fuser)) {
                    common_redirect(common_local_url('twittersettings'));
                }
Julien C's avatar
Julien C committed
103
            }
104
        }
105

Julien C's avatar
Julien C committed
106
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
107

Julien C's avatar
Julien C committed
108
            // User was not logged in to StatusNet before
109

Julien C's avatar
Julien C committed
110
            $this->twuid = $this->trimmed('twuid');
111

Zach Copley's avatar
Zach Copley committed
112
            $this->tw_fields = array('screen_name' => $this->trimmed('tw_fields_screen_name'),
113 114
                                     'fullname' => $this->trimmed('tw_fields_fullname'));

Julien C's avatar
Julien C committed
115 116 117
            $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret'));

            $token = $this->trimmed('token');
118

Julien C's avatar
Julien C committed
119 120 121 122
            if (!$token || $token != common_session_token()) {
                $this->showForm(_('There was a problem with your session token. Try again, please.'));
                return;
            }
123

Julien C's avatar
Julien C committed
124 125 126 127 128 129 130 131 132 133
            if ($this->arg('create')) {
                if (!$this->boolean('license')) {
                    $this->showForm(_('You can\'t register if you don\'t agree to the license.'),
                                    $this->trimmed('newname'));
                    return;
                }
                $this->createNewUser();
            } else if ($this->arg('connect')) {
                $this->connectNewUser();
            } else {
134
                common_debug('Twitter bridge - ' . print_r($this->args, true));
Julien C's avatar
Julien C committed
135 136 137
                $this->showForm(_('Something weird happened.'),
                                $this->trimmed('newname'));
            }
138
        } else {
Julien C's avatar
Julien C committed
139 140 141
            // $this->oauth_token is only populated once Twitter authorizes our
            // request token. If it's empty we're at the beginning of the auth
            // process
142

Julien C's avatar
Julien C committed
143 144 145 146 147
            if (empty($this->oauth_token)) {
                $this->authorizeRequestToken();
            } else {
                $this->saveAccessToken();
            }
148 149
        }
    }
150

151 152 153 154 155 156 157 158 159
    /**
     * Asks Twitter for a request token, and then redirects to Twitter
     * to authorize it.
     *
     * @return nothing
     */
    function authorizeRequestToken()
    {
        try {
160

161
            // Get a new request token and authorize it
162

163
            $client  = new TwitterOAuthClient();
164
            $req_tok = $client->getRequestToken();
165

166
            // Sock the request token away in the session temporarily
167

168
            $_SESSION['twitter_request_token']        = $req_tok->key;
169
            $_SESSION['twitter_request_token_secret'] = $req_tok->secret;
170

171
            $auth_link = $client->getAuthorizeLink($req_tok, $this->signin);
172

173
        } catch (OAuthClientException $e) {
174 175 176 177 178 179 180
            $msg = sprintf(
                'OAuth client error - code: %1s, msg: %2s',
                $e->getCode(),
                $e->getMessage()
            );
            common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
            $this->serverError(
181
                _m('Couldn\'t link your Twitter account.')
182
            );
183
        }
184

185 186
        common_redirect($auth_link);
    }
187

188 189 190 191 192 193 194 195 196 197
    /**
     * Called when Twitter returns an authorized request token. Exchanges
     * it for an access token and stores it.
     *
     * @return nothing
     */
    function saveAccessToken()
    {
        // Check to make sure Twitter returned the same request
        // token we sent them
198

199
        if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
200 201 202
            $this->serverError(
                _m('Couldn\'t link your Twitter account: oauth_token mismatch.')
            );
203
        }
204

205 206
        $twitter_user = null;

207
        try {
208

209 210
            $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
                $_SESSION['twitter_request_token_secret']);
211

212
            // Exchange the request token for an access token
213

214
            $atok = $client->getAccessToken($this->verifier);
215

216
            // Test the access token and get the user's Twitter info
217

218
            $client       = new TwitterOAuthClient($atok->key, $atok->secret);
219
            $twitter_user = $client->verifyCredentials();
220

221
        } catch (OAuthClientException $e) {
222 223 224 225 226 227 228
            $msg = sprintf(
                'OAuth client error - code: %1$s, msg: %2$s',
                $e->getCode(),
                $e->getMessage()
            );
            common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
            $this->serverError(
229
                _m('Couldn\'t link your Twitter account.')
230
            );
231
        }
232

Julien C's avatar
Julien C committed
233
        if (common_logged_in()) {
234

Julien C's avatar
Julien C committed
235
            // Save the access token and Twitter user info
236

Zach Copley's avatar
Zach Copley committed
237 238
            $user = common_current_user();
            $this->saveForeignLink($user->id, $twitter_user->id, $atok);
239
            save_twitter_user($twitter_user->id, $twitter_user->screen_name);
240 241 242

        } else {

Julien C's avatar
Julien C committed
243
            $this->twuid = $twitter_user->id;
Zach Copley's avatar
Zach Copley committed
244 245
            $this->tw_fields = array("screen_name" => $twitter_user->screen_name,
                                     "name" => $twitter_user->name);
Julien C's avatar
Julien C committed
246 247 248
            $this->access_token = $atok;
            $this->tryLogin();
        }
249

250
        // Clean up the the mess we made in the session
251

252 253
        unset($_SESSION['twitter_request_token']);
        unset($_SESSION['twitter_request_token_secret']);
254

Julien C's avatar
Julien C committed
255 256 257
        if (common_logged_in()) {
            common_redirect(common_local_url('twittersettings'));
        }
258
    }
259

260 261 262 263
    /**
     * Saves a Foreign_link between Twitter user and local user,
     * which includes the access token and secret.
     *
Zach Copley's avatar
Zach Copley committed
264 265 266
     * @param int        $user_id StatusNet user ID
     * @param int        $twuid   Twitter user ID
     * @param OAuthToken $token   the access token to save
267 268 269
     *
     * @return nothing
     */
Zach Copley's avatar
Zach Copley committed
270
    function saveForeignLink($user_id, $twuid, $access_token)
271 272 273
    {
        $flink = new Foreign_link();

274 275
        $flink->user_id = $user_id;
        $flink->service = TWITTER_SERVICE;
276 277 278 279 280

        // delete stale flink, if any
        $result = $flink->find(true);

        if (!empty($result)) {
281
            $flink->safeDelete();
282
        }
283

284 285 286 287
        $flink->user_id     = $user_id;
        $flink->foreign_id  = $twuid;
        $flink->service     = TWITTER_SERVICE;

Zach Copley's avatar
Zach Copley committed
288
        $creds = TwitterOAuthClient::packToken($access_token);
Julien C's avatar
Julien C committed
289

290 291
        $flink->credentials = $creds;
        $flink->created     = common_sql_now();
Julien C's avatar
Julien C committed
292

293 294 295 296 297 298 299 300
        // Defaults: noticesync on, everything else off

        $flink->set_flags(true, false, false, false);

        $flink_id = $flink->insert();

        if (empty($flink_id)) {
            common_log_db_error($flink, 'INSERT', __FILE__);
301
            $this->serverError(_('Couldn\'t link your Twitter account.'));
302 303 304 305
        }

        return $flink_id;
    }
Julien C's avatar
Julien C committed
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334

    function showPageNotice()
    {
        if ($this->error) {
            $this->element('div', array('class' => 'error'), $this->error);
        } else {
            $this->element('div', 'instructions',
                           sprintf(_('This is the first time you\'ve logged into %s so we must connect your Twitter account to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name')));
        }
    }

    function title()
    {
        return _('Twitter Account Setup');
    }

    function showForm($error=null, $username=null)
    {
        $this->error = $error;
        $this->username = $username;

        $this->showPage();
    }

    function showPage()
    {
        parent::showPage();
    }

335 336 337 338 339
    /**
     * @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.
     */
Julien C's avatar
Julien C committed
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
    function showContent()
    {
        if (!empty($this->message_text)) {
            $this->element('p', null, $this->message);
            return;
        }

        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_settings_twitter_connect',
                                          'class' => 'form_settings',
                                          'action' => common_local_url('twitterauthorization')));
        $this->elementStart('fieldset', array('id' => 'settings_twitter_connect_options'));
        $this->element('legend', null, _('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'));
361 362 363 364 365 366 367 368 369
        $message = _('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));
Julien C's avatar
Julien C committed
370 371 372 373 374 375
        $this->elementEnd('label');
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->hidden('access_token_key', $this->access_token->key);
        $this->hidden('access_token_secret', $this->access_token->secret);
        $this->hidden('twuid', $this->twuid);
Zach Copley's avatar
Zach Copley committed
376
        $this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
Julien C's avatar
Julien C committed
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
        $this->hidden('tw_fields_name', $this->tw_fields['name']);

        $this->elementStart('fieldset');
        $this->hidden('token', common_session_token());
        $this->element('legend', null,
                       _('Create new account'));
        $this->element('p', null,
                       _('Create a new user with this nickname.'));
        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        $this->input('newname', _('New nickname'),
                     ($this->username) ? $this->username : '',
                     _('1-64 lowercase letters or numbers, no punctuation or spaces'));
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->submit('create', _('Create'));
        $this->elementEnd('fieldset');

        $this->elementStart('fieldset');
        $this->element('legend', null,
                       _('Connect existing account'));
        $this->element('p', null,
                       _('If you already have an account, login with your username and password to connect it to your Twitter account.'));
        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        $this->input('nickname', _('Existing nickname'));
        $this->elementEnd('li');
        $this->elementStart('li');
        $this->password('password', _('Password'));
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->submit('connect', _('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')) {
            $this->clientError(_('Registration not allowed.'));
            return;
        }

        $invite = null;

        if (common_config('site', 'inviteonly')) {
            $code = $_SESSION['invitecode'];
            if (empty($code)) {
                $this->clientError(_('Registration not allowed.'));
                return;
            }

            $invite = Invitation::staticGet($code);

            if (empty($invite)) {
                $this->clientError(_('Not a valid invitation code.'));
                return;
            }
        }

        $nickname = $this->trimmed('newname');

        if (!Validate::string($nickname, array('min_length' => 1,
                                               'max_length' => 64,
                                               'format' => NICKNAME_FMT))) {
            $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
            return;
        }

        if (!User::allowed_nickname($nickname)) {
            $this->showForm(_('Nickname not allowed.'));
            return;
        }

        if (User::staticGet('nickname', $nickname)) {
            $this->showForm(_('Nickname already in use. Try another one.'));
            return;
        }

Zach Copley's avatar
Zach Copley committed
464
        $fullname = trim($this->tw_fields['name']);
Julien C's avatar
Julien C committed
465 466 467 468 469 470 471 472 473

        $args = array('nickname' => $nickname, 'fullname' => $fullname);

        if (!empty($invite)) {
            $args['code'] = $invite->code;
        }

        $user = User::register($args);

474 475 476 477 478
        if (empty($user)) {
            $this->serverError(_('Error registering user.'));
            return;
        }

Zach Copley's avatar
Zach Copley committed
479 480 481 482 483
        $result = $this->saveForeignLink($user->id,
                                         $this->twuid,
                                         $this->access_token);

        save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
Julien C's avatar
Julien C committed
484 485 486 487 488 489 490 491 492

        if (!$result) {
            $this->serverError(_('Error connecting user to Twitter.'));
            return;
        }

        common_set_user($user);
        common_real_login(true);

493
        common_debug('TwitterBridge Plugin - ' .
Zach Copley's avatar
Zach Copley committed
494
                     "Registered new user $user->id from Twitter user $this->twuid");
Julien C's avatar
Julien C committed
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512

        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(_('Invalid username or password.'));
            return;
        }

        $user = User::staticGet('nickname', $nickname);

        if (!empty($user)) {
513
            common_debug('TwitterBridge Plugin - ' .
Julien C's avatar
Julien C committed
514 515 516
                         "Legit user to connect to Twitter: $nickname");
        }

Zach Copley's avatar
Zach Copley committed
517 518 519 520 521
        $result = $this->saveForeignLink($user->id,
                                         $this->twuid,
                                         $this->access_token);

        save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
Julien C's avatar
Julien C committed
522 523 524 525 526 527

        if (!$result) {
            $this->serverError(_('Error connecting user to Twitter.'));
            return;
        }

528
        common_debug('TwitterBridge Plugin - ' .
Zach Copley's avatar
Zach Copley committed
529
                     "Connected Twitter user $this->twuid to local user $user->id");
Julien C's avatar
Julien C committed
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547

        common_set_user($user);
        common_real_login(true);

        $this->goHome($user->nickname);
    }

    function connectUser()
    {
        $user = common_current_user();

        $result = $this->flinkUser($user->id, $this->twuid);

        if (empty($result)) {
            $this->serverError(_('Error connecting user to Twitter.'));
            return;
        }

548
        common_debug('TwitterBridge Plugin - ' .
Zach Copley's avatar
Zach Copley committed
549
                     "Connected Twitter user $this->twuid to local user $user->id");
Julien C's avatar
Julien C committed
550 551 552 553

        // Return to Twitter connection settings tab
        common_redirect(common_local_url('twittersettings'), 303);
    }
554

Julien C's avatar
Julien C committed
555 556
    function tryLogin()
    {
557 558
        common_debug('TwitterBridge Plugin - ' .
                     "Trying login for Twitter user $this->twuid.");
Julien C's avatar
Julien C committed
559

Zach Copley's avatar
Zach Copley committed
560 561
        $flink = Foreign_link::getByForeignID($this->twuid,
                                              TWITTER_SERVICE);
Julien C's avatar
Julien C committed
562 563 564 565 566 567

        if (!empty($flink)) {
            $user = $flink->getUser();

            if (!empty($user)) {

568
                common_debug('TwitterBridge Plugin - ' .
Julien C's avatar
Julien C committed
569 570 571 572 573 574 575 576 577
                             "Logged in Twitter user $flink->foreign_id as user $user->id ($user->nickname)");

                common_set_user($user);
                common_real_login(true);
                $this->goHome($user->nickname);
            }

        } else {

578
            common_debug('TwitterBridge Plugin - ' .
Julien C's avatar
Julien C committed
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
                         "No flink found for twuid: $this->twuid - 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 bestNewNickname()
    {
        if (!empty($this->tw_fields['name'])) {
            $nickname = $this->nicknamize($this->tw_fields['name']);
            if ($this->isNewNickname($nickname)) {
                return $nickname;
            }
        }

        return null;
    }

     // Given a string, try to make it work as a nickname

     function nicknamize($str)
     {
         $str = preg_replace('/\W/', '', $str);
         $str = str_replace(array('-', '_'), '', $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;
    }

637 638
}