ostatussub.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <?php
  2. /*
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2009-2010, StatusNet, Inc.
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. /**
  20. * @package OStatusPlugin
  21. * @maintainer Brion Vibber <brion@status.net>
  22. */
  23. if (!defined('GNUSOCIAL') && !defined('STATUSNET')) { exit(1); }
  24. /**
  25. * Key UI methods:
  26. *
  27. * showInputForm() - form asking for a remote profile account or URL
  28. * We end up back here on errors
  29. *
  30. * showPreviewForm() - surrounding form for preview-and-confirm
  31. * preview() - display profile for a remote user
  32. *
  33. * success() - redirects to subscriptions page on subscribe
  34. */
  35. class OStatusSubAction extends Action
  36. {
  37. protected $profile_uri; // provided acct: or URI of remote entity
  38. protected $oprofile; // Ostatus_profile of remote entity, if valid
  39. protected function prepare(array $args=array())
  40. {
  41. parent::prepare($args);
  42. if (!common_logged_in()) {
  43. // XXX: selfURL() didn't work. :<
  44. common_set_returnto($_SERVER['REQUEST_URI']);
  45. if (Event::handle('RedirectToLogin', array($this, null))) {
  46. common_redirect(common_local_url('login'), 303);
  47. }
  48. return false;
  49. }
  50. if ($this->pullRemoteProfile()) {
  51. $this->validateRemoteProfile();
  52. }
  53. return true;
  54. }
  55. /**
  56. * Handle the submission.
  57. */
  58. protected function handle()
  59. {
  60. parent::handle();
  61. if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  62. $this->handlePost();
  63. } else {
  64. $this->showForm();
  65. }
  66. }
  67. /**
  68. * Show the initial form, when we haven't yet been given a valid
  69. * remote profile.
  70. */
  71. function showInputForm()
  72. {
  73. $this->elementStart('form', array('method' => 'post',
  74. 'id' => 'form_ostatus_sub',
  75. 'class' => 'form_settings',
  76. 'action' => $this->selfLink()));
  77. $this->hidden('token', common_session_token());
  78. $this->elementStart('fieldset', array('id' => 'settings_feeds'));
  79. $this->elementStart('ul', 'form_data');
  80. $this->elementStart('li');
  81. $this->input('profile',
  82. // TRANS: Field label for a field that takes an OStatus user address.
  83. _m('Subscribe to'),
  84. $this->profile_uri,
  85. // TRANS: Tooltip for field label "Subscribe to".
  86. _m('OStatus user\'s address, like nickname@example.com or http://example.net/nickname.'));
  87. $this->elementEnd('li');
  88. $this->elementEnd('ul');
  89. // TRANS: Button text.
  90. $this->submit('validate', _m('BUTTON','Continue'));
  91. $this->elementEnd('fieldset');
  92. $this->elementEnd('form');
  93. }
  94. /**
  95. * Show the preview-and-confirm form. We've got a valid remote
  96. * profile and are ready to poke it!
  97. *
  98. * This controls the wrapper form; actual profile display will
  99. * be in previewUser() or previewGroup() depending on the type.
  100. */
  101. function showPreviewForm()
  102. {
  103. $ok = $this->preview();
  104. if (!$ok) {
  105. // @todo FIXME maybe provide a cancel button or link back?
  106. return;
  107. }
  108. $this->elementStart('div', 'entity_actions');
  109. $this->elementStart('ul');
  110. $this->elementStart('li', 'entity_subscribe');
  111. $this->elementStart('form', array('method' => 'post',
  112. 'id' => 'form_ostatus_sub',
  113. 'class' => 'form_remote_authorize',
  114. 'action' =>
  115. $this->selfLink()));
  116. $this->elementStart('fieldset');
  117. $this->hidden('token', common_session_token());
  118. $this->hidden('profile', $this->profile_uri);
  119. if ($this->oprofile->isGroup()) {
  120. // TRANS: Button text.
  121. $this->submit('submit', _m('Join'), 'submit', null,
  122. // TRANS: Tooltip for button "Join".
  123. _m('BUTTON','Join this group'));
  124. } else {
  125. // TRANS: Button text.
  126. $this->submit('submit', _m('BUTTON','Confirm'), 'submit', null,
  127. // TRANS: Tooltip for button "Confirm".
  128. _m('Subscribe to this user'));
  129. }
  130. $this->elementEnd('fieldset');
  131. $this->elementEnd('form');
  132. $this->elementEnd('li');
  133. $this->elementEnd('ul');
  134. $this->elementEnd('div');
  135. }
  136. /**
  137. * Show a preview for a remote user's profile
  138. * @return boolean true if we're ok to try subscribing
  139. */
  140. function preview()
  141. {
  142. // Throws NoProfileException on localProfile when remote user's Profile not found
  143. $profile = $this->oprofile->localProfile();
  144. if ($this->scoped->isSubscribed($profile)) {
  145. $this->element('div', array('class' => 'error'),
  146. // TRANS: Extra paragraph in remote profile view when already subscribed.
  147. _m('You are already subscribed to this user.'));
  148. $ok = false;
  149. } else {
  150. $ok = true;
  151. }
  152. $avatarUrl = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
  153. $this->showEntity($profile,
  154. $profile->profileurl,
  155. $avatarUrl,
  156. $profile->bio);
  157. return $ok;
  158. }
  159. function showEntity($entity, $profile, $avatar, $note)
  160. {
  161. $nickname = $entity->nickname;
  162. $fullname = $entity->fullname;
  163. $homepage = $entity->homepage;
  164. $location = $entity->location;
  165. $this->elementStart('div', 'entity_profile vcard');
  166. $this->element('img', array('src' => $avatar,
  167. 'class' => 'photo avatar entity_depiction',
  168. 'width' => AVATAR_PROFILE_SIZE,
  169. 'height' => AVATAR_PROFILE_SIZE,
  170. 'alt' => $nickname));
  171. $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname entity_nickname';
  172. $this->elementStart('a', array('href' => $profile,
  173. 'class' => 'url '.$hasFN));
  174. $this->text($nickname);
  175. $this->elementEnd('a');
  176. if (!is_null($fullname)) {
  177. $this->elementStart('div', 'fn entity_fn');
  178. $this->text($fullname);
  179. $this->elementEnd('div');
  180. }
  181. if (!is_null($location)) {
  182. $this->elementStart('div', 'label entity_location');
  183. $this->text($location);
  184. $this->elementEnd('div');
  185. }
  186. if (!is_null($homepage)) {
  187. $this->elementStart('a', array('href' => $homepage,
  188. 'class' => 'url entity_url'));
  189. $this->text($homepage);
  190. $this->elementEnd('a');
  191. }
  192. if (!is_null($note)) {
  193. $this->elementStart('div', 'note entity_note');
  194. $this->text($note);
  195. $this->elementEnd('div');
  196. }
  197. $this->elementEnd('div');
  198. }
  199. /**
  200. * Redirect on successful remote user subscription
  201. */
  202. function success()
  203. {
  204. $url = common_local_url('subscriptions', array('nickname' => $this->scoped->nickname));
  205. common_redirect($url, 303);
  206. }
  207. /**
  208. * Pull data for a remote profile and check if it's valid.
  209. * Fills out error UI string in $this->error
  210. * Fills out $this->oprofile on success.
  211. *
  212. * @return boolean
  213. */
  214. function pullRemoteProfile()
  215. {
  216. $validate = new Validate();
  217. try {
  218. $this->profile_uri = Discovery::normalize($this->trimmed('profile'));
  219. } catch (Exception $e) {
  220. return false;
  221. }
  222. try {
  223. if (Discovery::isAcct($this->profile_uri) && $validate->email(mb_substr($this->profile_uri, 5))) {
  224. $this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri);
  225. } else if ($validate->uri($this->profile_uri)) {
  226. $this->oprofile = Ostatus_profile::ensureProfileURL($this->profile_uri);
  227. } else {
  228. // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
  229. // TRANS: and example.net, as these are official standard domain names for use in examples.
  230. $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
  231. common_debug('Invalid address format.', __FILE__);
  232. return false;
  233. }
  234. return true;
  235. } catch (FeedSubBadURLException $e) {
  236. // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
  237. // TRANS: and example.net, as these are official standard domain names for use in examples.
  238. $this->error = _m('Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.');
  239. common_debug('Invalid URL or could not reach server.', __FILE__);
  240. } catch (FeedSubBadResponseException $e) {
  241. // TRANS: Error text.
  242. $this->error = _m('Sorry, we could not reach that feed. Please try that OStatus address again later.');
  243. common_debug('Cannot read feed; server returned error.', __FILE__);
  244. } catch (FeedSubEmptyException $e) {
  245. // TRANS: Error text.
  246. $this->error = _m('Sorry, we could not reach that feed. Please try that OStatus address again later.');
  247. common_debug('Cannot read feed; server returned an empty page.', __FILE__);
  248. } catch (FeedSubBadHTMLException $e) {
  249. // TRANS: Error text.
  250. $this->error = _m('Sorry, we could not reach that feed. Please try that OStatus address again later.');
  251. common_debug('Bad HTML, could not find feed link.', __FILE__);
  252. } catch (FeedSubNoFeedException $e) {
  253. // TRANS: Error text.
  254. $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later.");
  255. common_debug('Could not find a feed linked from this URL.', __FILE__);
  256. } catch (FeedSubUnrecognizedTypeException $e) {
  257. // TRANS: Error text.
  258. $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later.");
  259. common_debug('Not a recognized feed type.', __FILE__);
  260. } catch (FeedSubNoHubException $e) {
  261. // TRANS: Error text.
  262. $this->error = _m("Sorry, that feed is not Pubsubhubub enabled.");
  263. common_debug('No hub found.', __FILE__);
  264. } catch (Exception $e) {
  265. // Any new ones we forgot about
  266. // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
  267. // TRANS: and example.net, as these are official standard domain names for use in examples.
  268. $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
  269. common_debug(sprintf('Bad feed URL: %s %s', get_class($e), $e->getMessage()), __FILE__);
  270. }
  271. return false;
  272. }
  273. function validateRemoteProfile()
  274. {
  275. // Send us to the respective subscription form for conf
  276. if ($this->oprofile->isGroup()) {
  277. $target = common_local_url('ostatusgroup', array(), array('profile' => $this->profile_uri));
  278. common_redirect($target, 303);
  279. } else if ($this->oprofile->isPeopletag()) {
  280. $target = common_local_url('ostatuspeopletag', array(), array('profile' => $this->profile_uri));
  281. common_redirect($target, 303);
  282. }
  283. }
  284. /**
  285. * Attempt to finalize subscription.
  286. * validateFeed must have been run first.
  287. *
  288. * Calls showForm on failure or success on success.
  289. */
  290. function saveFeed()
  291. {
  292. // And subscribe the current user to the local profile
  293. $local = $this->oprofile->localProfile();
  294. if ($this->scoped->isSubscribed($local)) {
  295. // TRANS: OStatus remote subscription dialog error.
  296. $this->showForm(_m('Already subscribed!'));
  297. } elseif (Subscription::start($this->scoped, $local)) {
  298. $this->success();
  299. } else {
  300. // TRANS: OStatus remote subscription dialog error.
  301. $this->showForm(_m('Remote subscription failed!'));
  302. }
  303. }
  304. /**
  305. * Handle posts to this form
  306. *
  307. * @return void
  308. */
  309. function handlePost()
  310. {
  311. // CSRF protection
  312. $token = $this->trimmed('token');
  313. if (!$token || $token != common_session_token()) {
  314. // TRANS: Client error displayed when the session token does not match or is not given.
  315. $this->showForm(_m('There was a problem with your session token. '.
  316. 'Try again, please.'));
  317. return;
  318. }
  319. if ($this->oprofile) {
  320. if ($this->arg('submit')) {
  321. $this->saveFeed();
  322. return;
  323. }
  324. }
  325. $this->showForm();
  326. }
  327. /**
  328. * Show the appropriate form based on our input state.
  329. */
  330. function showForm($err=null)
  331. {
  332. if ($err) {
  333. $this->error = $err;
  334. }
  335. if ($this->boolean('ajax')) {
  336. $this->startHTML('text/xml;charset=utf-8');
  337. $this->elementStart('head');
  338. // TRANS: Form title.
  339. $this->element('title', null, _m('Subscribe to user'));
  340. $this->elementEnd('head');
  341. $this->elementStart('body');
  342. $this->showContent();
  343. $this->elementEnd('body');
  344. $this->endHTML();
  345. } else {
  346. $this->showPage();
  347. }
  348. }
  349. /**
  350. * Title of the page
  351. *
  352. * @return string Title of the page
  353. */
  354. function title()
  355. {
  356. // TRANS: Page title for OStatus remote subscription form.
  357. return !empty($this->profile_uri) ? _m('Confirm') : _m('Remote subscription');
  358. }
  359. /**
  360. * Instructions for use
  361. *
  362. * @return instructions for use
  363. */
  364. function getInstructions()
  365. {
  366. // TRANS: Instructions.
  367. return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:');
  368. }
  369. function showPageNotice()
  370. {
  371. if (!empty($this->error)) {
  372. $this->element('p', 'error', $this->error);
  373. }
  374. }
  375. /**
  376. * Content area of the page
  377. *
  378. * Shows a form for associating a remote OStatus account with this
  379. * StatusNet account.
  380. *
  381. * @return void
  382. */
  383. function showContent()
  384. {
  385. if ($this->oprofile) {
  386. $this->showPreviewForm();
  387. } else {
  388. $this->showInputForm();
  389. }
  390. }
  391. function showScripts()
  392. {
  393. parent::showScripts();
  394. $this->autofocus('feedurl');
  395. }
  396. function selfLink()
  397. {
  398. return common_local_url('ostatussub');
  399. }
  400. /**
  401. * Disable the send-notice form at the top of the page.
  402. * This is really just a hack for the broken CSS in the Cloudy theme,
  403. * I think; copying from other non-notice-navigation pages that do this
  404. * as well. There will be plenty of others also broken.
  405. *
  406. * @fixme fix the cloudy theme
  407. * @fixme do this in a more general way
  408. */
  409. function showNoticeForm() {
  410. // nop
  411. }
  412. }