Completed
Push — master ( 87925f...2aa9fa )
by Damien
06:56
created

User   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 392
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 51.08%

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 15
dl 0
loc 392
ccs 71
cts 139
cp 0.5108
rs 8.8798
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getByResponse() 0 31 5
A login() 0 23 4
A sync() 0 20 1
A save() 0 11 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\Field;
10
use craft\elements\User as UserElement;
11
use craft\models\FieldLayout;
12
use flipbox\saml\core\exceptions\InvalidMessage;
13
use flipbox\saml\core\helpers\ProviderHelper;
14
use flipbox\saml\sp\helpers\UserHelper;
15
use flipbox\saml\sp\models\Settings;
16
use flipbox\saml\sp\records\ProviderIdentityRecord;
17
use flipbox\saml\sp\records\ProviderRecord;
18
use flipbox\saml\sp\Saml;
19
use SAML2\Response as SamlResponse;
20
use SAML2\XML\saml\Attribute;
21
use yii\base\UserException;
22
23
/**
24
 * Class User
25
 * @package flipbox\saml\sp\services
26
 */
27
class User
28
{
29
    use AssertionTrait;
30
    /**
31
     * @var FieldLayout|null
32
     */
33
    private $fieldLayout;
34
    /**
35
     * @var Field[]
36
     */
37
    private $fields = [];
38
39
    /**
40
     * @param SamlResponse $response
41
     * @return UserElement
42
     * @throws InvalidMessage
43
     * @throws UserException
44
     */
45
    public function getByResponse(SamlResponse $response, ProviderRecord $serviceProvider, Settings $settings)
46
    {
47
48
        $username = null;
49
50
        if (!is_null($settings->nameIdAttributeOverride)) {
51
            // use override
52
            foreach ($this->getAssertions(
53
                $response,
54
                $serviceProvider
55
            ) as $assertion) {
56
                $attributes = $assertion->getAttributes();
57
                if (isset($attributes[$settings->nameIdAttributeOverride])) {
58
                    $attributeValue = $attributes[$settings->nameIdAttributeOverride];
59
                    $username = $this->getAttributeValue($attributeValue);
60
                }
61
            }
62
        } else {
63
            // use nameid
64
            $assertion = $this->getFirstAssertion($response, $serviceProvider);
65
66
            if (! $assertion->getNameId()) {
67
                throw new InvalidMessage('Name ID is missing.');
68
            }
69
            $username = $assertion->getNameId()->getValue();
70
71
            Saml::debug('NameId: ' . $assertion->getNameId()->getValue());
72
        }
73
74
        return $this->find($username);
75
    }
76
77
    /**
78
     * @param ProviderIdentityRecord $identity
79
     * @return bool
80
     * @throws UserException
81
     * @throws \Throwable
82
     */
83
    public function login(\flipbox\saml\sp\records\ProviderIdentityRecord $identity)
84
    {
85
        if ($identity->getUser()->getStatus() !== UserElement::STATUS_ACTIVE) {
86
            if (! \Craft::$app->getUsers()->activateUser($identity->getUser())) {
87
                throw new UserException("Can't activate user.");
88
            }
89
        }
90
91
        if (\Craft::$app->getUser()->login(
92
            $identity->getUser(),
93
            /**
94
             * @todo read session duration from the response
95
             */
96
            \Craft::$app->getConfig()->getGeneral()->userSessionDuration
97
        )
98
        ) {
99
            $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...
100
        } else {
101
            throw new UserException("User login failed.");
102
        }
103
104
        return true;
105
    }
106
107
    /**
108
     * @param UserElement $user
109
     * @param SamlResponse $response
110
     * @param ProviderRecord $idp
111
     * @param Settings $settings
112
     * @throws UserException
113
     * @throws \Throwable
114
     * @throws \craft\errors\ElementNotFoundException
115
     * @throws \yii\base\Exception
116
     */
117 3
    public function sync(
118
        UserElement $user,
119
        SamlResponse $response,
120
        ProviderRecord $idp,
121
        ProviderRecord $sp,
122
        Settings $settings
123
    ) {
124
125
        // enable and transform the user
126 3
        $this->construct(
127 3
            $user,
128 1
            $response,
129 1
            $idp,
130 1
            $sp,
131 1
            $settings
132
        );
133
134
        // Save
135 3
        $this->save($user);
136
    }
137
138
    /**
139
     * @param UserElement $user
140
     * @return bool
141
     * @throws UserException
142
     * @throws \Throwable
143
     * @throws \craft\errors\ElementNotFoundException
144
     * @throws \yii\base\Exception
145
     */
146 3
    protected function save(UserElement $user)
147
    {
148 3
        if (! \Craft::$app->getElements()->saveElement($user)) {
149
            Saml::error(
150
                'User save failed: ' . \json_encode($user->getErrors())
151
            );
152
            throw new UserException("User save failed: " . \json_encode($user->getErrors()));
153
        }
154
155
        return true;
156
    }
157
158
    /**
159
     * Response Based Methods
160
     */
161
162
    /**
163
     * @param UserElement $user
164
     * @param SamlResponse $response
165
     * @param ProviderRecord $idp
166
     * @param Settings $settings
167
     * @throws UserException
168
     * @throws \Throwable
169
     */
170 3
    protected function construct(
171
        UserElement $user,
172
        SamlResponse $response,
173
        ProviderRecord $idp,
174
        ProviderRecord $sp,
175
        Settings $settings
176
    ) {
177
        /**
178
         * Is User Active?
179
         */
180 3
        if (! UserHelper::isUserActive($user)) {
181
            if (! $settings->enableUsers) {
182
                throw new UserException('User access denied.');
183
            }
184
            UserHelper::enableUser($user);
185
        }
186
187 3
        foreach ($this->getAssertions($response, $sp) as $assertion) {
188 3
            $hasAttributes = count($assertion->getAttributes()) > 0;
189 3
            Saml::debug('assertion attributes: ' . \json_encode($assertion->getAttributes()));
190 3
            if ($hasAttributes) {
191 3
                $this->transform(
192 3
                    $user,
193 1
                    $response,
194 1
                    $idp,
195 1
                    $sp,
196 1
                    $settings
197
                );
198
            } else {
199
                /**
200
                 * There doesn't seem to be any attribute statements.
201
                 * Try and use username for the email and move on.
202
                 */
203
                Saml::warning(
204
                    'No attribute statements found! Trying to assign username as the email.'
205
                );
206
                $user->email = $user->email ?: $user->username;
207
            }
208
        }
209 3
    }
210
211
    /**
212
     * @param UserElement $user
213
     * @param SamlResponse $response
214
     * @return UserElement
215
     */
216 3
    protected function transform(
217
        UserElement $user,
218
        SamlResponse $response,
219
        ProviderRecord $idp,
220
        ProviderRecord $sp,
221
        Settings $settings
222
    ) {
223
224 3
        foreach ($this->getAssertions($response, $sp) as $assertion) {
225
            /**
226
             * Check the provider first
227
             */
228 3
            $attributeMap = ProviderHelper::providerMappingToKeyValue(
229 3
                $idp
230
            ) ?:
231 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...
232
233 3
            Saml::debug('Attribute Map: ' . \json_encode($attributeMap));
234
235
            /**
236
             * Loop thru attributes and set to the user
237
             */
238 3
            foreach ($assertion->getAttributes() as $attributeName => $attributeValue) {
239 3
                Saml::debug('Attributes: ' . $attributeName . ' ' . \json_encode($attributeValue));
240 3
                if (isset($attributeMap[$attributeName])) {
241 3
                    $craftProperty = $attributeMap[$attributeName];
242 3
                    $this->assignProperty(
243 3
                        $user,
244 1
                        $attributeName,
245 1
                        $attributeValue,
246 1
                        $craftProperty
247
                    );
248
                } else {
249 3
                    Saml::debug('No match for: ' . $attributeName);
250
                }
251
            }
252
        }
253
254 3
        return $user;
255
    }
256
257
    /**
258
     * @param UserElement $user
259
     * @param $attributeName
260
     * @param $attributeValue
261
     * @param $craftProperty
262
     */
263 3
    protected function assignProperty(
264
        UserElement $user,
265
        $attributeName,
266
        $attributeValue,
267
        $craftProperty
268
    ) {
269
270 3
        $originalValues = $attributeValue;
271 3
        if (is_array($attributeValue)) {
272 3
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
273
        }
274
275 3
        if (is_string($craftProperty) && in_array($craftProperty, $user->attributes())) {
276 3
            Saml::debug(
277 3
                sprintf(
278 3
                    'Attribute %s is scalar and should set value "%s" to user->%s',
279 1
                    $attributeName,
280 1
                    $attributeValue,
281 1
                    $craftProperty
282
                )
283
            );
284
285 3
            $this->setSimpleProperty($user, $craftProperty, $attributeValue);
286
        } elseif (is_callable($craftProperty)) {
287
            Saml::debug(
288
                sprintf(
289
                    'Attribute %s is handled with a callable.',
290
                    $attributeName
291
                )
292
            );
293
294
            call_user_func($craftProperty, $user, [
295
                $attributeName => $originalValues,
296
            ]);
297
        }
298 3
    }
299
300
    /**
301
     * @param UserElement $user
302
     * @return Field|null
303
     */
304 3
    protected function getFieldLayoutField(UserElement $user, $fieldHandle)
305
    {
306 3
        if (! $this->fieldLayout) {
307 3
            $this->fieldLayout = $user->getFieldLayout();
308
        }
309 3
        if (is_null($this->fieldLayout)) {
310
            return null;
311
        }
312
313 3
        if (! isset($this->fields[$fieldHandle])) {
314 3
            $this->fields[$fieldHandle] = $this->fieldLayout->getFieldByHandle($fieldHandle);
315
        }
316
317
318 3
        return $this->fields[$fieldHandle];
319
    }
320
321
    /**
322
     * @param UserElement $user
323
     * @param string $name
324
     * @param mixed $value
325
     */
326 3
    private function setSimpleProperty(UserElement $user, $name, $value)
327
    {
328 3
        $field = $this->getFieldLayoutField($user, $name);
329
330 3
        Saml::info(
331 3
            sprintf(
332 3
                '%s as %s. Is Field? %s',
333 1
                $name,
334 1
                $value,
335 3
                $field instanceof Field ? $field->id : 'Nope'
336
            )
337
        );
338
339 3
        if (! is_null($field)) {
340
            $user->setFieldValue($name, $value);
341
        } else {
342 3
            $user->{$name} = $value;
343
        }
344 3
    }
345
346
    /**************************************************
347
     * Craft User Methods
348
     **************************************************/
349
350
    /**
351
     * @param $username
352
     * @return UserElement
353
     * @throws UserException
354
     */
355
    protected function find($username)
356
    {
357
        return $this->forceGet($username);
358
    }
359
360
    /**
361
     * @param $username
362
     * @return UserElement
363
     * @throws UserException
364
     */
365
    protected function forceGet($username)
366
    {
367
368
        /**
369
         * Is there a user that exists already?
370
         */
371
        if (!($user = $this->getByUsernameOrEmail($username))) {
372
            // Should we create a new user? what's the setting say?
373
            if (! Saml::getInstance()->getSettings()->createUser) {
374
                throw new UserException("System doesn't have permission to create a new user.");
375
            }
376
377
            // new user!
378
            $user = new UserElement(
379
                [
380
                    'username' => $username,
381
                ]
382
            );
383
        }
384
385
        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...
386
    }
387
388
    /**
389
     * @param $usernameOrEmail
390
     * @param bool $archived
391
     * @return array|bool|\craft\base\ElementInterface|UserElement|null
392
     */
393
    protected function getByUsernameOrEmail($usernameOrEmail, $archived = false)
394
    {
395
396
        return UserElement::find()
397
            ->where(
398
                [
399
                    'or',
400
                    ['username' => $usernameOrEmail],
401
                    ['email' => $usernameOrEmail],
402
                ]
403
            )
404
            ->status(null)
405
            ->archived($archived)
406
            ->one();
407
    }
408
409
    private function getAttributeValue($attributeValue)
410
    {
411
412
        if (is_array($attributeValue)) {
413
            $attributeValue = isset($attributeValue[0]) ? $attributeValue[0] : null;
414
        }
415
416
        return $attributeValue;
417
    }
418
}
419