Completed
Push — master ( 813ab5...8e7281 )
by Magnar Ovedal
03:45
created

Dictionary::getDictionaryWordCheckSubstrings()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 4
nop 1
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 4
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 InvalidArgumentException;
8
use RuntimeException;
9
use Stadly\PasswordPolice\Policy;
10
use Stadly\PasswordPolice\WordList\WordListInterface;
11
12
final class Dictionary implements RuleInterface
13
{
14
    /**
15
     * @var WordListInterface Word list for the dictionary.
16
     */
17
    private $wordList;
18
19
    /**
20
     * @var int Minimum word length to consider.
21
     */
22
    private $minWordLength;
23
24
    /**
25
     * @var int|null Maximum word length to consider.
26
     */
27
    private $maxWordLength;
28
29
    /**
30
     * @var bool Whether all substrings of the password should be checked.
31
     */
32
    private $checkSubstrings;
33
34
    /**
35
     * @param WordListInterface $wordList Word list for the dictionary.
36
     * @param int $minWordLength Ignore words shorter than this.
37
     * @param int|null $maxWordLength Ignore words longer than this.
38
     * @param bool $checkSubstrings Check all substrings of the password, not just the whole password.
39
     */
40 7
    public function __construct(
41
        WordListInterface $wordList,
42
        int $minWordLength = 3,
43
        ?int $maxWordLength = 25,
44
        bool $checkSubstrings = true
45
    ) {
46 7
        if ($minWordLength < 1) {
47 2
            throw new InvalidArgumentException('Minimum word length must be positive.');
48
        }
49 5
        if ($maxWordLength !== null && $maxWordLength < $minWordLength) {
50 1
            throw new InvalidArgumentException('Maximum word length cannot be smaller than mininum word length.');
51
        }
52
53 4
        $this->wordList = $wordList;
54 4
        $this->minWordLength = $minWordLength;
55 4
        $this->maxWordLength = $maxWordLength;
56 4
        $this->checkSubstrings = $checkSubstrings;
57 4
    }
58
59
    /**
60
     * @return WordListInterface Word list for the dictionary.
61
     */
62 1
    public function getWordList(): WordListInterface
63
    {
64 1
        return $this->wordList;
65
    }
66
67
    /**
68
     * @return int Minimum word length to consider.
69
     */
70 1
    public function getMinWordLength(): int
71
    {
72 1
        return $this->minWordLength;
73
    }
74
75
    /**
76
     * @return int|null Maximum word length to consider.
77
     */
78 1
    public function getMaxWordLength(): ?int
79
    {
80 1
        return $this->maxWordLength;
81
    }
82
83
    /**
84
     * {@inheritDoc}
85
     */
86 15
    public function test($password): bool
87
    {
88 15
        $word = $this->getDictionaryWord((string)$password);
89
90 14
        return $word === null;
91
    }
92
93
    /**
94
     * {@inheritDoc}
95
     */
96 2
    public function enforce($password): void
97
    {
98 2
        $word = $this->getDictionaryWord((string)$password);
99
100 2
        if ($word !== null) {
101 1
            throw new RuleException($this, $this->getMessage());
102
        }
103 1
    }
104
105
    /**
106
     * @param string $password Password to find dictionary words in.
107
     * @return string|null Dictionary word in the password.
108
     * @throws TestException If an error occurred while using the word list.
109
     */
110 17
    private function getDictionaryWord(string $password): ?string
111
    {
112 17
        if ($this->checkSubstrings) {
113 10
            return $this->getDictionaryWordCheckSubstrings($password);
114
        }
115
116 7
        return $this->getDictionaryWordCheckWord($password);
117
    }
118
119
    /**
120
     * @param string $password Password to find dictionary words in.
121
     * @return string|null Dictionary word in the password.
122
     * @throws TestException If an error occurred while using the word list.
123
     */
124 7
    private function getDictionaryWordCheckWord(string $password): ?string
125
    {
126 7
        if (mb_strlen($password) < $this->minWordLength) {
127 1
            return null;
128
        }
129
130 6
        if ($this->maxWordLength !== null && $this->maxWordLength < mb_strlen($password)) {
131 1
            return null;
132
        }
133
134 5
        if ($this->wordListContains($password)) {
135 2
            return $password;
136
        }
137
138 3
        return null;
139
    }
140
141
    /**
142
     * @param string $password Password to find dictionary words in.
143
     * @return string|null Dictionary word in the password.
144
     * @throws TestException If an error occurred while using the word list.
145
     */
146 10
    private function getDictionaryWordCheckSubstrings(string $password): ?string
147
    {
148 10
        for ($start = 0; $start < mb_strlen($password); ++$start) {
149 10
            $word = mb_substr($password, $start, $this->maxWordLength);
150
151 10
            for ($wordLength = mb_strlen($word); $this->minWordLength <= $wordLength; --$wordLength) {
152 9
                $word = mb_substr($word, 0, $wordLength);
153
154 9
                if ($this->wordListContains($word)) {
155 6
                    return $word;
156
                }
157
            }
158
        }
159
160 3
        return null;
161
    }
162
163
    /**
164
     * @param string $word Word to check.
165
     * @return bool Whether the word list contains the word.
166
     * @throws TestException If an error occurred while using the word list.
167
     */
168 14
    private function wordListContains(string $word): bool
169
    {
170
        try {
171 14
            if ($this->wordList->contains($word)) {
172 13
                return true;
173
            }
174 1
        } catch (RuntimeException $exception) {
175 1
            throw new TestException($this, 'An error occurred while using the word list.', $exception);
176
        }
177
178 8
        return false;
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 2
    public function getMessage(): string
185
    {
186 2
        $translator = Policy::getTranslator();
187
188 2
        if ($this->checkSubstrings) {
189 1
            return $translator->trans(
190 1
                'Must not contain dictionary words.'
191
            );
192
        } else {
193 1
            return $translator->trans(
194 1
                'Must not be a dictionary word.'
195
            );
196
        }
197
    }
198
}
199