upstream V3 development https://www.gnusocial.rocks/v3
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

198 lines
7.8 KiB

  1. <?php
  2. declare(strict_types = 1);
  3. namespace App\Controller;
  4. use App\Core\Controller;
  5. use App\Core\DB\DB;
  6. use App\Core\Event;
  7. use App\Core\Form;
  8. use App\Core\Log;
  9. use App\Entity\Actor;
  10. use App\Entity\LocalUser;
  11. use App\Entity\Subscription;
  12. use App\Security\Authenticator;
  13. use App\Security\EmailVerifier;
  14. use App\Util\Common;
  15. use App\Util\Exception\DuplicateFoundException;
  16. use App\Util\Exception\EmailTakenException;
  17. use App\Util\Exception\NicknameEmptyException;
  18. use App\Util\Exception\NicknameException;
  19. use App\Util\Exception\NicknameInvalidException;
  20. use App\Util\Exception\NicknameNotAllowedException;
  21. use App\Util\Exception\NicknameTakenException;
  22. use App\Util\Exception\NicknameTooLongException;
  23. use App\Util\Exception\NotFoundException;
  24. use App\Util\Exception\ServerException;
  25. use App\Util\Form\FormFields;
  26. use App\Util\Nickname;
  27. use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
  28. use LogicException;
  29. use Symfony\Component\Form\Extension\Core\Type\EmailType;
  30. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  31. use Symfony\Component\Form\Extension\Core\Type\TextType;
  32. use Symfony\Component\HttpFoundation\Request;
  33. use Symfony\Component\HttpFoundation\Response;
  34. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  35. use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
  36. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  37. use Symfony\Component\Validator\Constraints\Length;
  38. use Symfony\Component\Validator\Constraints\NotBlank;
  39. use function App\Core\I18n\_m;
  40. class Security extends Controller
  41. {
  42. /**
  43. * Log a user in
  44. */
  45. public function login(AuthenticationUtils $authenticationUtils)
  46. {
  47. // Skip if already logged in
  48. if ($this->getUser()) {
  49. return $this->redirectToRoute('main_all');
  50. }
  51. // get the login error if there is one
  52. $error = $authenticationUtils->getLastAuthenticationError();
  53. // last username entered by the user
  54. $last_login_id = $authenticationUtils->getLastUsername();
  55. return [
  56. '_template' => 'security/login.html.twig',
  57. 'last_login_id' => $last_login_id,
  58. 'error' => $error,
  59. ];
  60. }
  61. /**
  62. * @codeCoverageIgnore
  63. */
  64. public function logout()
  65. {
  66. throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
  67. }
  68. /**
  69. * Register a user, making sure the nickname is not reserved and
  70. * possibly sending a confirmation email
  71. *
  72. * @throws DuplicateFoundException
  73. * @throws EmailTakenException
  74. * @throws EmailTakenException
  75. * @throws NicknameEmptyException
  76. * @throws NicknameException
  77. * @throws NicknameInvalidException
  78. * @throws NicknameNotAllowedException
  79. * @throws NicknameTakenException
  80. * @throws NicknameTooLongException
  81. * @throws ServerException
  82. */
  83. public function register(
  84. Request $request,
  85. UserPasswordHasherInterface $user_password_hasher,
  86. Authenticator $authenticator,
  87. GuardAuthenticatorHandler $guard,
  88. ): array|Response {
  89. $form = Form::create([
  90. ['nickname', TextType::class, [
  91. 'label' => _m('Nickname'),
  92. 'help' => _m('Your desired nickname (e.g., j0hnD03)'),
  93. 'constraints' => [
  94. new NotBlank(['message' => _m('Please enter a nickname')]),
  95. new Length([
  96. 'max' => Nickname::MAX_LEN,
  97. 'maxMessage' => _m(['Your nickname must be at most # characters long'], ['count' => Nickname::MAX_LEN]),
  98. ]),
  99. ],
  100. 'block_name' => 'nickname',
  101. 'label_attr' => ['class' => 'section-form-label'],
  102. 'invalid_message' => _m('Nickname not valid. Please provide a valid nickname.'),
  103. ]],
  104. ['email', EmailType::class, [
  105. 'label' => _m('Email'),
  106. 'help' => _m('Desired email for this account (e.g., john@provider.com)'),
  107. 'constraints' => [new NotBlank(['message' => _m('Please enter an email')])],
  108. 'block_name' => 'email',
  109. 'label_attr' => ['class' => 'section-form-label'],
  110. 'invalid_message' => _m('Email not valid. Please provide a valid email.'),
  111. 'attr' => ['autocomplete' => 'email'],
  112. ]],
  113. FormFields::repeated_password(['attr' => ['autocomplete' => 'new-password']]),
  114. ['register', SubmitType::class, ['label' => _m('Register')]],
  115. ], form_options: ['block_prefix' => 'register']);
  116. $form->handleRequest($request);
  117. if ($form->isSubmitted() && $form->isValid()) {
  118. $data = $form->getData();
  119. $data['password'] = $form->get('password')->getData();
  120. // Already used is checked below
  121. $nickname = Nickname::normalize($data['nickname'], check_already_used: false, which: Nickname::CHECK_LOCAL_USER, check_is_allowed: false);
  122. try {
  123. $found_user = DB::findOneBy('local_user', ['or' => ['nickname' => $nickname, 'outgoing_email' => $data['email']]]);
  124. if ($found_user->getNickname() === $nickname) {
  125. throw new NicknameTakenException($found_user->getActor());
  126. } elseif ($found_user->getOutgoingEmail() === $data['email']) {
  127. throw new EmailTakenException($found_user->getActor());
  128. }
  129. unset($found_user);
  130. } catch (NotFoundException) {
  131. // continue
  132. }
  133. try {
  134. // This already checks if the nickname is being used
  135. $actor = Actor::create(['nickname' => $nickname, 'is_local' => true]);
  136. $user = LocalUser::create([
  137. 'nickname' => $nickname,
  138. 'outgoing_email' => $data['email'],
  139. 'incoming_email' => $data['email'],
  140. ]);
  141. $user->setPassword($user_password_hasher->hashPassword($user, $data['password']));
  142. DB::persistWithSameId(
  143. $actor,
  144. $user,
  145. // Self subscription
  146. fn (int $id) => DB::persist(Subscription::create(['subscriber' => $id, 'subscribed' => $id])),
  147. );
  148. Event::handle('SuccessfulLocalUserRegistration', [$actor, $user]);
  149. DB::flush();
  150. // @codeCoverageIgnoreStart
  151. } catch (UniqueConstraintViolationException $e) {
  152. // _something_ was duplicated, but since we already check if nickname is in use, we can't tell what went wrong
  153. $m = 'An error occurred while trying to register';
  154. Log::critical($m . " with nickname: '{$nickname}' and email '{$data['email']}'");
  155. throw new ServerException(_m($m), previous: $e);
  156. }
  157. // @codeCoverageIgnoreEnd
  158. // generate a signed url and email it to the user
  159. if ($_ENV['APP_ENV'] !== 'dev' && Common::config('site', 'use_email')) {
  160. // @codeCoverageIgnoreStart
  161. // TODO: Implement send confirmation email
  162. // (new EmailVerifier())->sendEmailConfirmation($user);
  163. // @codeCoverageIgnoreEnd
  164. } else {
  165. $user->setIsEmailVerified(true);
  166. }
  167. return $guard->authenticateUserAndHandleSuccess(
  168. $user,
  169. $request,
  170. $authenticator,
  171. 'main',
  172. );
  173. }
  174. return [
  175. '_template' => 'security/register.html.twig',
  176. 'registration_form' => $form->createView(),
  177. ];
  178. }
  179. }