Completed
Push — master ( 323b38...f8d35a )
by Magnar Ovedal
03:57
created

GuessableData::validate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

218
    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...
219
    {
220 1
        $translator = Policy::getTranslator();
221
222 1
        return $translator->trans(
223 1
            'Must not contain guessable data.'
224
        );
225
    }
226
}
227