Completed
Push — issue#830 ( eac670...15bb28 )
by Guilherme
04:33
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
     * @throws \Doctrine\ORM\ORMException
134
     * @throws \Doctrine\ORM\OptimisticLockException
135
     */
136 2
    public function createPhoneVerification(PersonInterface $person, PhoneNumber $phone)
137
    {
138 2
        $existingVerification = $this->getPhoneVerification($person, $phone);
139 2
        if ($existingVerification instanceof PhoneVerificationInterface) {
0 ignored issues
show
introduced by
$existingVerification is always a sub-type of LoginCidadao\PhoneVerifi...neVerificationInterface.
Loading history...
140 1
            $this->removePhoneVerification($existingVerification);
141
        }
142
143 2
        $phoneVerification = new PhoneVerification();
144 2
        $phoneVerification->setPerson($person)
145 2
            ->setPhone($phone)
146 2
            ->setVerificationCode($this->generateVerificationCode())
147 2
            ->setVerificationToken($this->generateVerificationToken());
148
149 2
        $this->em->persist($phoneVerification);
150 2
        $this->em->flush($phoneVerification);
151
152 2
        return $phoneVerification;
153
    }
154
155
    /**
156
     * @param PersonInterface $person
157
     * @param mixed $phone
158
     * @return PhoneVerificationInterface|null
159
     */
160 1
    public function getPendingPhoneVerification(PersonInterface $person, PhoneNumber $phone)
161
    {
162
        /** @var PhoneVerificationInterface $phoneVerification */
163 1
        $phoneVerification = $this->phoneVerificationRepository->findOneBy(
164
            [
165 1
                'person' => $person,
166 1
                'phone' => $phone,
167
                'verifiedAt' => null,
168
            ]
169
        );
170
171 1
        return $phoneVerification;
172
    }
173
174
    /**
175
     * @param PersonInterface $person
176
     * @return PhoneVerificationInterface[]
177
     */
178 1
    public function getAllPendingPhoneVerification(PersonInterface $person)
179
    {
180
        /** @var PhoneVerificationInterface[] $phoneVerification */
181 1
        $phoneVerification = $this->phoneVerificationRepository->findBy(
182
            [
183 1
                'person' => $person,
184
                'verifiedAt' => null,
185
            ]
186
        );
187
188 1
        return $phoneVerification;
189
    }
190
191
    /**
192
     * @param PhoneVerificationInterface $phoneVerification
193
     * @return bool
194
     * @throws \Doctrine\ORM\ORMException
195
     * @throws \Doctrine\ORM\OptimisticLockException
196
     */
197 2
    public function removePhoneVerification(PhoneVerificationInterface $phoneVerification)
198
    {
199 2
        $this->em->remove($phoneVerification);
200 2
        $this->em->flush($phoneVerification);
201
202 2
        return true;
203
    }
204
205
    /**
206
     * @param PersonInterface $person
207
     * @param mixed $phone
208
     * @return PhoneVerificationInterface
209
     * @throws \Doctrine\ORM\ORMException
210
     * @throws \Doctrine\ORM\OptimisticLockException
211
     */
212 1
    public function enforcePhoneVerification(PersonInterface $person, PhoneNumber $phone)
213
    {
214 1
        $phoneVerification = $this->getPhoneVerification($person, $phone);
215
216 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...
217
    }
218
219 2
    private function generateVerificationCode()
220
    {
221 2
        $length = $this->options->getLength();
222 2
        $useNumbers = $this->options->isUseNumbers();
223 2
        $useLower = $this->options->isUseLowerCase();
224 2
        $useUpper = $this->options->isUseUpperCase();
225
226 2
        $keySpace = $useNumbers ? '0123456789' : '';
227 2
        $keySpace .= $useLower ? 'abcdefghijklmnopqrstuvwxyz' : '';
228 2
        $keySpace .= $useUpper ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '';
229
230 2
        $code = '';
231 2
        $max = mb_strlen($keySpace, '8bit') - 1;
232 2
        for ($i = 0; $i < $length; ++$i) {
233 2
            $code .= $keySpace[random_int(0, $max)];
234
        }
235
236 2
        return $code;
237
    }
238
239 2
    private function generateVerificationToken()
240
    {
241 2
        $length = $this->options->getVerificationTokenLength();
242
243
        // We divide by 2 otherwise the resulting string would be twice as long
244 2
        return bin2hex(random_bytes($length / 2));
245
    }
246
247
    /**
248
     * Verifies code without dispatching any event or making any changes.
249
     *
250
     * @param $provided
251
     * @param $expected
252
     * @return bool
253
     */
254 5
    public function checkVerificationCode($provided, $expected)
255
    {
256 5
        if ($this->options->isCaseSensitive()) {
257 1
            return $provided === $expected;
258
        } else {
259 4
            return strtolower($provided) === strtolower($expected);
260
        }
261
    }
262
263
    /**
264
     * Verifies a phone and dispatches event.
265
     *
266
     * @param PhoneVerificationInterface $phoneVerification
267
     * @param $providedCode
268
     * @return bool
269
     * @throws \Doctrine\ORM\ORMException
270
     * @throws \Doctrine\ORM\OptimisticLockException
271
     */
272 3
    public function verify(PhoneVerificationInterface $phoneVerification, $providedCode)
273
    {
274 3
        if ($this->checkVerificationCode($providedCode, $phoneVerification->getVerificationCode())) {
275 2
            $phoneVerification->setVerifiedAt(new \DateTime());
276 2
            $this->em->persist($phoneVerification);
277 2
            $this->em->flush($phoneVerification);
278
279 2
            $event = new PhoneVerificationEvent($phoneVerification, $providedCode);
280 2
            $this->dispatcher->dispatch(PhoneVerificationEvents::PHONE_VERIFIED, $event);
281
282 2
            return true;
283
        } else {
284 1
            return false;
285
        }
286
    }
287
288 4
    public function sendVerificationCode(PhoneVerificationInterface $phoneVerification, $forceSend = false)
289
    {
290 4
        $nextDate = $this->getNextResendDate($phoneVerification);
291 4
        if (!$forceSend && $nextDate > new \DateTime()) {
292
            // We can't resend the verification code yet
293 1
            $retryAfter = $nextDate->getTimestamp() - time();
294 1
            throw new TooManyRequestsHttpException(
295 1
                $retryAfter,
296 1
                "tasks.verify_phone.resend.errors.too_many_requests"
297
            );
298
        }
299
300 3
        $event = new SendPhoneVerificationEvent($phoneVerification);
301 3
        $this->dispatcher->dispatch(PhoneVerificationEvents::PHONE_VERIFICATION_REQUESTED, $event);
302
303 3
        $sentVerification = $event->getSentVerification();
304
305 3
        if (!$sentVerification) {
0 ignored issues
show
introduced by
$sentVerification is of type LoginCidadao\PhoneVerifi...ntVerificationInterface, thus it always evaluated to true.
Loading history...
306 1
            throw new VerificationNotSentException();
307
        }
308
309 2
        return $sentVerification;
310
    }
311
312
    /**
313
     * @param SentVerificationInterface $sentVerification
314
     * @return \LoginCidadao\PhoneVerificationBundle\Entity\SentVerification|SentVerificationInterface
315
     * @throws \Doctrine\ORM\ORMException
316
     * @throws \Doctrine\ORM\OptimisticLockException
317
     */
318 1
    public function registerVerificationSent(SentVerificationInterface $sentVerification)
319
    {
320 1
        $this->em->persist($sentVerification);
321 1
        $this->em->flush($sentVerification);
322
323 1
        return $sentVerification;
324
    }
325
326 5
    public function getLastSentVerification(PhoneVerificationInterface $phoneVerification)
327
    {
328 5
        return $this->sentVerificationRepository->getLastVerificationSent($phoneVerification);
329
    }
330
331 5
    public function getNextResendDate(PhoneVerificationInterface $phoneVerification)
332
    {
333 5
        $lastSentVerification = $this->getLastSentVerification($phoneVerification);
334 5
        if (!$lastSentVerification) {
335 3
            return new \DateTime();
336
        }
337
338 2
        $timeout = \DateInterval::createFromDateString($this->options->getSmsResendTimeout());
339
340 2
        return $lastSentVerification->getSentAt()->add($timeout);
341
    }
342
343
    /**
344
     * @param PhoneVerificationInterface $phoneVerification
345
     * @param string $token
346
     * @return bool
347
     * @throws \Doctrine\ORM\ORMException
348
     * @throws \Doctrine\ORM\OptimisticLockException
349
     */
350 3
    public function verifyToken(PhoneVerificationInterface $phoneVerification, $token)
351
    {
352 3
        if ($phoneVerification->isVerified()) {
353 1
            return true;
354
        }
355
356 2
        if ($phoneVerification->getVerificationToken() !== $token) {
357 1
            return false;
358
        }
359
360 1
        return $this->verify($phoneVerification, $phoneVerification->getVerificationCode());
361
    }
362
363
    /**
364
     * @inheritDoc
365
     */
366 2
    public function isVerificationMandatory(PhoneVerificationInterface $phoneVerification): bool
367
    {
368 2
        $accountsCount = $this->personRepository->countByPhone($phoneVerification->getPhone());
369
370 2
        return $accountsCount >= $this->options->getEnforceVerificationThreshold();
371
    }
372
373
    /**
374
     * @inheritDoc
375
     */
376 1
    public function countVerified(PhoneNumber $phoneNumber): int
377
    {
378 1
        return $this->phoneVerificationRepository->countVerified($phoneNumber);
379
    }
380
}
381