Completed
Push — master ( 620c99...e38637 )
by Damien
10:53 queued 06:32
created

User   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 41.78%

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 17
dl 0
loc 412
ccs 61
cts 146
cp 0.4178
rs 8.8798
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getByResponse() 0 38 5
A login() 0 23 4
A sync() 0 20 1
A save() 0 21 2
B construct() 0 40 6
B transform() 0 40 5
B assignProperty() 0 36 6
A getFieldLayoutField() 0 16 4
A setSimpleProperty() 0 19 3
A find() 0 4 1
A forceGet() 0 22 3
A getByUsernameOrEmail() 0 15 1
A getAttributeValue() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like User often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use User, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @copyright  Copyright (c) Flipbox Digital Limited
5
 */
6
7
namespace flipbox\saml\sp\services\login;
8
9
use craft\base\Component;
10
use craft\base\Field;
11
use craft\elements\User as UserElement;
12
use craft\events\ElementEvent;
13
use craft\models\FieldLayout;
14
use flipbox\saml\core\exceptions\InvalidMessage;
15
use flipbox\saml\core\helpers\ProviderHelper;
16
use flipbox\saml\sp\helpers\UserHelper;
17
use flipbox\saml\sp\models\Settings;
18
use flipbox\saml\sp\records\ProviderIdentityRecord;
19
use flipbox\saml\sp\records\ProviderRecord;
20
use flipbox\saml\sp\Saml;
21
use SAML2\Response as SamlResponse;
22
use yii\base\UserException;
23
24
/**
25
 * Class User
26
 * @package flipbox\saml\sp\services
27
 */
28
class User extends Component
29
{
30
    use AssertionTrait;
31
32
    const EVENT_BEFORE_USER_SAVE = 'eventBeforeUserSave';
33
34
    /**
35
     * @var FieldLayout|null
36
     */
37
    private $fieldLayout;
38
    /**
39
     * @var Field[]
40
     */
41
    private $fields = [];
42
43
    /**
44
     * @param SamlResponse $response
45
     * @return UserElement
46
     * @throws InvalidMessage
47
     * @throws UserException
48
     */
49
    public function getByResponse(
50
        SamlResponse $response,
51
        ProviderRecord $serviceProvider,
52
        ProviderRecord $identityProvider,
53
        Settings $settings
54
    )
55
    {
56
57
        $username = null;
58
59
        $nameIdOverride = $settings->nameIdAttributeOverride ?? $identityProvider->nameIdOverride;
0 ignored issues
show
Documentation introduced by
The property nameIdOverride does not exist on object<flipbox\saml\sp\records\ProviderRecord>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read 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.");
        }
    }

}

If the property has read access only, you can use the @property-read 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...
60
61
        if (!is_null($nameIdOverride)) {
62
            // use override
63
            foreach ($this->getAssertions(
64
                $response,
65
                $serviceProvider
66
            ) as $assertion) {
67
                $attributes = $assertion->getAttributes();
68
                if (isset($attributes[$nameIdOverride])) {
69
                    $attributeValue = $attributes[$nameIdOverride];
70
                    $username = $this->getAttributeValue($attributeValue);
71
                }
72
            }
73
        } else {
74
            // use nameid
75
            $assertion = $this->getFirstAssertion($response, $serviceProvider);
76
77
            if (! $assertion->getNameId()) {
78
                throw new InvalidMessage('Name ID is missing.');
79
            }
80
            $username = $assertion->getNameId()->getValue();
81
82
            Saml::debug('NameId: ' . $assertion->getNameId()->getValue());
83
        }
84
85
        return $this->find($username);
86
    }
87
88
    /**
89
     * @param ProviderIdentityRecord $identity
90
     * @return bool
91
     * @throws UserException
92
     * @throws \Throwable
93
     */
94
    public function login(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
95
    {
96
        if ($identity->getUser()->getStatus() !== UserElement::STATUS_ACTIVE) {
97
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
98
                throw new UserException("Can't activate user.");
99
            }
100
        }
101
102
        if (\Craft::$app->getUser()->login(
103
            $identity->getUser(),
104
            /**
105
             * @todo read session duration from the response
106
             */
107
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
108
        )
109
        ) {
110
            $identity->lastLoginDate = new \DateTime();
0 ignored issues
show
Documentation introduced by
The property lastLoginDate does not exist on object<flipbox\saml\sp\r...ProviderIdentityRecord>. 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...
111
        } else {
112
            throw new UserException("User login failed.");
113
        }
114
115
        return true;
116
    }
117
118
    /**
119
     * @param UserElement $user
120
     * @param SamlResponse $response
121
     * @param ProviderRecord $idp
122
     * @param Settings $settings
123
     * @throws UserException
124
     * @throws \Throwable
125
     * @throws \craft\errors\ElementNotFoundException
126
     * @throws \yii\base\Exception
127
     */
128
    public function sync(
129
        UserElement $user,
130
        SamlResponse $response,
131
        ProviderRecord $idp,
132
        ProviderRecord $sp,
133
        Settings $settings
134
    ) {
135
136
        // enable and transform the user
137
        $this->construct(
138
            $user,
139
            $response,
140
            $idp,
141
            $sp,
142
            $settings
143
        );
144
145
        // Save
146
        $this->save($user);
147
    }
148
149
    /**
150
     * @param UserElement $user
151
     * @return bool
152
     * @throws UserException
153
     * @throws \Throwable
154
     * @throws \craft\errors\ElementNotFoundException
155
     * @throws \yii\base\Exception
156
     */
157
    protected function save(UserElement $user)
158
    {
159
160
        $event = new ElementEvent();
161
        $event->element = $user;
162
        $event->isNew = !$user->id;
163
164
        $this->trigger(
165
            static::EVENT_BEFORE_USER_SAVE,
166
            $event
167
        );
168
169
        if (! \Craft::$app->getElements()->saveElement($user)) {
170
            Saml::error(
171
                'User save failed: ' . \json_encode($user->getErrors())
172
            );
173
            throw new UserException("User save failed: " . \json_encode($user->getErrors()));
174
        }
175
176
        return true;
177
    }
178
179
    /**
180
     * Response Based Methods
181
     */
182
183
    /**
184
     * @param UserElement $user
185
     * @param SamlResponse $response
186
     * @param ProviderRecord $idp
187
     * @param Settings $settings
188
     * @throws UserException
189
     * @throws \Throwable
190
     */
191 3
    protected function construct(
192
        UserElement $user,
193
        SamlResponse $response,
194
        ProviderRecord $idp,
195
        ProviderRecord $sp,
196
        Settings $settings
197
    ) {
198
        /**
199
         * Is User Active?
200
         */
201 3
        if (! UserHelper::isUserActive($user)) {
202
            if (! $settings->enableUsers) {
203
                throw new UserException('User access denied.');
204
            }
205
            UserHelper::enableUser($user);
206
        }
207
208 3
        foreach ($this->getAssertions($response, $sp) as $assertion) {
209 3
            $hasAttributes = count($assertion->getAttributes()) > 0;
210 3
            Saml::debug('assertion attributes: ' . \json_encode($assertion->getAttributes()));
211 3
            if ($hasAttributes) {
212 3
                $this->transform(
213 3
                    $user,
214 1
                    $response,
215 1
                    $idp,
216 1
                    $sp,
217 1
                    $settings
218
                );
219
            } else {
220
                /**
221
                 * There doesn't seem to be any attribute statements.
222
                 * Try and use username for the email and move on.
223
                 */
224
                Saml::warning(
225
                    'No attribute statements found! Trying to assign username as the email.'
226
                );
227
                $user->email = $user->email ?: $user->username;
228
            }
229
        }
230 3
    }
231
232
    /**
233
     * @param UserElement $user
234
     * @param SamlResponse $response
235
     * @return UserElement
236
     */
237 3
    protected function transform(
238
        UserElement $user,
239
        SamlResponse $response,
240
        ProviderRecord $idp,
241
        ProviderRecord $sp,
242
        Settings $settings
243
    ) {
244
245 3
        foreach ($this->getAssertions($response, $sp) as $assertion) {
246
            /**
247
             * Check the provider first
248
             */
249 3
            $attributeMap = ProviderHelper::providerMappingToKeyValue(
250 3
                $idp
251
            ) ?:
252 3
                $settings->responseAttributeMap;
0 ignored issues
show
Deprecated Code introduced by
The property flipbox\saml\sp\models\S...::$responseAttributeMap has been deprecated.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
253
254 3
            Saml::debug('Attribute Map: ' . \json_encode($attributeMap));
255
256
            /**
257
             * Loop thru attributes and set to the user
258
             */
259 3
            foreach ($assertion->getAttributes() as $attributeName => $attributeValue) {
260 3
                Saml::debug('Attributes: ' . $attributeName . ' ' . \json_encode($attributeValue));
261 3
                if (isset($attributeMap[$attributeName])) {
262 3
                    $craftProperty = $attributeMap[$attributeName];
263 3
                    $this->assignProperty(
264 3
                        $user,
265 1
                        $attributeName,
266 1
                        $attributeValue,
267 1
                        $craftProperty
268
                    );
269
                } else {
270 3
                    Saml::debug('No match for: ' . $attributeName);
271
                }
272
            }
273
        }
274
275 3
        return $user;
276
    }
277
278
    /**
279
     * @param UserElement $user
280
     * @param $attributeName
281
     * @param $attributeValue
282
     * @param $craftProperty
283
     */
284 3
    protected function assignProperty(
285
        UserElement $user,
286
        $attributeName,
287
        $attributeValue,
288
        $craftProperty
289
    ) {
290
291 3
        $originalValues = $attributeValue;
292 3
        if (is_array($attributeValue)) {
293 3
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
294
        }
295
296 3
        if (is_string($craftProperty) && in_array($craftProperty, $user->attributes())) {
297 3
            Saml::debug(
298 3
                sprintf(
299 3
                    'Attribute %s is scalar and should set value "%s" to user->%s',
300 1
                    $attributeName,
301 1
                    $attributeValue,
302 1
                    $craftProperty
303
                )
304
            );
305
306 3
            $this->setSimpleProperty($user, $craftProperty, $attributeValue);
307
        } elseif (is_callable($craftProperty)) {
308
            Saml::debug(
309
                sprintf(
310
                    'Attribute %s is handled with a callable.',
311
                    $attributeName
312
                )
313
            );
314
315
            call_user_func($craftProperty, $user, [
316
                $attributeName => $originalValues,
317
            ]);
318
        }
319 3
    }
320
321
    /**
322
     * @param UserElement $user
323
     * @return Field|null
324
     */
325 3
    protected function getFieldLayoutField(UserElement $user, $fieldHandle)
326
    {
327 3
        if (! $this->fieldLayout) {
328 3
            $this->fieldLayout = $user->getFieldLayout();
329
        }
330 3
        if (is_null($this->fieldLayout)) {
331
            return null;
332
        }
333
334 3
        if (! isset($this->fields[$fieldHandle])) {
335 3
            $this->fields[$fieldHandle] = $this->fieldLayout->getFieldByHandle($fieldHandle);
336
        }
337
338
339 3
        return $this->fields[$fieldHandle];
340
    }
341
342
    /**
343
     * @param UserElement $user
344
     * @param string $name
345
     * @param mixed $value
346
     */
347 3
    private function setSimpleProperty(UserElement $user, $name, $value)
348
    {
349 3
        $field = $this->getFieldLayoutField($user, $name);
350
351 3
        Saml::info(
352 3
            sprintf(
353 3
                '%s as %s. Is Field? %s',
354 1
                $name,
355 1
                $value,
356 3
                $field instanceof Field ? $field->id : 'Nope'
357
            )
358
        );
359
360 3
        if (! is_null($field)) {
361
            $user->setFieldValue($name, $value);
362
        } else {
363 3
            $user->{$name} = $value;
364
        }
365 3
    }
366
367
    /**************************************************
368
     * Craft User Methods
369
     **************************************************/
370
371
    /**
372
     * @param $username
373
     * @return UserElement
374
     * @throws UserException
375
     */
376
    protected function find($username)
377
    {
378
        return $this->forceGet($username);
379
    }
380
381
    /**
382
     * @param $username
383
     * @return UserElement
384
     * @throws UserException
385
     */
386
    protected function forceGet($username)
387
    {
388
389
        /**
390
         * Is there a user that exists already?
391
         */
392
        if (!($user = $this->getByUsernameOrEmail($username))) {
393
            // Should we create a new user? what's the setting say?
394
            if (! Saml::getInstance()->getSettings()->createUser) {
395
                throw new UserException("System doesn't have permission to create a new user.");
396
            }
397
398
            // new user!
399
            $user = new UserElement(
400
                [
401
                    'username' => $username,
402
                ]
403
            );
404
        }
405
406
        return $user;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $user; (array|boolean|craft\base\ElementInterface) is incompatible with the return type documented by flipbox\saml\sp\services\login\User::forceGet of type craft\elements\User.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
407
    }
408
409
    /**
410
     * @param $usernameOrEmail
411
     * @param bool $archived
412
     * @return array|bool|\craft\base\ElementInterface|UserElement|null
413
     */
414
    protected function getByUsernameOrEmail($usernameOrEmail, $archived = false)
415
    {
416
417
        return UserElement::find()
418
            ->where(
419
                [
420
                    'or',
421
                    ['username' => $usernameOrEmail],
422
                    ['email' => $usernameOrEmail],
423
                ]
424
            )
425
            ->status(null)
426
            ->archived($archived)
427
            ->one();
428
    }
429
430
    private function getAttributeValue($attributeValue)
431
    {
432
433
        if (is_array($attributeValue)) {
434
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
435
        }
436
437
        return $attributeValue;
438
    }
439
}
440