Passed
Push — master ( 7b8565...12ec71 )
by Magnar Ovedal
03:12
created

GuessableData::getUniqueWords()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use DateTimeInterface;
8
use Stadly\PasswordPolice\Password;
9
use Stadly\PasswordPolice\Policy;
10
use Stadly\PasswordPolice\Rule;
11
use Stadly\PasswordPolice\ValidationError;
12
use Stadly\PasswordPolice\WordConverter;
13
use Traversable;
14
15
final class GuessableData implements Rule
16
{
17
    private const DATE_FORMATS = [
18
        // Year
19
        ['Y'], // 2018
20
21
        // Year month
22
        ['y', 'n'], // 18 8
23
        ['y', 'm'], // 18 08
24
        ['y', 'M'], // 18 Aug
25
        ['y', 'F'], // 18 August
26
27
        // Month year
28
        ['n', 'y'], // 8 18
29
        ['M', 'y'], // Aug 18
30
        ['F', 'y'], // August 18
31
32
        // Day month
33
        ['j', 'n'], // 4 8
34
        ['j', 'm'], // 4 08
35
        ['j', 'M'], // 4 Aug
36
        ['j', 'F'], // 4 August
37
38
        // Month day
39
        ['n', 'j'], // 8 4
40
        ['n', 'd'], // 8 04
41
        ['M', 'j'], // Aug 4
42
        ['M', 'd'], // Aug 04
43
        ['F', 'j'], // August 4
44
        ['F', 'd'], // August 04
45
    ];
46
47
    private const DATE_SEPARATORS = [
48
        '',
49
        '-',
50
        ' ',
51
        '/',
52
        '.',
53
        ',',
54
        '. ',
55
        ', ',
56
    ];
57
58
    /**
59
     * @var (string|DateTimeInterface)[] Guessable data.
60
     */
61
    private $guessableData;
62
63
    /**
64
     * @var WordConverter[] Word converters.
65
     */
66
    private $wordConverters;
67
68
    /**
69
     * @var int Constraint weight.
70
     */
71
    private $weight;
72
73
    /**
74
     * @param (string|DateTimeInterface)[] $guessableData Guessable data.
75
     * @param WordConverter[] $wordConverters Word converters.
76
     * @param int $weight Constraint weight.
77
     */
78 1
    public function __construct(array $guessableData = [], array $wordConverters = [], int $weight = 1)
79
    {
80 1
        $this->guessableData = $guessableData;
81 1
        $this->wordConverters = $wordConverters;
82 1
        $this->weight = $weight;
83 1
    }
84
85
    /**
86
     * Check whether a password is in compliance with the rule.
87
     *
88
     * @param Password|string $password Password to check.
89
     * @param int|null $weight Don't consider constraints with lower weights.
90
     * @return bool Whether the password is in compliance with the rule.
91
     */
92 12
    public function test($password, ?int $weight = 1): bool
93
    {
94 12
        if ($weight !== null && $this->weight < $weight) {
95 1
            return true;
96
        }
97
98 11
        $data = $this->getGuessableData($password);
99
100 11
        return $data === null;
101
    }
102
103
    /**
104
     * Validate that a password is in compliance with the rule.
105
     *
106
     * @param Password|string $password Password to validate.
107
     * @return ValidationError|null Validation error describing why the password is not in compliance with the rule.
108
     */
109 2
    public function validate($password): ?ValidationError
110
    {
111 2
        $data = $this->getGuessableData($password);
112
113 2
        if ($data !== null) {
114 1
            return new ValidationError(
115 1
                $this->getMessage($data),
116 1
                $password,
117 1
                $this,
118 1
                $this->weight
119
            );
120
        }
121
122 1
        return null;
123
    }
124
125
    /**
126
     * @param Password|string $password Password to find guessable data in.
127
     * @return string|DateTimeInterface|null Guessable data in the password.
128
     */
129 13
    private function getGuessableData($password)
130
    {
131 13
        $guessableData = $this->guessableData;
132 13
        if ($password instanceof Password) {
133 12
            $guessableData = array_merge($guessableData, $password->getGuessableData());
134
        }
135
136 13
        foreach ($this->getWordsToCheck((string)$password) as $word) {
137 13
            foreach ($guessableData as $data) {
138 12
                if ($this->contains($word, $data)) {
139 13
                    return $data;
140
                }
141
            }
142
        }
143
144 8
        return null;
145
    }
146
147
    /**
148
     * @param string $word Word to check.
149
     * @return Traversable<string> Variants of the word to check.
150
     */
151 13
    private function getWordsToCheck(string $word): Traversable
152
    {
153 13
        yield from $this->getUniqueWords($this->getConvertedWords($word));
154 8
    }
155
156
    /**
157
     * @param Traversable<string> $words Words to filter.
158
     * @return Traversable<string> Unique words.
159
     */
160 13
    private function getUniqueWords(Traversable $words): Traversable
161
    {
162 13
        $checked = [];
163 13
        foreach ($words as $word) {
164 13
            if (isset($checked[$word])) {
165 2
                continue;
166
            }
167
168 13
            $checked[$word] = true;
169 13
            yield $word;
170
        }
171 8
    }
172
173
    /**
174
     * @param string $word Word to convert.
175
     * @return Traversable<string> Converted words. May contain duplicates.
176
     */
177 13
    private function getConvertedWords(string $word): Traversable
178
    {
179 13
        yield $word;
180
181 8
        foreach ($this->wordConverters as $wordConverter) {
182 4
            foreach ($wordConverter->convert($word) as $converted) {
183 4
                yield $converted;
184
            }
185
        }
186 8
    }
187
188
    /**
189
     * @param string $password Password to check.
190
     * @param string|DateTimeInterface $data Data to check.
191
     * @return bool Whether the password contains the data.
192
     */
193 12
    private function contains(string $password, $data): bool
194
    {
195 12
        if ($data instanceof DateTimeInterface) {
196 6
            return $this->containsDate($password, $data);
197
        }
198
199 7
        return $this->containsString($password, $data);
200
    }
201
202
    /**
203
     * @param string $password Password to check.
204
     * @param string $string String to check.
205
     * @return bool Whether the password contains the string.
206
     */
207 12
    private function containsString(string $password, string $string): bool
208
    {
209 12
        return mb_stripos($password, $string) !== false;
210
    }
211
212
    /**
213
     * @param string $password Password to check.
214
     * @param DateTimeInterface $date Date to check.
215
     * @return bool Whether the password contains the date.
216
     */
217 6
    private function containsDate(string $password, DateTimeInterface $date): bool
218
    {
219 6
        foreach ($this->getDateFormats() as $format) {
220 6
            if ($this->containsString($password, $date->format($format))) {
221 6
                return true;
222
            }
223
        }
224
225 4
        return false;
226
    }
227
228
    /**
229
     * @return iterable<string> Date formats.
230
     */
231 6
    private function getDateFormats(): iterable
232
    {
233 6
        foreach (self::DATE_FORMATS as $format) {
234 6
            foreach (self::DATE_SEPARATORS as $separator) {
235 6
                yield implode($separator, $format);
236
            }
237
        }
238 4
    }
239
240
    /**
241
     * @param string|DateTimeInterface $data Data that violates the constraint.
242
     * @return string Message explaining the violation.
243
     */
244 1
    private function getMessage($data): string
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

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

244
    private function getMessage(/** @scrutinizer ignore-unused */ $data): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
245
    {
246 1
        $translator = Policy::getTranslator();
247
248 1
        return $translator->trans(
249 1
            'Must not contain guessable data.'
250
        );
251
    }
252
}
253