ChangeWithIntervalRule::getViolation()   A
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 8
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 16
ccs 9
cts 9
cp 1
crap 6
rs 9.2222
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use Carbon\CarbonInterval;
8
use DateInterval;
9
use DateTimeInterface;
10
use StableSort\StableSort;
11
use Stadly\Date\Interval;
12
use Stadly\PasswordPolice\Constraint\DateIntervalConstraint;
13
use Stadly\PasswordPolice\Password;
14
use Stadly\PasswordPolice\Rule;
15
use Stadly\PasswordPolice\ValidationError;
16
use Symfony\Contracts\Translation\LocaleAwareInterface;
17
use Symfony\Contracts\Translation\TranslatorInterface;
18
19
final class ChangeWithIntervalRule implements Rule
20
{
21
    /**
22
     * @var array<DateIntervalConstraint> Rule constraints.
23
     */
24
    private $constraints = [];
25
26
    /**
27
     * @param DateInterval $min Minimum time since last password change.
28
     * @param DateInterval|null $max Maximum time since last password change.
29
     * @param int $weight Constraint weight.
30
     */
31 14
    public function __construct(DateInterval $min, ?DateInterval $max = null, int $weight = 1)
32
    {
33 14
        $this->addConstraint($min, $max, $weight);
34 12
    }
35
36
    /**
37
     * @param DateInterval $min Minimum time since last password change.
38
     * @param DateInterval|null $max Maximum time since last password change.
39
     * @param int $weight Constraint weight.
40
     * @return $this
41
     */
42 1
    public function addConstraint(DateInterval $min, ?DateInterval $max = null, int $weight = 1): self
43
    {
44 1
        $this->constraints[] = new DateIntervalConstraint($min, $max, $weight);
45
46
        StableSort::usort($this->constraints, static function (
47
            DateIntervalConstraint $a,
48
            DateIntervalConstraint $b
49
        ): int {
50 1
            return $b->getWeight() <=> $a->getWeight();
51 1
        });
52
53 1
        return $this;
54
    }
55
56
    /**
57
     * Check whether a password is in compliance with the rule.
58
     *
59
     * @param Password|string $password Password to check.
60
     * @param int|null $weight Don't consider constraints with lower weights.
61
     * @return bool Whether the password is in compliance with the rule.
62
     */
63 6
    public function test($password, ?int $weight = null): bool
64
    {
65 6
        $date = $this->getDate($password);
66 6
        $constraint = $this->getViolation($date, $weight);
67
68 6
        return $constraint === null;
69
    }
70
71
    /**
72
     * Validate that a password is in compliance with the rule.
73
     *
74
     * @param Password|string $password Password to validate.
75
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
76
     * @return ValidationError|null Validation error describing why the password is not in compliance with the rule.
77
     */
78 5
    public function validate($password, TranslatorInterface $translator): ?ValidationError
79
    {
80 5
        $date = $this->getDate($password);
81 5
        $constraint = $this->getViolation($date);
82
83 5
        if ($constraint !== null) {
84 4
            assert($date !== null);
85 4
            return new ValidationError(
86 4
                $this->getMessage($constraint, $date, $translator),
87 4
                $password,
88 4
                $this,
89 4
                $constraint->getWeight()
90
            );
91
        }
92
93 1
        return null;
94
    }
95
96
    /**
97
     * @param DateTimeInterface|null $date When the password was set.
98
     * @param int|null $weight Don't consider constraints with lower weights.
99
     * @return DateIntervalConstraint|null Constraint violated by the date.
100
     */
101 11
    private function getViolation(?DateTimeInterface $date, ?int $weight = null): ?DateIntervalConstraint
102
    {
103 11
        if ($date === null) {
104 1
            return null;
105
        }
106
107 10
        foreach ($this->constraints as $constraint) {
108 10
            if ($weight !== null && $constraint->getWeight() < $weight) {
109 1
                continue;
110
            }
111 9
            if (!$constraint->test($date)) {
112 9
                return $constraint;
113
            }
114
        }
115
116 4
        return null;
117
    }
118
119
    /**
120
     * @param Password|string $password Password to check when was set.
121
     * @return DateTimeInterface|null When the password was set.
122
     */
123 11
    private function getDate($password): ?DateTimeInterface
124
    {
125 11
        if ($password instanceof Password) {
126 10
            $formerPasswords = $password->getFormerPasswords();
127
128 10
            if ($formerPasswords !== []) {
129 10
                return reset($formerPasswords)->getDate();
130
            }
131
        }
132 1
        return null;
133
    }
134
135
    /**
136
     * @param DateIntervalConstraint $constraint Constraint that is violated.
137
     * @param DateTimeInterface $date Date that violates the constraint.
138
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
139
     * @return string Message explaining the violation.
140
     */
141 4
    private function getMessage(
142
        DateIntervalConstraint $constraint,
143
        DateTimeInterface $date,
0 ignored issues
show
Unused Code introduced by
The parameter $date 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

143
        /** @scrutinizer ignore-unused */ DateTimeInterface $date,

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...
144
        TranslatorInterface $translator
145
    ): string {
146 4
        $locale = $translator->getLocale();
147
148 4
        $constraintMin = $constraint->getMin();
149 4
        $constraintMax = $constraint->getMax();
150
151 4
        $min = CarbonInterval::instance($constraintMin);
152 4
        $min->locale($locale);
153 4
        $minString = $min->forHumans(['join' => true]);
154
155 4
        if ($constraintMax === null) {
156 1
            return $translator->trans(
157 1
                'There must be at least %interval% between password changes.',
158 1
                ['%interval%' => $minString]
159
            );
160
        }
161
162 3
        $max = CarbonInterval::instance($constraintMax);
163 3
        $max->locale($locale);
164 3
        $maxString = $max->forHumans(['join' => true]);
165
166 3
        if (Interval::compare(new DateInterval('PT0S'), $constraintMin) === 0) {
167 1
            return $translator->trans(
168 1
                'There must be at most %interval% between password changes.',
169 1
                ['%interval%' => $maxString]
170
            );
171
        }
172
173 2
        if (Interval::compare($constraintMin, $constraintMax) === 0) {
174 1
            return $translator->trans(
175 1
                'There must be exactly %interval% between password changes.',
176 1
                ['%interval%' => $minString]
177
            );
178
        }
179
180 1
        return $translator->trans(
181 1
            'There must be between %min% and %max% between password changes.',
182 1
            ['%min%' => $minString, '%max%' => $maxString]
183
        );
184
    }
185
}
186