Passed
Push — main ( 32046f...b819d2 )
by Axel
04:42
created

UserAdministrationController::verify()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 19
nc 7
nop 4
dl 0
loc 34
rs 9.0111
c 2
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Zikula package.
7
 *
8
 * Copyright Zikula - https://ziku.la/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Zikula\ZAuthBundle\Controller;
15
16
use Doctrine\Persistence\ManagerRegistry;
17
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
18
use Symfony\Component\HttpFoundation\RedirectResponse;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\Routing\Annotation\Route;
22
use Symfony\Component\Routing\RouterInterface;
23
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
24
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
25
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
26
use Symfony\Contracts\Translation\TranslatorInterface;
27
use Zikula\Bundle\CoreBundle\Filter\AlphaFilter;
28
use Zikula\Bundle\CoreBundle\Response\PlainResponse;
29
use Zikula\Bundle\CoreBundle\Translation\TranslatorTrait;
30
use Zikula\Component\SortableColumns\Column;
31
use Zikula\Component\SortableColumns\SortableColumns;
32
use Zikula\PermissionsBundle\Annotation\PermissionCheck;
33
use Zikula\PermissionsBundle\Api\ApiInterface\PermissionApiInterface;
34
use Zikula\UsersBundle\Collector\AuthenticationMethodCollector;
35
use Zikula\UsersBundle\Entity\User;
36
use Zikula\UsersBundle\Event\ActiveUserPostUpdatedEvent;
37
use Zikula\UsersBundle\Event\EditUserFormPostCreatedEvent;
38
use Zikula\UsersBundle\Event\EditUserFormPostValidatedEvent;
39
use Zikula\UsersBundle\Event\RegistrationPostDeletedEvent;
40
use Zikula\UsersBundle\Event\RegistrationPostSuccessEvent;
41
use Zikula\UsersBundle\Helper\MailHelper as UsersMailHelper;
42
use Zikula\UsersBundle\Helper\RegistrationHelper;
43
use Zikula\UsersBundle\Repository\UserRepositoryInterface;
44
use Zikula\UsersBundle\UsersConstant;
45
use Zikula\ZAuthBundle\Entity\AuthenticationMapping;
46
use Zikula\ZAuthBundle\Form\Type\AdminCreatedUserType;
47
use Zikula\ZAuthBundle\Form\Type\AdminModifyUserType;
48
use Zikula\ZAuthBundle\Form\Type\BatchForcePasswordChangeType;
49
use Zikula\ZAuthBundle\Form\Type\SendVerificationConfirmationType;
50
use Zikula\ZAuthBundle\Form\Type\TogglePasswordConfirmationType;
51
use Zikula\ZAuthBundle\Helper\AdministrationActionsHelper;
52
use Zikula\ZAuthBundle\Helper\BatchPasswordChangeHelper;
53
use Zikula\ZAuthBundle\Helper\LostPasswordVerificationHelper;
54
use Zikula\ZAuthBundle\Helper\MailHelper;
55
use Zikula\ZAuthBundle\Helper\RegistrationVerificationHelper;
56
use Zikula\ZAuthBundle\Repository\AuthenticationMappingRepositoryInterface;
57
use Zikula\ZAuthBundle\ZAuthConstant;
58
59
#[Route('/zauth/admin')]
60
class UserAdministrationController extends AbstractController
61
{
62
    use TranslatorTrait;
63
64
    public function __construct(
65
        TranslatorInterface $translator,
66
        private readonly PermissionApiInterface $permissionApi,
67
        private readonly int $usersPerPage,
68
        private readonly int $minimumPasswordLength,
69
        private readonly bool $usePasswordStrengthMeter,
70
        private readonly int $changePasswordExpireDays
71
    ) {
72
        $this->setTranslator($translator);
73
    }
74
75
    #[Route('/list/{sort}/{sortdir}/{letter}/{page}', name: 'zikulazauthbundle_useradministration_listmappings', methods: ['GET'], requirements: ['page' => '\d+'])]
76
    #[PermissionCheck('moderate')]
77
    public function listMappings(
78
        Request $request,
79
        AuthenticationMappingRepositoryInterface $authenticationMappingRepository,
80
        RouterInterface $router,
81
        AdministrationActionsHelper $actionsHelper,
82
        string $sort = 'uid',
83
        string $sortdir = 'DESC',
84
        string $letter = 'all',
85
        int $page = 1
86
    ): Response {
87
        $sortableColumns = new SortableColumns($router, 'zikulazauthbundle_useradministration_listmappings', 'sort', 'sortdir');
88
        $sortableColumns->addColumns([new Column('uname'), new Column('uid')]);
89
        $sortableColumns->setOrderByFromRequest($request);
90
        $sortableColumns->setAdditionalUrlParameters([
91
            'letter' => $letter,
92
            'page' => $page
93
        ]);
94
95
        $filter = [];
96
        if (!empty($letter) && 'all' !== $letter) {
97
            $filter['uname'] = ['operator' => 'like', 'operand' => "${letter}%"];
98
        }
99
        $paginator = $authenticationMappingRepository->query($filter, [$sort => $sortdir], 'and', $page, $this->usersPerPage);
100
        $paginator->setRoute('zikulazauthbundle_useradministration_listmappings');
101
        $routeParameters = [
102
            'sort' => $sort,
103
            'sortdir' => $sortdir,
104
            'letter' => $letter,
105
        ];
106
        $paginator->setRouteParameters($routeParameters);
107
108
        return $this->render('@ZikulaZAuth/UserAdministration/list.html.twig', [
109
            'sort' => $sortableColumns->generateSortableColumns(),
110
            'actionsHelper' => $actionsHelper,
111
            'alpha' => new AlphaFilter('zikulazauthbundle_useradministration_listmappings', $routeParameters, $letter),
112
            'paginator' => $paginator,
113
        ]);
114
    }
115
116
    /**
117
     * Called from UsersBundle/Resources/public/js/Zikula.Users.Admin.View.js
118
     * to populate a username search
119
     */
120
    #[Route('/getusersbyfragmentastable', name: 'zikulazauthbundle_useradministration_getusersbyfragmentastable', methods: ['POST'], options: ['expose' => true])]
121
    public function getUsersByFragmentAsTable(
122
        Request $request,
123
        AuthenticationMappingRepositoryInterface $authenticationMappingRepository,
124
        AdministrationActionsHelper $actionsHelper
125
    ): Response {
126
        if (!$this->permissionApi->hasPermission('ZikulaZAuthModule::', '::', ACCESS_MODERATE)) {
127
            return new PlainResponse('');
128
        }
129
        $fragment = $request->request->get('fragment');
130
        $filter = [
131
            'uname' => ['operator' => 'like', 'operand' => $fragment . '%']
132
        ];
133
        $mappings = $authenticationMappingRepository->query($filter);
134
135
        return $this->render('@ZikulaZAuth/UserAdministration/userlist.html.twig', [
136
            'mappings' => $mappings->getResults(),
137
            'actionsHelper' => $actionsHelper,
138
        ], new PlainResponse());
139
    }
140
141
    #[Route('/user/create', name: 'zikulazauthbundle_useradministration_create')]
142
    #[PermissionCheck('admin')]
143
    public function create(
144
        Request $request,
145
        AuthenticationMethodCollector $authenticationMethodCollector,
146
        UserRepositoryInterface $userRepository,
147
        RegistrationHelper $registrationHelper,
148
        UsersMailHelper $mailHelper,
149
        EventDispatcherInterface $eventDispatcher
150
    ): Response {
151
        $mapping = new AuthenticationMapping();
152
        $form = $this->createForm(AdminCreatedUserType::class, $mapping, [
153
            'minimumPasswordLength' => $this->minimumPasswordLength,
154
        ]);
155
        $editUserFormPostCreatedEvent = new EditUserFormPostCreatedEvent($form);
156
        $eventDispatcher->dispatch($editUserFormPostCreatedEvent);
157
        $form->handleRequest($request);
158
159
        if ($form->isSubmitted() && $form->isValid()) {
160
            if ($form->get('submit')->isClicked()) {
0 ignored issues
show
Bug introduced by
The method isClicked() does not exist on Symfony\Component\Form\FormInterface. It seems like you code against a sub-type of Symfony\Component\Form\FormInterface such as Symfony\Component\Form\SubmitButton. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

160
            if ($form->get('submit')->/** @scrutinizer ignore-call */ isClicked()) {
Loading history...
161
                $mapping = $form->getData();
162
                $passToSend = $form['sendpass']->getData() ? $mapping->getPass() : '';
163
                $authMethodName = (ZAuthConstant::AUTHENTICATION_METHOD_EITHER === $mapping->getMethod()) ? ZAuthConstant::AUTHENTICATION_METHOD_UNAME : $mapping->getMethod();
164
                $authMethod = $authenticationMethodCollector->get($authMethodName);
165
166
                if ($request->hasSession() && ($session = $request->getSession())) {
167
                    $session->set(ZAuthConstant::SESSION_EMAIL_VERIFICATION_STATE, ($form['usermustverify']->getData() ? 'Y' : 'N'));
168
                }
169
170
                $userData = $mapping->getUserEntityData();
171
                if (null === $userData['uid']) {
172
                    unset($userData['uid']);
173
                }
174
                $user = new User();
175
                foreach ($userData as $fieldName => $fieldValue) {
176
                    $setter = 'set' . ucfirst($fieldName);
177
                    $user->{$setter}($fieldValue);
178
                }
179
                $user->setAttribute(UsersConstant::AUTHENTICATION_METHOD_ATTRIBUTE_KEY, $mapping->getMethod());
180
                $registrationHelper->registerNewUser($user);
181
                if (UsersConstant::ACTIVATED_PENDING_REG === $user->getActivated()) {
182
                    $notificationErrors = $mailHelper->createAndSendRegistrationMail($user, $form['usernotification']->getData(), $form['adminnotification']->getData(), $passToSend);
183
                } else {
184
                    $notificationErrors = $mailHelper->createAndSendUserMail($user, $form['usernotification']->getData(), $form['adminnotification']->getData(), $passToSend);
185
                }
186
                if (!empty($notificationErrors)) {
187
                    $this->addFlash('error', 'Errors creating user!');
188
                    $this->addFlash('error', implode('<br />', $notificationErrors));
189
                }
190
                $mapping->setUid($user->getUid());
191
                $mapping->setVerifiedEmail(!$form['usermustverify']->getData());
192
                if (!$authMethod->register($mapping->toArray())) {
193
                    $this->addFlash('error', 'The create process failed for an unknown reason.');
194
                    $userRepository->removeAndFlush($user);
195
                    $eventDispatcher->dispatch(new RegistrationPostDeletedEvent($user));
196
197
                    return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
198
                }
199
                $eventDispatcher->dispatch(new EditUserFormPostValidatedEvent($form, $user));
200
                $eventDispatcher->dispatch(new RegistrationPostSuccessEvent($user));
201
202
                if (UsersConstant::ACTIVATED_PENDING_REG === $user->getActivated()) {
203
                    $this->addFlash('status', 'Done! Created new registration application.');
204
                } elseif (null !== $user->getActivated()) {
0 ignored issues
show
introduced by
The condition null !== $user->getActivated() is always true.
Loading history...
205
                    $this->addFlash('status', 'Done! Created new user account.');
206
                } else {
207
                    $this->addFlash('error', 'Warning! New user information has been saved, however there may have been an issue saving it properly.');
208
                }
209
210
                return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
211
            }
212
            if ($form->get('cancel')->isClicked()) {
213
                $this->addFlash('status', 'Operation cancelled.');
214
            }
215
        }
216
217
        return $this->render('@ZikulaZAuth/UserAdministration/create.html.twig', [
218
            'form' => $form->createView(),
219
            'additionalTemplates' => isset($editUserFormPostCreatedEvent) ? $editUserFormPostCreatedEvent->getTemplates() : [],
220
            'usePasswordStrengthMeter' => $this->usePasswordStrengthMeter,
221
        ]);
222
    }
223
224
    /**
225
     * @throws AccessDeniedException Thrown if the user hasn't edit permissions for the mapping record
226
     */
227
    #[Route('/user/modify/{mapping}', name: 'zikulazauthbundle_useradministration_modify', requirements: ['mapping' => '^[1-9]\d*$'])]
228
    public function modify(
229
        Request $request,
230
        AuthenticationMapping $mapping,
231
        EncoderFactoryInterface $encoderFactory,
232
        UserRepositoryInterface $userRepository,
233
        AuthenticationMappingRepositoryInterface $authenticationMappingRepository,
234
        EventDispatcherInterface $eventDispatcher
235
    ): Response {
236
        if (!$this->permissionApi->hasPermission('ZikulaZAuthModule::', $mapping->getUname() . '::' . $mapping->getUid(), ACCESS_EDIT)) {
237
            throw new AccessDeniedException();
238
        }
239
        if (1 === $mapping->getUid()) {
240
            throw new AccessDeniedException($this->trans("Error! You can't edit the guest account."));
241
        }
242
243
        $form = $this->createForm(AdminModifyUserType::class, $mapping, [
244
            'minimumPasswordLength' => $this->minimumPasswordLength,
245
        ]);
246
        $originalMapping = clone $mapping;
247
        $editUserFormPostCreatedEvent = new EditUserFormPostCreatedEvent($form);
248
        $eventDispatcher->dispatch($editUserFormPostCreatedEvent);
249
        $form->handleRequest($request);
250
251
        $originalUser = clone $userRepository->find($mapping->getUid());
252
253
        if ($form->isSubmitted() && $form->isValid()) {
254
            if ($form->get('submit')->isClicked()) {
255
                /** @var AuthenticationMapping $mapping */
256
                $mapping = $form->getData();
257
                if ($form->get('setpass')->getData()) {
258
                    $mapping->setPass($encoderFactory->getEncoder($mapping)->encodePassword($mapping->getPass(), null));
259
                } else {
260
                    $mapping->setPass($originalMapping->getPass());
261
                }
262
                $authenticationMappingRepository->persistAndFlush($mapping);
263
                $userData = $mapping->getUserEntityData();
264
                /** @var User $user */
265
                $user = $userRepository->find($mapping->getUid());
266
                foreach ($userData as $fieldName => $fieldValue) {
267
                    $setter = 'set' . ucfirst($fieldName);
268
                    $user->{$setter}($fieldValue);
269
                }
270
                $userRepository->persistAndFlush($user);
271
272
                $eventDispatcher->dispatch(new ActiveUserPostUpdatedEvent($user, $originalUser));
273
274
                $eventDispatcher->dispatch(new EditUserFormPostValidatedEvent($form, $user));
275
276
                $this->addFlash('status', "Done! Saved user's account information.");
277
            } elseif ($form->get('cancel')->isClicked()) {
278
                $this->addFlash('status', 'Operation cancelled.');
279
            }
280
281
            return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
282
        }
283
284
        return $this->render('@ZikulaZAuth/UserAdministration/modify.html.twig', [
285
            'form' => $form->createView(),
286
            'additionalTemplates' => isset($editUserFormPostCreatedEvent) ? $editUserFormPostCreatedEvent->getTemplates() : [],
287
            'usePasswordStrengthMeter' => $this->usePasswordStrengthMeter,
288
        ]);
289
    }
290
291
    #[Route('/verify/{mapping}', name: 'zikulazauthbundle_useradministration_verify', requirements: ['mapping' => '^[1-9]\d*$'])]
292
    #[PermissionCheck('moderate')]
293
    public function verify(
294
        Request $request,
295
        AuthenticationMapping $mapping,
296
        AuthenticationMappingRepositoryInterface $authenticationMappingRepository,
297
        RegistrationVerificationHelper $registrationVerificationHelper
298
    ): Response {
299
        $form = $this->createForm(SendVerificationConfirmationType::class, [
300
            'mapping' => $mapping->getId()
301
        ]);
302
303
        $form->handleRequest($request);
304
        if ($form->isSubmitted() && $form->isValid()) {
305
            if ($form->get('confirm')->isClicked()) {
306
                /** @var AuthenticationMapping $modifiedMapping */
307
                $modifiedMapping = $authenticationMappingRepository->find($form->get('mapping')->getData());
308
                $verificationSent = $registrationVerificationHelper->sendVerificationCode($modifiedMapping);
309
                if (!$verificationSent) {
0 ignored issues
show
introduced by
$verificationSent is of type DateTime, thus it always evaluated to true.
Loading history...
310
                    $this->addFlash('error', $this->trans('Sorry! There was a problem sending a verification code to %sub%.', ['%sub%' => $modifiedMapping->getUname()]));
311
                } else {
312
                    $this->addFlash('status', $this->trans('Done! Verification code sent to %sub%.', ['%sub%' => $modifiedMapping->getUname()]));
313
                }
314
            }
315
            if ($form->get('cancel')->isClicked()) {
316
                $this->addFlash('status', 'Operation cancelled.');
317
            }
318
319
            return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
320
        }
321
322
        return $this->render('@ZikulaZAuth/UserAdministration/verify.html.twig', [
323
            'form' => $form->createView(),
324
            'mapping' => $mapping,
325
        ]);
326
    }
327
328
    /**
329
     * @throws AccessDeniedException Thrown if the user hasn't moderate permissions for the mapping record
330
     */
331
    #[Route('/send-confirmation/{mapping}', name: 'zikulazauthbundle_useradministration_sendconfirmation', requirements: ['mapping' => '^[1-9]\d*$'])]
332
    public function sendConfirmation(
333
        AuthenticationMapping $mapping,
334
        LostPasswordVerificationHelper $lostPasswordVerificationHelper,
335
        MailHelper $mailHelper
336
    ): RedirectResponse {
337
        if (!$this->permissionApi->hasPermission('ZikulaZAuthModule::', $mapping->getUname() . '::' . $mapping->getUid(), ACCESS_MODERATE)) {
338
            throw new AccessDeniedException();
339
        }
340
        $lostPasswordId = $lostPasswordVerificationHelper->createLostPasswordId($mapping);
341
        $mailSent = $mailHelper->sendNotification($mapping->getEmail(), 'lostpassword', [
342
            'uname' => $mapping->getUname(),
343
            'validDays' => $this->changePasswordExpireDays,
344
            'lostPasswordId' => $lostPasswordId,
345
            'requestedByAdmin' => true,
346
        ]);
347
        if ($mailSent) {
348
            $this->addFlash('status', $this->trans('Done! The password recovery verification link for %userName% has been sent via e-mail.', ['%userName%' => $mapping->getUname()]));
349
        }
350
351
        return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
352
    }
353
354
    /**
355
     * @throws AccessDeniedException Thrown if the user hasn't moderate permissions for the mapping record
356
     */
357
    #[Route('/send-username/{mapping}', name: 'zikulazauthbundle_useradministration_sendusername', requirements: ['mapping' => '^[1-9]\d*$'])]
358
    public function sendUserName(
359
        AuthenticationMapping $mapping,
360
        MailHelper $mailHelper
361
    ): RedirectResponse {
362
        if (!$this->permissionApi->hasPermission('ZikulaZAuthModule::', $mapping->getUname() . '::' . $mapping->getUid(), ACCESS_MODERATE)) {
363
            throw new AccessDeniedException();
364
        }
365
        $mailSent = $mailHelper->sendNotification($mapping->getEmail(), 'lostuname', [
366
            'uname' => $mapping->getUname(),
367
            'requestedByAdmin' => true,
368
        ]);
369
370
        if ($mailSent) {
371
            $this->addFlash('status', $this->trans('Done! The user name for %userName% has been sent via e-mail.', ['%userName%' => $mapping->getUname()]));
372
        }
373
374
        return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
375
    }
376
377
    /**
378
     * @param User $user // note: this is intentionally left as UserEntity instead of mapping because of need to access attributes
379
     *
380
     * @throws AccessDeniedException Thrown if the user hasn't moderate permissions for the user record
381
     */
382
    #[Route('/toggle-password-change/{user}', name: 'zikulazauthbundle_useradministration_togglepasswordchange', requirements: ['user' => '^[1-9]\d*$'])]
383
    public function togglePasswordChange(Request $request, ManagerRegistry $doctrine, User $user)
384
    {
385
        if (!$this->permissionApi->hasPermission('ZikulaZAuthModule::', $user->getUname() . '::' . $user->getUid(), ACCESS_MODERATE)) {
386
            throw new AccessDeniedException();
387
        }
388
        if ($user->getAttributes()->containsKey(ZAuthConstant::REQUIRE_PASSWORD_CHANGE_KEY)) {
389
            $mustChangePass = $user->getAttributes()->get(ZAuthConstant::REQUIRE_PASSWORD_CHANGE_KEY);
390
        } else {
391
            $mustChangePass = false;
392
        }
393
        $form = $this->createForm(TogglePasswordConfirmationType::class, [
394
            'uid' => $user->getUid()
395
        ], [
396
            'mustChangePass' => $mustChangePass
397
        ]);
398
        $form->handleRequest($request);
399
        if ($form->isSubmitted() && $form->isValid()) {
400
            if ($form->get('toggle')->isClicked()) {
401
                if ($user->getAttributes()->containsKey(ZAuthConstant::REQUIRE_PASSWORD_CHANGE_KEY) && (bool) $user->getAttributes()->get(ZAuthConstant::REQUIRE_PASSWORD_CHANGE_KEY)) {
402
                    $user->getAttributes()->remove(ZAuthConstant::REQUIRE_PASSWORD_CHANGE_KEY);
403
                    $this->addFlash('success', $this->trans('Done! A password change will no longer be required for %userName%.', ['%userName%' => $user->getUname()]));
404
                } else {
405
                    $user->setAttribute(ZAuthConstant::REQUIRE_PASSWORD_CHANGE_KEY, true);
406
                    $this->addFlash('success', $this->trans('Done! A password change will be required the next time %userName% logs in.', ['%userName%' => $user->getUname()]));
407
                }
408
                $doctrine->getManager()->flush();
409
            } elseif ($form->get('cancel')->isClicked()) {
410
                $this->addFlash('info', 'Operation cancelled.');
411
            }
412
413
            return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
414
        }
415
416
        return $this->render('@ZikulaZAuth/UserAdministration/togglePasswordChange.html.twig', [
417
            'form' => $form->createView(),
418
            'mustChangePass' => $mustChangePass,
419
            'user' => $user,
420
        ]);
421
    }
422
423
    #[Route('/batch-force-password-change', name: 'zikulazauthbundle_useradministration_batchforcepasswordchange')]
424
    #[PermissionCheck('admin')]
425
    public function batchForcePasswordChange(
426
        BatchPasswordChangeHelper $batchPasswordChangeHelper,
427
        Request $request
428
    ): Response {
429
        $form = $this->createForm(BatchForcePasswordChangeType::class);
430
        $form->handleRequest($request);
431
        if ($form->isSubmitted() && $form->isValid()) {
432
            if ($form->get('submit')->isClicked()) {
433
                $count = $batchPasswordChangeHelper->requirePasswordChangeByGroup($form->get('group')->getData());
434
                $this->addFlash('success', $this->trans('Operation complete. %count% user(s) changed.', ['%count%' => $count]));
435
            } elseif ($form->get('cancel')->isClicked()) {
436
                $this->addFlash('info', 'Operation cancelled.');
437
            }
438
439
            return $this->redirectToRoute('zikulazauthbundle_useradministration_listmappings');
440
        }
441
442
        return $this->render('@ZikulaZAuth/UserAdministration/batchForcePasswordChange.html.twig', [
443
            'form' => $form->createView(),
444
        ]);
445
    }
446
}
447