Passed
Push — master ( bab290...961f54 )
by Magnar Ovedal
03:07
created

GuessableData   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 195
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 66
dl 0
loc 195
ccs 52
cts 52
cp 1
rs 10
c 0
b 0
f 0
wmc 25

11 Methods

Rating   Name   Duplication   Size   Complexity  
A contains() 0 7 2
A containsString() 0 3 1
A getWordsToCheck() 0 10 3
A getDateFormats() 0 5 3
A getMessage() 0 6 1
A containsDate() 0 9 3
A getConvertedWords() 0 7 3
A __construct() 0 3 1
A enforce() 0 6 2
A test() 0 5 1
A getGuessableData() 0 12 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use DateTimeInterface;
8
use InvalidArgumentException;
9
use RuntimeException;
10
use Traversable;
11
use Stadly\PasswordPolice\Password;
12
use Stadly\PasswordPolice\Policy;
13
use Stadly\PasswordPolice\WordConverter\WordConverterInterface;
14
use Stadly\PasswordPolice\WordList\WordListInterface;
15
16
final class GuessableData implements RuleInterface
17
{
18
    private const DATE_FORMATS = [
19
        // Year
20
        ['Y'], // 2018
21
22
        // Year month
23
        ['y', 'n'], // 18 8
24
        ['y', 'm'], // 18 08
25
        ['y', 'M'], // 18 Aug
26
        ['y', 'F'], // 18 August
27
28
        // Month year
29
        ['n', 'y'], // 8 18
30
        ['M', 'y'], // Aug 18
31
        ['F', 'y'], // August 18
32
33
        // Day month
34
        ['j', 'n'], // 4 8
35
        ['j', 'm'], // 4 08
36
        ['j', 'M'], // 4 Aug
37
        ['j', 'F'], // 4 August
38
39
        // Month day
40
        ['n', 'j'], // 8 4
41
        ['n', 'd'], // 8 04
42
        ['M', 'j'], // Aug 4
43
        ['M', 'd'], // Aug 04
44
        ['F', 'j'], // August 4
45
        ['F', 'd'], // August 04
46
    ];
47
48
    private const DATE_SEPARATORS = [
49
        '',
50
        '-',
51
        ' ',
52
        '/',
53
        '.',
54
        ',',
55
        '. ',
56
        ', ',
57
    ];
58
59
    /**
60
     * @var WordConverterInterface[] Word converters.
61
     */
62
    private $wordConverters;
63
64
    /**
65
     * @param WordConverterInterface... $wordConverters Word converters.
0 ignored issues
show
Documentation Bug introduced by
The doc comment WordConverterInterface... at position 0 could not be parsed: Unknown type name 'WordConverterInterface...' at position 0 in WordConverterInterface....
Loading history...
66
     */
67 1
    public function __construct(WordConverterInterface... $wordConverters)
68
    {
69 1
        $this->wordConverters = $wordConverters;
70 1
    }
71
72
    /**
73
     * Check whether a password is in compliance with the rule.
74
     *
75
     * @param Password|string $password Password to check.
76
     * @return bool Whether the password is in compliance with the rule.
77
     */
78 9
    public function test($password): bool
79
    {
80 9
        $data = $this->getGuessableData($password);
81
82 9
        return $data === null;
83
    }
84
85
    /**
86
     * Enforce that a password is in compliance with the rule.
87
     *
88
     * @param Password|string $password Password that must adhere to the rule.
89
     * @throws RuleException If the password does not adhrere to the rule.
90
     */
91 2
    public function enforce($password): void
92
    {
93 2
        $data = $this->getGuessableData($password);
94
95 2
        if ($data !== null) {
96 1
            throw new RuleException($this, $this->getMessage());
97
        }
98 1
    }
99
100
    /**
101
     * @param Password|string $password Password to find guessable data in.
102
     * @return string|DateTimeInterface|null Guessable data in the password.
103
     */
104 11
    private function getGuessableData($password)
105
    {
106 11
        if ($password instanceof Password) {
107 10
            foreach ($this->getWordsToCheck((string)$password) as $word) {
108 10
                foreach ($password->getGuessableData() as $data) {
109 10
                    if ($this->contains($word, $data)) {
110 10
                        return $data;
111
                    }
112
                }
113
            }
114
        }
115 8
        return null;
116
    }
117
118
    /**
119
     * @param string $word Word to check.
120
     * @return Traversable<string> Variants of the word to check.
121
     */
122 10
    private function getWordsToCheck(string $word): Traversable
123
    {
124 10
        $checked = [];
125 10
        foreach ($this->getConvertedWords($word) as $wordToCheck) {
126 10
            if (isset($checked[$wordToCheck])) {
127 2
                continue;
128
            }
129
130 10
            $checked[$wordToCheck] = true;
131 10
            yield $wordToCheck;
132
        }
133 7
    }
134
135
    /**
136
     * @param string $word Word to convert.
137
     * @return Traversable<string> Converted words. May contain duplicates.
138
     */
139 10
    private function getConvertedWords(string $word): Traversable
140
    {
141 10
        yield $word;
142
143 7
        foreach ($this->wordConverters as $wordConverter) {
144 4
            foreach ($wordConverter->convert($word) as $converted) {
145 4
                yield $converted;
146
            }
147
        }
148 7
    }
149
150
    /**
151
     * @param string $password Password to check.
152
     * @param string|DateTimeInterface $data Data to check.
153
     * @return bool Whether the password contains the data.
154
     */
155 10
    private function contains(string $password, $data): bool
156
    {
157 10
        if ($data instanceof DateTimeInterface) {
158 5
            return $this->containsDate($password, $data);
159
        }
160
161 6
        return $this->containsString($password, $data);
162
    }
163
164
    /**
165
     * @param string $password Password to check.
166
     * @param string $string String to check.
167
     * @return bool Whether the password contains the string.
168
     */
169 10
    private function containsString(string $password, string $string): bool
170
    {
171 10
        return false !== mb_stripos($password, $string);
172
    }
173
174
    /**
175
     * @param string $password Password to check.
176
     * @param DateTimeInterface $date Date to check.
177
     * @return bool Whether the password contains the date.
178
     */
179 5
    private function containsDate(string $password, DateTimeInterface $date): bool
180
    {
181 5
        foreach ($this->getDateFormats() as $format) {
182 5
            if ($this->containsString($password, $date->format($format))) {
183 5
                return true;
184
            }
185
        }
186
187 4
        return false;
188
    }
189
190
    /**
191
     * @return iterable<string> Date formats.
192
     */
193 5
    private function getDateFormats(): iterable
194
    {
195 5
        foreach (self::DATE_FORMATS as $format) {
196 5
            foreach (self::DATE_SEPARATORS as $separator) {
197 5
                yield implode($separator, $format);
198
            }
199
        }
200 4
    }
201
202
    /**
203
     * {@inheritDoc}
204
     */
205 1
    public function getMessage(): string
206
    {
207 1
        $translator = Policy::getTranslator();
208
209 1
        return $translator->trans(
210 1
            'Must not contain guessable data.'
211
        );
212
    }
213
}
214