twitterauthorization.php 23 KB

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