Completed
Push — 4 ( dac247...1c9660 )
by Damian
29s
created

PasswordValidator::getHistoricCount()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\Core\Extensible;
7
use SilverStripe\Core\Injector\Injectable;
8
use SilverStripe\Dev\Deprecation;
9
use SilverStripe\ORM\ValidationResult;
10
11
/**
12
 * This class represents a validator for member passwords.
13
 *
14
 * <code>
15
 * $pwdVal = new PasswordValidator();
16
 * $pwdValidator->minLength(7);
17
 * $pwdValidator->checkHistoricalPasswords(6);
18
 * $pwdValidator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
19
 *
20
 * Member::set_password_validator($pwdValidator);
21
 * </code>
22
 */
23
class PasswordValidator
24
{
25
    use Injectable;
26
    use Configurable;
27
    use Extensible;
28
29
    /**
30
     * @config
31
     * @var array
32
     */
33
    private static $character_strength_tests = [
34
        'lowercase' => '/[a-z]/',
35
        'uppercase' => '/[A-Z]/',
36
        'digits' => '/[0-9]/',
37
        'punctuation' => '/[^A-Za-z0-9]/',
38
    ];
39
40
    /**
41
     * @config
42
     * @var integer
43
     */
44
    private static $min_length = null;
45
46
    /**
47
     * @config
48
     * @var integer
49
     */
50
    private static $min_test_score = null;
51
52
    /**
53
     * @config
54
     * @var integer
55
     */
56
    private static $historic_count = null;
57
58
    /**
59
     * @var integer
60
     */
61
    protected $minLength = null;
62
63
    /**
64
     * @var integer
65
     */
66
    protected $minScore = null;
67
68
    /**
69
     * @var array
70
     */
71
    protected $testNames = null;
72
73
    /**
74
     * @var integer
75
     */
76
    protected $historicalPasswordCount = null;
77
78
    /**
79
     * @deprecated 5.0
80
     * Minimum password length
81
     *
82
     * @param int $minLength
83
     * @return $this
84
     */
85
    public function minLength($minLength)
86
    {
87
        Deprecation::notice('5.0', 'Use ->setMinLength($value) instead.');
88
        return $this->setMinLength($minLength);
89
    }
90
91
    /**
92
     * @deprecated 5.0
93
     * Check the character strength of the password.
94
     *
95
     * Eg: $this->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"))
96
     *
97
     * @param int $minScore The minimum number of character tests that must pass
98
     * @param array $testNames The names of the tests to perform
99
     * @return $this
100
     */
101
    public function characterStrength($minScore, $testNames = null)
102
    {
103
104
        Deprecation::notice(
105
            '5.0',
106
            'Use ->setMinTestScore($score) and ->setTextNames($names) instead.'
107
        );
108
        return $this->setMinTestScore($minScore)
109
            ->setTestNames($testNames);
110
    }
111
112
    /**
113
     * @deprecated 5.0
114
     * Check a number of previous passwords that the user has used, and don't let them change to that.
115
     *
116
     * @param int $count
117
     * @return $this
118
     */
119
    public function checkHistoricalPasswords($count)
120
    {
121
        Deprecation::notice('5.0', 'Use ->setHistoricCount($value) instead.');
122
        return $this->setHistoricCount($count);
123
    }
124
125
    /**
126
     * @return integer
127
     */
128
    public function getMinLength()
129
    {
130
        if ($this->minLength !== null) {
0 ignored issues
show
introduced by
The condition $this->minLength !== null can never be false.
Loading history...
131
            return $this->minLength;
132
        }
133
        return $this->config()->get('min_length');
134
    }
135
136
    /**
137
     * @param $minLength
138
     * @return $this
139
     */
140
    public function setMinLength($minLength)
141
    {
142
        $this->minLength = $minLength;
143
        return $this;
144
    }
145
146
    /**
147
     * @return integer
148
     */
149
    public function getMinTestScore()
150
    {
151
        if ($this->minScore !== null) {
0 ignored issues
show
introduced by
The condition $this->minScore !== null can never be false.
Loading history...
152
            return $this->minScore;
153
        }
154
        return $this->config()->get('min_test_score');
155
    }
156
157
    /**
158
     * @param $minScore
159
     * @return $this
160
     */
161
    public function setMinTestScore($minScore)
162
    {
163
        $this->minScore = $minScore;
164
        return $this;
165
    }
166
167
    /**
168
     * @return array
169
     */
170
    public function getTestNames()
171
    {
172
        if ($this->testNames !== null) {
0 ignored issues
show
introduced by
The condition $this->testNames !== null can never be false.
Loading history...
173
            return $this->testNames;
174
        }
175
        return array_keys(array_filter($this->getTests()));
176
    }
177
178
    /**
179
     * @param $testNames
180
     * @return $this
181
     */
182
    public function setTestNames($testNames)
183
    {
184
        $this->testNames = $testNames;
185
        return $this;
186
    }
187
188
    /**
189
     * @return integer
190
     */
191
    public function getHistoricCount()
192
    {
193
        if ($this->historicalPasswordCount !== null) {
0 ignored issues
show
introduced by
The condition $this->historicalPasswordCount !== null can never be false.
Loading history...
194
            return $this->historicalPasswordCount;
195
        }
196
        return $this->config()->get('historic_count');
197
    }
198
199
    /**
200
     * @param $count
201
     * @return $this
202
     */
203
    public function setHistoricCount($count)
204
    {
205
        $this->historicalPasswordCount = $count;
206
        return $this;
207
    }
208
209
    /**
210
     * @return array
211
     */
212
    public function getTests()
213
    {
214
        return $this->config()->get('character_strength_tests');
215
    }
216
217
    /**
218
     * @param String $password
219
     * @param Member $member
220
     * @return ValidationResult
221
     */
222
    public function validate($password, $member)
223
    {
224
        $valid = ValidationResult::create();
225
226
        $minLength = $this->getMinLength();
227
        if ($minLength && strlen($password) < $minLength) {
228
            $error = _t(
229
                'SilverStripe\\Security\\PasswordValidator.TOOSHORT',
230
                'Password is too short, it must be {minimum} or more characters long',
231
                ['minimum' => $this->minLength]
232
            );
233
234
            $valid->addError($error, 'bad', 'TOO_SHORT');
235
        }
236
237
        $minTestScore = $this->getMinTestScore();
238
        if ($minTestScore) {
239
            $missedTests = [];
240
            $testNames = $this->getTestNames();
241
            $tests = $this->getTests();
242
243
            foreach ($testNames as $name) {
244
                if (preg_match($tests[$name], $password)) {
245
                    continue;
246
                }
247
                $missedTests[] = _t(
248
                    'SilverStripe\\Security\\PasswordValidator.STRENGTHTEST' . strtoupper($name),
249
                    $name,
250
                    'The user needs to add this to their password for more complexity'
251
                );
252
            }
253
254
            $score = count($this->testNames) - count($missedTests);
255
            if ($score < $minTestScore) {
256
                $error = _t(
257
                    'SilverStripe\\Security\\PasswordValidator.LOWCHARSTRENGTH',
258
                    'Please increase password strength by adding some of the following characters: {chars}',
259
                    ['chars' => implode(', ', $missedTests)]
260
                );
261
                $valid->addError($error, 'bad', 'LOW_CHARACTER_STRENGTH');
262
            }
263
        }
264
265
        $historicCount = $this->getHistoricCount();
266
        if ($historicCount) {
267
            $previousPasswords = MemberPassword::get()
268
                ->where(array('"MemberPassword"."MemberID"' => $member->ID))
269
                ->sort('"Created" DESC, "ID" DESC')
270
                ->limit($historicCount);
271
            /** @var MemberPassword $previousPassword */
272
            foreach ($previousPasswords as $previousPassword) {
273
                if ($previousPassword->checkPassword($password)) {
274
                    $error =  _t(
275
                        'SilverStripe\\Security\\PasswordValidator.PREVPASSWORD',
276
                        'You\'ve already used that password in the past, please choose a new password'
277
                    );
278
                    $valid->addError($error, 'bad', 'PREVIOUS_PASSWORD');
279
                    break;
280
                }
281
            }
282
        }
283
284
        $this->extend('updateValidatePassword', $password, $member, $valid, $this);
285
286
        return $valid;
287
    }
288
}
289