Completed
Push — master ( 77bc5a...fb6707 )
by Magnar Ovedal
09:03 queued 06:21
created

UpperCaseRule::addConstraint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 3
dl 0
loc 9
ccs 5
cts 5
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 StableSort\StableSort;
8
use Stadly\PasswordPolice\Constraint\CountConstraint;
9
use Stadly\PasswordPolice\Password;
10
use Stadly\PasswordPolice\Policy;
11
use Stadly\PasswordPolice\Rule;
12
use Stadly\PasswordPolice\ValidationError;
13
14
final class UpperCaseRule implements Rule
15
{
16
    /**
17
     * @var CountConstraint[] Rule constraints.
18
     */
19
    private $constraints;
20
21
    /**
22
     * @param int $min Minimum number of upper case letters.
23
     * @param int|null $max Maximum number of upper case letters.
24
     * @param int $weight Constraint weight.
25
     */
26 7
    public function __construct(int $min = 1, ?int $max = null, int $weight = 1)
27
    {
28 7
        $this->addConstraint($min, $max, $weight);
29 5
    }
30
31
    /**
32
     * @param int $min Minimum number of upper case letters.
33
     * @param int|null $max Maximum number of upper case letters.
34
     * @param int $weight Constraint weight.
35
     * @return $this
36
     */
37 2
    public function addConstraint(int $min = 1, ?int $max = null, int $weight = 1): self
38
    {
39 2
        $this->constraints[] = new CountConstraint($min, $max, $weight);
40
41
        StableSort::usort($this->constraints, static function (CountConstraint $a, CountConstraint $b): int {
42 2
            return $b->getWeight() <=> $a->getWeight();
43 2
        });
44
45 2
        return $this;
46
    }
47
48
    /**
49
     * Check whether a password is in compliance with the rule.
50
     *
51
     * @param Password|string $password Password to check.
52
     * @param int|null $weight Don't consider constraints with lower weights.
53
     * @return bool Whether the password is in compliance with the rule.
54
     */
55 7
    public function test($password, ?int $weight = 1): bool
56
    {
57 7
        $count = $this->getCount((string)$password);
58 7
        $constraint = $this->getViolation($count, $weight);
59
60 7
        return $constraint === null;
61
    }
62
63
    /**
64
     * Validate that a password is in compliance with the rule.
65
     *
66
     * @param Password|string $password Password to validate.
67
     * @return ValidationError|null Validation error describing why the password is not in compliance with the rule.
68
     */
69 6
    public function validate($password): ?ValidationError
70
    {
71 6
        $count = $this->getCount((string)$password);
72 6
        $constraint = $this->getViolation($count);
73
74 6
        if ($constraint !== null) {
75 5
            return new ValidationError(
76 5
                $this->getMessage($constraint, $count),
77 5
                $password,
78 5
                $this,
79 5
                $constraint->getWeight()
80
            );
81
        }
82
83 1
        return null;
84
    }
85
86
    /**
87
     * @param int $count Number of upper case characters.
88
     * @param int|null $weight Don't consider constraints with lower weights.
89
     * @return CountConstraint|null Constraint violated by the count.
90
     */
91 13
    private function getViolation(int $count, ?int $weight = null): ?CountConstraint
92
    {
93 13
        foreach ($this->constraints as $constraint) {
94 13
            if ($weight !== null && $constraint->getWeight() < $weight) {
95 1
                continue;
96
            }
97 12
            if (!$constraint->test($count)) {
98 12
                return $constraint;
99
            }
100
        }
101
102 5
        return null;
103
    }
104
105
    /**
106
     * @param string $password Password to count characters in.
107
     * @return int Number of upper case characters.
108
     */
109 13
    private function getCount(string $password): int
110
    {
111 13
        $lowerCase = mb_strtolower($password);
112
113 13
        $passwordCharacters = $this->splitString($password);
114 13
        $lowerCaseCharacters = $this->splitString($lowerCase);
115 13
        assert(count($passwordCharacters) === count($lowerCaseCharacters));
116
117 13
        $count = 0;
118 13
        for ($i = count($passwordCharacters)-1; $i >= 0; --$i) {
119 13
            if ($passwordCharacters[$i] !== $lowerCaseCharacters[$i]) {
120 11
                ++$count;
121
            }
122
        }
123
124 13
        return $count;
125
    }
126
127
    /**
128
     * @param string $string String to split into individual characters.
129
     * @return string[] Array of characters.
130
     */
131 13
    private function splitString(string $string): array
132
    {
133 13
        $characters = preg_split('{}u', $string, -1, PREG_SPLIT_NO_EMPTY);
134 13
        assert($characters !== false);
135
136 13
        return $characters;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $characters returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
137
    }
138
139
    /**
140
     * @param CountConstraint $constraint Constraint that is violated.
141
     * @param int $count Count that violates the constraint.
142
     * @return string Message explaining the violation.
143
     */
144 5
    private function getMessage(CountConstraint $constraint, int $count): string
0 ignored issues
show
Unused Code introduced by
The parameter $count 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

144
    private function getMessage(CountConstraint $constraint, /** @scrutinizer ignore-unused */ int $count): 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...
145
    {
146 5
        $translator = Policy::getTranslator();
147
148 5
        if ($constraint->getMax() === null) {
149 1
            return $translator->trans(
150
                'There must be at least one upper case character.|'.
151 1
                'There must be at least %count% upper case characters.',
152 1
                ['%count%' => $constraint->getMin()]
153
            );
154
        }
155
156 4
        if ($constraint->getMax() === 0) {
157 1
            return $translator->trans(
158 1
                'There must be no upper case characters.'
159
            );
160
        }
161
162 3
        if ($constraint->getMin() === 0) {
163 1
            return $translator->trans(
164
                'There must be at most one upper case character.|'.
165 1
                'There must be at most %count% upper case characters.',
166 1
                ['%count%' => $constraint->getMax()]
167
            );
168
        }
169
170 2
        if ($constraint->getMin() === $constraint->getMax()) {
171 1
            return $translator->trans(
172
                'There must be exactly one upper case character.|'.
173 1
                'There must be exactly %count% upper case characters.',
174 1
                ['%count%' => $constraint->getMin()]
175
            );
176
        }
177
178 1
        return $translator->trans(
179 1
            'There must be between %min% and %max% upper case characters.',
180 1
            ['%min%' => $constraint->getMin(), '%max%' => $constraint->getMax()]
181
        );
182
    }
183
}
184