3 * StatusNet, the distributed open-source microblogging tool
5 * Class for doing OAuth authentication against Twitter
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 * @category TwitterauthorizationAction
24 * @author Zach Copley <zach@status.net>
25 * @copyright 2009 StatusNet, Inc.
26 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
27 * @link http://status.net/
30 if (!defined('STATUSNET') && !defined('LACONICA')) {
34 require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
37 * Class for doing OAuth authentication against Twitter
39 * Peforms the OAuth "dance" between StatusNet and Twitter -- requests a token,
40 * authorizes it, and exchanges it for an access token. It also creates a link
41 * (Foreign_link) between the StatusNet user and Twitter user and stores the
42 * access token and secret in the link.
46 * @author Zach Copley <zach@status.net>
47 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
48 * @link http://laconi.ca/
51 class TwitterauthorizationAction extends Action
54 var $tw_fields = null;
55 var $access_token = null;
59 * Initialize class members. Looks for 'oauth_token' parameter.
61 * @param array $args misc. arguments
63 * @return boolean true
65 function prepare($args)
67 parent::prepare($args);
69 $this->signin = $this->boolean('signin');
70 $this->oauth_token = $this->arg('oauth_token');
78 * @param array $args is ignored since it's now passed in in prepare()
82 function handle($args)
84 parent::handle($args);
86 if (common_logged_in()) {
87 $user = common_current_user();
88 $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
90 // If there's already a foreign link record, it means we already
91 // have an access token, and this is unecessary. So go back.
94 common_redirect(common_local_url('twittersettings'));
98 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
100 // User was not logged in to StatusNet before
102 $this->twuid = $this->trimmed('twuid');
104 $this->tw_fields = array('screen_name' => $this->trimmed('tw_fields_screen_name'),
105 'fullname' => $this->trimmed('tw_fields_fullname'));
107 $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret'));
109 $token = $this->trimmed('token');
111 if (!$token || $token != common_session_token()) {
112 $this->showForm(_('There was a problem with your session token. Try again, please.'));
116 if ($this->arg('create')) {
117 if (!$this->boolean('license')) {
118 $this->showForm(_('You can\'t register if you don\'t agree to the license.'),
119 $this->trimmed('newname'));
122 $this->createNewUser();
123 } else if ($this->arg('connect')) {
124 $this->connectNewUser();
126 common_debug('Twitter Connect Plugin - ' .
127 print_r($this->args, true));
128 $this->showForm(_('Something weird happened.'),
129 $this->trimmed('newname'));
132 // $this->oauth_token is only populated once Twitter authorizes our
133 // request token. If it's empty we're at the beginning of the auth
136 if (empty($this->oauth_token)) {
137 $this->authorizeRequestToken();
139 $this->saveAccessToken();
145 * Asks Twitter for a request token, and then redirects to Twitter
150 function authorizeRequestToken()
154 // Get a new request token and authorize it
156 $client = new TwitterOAuthClient();
158 $client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
160 // Sock the request token away in the session temporarily
162 $_SESSION['twitter_request_token'] = $req_tok->key;
163 $_SESSION['twitter_request_token_secret'] = $req_tok->secret;
165 $auth_link = $client->getAuthorizeLink($req_tok, $this->signin);
167 } catch (OAuthClientException $e) {
168 $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
169 $e->getCode(), $e->getMessage());
170 $this->serverError(_m('Couldn\'t link your Twitter account.'));
173 common_redirect($auth_link);
177 * Called when Twitter returns an authorized request token. Exchanges
178 * it for an access token and stores it.
182 function saveAccessToken()
185 // Check to make sure Twitter returned the same request
186 // token we sent them
188 if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
189 $this->serverError(_m('Couldn\'t link your Twitter account.'));
192 $twitter_user = null;
196 $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
197 $_SESSION['twitter_request_token_secret']);
199 // Exchange the request token for an access token
201 $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
203 // Test the access token and get the user's Twitter info
205 $client = new TwitterOAuthClient($atok->key, $atok->secret);
206 $twitter_user = $client->verifyCredentials();
208 } catch (OAuthClientException $e) {
209 $msg = sprintf('OAuth client error - code: %1$s, msg: %2$s',
210 $e->getCode(), $e->getMessage());
211 $this->serverError(_m('Couldn\'t link your Twitter account.'));
214 if (common_logged_in()) {
216 // Save the access token and Twitter user info
218 $user = common_current_user();
219 $this->saveForeignLink($user->id, $twitter_user->id, $atok);
220 save_twitter_user($twitter_user->id, $twitter_user->name);
224 $this->twuid = $twitter_user->id;
225 $this->tw_fields = array("screen_name" => $twitter_user->screen_name,
226 "name" => $twitter_user->name);
227 $this->access_token = $atok;
231 // Clean up the the mess we made in the session
233 unset($_SESSION['twitter_request_token']);
234 unset($_SESSION['twitter_request_token_secret']);
236 if (common_logged_in()) {
237 common_redirect(common_local_url('twittersettings'));
242 * Saves a Foreign_link between Twitter user and local user,
243 * which includes the access token and secret.
245 * @param int $user_id StatusNet user ID
246 * @param int $twuid Twitter user ID
247 * @param OAuthToken $token the access token to save
251 function saveForeignLink($user_id, $twuid, $access_token)
253 $flink = new Foreign_link();
255 $flink->user_id = $user_id;
256 $flink->foreign_id = $twuid;
257 $flink->service = TWITTER_SERVICE;
259 $creds = TwitterOAuthClient::packToken($access_token);
261 $flink->credentials = $creds;
262 $flink->created = common_sql_now();
264 // Defaults: noticesync on, everything else off
266 $flink->set_flags(true, false, false, false);
268 $flink_id = $flink->insert();
270 if (empty($flink_id)) {
271 common_log_db_error($flink, 'INSERT', __FILE__);
272 $this->serverError(_('Couldn\'t link your Twitter account.'));
278 function showPageNotice()
281 $this->element('div', array('class' => 'error'), $this->error);
283 $this->element('div', 'instructions',
284 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')));
290 return _('Twitter Account Setup');
293 function showForm($error=null, $username=null)
295 $this->error = $error;
296 $this->username = $username;
306 function showContent()
308 if (!empty($this->message_text)) {
309 $this->element('p', null, $this->message);
313 $this->elementStart('form', array('method' => 'post',
314 'id' => 'form_settings_twitter_connect',
315 'class' => 'form_settings',
316 'action' => common_local_url('twitterauthorization')));
317 $this->elementStart('fieldset', array('id' => 'settings_twitter_connect_options'));
318 $this->element('legend', null, _('Connection options'));
319 $this->elementStart('ul', 'form_data');
320 $this->elementStart('li');
321 $this->element('input', array('type' => 'checkbox',
323 'class' => 'checkbox',
326 $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
327 $this->text(_('My text and files are available under '));
328 $this->element('a', array('href' => common_config('license', 'url')),
329 common_config('license', 'title'));
330 $this->text(_(' except this private data: password, email address, IM address, phone number.'));
331 $this->elementEnd('label');
332 $this->elementEnd('li');
333 $this->elementEnd('ul');
334 $this->hidden('access_token_key', $this->access_token->key);
335 $this->hidden('access_token_secret', $this->access_token->secret);
336 $this->hidden('twuid', $this->twuid);
337 $this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
338 $this->hidden('tw_fields_name', $this->tw_fields['name']);
340 $this->elementStart('fieldset');
341 $this->hidden('token', common_session_token());
342 $this->element('legend', null,
343 _('Create new account'));
344 $this->element('p', null,
345 _('Create a new user with this nickname.'));
346 $this->elementStart('ul', 'form_data');
347 $this->elementStart('li');
348 $this->input('newname', _('New nickname'),
349 ($this->username) ? $this->username : '',
350 _('1-64 lowercase letters or numbers, no punctuation or spaces'));
351 $this->elementEnd('li');
352 $this->elementEnd('ul');
353 $this->submit('create', _('Create'));
354 $this->elementEnd('fieldset');
356 $this->elementStart('fieldset');
357 $this->element('legend', null,
358 _('Connect existing account'));
359 $this->element('p', null,
360 _('If you already have an account, login with your username and password to connect it to your Twitter account.'));
361 $this->elementStart('ul', 'form_data');
362 $this->elementStart('li');
363 $this->input('nickname', _('Existing nickname'));
364 $this->elementEnd('li');
365 $this->elementStart('li');
366 $this->password('password', _('Password'));
367 $this->elementEnd('li');
368 $this->elementEnd('ul');
369 $this->submit('connect', _('Connect'));
370 $this->elementEnd('fieldset');
372 $this->elementEnd('fieldset');
373 $this->elementEnd('form');
376 function message($msg)
378 $this->message_text = $msg;
382 function createNewUser()
384 if (common_config('site', 'closed')) {
385 $this->clientError(_('Registration not allowed.'));
391 if (common_config('site', 'inviteonly')) {
392 $code = $_SESSION['invitecode'];
394 $this->clientError(_('Registration not allowed.'));
398 $invite = Invitation::staticGet($code);
400 if (empty($invite)) {
401 $this->clientError(_('Not a valid invitation code.'));
406 $nickname = $this->trimmed('newname');
408 if (!Validate::string($nickname, array('min_length' => 1,
410 'format' => NICKNAME_FMT))) {
411 $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
415 if (!User::allowed_nickname($nickname)) {
416 $this->showForm(_('Nickname not allowed.'));
420 if (User::staticGet('nickname', $nickname)) {
421 $this->showForm(_('Nickname already in use. Try another one.'));
425 $fullname = trim($this->tw_fields['name']);
427 $args = array('nickname' => $nickname, 'fullname' => $fullname);
429 if (!empty($invite)) {
430 $args['code'] = $invite->code;
433 $user = User::register($args);
435 $result = $this->saveForeignLink($user->id,
437 $this->access_token);
439 save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
442 $this->serverError(_('Error connecting user to Twitter.'));
446 common_set_user($user);
447 common_real_login(true);
449 common_debug('TwitterBridge Plugin - ' .
450 "Registered new user $user->id from Twitter user $this->twuid");
452 common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)),
456 function connectNewUser()
458 $nickname = $this->trimmed('nickname');
459 $password = $this->trimmed('password');
461 if (!common_check_user($nickname, $password)) {
462 $this->showForm(_('Invalid username or password.'));
466 $user = User::staticGet('nickname', $nickname);
469 common_debug('TwitterBridge Plugin - ' .
470 "Legit user to connect to Twitter: $nickname");
473 $result = $this->saveForeignLink($user->id,
475 $this->access_token);
477 save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
480 $this->serverError(_('Error connecting user to Twitter.'));
484 common_debug('TwitterBridge Plugin - ' .
485 "Connected Twitter user $this->twuid to local user $user->id");
487 common_set_user($user);
488 common_real_login(true);
490 $this->goHome($user->nickname);
493 function connectUser()
495 $user = common_current_user();
497 $result = $this->flinkUser($user->id, $this->twuid);
499 if (empty($result)) {
500 $this->serverError(_('Error connecting user to Twitter.'));
504 common_debug('TwitterBridge Plugin - ' .
505 "Connected Twitter user $this->twuid to local user $user->id");
507 // Return to Twitter connection settings tab
508 common_redirect(common_local_url('twittersettings'), 303);
513 common_debug('TwitterBridge Plugin - ' .
514 "Trying login for Twitter user $this->twuid.");
516 $flink = Foreign_link::getByForeignID($this->twuid,
519 if (!empty($flink)) {
520 $user = $flink->getUser();
524 common_debug('TwitterBridge Plugin - ' .
525 "Logged in Twitter user $flink->foreign_id as user $user->id ($user->nickname)");
527 common_set_user($user);
528 common_real_login(true);
529 $this->goHome($user->nickname);
534 common_debug('TwitterBridge Plugin - ' .
535 "No flink found for twuid: $this->twuid - new user");
537 $this->showForm(null, $this->bestNewNickname());
541 function goHome($nickname)
543 $url = common_get_returnto();
545 // We don't have to return to it again
546 common_set_returnto(null);
548 $url = common_local_url('all',
553 common_redirect($url, 303);
556 function bestNewNickname()
558 if (!empty($this->tw_fields['name'])) {
559 $nickname = $this->nicknamize($this->tw_fields['name']);
560 if ($this->isNewNickname($nickname)) {
568 // Given a string, try to make it work as a nickname
570 function nicknamize($str)
572 $str = preg_replace('/\W/', '', $str);
573 $str = str_replace(array('-', '_'), '', $str);
574 return strtolower($str);
577 function isNewNickname($str)
579 if (!Validate::string($str, array('min_length' => 1,
581 'format' => NICKNAME_FMT))) {
584 if (!User::allowed_nickname($str)) {
587 if (User::staticGet('nickname', $str)) {