NoReuseRule   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 148
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 45
c 1
b 0
f 0
dl 0
loc 148
ccs 50
cts 50
cp 1
rs 10
wmc 19

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getViolation() 0 14 6
A getPositions() 0 16 5
A getMessage() 0 12 2
A validate() 0 15 2
A test() 0 6 1
A getHashFunction() 0 3 1
A __construct() 0 4 1
A addConstraint() 0 9 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use StableSort\StableSort;
8
use Stadly\PasswordPolice\Constraint\PositionConstraint;
9
use Stadly\PasswordPolice\HashFunction;
10
use Stadly\PasswordPolice\Password;
11
use Stadly\PasswordPolice\Rule;
12
use Stadly\PasswordPolice\ValidationError;
13
use Symfony\Contracts\Translation\LocaleAwareInterface;
14
use Symfony\Contracts\Translation\TranslatorInterface;
15
16
final class NoReuseRule implements Rule
17
{
18
    /**
19
     * @var HashFunction Hash function.
20
     */
21
    private $hashFunction;
22
23
    /**
24
     * @var array<PositionConstraint> Rule constraints.
25
     */
26
    private $constraints = [];
27
28
    /**
29
     * @param HashFunction $hashFunction Hash function to use when comparing passwords.
30
     * @param int|null $count Number of former passwords to consider.
31
     * @param int $first First former password to consider.
32
     * @param int $weight Constraint weight.
33
     */
34 14
    public function __construct(HashFunction $hashFunction, ?int $count = null, int $first = 0, int $weight = 1)
35
    {
36 14
        $this->hashFunction = $hashFunction;
37 14
        $this->addConstraint($count, $first, $weight);
38 11
    }
39
40
    /**
41
     * @param int|null $count Number of former passwords to consider.
42
     * @param int $first First former password to consider.
43
     * @param int $weight Constraint weight.
44
     * @return $this
45
     */
46 1
    public function addConstraint(?int $count = null, int $first = 0, int $weight = 1): self
47
    {
48 1
        $this->constraints[] = new PositionConstraint($first, $count, $weight);
49
50
        StableSort::usort($this->constraints, static function (PositionConstraint $a, PositionConstraint $b): int {
51 1
            return $b->getWeight() <=> $a->getWeight();
52 1
        });
53
54 1
        return $this;
55
    }
56
57
    /**
58
     * @return HashFunction Hash function.
59
     */
60 1
    public function getHashFunction(): HashFunction
61
    {
62 1
        return $this->hashFunction;
63
    }
64
65
    /**
66
     * Check whether a password is in compliance with the rule.
67
     *
68
     * @param Password|string $password Password to check.
69
     * @param int|null $weight Don't consider constraints with lower weights.
70
     * @return bool Whether the password is in compliance with the rule.
71
     */
72 6
    public function test($password, ?int $weight = null): bool
73
    {
74 6
        $positions = $this->getPositions($password);
75 6
        $constraint = $this->getViolation($positions, $weight);
76
77 6
        return $constraint === null;
78
    }
79
80
    /**
81
     * Validate that a password is in compliance with the rule.
82
     *
83
     * @param Password|string $password Password to validate.
84
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
85
     * @return ValidationError|null Validation error describing why the password is not in compliance with the rule.
86
     */
87 3
    public function validate($password, TranslatorInterface $translator): ?ValidationError
88
    {
89 3
        $positions = $this->getPositions($password);
90 3
        $constraint = $this->getViolation($positions);
91
92 3
        if ($constraint !== null) {
93 2
            return new ValidationError(
94 2
                $this->getMessage($constraint, $translator),
95 2
                $password,
96 2
                $this,
97 2
                $constraint->getWeight()
98
            );
99
        }
100
101 1
        return null;
102
    }
103
104
    /**
105
     * @param array<int> $positions Positions of former passwords matching the password.
106
     * @param int|null $weight Don't consider constraints with lower weights.
107
     * @return PositionConstraint|null Constraint violated by the positions.
108
     */
109 9
    private function getViolation(array $positions, ?int $weight = null): ?PositionConstraint
110
    {
111 9
        foreach ($this->constraints as $constraint) {
112 9
            if ($weight !== null && $constraint->getWeight() < $weight) {
113 1
                continue;
114
            }
115 8
            foreach ($positions as $position) {
116 7
                if ($constraint->test($position)) {
117 8
                    return $constraint;
118
                }
119
            }
120
        }
121
122 5
        return null;
123
    }
124
125
    /**
126
     * @param Password|string $password Password to compare with former passwords.
127
     * @return array<int> Positions of former passwords matching the password.
128
     */
129 9
    private function getPositions($password): array
130
    {
131 9
        $positions = [];
132
133 9
        if ($password instanceof Password) {
134 8
            $position = 0;
135 8
            foreach ($password->getFormerPasswords() as $formerPassword) {
136 8
                $passwordHash = $formerPassword->getHash();
137 8
                if ($passwordHash !== null && $this->hashFunction->compare((string)$password, $passwordHash)) {
138 8
                    $positions[] = $position;
139
                }
140 8
                ++$position;
141
            }
142
        }
143
144 9
        return $positions;
145
    }
146
147
    /**
148
     * @param PositionConstraint $constraint Constraint that is violated.
149
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
150
     * @return string Message explaining the violation.
151
     */
152 2
    private function getMessage(PositionConstraint $constraint, TranslatorInterface $translator): string
153
    {
154 2
        if ($constraint->getCount() === null) {
155 1
            return $translator->trans(
156 1
                'Formerly used passwords cannot be reused.'
157
            );
158
        }
159
160 1
        return $translator->trans(
161
            'The most recently used password cannot be reused.|' .
162 1
            'The %count% most recently used passwords cannot be reused.',
163 1
            ['%count%' => $constraint->getCount()]
164
        );
165
    }
166
}
167