Passed
Push — master ( bf65f8...a39021 )
by Magnar Ovedal
03:21 queued 28s
created

HaveIBeenPwnedRule::setRequestFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

195
    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...
196
    {
197 5
        $translator = Policy::getTranslator();
198
199 5
        if ($constraint->getMax() === null) {
200 1
            return $translator->trans(
201
                'Must appear at least once in breaches.|'.
202 1
                'Must appear at least %count% times in breaches.',
203 1
                ['%count%' => $constraint->getMin()]
204
            );
205
        }
206
207 4
        if ($constraint->getMax() === 0) {
208 1
            return $translator->trans(
209 1
                'Must not appear in any breaches.'
210
            );
211
        }
212
213 3
        if ($constraint->getMin() === 0) {
214 1
            return $translator->trans(
215
                'Must appear at most once in breaches.|'.
216 1
                'Must appear at most %count% times in breaches.',
217 1
                ['%count%' => $constraint->getMax()]
218
            );
219
        }
220
221 2
        if ($constraint->getMin() === $constraint->getMax()) {
222 1
            return $translator->trans(
223
                'Must appear exactly once in breaches.|'.
224 1
                'Must appear exactly %count% times in breaches.',
225 1
                ['%count%' => $constraint->getMin()]
226
            );
227
        }
228
229 1
        return $translator->trans(
230 1
            'Must appear between %min% and %max% times in breaches.',
231 1
            ['%min%' => $constraint->getMin(), '%max%' => $constraint->getMax()]
232
        );
233
    }
234
}
235