Completed
Push — symfony5 ( 074008...b11eb2 )
by
unknown
267:19 queued 255:17
created

UserService   F

Complexity

Total Complexity 139

Size/Duplication

Total Lines 1321
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 49

Importance

Changes 0
Metric Value
dl 0
loc 1321
c 0
b 0
f 0
wmc 139
lcom 1
cbo 49
rs 0.8

41 Methods

Rating   Name   Duplication   Size   Complexity  
A setLogger() 0 4 1
A __construct() 0 22 1
A createUserGroup() 0 33 4
A loadUserGroup() 0 6 1
A loadSubUserGroups() 0 34 5
A searchSubGroups() 0 16 1
A deleteUserGroup() 0 16 2
A moveUserGroup() 0 31 4
B updateUserGroup() 0 41 6
A loadUserByToken() 0 10 3
A deleteUser() 0 18 2
F updateUser() 0 96 16
B updateUserToken() 0 31 6
A expireUserToken() 0 11 2
A assignUserToUserGroup() 0 37 5
B unAssignUserFromUserGroup() 0 37 7
B loadUserGroupsOfUser() 0 46 5
A loadUsersOfUserGroup() 0 45 3
A isUser() 0 17 4
A isUserGroup() 0 4 1
A newUserCreateStruct() 0 41 4
A newUserGroupCreateStruct() 0 16 2
A newUserUpdateStruct() 0 4 1
A newUserGroupUpdateStruct() 0 4 1
B validatePassword() 0 38 8
A buildDomainUserGroupObject() 0 18 2
A buildDomainUserObject() 0 25 2
B getPasswordInfo() 0 31 6
A getUserFieldDefinition() 0 4 1
B createUser() 0 47 5
A loadUser() 0 31 5
A checkUserCredentials() 0 4 1
A updatePasswordHash() 0 29 4
A loadUserByLogin() 0 10 3
A loadUserByEmail() 0 10 2
A loadUsersByEmail() 0 13 3
A comparePasswordHashForSPIUser() 0 4 1
A comparePasswordHashForAPIUser() 0 4 1
A comparePasswordHashes() 0 7 1
A isUserProfileUpdateRequested() 0 9 5
A getDateTime() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like UserService 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 UserService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * File containing the eZ\Publish\Core\Repository\UserService class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Publish\Core\Repository;
10
11
use DateInterval;
12
use DateTime;
13
use DateTimeImmutable;
14
use DateTimeInterface;
15
use Exception;
16
use eZ\Publish\API\Repository\PermissionResolver;
17
use eZ\Publish\API\Repository\Repository as RepositoryInterface;
18
use eZ\Publish\API\Repository\UserService as UserServiceInterface;
19
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
20
use eZ\Publish\API\Repository\Values\Content\Location;
21
use eZ\Publish\API\Repository\Values\Content\Field;
22
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
23
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\ContentTypeId as CriterionContentTypeId;
24
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LocationId as CriterionLocationId;
25
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd as CriterionLogicalAnd;
26
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\ParentLocationId as CriterionParentLocationId;
27
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
28
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
29
use eZ\Publish\API\Repository\Values\User\PasswordInfo;
30
use eZ\Publish\API\Repository\Values\User\PasswordValidationContext;
31
use eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct;
32
use eZ\Publish\Core\Repository\Validator\UserPasswordValidator;
33
use eZ\Publish\Core\Repository\User\PasswordHashServiceInterface;
34
use eZ\Publish\Core\Repository\Values\User\UserCreateStruct;
35
use eZ\Publish\API\Repository\Values\User\UserCreateStruct as APIUserCreateStruct;
36
use eZ\Publish\API\Repository\Values\User\UserUpdateStruct;
37
use eZ\Publish\API\Repository\Values\User\User as APIUser;
38
use eZ\Publish\API\Repository\Values\User\UserGroup as APIUserGroup;
39
use eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct as APIUserGroupCreateStruct;
40
use eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct;
41
use eZ\Publish\Core\Base\Exceptions\BadStateException;
42
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
43
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
44
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
45
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
46
use eZ\Publish\Core\FieldType\User\Value as UserValue;
47
use eZ\Publish\Core\FieldType\User\Type as UserType;
48
use eZ\Publish\Core\FieldType\ValidationError;
49
use eZ\Publish\Core\Repository\Values\User\User;
50
use eZ\Publish\Core\Repository\Values\User\UserGroup;
51
use eZ\Publish\Core\Repository\Values\User\UserGroupCreateStruct;
52
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
53
use eZ\Publish\SPI\Persistence\User as SPIUser;
54
use eZ\Publish\SPI\Persistence\User\Handler;
55
use eZ\Publish\SPI\Persistence\User\UserTokenUpdateStruct as SPIUserTokenUpdateStruct;
56
use Psr\Log\LoggerInterface;
57
58
/**
59
 * This service provides methods for managing users and user groups.
60
 *
61
 * @example Examples/user.php
62
 */
63
class UserService implements UserServiceInterface
64
{
65
    /** @var \eZ\Publish\API\Repository\Repository */
66
    protected $repository;
67
68
    /** @var \eZ\Publish\SPI\Persistence\User\Handler */
69
    protected $userHandler;
70
71
    /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */
72
    private $locationHandler;
73
74
    /** @var array */
75
    protected $settings;
76
77
    /** @var \Psr\Log\LoggerInterface|null */
78
    protected $logger;
79
80
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
81
    private $permissionResolver;
82
83
    /** @var \eZ\Publish\Core\Repository\User\PasswordHashServiceInterface */
84
    private $passwordHashService;
85
86
    public function setLogger(LoggerInterface $logger = null)
87
    {
88
        $this->logger = $logger;
89
    }
90
91
    /**
92
     * Setups service with reference to repository object that created it & corresponding handler.
93
     *
94
     * @param \eZ\Publish\API\Repository\Repository $repository
95
     * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
96
     * @param \eZ\Publish\SPI\Persistence\Content\Location\Handler $locationHandler
97
     * @param array $settings
98
     */
99
    public function __construct(
100
        RepositoryInterface $repository,
101
        PermissionResolver $permissionResolver,
102
        Handler $userHandler,
103
        LocationHandler $locationHandler,
104
        PasswordHashServiceInterface $passwordHashGenerator,
105
        array $settings = []
106
    ) {
107
        $this->repository = $repository;
108
        $this->permissionResolver = $permissionResolver;
109
        $this->userHandler = $userHandler;
110
        $this->locationHandler = $locationHandler;
111
        // Union makes sure default settings are ignored if provided in argument
112
        $this->settings = $settings + [
113
            'defaultUserPlacement' => 12,
114
            'userClassID' => 4, // @todo Rename this settings to swap out "Class" for "Type"
115
            'userGroupClassID' => 3,
116
            'hashType' => $passwordHashGenerator->getDefaultHashType(),
117
            'siteName' => 'ez.no',
118
        ];
119
        $this->passwordHashService = $passwordHashGenerator;
120
    }
121
122
    /**
123
     * Creates a new user group using the data provided in the ContentCreateStruct parameter.
124
     *
125
     * In 4.x in the content type parameter in the profile is ignored
126
     * - the content type is determined via configuration and can be set to null.
127
     * The returned version is published.
128
     *
129
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct $userGroupCreateStruct a structure for setting all necessary data to create this user group
130
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $parentGroup
131
     *
132
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
133
     *
134
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
135
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the input structure has invalid data
136
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupCreateStruct is not valid
137
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
138
     */
139
    public function createUserGroup(APIUserGroupCreateStruct $userGroupCreateStruct, APIUserGroup $parentGroup)
140
    {
141
        $contentService = $this->repository->getContentService();
142
        $locationService = $this->repository->getLocationService();
143
        $contentTypeService = $this->repository->getContentTypeService();
144
145
        if ($userGroupCreateStruct->contentType === null) {
146
            $userGroupContentType = $contentTypeService->loadContentType($this->settings['userGroupClassID']);
147
            $userGroupCreateStruct->contentType = $userGroupContentType;
148
        }
149
150
        $loadedParentGroup = $this->loadUserGroup($parentGroup->id);
151
152
        if ($loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
153
            throw new InvalidArgumentException('parentGroup', 'parent User Group has no main Location');
154
        }
155
156
        $locationCreateStruct = $locationService->newLocationCreateStruct(
157
            $loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId
158
        );
159
160
        $this->repository->beginTransaction();
161
        try {
162
            $contentDraft = $contentService->createContent($userGroupCreateStruct, [$locationCreateStruct]);
163
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
164
            $this->repository->commit();
165
        } catch (Exception $e) {
166
            $this->repository->rollback();
167
            throw $e;
168
        }
169
170
        return $this->buildDomainUserGroupObject($publishedContent);
171
    }
172
173
    /**
174
     * Loads a user group for the given id.
175
     *
176
     * @param mixed $id
177
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
178
     *
179
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
180
     *
181
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
182
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the user group with the given id was not found
183
     */
184
    public function loadUserGroup($id, array $prioritizedLanguages = [])
185
    {
186
        $content = $this->repository->getContentService()->loadContent($id, $prioritizedLanguages);
187
188
        return $this->buildDomainUserGroupObject($content);
189
    }
190
191
    /**
192
     * Loads the sub groups of a user group.
193
     *
194
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
195
     * @param int $offset the start offset for paging
196
     * @param int $limit the number of user groups returned
197
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
198
     *
199
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
200
     *
201
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the user group
202
     */
203
    public function loadSubUserGroups(APIUserGroup $userGroup, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
204
    {
205
        $locationService = $this->repository->getLocationService();
206
207
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
208
        if (!$this->permissionResolver->canUser('content', 'read', $loadedUserGroup)) {
209
            throw new UnauthorizedException('content', 'read');
210
        }
211
212
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
213
            return [];
214
        }
215
216
        $mainGroupLocation = $locationService->loadLocation(
217
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
218
        );
219
220
        $searchResult = $this->searchSubGroups($mainGroupLocation, $offset, $limit);
221
        if ($searchResult->totalCount == 0) {
222
            return [];
223
        }
224
225
        $subUserGroups = [];
226
        foreach ($searchResult->searchHits as $searchHit) {
227
            $subUserGroups[] = $this->buildDomainUserGroupObject(
228
                $this->repository->getContentService()->internalLoadContentById(
0 ignored issues
show
Bug introduced by
The method internalLoadContentById() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
229
                    $searchHit->valueObject->contentInfo->id,
0 ignored issues
show
Documentation introduced by
The property contentInfo does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. 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...
230
                    $prioritizedLanguages
231
                )
232
            );
233
        }
234
235
        return $subUserGroups;
236
    }
237
238
    /**
239
     * Returns (searches) subgroups of a user group described by its main location.
240
     *
241
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
242
     * @param int $offset
243
     * @param int $limit
244
     *
245
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
246
     */
247
    protected function searchSubGroups(Location $location, $offset = 0, $limit = 25)
248
    {
249
        $searchQuery = new LocationQuery();
250
251
        $searchQuery->offset = $offset;
252
        $searchQuery->limit = $limit;
253
254
        $searchQuery->filter = new CriterionLogicalAnd([
255
            new CriterionContentTypeId($this->settings['userGroupClassID']),
256
            new CriterionParentLocationId($location->id),
257
        ]);
258
259
        $searchQuery->sortClauses = $location->getSortClauses();
0 ignored issues
show
Documentation Bug introduced by
It seems like $location->getSortClauses() of type array<integer,object,{"0":"object"}> is incompatible with the declared type array<integer,object<eZ\...tent\Query\SortClause>> of property $sortClauses.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
260
261
        return $this->repository->getSearchService()->findLocations($searchQuery, [], false);
262
    }
263
264
    /**
265
     * Removes a user group.
266
     *
267
     * the users which are not assigned to other groups will be deleted.
268
     *
269
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
270
     *
271
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
272
     */
273
    public function deleteUserGroup(APIUserGroup $userGroup)
274
    {
275
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
276
277
        $this->repository->beginTransaction();
278
        try {
279
            //@todo: what happens to sub user groups and users below sub user groups
280
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUserGroup->getVersionInfo()->getContentInfo());
281
            $this->repository->commit();
282
        } catch (Exception $e) {
283
            $this->repository->rollback();
284
            throw $e;
285
        }
286
287
        return $affectedLocationIds;
288
    }
289
290
    /**
291
     * Moves the user group to another parent.
292
     *
293
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
294
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $newParent
295
     *
296
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
297
     */
298
    public function moveUserGroup(APIUserGroup $userGroup, APIUserGroup $newParent)
299
    {
300
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
301
        $loadedNewParent = $this->loadUserGroup($newParent->id);
302
303
        $locationService = $this->repository->getLocationService();
304
305
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
306
            throw new BadStateException('userGroup', 'existing User Group is not stored and/or does not have any Location yet');
307
        }
308
309
        if ($loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId === null) {
310
            throw new BadStateException('newParent', 'new User Group is not stored and/or does not have any Location yet');
311
        }
312
313
        $userGroupMainLocation = $locationService->loadLocation(
314
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
315
        );
316
        $newParentMainLocation = $locationService->loadLocation(
317
            $loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId
318
        );
319
320
        $this->repository->beginTransaction();
321
        try {
322
            $locationService->moveSubtree($userGroupMainLocation, $newParentMainLocation);
323
            $this->repository->commit();
324
        } catch (Exception $e) {
325
            $this->repository->rollback();
326
            throw $e;
327
        }
328
    }
329
330
    /**
331
     * Updates the group profile with fields and meta data.
332
     *
333
     * 4.x: If the versionUpdateStruct is set in $userGroupUpdateStruct, this method internally creates a content draft, updates ts with the provided data
334
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
335
     *
336
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
337
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct $userGroupUpdateStruct
338
     *
339
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
340
     *
341
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user group
342
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupUpdateStruct is not valid
343
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
344
     */
345
    public function updateUserGroup(APIUserGroup $userGroup, UserGroupUpdateStruct $userGroupUpdateStruct)
346
    {
347
        if ($userGroupUpdateStruct->contentUpdateStruct === null &&
348
            $userGroupUpdateStruct->contentMetadataUpdateStruct === null) {
349
            // both update structs are empty, nothing to do
350
            return $userGroup;
351
        }
352
353
        $contentService = $this->repository->getContentService();
354
355
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
356
357
        $this->repository->beginTransaction();
358
        try {
359
            $publishedContent = $loadedUserGroup;
360
            if ($userGroupUpdateStruct->contentUpdateStruct !== null) {
361
                $contentDraft = $contentService->createContentDraft($loadedUserGroup->getVersionInfo()->getContentInfo());
362
363
                $contentDraft = $contentService->updateContent(
364
                    $contentDraft->getVersionInfo(),
365
                    $userGroupUpdateStruct->contentUpdateStruct
366
                );
367
368
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
369
            }
370
371
            if ($userGroupUpdateStruct->contentMetadataUpdateStruct !== null) {
372
                $publishedContent = $contentService->updateContentMetadata(
373
                    $publishedContent->getVersionInfo()->getContentInfo(),
374
                    $userGroupUpdateStruct->contentMetadataUpdateStruct
375
                );
376
            }
377
378
            $this->repository->commit();
379
        } catch (Exception $e) {
380
            $this->repository->rollback();
381
            throw $e;
382
        }
383
384
        return $this->buildDomainUserGroupObject($publishedContent);
385
    }
386
387
    /**
388
     * Create a new user. The created user is published by this method.
389
     *
390
     * @param \eZ\Publish\API\Repository\Values\User\UserCreateStruct $userCreateStruct the data used for creating the user
391
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup[] $parentGroups the groups which are assigned to the user after creation
392
     *
393
     * @return \eZ\Publish\API\Repository\Values\User\User
394
     *
395
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
396
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userCreateStruct is not valid
397
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
398
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a user with provided login already exists
399
     */
400
    public function createUser(APIUserCreateStruct $userCreateStruct, array $parentGroups)
401
    {
402
        $contentService = $this->repository->getContentService();
403
        $locationService = $this->repository->getLocationService();
404
405
        $locationCreateStructs = [];
406
        foreach ($parentGroups as $parentGroup) {
407
            $parentGroup = $this->loadUserGroup($parentGroup->id);
408
            if ($parentGroup->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
409
                $locationCreateStructs[] = $locationService->newLocationCreateStruct(
410
                    $parentGroup->getVersionInfo()->getContentInfo()->mainLocationId
411
                );
412
            }
413
        }
414
415
        // Search for the first ezuser field type in content type
416
        $userFieldDefinition = $this->getUserFieldDefinition($userCreateStruct->contentType);
417
        if ($userFieldDefinition === null) {
418
            throw new ContentValidationException('the provided Content Type does not contain the ezuser Field Type');
419
        }
420
421
        $this->repository->beginTransaction();
422
        try {
423
            $contentDraft = $contentService->createContent($userCreateStruct, $locationCreateStructs);
424
            // There is no need to create user separately, just load it from SPI
425
            $spiUser = $this->userHandler->load($contentDraft->id);
426
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
427
428
            // User\Handler::create call is currently used to clear cache only
429
            $this->userHandler->create(
430
                new SPIUser(
431
                    [
432
                        'id' => $spiUser->id,
433
                        'login' => $spiUser->login,
434
                        'email' => $spiUser->email,
435
                    ]
436
                )
437
            );
438
439
            $this->repository->commit();
440
        } catch (Exception $e) {
441
            $this->repository->rollback();
442
            throw $e;
443
        }
444
445
        return $this->buildDomainUserObject($spiUser, $publishedContent);
446
    }
447
448
    /**
449
     * Loads a user.
450
     *
451
     * @param mixed $userId
452
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
453
     *
454
     * @return \eZ\Publish\API\Repository\Values\User\User
455
     *
456
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given id was not found
457
     */
458
    public function loadUser($userId, array $prioritizedLanguages = [])
459
    {
460
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
461
        $content = $this->repository->getContentService()->internalLoadContentById($userId, $prioritizedLanguages);
0 ignored issues
show
Bug introduced by
The method internalLoadContentById() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
462
        // Get spiUser value from Field Value
463
        foreach ($content->getFields() as $field) {
464
            if (!$field->value instanceof UserValue) {
465
                continue;
466
            }
467
468
            /** @var \eZ\Publish\Core\FieldType\User\Value $value */
469
            $value = $field->value;
470
            $spiUser = new SPIUser();
471
            $spiUser->id = $value->contentId;
472
            $spiUser->login = $value->login;
473
            $spiUser->email = $value->email;
474
            $spiUser->hashAlgorithm = $value->passwordHashType;
475
            $spiUser->passwordHash = $value->passwordHash;
476
            $spiUser->passwordUpdatedAt = $value->passwordUpdatedAt ? $value->passwordUpdatedAt->getTimestamp() : null;
477
            $spiUser->isEnabled = $value->enabled;
478
            $spiUser->maxLogin = $value->maxLogin;
479
            break;
480
        }
481
482
        // If for some reason not found, load it
483
        if (!isset($spiUser)) {
484
            $spiUser = $this->userHandler->load($userId);
485
        }
486
487
        return $this->buildDomainUserObject($spiUser, $content);
488
    }
489
490
    /**
491
     * Checks if credentials are valid for provided User.
492
     *
493
     * @param \eZ\Publish\API\Repository\Values\User\User $user
494
     * @param string $credentials
495
     *
496
     * @return bool
497
     */
498
    public function checkUserCredentials(APIUser $user, string $credentials): bool
499
    {
500
        return $this->comparePasswordHashForAPIUser($user, $credentials);
501
    }
502
503
    /**
504
     * Update password hash to the type configured for the service, if they differ.
505
     *
506
     * @param string $login User login
507
     * @param string $password User password
508
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
509
     *
510
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException if the password is not correctly saved, in which case the update is reverted
511
     */
512
    private function updatePasswordHash($login, $password, SPIUser $spiUser)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $login is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
513
    {
514
        $hashType = $this->passwordHashService->getDefaultHashType();
515
        if ($spiUser->hashAlgorithm === $hashType) {
516
            return;
517
        }
518
519
        $spiUser->passwordHash = $this->passwordHashService->createPasswordHash($password, $hashType);
520
        $spiUser->hashAlgorithm = $hashType;
521
522
        $this->repository->beginTransaction();
523
        $this->userHandler->update($spiUser);
524
        $reloadedSpiUser = $this->userHandler->load($spiUser->id);
525
526
        if ($reloadedSpiUser->passwordHash === $spiUser->passwordHash) {
527
            $this->repository->commit();
528
        } else {
529
            // Password hash was not correctly saved, possible cause: EZP-28692
530
            $this->repository->rollback();
531
            if (isset($this->logger)) {
532
                $this->logger->critical('Password hash could not be updated. Please verify that your database schema is up to date.');
533
            }
534
535
            throw new BadStateException(
536
                'user',
537
                'Could not save updated password hash, reverting to previous hash. Please verify that your database schema is up to date.'
538
            );
539
        }
540
    }
541
542
    /**
543
     * Loads a user for the given login.
544
     *
545
     * {@inheritdoc}
546
     *
547
     * @param string $login
548
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
549
     *
550
     * @return \eZ\Publish\API\Repository\Values\User\User
551
     *
552
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
553
     */
554
    public function loadUserByLogin($login, array $prioritizedLanguages = [])
555
    {
556
        if (!is_string($login) || empty($login)) {
557
            throw new InvalidArgumentValue('login', $login);
558
        }
559
560
        $spiUser = $this->userHandler->loadByLogin($login);
561
562
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
563
    }
564
565
    /**
566
     * Loads a user for the given email.
567
     *
568
     * {@inheritdoc}
569
     *
570
     * @param string $email
571
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
572
     *
573
     * @return \eZ\Publish\API\Repository\Values\User\User
574
     *
575
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
576
     */
577
    public function loadUserByEmail(string $email, array $prioritizedLanguages = []): APIUser
578
    {
579
        if (empty($email)) {
580
            throw new InvalidArgumentValue('email', $email);
581
        }
582
583
        $spiUser = $this->userHandler->loadByEmail($email);
584
585
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
586
    }
587
588
    /**
589
     * Loads a user for the given email.
590
     *
591
     * {@inheritdoc}
592
     *
593
     * @param string $email
594
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
595
     *
596
     * @return \eZ\Publish\API\Repository\Values\User\User[]
597
     *
598
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
599
     */
600
    public function loadUsersByEmail(string $email, array $prioritizedLanguages = []): array
601
    {
602
        if (empty($email)) {
603
            throw new InvalidArgumentValue('email', $email);
604
        }
605
606
        $users = [];
607
        foreach ($this->userHandler->loadUsersByEmail($email) as $spiUser) {
608
            $users[] = $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
609
        }
610
611
        return $users;
612
    }
613
614
    /**
615
     * Loads a user for the given token.
616
     *
617
     * {@inheritdoc}
618
     *
619
     * @param string $hash
620
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
621
     *
622
     * @return \eZ\Publish\API\Repository\Values\User\User
623
     *
624
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
625
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
626
     */
627
    public function loadUserByToken($hash, array $prioritizedLanguages = [])
628
    {
629
        if (!is_string($hash) || empty($hash)) {
630
            throw new InvalidArgumentValue('hash', $hash);
631
        }
632
633
        $spiUser = $this->userHandler->loadUserByToken($hash);
634
635
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
636
    }
637
638
    /**
639
     * This method deletes a user.
640
     *
641
     * @param \eZ\Publish\API\Repository\Values\User\User $user
642
     *
643
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to delete the user
644
     */
645
    public function deleteUser(APIUser $user)
646
    {
647
        $loadedUser = $this->loadUser($user->id);
648
649
        $this->repository->beginTransaction();
650
        try {
651
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo());
652
653
            // User\Handler::delete call is currently used to clear cache only
654
            $this->userHandler->delete($loadedUser->id);
655
            $this->repository->commit();
656
        } catch (Exception $e) {
657
            $this->repository->rollback();
658
            throw $e;
659
        }
660
661
        return $affectedLocationIds;
662
    }
663
664
    /**
665
     * Updates a user.
666
     *
667
     * 4.x: If the versionUpdateStruct is set in the user update structure, this method internally creates a content draft, updates ts with the provided data
668
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
669
     *
670
     * @param \eZ\Publish\API\Repository\Values\User\User $user
671
     * @param \eZ\Publish\API\Repository\Values\User\UserUpdateStruct $userUpdateStruct
672
     *
673
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userUpdateStruct is not valid
674
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
675
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user
676
     *
677
     * @return \eZ\Publish\API\Repository\Values\User\User
678
     */
679
    public function updateUser(APIUser $user, UserUpdateStruct $userUpdateStruct)
680
    {
681
        $loadedUser = $this->loadUser($user->id);
682
683
        $contentService = $this->repository->getContentService();
684
685
        $canEditContent = $this->permissionResolver->canUser('content', 'edit', $loadedUser);
686
687
        if (!$canEditContent && $this->isUserProfileUpdateRequested($userUpdateStruct)) {
688
            throw new UnauthorizedException('content', 'edit');
689
        }
690
691
        $userFieldDefinition = null;
692
        foreach ($loadedUser->getContentType()->fieldDefinitions as $fieldDefinition) {
693
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
694
                $userFieldDefinition = $fieldDefinition;
695
                break;
696
            }
697
        }
698
699
        if ($userFieldDefinition === null) {
700
            throw new ContentValidationException('The provided Content Type does not contain the ezuser Field Type');
701
        }
702
703
        $userUpdateStruct->contentUpdateStruct = $userUpdateStruct->contentUpdateStruct ?? $contentService->newContentUpdateStruct();
704
705
        $providedUserUpdateDataInField = false;
706
        foreach ($userUpdateStruct->contentUpdateStruct->fields as $field) {
707
            if ($field->value instanceof UserValue) {
708
                $providedUserUpdateDataInField = true;
709
                break;
710
            }
711
        }
712
713
        if (!$providedUserUpdateDataInField) {
714
            $userUpdateStruct->contentUpdateStruct->setField(
715
                $userFieldDefinition->identifier,
716
                new UserValue([
717
                    'contentId' => $loadedUser->id,
718
                    'hasStoredLogin' => true,
719
                    'login' => $loadedUser->login,
720
                    'email' => $userUpdateStruct->email ?? $loadedUser->email,
721
                    'plainPassword' => $userUpdateStruct->password,
722
                    'enabled' => $userUpdateStruct->enabled ?? $loadedUser->enabled,
723
                    'maxLogin' => $userUpdateStruct->maxLogin ?? $loadedUser->maxLogin,
724
                    'passwordHashType' => $user->hashAlgorithm,
725
                    'passwordHash' => $user->passwordHash,
726
                ])
727
            );
728
        }
729
730
        if (!empty($userUpdateStruct->password) &&
731
            !$canEditContent &&
732
            !$this->permissionResolver->canUser('user', 'password', $loadedUser)
733
        ) {
734
            throw new UnauthorizedException('user', 'password');
735
        }
736
737
        $this->repository->beginTransaction();
738
        try {
739
            $publishedContent = $loadedUser;
740
            if ($userUpdateStruct->contentUpdateStruct !== null) {
741
                $contentDraft = $contentService->createContentDraft($loadedUser->getVersionInfo()->getContentInfo());
742
                $contentDraft = $contentService->updateContent(
743
                    $contentDraft->getVersionInfo(),
744
                    $userUpdateStruct->contentUpdateStruct
745
                );
746
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
747
            }
748
749
            if ($userUpdateStruct->contentMetadataUpdateStruct !== null) {
750
                $contentService->updateContentMetadata(
751
                    $publishedContent->getVersionInfo()->getContentInfo(),
752
                    $userUpdateStruct->contentMetadataUpdateStruct
753
                );
754
            }
755
756
            // User\Handler::update call is currently used to clear cache only
757
            $this->userHandler->update(
758
                new SPIUser(
759
                    [
760
                        'id' => $loadedUser->id,
761
                        'login' => $loadedUser->login,
762
                        'email' => $userUpdateStruct->email ?: $loadedUser->email,
763
                    ]
764
                )
765
            );
766
767
            $this->repository->commit();
768
        } catch (Exception $e) {
769
            $this->repository->rollback();
770
            throw $e;
771
        }
772
773
        return $this->loadUser($loadedUser->id);
774
    }
775
776
    /**
777
     * Update the user token information specified by the user token struct.
778
     *
779
     * @param \eZ\Publish\API\Repository\Values\User\User $user
780
     * @param \eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct $userTokenUpdateStruct
781
     *
782
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
783
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
784
     * @throws \RuntimeException
785
     * @throws \Exception
786
     *
787
     * @return \eZ\Publish\API\Repository\Values\User\User
788
     */
789
    public function updateUserToken(APIUser $user, UserTokenUpdateStruct $userTokenUpdateStruct)
790
    {
791
        $loadedUser = $this->loadUser($user->id);
792
793
        if ($userTokenUpdateStruct->hashKey !== null && (!is_string($userTokenUpdateStruct->hashKey) || empty($userTokenUpdateStruct->hashKey))) {
794
            throw new InvalidArgumentValue('hashKey', $userTokenUpdateStruct->hashKey, 'UserTokenUpdateStruct');
795
        }
796
797
        if ($userTokenUpdateStruct->time === null) {
798
            throw new InvalidArgumentValue('time', $userTokenUpdateStruct->time, 'UserTokenUpdateStruct');
799
        }
800
801
        $this->repository->beginTransaction();
802
        try {
803
            $this->userHandler->updateUserToken(
804
                new SPIUserTokenUpdateStruct(
805
                    [
806
                        'userId' => $loadedUser->id,
807
                        'hashKey' => $userTokenUpdateStruct->hashKey,
808
                        'time' => $userTokenUpdateStruct->time->getTimestamp(),
809
                    ]
810
                )
811
            );
812
            $this->repository->commit();
813
        } catch (Exception $e) {
814
            $this->repository->rollback();
815
            throw $e;
816
        }
817
818
        return $this->loadUser($loadedUser->id);
819
    }
820
821
    /**
822
     * Expires user token with user hash.
823
     *
824
     * @param string $hash
825
     */
826
    public function expireUserToken($hash)
827
    {
828
        $this->repository->beginTransaction();
829
        try {
830
            $this->userHandler->expireUserToken($hash);
831
            $this->repository->commit();
832
        } catch (Exception $e) {
833
            $this->repository->rollback();
834
            throw $e;
835
        }
836
    }
837
838
    /**
839
     * Assigns a new user group to the user.
840
     *
841
     * @param \eZ\Publish\API\Repository\Values\User\User $user
842
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
843
     *
844
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to assign the user group to the user
845
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is already in the given user group
846
     */
847
    public function assignUserToUserGroup(APIUser $user, APIUserGroup $userGroup)
848
    {
849
        $loadedUser = $this->loadUser($user->id);
850
        $loadedGroup = $this->loadUserGroup($userGroup->id);
851
        $locationService = $this->repository->getLocationService();
852
853
        $existingGroupIds = [];
854
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
855
        foreach ($userLocations as $userLocation) {
856
            $existingGroupIds[] = $userLocation->parentLocationId;
857
        }
858
859
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
860
            throw new BadStateException('userGroup', 'User Group has no main Location or no Locations');
861
        }
862
863
        if (in_array($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId, $existingGroupIds)) {
864
            // user is already assigned to the user group
865
            throw new InvalidArgumentException('user', 'User is already in the given User Group');
866
        }
867
868
        $locationCreateStruct = $locationService->newLocationCreateStruct(
869
            $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId
870
        );
871
872
        $this->repository->beginTransaction();
873
        try {
874
            $locationService->createLocation(
875
                $loadedUser->getVersionInfo()->getContentInfo(),
876
                $locationCreateStruct
877
            );
878
            $this->repository->commit();
879
        } catch (Exception $e) {
880
            $this->repository->rollback();
881
            throw $e;
882
        }
883
    }
884
885
    /**
886
     * Removes a user group from the user.
887
     *
888
     * @param \eZ\Publish\API\Repository\Values\User\User $user
889
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
890
     *
891
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to remove the user group from the user
892
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is not in the given user group
893
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $userGroup is the last assigned user group
894
     */
895
    public function unAssignUserFromUserGroup(APIUser $user, APIUserGroup $userGroup)
896
    {
897
        $loadedUser = $this->loadUser($user->id);
898
        $loadedGroup = $this->loadUserGroup($userGroup->id);
899
        $locationService = $this->repository->getLocationService();
900
901
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
902
        if (empty($userLocations)) {
903
            throw new BadStateException('user', 'User has no Locations, cannot unassign from group');
904
        }
905
906
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
907
            throw new BadStateException('userGroup', 'User Group has no main Location or no Locations, cannot unassign');
908
        }
909
910
        foreach ($userLocations as $userLocation) {
911
            if ($userLocation->parentLocationId == $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId) {
912
                // Throw this specific BadState when we know argument is valid
913
                if (count($userLocations) === 1) {
914
                    throw new BadStateException('user', 'User only has one User Group, cannot unassign from last group');
915
                }
916
917
                $this->repository->beginTransaction();
918
                try {
919
                    $locationService->deleteLocation($userLocation);
920
                    $this->repository->commit();
921
922
                    return;
923
                } catch (Exception $e) {
924
                    $this->repository->rollback();
925
                    throw $e;
926
                }
927
            }
928
        }
929
930
        throw new InvalidArgumentException('userGroup', 'User is not in the given User Group');
931
    }
932
933
    /**
934
     * Loads the user groups the user belongs to.
935
     *
936
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed read the user or user group
937
     *
938
     * @param \eZ\Publish\API\Repository\Values\User\User $user
939
     * @param int $offset the start offset for paging
940
     * @param int $limit the number of user groups returned
941
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
942
     *
943
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
944
     */
945
    public function loadUserGroupsOfUser(APIUser $user, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
946
    {
947
        $locationService = $this->repository->getLocationService();
948
949
        if (!$this->repository->getPermissionResolver()->canUser('content', 'read', $user)) {
950
            throw new UnauthorizedException('content', 'read');
951
        }
952
953
        $userLocations = $locationService->loadLocations(
954
            $user->getVersionInfo()->getContentInfo()
955
        );
956
957
        $parentLocationIds = [];
958
        foreach ($userLocations as $userLocation) {
959
            if ($userLocation->parentLocationId !== null) {
960
                $parentLocationIds[] = $userLocation->parentLocationId;
961
            }
962
        }
963
964
        $searchQuery = new LocationQuery();
965
966
        $searchQuery->offset = $offset;
967
        $searchQuery->limit = $limit;
968
        $searchQuery->performCount = false;
969
970
        $searchQuery->filter = new CriterionLogicalAnd(
971
            [
972
                new CriterionContentTypeId($this->settings['userGroupClassID']),
973
                new CriterionLocationId($parentLocationIds),
974
            ]
975
        );
976
977
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
978
979
        $userGroups = [];
980
        foreach ($searchResult->searchHits as $resultItem) {
981
            $userGroups[] = $this->buildDomainUserGroupObject(
982
                $this->repository->getContentService()->internalLoadContentById(
0 ignored issues
show
Bug introduced by
The method internalLoadContentById() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
983
                    $resultItem->valueObject->contentInfo->id,
0 ignored issues
show
Documentation introduced by
The property contentInfo does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. 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...
984
                    $prioritizedLanguages
985
                )
986
            );
987
        }
988
989
        return $userGroups;
990
    }
991
992
    /**
993
     * Loads the users of a user group.
994
     *
995
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the users or user group
996
     *
997
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
998
     * @param int $offset the start offset for paging
999
     * @param int $limit the number of users returned
1000
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1001
     *
1002
     * @return \eZ\Publish\API\Repository\Values\User\User[]
1003
     */
1004
    public function loadUsersOfUserGroup(
1005
        APIUserGroup $userGroup,
1006
        $offset = 0,
1007
        $limit = 25,
1008
        array $prioritizedLanguages = []
1009
    ) {
1010
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
1011
1012
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1013
            return [];
1014
        }
1015
1016
        $mainGroupLocation = $this->repository->getLocationService()->loadLocation(
1017
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
1018
        );
1019
1020
        $searchQuery = new LocationQuery();
1021
1022
        $searchQuery->filter = new CriterionLogicalAnd(
1023
            [
1024
                new CriterionContentTypeId($this->settings['userClassID']),
1025
                new CriterionParentLocationId($mainGroupLocation->id),
1026
            ]
1027
        );
1028
1029
        $searchQuery->offset = $offset;
1030
        $searchQuery->limit = $limit;
1031
        $searchQuery->performCount = false;
1032
        $searchQuery->sortClauses = $mainGroupLocation->getSortClauses();
0 ignored issues
show
Documentation Bug introduced by
It seems like $mainGroupLocation->getSortClauses() of type array<integer,object,{"0":"object"}> is incompatible with the declared type array<integer,object<eZ\...tent\Query\SortClause>> of property $sortClauses.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1033
1034
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1035
1036
        $users = [];
1037
        foreach ($searchResult->searchHits as $resultItem) {
1038
            $users[] = $this->buildDomainUserObject(
1039
                $this->userHandler->load($resultItem->valueObject->contentInfo->id),
0 ignored issues
show
Documentation introduced by
The property contentInfo does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. 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...
1040
                $this->repository->getContentService()->internalLoadContentById(
0 ignored issues
show
Bug introduced by
The method internalLoadContentById() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1041
                    $resultItem->valueObject->contentInfo->id,
0 ignored issues
show
Documentation introduced by
The property contentInfo does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. 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...
1042
                    $prioritizedLanguages
1043
                )
1044
            );
1045
        }
1046
1047
        return $users;
1048
    }
1049
1050
    /**
1051
     * {@inheritdoc}
1052
     */
1053
    public function isUser(APIContent $content): bool
1054
    {
1055
        // First check against config for fast check
1056
        if ($this->settings['userClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId) {
1057
            return true;
1058
        }
1059
1060
        // For users we ultimately need to look for ezuser type as content type id could be several for users.
1061
        // And config might be different from one SA to the next, which we don't care about here.
1062
        foreach ($content->getFields() as $field) {
1063
            if ($field->fieldTypeIdentifier === 'ezuser') {
1064
                return true;
1065
            }
1066
        }
1067
1068
        return false;
1069
    }
1070
1071
    /**
1072
     * {@inheritdoc}
1073
     */
1074
    public function isUserGroup(APIContent $content): bool
1075
    {
1076
        return $this->settings['userGroupClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId;
1077
    }
1078
1079
    /**
1080
     * Instantiate a user create class.
1081
     *
1082
     * @param string $login the login of the new user
1083
     * @param string $email the email of the new user
1084
     * @param string $password the plain password of the new user
1085
     * @param string $mainLanguageCode the main language for the underlying content object
1086
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType 5.x the content type for the underlying content object. In 4.x it is ignored and taken from the configuration
1087
     *
1088
     * @return \eZ\Publish\API\Repository\Values\User\UserCreateStruct
1089
     */
1090
    public function newUserCreateStruct($login, $email, $password, $mainLanguageCode, $contentType = null)
1091
    {
1092
        if ($contentType === null) {
1093
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1094
                $this->settings['userClassID']
1095
            );
1096
        }
1097
1098
        $fieldDefIdentifier = '';
1099
        foreach ($contentType->fieldDefinitions as $fieldDefinition) {
1100
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
1101
                $fieldDefIdentifier = $fieldDefinition->identifier;
1102
                break;
1103
            }
1104
        }
1105
1106
        return new UserCreateStruct(
1107
            [
1108
                'contentType' => $contentType,
1109
                'mainLanguageCode' => $mainLanguageCode,
1110
                'login' => $login,
1111
                'email' => $email,
1112
                'password' => $password,
1113
                'enabled' => true,
1114
                'fields' => [
1115
                    new Field([
1116
                        'fieldDefIdentifier' => $fieldDefIdentifier,
1117
                        'languageCode' => $mainLanguageCode,
1118
                        'fieldTypeIdentifier' => 'ezuser',
1119
                        'value' => new UserValue([
1120
                            'login' => $login,
1121
                            'email' => $email,
1122
                            'plainPassword' => $password,
1123
                            'enabled' => true,
1124
                            'passwordUpdatedAt' => new DateTime(),
1125
                        ]),
1126
                    ]),
1127
                ],
1128
            ]
1129
        );
1130
    }
1131
1132
    /**
1133
     * Instantiate a user group create class.
1134
     *
1135
     * @param string $mainLanguageCode The main language for the underlying content object
1136
     * @param null|\eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType 5.x the content type for the underlying content object. In 4.x it is ignored and taken from the configuration
1137
     *
1138
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct
1139
     */
1140
    public function newUserGroupCreateStruct($mainLanguageCode, $contentType = null)
1141
    {
1142
        if ($contentType === null) {
1143
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1144
                $this->settings['userGroupClassID']
1145
            );
1146
        }
1147
1148
        return new UserGroupCreateStruct(
1149
            [
1150
                'contentType' => $contentType,
1151
                'mainLanguageCode' => $mainLanguageCode,
1152
                'fields' => [],
1153
            ]
1154
        );
1155
    }
1156
1157
    /**
1158
     * Instantiate a new user update struct.
1159
     *
1160
     * @return \eZ\Publish\API\Repository\Values\User\UserUpdateStruct
1161
     */
1162
    public function newUserUpdateStruct()
1163
    {
1164
        return new UserUpdateStruct();
1165
    }
1166
1167
    /**
1168
     * Instantiate a new user group update struct.
1169
     *
1170
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
1171
     */
1172
    public function newUserGroupUpdateStruct()
1173
    {
1174
        return new UserGroupUpdateStruct();
1175
    }
1176
1177
    /**
1178
     * {@inheritdoc}
1179
     */
1180
    public function validatePassword(string $password, PasswordValidationContext $context = null): array
1181
    {
1182
        $errors = [];
1183
1184
        if ($context === null) {
1185
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1186
                $this->settings['userClassID']
1187
            );
1188
1189
            $context = new PasswordValidationContext([
1190
                'contentType' => $contentType,
1191
            ]);
1192
        }
1193
1194
        // Search for the first ezuser field type in content type
1195
        $userFieldDefinition = $this->getUserFieldDefinition($context->contentType);
0 ignored issues
show
Bug introduced by
It seems like $context->contentType can be null; however, getUserFieldDefinition() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1196
        if ($userFieldDefinition === null) {
1197
            throw new ContentValidationException('The provided Content Type does not contain the ezuser Field Type');
1198
        }
1199
1200
        $configuration = $userFieldDefinition->getValidatorConfiguration();
1201
        if (isset($configuration['PasswordValueValidator'])) {
1202
            $errors = (new UserPasswordValidator($configuration['PasswordValueValidator']))->validate($password);
1203
        }
1204
1205
        if ($context->user !== null) {
1206
            $isPasswordTTLEnabled = $this->getPasswordInfo($context->user)->hasExpirationDate();
1207
            $isNewPasswordRequired = $configuration['PasswordValueValidator']['requireNewPassword'] ?? false;
1208
1209
            if (($isPasswordTTLEnabled || $isNewPasswordRequired) &&
1210
                $this->comparePasswordHashForAPIUser($context->user, $password)
1211
            ) {
1212
                $errors[] = new ValidationError('New password cannot be the same as old password', null, [], 'password');
1213
            }
1214
        }
1215
1216
        return $errors;
1217
    }
1218
1219
    /**
1220
     * Builds the domain UserGroup object from provided Content object.
1221
     *
1222
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1223
     *
1224
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
1225
     */
1226
    protected function buildDomainUserGroupObject(APIContent $content)
1227
    {
1228
        $locationService = $this->repository->getLocationService();
1229
1230
        if ($content->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
1231
            $mainLocation = $locationService->loadLocation(
1232
                $content->getVersionInfo()->getContentInfo()->mainLocationId
1233
            );
1234
            $parentLocation = $this->locationHandler->load($mainLocation->parentLocationId);
1235
        }
1236
1237
        return new UserGroup(
1238
            [
1239
                'content' => $content,
1240
                'parentId' => $parentLocation->contentId ?? null,
1241
            ]
1242
        );
1243
    }
1244
1245
    /**
1246
     * Builds the domain user object from provided persistence user object.
1247
     *
1248
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
1249
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
1250
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1251
     *
1252
     * @return \eZ\Publish\API\Repository\Values\User\User
1253
     */
1254
    protected function buildDomainUserObject(
1255
        SPIUser $spiUser,
1256
        APIContent $content = null,
1257
        array $prioritizedLanguages = []
1258
    ) {
1259
        if ($content === null) {
1260
            $content = $this->repository->getContentService()->internalLoadContentById(
0 ignored issues
show
Bug introduced by
The method internalLoadContentById() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1261
                $spiUser->id,
1262
                $prioritizedLanguages
1263
            );
1264
        }
1265
1266
        return new User(
1267
            [
1268
                'content' => $content,
1269
                'login' => $spiUser->login,
1270
                'email' => $spiUser->email,
1271
                'passwordHash' => $spiUser->passwordHash,
1272
                'passwordUpdatedAt' => $this->getDateTime($spiUser->passwordUpdatedAt),
1273
                'hashAlgorithm' => (int)$spiUser->hashAlgorithm,
1274
                'enabled' => $spiUser->isEnabled,
1275
                'maxLogin' => (int)$spiUser->maxLogin,
1276
            ]
1277
        );
1278
    }
1279
1280
    public function getPasswordInfo(APIUser $user): PasswordInfo
1281
    {
1282
        $passwordUpdatedAt = $user->passwordUpdatedAt;
1283
        if ($passwordUpdatedAt === null) {
1284
            return new PasswordInfo();
1285
        }
1286
1287
        $definition = $this->getUserFieldDefinition($user->getContentType());
1288
        if ($definition === null) {
1289
            return new PasswordInfo();
1290
        }
1291
1292
        $expirationDate = null;
1293
        $expirationWarningDate = null;
1294
1295
        $passwordTTL = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_SETTING];
1296
        if ($passwordTTL > 0) {
1297
            if ($passwordUpdatedAt instanceof DateTime) {
1298
                $passwordUpdatedAt = DateTimeImmutable::createFromMutable($passwordUpdatedAt);
1299
            }
1300
1301
            $expirationDate = $passwordUpdatedAt->add(new DateInterval(sprintf('P%dD', $passwordTTL)));
1302
1303
            $passwordTTLWarning = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_WARNING_SETTING];
1304
            if ($passwordTTLWarning > 0) {
1305
                $expirationWarningDate = $expirationDate->sub(new DateInterval(sprintf('P%dD', $passwordTTLWarning)));
1306
            }
1307
        }
1308
1309
        return new PasswordInfo($expirationDate, $expirationWarningDate);
1310
    }
1311
1312
    private function getUserFieldDefinition(ContentType $contentType): ?FieldDefinition
1313
    {
1314
        return $contentType->getFirstFieldDefinitionOfType('ezuser');
1315
    }
1316
1317
    /**
1318
     * Verifies if the provided login and password are valid for eZ\Publish\SPI\Persistence\User.
1319
     *
1320
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1321
     */
1322
    protected function comparePasswordHashForSPIUser(SPIUser $user, string $password): bool
1323
    {
1324
        return $this->comparePasswordHashes($password, $user->passwordHash, $user->hashAlgorithm);
1325
    }
1326
1327
    /**
1328
     * Verifies if the provided login and password are valid for eZ\Publish\API\Repository\Values\User\User.
1329
     *
1330
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1331
     */
1332
    protected function comparePasswordHashForAPIUser(APIUser $user, string $password): bool
1333
    {
1334
        return $this->comparePasswordHashes($password, $user->passwordHash, $user->hashAlgorithm);
1335
    }
1336
1337
    /**
1338
     * Verifies if the provided login and password are valid against given password hash and hash type.
1339
     *
1340
     * @param string $plainPassword User password
1341
     * @param string $passwordHash User password hash
1342
     * @param int $hashAlgorithm Hash type
1343
     *
1344
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1345
     */
1346
    private function comparePasswordHashes(
1347
        string $plainPassword,
1348
        string $passwordHash,
1349
        int $hashAlgorithm
1350
    ): bool {
1351
        return $this->passwordHashService->isValidPassword($plainPassword, $passwordHash, $hashAlgorithm);
1352
    }
1353
1354
    /**
1355
     * Return true if any of the UserUpdateStruct properties refers to User Profile (Content) update.
1356
     *
1357
     * @param UserUpdateStruct $userUpdateStruct
1358
     *
1359
     * @return bool
1360
     */
1361
    private function isUserProfileUpdateRequested(UserUpdateStruct $userUpdateStruct)
1362
    {
1363
        return
1364
            !empty($userUpdateStruct->contentUpdateStruct) ||
1365
            !empty($userUpdateStruct->contentMetadataUpdateStruct) ||
1366
            !empty($userUpdateStruct->email) ||
1367
            !empty($userUpdateStruct->enabled) ||
1368
            !empty($userUpdateStruct->maxLogin);
1369
    }
1370
1371
    private function getDateTime(?int $timestamp): ?DateTimeInterface
1372
    {
1373
        if ($timestamp !== null) {
1374
            // Instead of using DateTime(ts) we use setTimeStamp() so timezone does not get set to UTC
1375
            $dateTime = new DateTime();
1376
            $dateTime->setTimestamp($timestamp);
1377
1378
            return DateTimeImmutable::createFromMutable($dateTime);
1379
        }
1380
1381
        return null;
1382
    }
1383
}
1384