Passed
Push — master ( 7438ce...69adc8 )
by Guilherme
01:29 queued 11s
created

Nfg::connectCallback()   B

Complexity

Conditions 7
Paths 14

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 31
nc 14
nop 3
dl 0
loc 50
ccs 30
cts 30
cp 1
crap 7
rs 8.4906
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 PROCERGS\LoginCidadao\NfgBundle\Service;
12
13
use Doctrine\ORM\EntityManager;
14
use FOS\UserBundle\Model\UserManagerInterface;
15
use FOS\UserBundle\Event\FilterUserResponseEvent;
16
use FOS\UserBundle\Event\FormEvent;
17
use FOS\UserBundle\Event\GetResponseUserEvent;
18
use FOS\UserBundle\Form\Factory\FormFactory;
19
use FOS\UserBundle\FOSUserEvents;
20
use FOS\UserBundle\Security\LoginManagerInterface;
21
use FOS\UserBundle\Util\CanonicalizerInterface;
22
use LoginCidadao\CoreBundle\Model\PersonInterface;
23
use PROCERGS\LoginCidadao\NfgBundle\Entity\NfgProfile;
24
use PROCERGS\LoginCidadao\CoreBundle\Entity\PersonMeuRS;
25
use PROCERGS\LoginCidadao\CoreBundle\Helper\MeuRSHelper;
26
use PROCERGS\LoginCidadao\NfgBundle\Entity\NfgProfileRepository;
27
use PROCERGS\LoginCidadao\NfgBundle\Event\GetConnectCallbackResponseEvent;
28
use PROCERGS\LoginCidadao\NfgBundle\Event\GetDisconnectCallbackResponseEvent;
29
use PROCERGS\LoginCidadao\NfgBundle\Event\GetLoginCallbackResponseEvent;
30
use PROCERGS\LoginCidadao\NfgBundle\Exception\ConnectionNotFoundException;
31
use PROCERGS\LoginCidadao\NfgBundle\Exception\CpfInUseException;
32
use PROCERGS\LoginCidadao\NfgBundle\Exception\CpfMismatchException;
33
use PROCERGS\LoginCidadao\NfgBundle\Exception\EmailInUseException;
34
use PROCERGS\LoginCidadao\NfgBundle\Exception\MissingRequiredInformationException;
35
use PROCERGS\LoginCidadao\NfgBundle\Exception\NfgAccountCollisionException;
36
use PROCERGS\LoginCidadao\NfgBundle\Exception\NfgServiceUnavailableException;
37
use PROCERGS\LoginCidadao\NfgBundle\Exception\OverrideResponseException;
38
use PROCERGS\LoginCidadao\NfgBundle\Helper\UrlHelper;
39
use PROCERGS\LoginCidadao\NfgBundle\Mailer\MailerInterface;
40
use PROCERGS\LoginCidadao\NfgBundle\NfgEvents;
41
use PROCERGS\LoginCidadao\NfgBundle\Traits\CircuitBreakerAwareTrait;
42
use Psr\Log\LoggerAwareInterface;
43
use Psr\Log\LoggerInterface;
44
use Ramsey\Uuid\Uuid;
45
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
46
use Symfony\Component\HttpFoundation\JsonResponse;
47
use Symfony\Component\HttpFoundation\RedirectResponse;
48
use Symfony\Component\HttpFoundation\Request;
49
use Symfony\Component\HttpFoundation\Response;
50
use Symfony\Component\HttpFoundation\Session\SessionInterface;
51
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
52
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
53
use Symfony\Component\Routing\RouterInterface;
54
use Symfony\Component\Security\Core\Exception\AccountStatusException;
55
56
class Nfg implements LoggerAwareInterface
57
{
58
    use CircuitBreakerAwareTrait {
59
        reportSuccess as traitReportSuccess;
60
        reportFailure as traitReportFailure;
61
    }
62
63
    /**
64
     * Key used to store the NFG AccessID in session
65
     */
66
    const ACCESS_ID_SESSION_KEY = 'nfg.access_id';
67
68
    /** @var EntityManager */
69
    private $em;
70
71
    /** @var NfgSoapInterface */
72
    private $nfgSoap;
73
74
    /** @var RouterInterface */
75
    private $router;
76
77
    /** @var SessionInterface */
78
    private $session;
79
80
    /** @var LoginManagerInterface */
81
    private $loginManager;
82
83
    /** @var MeuRSHelper */
84
    private $meuRSHelper;
85
86
    /** @var LoggerInterface */
87
    private $logger;
88
89
    /** @var string */
90
    private $loginEndpoint;
91
92
    /** @var string */
93
    private $authorizationEndpoint;
94
95
    /** @var string */
96
    private $firewallName;
97
98
    /** @var UserManagerInterface */
99
    private $userManager;
100
101
    /** @var EventDispatcherInterface */
102
    private $dispatcher;
103
104
    /** @var FormFactory */
105
    private $formFactory;
106
107
    /** @var NfgProfileRepository */
108
    private $nfgProfileRepository;
109
110
    /** @var MailerInterface */
111
    private $mailer;
112
113
    /** @var CanonicalizerInterface */
114
    private $emailCanonicalizer;
115
116 31
    public function __construct(
117
        EntityManager $em,
118
        NfgSoapInterface $client,
119
        RouterInterface $router,
120
        SessionInterface $session,
121
        LoginManagerInterface $loginManager,
122
        MeuRSHelper $meuRSHelper,
123
        EventDispatcherInterface $dispatcher,
124
        UserManagerInterface $userManager,
125
        FormFactory $formFactory,
126
        NfgProfileRepository $nfgProfileRepository,
127
        MailerInterface $mailer,
128
        CanonicalizerInterface $emailCanonicalizer,
129
        $firewallName,
130
        $loginEndpoint,
131
        $authorizationEndpoint
132
    ) {
133 31
        $this->em = $em;
134 31
        $this->nfgSoap = $client;
135 31
        $this->router = $router;
136 31
        $this->session = $session;
137 31
        $this->loginManager = $loginManager;
138 31
        $this->meuRSHelper = $meuRSHelper;
139 31
        $this->dispatcher = $dispatcher;
140 31
        $this->userManager = $userManager;
141 31
        $this->formFactory = $formFactory;
142 31
        $this->nfgProfileRepository = $nfgProfileRepository;
143 31
        $this->mailer = $mailer;
144 31
        $this->emailCanonicalizer = $emailCanonicalizer;
145 31
        $this->firewallName = $firewallName;
146 31
        $this->loginEndpoint = $loginEndpoint;
147 31
        $this->authorizationEndpoint = $authorizationEndpoint;
148 31
    }
149
150
    /**
151
     * @return string
152
     * @throws NfgServiceUnavailableException
153
     */
154 5
    private function getAccessId()
155
    {
156 5
        $nfgSoap = $this->nfgSoap;
157
158 5
        return $this->protect(function () use ($nfgSoap) {
159
            try {
160 3
                $accessId = $nfgSoap->getAccessID();
161
162 1
                return $accessId;
163 2
            } catch (NfgServiceUnavailableException $e) {
164 1
                throw $e;
165 1
            } catch (\Exception $e) {
166 1
                throw new NfgServiceUnavailableException($e->getMessage(), 500, $e);
167
            }
168 5
        });
169
    }
170
171
    /**
172
     * @param string $accessToken
173
     * @param string|null $voterRegistration
174
     * @param bool $testRequiredInfo
175
     * @return NfgProfile
176
     */
177 18
    public function getUserInfo($accessToken, $voterRegistration = null, $testRequiredInfo = true)
178
    {
179 18
        $nfgSoap = $this->nfgSoap;
180
181
        try {
182 18
            $nfgProfile = $this->protect(function () use ($nfgSoap, $accessToken, $voterRegistration) {
0 ignored issues
show
Unused Code introduced by
The import $nfgSoap is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
183 17
                return $this->nfgSoap->getUserInfo($accessToken, $voterRegistration);
184 18
            });
185 3
        } catch (NfgServiceUnavailableException $e) {
186 2
            throw $e;
187 1
        } catch (\Exception $e) {
188 1
            throw new NfgServiceUnavailableException($e->getMessage(), 500, $e);
189
        }
190
191 15
        $requiredInfo = [$nfgProfile->getName(), $nfgProfile->getCpf(), $nfgProfile->getEmail()];
192 15
        $missingRequiredInfo = array_search(null, $requiredInfo);
193
194 15
        if ($testRequiredInfo && false !== $missingRequiredInfo) {
195 2
            throw new MissingRequiredInformationException('Some needed information was not authorized on NFG.');
196
        }
197
198 13
        return $nfgProfile;
199
    }
200
201
    /**
202
     * @return JsonResponse
203
     */
204 4
    public function login()
205
    {
206 4
        return $this->redirect($this->loginEndpoint, 'nfg_login_callback');
207
    }
208
209 6
    public function loginCallback(array $params, $secret)
210
    {
211 6
        $cpf = array_key_exists('cpf', $params) ? $params['cpf'] : null;
212 6
        $accessId = array_key_exists('accessId', $params) ? $params['accessId'] : null;
213 6
        $prsec = array_key_exists('prsec', $params) ? $params['prsec'] : null;
214
215 6
        if (!$cpf || !$accessId || !$prsec) {
216 1
            throw new BadRequestHttpException('Missing CPF, AccessID or PRSEC');
217
        }
218
219 5
        $signature = hash_hmac('sha256', "$cpf$accessId", $secret);
220 5
        if (!$signature || strcmp(strtolower($signature), strtolower($prsec)) !== 0) {
221 1
            throw new AccessDeniedHttpException('Invalid PRSEC signature.');
222
        }
223
224 4
        if ($this->session->get(self::ACCESS_ID_SESSION_KEY) !== $accessId) {
225 1
            throw new AccessDeniedHttpException('Invalid AccessID');
226
        }
227
228
        /** @var PersonInterface $user */
229 3
        $personMeuRS = $this->meuRSHelper->getPersonByCpf($this->sanitizeCpf($cpf), true);
230
231 3
        if (!$personMeuRS || !$personMeuRS->getPerson() || !$personMeuRS->getNfgAccessToken()) {
232 1
            throw new ConnectionNotFoundException('No user found matching this CPF');
233
        }
234 2
        $user = $personMeuRS->getPerson();
235
236 2
        return $this->logInUser($user, $params);
237
    }
238
239 1
    public function connect()
240
    {
241 1
        return $this->redirect($this->authorizationEndpoint, 'nfg_connect_callback');
242
    }
243
244
    /**
245
     * @param Request $request
246
     * @param PersonMeuRS $personMeuRS
247
     * @param bool $overrideExisting
248
     * @return Response
249
     */
250 19
    public function connectCallback(Request $request, PersonMeuRS $personMeuRS, $overrideExisting = false)
251
    {
252 19
        $response = null;
253 19
        $accessToken = $request->get('paccessid');
254 19
        if (!$accessToken) {
255 1
            throw new BadRequestHttpException("Missing paccessid parameter");
256
        }
257
258 18
        $nfgProfile = $this->getUserInfo($accessToken, $personMeuRS->getVoterRegistration());
259
260 13
        if (!($personMeuRS->getPerson() instanceof PersonInterface)) {
0 ignored issues
show
introduced by
$personMeuRS->getPerson() is always a sub-type of LoginCidadao\CoreBundle\Model\PersonInterface.
Loading history...
261
            try {
262 7
                $response = $this->register($request, $personMeuRS, $nfgProfile);
263 5
            } catch (OverrideResponseException $e) {
264 3
                $event = new GetConnectCallbackResponseEvent(
265 3
                    $request, $personMeuRS, $overrideExisting, $e->getResponse()
266
                );
267 3
                $this->dispatcher->dispatch(NfgEvents::CONNECT_CALLBACK_RESPONSE, $event);
268
269 3
                return $event->getResponse();
270
            }
271
        }
272
273 8
        $sanitizedCpf = $this->sanitizeCpf($nfgProfile->getCpf());
274 8
        if (!$personMeuRS->getPerson()->getCpf()) {
275 3
            $personMeuRS->getPerson()->setCpf($sanitizedCpf);
276
        }
277
278
        try {
279 8
            $this->checkCpf($personMeuRS, $nfgProfile, $overrideExisting);
280 2
        } catch (NfgAccountCollisionException $e) {
281 1
            $e->setAccessToken($accessToken);
282 1
            throw $e;
283
        }
284
285 6
        $nfgProfile = $this->syncNfgProfile($nfgProfile);
286
287 6
        $this->em->persist($nfgProfile);
288 6
        $personMeuRS->setNfgProfile($nfgProfile);
289 6
        $personMeuRS->setNfgAccessToken($accessToken);
290 6
        $this->em->flush();
291
292 6
        if (!$response) {
0 ignored issues
show
introduced by
$response is of type null, thus it always evaluated to false.
Loading history...
293 4
            $response = new RedirectResponse($this->router->generate('fos_user_profile_edit'));
294
        }
295
296 6
        $event = new GetConnectCallbackResponseEvent($request, $personMeuRS, $overrideExisting, $response);
297 6
        $this->dispatcher->dispatch(NfgEvents::CONNECT_CALLBACK_RESPONSE, $event);
298
299 6
        return $event->getResponse();
300
    }
301
302
    /**
303
     * @param PersonMeuRS $personMeuRS
304
     * @return Response
305
     */
306 2
    public function disconnect(PersonMeuRS $personMeuRS)
307
    {
308 2
        if ($personMeuRS->getNfgProfile()) {
309 2
            $this->em->remove($personMeuRS->getNfgProfile());
310 2
            $personMeuRS->setNfgAccessToken(null);
311 2
            $personMeuRS->setNfgProfile(null);
312 2
            $this->em->flush();
313
        }
314
315 2
        $response = new RedirectResponse($this->router->generate('fos_user_profile_edit'));
316 2
        $event = new GetDisconnectCallbackResponseEvent($personMeuRS, $response);
317 2
        $this->dispatcher->dispatch(NfgEvents::DISCONNECT_CALLBACK_RESPONSE, $event);
318
319 2
        return $event->getResponse();
320
    }
321
322 5
    private function redirect($endpoint, $callbackRoute)
323
    {
324 5
        $accessId = $this->getAccessId();
325 1
        $this->session->set(self::ACCESS_ID_SESSION_KEY, $accessId);
326 1
        $callbackUrl = $this->router->generate($callbackRoute, [], RouterInterface::ABSOLUTE_URL);
327
328 1
        $url = parse_url($endpoint);
329 1
        $url['query'] = UrlHelper::addToQuery(
330
            [
331 1
                'accessid' => $accessId,
332 1
                'urlretorno' => $callbackUrl,
333
            ],
334 1
            isset($url['query']) ? $url['query'] : null
335
        );
336
337 1
        return new JsonResponse(['target' => http_build_url($url)]);
338
    }
339
340
    /**
341
     * Sets a logger instance on the object.
342
     *
343
     * @param LoggerInterface $logger
344
     *
345
     * @return void
346
     */
347 2
    public function setLogger(LoggerInterface $logger)
348
    {
349 2
        $this->logger = $logger;
350 2
    }
351
352 15
    private function sanitizeCpf($cpf)
353
    {
354 15
        return str_pad(preg_replace('/[^0-9]/', '', $cpf), 11, '0', STR_PAD_LEFT);
355
    }
356
357
    /**
358
     * @param PersonMeuRS $personMeuRS
359
     * @param NfgProfile $nfgProfile
360
     * @param bool $overrideExisting
361
     */
362 8
    private function checkCpf(PersonMeuRS $personMeuRS, NfgProfile $nfgProfile, $overrideExisting = false)
363
    {
364 8
        $person = $personMeuRS->getPerson();
365
366
        // Check data inconsistency
367 8
        if ($person->getCpf() !== $this->sanitizeCpf($nfgProfile->getCpf())) {
368 1
            throw new CpfMismatchException("User's CPF doesn't match CPF from NFG.");
369
        }
370
371
        // Check CPF collision
372 7
        $otherPerson = $this->meuRSHelper->getPersonByCpf($person->getCpf(), true);
373 7
        if (null === $otherPerson || $otherPerson->getId() === $personMeuRS->getId()) {
374
            // No collision found. We're good! :)
375 4
            return;
376
        }
377
378 3
        if (!$otherPerson->getNfgProfile()) {
379
            // The other person isn't linked with NFG, so $person can safely get the CPF
380 1
            $otherPerson->getPerson()->setCpf(null);
381 1
            $this->em->persist($otherPerson->getPerson());
382 1
            $this->em->flush($otherPerson->getPerson());
383
384 1
            $this->mailer->notifyCpfLost($otherPerson->getPerson());
385
386 1
            return;
387
        }
388
389
        // Both users are linked to the same NFG account
390
        // What should we do?
391 2
        if (false === $overrideExisting) {
392 1
            throw new NfgAccountCollisionException();
393
        }
394
        // The user's choice was to remove the previous connection and use this new one
395 1
        $otherPerson->getPerson()->setCpf(null);
396 1
        $this->em->persist($otherPerson->getPerson());
397 1
        $this->em->flush($otherPerson->getPerson());
398 1
        $this->disconnect($otherPerson);
399 1
        $this->mailer->notifyConnectionTransferred($otherPerson->getPerson());
400 1
    }
401
402
    /**
403
     * @param Request $request
404
     * @param PersonMeuRS $personMeuRS
405
     * @param NfgProfile $nfgProfile
406
     * @return null|RedirectResponse|Response
407
     * @throws OverrideResponseException
408
     */
409 7
    private function register(Request $request, PersonMeuRS $personMeuRS, NfgProfile $nfgProfile)
410
    {
411 7
        $email = $this->emailCanonicalizer->canonicalize($nfgProfile->getEmail());
412 7
        if ($this->meuRSHelper->getPersonByEmail($email, true) !== null) {
413 1
            throw new EmailInUseException();
414
        }
415
416 6
        $sanitizedCpf = $this->sanitizeCpf($nfgProfile->getCpf());
417 6
        $otherPersonMeuRS = $this->meuRSHelper->getPersonByCpf($sanitizedCpf, true);
418
419 6
        if ($otherPersonMeuRS !== null) {
420 3
            if ($otherPersonMeuRS->getNfgProfile()) {
421 1
                $otherPersonNfgCpf = $otherPersonMeuRS->getNfgProfile()->getCpf();
422
            } else {
423 2
                $otherPersonNfgCpf = null;
424
            }
425 3
            if ($otherPersonMeuRS->getNfgAccessToken() && $otherPersonNfgCpf == $sanitizedCpf) {
426 1
                $response = $this->logInUser($otherPersonMeuRS->getPerson());
427 1
                throw new OverrideResponseException($response);
428
            }
429 2
            $this->handleCpfCollision($otherPersonMeuRS);
430
        }
431
432 4
        $names = explode(' ', $nfgProfile->getName());
433
434
        /** @var PersonInterface $user */
435 4
        $user = $this->userManager->createUser();
436 4
        $user->setUsername(Uuid::uuid4()->toString());
437
        $user
438 4
            ->setFirstName(array_shift($names))
439 4
            ->setSurname(implode(' ', $names))
440 4
            ->setEmail($nfgProfile->getEmail())
441 4
            ->setCpf($sanitizedCpf)
442 4
            ->setBirthdate($nfgProfile->getBirthdate())
443 4
            ->setMobile($nfgProfile->getMobile())
444 4
            ->setPassword('')
445 4
            ->setEnabled(true);
446
447 4
        $event = new GetResponseUserEvent($user, $request);
448 4
        $this->dispatcher->dispatch(FOSUserEvents::REGISTRATION_INITIALIZE, $event);
449
450 4
        if (null !== $event->getResponse()) {
451 2
            throw new OverrideResponseException($event->getResponse());
452
        }
453
454 2
        $form = $this->formFactory->createForm();
455 2
        $form->setData($user);
456
457 2
        $event = new FormEvent($form, $request);
458 2
        $this->dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event);
459
460 2
        $this->userManager->updateUser($user);
461
462 2
        $nfgProfile = $this->syncNfgProfile($nfgProfile);
463 2
        $personMeuRS->setPerson($user);
464 2
        $personMeuRS->setNfgProfile($nfgProfile);
465 2
        $this->em->persist($personMeuRS);
466 2
        $this->em->persist($nfgProfile);
467 2
        $this->em->flush();
468
469 2
        if (null === $response = $event->getResponse()) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $response is correct as $event->getResponse() targeting FOS\UserBundle\Event\FormEvent::getResponse() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
introduced by
The condition null === $response = $event->getResponse() is always true.
Loading history...
470 2
            $url = $this->router->generate('fos_user_registration_confirmed');
471 2
            $response = new RedirectResponse($url);
472
        }
473
474 2
        $this->dispatcher->dispatch(
475 2
            FOSUserEvents::REGISTRATION_COMPLETED,
476 2
            new FilterUserResponseEvent($user, $request, $response)
477
        );
478
479 2
        return $response;
480
    }
481
482
    /**
483
     * @param NfgProfile $latestNfgProfile
484
     * @return NfgProfile
485
     */
486 6
    public function syncNfgProfile(NfgProfile $latestNfgProfile)
487
    {
488 6
        $existingNfgProfile = $this->nfgProfileRepository->findByCpf($latestNfgProfile->getCpf());
489
490 6
        if ($existingNfgProfile instanceof NfgProfile) {
491
            $existingNfgProfile
492 1
                ->setName($latestNfgProfile->getName())
493 1
                ->setEmail($latestNfgProfile->getEmail())
494 1
                ->setBirthdate($latestNfgProfile->getBirthdate())
495 1
                ->setMobile($latestNfgProfile->getMobile())
496 1
                ->setAccessLvl($latestNfgProfile->getAccessLvl())
497 1
                ->setVoterRegistration($latestNfgProfile->getVoterRegistration())
498 1
                ->setVoterRegistrationSit($latestNfgProfile->getVoterRegistrationSit());
499
500 1
            return $existingNfgProfile;
501
        } else {
502 5
            return $latestNfgProfile;
503
        }
504
    }
505
506 3
    private function logInUser(PersonInterface $user, array $params = [])
507
    {
508 3
        $response = new RedirectResponse($this->router->generate('lc_home'));
509
510
        try {
511 3
            $this->loginManager->logInUser($this->firewallName, $user, $response);
512 1
        } catch (AccountStatusException $e) {
513
            // User account is disabled or something like that
514 1
            throw $e;
515
        }
516
517 2
        $event = new GetLoginCallbackResponseEvent($params, $response);
518 2
        $this->dispatcher->dispatch(NfgEvents::LOGIN_CALLBACK_RESPONSE, $event);
519
520 2
        return $event->getResponse();
521
    }
522
523 2
    private function handleCpfCollision(PersonMeuRS $otherPersonMeuRS)
524
    {
525 2
        if (!$otherPersonMeuRS->getNfgAccessToken()) {
526 1
            $otherPersonMeuRS->getPerson()->setCpf(null);
527 1
            $this->mailer->notifyCpfLost($otherPersonMeuRS->getPerson());
528 1
            $this->em->persist($otherPersonMeuRS->getPerson());
529 1
            $this->em->flush($otherPersonMeuRS->getPerson());
530
        } else {
531 1
            throw new CpfInUseException();
532
        }
533 1
    }
534
}
535