PersonSettings.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social 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. // GNU social 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 GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. /**
  20. * Handle network public feed
  21. *
  22. * @package GNUsocial
  23. * @category Controller
  24. *
  25. * @author Hugo Sales <hugo@hsal.es>
  26. * @author Eliseu Amaro <eliseu@fc.up.pt>
  27. * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
  28. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  29. */
  30. namespace Component\Person\Controller;
  31. // {{{ Imports
  32. use App\Core\Controller;
  33. use App\Core\DB;
  34. use App\Core\Event;
  35. use App\Core\Form;
  36. use function App\Core\I18n\_m;
  37. use App\Core\Log;
  38. use App\Entity\Actor;
  39. use App\Util\Common;
  40. use App\Util\Exception\AuthenticationException;
  41. use App\Util\Exception\NicknameEmptyException;
  42. use App\Util\Exception\NicknameInvalidException;
  43. use App\Util\Exception\NicknameNotAllowedException;
  44. use App\Util\Exception\NicknameTakenException;
  45. use App\Util\Exception\NicknameTooLongException;
  46. use App\Util\Exception\NoLoggedInUser;
  47. use App\Util\Exception\RedirectException;
  48. use App\Util\Exception\ServerException;
  49. use App\Util\Form\ActorArrayTransformer;
  50. use App\Util\Form\ActorForms;
  51. use App\Util\Form\FormFields;
  52. use App\Util\Formatting;
  53. use Component\Language\Controller\Language as LanguageController;
  54. use Component\Notification\Entity\UserNotificationPrefs;
  55. use Doctrine\DBAL\Types\Types;
  56. use Exception;
  57. use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
  58. use Symfony\Component\Form\Extension\Core\Type\PasswordType;
  59. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  60. use Symfony\Component\Form\Extension\Core\Type\TextType;
  61. use Symfony\Component\Form\FormInterface;
  62. use Symfony\Component\HttpFoundation\Request;
  63. // }}} Imports
  64. class PersonSettings extends Controller
  65. {
  66. /**
  67. * Return main settings page forms
  68. *
  69. * @throws \App\Util\Exception\ClientException
  70. * @throws \App\Util\Exception\NicknameException
  71. * @throws \Doctrine\DBAL\Exception
  72. * @throws AuthenticationException
  73. * @throws NicknameEmptyException
  74. * @throws NicknameInvalidException
  75. * @throws NicknameNotAllowedException
  76. * @throws NicknameTakenException
  77. * @throws NicknameTooLongException
  78. * @throws NoLoggedInUser
  79. * @throws RedirectException
  80. * @throws ServerException
  81. *
  82. * @return ControllerResultType
  83. */
  84. public function allSettings(Request $request, LanguageController $language): array
  85. {
  86. // Ensure the user is logged in and retrieve Actor object for given user
  87. $user = Common::ensureLoggedIn();
  88. // Must be persisted
  89. $actor = DB::findOneBy(Actor::class, ['id' => $user->getId()]);
  90. $personal_form = ActorForms::personalInfo(request: $request, scope: $actor, target: $actor);
  91. $email_form = self::email($request);
  92. $password_form = self::password($request);
  93. $notifications_form_array = self::notifications($request);
  94. $language_form = $language->settings($request);
  95. return [
  96. '_template' => 'person/settings.html.twig',
  97. 'personal_info_form' => $personal_form->createView(),
  98. 'email_form' => $email_form->createView(),
  99. 'password_form' => $password_form->createView(),
  100. 'language_form' => $language_form->createView(),
  101. 'tabbed_forms_notify' => $notifications_form_array,
  102. 'open_details_query' => $this->string('open'),
  103. ];
  104. }
  105. /**
  106. * Change email settings form
  107. *
  108. * @throws NoLoggedInUser
  109. * @throws ServerException
  110. */
  111. private static function email(Request $request): FormInterface
  112. {
  113. $user = Common::ensureLoggedIn();
  114. // TODO Add support missing settings
  115. $form = Form::create([
  116. ['outgoing_email_sanitized', TextType::class,
  117. [
  118. 'label' => _m('Outgoing email'),
  119. 'required' => false,
  120. 'help' => _m('Change the email we use to contact you'),
  121. 'data' => $user->getOutgoingEmail() ?: '',
  122. ],
  123. ],
  124. ['incoming_email_sanitized', TextType::class,
  125. [
  126. 'label' => _m('Incoming email'),
  127. 'required' => false,
  128. 'help' => _m('Change the email you use to contact us (for posting, for instance)'),
  129. 'data' => $user->getIncomingEmail() ?: '',
  130. ],
  131. ],
  132. ['save_email', SubmitType::class, ['label' => _m('Save email info')]],
  133. ]);
  134. $form->handleRequest($request);
  135. if ($form->isSubmitted() && $form->isValid()) {
  136. $data = $form->getData();
  137. foreach ($data as $key => $val) {
  138. $method = 'set' . ucfirst(Formatting::snakeCaseToCamelCase($key));
  139. if (method_exists($user, $method)) {
  140. $user->{$method}($val);
  141. }
  142. }
  143. DB::flush();
  144. }
  145. return $form;
  146. }
  147. /**
  148. * Change password form
  149. *
  150. * @throws AuthenticationException
  151. * @throws NoLoggedInUser
  152. * @throws ServerException
  153. */
  154. private static function password(Request $request): FormInterface
  155. {
  156. $user = Common::ensureLoggedIn();
  157. // TODO Add support missing settings
  158. $form = Form::create([
  159. ['old_password', PasswordType::class, ['label' => _m('Old password'), 'required' => true, 'help' => _m('Enter your old password for verification'), 'attr' => ['placeholder' => '********']]],
  160. FormFields::repeated_password(['required' => true]),
  161. ['save_password', SubmitType::class, ['label' => _m('Save new password')]],
  162. ]);
  163. $form->handleRequest($request);
  164. if ($form->isSubmitted() && $form->isValid()) {
  165. $data = $form->getData();
  166. if (!\is_null($data['old_password'])) {
  167. $data['password'] = $form->get('password')->getData();
  168. if (!($user->changePassword($data['old_password'], $data['password']))) {
  169. throw new AuthenticationException(_m('The provided password is incorrect'));
  170. }
  171. }
  172. unset($data['old_password'], $data['password']);
  173. foreach ($data as $key => $val) {
  174. $method = 'set' . ucfirst(Formatting::snakeCaseToCamelCase($key));
  175. if (method_exists($user, $method)) {
  176. $user->{$method}($val);
  177. }
  178. }
  179. DB::flush();
  180. }
  181. return $form;
  182. }
  183. /**
  184. * Local user notification settings tabbed panel
  185. *
  186. * @throws \Doctrine\DBAL\Exception
  187. * @throws NoLoggedInUser
  188. * @throws ServerException
  189. *
  190. * @return ControllerResultType[]
  191. */
  192. private static function notifications(Request $request): array
  193. {
  194. $user = Common::ensureLoggedIn();
  195. $schema = DB::getConnection()->getSchemaManager();
  196. $platform = $schema->getDatabasePlatform();
  197. $columns = Common::arrayRemoveKeys($schema->listTableColumns('user_notification_prefs'), ['user_id', 'transport', 'created', 'modified']);
  198. $form_defs = ['placeholder' => []];
  199. foreach ($columns as $name => $col) {
  200. $type = $col->getType();
  201. // TODO: current value is never retrieved properly, form always gets defaults
  202. $val = $type->convertToPHPValue($col->getDefault(), $platform);
  203. $type_str = $type->getName();
  204. $label = str_replace('_', ' ', ucfirst($name));
  205. $labels = [
  206. 'target_actor_id' => 'Target Actors',
  207. 'dm' => 'DM',
  208. ];
  209. $help = [
  210. 'target_actor_id' => 'If specified, these settings apply only to these profiles (comma- or space-separated list)',
  211. 'activity_by_subscribed' => 'Notify me when someone I subscribed has new activity',
  212. 'mention' => 'Notify me when mentions me in a notice',
  213. 'reply' => 'Notify me when someone replies to a notice made by me',
  214. 'subscription' => 'Notify me when someone subscribes to me or asks for permission to do so',
  215. 'favorite' => 'Notify me when someone favorites one of my notices',
  216. 'nudge' => 'Notify me when someone nudges me',
  217. 'dm' => 'Notify me when someone sends me a direct message',
  218. 'post_on_status_change' => 'Post a notice when my status in this service changes',
  219. 'enable_posting' => 'Enable posting from this service',
  220. ];
  221. switch ($type_str) {
  222. case Types::BOOLEAN:
  223. $form_defs['placeholder'][$name] = [$name, CheckboxType::class, ['data' => $val, 'required' => false, 'label' => _m($labels[$name] ?? $label), 'help' => _m($help[$name])]];
  224. break;
  225. case Types::INTEGER:
  226. if ($name === 'target_actor_id') {
  227. $form_defs['placeholder'][$name] = [$name, TextType::class, ['data' => $val, 'required' => false, 'label' => _m($labels[$name]), 'help' => _m($help[$name])], 'transformer' => ActorArrayTransformer::class];
  228. }
  229. break;
  230. default:
  231. // @codeCoverageIgnoreStart
  232. Log::critical("Structure of table user_notification_prefs changed in a way not accounted to in notification settings ({$name}): " . $type_str);
  233. throw new ServerException(_m('Internal server error'));
  234. // @codeCoverageIgnoreEnd
  235. }
  236. }
  237. $form_defs['placeholder']['save'] = fn (string $transport, string $form_name) => [$form_name, SubmitType::class,
  238. ['label' => _m('Save notification settings for {transport}', ['transport' => $transport])], ];
  239. Event::handle('AddNotificationTransport', [&$form_defs]);
  240. unset($form_defs['placeholder']);
  241. $tabbed_forms = [];
  242. foreach ($form_defs as $transport_name => $f) { // @phpstan-ignore-line
  243. unset($f['save']);
  244. $form = Form::create($f);
  245. $tabbed_forms[$transport_name]['title'] = $transport_name;
  246. $tabbed_forms[$transport_name]['desc'] = _m('{transport} notification settings', ['transport' => $transport_name]);
  247. $tabbed_forms[$transport_name]['id'] = "settings-notifications-{$transport_name}";
  248. $tabbed_forms[$transport_name]['form'] = $form->createView();
  249. $form->handleRequest($request);
  250. // TODO: on submit, form reports a nonce error. Therefore, user changes are not applied
  251. // errors: array:1 [▼
  252. // 0 => Symfony\Component\Form\FormError {#2956 ▼
  253. // #messageTemplate: "Invalid nonce"
  254. // #messageParameters: []
  255. // #messagePluralization: null
  256. // -message: "Invalid nonce"
  257. // -cause: Symfony\Component\Security\Csrf\CsrfToken {#2955 ▶}
  258. // -origin: Symfony\Component\Form\Form {#2868}
  259. // }
  260. // ]
  261. if ($form->isSubmitted()) {
  262. $data = $form->getData();
  263. unset($data['translation_domain']);
  264. try {
  265. [$entity, $is_update] = UserNotificationPrefs::checkExistingAndCreateOrUpdate(
  266. array_merge(['user_id' => $user->getId(), 'transport' => $transport_name], $data),
  267. find_by_keys: ['user_id', 'transport'],
  268. );
  269. if (!$is_update) {
  270. DB::persist($entity);
  271. }
  272. DB::flush();
  273. // @codeCoverageIgnoreStart
  274. } catch (Exception $e) {
  275. // Somehow, the exception doesn't bubble up in phpunit
  276. // dd($data, $e);
  277. // @codeCoverageIgnoreEnd
  278. Log::critical('Exception at ' . $e->getFile() . ':' . $e->getLine() . ': ' . $e->getMessage());
  279. }
  280. }
  281. }
  282. return $tabbed_forms;
  283. }
  284. }