Completed
Push — master ( b9c500...b355e1 )
by Allan
03:25
created

UsersController::sendTokenByEmail()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 3
Bugs 1 Features 1
Metric Value
c 3
b 1
f 1
dl 0
loc 10
ccs 9
cts 9
cp 1
rs 9.4286
cc 1
eloc 8
nc 1
nop 2
crap 1
1
<?php
2
3
namespace SMG\UserBundle\Controller;
4
5
use Symfony\Component\HttpFoundation\Request;
6
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
7
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
8
use Symfony\Component\HttpFoundation\Response;
9
use FOS\RestBundle\Controller\FOSRestController;
10
use FOS\RestBundle\Controller\Annotations;
11
use FOS\RestBundle\View\View;
12
use Symfony\Component\Validator\Constraints as Assert;
13
use SMG\UserBundle\Entity\User;
14
use SMG\ManagerBundle\Controller\Traits\HandleUserTrait;
15
16
class UsersController extends FOSRestController
17
{
18
    use HandleUserTrait;
19
20
    const TOKEN_DIGITS = 6;
21
    /**
22
     * @Annotations\Post("/users")
23
     * @ParamConverter("user", converter="fos_rest.request_body")
24
     */
25 10
    public function postUsersAction(User $user)
26
    {
27 10
        $manager = $this->get('fos_user.user_manager');
28
29 10
        $token = $manager->deleteIfNonEnabledExists($user);
30 10
        $errors = $this->validates(
31
            $user,
0 ignored issues
show
Documentation introduced by
$user is of type object<SMG\UserBundle\Entity\User>, but the function expects a object<SMG\ManagerBundle\Controller\Traits\User>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
32 10
            'mobile_app_registration'
33
        );
34 10
        if (count($errors) > 0) {
35 5
            return $this->handleView(
36 5
                new View($errors, Response::HTTP_BAD_REQUEST)
37
            );
38
        }
39
40
        // we need to create a new user to get the salt
41 8
        $newUser = $manager->createUser();
42
43 8
        $phoneNumber = $user->getPhoneNumber();
44 8
        $email = $user->getEmail();
45
46
        // normalize phone number with international suffix
47 8
        if (!is_null($phoneNumber)) {
48 5
            $phoneNumber = str_replace('+', '00', $phoneNumber);
49
        }
50
51 8
        $newUser->setUsername($user->getUsername());
52 8
        $newUser->setPhoneNumber($phoneNumber);
53 8
        $newUser->setEmail($email);
54 8
        $newUser->setPlainPassword($user->getPlainPassword());
55 8
        $newUser->setRoles(array('ROLE_USER'));
56
57 8
        if (is_null($token)) {
58 8
            $token = $this->generateToken();
59
        }
60 8
        $this->sendToken($email, $phoneNumber, $token);
61
62 8
        $newUser->setConfirmationToken($token);
63 8
        $newUser->setEnabled(false);
64 8
        $newUser->setLocked(true);
65 8
        $manager->updateUser($newUser);
66
67 8
        return $this->handleView(
68 8
            new View(
69
                array(
70 8
                    'id' => $newUser->getId(),
71
                ),
72 8
                Response::HTTP_ACCEPTED
73
            )
74
        );
75
    }
76
77
    /**
78
     * @Annotations\Patch("/users/{id}/password")
79
     */
80 2
    public function patchUserPasswordAction(User $user, Request $request)
81
    {
82 2
        $requestData = $this->requestIsJsonWithKeysOrThrow(
83
            $request,
84 2
            ['new_password', 'old_password']
85
        );
86
87 2 View Code Duplication
        if ($this->isPasswordCorrect($user, $requestData['old_password'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
88 1
            return $this->handleView(
89 1
                new View(
90 1
                    ['message' => 'bst.password.wrong'],
91 1
                    Response::HTTP_FORBIDDEN
92
                )
93
            );
94
        }
95
96 1
        $this->updateUserPassword($user, $requestData['new_password']);
97
98
        //TODO maybe replace by something more informative
99 1
        return $this->handleView(new View());
100
    }
101
102
    /**
103
     * Request change user's email or phone.
104
     *
105
     * @Annotations\Patch("/users/{id}/request-change-contact-info")
106
     */
107 6
    public function patchUserRequestChangeContactInfoAction(
108
        User $user,
109
        Request $request
110
    ) {
111 6
        $requestData = $this->requestIsJsonWithKeysOrThrow(
112
            $request,
113 6
            ['new_contact_info', 'password']
114
        );
115
116 5 View Code Duplication
        if ($this->isPasswordCorrect($user, $requestData['password'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
117 1
            return $this->handleView(
118 1
                new View(
119 1
                    ['message' => 'bst.password.wrong'],
120 1
                    Response::HTTP_FORBIDDEN
121
                )
122
            );
123
        }
124
125 4
        $contactInfo = $requestData['new_contact_info'];
126
127 4
        $validator = $this->container->get('validator');
128
129 4
        $token = $this->generateToken();
130 4
        $user->setConfirmationToken($token);
131
132 4
        $emailAssert = new Assert\Email();
133 4
        $emailAssert->message = 'bst.email.invalid';
134
135 4
        $errors = $validator->validateValue($contactInfo, $emailAssert);
136 4
        if (count($errors) === 0) {
137 1
            $this->sendTokenByEmail($contactInfo, $token);
138 1
            $this->get('fos_user.user_manager')->updateUser($user);
139
140 1
            return $this->handleView(new View());
141
        }
142
143 3
        $oldPhoneNumber = $user->getPhoneNumber();
144
        // we set user directly here so we can reuse the validator
145
        // of User entity for phone number
146 3
        $phoneNumber = str_replace('+', '00', $contactInfo);
147 3
        $user->setPhoneNumber($phoneNumber);
148
149 3
        $errors = $validator->validate($user, ['phone_check']);
150 3
        if (count($errors) === 0) {
151 1
            $this->sendTokenByPhone($phoneNumber, $token);
152
153
            // we put back the old phone number as it will be updated
154
            // only if the user send us back the confirmation token
155 1
            $user->setPhoneNumber($oldPhoneNumber);
156 1
            $this->get('fos_user.user_manager')->updateUser($user);
157
158 1
            return $this->handleView(new View());
159
        }
160
161 2
        return $this->handleView(
162 2
            new View(
163 2
                ['message' => 'bst.changecontactinfo.invalid'],
164 2
                Response::HTTP_BAD_REQUEST
165
            )
166
        );
167
    }
168
169
    /**
170
     * change user's email or phone, with validation code received in previous step.
171
     *
172
     * @Annotations\Patch("/users/{id}/contact-info")
173
     */
174 5
    public function patchUserChangeContactInfoAction(User $user, Request $request)
175
    {
176 5
        $requestData = $this->requestIsJsonWithKeysOrThrow(
177
            $request,
178 5
            ['new_contact_info', 'validation_code']
179
        );
180
181 5
        if ($requestData['validation_code'] !== $user->getConfirmationToken()) {
182 1
            throw new BadRequestHttpException('wrong validation code');
183
        }
184
185 4
        $contactInfo = $requestData['new_contact_info'];
186
187 4
        $manager = $this->get('fos_user.user_manager');
188 4
        $validator = $this->container->get('validator');
189
190 4
        $emailAssert = new Assert\Email();
191 4
        $emailAssert->message = 'bst.email.invalid';
192
193 4
        $errors = $validator->validateValue($contactInfo, $emailAssert);
194 4 View Code Duplication
        if (count($errors) === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
195 1
            $this->get('logger')->info(
196
                'updated email of '.
197 1
                $user->getId().
198 1
                ' with '.
199 1
                $contactInfo
200
            );
201 1
            $user->setEmail($contactInfo);
202 1
            $manager->updateUser($user);
203
204 1
            return $this->handleView(new View());
205
        }
206
207
        // we set user directly here so we can reuse the validator
208
        // of User entity for phone number
209 3
        $phoneNumber = str_replace('+', '00', $contactInfo);
210 3
        $user->setPhoneNumber($phoneNumber);
211
212 3
        $errors = $validator->validate($user, ['phone_check']);
213 3 View Code Duplication
        if (count($errors) === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
214 1
            $this->get('logger')->info(
215
                'updated phone of '.
216 1
                $user->getId().
217 1
                ' with '.
218 1
                $phoneNumber
219
            );
220 1
            $manager->updateUser($user);
221
222 1
            return $this->handleView(new View());
223
        }
224
225 2
        return $this->handleView(
226 2
            new View(
227 2
                ['message' => 'bst.changecontactinfo.invalid'],
228 2
                Response::HTTP_BAD_REQUEST
229
            )
230
        );
231
    }
232
233
    /**
234
     * Permit a user who has forgotten his password to request
235
     * a validation to be sent to either his email or phone number.
236
     *
237
     * @Annotations\Post("/users/forgot-password")
238
     */
239 2
    public function postUsersForgotPasswordAction(Request $request)
240
    {
241 2
        $requestData = $this->requestIsJsonWithKeysOrThrow(
242
            $request,
243 2
            ['contact_info']
244
        );
245
246 2
        $contactInfo = $requestData['contact_info'];
247
248 2
        $manager = $this->get('fos_user.user_manager');
249
250
        // it will check also email and phone number
251 2
        $user = $manager->loadUserByUsername($contactInfo);
252
253 2
        if (is_null($user)) {
254 1
            throw $this->createNotFoundException();
255
        }
256
257 1
        $token = $this->generateToken();
258 1
        $this->sendToken(
259 1
            $user->getEmail(),
260 1
            $user->getPhoneNumber(),
261
            $token
262
        );
263 1
        $user->setConfirmationToken($token);
264
265 1
        $manager->updateUser($user);
266
267 1
        return $this->handleView(
268 1
            new View(['id' => $user->getId()])
269
        );
270
    }
271
272
    /**
273
     * Used for a user to resend his confirmation token.
274
     *
275
     * @param User    $user    the user who's reseting password
276
     * @param Request $request
0 ignored issues
show
Bug introduced by
There is no parameter named $request. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
277
     *
278
     * @Annotations\Patch("/users/{id}/resend-confirmation-token")
279
     */
280 1
    public function patchUserResendConfirmationTokenAction(User $user)
281
    {
282 1
        $this->sendToken(
283 1
            $user->getEmail(),
284 1
            $user->getPhoneNumber(),
285 1
            $user->getConfirmationToken()
286
        );
287
288 1
        return $this->handleView(new View());
289
    }
290
291
    /**
292
     * Used for a user to reset his password if he's in possession
293
     * of a validation code send to him during an earlier step.
294
     *
295
     * @param User    $user    the user who's reseting password
296
     * @param Request $request
297
     *
298
     * @Annotations\Patch("/users/{id}/reset-password")
299
     */
300 2
    public function patchUserResetPasswordAction(
301
        User $user,
302
        Request $request
303
    ) {
304 2
        $requestData = $this->requestIsJsonWithKeysOrThrow(
305
            $request,
306 2
            ['new_password', 'validation_code']
307
        );
308
309 2
        if ($requestData['validation_code'] !== $user->getConfirmationToken()) {
310 1
            throw new BadRequestHttpException();
311
        }
312
313 1
        $this->updateUserPassword($user, $requestData['new_password']);
314
315 1
        return $this->handleView(new View());
316
    }
317
318
    /**
319
     * @Annotations\Put("/users/{id}/confirmation-token/{confirmationToken}")
320
     */
321 2
    public function putUserActivationCodeAction(User $user, $confirmationToken)
322
    {
323 2
        if ($user->getConfirmationToken() !== $confirmationToken) {
324 1
            throw $this->createNotFoundException('No confirmation token invalid');
325
        }
326
327 1
        $user->setEnabled(true);
328 1
        $user->setLocked(false);
329
330 1
        $this->get('fos_user.user_manager')->updateUser($user);
331
332 1
        return $this->handleView(
333 1
            new View(
334
                //TODO maybe replace by something more informative
335 1
                array(),
336 1
                Response::HTTP_CREATED
337
            )
338
        );
339
    }
340
341
    /**
342
     * @return bool
343
     */
344 7
    private function isPasswordCorrect(User $user, $password)
345
    {
346 7
        $encoderService = $this->get('security.encoder_factory');
347 7
        $encoder = $encoderService->getEncoder($user);
348 7
        $encodedPass = $encoder->encodePassword(
349
            $password,
350 7
            $user->getSalt()
351
        );
352
353 7
        return $encodedPass !== $user->getPassword();
354
    }
355
356
    /**
357
     */
358 2
    private function updateUserPassword(User $user, $newPassword)
359
    {
360 2
        $user->setPlainPassword($newPassword);
361 2
        $manager = $this->get('fos_user.user_manager');
362 2
        $manager->updateUser($user);
363 2
    }
364
365
    /**
366
     * Check if the JSON sent data is correct
367
     * for the current called action
368
     * and throws a bad request exception if the input is wrong.
369
     *
370
     * @return array
371
     */
372 17
    private function requestIsJsonWithKeysOrThrow(
373
        Request $request,
374
        array $keys,
375
        $message = 'bst.json.field_missing'
376
    ) {
377 17
        $json = json_decode($request->getContent(), true);
378
379 17
        foreach ($keys as $key) {
380 17
            if (empty($json[$key])) {
381 17
                throw new BadRequestHttpException($message);
382
            }
383
        }
384
385 16
        return $json;
386
    }
387
388
    /**
389
     * /!\ This method does not validate the input
390
     * making sure the email is a real email and phone is an actual phone
391
     * is the responsability of the calling method.
392
     *
393
     * @param string|null $email if null, email not sent
394
     * @param string|null $phone if null, SMS not sent
395
     * @param string      $token token to send to the user
396
     */
397 10
    private function sendToken(
398
        $email,
399
        $phone,
400
        $token
401
    ) {
402 10
        if (!empty($email)) {
403 5
            $this->sendTokenByEmail($email, $token);
404
        }
405
406 10
        if (!empty($phone)) {
407 6
            $this->sendTokenByPhone($phone, $token);
408
        }
409 10
    }
410
411
    /**
412
     *
413
     */
414 6
    private function sendTokenByEmail($email, $token)
415
    {
416 6
        $mailer = $this->get('mailer');
417 6
        $message = $mailer->createMessage()
418 6
            ->setSubject('Your token')
419 6
            ->setFrom($this->container->getParameter('mailer_user'))
420 6
            ->setTo($email)
421 6
            ->setBody('Token '.$token, 'text/plain');
422 6
        $mailer->send($message);
423 6
    }
424
425
    /**
426
     *
427
     */
428 7
    private function sendTokenByPhone($phone, $token)
429
    {
430 7
        $smsSender = $this->container->get('sms');
431 7
        $sms = $smsSender->createSms($phone, $token);
432 7
        $result = $smsSender->sendSms($sms);
433 7
        $this->get('logger')->info("sms token $token send to phone $phone");
434 7
        if (is_array($result)) {
435
            $this->get('logger')->info($result[0]);
436
        }
437 7
    }
438
439 13
    private function generateToken()
440
    {
441
        // shamelessy taken from http://stackoverflow.com/a/8216031/1185460
442
        // one could get('fos_user.util.token_generator') instead
443
        // but it's not mobile user friendly
444
445 13
        return str_pad(
446 13
            rand(0, pow(10, self::TOKEN_DIGITS) - 1),
447 13
            self::TOKEN_DIGITS,
448 13
            '0',
449 13
            STR_PAD_LEFT
450
        );
451
    }
452
}
453