twitterauthorization.php 23 KB

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