remotefollowsub.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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('profile',
  91. // TRANS: Field label for a field that takes an user address.
  92. _m('Subscribe to'),
  93. $this->uri,
  94. // TRANS: Tooltip for field label "Subscribe to".
  95. _m('User\'s address, like nickname@example.com or http://example.net/nickname.'));
  96. $this->elementEnd('li');
  97. $this->elementEnd('ul');
  98. // TRANS: Button text.
  99. $this->submit('validate', _m('BUTTON','Continue'));
  100. $this->elementEnd('fieldset');
  101. $this->elementEnd('form');
  102. }
  103. /**
  104. * Show the preview-and-confirm form. We've got a valid remote
  105. * profile and are ready to poke it!
  106. *
  107. * @return void
  108. */
  109. public function showPreviewForm(): void
  110. {
  111. if (!$this->preview()) {
  112. return;
  113. }
  114. $this->elementStart('div', 'entity_actions');
  115. $this->elementStart('ul');
  116. $this->elementStart('li', 'entity_subscribe');
  117. $this->elementStart('form', ['method' => 'post',
  118. 'id' => 'form_ostatus_sub',
  119. 'class' => 'form_remote_authorize',
  120. 'action' => $this->selfLink()]);
  121. $this->elementStart('fieldset');
  122. $this->hidden('token', common_session_token());
  123. $this->hidden('profile', $this->uri);
  124. $this->submit('submit',
  125. // TRANS: Button text.
  126. _m('BUTTON','Confirm'),
  127. 'submit',
  128. null,
  129. // TRANS: Tooltip for button "Confirm".
  130. _m('Subscribe to this user'));
  131. $this->elementEnd('fieldset');
  132. $this->elementEnd('form');
  133. $this->elementEnd('li');
  134. $this->elementEnd('ul');
  135. $this->elementEnd('div');
  136. }
  137. /**
  138. * Show a preview for a remote user's profile.
  139. *
  140. * @return bool true if we're ok to try subscribing, false otherwise
  141. */
  142. public function preview(): bool
  143. {
  144. if ($this->scoped->isSubscribed($this->profile)) {
  145. $this->element('div',
  146. ['class' => 'error'],
  147. // TRANS: Extra paragraph in remote profile view when already subscribed.
  148. _m('You are already subscribed to this user.'));
  149. $ok = false;
  150. } else {
  151. $ok = true;
  152. }
  153. $avatarUrl = $this->profile->avatarUrl(AVATAR_PROFILE_SIZE);
  154. $this->showEntity($this->profile,
  155. $this->profile->getUrl(),
  156. $avatarUrl,
  157. $this->profile->getDescription());
  158. return $ok;
  159. }
  160. /**
  161. * Show someone's profile.
  162. *
  163. * @return void
  164. */
  165. public function showEntity(Profile $entity, string $profile_url, string $avatar, ?string $note): void
  166. {
  167. $nickname = $entity->getNickname();
  168. $fullname = $entity->getFullname();
  169. $homepage = $entity->getHomepage();
  170. $location = $entity->getLocation();
  171. $this->elementStart('div', 'entity_profile vcard');
  172. $this->element('img', ['src' => $avatar,
  173. 'class' => 'photo avatar entity_depiction',
  174. 'width' => AVATAR_PROFILE_SIZE,
  175. 'height' => AVATAR_PROFILE_SIZE,
  176. 'alt' => $nickname]);
  177. $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname entity_nickname';
  178. $this->elementStart('a', ['href' => $profile_url,
  179. 'class' => 'url '.$hasFN]);
  180. $this->text($nickname);
  181. $this->elementEnd('a');
  182. if (!is_null($fullname)) {
  183. $this->elementStart('div', 'fn entity_fn');
  184. $this->text($fullname);
  185. $this->elementEnd('div');
  186. }
  187. if (!is_null($location)) {
  188. $this->elementStart('div', 'label entity_location');
  189. $this->text($location);
  190. $this->elementEnd('div');
  191. }
  192. if (!is_null($homepage)) {
  193. $this->elementStart('a', ['href' => $homepage,
  194. 'class' => 'url entity_url']);
  195. $this->text($homepage);
  196. $this->elementEnd('a');
  197. }
  198. if (!is_null($note)) {
  199. $this->elementStart('div', 'note entity_note');
  200. $this->text($note);
  201. $this->elementEnd('div');
  202. }
  203. $this->elementEnd('div');
  204. }
  205. /**
  206. * Redirect on successful remote follow
  207. *
  208. * @return void
  209. */
  210. public function success(): void
  211. {
  212. $url = common_local_url('subscriptions', ['nickname' => $this->scoped->getNickname()]);
  213. common_redirect($url, 303);
  214. }
  215. /**
  216. * Attempt to finalize subscription.
  217. *
  218. * @return void
  219. */
  220. public function follow(): void
  221. {
  222. if ($this->scoped->isSubscribed($this->profile)) {
  223. // TRANS: Remote subscription dialog error.
  224. $this->showForm(_m('Already subscribed!'));
  225. } elseif (Subscription::start($this->scoped, $this->profile)) {
  226. $this->success();
  227. } else {
  228. // TRANS: Remote subscription dialog error.
  229. $this->showForm(_m('Remote subscription failed!'));
  230. }
  231. }
  232. /**
  233. * Handle posts to this form
  234. *
  235. * @return void
  236. */
  237. public function handlePost(): void
  238. {
  239. // CSRF protection
  240. $token = $this->trimmed('token');
  241. if (!$token || $token != common_session_token()) {
  242. // TRANS: Client error displayed when the session token does not match or is not given.
  243. $this->showForm(_m('There was a problem with your session token. '.
  244. 'Try again, please.'));
  245. return;
  246. }
  247. if ($this->profile && $this->arg('submit')) {
  248. $this->follow();
  249. return;
  250. }
  251. $this->showForm();
  252. }
  253. /**
  254. * Show the appropriate form based on our input state.
  255. *
  256. * @return void
  257. */
  258. public function showForm(?string $err = null): void
  259. {
  260. if ($err) {
  261. $this->error = $err;
  262. }
  263. if ($this->boolean('ajax')) {
  264. $this->startHTML('text/xml;charset=utf-8');
  265. $this->elementStart('head');
  266. // TRANS: Form title.
  267. $this->element('title', null, _m('Subscribe to user'));
  268. $this->elementEnd('head');
  269. $this->elementStart('body');
  270. $this->showContent();
  271. $this->elementEnd('body');
  272. $this->endHTML();
  273. } else {
  274. $this->showPage();
  275. }
  276. }
  277. /**
  278. * Title of the page
  279. *
  280. * @return string title of the page
  281. */
  282. public function title(): string
  283. {
  284. // TRANS: Page title for remote subscription form.
  285. return !empty($this->uri) ? _m('Confirm') : _m('Remote subscription');
  286. }
  287. /**
  288. * Instructions for use
  289. *
  290. * @return string instructions for use
  291. */
  292. public function getInstructions(): string
  293. {
  294. // TRANS: Instructions.
  295. return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:');
  296. }
  297. /**
  298. * Show page notice.
  299. *
  300. * @return void
  301. */
  302. public function showPageNotice(): void
  303. {
  304. if (!empty($this->error)) {
  305. $this->element('p', 'error', $this->error);
  306. }
  307. }
  308. /**
  309. * Content area of the page
  310. *
  311. * @return void
  312. */
  313. public function showContent(): void
  314. {
  315. if ($this->profile) {
  316. $this->showPreviewForm();
  317. } else {
  318. $this->showInputForm();
  319. }
  320. }
  321. /**
  322. * Show javascript headers
  323. *
  324. * @return void
  325. */
  326. public function showScripts(): void
  327. {
  328. parent::showScripts();
  329. $this->autofocus('profile');
  330. }
  331. /**
  332. * Return url for this action
  333. *
  334. * @return string
  335. */
  336. function selfLink(): string
  337. {
  338. return common_local_url('RemoteFollowSub');
  339. }
  340. /**
  341. * Disable the send-notice form at the top of the page.
  342. * This is really just a hack for the broken CSS in the Cloudy theme,
  343. * I think; copying from other non-notice-navigation pages that do this
  344. * as well. There will be plenty of others also broken.
  345. *
  346. * @fixme fix the cloudy theme
  347. * @fixme do this in a more general way
  348. */
  349. public function showNoticeForm(): void
  350. {
  351. // nop
  352. }
  353. }