remotefollowsub.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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. * Remote Follow implementation for GNU social
  18. *
  19. * @package GNUsocial
  20. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  21. * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
  22. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  23. */
  24. defined('GNUSOCIAL') || die();
  25. /**
  26. * Remote-follow follow action
  27. *
  28. * @category Plugin
  29. * @package GNUsocial
  30. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  31. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  32. */
  33. class RemoteFollowSubAction extends Action
  34. {
  35. protected $uri; // acct: or uri of remote entity
  36. protected $profile; // profile of remote entity, if valid
  37. protected function prepare(array $args = [])
  38. {
  39. parent::prepare($args);
  40. if (!common_logged_in()) {
  41. common_set_returnto($_SERVER['REQUEST_URI']);
  42. if (Event::handle('RedirectToLogin', [$this, null])) {
  43. common_redirect(common_local_url('login'), 303);
  44. }
  45. return false;
  46. }
  47. if (!$this->profile && $this->arg('profile')) {
  48. $this->uri = $this->trimmed('profile');
  49. $profile = null;
  50. if (!Event::handle('RemoteFollowPullProfile', [$this->uri, &$profile]) && !is_null($profile)) {
  51. $this->profile = $profile;
  52. } else {
  53. // TRANS: Error displayed when there's failure in fetching the remote profile.
  54. $this->error = _m('Sorry, we could not reach that address. ' .
  55. 'Please make sure it is a valid address and try again later.');
  56. }
  57. }
  58. return true;
  59. }
  60. /**
  61. * Handles the submission.
  62. *
  63. * @return void
  64. */
  65. protected function handle(): void
  66. {
  67. parent::handle();
  68. if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  69. $this->handlePost();
  70. } else {
  71. $this->showForm();
  72. }
  73. }
  74. /**
  75. * Show the initial form, when we haven't yet been given a valid
  76. * remote profile.
  77. *
  78. * @return void
  79. */
  80. public function showInputForm(): void
  81. {
  82. $this->elementStart('form', ['method' => 'post',
  83. 'id' => 'form_ostatus_sub',
  84. 'class' => 'form_settings',
  85. 'action' => $this->selfLink()]);
  86. $this->hidden('token', common_session_token());
  87. $this->elementStart('fieldset', ['id' => 'settings_feeds']);
  88. $this->elementStart('ul', 'form_data');
  89. $this->elementStart('li');
  90. $this->input(
  91. 'profile',
  92. // TRANS: Field label for a field that takes an user address.
  93. _m('Subscribe to'),
  94. $this->uri,
  95. // TRANS: Tooltip for field label "Subscribe to".
  96. _m('User\'s address, like nickname@example.com or http://example.net/nickname.')
  97. );
  98. $this->elementEnd('li');
  99. $this->elementEnd('ul');
  100. // TRANS: Button text.
  101. $this->submit('validate', _m('BUTTON', 'Continue'));
  102. $this->elementEnd('fieldset');
  103. $this->elementEnd('form');
  104. }
  105. /**
  106. * Show the preview-and-confirm form. We've got a valid remote
  107. * profile and are ready to poke it!
  108. *
  109. * @return void
  110. */
  111. public function showPreviewForm(): void
  112. {
  113. if (!$this->preview()) {
  114. return;
  115. }
  116. $this->elementStart('div', 'entity_actions');
  117. $this->elementStart('ul');
  118. $this->elementStart('li', 'entity_subscribe');
  119. $this->elementStart('form', ['method' => 'post',
  120. 'id' => 'form_ostatus_sub',
  121. 'class' => 'form_remote_authorize',
  122. 'action' => $this->selfLink()]);
  123. $this->elementStart('fieldset');
  124. $this->hidden('token', common_session_token());
  125. $this->hidden('profile', $this->uri);
  126. $this->submit(
  127. 'submit',
  128. // TRANS: Button text.
  129. _m('BUTTON', 'Confirm'),
  130. 'submit',
  131. null,
  132. // TRANS: Tooltip for button "Confirm".
  133. _m('Subscribe to this user')
  134. );
  135. $this->elementEnd('fieldset');
  136. $this->elementEnd('form');
  137. $this->elementEnd('li');
  138. $this->elementEnd('ul');
  139. $this->elementEnd('div');
  140. }
  141. /**
  142. * Show a preview for a remote user's profile.
  143. *
  144. * @return bool true if we're ok to try subscribing, false otherwise
  145. */
  146. public function preview(): bool
  147. {
  148. if ($this->scoped->isSubscribed($this->profile)) {
  149. $this->element(
  150. 'div',
  151. ['class' => 'error'],
  152. // TRANS: Extra paragraph in remote profile view when already subscribed.
  153. _m('You are already subscribed to this user.')
  154. );
  155. $ok = false;
  156. } else {
  157. $ok = true;
  158. }
  159. $avatarUrl = $this->profile->avatarUrl(AVATAR_PROFILE_SIZE);
  160. $this->showEntity(
  161. $this->profile,
  162. $this->profile->getUrl(),
  163. $avatarUrl,
  164. $this->profile->getDescription()
  165. );
  166. return $ok;
  167. }
  168. /**
  169. * Show someone's profile.
  170. *
  171. * @return void
  172. */
  173. public function showEntity(Profile $entity, string $profile_url, string $avatar, ?string $note): void
  174. {
  175. $nickname = $entity->getNickname();
  176. $fullname = $entity->getFullname();
  177. $homepage = $entity->getHomepage();
  178. $location = $entity->getLocation();
  179. $this->elementStart('div', 'entity_profile vcard');
  180. $this->element('img', ['src' => $avatar,
  181. 'class' => 'photo avatar entity_depiction',
  182. 'width' => AVATAR_PROFILE_SIZE,
  183. 'height' => AVATAR_PROFILE_SIZE,
  184. 'alt' => $nickname]);
  185. $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname entity_nickname';
  186. $this->elementStart('a', ['href' => $profile_url,
  187. 'class' => 'url '.$hasFN]);
  188. $this->text($nickname);
  189. $this->elementEnd('a');
  190. if (!is_null($fullname)) {
  191. $this->elementStart('div', 'fn entity_fn');
  192. $this->text($fullname);
  193. $this->elementEnd('div');
  194. }
  195. $location_name = (is_null($location) ? null : $location->getName());
  196. if (!is_null($location_name)) {
  197. $this->elementStart('div', 'label entity_location');
  198. $this->text($location_name);
  199. $this->elementEnd('div');
  200. }
  201. if (!is_null($homepage)) {
  202. $this->elementStart('a', ['href' => $homepage,
  203. 'class' => 'url entity_url']);
  204. $this->text($homepage);
  205. $this->elementEnd('a');
  206. }
  207. if (!is_null($note)) {
  208. $this->elementStart('div', 'note entity_note');
  209. $this->text($note);
  210. $this->elementEnd('div');
  211. }
  212. $this->elementEnd('div');
  213. }
  214. /**
  215. * Redirect on successful remote follow
  216. *
  217. * @return void
  218. */
  219. public function success(): void
  220. {
  221. $url = common_local_url('subscriptions', ['nickname' => $this->scoped->getNickname()]);
  222. common_redirect($url, 303);
  223. }
  224. /**
  225. * Attempt to finalize subscription.
  226. *
  227. * @return void
  228. */
  229. public function follow(): void
  230. {
  231. if ($this->scoped->isSubscribed($this->profile)) {
  232. // TRANS: Remote subscription dialog error.
  233. $this->showForm(_m('Already subscribed!'));
  234. } elseif (Subscription::start($this->scoped, $this->profile)) {
  235. $this->success();
  236. } else {
  237. // TRANS: Remote subscription dialog error.
  238. $this->showForm(_m('Remote subscription failed!'));
  239. }
  240. }
  241. /**
  242. * Handle posts to this form
  243. *
  244. * @return void
  245. */
  246. public function handlePost(): void
  247. {
  248. // CSRF protection
  249. $token = $this->trimmed('token');
  250. if (!$token || $token != common_session_token()) {
  251. // TRANS: Client error displayed when the session token does not match or is not given.
  252. $this->showForm(_m('There was a problem with your session token. '.
  253. 'Try again, please.'));
  254. return;
  255. }
  256. if ($this->profile && $this->arg('submit')) {
  257. $this->follow();
  258. return;
  259. }
  260. $this->showForm();
  261. }
  262. /**
  263. * Show the appropriate form based on our input state.
  264. *
  265. * @return void
  266. */
  267. public function showForm(?string $err = null): void
  268. {
  269. if ($err) {
  270. $this->error = $err;
  271. }
  272. if ($this->boolean('ajax')) {
  273. $this->startHTML('text/xml;charset=utf-8');
  274. $this->elementStart('head');
  275. // TRANS: Form title.
  276. $this->element('title', null, _m('Subscribe to user'));
  277. $this->elementEnd('head');
  278. $this->elementStart('body');
  279. $this->showContent();
  280. $this->elementEnd('body');
  281. $this->endHTML();
  282. } else {
  283. $this->showPage();
  284. }
  285. }
  286. /**
  287. * Title of the page
  288. *
  289. * @return string title of the page
  290. */
  291. public function title(): string
  292. {
  293. // TRANS: Page title for remote subscription form.
  294. return !empty($this->uri) ? _m('Confirm') : _m('Remote subscription');
  295. }
  296. /**
  297. * Instructions for use
  298. *
  299. * @return string instructions for use
  300. */
  301. public function getInstructions(): string
  302. {
  303. // TRANS: Instructions.
  304. return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:');
  305. }
  306. /**
  307. * Show page notice.
  308. *
  309. * @return void
  310. */
  311. public function showPageNotice(): void
  312. {
  313. if (!empty($this->error)) {
  314. $this->element('p', 'error', $this->error);
  315. }
  316. }
  317. /**
  318. * Content area of the page
  319. *
  320. * @return void
  321. */
  322. public function showContent(): void
  323. {
  324. if ($this->profile) {
  325. $this->showPreviewForm();
  326. } else {
  327. $this->showInputForm();
  328. }
  329. }
  330. /**
  331. * Show javascript headers
  332. *
  333. * @return void
  334. */
  335. public function showScripts(): void
  336. {
  337. parent::showScripts();
  338. $this->autofocus('profile');
  339. }
  340. /**
  341. * Return url for this action
  342. *
  343. * @return string
  344. */
  345. public function selfLink(): string
  346. {
  347. return common_local_url('RemoteFollowSub');
  348. }
  349. /**
  350. * Disable the send-notice form at the top of the page.
  351. * This is really just a hack for the broken CSS in the Cloudy theme,
  352. * I think; copying from other non-notice-navigation pages that do this
  353. * as well. There will be plenty of others also broken.
  354. *
  355. * @fixme fix the cloudy theme
  356. * @fixme do this in a more general way
  357. */
  358. public function showNoticeForm(): void
  359. {
  360. // nop
  361. }
  362. }