generateVerificationToken()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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