Passed
Push — master ( 12ec71...6a4e7a )
by Magnar Ovedal
02:59
created

Dictionary::getMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
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\Rule;
11
use Stadly\PasswordPolice\ValidationError;
12
use Stadly\PasswordPolice\WordFormatter;
13
use Stadly\PasswordPolice\WordList;
14
use Traversable;
15
16
final class Dictionary implements Rule
17
{
18
    /**
19
     * @var WordList Word list for the dictionary.
20
     */
21
    private $wordList;
22
23
    /**
24
     * @var int Minimum word length to consider.
25
     */
26
    private $minWordLength;
27
28
    /**
29
     * @var int|null Maximum word length to consider.
30
     */
31
    private $maxWordLength;
32
33
    /**
34
     * @var WordFormatter[] Word formatters.
35
     */
36
    private $wordFormatters;
37
38
    /**
39
     * @var int Constraint weight.
40
     */
41
    private $weight;
42
43
    /**
44
     * @param WordList $wordList Word list for the dictionary.
45
     * @param int $minWordLength Ignore words shorter than this.
46
     * @param int|null $maxWordLength Ignore words longer than this.
47
     * @param WordFormatter[] $wordFormatters Word formatters.
48
     * @param int $weight Constraint weight.
49
     */
50 7
    public function __construct(
51
        WordList $wordList,
52
        int $minWordLength = 3,
53
        ?int $maxWordLength = 25,
54
        array $wordFormatters = [],
55
        int $weight = 1
56
    ) {
57 7
        if ($minWordLength < 1) {
58 2
            throw new InvalidArgumentException('Minimum word length must be positive.');
59
        }
60 5
        if ($maxWordLength !== null && $maxWordLength < $minWordLength) {
61 1
            throw new InvalidArgumentException('Maximum word length cannot be smaller than mininum word length.');
62
        }
63
64 4
        $this->wordList = $wordList;
65 4
        $this->minWordLength = $minWordLength;
66 4
        $this->maxWordLength = $maxWordLength;
67 4
        $this->wordFormatters = $wordFormatters;
68 4
        $this->weight = $weight;
69 4
    }
70
71
    /**
72
     * @return WordList Word list for the dictionary.
73
     */
74 1
    public function getWordList(): WordList
75
    {
76 1
        return $this->wordList;
77
    }
78
79
    /**
80
     * @return int Minimum word length to consider.
81
     */
82 1
    public function getMinWordLength(): int
83
    {
84 1
        return $this->minWordLength;
85
    }
86
87
    /**
88
     * @return int|null Maximum word length to consider.
89
     */
90 1
    public function getMaxWordLength(): ?int
91
    {
92 1
        return $this->maxWordLength;
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     */
98 11
    public function test($password, ?int $weight = 1): bool
99
    {
100 11
        if ($weight !== null && $this->weight < $weight) {
101 1
            return true;
102
        }
103
104 10
        $word = $this->getDictionaryWord((string)$password);
105
106 9
        return $word === null;
107
    }
108
109
    /**
110
     * {@inheritDoc}
111
     */
112 2
    public function validate($password): ?ValidationError
113
    {
114 2
        $word = $this->getDictionaryWord((string)$password);
115
116 2
        if ($word !== null) {
117 1
            return new ValidationError(
118 1
                $this->getMessage($word),
119 1
                $password,
120 1
                $this,
121 1
                $this->weight
122
            );
123
        }
124
125 1
        return null;
126
    }
127
128
    /**
129
     * @param string $password Password to find dictionary words in.
130
     * @return string|null Dictionary word in the password.
131
     * @throws Exception If an error occurred.
132
     */
133 12
    private function getDictionaryWord(string $password): ?string
134
    {
135 12
        foreach ($this->getWordsToCheck($password) as $word) {
136
            try {
137 10
                if ($this->wordList->contains($word)) {
138 9
                    return $word;
139
                }
140 1
            } catch (RuntimeException $exception) {
141 1
                throw new Exception(
142 1
                    $this,
143 1
                    'An error occurred while using the word list: '.$exception->getMessage(),
144 7
                    $exception
145
                );
146
            }
147
        }
148 8
        return null;
149
    }
150
151
    /**
152
     * @param string $word Word to check.
153
     * @return Traversable<string> Variants of the word to check.
154
     */
155 12
    private function getWordsToCheck(string $word): Traversable
156
    {
157 12
        $formattedWords = $this->getUniqueWords($this->getFormattedWords($word));
158
159 12
        foreach ($formattedWords as $formattedWord) {
160 12
            if ($this->minWordLength <= mb_strlen($formattedWord) &&
161 12
               ($this->maxWordLength === null || mb_strlen($formattedWord) <= $this->maxWordLength)
162
            ) {
163 12
                yield $formattedWord;
164
            }
165
        }
166 8
    }
167
168
    /**
169
     * @param Traversable<string> $words Words to filter.
170
     * @return Traversable<string> Unique words.
171
     */
172 12
    private function getUniqueWords(Traversable $words): Traversable
173
    {
174 12
        $checked = [];
175 12
        foreach ($words as $word) {
176 12
            if (isset($checked[$word])) {
177 1
                continue;
178
            }
179
180 12
            $checked[$word] = true;
181 12
            yield $word;
182
        }
183 8
    }
184
185
    /**
186
     * @param string $word Word to format.
187
     * @return Traversable<string> Formatted words. May contain duplicates.
188
     */
189 12
    private function getFormattedWords(string $word): Traversable
190
    {
191 12
        yield $word;
192
193 8
        foreach ($this->wordFormatters as $wordFormatter) {
194 2
            foreach ($wordFormatter->apply($word) as $formatted) {
195 2
                yield $formatted;
196
            }
197
        }
198 8
    }
199
200
    /**
201
     * @param string $word Word that violates the constraint.
202
     * @return string Message explaining the violation.
203
     */
204 1
    private function getMessage(string $word): string
0 ignored issues
show
Unused Code introduced by
The parameter $word 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

204
    private function getMessage(/** @scrutinizer ignore-unused */ string $word): 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...
205
    {
206 1
        $translator = Policy::getTranslator();
207
208 1
        return $translator->trans(
209 1
            'Must not contain dictionary words.'
210
        );
211
    }
212
}
213