evaluatePwnedPasswordCheck()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Extension "fe_change_pwd" for TYPO3 CMS.
7
 *
8
 * For the full copyright and license information, please read the
9
 * LICENSE.txt file that was distributed with this source code.
10
 */
11
12
namespace Derhansen\FeChangePwd\Validation\Validator;
13
14
use Derhansen\FeChangePwd\Domain\Model\Dto\ChangePassword;
15
use Derhansen\FeChangePwd\Service\LocalizationService;
16
use Derhansen\FeChangePwd\Service\OldPasswordService;
17
use Derhansen\FeChangePwd\Service\PwnedPasswordsService;
18
use Derhansen\FeChangePwd\Service\SettingsService;
19
use TYPO3\CMS\Core\Utility\GeneralUtility;
20
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
21
22
/**
23
 * Class ChangePasswordValidator
24
 */
25
class ChangePasswordValidator extends AbstractValidator
26
{
27
    protected array $checks = [
28
        'capitalCharCheck',
29
        'lowerCaseCharCheck',
30
        'digitCheck',
31
        'specialCharCheck',
32
    ];
33
34
    protected SettingsService $settingsService;
35
    protected LocalizationService $localizationService;
36
    protected OldPasswordService $oldPasswordService;
37
    protected PwnedPasswordsService $pwnedPasswordsService;
38
39
    public function __construct(array $options = [])
40
    {
41
        $this->settingsService = GeneralUtility::makeInstance(SettingsService::class);
42
        $this->localizationService = GeneralUtility::makeInstance(LocalizationService::class);
43
        $this->oldPasswordService = GeneralUtility::makeInstance(OldPasswordService::class);
44
        $this->pwnedPasswordsService = GeneralUtility::makeInstance(PwnedPasswordsService::class);
45
        parent::__construct($options);
46
    }
47
48
    /**
49
     * Validates the password of the given ChangePassword object against the configured password complexity
50
     *
51
     * @param ChangePassword $value
52
     *
53
     * @return bool
54
     */
55
    protected function isValid($value): bool
56
    {
57
        $result = true;
58
        $settings = $this->settingsService->getSettings();
59
60
        // Early return if old password is required, but either empty or not valid
61
        if (isset($settings['requireCurrentPassword']['enabled']) &&
62
            (bool)$settings['requireCurrentPassword']['enabled'] &&
63
            !$value->getSkipCurrentPasswordCheck()
64
        ) {
65
            $requireCurrentPasswordResult = $this->evaluateRequireCurrentPassword($value);
66
            if ($requireCurrentPasswordResult === false) {
67
                return false;
68
            }
69
        }
70
71
        // Early return if no passwords are given
72
        if ($value->getPassword1() === '' || $value->getPassword2() === '') {
73
            $this->addError(
74
                $this->localizationService->translate('passwordFieldsEmptyOrNotBothFilledOut'),
75
                1537701950
76
            );
77
78
            return false;
79
        }
80
81
        if ($value->getPassword1() !== $value->getPassword2()) {
82
            $this->addError(
83
                $this->localizationService->translate('passwordsDoNotMatch'),
84
                1537701950
85
            );
86
            // Early return, no other checks need to be done if passwords do not match
87
            return false;
88
        }
89
90
        if (isset($settings['passwordComplexity']['minLength'])) {
91
            $this->evaluateMinLengthCheck($value, (int)$settings['passwordComplexity']['minLength']);
92
        }
93
94
        foreach ($this->checks as $check) {
95
            if (isset($settings['passwordComplexity'][$check]) &&
96
                (bool)$settings['passwordComplexity'][$check]
97
            ) {
98
                $this->evaluatePasswordCheck($value, $check);
99
            }
100
        }
101
102
        if (isset($settings['pwnedpasswordsCheck']['enabled']) && (bool)$settings['pwnedpasswordsCheck']['enabled']) {
103
            $this->evaluatePwnedPasswordCheck($value);
104
        }
105
106
        if (isset($settings['oldPasswordCheck']['enabled']) && (bool)$settings['oldPasswordCheck']['enabled']) {
107
            $this->evaluateOldPasswordCheck($value);
108
        }
109
110
        if ($this->result->hasErrors()) {
111
            $result = false;
112
        }
113
114
        return $result;
115
    }
116
117
    /**
118
     * Checks if the password complexity in regards to minimum password length in met
119
     *
120
     * @param ChangePassword $changePassword
121
     * @param int $minLength
122
     */
123
    protected function evaluateMinLengthCheck(ChangePassword $changePassword, int $minLength): void
124
    {
125
        if (strlen($changePassword->getPassword1()) < $minLength) {
126
            $this->addError(
127
                $this->localizationService->translate('passwordComplexity.failure.minLength', [$minLength]),
128
                1537898028
129
            );
130
        }
131
    }
132
133
    /**
134
     * Evaluates the password complexity in regards to the given check
135
     *
136
     * @param ChangePassword $changePassword
137
     * @param string $check
138
     */
139
    protected function evaluatePasswordCheck(ChangePassword $changePassword, string $check): void
140
    {
141
        $patterns = [
142
            'capitalCharCheck' => '/[A-Z]/',
143
            'lowerCaseCharCheck' => '/[a-z]/',
144
            'digitCheck' => '/[0-9]/',
145
            'specialCharCheck' => '/[^0-9a-z]/i',
146
        ];
147
148
        if (isset($patterns[$check])) {
149
            if (!preg_match($patterns[$check], $changePassword->getPassword1()) > 0) {
150
                $this->addError(
151
                    $this->localizationService->translate('passwordComplexity.failure.' . $check),
152
                    1537898029
153
                );
154
            }
155
        }
156
    }
157
158
    /**
159
     * Evaluates the password using the pwnedpasswords API
160
     *
161
     * @param ChangePassword $changePassword
162
     */
163
    protected function evaluatePwnedPasswordCheck(ChangePassword $changePassword): void
164
    {
165
        $foundCount = $this->pwnedPasswordsService->checkPassword($changePassword->getPassword1());
166
        if ($foundCount > 0) {
167
            $this->addError(
168
                $this->localizationService->translate('pwnedPasswordFailure', [$foundCount]),
169
                1537898030
170
            );
171
        }
172
    }
173
174
    /**
175
     * Evaluates the password against the current password
176
     *
177
     * @param ChangePassword $changePassword
178
     */
179
    protected function evaluateOldPasswordCheck(ChangePassword $changePassword): void
180
    {
181
        if ($this->oldPasswordService->checkEqualsOldPassword(
182
            $changePassword->getPassword1(),
183
            $changePassword->getFeUserPasswordHash()
184
        )) {
185
            $this->addError(
186
                $this->localizationService->translate('oldPasswordFailure'),
187
                1570880406
188
            );
189
        }
190
    }
191
192
    /**
193
     * Evaluates if the current password is not empty and valid
194
     *
195
     * @param ChangePassword $changePassword
196
     * @return bool
197
     */
198
    protected function evaluateRequireCurrentPassword(ChangePassword $changePassword): bool
199
    {
200
        $result = true;
201
        $oldPasswordEmpty = $changePassword->getCurrentPassword() === '';
202
        if ($oldPasswordEmpty) {
203
            $result = false;
204
            $this->addError(
205
                $this->localizationService->translate('currentPasswordEmpty'),
206
                1570880411
207
            );
208
        }
209
210
        if ($oldPasswordEmpty === false &&
211
            !$this->oldPasswordService->checkEqualsOldPassword(
212
                $changePassword->getCurrentPassword(),
213
                $changePassword->getFeUserPasswordHash()
214
            )
215
        ) {
216
            $result = false;
217
            $this->addError(
218
                $this->localizationService->translate('currentPasswordFailure'),
219
                1570880417
220
            );
221
        }
222
        return $result;
223
    }
224
}
225