Passed
Pull Request — master (#5836)
by
unknown
07:43
created

AccountController::changePassword()   C

Complexity

Conditions 17
Paths 23

Size

Total Lines 96
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 70
nc 23
nop 3
dl 0
loc 96
rs 5.2166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Entity\User;
10
use Chamilo\CoreBundle\Form\ChangePasswordType;
11
use Chamilo\CoreBundle\Form\ProfileType;
12
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
13
use Chamilo\CoreBundle\Repository\Node\UserRepository;
14
use Chamilo\CoreBundle\ServiceHelper\UserHelper;
15
use Chamilo\CoreBundle\Settings\SettingsManager;
16
use Chamilo\CoreBundle\Traits\ControllerTrait;
17
use OTPHP\TOTP;
18
use Security;
19
use Symfony\Component\Form\FormError;
20
use Symfony\Component\HttpFoundation\RedirectResponse;
21
use Symfony\Component\HttpFoundation\Request;
22
use Symfony\Component\HttpFoundation\Response;
23
use Symfony\Component\Routing\Attribute\Route;
24
use Symfony\Component\Security\Core\User\UserInterface;
25
use Symfony\Component\Security\Csrf\CsrfToken;
26
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
27
use Symfony\Contracts\Translation\TranslatorInterface;
28
use Endroid\QrCode\Builder\Builder;
29
use Endroid\QrCode\Encoding\Encoding;
30
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
31
use Endroid\QrCode\Writer\PngWriter;
32
33
/**
34
 * @author Julio Montoya <[email protected]>
35
 */
36
#[Route('/account')]
37
class AccountController extends BaseController
38
{
39
    use ControllerTrait;
40
41
    public function __construct(
42
        private readonly UserHelper $userHelper,
43
        private readonly TranslatorInterface $translator
44
    ) {}
45
46
    #[Route('/edit', name: 'chamilo_core_account_edit', methods: ['GET', 'POST'])]
47
    public function edit(Request $request, UserRepository $userRepository, IllustrationRepository $illustrationRepo, SettingsManager $settingsManager): Response
48
    {
49
        $user = $this->userHelper->getCurrent();
50
51
        if (!\is_object($user) || !$user instanceof UserInterface) {
52
            throw $this->createAccessDeniedException('This user does not have access to this section');
53
        }
54
55
        /** @var User $user */
56
        $form = $this->createForm(ProfileType::class, $user);
57
        $form->setData($user);
58
        $form->handleRequest($request);
59
60
        if ($form->isSubmitted() && $form->isValid()) {
61
            $illustration = $form['illustration']->getData();
62
            if ($illustration) {
63
                $illustrationRepo->deleteIllustration($user);
64
                $illustrationRepo->addIllustration($user, $user, $illustration);
65
            }
66
67
            $showTermsIfProfileCompleted = ('true' === $settingsManager->getSetting('show_terms_if_profile_completed'));
68
            $user->setProfileCompleted($showTermsIfProfileCompleted);
69
70
            $userRepository->updateUser($user);
71
            $this->addFlash('success', $this->trans('Updated'));
72
            $url = $this->generateUrl('chamilo_core_account_home');
73
74
            $request->getSession()->set('_locale_user', $user->getLocale());
75
76
            return new RedirectResponse($url);
77
        }
78
79
        return $this->render('@ChamiloCore/Account/edit.html.twig', [
80
            'form' => $form,
81
            'user' => $user,
82
        ]);
83
    }
84
85
    #[Route('/change-password', name: 'chamilo_core_account_change_password', methods: ['GET', 'POST'])]
86
    public function changePassword(Request $request, UserRepository $userRepository, CsrfTokenManagerInterface $csrfTokenManager): Response
87
    {
88
        /* @var User $user */
89
        $user = $this->getUser();
90
91
        if (!\is_object($user) || !$user instanceof UserInterface) {
92
            throw $this->createAccessDeniedException('This user does not have access to this section');
93
        }
94
95
        $form = $this->createForm(ChangePasswordType::class, [
96
            'enable2FA' => $user->getMfaEnabled(),
97
        ]);
98
        $form->handleRequest($request);
99
100
        $qrCodeBase64 = null;
101
        if ($user->getMfaEnabled() && $user->getMfaService() === 'TOTP' && $user->getMfaSecret()) {
102
            $decryptedSecret = $this->decryptTOTPSecret($user->getMfaSecret(), $_ENV['APP_SECRET']);
103
            $totp = TOTP::create($decryptedSecret);
104
            $totp->setLabel($user->getEmail());
105
106
            $qrCodeResult = Builder::create()
107
                ->writer(new PngWriter())
108
                ->data($totp->getProvisioningUri())
109
                ->encoding(new Encoding('UTF-8'))
110
                ->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
111
                ->size(300)
112
                ->margin(10)
113
                ->build();
114
115
            $qrCodeBase64 = base64_encode($qrCodeResult->getString());
116
        }
117
118
        if ($form->isSubmitted() && $form->isValid()) {
119
            $submittedToken = $request->request->get('_token');
120
121
            if (!$csrfTokenManager->isTokenValid(new CsrfToken('change_password', $submittedToken))) {
122
                $form->addError(new FormError($this->translator->trans('CSRF token is invalid. Please try again.')));
123
            } else {
124
                $currentPassword = $form->get('currentPassword')->getData();
125
                $newPassword = $form->get('newPassword')->getData();
126
                $confirmPassword = $form->get('confirmPassword')->getData();
127
                $enable2FA = $form->get('enable2FA')->getData();
128
129
                if ($enable2FA && !$user->getMfaSecret()) {
130
                    $totp = TOTP::create();
131
                    $totp->setLabel($user->getEmail());
132
                    $encryptedSecret = $this->encryptTOTPSecret($totp->getSecret(), $_ENV['APP_SECRET']);
133
                    $user->setMfaSecret($encryptedSecret);
134
                    $user->setMfaEnabled(true);
135
                    $user->setMfaService('TOTP');
136
                    $userRepository->updateUser($user);
137
138
                    $qrCodeResult = Builder::create()
139
                        ->writer(new PngWriter())
140
                        ->data($totp->getProvisioningUri())
141
                        ->encoding(new Encoding('UTF-8'))
142
                        ->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
143
                        ->size(300)
144
                        ->margin(10)
145
                        ->build();
146
147
                    $qrCodeBase64 = base64_encode($qrCodeResult->getString());
148
149
                    return $this->render('@ChamiloCore/Account/change_password.html.twig', [
150
                        'form' => $form->createView(),
151
                        'qrCode' => $qrCodeBase64,
152
                        'user' => $user
153
                    ]);
154
                } elseif (!$enable2FA) {
155
                    $user->setMfaEnabled(false);
156
                    $user->setMfaSecret(null);
157
                    $userRepository->updateUser($user);
158
                    $this->addFlash('success', '2FA disabled successfully.');
159
                }
160
161
                if ($newPassword || $confirmPassword || $currentPassword) {
162
                    if (!$userRepository->isPasswordValid($user, $currentPassword)) {
163
                        $form->get('currentPassword')->addError(new FormError($this->translator->trans('Current password is incorrect.')));
164
                    } elseif ($newPassword !== $confirmPassword) {
165
                        $form->get('confirmPassword')->addError(new FormError($this->translator->trans('Passwords do not match.')));
166
                    } else {
167
                        $user->setPlainPassword($newPassword);
168
                        $userRepository->updateUser($user);
169
                        $this->addFlash('success', 'Password updated successfully.');
170
                    }
171
                }
172
173
                return $this->redirectToRoute('chamilo_core_account_home');
174
            }
175
        }
176
177
        return $this->render('@ChamiloCore/Account/change_password.html.twig', [
178
            'form' => $form->createView(),
179
            'qrCode' => $qrCodeBase64,
180
            'user' => $user
181
        ]);
182
    }
183
184
    /**
185
     * Encrypts the TOTP secret using AES-256-CBC encryption.
186
     */
187
    private function encryptTOTPSecret(string $secret, string $encryptionKey): string
188
    {
189
        $cipherMethod = 'aes-256-cbc';
190
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipherMethod));
191
        $encryptedSecret = openssl_encrypt($secret, $cipherMethod, $encryptionKey, 0, $iv);
192
193
        return base64_encode($iv . '::' . $encryptedSecret);
194
    }
195
196
    /**
197
     * Decrypts the TOTP secret using AES-256-CBC decryption.
198
     */
199
    private function decryptTOTPSecret(string $encryptedSecret, string $encryptionKey): string
200
    {
201
        $cipherMethod = 'aes-256-cbc';
202
        list($iv, $encryptedData) = explode('::', base64_decode($encryptedSecret), 2);
203
204
        return openssl_decrypt($encryptedData, $cipherMethod, $encryptionKey, 0, $iv);
205
    }
206
207
    /**
208
     * Validate the password against the same requirements as the client-side validation.
209
     */
210
    private function validatePassword(string $password): array
0 ignored issues
show
Unused Code introduced by
The method validatePassword() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
211
    {
212
        $errors = [];
213
        $minRequirements = Security::getPasswordRequirements()['min'];
214
215
        if (\strlen($password) < $minRequirements['length']) {
216
            $errors[] = $this->translator->trans('Password must be at least %length% characters long.', ['%length%' => $minRequirements['length']]);
217
        }
218
        if ($minRequirements['lowercase'] > 0 && !preg_match('/[a-z]/', $password)) {
219
            $errors[] = $this->translator->trans('Password must contain at least %count% lowercase characters.', ['%count%' => $minRequirements['lowercase']]);
220
        }
221
        if ($minRequirements['uppercase'] > 0 && !preg_match('/[A-Z]/', $password)) {
222
            $errors[] = $this->translator->trans('Password must contain at least %count% uppercase characters.', ['%count%' => $minRequirements['uppercase']]);
223
        }
224
        if ($minRequirements['numeric'] > 0 && !preg_match('/[0-9]/', $password)) {
225
            $errors[] = $this->translator->trans('Password must contain at least %count% numerical (0-9) characters.', ['%count%' => $minRequirements['numeric']]);
226
        }
227
        if ($minRequirements['specials'] > 0 && !preg_match('/[\W]/', $password)) {
228
            $errors[] = $this->translator->trans('Password must contain at least %count% special characters.', ['%count%' => $minRequirements['specials']]);
229
        }
230
231
        return $errors;
232
    }
233
}
234