Passed
Push — master ( 08a7b5...bf65f8 )
by Magnar Ovedal
03:29 queued 42s
created

GuessableDataRule::containsString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
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 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\WordFormatter;
13
use Traversable;
14
15
final class GuessableDataRule 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 WordFormatter[] Word formatters.
65
     */
66
    private $wordFormatters;
67
68
    /**
69
     * @var int Constraint weight.
70
     */
71
    private $weight;
72
73
    /**
74
     * @param (string|DateTimeInterface)[] $guessableData Guessable data.
75
     * @param WordFormatter[] $wordFormatters Word formatters.
76
     * @param int $weight Constraint weight.
77
     */
78 1
    public function __construct(array $guessableData = [], array $wordFormatters = [], int $weight = 1)
79
    {
80 1
        $this->guessableData = $guessableData;
81 1
        $this->wordFormatters = $wordFormatters;
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->getFormattedWords((string)$password) as $word) {
137 13
            foreach ($guessableData as $data) {
138 12
                if ($this->contains($word, $data)) {
0 ignored issues
show
Bug introduced by
It seems like $word can also be of type Traversable; however, parameter $password of Stadly\PasswordPolice\Ru...bleDataRule::contains() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

138
                if ($this->contains(/** @scrutinizer ignore-type */ $word, $data)) {
Loading history...
139 13
                    return $data;
140
                }
141
            }
142
        }
143
144 8
        return null;
145
    }
146
147
    /**
148
     * @param string $word Word to format.
149
     * @return Traversable<string> Formatted words. May contain duplicates.
150
     */
151 13
    private function getFormattedWords(string $word): Traversable
152
    {
153 13
        yield $word;
154
155 8
        foreach ($this->wordFormatters as $wordFormatter) {
156 4
            yield from $wordFormatter->apply([$word]);
157
        }
158 8
    }
159
160
    /**
161
     * @param string $password Password to check.
162
     * @param string|DateTimeInterface $data Data to check.
163
     * @return bool Whether the password contains the data.
164
     */
165 12
    private function contains(string $password, $data): bool
166
    {
167 12
        if ($data instanceof DateTimeInterface) {
168 6
            return $this->containsDate($password, $data);
169
        }
170
171 7
        return $this->containsString($password, $data);
172
    }
173
174
    /**
175
     * @param string $password Password to check.
176
     * @param string $string String to check.
177
     * @return bool Whether the password contains the string.
178
     */
179 12
    private function containsString(string $password, string $string): bool
180
    {
181 12
        return mb_stripos($password, $string) !== false;
182
    }
183
184
    /**
185
     * @param string $password Password to check.
186
     * @param DateTimeInterface $date Date to check.
187
     * @return bool Whether the password contains the date.
188
     */
189 6
    private function containsDate(string $password, DateTimeInterface $date): bool
190
    {
191 6
        foreach ($this->getDateFormats() as $format) {
192 6
            if ($this->containsString($password, $date->format($format))) {
193 6
                return true;
194
            }
195
        }
196
197 4
        return false;
198
    }
199
200
    /**
201
     * @return iterable<string> Date formats.
202
     */
203 6
    private function getDateFormats(): iterable
204
    {
205 6
        foreach (self::DATE_FORMATS as $format) {
206 6
            foreach (self::DATE_SEPARATORS as $separator) {
207 6
                yield implode($separator, $format);
208
            }
209
        }
210 4
    }
211
212
    /**
213
     * @param string|DateTimeInterface $data Data that violates the constraint.
214
     * @return string Message explaining the violation.
215
     */
216 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

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