Completed
Pull Request — master (#317)
by Guilherme
04:03
created

PhoneVerificationService::countVerified()   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
 * This file is part of the login-cidadao project or it's bundles.
4
 *
5
 * (c) Guilherme Donato <guilhermednt on github>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace LoginCidadao\PhoneVerificationBundle\Service;
12
13
use Doctrine\ORM\EntityManagerInterface;
14
use libphonenumber\PhoneNumber;
15
use LoginCidadao\CoreBundle\Entity\Person;
16
use LoginCidadao\CoreBundle\Entity\PersonRepository;
17
use LoginCidadao\PhoneVerificationBundle\Entity\SentVerification;
18
use LoginCidadao\PhoneVerificationBundle\Entity\SentVerificationRepository;
19
use LoginCidadao\PhoneVerificationBundle\Event\PhoneVerificationEvent;
20
use LoginCidadao\PhoneVerificationBundle\Event\SendPhoneVerificationEvent;
21
use LoginCidadao\PhoneVerificationBundle\Exception\VerificationNotSentException;
22
use LoginCidadao\PhoneVerificationBundle\Model\SentVerificationInterface;
23
use LoginCidadao\PhoneVerificationBundle\PhoneVerificationEvents;
24
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
25
use LoginCidadao\CoreBundle\Model\PersonInterface;
26
use LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerification;
27
use LoginCidadao\PhoneVerificationBundle\Entity\PhoneVerificationRepository;
28
use LoginCidadao\PhoneVerificationBundle\Model\PhoneVerificationInterface;
29
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
30
31
class PhoneVerificationService implements PhoneVerificationServiceInterface
32
{
33
    /** @var PhoneVerificationOptions */
34
    private $options;
35
36
    /** @var EntityManagerInterface */
37
    private $em;
38
39
    /** @var PhoneVerificationRepository */
40
    private $phoneVerificationRepository;
41
42
    /** @var SentVerificationRepository */
43
    private $sentVerificationRepository;
44
45
    /** @var PersonRepository */
46
    private $personRepository;
47
48
    /** @var EventDispatcherInterface */
49
    private $dispatcher;
50
51
    /**
52
     * PhoneVerificationService constructor.
53
     * @param PhoneVerificationOptions $options
54
     * @param EntityManagerInterface $em
55
     * @param EventDispatcherInterface $dispatcher
56
     */
57 23
    public function __construct(
58
        PhoneVerificationOptions $options,
59
        EntityManagerInterface $em,
60
        EventDispatcherInterface $dispatcher
61
    ) {
62 23
        $this->options = $options;
63 23
        $this->em = $em;
64 23
        $this->dispatcher = $dispatcher;
65 23
        $this->phoneVerificationRepository = $this->em->getRepository(PhoneVerification::class);
66 23
        $this->sentVerificationRepository = $this->em->getRepository(SentVerification::class);
67 23
        $this->personRepository = $this->em->getRepository(Person::class);
68 23
    }
69
70
    /**
71
     * Gets phone verification record (PhoneVerificationInterface) for the given phone number.
72
     *
73
     * @param PersonInterface $person
74
     * @param mixed $phone
75
     * @return PhoneVerificationInterface
76
     */
77 3
    public function getPhoneVerification(PersonInterface $person, PhoneNumber $phone)
78
    {
79
        /** @var PhoneVerificationInterface $phoneVerification */
80 3
        $phoneVerification = $this->phoneVerificationRepository->findOneBy(
81
            [
82 3
                'person' => $person,
83 3
                'phone' => $phone,
84
            ]
85
        );
86
87 3
        return $phoneVerification;
88
    }
89
90
    /**
91
     * @param PersonInterface $person
92
     * @param mixed $id
93
     * @return PhoneVerificationInterface
94
     */
95 1
    public function getPhoneVerificationById($id, PersonInterface $person = null)
96
    {
97 1
        $criteria = ['id' => $id];
98 1
        if ($person) {
99 1
            $criteria['person'] = $person;
100
        }
101
102
        /** @var PhoneVerificationInterface $phoneVerification */
103 1
        $phoneVerification = $this->phoneVerificationRepository->findOneBy($criteria);
104
105 1
        return $phoneVerification;
106
    }
107
108
    /**
109
     * @param PersonInterface $person
110
     * @param mixed $id
111
     * @return PhoneVerificationInterface
112
     */
113 1
    public function getPendingPhoneVerificationById($id, PersonInterface $person = null)
114
    {
115
        $criteria = [
116 1
            'id' => $id,
117
            'verifiedAt' => null,
118
        ];
119 1
        if ($person) {
120 1
            $criteria['person'] = $person;
121
        }
122
123
        /** @var PhoneVerificationInterface $phoneVerification */
124 1
        $phoneVerification = $this->phoneVerificationRepository->findOneBy($criteria);
125
126 1
        return $phoneVerification;
127
    }
128
129
    /**
130
     * @param PersonInterface $person
131
     * @param mixed $phone
132
     * @return PhoneVerificationInterface
133
     */
134 2
    public function createPhoneVerification(PersonInterface $person, PhoneNumber $phone)
135
    {
136 2
        $existingVerification = $this->getPhoneVerification($person, $phone);
137 2
        if ($existingVerification instanceof PhoneVerificationInterface) {
0 ignored issues
show
introduced by
$existingVerification is always a sub-type of LoginCidadao\PhoneVerifi...neVerificationInterface.
Loading history...
138 1
            $this->removePhoneVerification($existingVerification);
139
        }
140
141 2
        $phoneVerification = new PhoneVerification();
142 2
        $phoneVerification->setPerson($person)
143 2
            ->setPhone($phone)
144 2
            ->setVerificationCode($this->generateVerificationCode())
145 2
            ->setVerificationToken($this->generateVerificationToken());
146
147 2
        $this->em->persist($phoneVerification);
148 2
        $this->em->flush($phoneVerification);
149
150 2
        return $phoneVerification;
151
    }
152
153
    /**
154
     * @param PersonInterface $person
155
     * @param mixed $phone
156
     * @return PhoneVerificationInterface|null
157
     */
158 1
    public function getPendingPhoneVerification(PersonInterface $person, PhoneNumber $phone)
159
    {
160
        /** @var PhoneVerificationInterface $phoneVerification */
161 1
        $phoneVerification = $this->phoneVerificationRepository->findOneBy(
162
            [
163 1
                'person' => $person,
164 1
                'phone' => $phone,
165
                'verifiedAt' => null,
166
            ]
167
        );
168
169 1
        return $phoneVerification;
170
    }
171
172
    /**
173
     * @param PersonInterface $person
174
     * @return PhoneVerificationInterface[]
175
     */
176 1
    public function getAllPendingPhoneVerification(PersonInterface $person)
177
    {
178
        /** @var PhoneVerificationInterface[] $phoneVerification */
179 1
        $phoneVerification = $this->phoneVerificationRepository->findBy(
180
            [
181 1
                'person' => $person,
182
                'verifiedAt' => null,
183
            ]
184
        );
185
186 1
        return $phoneVerification;
187
    }
188
189
    /**
190
     * @param PhoneVerificationInterface $phoneVerification
191
     * @return bool
192
     */
193 2
    public function removePhoneVerification(PhoneVerificationInterface $phoneVerification)
194
    {
195 2
        $this->em->remove($phoneVerification);
196 2
        $this->em->flush($phoneVerification);
197
198 2
        return true;
199
    }
200
201
    /**
202
     * @param PersonInterface $person
203
     * @param mixed $phone
204
     * @return PhoneVerificationInterface
205
     */
206 1
    public function enforcePhoneVerification(PersonInterface $person, PhoneNumber $phone)
207
    {
208 1
        $phoneVerification = $this->getPhoneVerification($person, $phone);
209
210 1
        return $phoneVerification ?: $this->createPhoneVerification($person, $phone);
0 ignored issues
show
introduced by
$phoneVerification is of type LoginCidadao\PhoneVerifi...neVerificationInterface, thus it always evaluated to true.
Loading history...
211
    }
212
213 2
    private function generateVerificationCode()
214
    {
215 2
        $length = $this->options->getLength();
216 2
        $useNumbers = $this->options->isUseNumbers();
217 2
        $useLower = $this->options->isUseLowerCase();
218 2
        $useUpper = $this->options->isUseUpperCase();
219
220 2
        $keySpace = $useNumbers ? '0123456789' : '';
221 2
        $keySpace .= $useLower ? 'abcdefghijklmnopqrstuvwxyz' : '';
222 2
        $keySpace .= $useUpper ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '';
223
224 2
        $code = '';
225 2
        $max = mb_strlen($keySpace, '8bit') - 1;
226 2
        for ($i = 0; $i < $length; ++$i) {
227 2
            $code .= $keySpace[random_int(0, $max)];
228
        }
229
230 2
        return $code;
231
    }
232
233 2
    private function generateVerificationToken()
234
    {
235 2
        $length = $this->options->getVerificationTokenLength();
236
237
        // We divide by 2 otherwise the resulting string would be twice as long
238 2
        return bin2hex(random_bytes($length / 2));
239
    }
240
241
    /**
242
     * Verifies code without dispatching any event or making any changes.
243
     *
244
     * @param $provided
245
     * @param $expected
246
     * @return bool
247
     */
248 5
    public function checkVerificationCode($provided, $expected)
249
    {
250 5
        if ($this->options->isCaseSensitive()) {
251 1
            return $provided === $expected;
252
        } else {
253 4
            return strtolower($provided) === strtolower($expected);
254
        }
255
    }
256
257
    /**
258
     * Verifies a phone and dispatches event.
259
     *
260
     * @param PhoneVerificationInterface $phoneVerification
261
     * @param $providedCode
262
     * @return bool
263
     */
264 3
    public function verify(PhoneVerificationInterface $phoneVerification, $providedCode)
265
    {
266 3
        if ($this->checkVerificationCode($providedCode, $phoneVerification->getVerificationCode())) {
267 2
            $phoneVerification->setVerifiedAt(new \DateTime());
268 2
            $this->em->persist($phoneVerification);
269 2
            $this->em->flush($phoneVerification);
270
271 2
            $event = new PhoneVerificationEvent($phoneVerification, $providedCode);
272 2
            $this->dispatcher->dispatch(PhoneVerificationEvents::PHONE_VERIFIED, $event);
273
274 2
            return true;
275
        } else {
276 1
            return false;
277
        }
278
    }
279
280 4
    public function sendVerificationCode(PhoneVerificationInterface $phoneVerification, $forceSend = false)
281
    {
282 4
        $nextDate = $this->getNextResendDate($phoneVerification);
283 4
        if (!$forceSend && $nextDate > new \DateTime()) {
284
            // We can't resend the verification code yet
285 1
            $retryAfter = $nextDate->getTimestamp() - time();
286 1
            throw new TooManyRequestsHttpException(
287 1
                $retryAfter,
288 1
                "tasks.verify_phone.resend.errors.too_many_requests"
289
            );
290
        }
291
292 3
        $event = new SendPhoneVerificationEvent($phoneVerification);
293 3
        $this->dispatcher->dispatch(PhoneVerificationEvents::PHONE_VERIFICATION_REQUESTED, $event);
294
295 3
        $sentVerification = $event->getSentVerification();
296
297 3
        if (!$sentVerification) {
0 ignored issues
show
introduced by
$sentVerification is of type LoginCidadao\PhoneVerifi...ntVerificationInterface, thus it always evaluated to true.
Loading history...
298 1
            throw new VerificationNotSentException();
299
        }
300
301 2
        return $sentVerification;
302
    }
303
304
    /**
305
     * @param SentVerificationInterface $sentVerification
306
     * @return \LoginCidadao\PhoneVerificationBundle\Entity\SentVerification|SentVerificationInterface
307
     */
308 1
    public function registerVerificationSent(SentVerificationInterface $sentVerification)
309
    {
310 1
        $this->em->persist($sentVerification);
311 1
        $this->em->flush($sentVerification);
312
313 1
        return $sentVerification;
314
    }
315
316 5
    public function getLastSentVerification(PhoneVerificationInterface $phoneVerification)
317
    {
318 5
        return $this->sentVerificationRepository->getLastVerificationSent($phoneVerification);
319
    }
320
321 5
    public function getNextResendDate(PhoneVerificationInterface $phoneVerification)
322
    {
323 5
        $lastSentVerification = $this->getLastSentVerification($phoneVerification);
324 5
        if (!$lastSentVerification) {
325 3
            return new \DateTime();
326
        }
327
328 2
        $timeout = \DateInterval::createFromDateString($this->options->getSmsResendTimeout());
329
330 2
        return $lastSentVerification->getSentAt()->add($timeout);
331
    }
332
333
    /**
334
     * @param PhoneVerificationInterface $phoneVerification
335
     * @param string $token
336
     * @return bool
337
     */
338 3
    public function verifyToken(PhoneVerificationInterface $phoneVerification, $token)
339
    {
340 3
        if ($phoneVerification->isVerified()) {
341 1
            return true;
342
        }
343
344 2
        if ($phoneVerification->getVerificationToken() !== $token) {
345 1
            return false;
346
        }
347
348 1
        return $this->verify($phoneVerification, $phoneVerification->getVerificationCode());
349
    }
350
351
    /**
352
     * @inheritDoc
353
     */
354 2
    public function isVerificationMandatory(PhoneVerificationInterface $phoneVerification): bool
355
    {
356 2
        $accountsCount = $this->personRepository->countByPhone($phoneVerification->getPhone());
357
358 2
        return $accountsCount >= $this->options->getEnforceVerificationThreshold();
359
    }
360
361
    /**
362
     * @inheritDoc
363
     */
364 1
    public function countVerified(PhoneNumber $phoneNumber): int
365
    {
366 1
        return $this->phoneVerificationRepository->countVerified($phoneNumber);
367
    }
368
}
369