Completed
Push — master ( f45cc5...2edd5b )
by Igor
05:32
created

AuthHandler::handle()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0073

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 25
ccs 14
cts 15
cp 0.9333
rs 8.439
cc 5
eloc 16
nc 5
nop 0
crap 5.0073
1
<?php
2
3
namespace app\components\auth;
4
5
use yii\authclient\ClientInterface;
6
use yii\helpers\ArrayHelper;
7
use app\models\User;
8
use app\models\UserProvider;
9
10
/**
11
 * AuthHandler handles successful authentication
12
 */
13
class AuthHandler
14
{
15
    /**
16
     * @var int
17
     */
18
    private $type;
19
    /**
20
     * @var User
21
     */
22
    private $user;
23
    /**
24
     * @var string
25
     */
26
    private $email;
27
    /**
28
     * @var bool
29
     */
30
    private $verified = false;
31
    /**
32
     * @var bool
33
     */
34
    private $exist = false;
35
    /**
36
     * @var ClientInterface
37
     */
38
    private $client;
39
40 8
    public function __construct(ClientInterface $client)
41
    {
42 8
        $this->client = $client;
43 8
        $this->type = UserProvider::getTypeByName($client->id);
0 ignored issues
show
Bug introduced by
Accessing id on the interface yii\authclient\ClientInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Documentation Bug introduced by
The property $type was declared of type integer, but \app\models\UserProvider...TypeByName($client->id) is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
44 8
    }
45
46
    /**
47
     * @SuppressWarnings(PHPMD.ElseExpression)
48
     */
49 8
    public function handle()
50
    {
51 8
        $user = $this->findUserByProvider();
52 8
        if ($user) {
53
            $this->exist = true;
54
        } else {
55 8
            $profile = $this->client->getUserAttributes();
56 8
            $this->email = ArrayHelper::getValue($profile, 'email');
57 8
            $this->verified = ArrayHelper::getValue($profile, 'verified');
58
59 8
            if ($this->verified && !empty($this->email)) {
60 1
                $user = User::findByEmail($this->email);
61
            }
62
63 8
            if (!$user) {
64 8
                $user = new User();
65 8
                $user->setProfile($this->parseProfile());
66
            }
67
        }
68
69 8
        $user->setProviders($this->parseProvider());
70 8
        $this->user = $user;
71
72 8
        return $this;
73
    }
74
75 8
    public function getUser()
76
    {
77 8
        return $this->user;
78
    }
79
80 8
    public function getEmail()
81
    {
82 8
        return $this->email;
83
    }
84
85 8
    public function isExist()
86
    {
87 8
        return $this->exist;
88
    }
89
90 8
    public function isVerified()
91
    {
92 8
        return $this->verified;
93
    }
94
95
    /**
96
     * Find user by provider
97
     *
98
     * @return app\models\User|null
99
     */
100 8
    private function findUserByProvider()
101
    {
102 8
        $profile = $this->client->getUserAttributes();
103 8
        $id = ArrayHelper::getValue($profile, 'id');
104 8
        if ($provider = UserProvider::findByProvider($this->type, $id)) {
105
            $user = $provider->user;
106
            $provider->setAttributes($this->parseProvider());
0 ignored issues
show
Bug introduced by
The method setAttributes cannot be called on $provider (of type array|boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
107
            $provider->save();
0 ignored issues
show
Bug introduced by
The method save cannot be called on $provider (of type array|boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
108
109
            return $user;
110
        }
111 8
        return null;
112
    }
113
114
    /**
115
     * Parse provider
116
     *
117
     * @return array
118
     */
119 8
    private function parseProvider()
120
    {
121 8
        $profile = $this->client->getUserAttributes();
122 8
        $token = $this->client->getAccessToken()->getParams();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface yii\authclient\ClientInterface as the method getAccessToken() does only exist in the following implementations of said interface: yii\authclient\BaseOAuth, yii\authclient\OAuth1, yii\authclient\OAuth2, yii\authclient\clients\Facebook, yii\authclient\clients\GitHub, yii\authclient\clients\Google, yii\authclient\clients\GoogleHybrid, yii\authclient\clients\LinkedIn, yii\authclient\clients\Live, yii\authclient\clients\Twitter, yii\authclient\clients\VKontakte, yii\authclient\clients\Yandex.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
123
124 8
        $data = [];
125 8
        switch ($this->type) {
126 8
            case UserProvider::TYPE_FACEBOOK:
127 1
                $data = $this->parseProviderFacebook($profile, $token);
128 1
                break;
129
130 7
            case UserProvider::TYPE_VKONTAKTE:
131 6
                $data = $this->parseProviderVkontakte($profile, $token);
132 6
                break;
133
134 1
            case UserProvider::TYPE_TWITTER:
135 1
                $data = $this->parseProviderTwitter($profile, $token);
136 1
                break;
137
        }
138 8
        $data['type'] = $this->type;
139 8
        return $data;
140
    }
141
142
    /**
143
     * Parse profile
144
     *
145
     * @return array
146
     */
147 8
    private function parseProfile()
148
    {
149 8
        $profile = $this->client->getUserAttributes();
150
151 8
        $data = [];
152 8
        switch ($this->type) {
153 8
            case UserProvider::TYPE_FACEBOOK:
154 1
                $data = $this->parseProfileFacebook($profile);
155 1
                break;
156
157 7
            case UserProvider::TYPE_VKONTAKTE:
158 6
                $data = $this->parseProfileVkontakte($profile);
159 6
                break;
160
161 1
            case UserProvider::TYPE_TWITTER:
162 1
                $data = $this->parseProfileTwitter($profile);
163 1
                break;
164
        }
165 8
        return $data;
166
    }
167
168
    /**
169
     * Prepare provider attributes for facebook
170
     *
171
     * @param array $profile
172
     * @param array $token
173
     * @return array
174
     */
175 1
    private function parseProviderFacebook($profile, $token)
176
    {
177
        return [
178 1
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
179 1
            'profile_url' => ArrayHelper::getValue($profile, 'link'),
180 1
            'access_token' => ArrayHelper::getValue($token, 'access_token'),
181 1
            'access_token_secret' => ''
182
        ];
183
    }
184
185
    /**
186
     * Prepare provider attributes for vkontakte
187
     *
188
     * @param array $profile
189
     * @param array $token
190
     * @return array
191
     */
192 6
    private function parseProviderVkontakte($profile, $token)
193
    {
194
        return [
195 6
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
196 6
            'profile_url' => 'https://vk.com/id' . ArrayHelper::getValue($profile, 'id'),
197 6
            'access_token' => ArrayHelper::getValue($token, 'access_token'),
198 6
            'access_token_secret' => ''
199
        ];
200
    }
201
202
    /**
203
     * Prepare provider attributes for twitter
204
     *
205
     * @param array $profile
206
     * @param array $token
207
     * @return array
208
     */
209 1
    private function parseProviderTwitter($profile, $token)
210
    {
211
        return [
212 1
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
213 1
            'profile_url' => 'https://twitter.com/' . ArrayHelper::getValue($profile, 'screen_name'),
214 1
            'access_token' => ArrayHelper::getValue($token, 'oauth_token'),
215 1
            'access_token_secret' => ArrayHelper::getValue($token, 'oauth_token_secret')
216
        ];
217
    }
218
219
    /**
220
     * Prepare profile attributes for facebook
221
     *
222
     * @param array $profile
223
     * @return array
224
     */
225 1
    private function parseProfileFacebook($profile)
226
    {
227
        return [
228 1
            'full_name' => trim(ArrayHelper::getValue($profile, 'name')),
229 1
            'birth_day' => '—',
230 1
            'photo' => ArrayHelper::getValue($profile, 'picture.data.url', '')
231
        ];
232
    }
233
234
    /**
235
     * Prepare profile attributes for vkontakte
236
     *
237
     * @param array $profile
238
     * @return array
239
     */
240 6
    private function parseProfileVkontakte($profile)
241
    {
242 6
        $firstName = ArrayHelper::getValue($profile, 'first_name');
243 6
        $lastName = ArrayHelper::getValue($profile, 'last_name');
244 6
        $birthDay = date_create_from_format('d.m.Y', ArrayHelper::getValue($profile, 'bdate'));
245
        return [
246 6
            'full_name' => trim($firstName . ' ' . $lastName),
247 6
            'birth_day' => date_format($birthDay, 'Y-m-d'),
248 6
            'photo' => str_replace('_50', '_400', ArrayHelper::getValue($profile, 'photo'))
249
        ];
250
    }
251
252
    /**
253
     * Prepare profile attributes for twitter
254
     *
255
     * @param array $profile
256
     * @return array
257
     */
258 1
    private function parseProfileTwitter($profile)
259
    {
260 1
        $photo = ArrayHelper::getValue($profile, 'profile_image_url');
261
        return [
262 1
            'full_name' => $profile['name'],
263 1
            'photo' => str_replace('_normal', '_400x400', $photo)
264
        ];
265
    }
266
}
267