twitterauthorization.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Class for doing OAuth authentication against Twitter
  6. *
  7. * PHP version 5
  8. *
  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.
  13. *
  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.
  18. *
  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/>.
  21. *
  22. * @category Plugin
  23. * @package StatusNet
  24. * @author Zach Copley <zach@status.net>
  25. * @author Julien C <chaumond@gmail.com>
  26. * @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. if (!defined('STATUSNET') && !defined('LACONICA')) {
  31. exit(1);
  32. }
  33. require_once dirname(__DIR__) . '/twitter.php';
  34. /**
  35. * Class for doing OAuth authentication against Twitter
  36. *
  37. * Peforms the OAuth "dance" between StatusNet and Twitter -- requests a token,
  38. * authorizes it, and exchanges it for an access token. It also creates a link
  39. * (Foreign_link) between the StatusNet user and Twitter user and stores the
  40. * access token and secret in the link.
  41. *
  42. * @category Plugin
  43. * @package StatusNet
  44. * @author Zach Copley <zach@status.net>
  45. * @author Julien C <chaumond@gmail.com>
  46. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  47. * @link http://status.net/
  48. *
  49. */
  50. class TwitterauthorizationAction extends Action
  51. {
  52. var $twuid = null;
  53. var $tw_fields = null;
  54. var $access_token = null;
  55. var $signin = null;
  56. var $verifier = null;
  57. /**
  58. * Initialize class members. Looks for 'oauth_token' parameter.
  59. *
  60. * @param array $args misc. arguments
  61. *
  62. * @return boolean true
  63. */
  64. function prepare($args)
  65. {
  66. parent::prepare($args);
  67. $this->signin = $this->boolean('signin');
  68. $this->oauth_token = $this->arg('oauth_token');
  69. $this->verifier = $this->arg('oauth_verifier');
  70. return true;
  71. }
  72. /**
  73. * Handler method
  74. *
  75. * @param array $args is ignored since it's now passed in in prepare()
  76. *
  77. * @return nothing
  78. */
  79. function handle($args)
  80. {
  81. parent::handle($args);
  82. if (common_logged_in()) {
  83. $user = common_current_user();
  84. $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
  85. // If there's already a foreign link record and a foreign user
  86. // it means the accounts are already linked, and this is unecessary.
  87. // So go back.
  88. if (isset($flink)) {
  89. $fuser = $flink->getForeignUser();
  90. if (!empty($fuser)) {
  91. common_redirect(common_local_url('twittersettings'));
  92. }
  93. }
  94. }
  95. if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  96. // User was not logged in to StatusNet before
  97. $this->twuid = $this->trimmed('twuid');
  98. $this->tw_fields = array('screen_name' => $this->trimmed('tw_fields_screen_name'),
  99. 'fullname' => $this->trimmed('tw_fields_fullname'));
  100. $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret'));
  101. $token = $this->trimmed('token');
  102. if (!$token || $token != common_session_token()) {
  103. // TRANS: Client error displayed when the session token does not match or is not given.
  104. $this->showForm(_m('There was a problem with your session token. Try again, please.'));
  105. return;
  106. }
  107. if ($this->arg('create')) {
  108. if (!$this->boolean('license')) {
  109. // TRANS: Form validation error displayed when the checkbox to agree to the license has not been checked.
  110. $this->showForm(_m('You cannot register if you do not agree to the license.'),
  111. $this->trimmed('newname'));
  112. return;
  113. }
  114. $this->createNewUser();
  115. } else if ($this->arg('connect')) {
  116. $this->connectNewUser();
  117. } else {
  118. common_debug('Twitter bridge - ' . print_r($this->args, true));
  119. // TRANS: Form validation error displayed when an unhandled error occurs.
  120. $this->showForm(_m('Something weird happened.'),
  121. $this->trimmed('newname'));
  122. }
  123. } else {
  124. // $this->oauth_token is only populated once Twitter authorizes our
  125. // request token. If it's empty we're at the beginning of the auth
  126. // process
  127. if (empty($this->oauth_token)) {
  128. $this->authorizeRequestToken();
  129. } else {
  130. $this->saveAccessToken();
  131. }
  132. }
  133. }
  134. /**
  135. * Asks Twitter for a request token, and then redirects to Twitter
  136. * to authorize it.
  137. *
  138. * @return nothing
  139. */
  140. function authorizeRequestToken()
  141. {
  142. try {
  143. // Get a new request token and authorize it
  144. $client = new TwitterOAuthClient();
  145. $req_tok = $client->getRequestToken();
  146. // Sock the request token away in the session temporarily
  147. $_SESSION['twitter_request_token'] = $req_tok->key;
  148. $_SESSION['twitter_request_token_secret'] = $req_tok->secret;
  149. $auth_link = $client->getAuthorizeLink($req_tok, $this->signin);
  150. } catch (OAuthClientException $e) {
  151. $msg = sprintf(
  152. 'OAuth client error - code: %1s, msg: %2s',
  153. $e->getCode(),
  154. $e->getMessage()
  155. );
  156. common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
  157. $this->serverError(
  158. // TRANS: Server error displayed when linking to a Twitter account fails.
  159. _m('Could not link your Twitter account.')
  160. );
  161. }
  162. common_redirect($auth_link);
  163. }
  164. /**
  165. * Called when Twitter returns an authorized request token. Exchanges
  166. * it for an access token and stores it.
  167. *
  168. * @return nothing
  169. */
  170. function saveAccessToken()
  171. {
  172. // Check to make sure Twitter returned the same request
  173. // token we sent them
  174. if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
  175. $this->serverError(
  176. // TRANS: Server error displayed when linking to a Twitter account fails because of an incorrect oauth_token.
  177. _m('Could not link your Twitter account: oauth_token mismatch.')
  178. );
  179. }
  180. $twitter_user = null;
  181. try {
  182. $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
  183. $_SESSION['twitter_request_token_secret']);
  184. // Exchange the request token for an access token
  185. $atok = $client->getAccessToken($this->verifier);
  186. // Test the access token and get the user's Twitter info
  187. $client = new TwitterOAuthClient($atok->key, $atok->secret);
  188. $twitter_user = $client->verifyCredentials();
  189. } catch (OAuthClientException $e) {
  190. $msg = sprintf(
  191. 'OAuth client error - code: %1$s, msg: %2$s',
  192. $e->getCode(),
  193. $e->getMessage()
  194. );
  195. common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
  196. $this->serverError(
  197. // TRANS: Server error displayed when linking to a Twitter account fails.
  198. _m('Could not link your Twitter account.')
  199. );
  200. }
  201. if (common_logged_in()) {
  202. // Save the access token and Twitter user info
  203. $user = common_current_user();
  204. $this->saveForeignLink($user->id, $twitter_user->id, $atok);
  205. save_twitter_user($twitter_user->id, $twitter_user->screen_name);
  206. } else {
  207. $this->twuid = $twitter_user->id;
  208. $this->tw_fields = array("screen_name" => $twitter_user->screen_name,
  209. "fullname" => $twitter_user->name);
  210. $this->access_token = $atok;
  211. $this->tryLogin();
  212. }
  213. // Clean up the the mess we made in the session
  214. unset($_SESSION['twitter_request_token']);
  215. unset($_SESSION['twitter_request_token_secret']);
  216. if (common_logged_in()) {
  217. common_redirect(common_local_url('twittersettings'));
  218. }
  219. }
  220. /**
  221. * Saves a Foreign_link between Twitter user and local user,
  222. * which includes the access token and secret.
  223. *
  224. * @param int $user_id StatusNet user ID
  225. * @param int $twuid Twitter user ID
  226. * @param OAuthToken $token the access token to save
  227. *
  228. * @return nothing
  229. */
  230. function saveForeignLink($user_id, $twuid, $access_token)
  231. {
  232. $flink = new Foreign_link();
  233. $flink->user_id = $user_id;
  234. $flink->service = TWITTER_SERVICE;
  235. // delete stale flink, if any
  236. $result = $flink->find(true);
  237. if (!empty($result)) {
  238. $flink->safeDelete();
  239. }
  240. $flink->user_id = $user_id;
  241. $flink->foreign_id = $twuid;
  242. $flink->service = TWITTER_SERVICE;
  243. $creds = TwitterOAuthClient::packToken($access_token);
  244. $flink->credentials = $creds;
  245. $flink->created = common_sql_now();
  246. // Defaults: noticesync on, everything else off
  247. $flink->set_flags(true, false, false, false);
  248. $flink_id = $flink->insert();
  249. if (empty($flink_id)) {
  250. common_log_db_error($flink, 'INSERT', __FILE__);
  251. // TRANS: Server error displayed when linking to a Twitter account fails.
  252. $this->serverError(_m('Could not link your Twitter account.'));
  253. }
  254. return $flink_id;
  255. }
  256. function showPageNotice()
  257. {
  258. if ($this->error) {
  259. $this->element('div', array('class' => 'error'), $this->error);
  260. } else {
  261. $this->element('div', 'instructions',
  262. // TRANS: Page instruction. %s is the StatusNet sitename.
  263. sprintf(_m('This is the first time you have 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')));
  264. }
  265. }
  266. function title()
  267. {
  268. // TRANS: Page title.
  269. return _m('Twitter Account Setup');
  270. }
  271. function showForm($error=null, $username=null)
  272. {
  273. $this->error = $error;
  274. $this->username = $username;
  275. $this->showPage();
  276. }
  277. function showPage()
  278. {
  279. parent::showPage();
  280. }
  281. /**
  282. * @fixme much of this duplicates core code, which is very fragile.
  283. * Should probably be replaced with an extensible mini version of
  284. * the core registration form.
  285. */
  286. function showContent()
  287. {
  288. if (!empty($this->message_text)) {
  289. $this->element('p', null, $this->message);
  290. return;
  291. }
  292. $this->elementStart('form', array('method' => 'post',
  293. 'id' => 'form_settings_twitter_connect',
  294. 'class' => 'form_settings',
  295. 'action' => common_local_url('twitterauthorization')));
  296. $this->elementStart('fieldset', array('id' => 'settings_twitter_connect_options'));
  297. // TRANS: Fieldset legend.
  298. $this->element('legend', null, _m('Connection options'));
  299. $this->hidden('access_token_key', $this->access_token->key);
  300. $this->hidden('access_token_secret', $this->access_token->secret);
  301. $this->hidden('twuid', $this->twuid);
  302. $this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
  303. $this->hidden('tw_fields_name', $this->tw_fields['fullname']);
  304. $this->hidden('token', common_session_token());
  305. // Don't allow new account creation if site is flagged as invite only
  306. if (common_config('site', 'inviteonly') == false) {
  307. $this->elementStart('fieldset');
  308. $this->element('legend', null,
  309. // TRANS: Fieldset legend.
  310. _m('Create new account'));
  311. $this->element('p', null,
  312. // TRANS: Sub form introduction text.
  313. _m('Create a new user with this nickname.'));
  314. $this->elementStart('ul', 'form_data');
  315. // Hook point for captcha etc
  316. Event::handle('StartRegistrationFormData', array($this));
  317. $this->elementStart('li');
  318. // TRANS: Field label.
  319. $this->input('newname', _m('New nickname'),
  320. ($this->username) ? $this->username : '',
  321. // TRANS: Field title for nickname field.
  322. _m('1-64 lowercase letters or numbers, no punctuation or spaces.'));
  323. $this->elementEnd('li');
  324. $this->elementStart('li');
  325. // TRANS: Field label.
  326. $this->input('email', _m('LABEL','Email'), $this->getEmail(),
  327. // TRANS: Field title for e-mail address field.
  328. _m('Used only for updates, announcements, '.
  329. 'and password recovery'));
  330. $this->elementEnd('li');
  331. // Hook point for captcha etc
  332. Event::handle('EndRegistrationFormData', array($this));
  333. $this->elementEnd('ul');
  334. // TRANS: Button text for creating a new StatusNet account in the Twitter connect page.
  335. $this->submit('create', _m('BUTTON','Create'));
  336. $this->elementEnd('fieldset');
  337. }
  338. $this->elementStart('fieldset');
  339. $this->element('legend', null,
  340. // TRANS: Fieldset legend.
  341. _m('Connect existing account'));
  342. $this->element('p', null,
  343. // TRANS: Sub form introduction text.
  344. _m('If you already have an account, login with your username and password to connect it to your Twitter account.'));
  345. $this->elementStart('ul', 'form_data');
  346. $this->elementStart('li');
  347. // TRANS: Field label.
  348. $this->input('nickname', _m('Existing nickname'));
  349. $this->elementEnd('li');
  350. $this->elementStart('li');
  351. // TRANS: Field label.
  352. $this->password('password', _m('Password'));
  353. $this->elementEnd('li');
  354. $this->elementEnd('ul');
  355. $this->elementEnd('fieldset');
  356. $this->elementStart('fieldset');
  357. $this->element('legend', null,
  358. // TRANS: Fieldset legend.
  359. _m('License'));
  360. $this->elementStart('ul', 'form_data');
  361. $this->elementStart('li');
  362. $this->element('input', array('type' => 'checkbox',
  363. 'id' => 'license',
  364. 'class' => 'checkbox',
  365. 'name' => 'license',
  366. 'value' => 'true'));
  367. $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
  368. // TRANS: Text for license agreement checkbox.
  369. // TRANS: %s is the license as configured for the StatusNet site.
  370. $message = _m('My text and files are available under %s ' .
  371. 'except this private data: password, ' .
  372. 'email address, IM address, and phone number.');
  373. $link = '<a href="' .
  374. htmlspecialchars(common_config('license', 'url')) .
  375. '">' .
  376. htmlspecialchars(common_config('license', 'title')) .
  377. '</a>';
  378. $this->raw(sprintf(htmlspecialchars($message), $link));
  379. $this->elementEnd('label');
  380. $this->elementEnd('li');
  381. $this->elementEnd('ul');
  382. $this->elementEnd('fieldset');
  383. // TRANS: Button text for connecting an existing StatusNet account in the Twitter connect page..
  384. $this->submit('connect', _m('BUTTON','Connect'));
  385. $this->elementEnd('fieldset');
  386. $this->elementEnd('form');
  387. }
  388. /**
  389. * Get specified e-mail from the form, or the invite code.
  390. *
  391. * @return string
  392. */
  393. function getEmail()
  394. {
  395. $email = $this->trimmed('email');
  396. if (!empty($email)) {
  397. return $email;
  398. }
  399. // Terrible hack for invites...
  400. if (common_config('site', 'inviteonly')) {
  401. $code = $_SESSION['invitecode'];
  402. if ($code) {
  403. $invite = Invitation::getKV($code);
  404. if ($invite && $invite->address_type == 'email') {
  405. return $invite->address;
  406. }
  407. }
  408. }
  409. return '';
  410. }
  411. function message($msg)
  412. {
  413. $this->message_text = $msg;
  414. $this->showPage();
  415. }
  416. function createNewUser()
  417. {
  418. if (!Event::handle('StartRegistrationTry', array($this))) {
  419. return;
  420. }
  421. if (common_config('site', 'closed')) {
  422. // TRANS: Client error displayed when trying to create a new user while creating new users is not allowed.
  423. $this->clientError(_m('Registration not allowed.'));
  424. }
  425. $invite = null;
  426. if (common_config('site', 'inviteonly')) {
  427. $code = $_SESSION['invitecode'];
  428. if (empty($code)) {
  429. // TRANS: Client error displayed when trying to create a new user while creating new users is not allowed.
  430. $this->clientError(_m('Registration not allowed.'));
  431. }
  432. $invite = Invitation::getKV($code);
  433. if (empty($invite)) {
  434. // TRANS: Client error displayed when trying to create a new user with an invalid invitation code.
  435. $this->clientError(_m('Not a valid invitation code.'));
  436. }
  437. }
  438. try {
  439. $nickname = Nickname::normalize($this->trimmed('newname'), true);
  440. } catch (NicknameException $e) {
  441. $this->showForm($e->getMessage());
  442. return;
  443. }
  444. $fullname = trim($this->tw_fields['fullname']);
  445. $args = array('nickname' => $nickname, 'fullname' => $fullname);
  446. if (!empty($invite)) {
  447. $args['code'] = $invite->code;
  448. }
  449. $email = $this->getEmail();
  450. if (!empty($email)) {
  451. $args['email'] = $email;
  452. }
  453. $user = User::register($args);
  454. if (empty($user)) {
  455. // TRANS: Server error displayed when creating a new user has failed.
  456. $this->serverError(_m('Error registering user.'));
  457. }
  458. $result = $this->saveForeignLink($user->id,
  459. $this->twuid,
  460. $this->access_token);
  461. save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
  462. if (!$result) {
  463. // TRANS: Server error displayed when connecting a user to a Twitter user has failed.
  464. $this->serverError(_m('Error connecting user to Twitter.'));
  465. }
  466. common_set_user($user);
  467. common_real_login(true);
  468. common_debug('TwitterBridge Plugin - ' .
  469. "Registered new user $user->id from Twitter user $this->twuid");
  470. Event::handle('EndRegistrationTry', array($this));
  471. common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)), 303);
  472. }
  473. function connectNewUser()
  474. {
  475. $nickname = $this->trimmed('nickname');
  476. $password = $this->trimmed('password');
  477. if (!common_check_user($nickname, $password)) {
  478. // TRANS: Form validation error displayed when connecting an existing user to a Twitter user fails because
  479. // TRANS: the provided username and/or password are incorrect.
  480. $this->showForm(_m('Invalid username or password.'));
  481. return;
  482. }
  483. $user = User::getKV('nickname', $nickname);
  484. if (!empty($user)) {
  485. common_debug('TwitterBridge Plugin - ' .
  486. "Legit user to connect to Twitter: $nickname");
  487. }
  488. $result = $this->saveForeignLink($user->id,
  489. $this->twuid,
  490. $this->access_token);
  491. save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
  492. if (!$result) {
  493. // TRANS: Server error displayed connecting a user to a Twitter user has failed.
  494. $this->serverError(_m('Error connecting user to Twitter.'));
  495. }
  496. common_debug('TwitterBridge Plugin - ' .
  497. "Connected Twitter user $this->twuid to local user $user->id");
  498. common_set_user($user);
  499. common_real_login(true);
  500. $this->goHome($user->nickname);
  501. }
  502. function connectUser()
  503. {
  504. $user = common_current_user();
  505. $result = $this->flinkUser($user->id, $this->twuid);
  506. if (empty($result)) {
  507. // TRANS: Server error displayed connecting a user to a Twitter user has failed.
  508. $this->serverError(_m('Error connecting user to Twitter.'));
  509. }
  510. common_debug('TwitterBridge Plugin - ' .
  511. "Connected Twitter user $this->twuid to local user $user->id");
  512. // Return to Twitter connection settings tab
  513. common_redirect(common_local_url('twittersettings'), 303);
  514. }
  515. function tryLogin()
  516. {
  517. common_debug('TwitterBridge Plugin - ' .
  518. "Trying login for Twitter user $this->twuid.");
  519. $flink = Foreign_link::getByForeignID($this->twuid,
  520. TWITTER_SERVICE);
  521. if (!empty($flink)) {
  522. $user = $flink->getUser();
  523. if (!empty($user)) {
  524. common_debug('TwitterBridge Plugin - ' .
  525. "Logged in Twitter user $flink->foreign_id as user $user->id ($user->nickname)");
  526. common_set_user($user);
  527. common_real_login(true);
  528. $this->goHome($user->nickname);
  529. }
  530. } else {
  531. common_debug('TwitterBridge Plugin - ' .
  532. "No flink found for twuid: $this->twuid - new user");
  533. $this->showForm(null, $this->bestNewNickname());
  534. }
  535. }
  536. function goHome($nickname)
  537. {
  538. $url = common_get_returnto();
  539. if ($url) {
  540. // We don't have to return to it again
  541. common_set_returnto(null);
  542. } else {
  543. $url = common_local_url('all',
  544. array('nickname' =>
  545. $nickname));
  546. }
  547. common_redirect($url, 303);
  548. }
  549. function bestNewNickname()
  550. {
  551. try {
  552. return Nickname::normalize($this->tw_fields['fullname'], true);
  553. } catch (NicknameException $e) {
  554. return null;
  555. }
  556. }
  557. }