HaveIBeenPwnedRule::getRequestFactory()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use Http\Factory\Discovery\HttpClient;
8
use Http\Factory\Discovery\HttpFactory;
9
use Psr\Http\Client\ClientExceptionInterface;
10
use Psr\Http\Client\ClientInterface;
11
use Psr\Http\Message\RequestFactoryInterface;
12
use RuntimeException;
13
use StableSort\StableSort;
14
use Stadly\PasswordPolice\Constraint\CountConstraint;
15
use Stadly\PasswordPolice\Rule;
16
use Stadly\PasswordPolice\ValidationError;
17
use Symfony\Contracts\Translation\LocaleAwareInterface;
18
use Symfony\Contracts\Translation\TranslatorInterface;
19
20
final class HaveIBeenPwnedRule implements Rule
21
{
22
    /**
23
     * @var array<CountConstraint> Rule constraints.
24
     */
25
    private $constraints = [];
26
27
    /**
28
     * @var ClientInterface|null HTTP client for sending requests.
29
     */
30
    private $client = null;
31
32
    /**
33
     * @var RequestFactoryInterface|null Request factory for generating HTTP requests.
34
     */
35
    private $requestFactory = null;
36
37
    /**
38
     * @param int|null $max Maximum number of appearances in breaches.
39
     * @param int $min Minimum number of appearances in breaches.
40
     * @param int $weight Constraint weight.
41
     */
42 21
    public function __construct(?int $max = 0, int $min = 0, int $weight = 1)
43
    {
44 21
        $this->addConstraint($max, $min, $weight);
45 19
    }
46
47
    /**
48
     * @param int|null $max Maximum number of appearances in breaches.
49
     * @param int $min Minimum number of appearances in breaches.
50
     * @param int $weight Constraint weight.
51
     * @return $this
52
     */
53 1
    public function addConstraint(?int $max = 0, int $min = 0, int $weight = 1): self
54
    {
55 1
        $this->constraints[] = new CountConstraint($min, $max, $weight);
56
57
        StableSort::usort($this->constraints, static function (CountConstraint $a, CountConstraint $b): int {
58 1
            return $b->getWeight() <=> $a->getWeight();
59 1
        });
60
61 1
        return $this;
62
    }
63
64
    /**
65
     * @param ClientInterface $client HTTP client for sending requests.
66
     */
67 1
    public function setClient(ClientInterface $client): void
68
    {
69 1
        $this->client = $client;
70 1
    }
71
72
    /**
73
     * @return ClientInterface HTTP client for sending requests.
74
     * @throws RuntimeException If a client could not be found.
75
     */
76 15
    private function getClient(): ClientInterface
77
    {
78 15
        if ($this->client === null) {
79 14
            $client = HttpClient::client();
80 13
            $this->client = $client;
81
        }
82 14
        return $this->client;
83
    }
84
85
    /**
86
     * @param RequestFactoryInterface $requestFactory Request factory for generating HTTP requests.
87
     */
88 1
    public function setRequestFactory(RequestFactoryInterface $requestFactory): void
89
    {
90 1
        $this->requestFactory = $requestFactory;
91 1
    }
92
93
    /**
94
     * @return RequestFactoryInterface Request factory for generating HTTP requests.
95
     * @throws RuntimeException If a request factory could not be found.
96
     */
97 16
    private function getRequestFactory(): RequestFactoryInterface
98
    {
99 16
        if ($this->requestFactory === null) {
100 16
            $this->requestFactory = HttpFactory::requestFactory();
101
        }
102 15
        return $this->requestFactory;
103
    }
104
105
    /**
106
     * @inheritDoc
107
     */
108 10
    public function test($password, ?int $weight = null): bool
109
    {
110 10
        $count = $this->getCount((string)$password);
111 7
        $constraint = $this->getViolation($count, $weight);
112
113 7
        return $constraint === null;
114
    }
115
116
    /**
117
     * @inheritDoc
118
     */
119 6
    public function validate($password, TranslatorInterface $translator): ?ValidationError
120
    {
121 6
        $count = $this->getCount((string)$password);
122 6
        $constraint = $this->getViolation($count);
123
124 6
        if ($constraint !== null) {
125 5
            return new ValidationError(
126 5
                $this->getMessage($constraint, $count, $translator),
127 5
                $password,
128 5
                $this,
129 5
                $constraint->getWeight()
130
            );
131
        }
132
133 1
        return null;
134
    }
135
136
    /**
137
     * @param int $count Number of appearances in breaches.
138
     * @param int|null $weight Don't consider constraints with lower weights.
139
     * @return CountConstraint|null Constraint violated by the count.
140
     */
141 13
    private function getViolation(int $count, ?int $weight = null): ?CountConstraint
142
    {
143 13
        foreach ($this->constraints as $constraint) {
144 13
            if ($weight !== null && $constraint->getWeight() < $weight) {
145 1
                continue;
146
            }
147 12
            if (!$constraint->test($count)) {
148 12
                return $constraint;
149
            }
150
        }
151
152 5
        return null;
153
    }
154
155
    /**
156
     * @param string $password Password to check in breaches.
157
     * @return int Number of appearances in breaches.
158
     * @throws CouldNotUseRuleException If an error occurred.
159
     */
160 16
    private function getCount(string $password): int
161
    {
162 16
        $sha1 = strtoupper(sha1($password));
163 16
        $prefix = substr($sha1, 0, 5);
164 16
        $suffix = substr($sha1, 5, 35);
165
166
        try {
167 16
            $requestFactory = $this->getRequestFactory();
168 15
            $request = $requestFactory->createRequest('GET', 'https://api.pwnedpasswords.com/range/' . $prefix);
169
170 15
            $client = $this->getClient();
171
172 14
            $response = $client->sendRequest($request);
173 13
            $body = $response->getBody();
174 13
            $contents = $body->getContents();
175 13
            $lines = explode("\r\n", $contents);
176 13
            foreach ($lines as $line) {
177 13
                if (substr($line, 0, 35) === $suffix) {
178 13
                    return (int)substr($line, 36);
179
                }
180
            }
181 2
            return 0;
182 3
        } catch (ClientExceptionInterface | RuntimeException $exception) {
183 3
            throw new CouldNotUseRuleException(
184 3
                $this,
185 3
                'An error occurred while using the Have I Been Pwned? service: ' . $exception->getMessage(),
186 3
                $exception
187
            );
188
        }
189
    }
190
191
    /**
192
     * @param CountConstraint $constraint Constraint that is violated.
193
     * @param int $count Count that violates the constraint.
194
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
195
     * @return string Message explaining the violation.
196
     */
197 5
    private function getMessage(CountConstraint $constraint, int $count, TranslatorInterface $translator): 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

197
    private function getMessage(CountConstraint $constraint, /** @scrutinizer ignore-unused */ int $count, TranslatorInterface $translator): 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...
198
    {
199 5
        if ($constraint->getMax() === null) {
200 1
            return $translator->trans(
201
                'The password must appear at least once in data breaches.|' .
202 1
                'The password must appear at least %count% times in data breaches.',
203 1
                ['%count%' => $constraint->getMin()]
204
            );
205
        }
206
207 4
        if ($constraint->getMax() === 0) {
208 1
            return $translator->trans(
209 1
                'The password cannot appear in data breaches.'
210
            );
211
        }
212
213 3
        if ($constraint->getMin() === 0) {
214 1
            return $translator->trans(
215
                'The password must appear at most once in data breaches.|' .
216 1
                'The password must appear at most %count% times in data breaches.',
217 1
                ['%count%' => $constraint->getMax()]
218
            );
219
        }
220
221 2
        if ($constraint->getMin() === $constraint->getMax()) {
222 1
            return $translator->trans(
223
                'The password must appear exactly once in data breaches.|' .
224 1
                'The password must appear exactly %count% times in data breaches.',
225 1
                ['%count%' => $constraint->getMin()]
226
            );
227
        }
228
229 1
        return $translator->trans(
230 1
            'The password must appear between %min% and %max% times in data breaches.',
231 1
            ['%min%' => $constraint->getMin(), '%max%' => $constraint->getMax()]
232
        );
233
    }
234
}
235