Passed
Push — master ( e559b6...f8119e )
by Magnar Ovedal
05:45 queued 02:46
created

GuessableDataRule::test()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 2
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 3
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 Stadly\PasswordPolice\WordFormatter\FormatterCombiner;
14
use Traversable;
15
16
final class GuessableDataRule implements Rule
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 (string|DateTimeInterface)[] Guessable data.
61
     */
62
    private $guessableData;
63
64
    /**
65
     * @var WordFormatter Word formatter.
66
     */
67
    private $wordFormatter;
68
69
    /**
70
     * @var int Constraint weight.
71
     */
72
    private $weight;
73
74
    /**
75
     * @param (string|DateTimeInterface)[] $guessableData Guessable data.
76
     * @param WordFormatter[] $wordFormatters Word formatters.
77
     * @param int $weight Constraint weight.
78
     */
79 1
    public function __construct(array $guessableData = [], array $wordFormatters = [], int $weight = 1)
80
    {
81 1
        $this->guessableData = $guessableData;
82 1
        $this->wordFormatter = new FormatterCombiner($wordFormatters);
83 1
        $this->weight = $weight;
84 1
    }
85
86
    /**
87
     * Check whether a password is in compliance with the rule.
88
     *
89
     * @param Password|string $password Password to check.
90
     * @param int|null $weight Don't consider constraints with lower weights.
91
     * @return bool Whether the password is in compliance with the rule.
92
     */
93 12
    public function test($password, ?int $weight = 1): bool
94
    {
95 12
        if ($weight !== null && $this->weight < $weight) {
96 1
            return true;
97
        }
98
99 11
        $data = $this->getGuessableData($password);
100
101 11
        return $data === null;
102
    }
103
104
    /**
105
     * Validate that a password is in compliance with the rule.
106
     *
107
     * @param Password|string $password Password to validate.
108
     * @return ValidationError|null Validation error describing why the password is not in compliance with the rule.
109
     */
110 2
    public function validate($password): ?ValidationError
111
    {
112 2
        $data = $this->getGuessableData($password);
113
114 2
        if ($data !== null) {
115 1
            return new ValidationError(
116 1
                $this->getMessage($data),
117 1
                $password,
118 1
                $this,
119 1
                $this->weight
120
            );
121
        }
122
123 1
        return null;
124
    }
125
126
    /**
127
     * @param Password|string $password Password to find guessable data in.
128
     * @return string|DateTimeInterface|null Guessable data in the password.
129
     */
130 13
    private function getGuessableData($password)
131
    {
132 13
        $guessableData = $this->guessableData;
133 13
        if ($password instanceof Password) {
134 12
            $guessableData = array_merge($guessableData, $password->getGuessableData());
135
        }
136
137 13
        foreach ($this->wordFormatter->apply([(string)$password]) as $word) {
138 13
            foreach ($guessableData as $data) {
139 12
                if ($this->contains($word, $data)) {
140 13
                    return $data;
141
                }
142
            }
143
        }
144
145 8
        return null;
146
    }
147
148
    /**
149
     * @param string $password Password to check.
150
     * @param string|DateTimeInterface $data Data to check.
151
     * @return bool Whether the password contains the data.
152
     */
153 12
    private function contains(string $password, $data): bool
154
    {
155 12
        if ($data instanceof DateTimeInterface) {
156 6
            return $this->containsDate($password, $data);
157
        }
158
159 7
        return $this->containsString($password, $data);
160
    }
161
162
    /**
163
     * @param string $password Password to check.
164
     * @param string $string String to check.
165
     * @return bool Whether the password contains the string.
166
     */
167 12
    private function containsString(string $password, string $string): bool
168
    {
169 12
        return mb_stripos($password, $string) !== false;
170
    }
171
172
    /**
173
     * @param string $password Password to check.
174
     * @param DateTimeInterface $date Date to check.
175
     * @return bool Whether the password contains the date.
176
     */
177 6
    private function containsDate(string $password, DateTimeInterface $date): bool
178
    {
179 6
        foreach ($this->getDateFormats() as $format) {
180 6
            if ($this->containsString($password, $date->format($format))) {
181 6
                return true;
182
            }
183
        }
184
185 4
        return false;
186
    }
187
188
    /**
189
     * @return iterable<string> Date formats.
190
     */
191 6
    private function getDateFormats(): iterable
192
    {
193 6
        foreach (self::DATE_FORMATS as $format) {
194 6
            foreach (self::DATE_SEPARATORS as $separator) {
195 6
                yield implode($separator, $format);
196
            }
197
        }
198 4
    }
199
200
    /**
201
     * @param string|DateTimeInterface $data Data that violates the constraint.
202
     * @return string Message explaining the violation.
203
     */
204 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

204
    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...
205
    {
206 1
        $translator = Policy::getTranslator();
207
208 1
        return $translator->trans(
209 1
            'Must not contain guessable data.'
210
        );
211
    }
212
}
213