Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
56 / 56
Security
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
2 / 2
11
100.00% covered (success)
100.00%
56 / 56
 login
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 logout
n/a
0 / 0
1
n/a
0 / 0
 register
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
48 / 48
1<?php
2
3namespace App\Controller;
4
5use App\Core\Controller;
6use App\Core\DB\DB;
7use App\Core\Form;
8use function App\Core\I18n\_m;
9use App\Core\Log;
10use App\Core\VisibilityScope;
11use App\Entity\Follow;
12use App\Entity\GSActor;
13use App\Entity\LocalUser;
14use App\Entity\Note;
15use App\Security\Authenticator;
16use app\Util\Common;
17use App\Util\Exception\EmailTakenException;
18use App\Util\Exception\NicknameTakenException;
19use App\Util\Exception\ServerException;
20use App\Util\Form\FormFields;
21use App\Util\Nickname;
22use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
23use Symfony\Component\Form\Extension\Core\Type\EmailType;
24use Symfony\Component\Form\Extension\Core\Type\SubmitType;
25use Symfony\Component\Form\Extension\Core\Type\TextType;
26use Symfony\Component\HttpFoundation\Request;
27use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
28use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
29use Symfony\Component\Validator\Constraints\Length;
30use Symfony\Component\Validator\Constraints\NotBlank;
31
32class Security extends Controller
33{
34    /**
35     * Log a user in
36     */
37    public function login(AuthenticationUtils $authenticationUtils)
38    {
39        if ($this->getUser()) {
40            return $this->redirectToRoute('main_all');
41        }
42
43        // get the login error if there is one
44        $error = $authenticationUtils->getLastAuthenticationError();
45        // last username entered by the user
46        $last_login_id = $authenticationUtils->getLastUsername();
47
48        return [
49            '_template'     => 'security/login.html.twig',
50            'last_login_id' => $last_login_id,
51            'error'         => $error,
52            'notes_fn'      => fn ()      => Note::getAllNotes(VisibilityScope::$instance_scope),
53        ];
54    }
55
56    /**
57     * @codeCoverageIgnore
58     */
59    public function logout()
60    {
61        throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
62    }
63
64    /**
65     * Register a user, making sure the nickname is not reserved and
66     * possibly sending a confirmation email
67     */
68    public function register(Request $request,
69                             GuardAuthenticatorHandler $guard_handler,
70                             Authenticator $authenticator)
71    {
72        $form = Form::create([
73            ['nickname', TextType::class, [
74                'label'       => _m('Nickname'),
75                'help'        => _m('Your desired nickname (e.g., j0hnD03)'),
76                'constraints' => [
77                    new NotBlank(['message' => _m('Please enter a nickname')]),
78                    new Length([
79                        'min'        => Common::config('nickname', 'min_length'),
80                        'minMessage' => _m(['Your nickname must be at least # characters long'], ['count' => Common::config('nickname', 'min_length')]),
81                        'max'        => Nickname::MAX_LEN,
82                        'maxMessage' => _m(['Your nickname must be at most # characters long'], ['count' => Nickname::MAX_LEN]), ]),
83                ],
84                'block_name' => 'nickname',
85                'label_attr' => ['class' => 'section-form-label'],
86            ]],
87            ['email', EmailType::class, [
88                'label'       => _m('Email'),
89                'help'        => _m('Desired email for this account (e.g., john@provider.com)'),
90                'constraints' => [ new NotBlank(['message' => _m('Please enter an email') ])],
91                'block_name'  => 'email',
92                'label_attr'  => ['class' => 'section-form-label'],
93            ]],
94            FormFields::repeated_password(),
95            ['register', SubmitType::class, ['label' => _m('Register')]],
96        ], form_options: ['block_prefix' => 'register']);
97
98        $form->handleRequest($request);
99
100        if ($form->isSubmitted() && $form->isValid()) {
101            $data             = $form->getData();
102            $data['password'] = $form->get('password')->getData();
103
104            // This will throw the appropriate errors, result ignored
105            $user = LocalUser::findByNicknameOrEmail($data['nickname'], $data['email']);
106            if ($user !== null) {
107                // If we do find something, there's a duplicate
108                if ($user->getNickname() == $data['nickname']) {
109                    throw new NicknameTakenException;
110                } else {
111                    throw new EmailTakenException;
112                }
113            }
114
115            $valid_nickname = Nickname::validate($data['nickname'], check_already_used: false);
116
117            try {
118                // This already checks if the nickname is being used
119                $actor = GSActor::create(['nickname' => $valid_nickname]);
120                $user  = LocalUser::create([
121                    'nickname'       => $valid_nickname,
122                    'outgoing_email' => $data['email'],
123                    'incoming_email' => $data['email'],
124                    'password'       => LocalUser::hashPassword($data['password']),
125                ]);
126                DB::persistWithSameId(
127                    $actor,
128                    $user,
129                    // Self follow
130                    fn (int $id) => DB::persist(Follow::create(['follower' => $id, 'followed' => $id]))
131                );
132                DB::flush();
133                // @codeCoverageIgnoreStart
134            } catch (UniqueConstraintViolationException $e) {
135                // _something_ was duplicated, but since we already check if nickname is in use, we can't tell what went wrong
136                $e = 'An error occurred while trying to register';
137                Log::critical($e . " with nickname: '{$valid_nickname}' and email '{$data['email']}'");
138                throw new ServerException(_m($e));
139            }
140            // @codeCoverageIgnoreEnd
141
142            // generate a signed url and email it to the user
143            if ($_ENV['APP_ENV'] !== 'dev' && Common::config('site', 'use_email')) {
144                // @codeCoverageIgnoreStart
145                Common::sendVerificationEmail();
146            // @codeCoverageIgnoreEnd
147            } else {
148                $user->setIsEmailVerified(true);
149            }
150
151            return $guard_handler->authenticateUserAndHandleSuccess(
152                $user,
153                $request,
154                $authenticator,
155                'main' // firewall name in security.yaml
156            );
157        }
158
159        return [
160            '_template'         => 'security/register.html.twig',
161            'registration_form' => $form->createView(),
162            'notes_fn'          => fn ()          => Note::getAllNotes(VisibilityScope::$instance_scope),
163        ];
164    }
165}