Completed
Push — master ( 7e20fc...28c56b )
by Igor
03:33
created

SocialAuth::parseProviderTwitter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 1
eloc 6
nc 1
nop 2
1
<?php
2
3
namespace app\services;
4
5
use yii\authclient\ClientInterface;
6
use yii\helpers\ArrayHelper;
7
use app\models\User;
8
use app\models\UserProvider;
9
10
/**
11
 * SocialAuth handles successful authentication
12
 */
13
class SocialAuth
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 $isVerified = false;
31
    /**
32
     * @var bool
33
     */
34
    private $isExist = false;
35
    /**
36
     * @var ClientInterface
37
     */
38
    private $client;
39
40
    public function __construct(ClientInterface $client)
41
    {
42
        $this->client = $client;
43
        $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
    }
45
46
    /**
47
     * @SuppressWarnings(PHPMD.ElseExpression)
48
     */
49
    public function execute()
50
    {
51
        $user = $this->findUserByProvider();
52
        if ($user) {
53
            $this->isExist = true;
54
        } else {
55
            $profile = $this->client->getUserAttributes();
56
            $this->email = ArrayHelper::getValue($profile, 'email');
57
            $this->isVerified = ArrayHelper::getValue($profile, 'verified');
58
59
            if ($this->isVerified && !empty($this->email)) {
60
                $user = User::findByEmail($this->email);
61
            }
62
63
            if (!$user) {
64
                $user = new User();
65
                $user->setProfile($this->parseProfile());
66
            }
67
68
            if ($this->isVerified) {
69
                $user->setConfirmed();
70
            }
71
        }
72
73
        $user->setProviders($this->parseProvider());
74
        $this->user = $user;
0 ignored issues
show
Documentation Bug introduced by
It seems like $user can also be of type object<app\services\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...
75
76
        return $this;
77
    }
78
79
    public function user()
80
    {
81
        return $this->user;
82
    }
83
84
    public function email()
85
    {
86
        return $this->email;
87
    }
88
89
    public function isExist()
90
    {
91
        return $this->isExist;
92
    }
93
94
    public function isVerified()
95
    {
96
        return $this->isVerified;
97
    }
98
99
    /**
100
     * Find user by provider
101
     *
102
     * @return app\models\User|null
103
     */
104
    private function findUserByProvider()
105
    {
106
        $profile = $this->client->getUserAttributes();
107
        $id = ArrayHelper::getValue($profile, 'id');
108
        if ($provider = UserProvider::findByProvider($this->type, $id)) {
109
            $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...
110
            $provider->setAttributes($this->parseProvider());
111
            $provider->save();
112
113
            return $user;
114
        }
115
        return null;
116
    }
117
118
    /**
119
     * Parse provider
120
     *
121
     * @return array
122
     */
123
    private function parseProvider()
124
    {
125
        $profile = $this->client->getUserAttributes();
126
        $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...
127
128
        $data = [];
129
        switch ($this->type) {
130
            case UserProvider::TYPE_FACEBOOK:
131
                $data = $this->parseProviderFacebook($profile, $token);
132
                break;
133
134
            case UserProvider::TYPE_VKONTAKTE:
135
                $data = $this->parseProviderVkontakte($profile, $token);
136
                break;
137
138
            case UserProvider::TYPE_TWITTER:
139
                $data = $this->parseProviderTwitter($profile, $token);
140
                break;
141
        }
142
        $data['type'] = $this->type;
143
        return $data;
144
    }
145
146
    /**
147
     * Parse profile
148
     *
149
     * @return array
150
     */
151
    private function parseProfile()
152
    {
153
        $profile = $this->client->getUserAttributes();
154
155
        $data = [];
156
        switch ($this->type) {
157
            case UserProvider::TYPE_FACEBOOK:
158
                $data = $this->parseProfileFacebook($profile);
159
                break;
160
161
            case UserProvider::TYPE_VKONTAKTE:
162
                $data = $this->parseProfileVkontakte($profile);
163
                break;
164
165
            case UserProvider::TYPE_TWITTER:
166
                $data = $this->parseProfileTwitter($profile);
167
                break;
168
        }
169
        return $data;
170
    }
171
172
    /**
173
     * Prepare provider attributes for facebook
174
     *
175
     * @param array $profile
176
     * @param array $token
177
     * @return array
178
     */
179
    private function parseProviderFacebook($profile, $token)
180
    {
181
        return [
182
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
183
            'profile_url' => ArrayHelper::getValue($profile, 'link'),
184
            'access_token' => ArrayHelper::getValue($token, 'access_token'),
185
            'access_token_secret' => ''
186
        ];
187
    }
188
189
    /**
190
     * Prepare provider attributes for vkontakte
191
     *
192
     * @param array $profile
193
     * @param array $token
194
     * @return array
195
     */
196
    private function parseProviderVkontakte($profile, $token)
197
    {
198
        return [
199
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
200
            'profile_url' => 'https://vk.com/id' . ArrayHelper::getValue($profile, 'id'),
201
            'access_token' => ArrayHelper::getValue($token, 'access_token'),
202
            'access_token_secret' => ''
203
        ];
204
    }
205
206
    /**
207
     * Prepare provider attributes for twitter
208
     *
209
     * @param array $profile
210
     * @param array $token
211
     * @return array
212
     */
213
    private function parseProviderTwitter($profile, $token)
214
    {
215
        return [
216
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
217
            'profile_url' => 'https://twitter.com/' . ArrayHelper::getValue($profile, 'screen_name'),
218
            'access_token' => ArrayHelper::getValue($token, 'oauth_token'),
219
            'access_token_secret' => ArrayHelper::getValue($token, 'oauth_token_secret')
220
        ];
221
    }
222
223
    /**
224
     * Prepare profile attributes for facebook
225
     *
226
     * @param array $profile
227
     * @return array
228
     */
229
    private function parseProfileFacebook($profile)
230
    {
231
        return [
232
            'full_name' => trim(ArrayHelper::getValue($profile, 'name')),
233
            'birth_day' => '—',
234
            'photo' => ArrayHelper::getValue($profile, 'picture.data.url', '')
235
        ];
236
    }
237
238
    /**
239
     * Prepare profile attributes for vkontakte
240
     *
241
     * @param array $profile
242
     * @return array
243
     */
244
    private function parseProfileVkontakte($profile)
245
    {
246
        $firstName = ArrayHelper::getValue($profile, 'first_name');
247
        $lastName = ArrayHelper::getValue($profile, 'last_name');
248
        $birthDay = date_create_from_format('d.m.Y', ArrayHelper::getValue($profile, 'bdate'));
249
        return [
250
            'full_name' => trim($firstName . ' ' . $lastName),
251
            'birth_day' => date_format($birthDay, 'Y-m-d'),
252
            'photo' => str_replace('_50', '_400', ArrayHelper::getValue($profile, 'photo'))
253
        ];
254
    }
255
256
    /**
257
     * Prepare profile attributes for twitter
258
     *
259
     * @param array $profile
260
     * @return array
261
     */
262
    private function parseProfileTwitter($profile)
263
    {
264
        $photo = ArrayHelper::getValue($profile, 'profile_image_url');
265
        return [
266
            'full_name' => $profile['name'],
267
            'photo' => str_replace('_normal', '_400x400', $photo)
268
        ];
269
    }
270
}
271