Completed
Push — master ( 8152db...56e446 )
by Igor
04:29
created

AuthHandler::parseProfileFacebook()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
c 1
b 0
f 0
cc 1
eloc 5
nc 1
nop 1
crap 1
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 2
            $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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $user can also be of type object<app\components\auth\app\models\User>. However, the property $user is declared as type object<app\models\User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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 2
            $user = $provider->user;
0 ignored issues
show
Documentation introduced by
The property user does not exist on object<app\models\UserProvider>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
106 2
            $provider->setAttributes($this->parseProvider());
107 2
            $provider->save();
108
109 2
            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