UserService::isUserGroup()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
cc 1
nc 1
nop 1
rs 10
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
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\Content\Search\SearchResult;
28
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
29
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
30
use eZ\Publish\API\Repository\Values\User\PasswordInfo;
31
use eZ\Publish\API\Repository\Values\User\PasswordValidationContext;
32
use eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct;
33
use eZ\Publish\Core\Repository\Validator\UserPasswordValidator;
34
use eZ\Publish\Core\Repository\User\PasswordHashServiceInterface;
35
use eZ\Publish\Core\Repository\Values\User\UserCreateStruct;
36
use eZ\Publish\API\Repository\Values\User\UserCreateStruct as APIUserCreateStruct;
37
use eZ\Publish\API\Repository\Values\User\UserUpdateStruct;
38
use eZ\Publish\API\Repository\Values\User\User as APIUser;
39
use eZ\Publish\API\Repository\Values\User\UserGroup as APIUserGroup;
40
use eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct as APIUserGroupCreateStruct;
41
use eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct;
42
use eZ\Publish\Core\Base\Exceptions\BadStateException;
43
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
44
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
45
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
46
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
47
use eZ\Publish\Core\FieldType\User\Value as UserValue;
48
use eZ\Publish\Core\FieldType\User\Type as UserType;
49
use eZ\Publish\Core\FieldType\ValidationError;
50
use eZ\Publish\Core\Repository\Values\User\User;
51
use eZ\Publish\Core\Repository\Values\User\UserGroup;
52
use eZ\Publish\Core\Repository\Values\User\UserGroupCreateStruct;
53
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
54
use eZ\Publish\SPI\Persistence\User as SPIUser;
55
use eZ\Publish\SPI\Persistence\User\Handler;
56
use eZ\Publish\SPI\Persistence\User\UserTokenUpdateStruct as SPIUserTokenUpdateStruct;
57
use Psr\Log\LoggerInterface;
58
59
/**
60
 * This service provides methods for managing users and user groups.
61
 */
62
class UserService implements UserServiceInterface
63
{
64
    /** @var \eZ\Publish\API\Repository\Repository */
65
    protected $repository;
66
67
    /** @var \eZ\Publish\SPI\Persistence\User\Handler */
68
    protected $userHandler;
69
70
    /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */
71
    private $locationHandler;
72
73
    /** @var array */
74
    protected $settings;
75
76
    /** @var \Psr\Log\LoggerInterface|null */
77
    protected $logger;
78
79
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
80
    private $permissionResolver;
81
82
    /** @var \eZ\Publish\Core\Repository\User\PasswordHashServiceInterface */
83
    private $passwordHashService;
84
85
    public function setLogger(LoggerInterface $logger = null)
86
    {
87
        $this->logger = $logger;
88
    }
89
90
    /**
91
     * Setups service with reference to repository object that created it & corresponding handler.
92
     *
93
     * @param \eZ\Publish\API\Repository\Repository $repository
94
     * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
95
     * @param \eZ\Publish\SPI\Persistence\Content\Location\Handler $locationHandler
96
     * @param array $settings
97
     */
98
    public function __construct(
99
        RepositoryInterface $repository,
100
        PermissionResolver $permissionResolver,
101
        Handler $userHandler,
102
        LocationHandler $locationHandler,
103
        PasswordHashServiceInterface $passwordHashGenerator,
104
        array $settings = []
105
    ) {
106
        $this->repository = $repository;
107
        $this->permissionResolver = $permissionResolver;
108
        $this->userHandler = $userHandler;
109
        $this->locationHandler = $locationHandler;
110
        // Union makes sure default settings are ignored if provided in argument
111
        $this->settings = $settings + [
112
            'defaultUserPlacement' => 12,
113
            'userClassID' => 4, // @todo Rename this settings to swap out "Class" for "Type"
114
            'userGroupClassID' => 3,
115
            'hashType' => $passwordHashGenerator->getDefaultHashType(),
116
            'siteName' => 'ez.no',
117
        ];
118
        $this->passwordHashService = $passwordHashGenerator;
119
    }
120
121
    /**
122
     * Creates a new user group using the data provided in the ContentCreateStruct parameter.
123
     *
124
     * In 4.x in the content type parameter in the profile is ignored
125
     * - the content type is determined via configuration and can be set to null.
126
     * The returned version is published.
127
     *
128
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct $userGroupCreateStruct a structure for setting all necessary data to create this user group
129
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $parentGroup
130
     *
131
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
132
     *
133
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
134
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the input structure has invalid data
135
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupCreateStruct is not valid
136
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
137
     */
138
    public function createUserGroup(APIUserGroupCreateStruct $userGroupCreateStruct, APIUserGroup $parentGroup): APIUserGroup
139
    {
140
        $contentService = $this->repository->getContentService();
141
        $locationService = $this->repository->getLocationService();
142
        $contentTypeService = $this->repository->getContentTypeService();
143
144
        if ($userGroupCreateStruct->contentType === null) {
145
            $userGroupContentType = $contentTypeService->loadContentType($this->settings['userGroupClassID']);
146
            $userGroupCreateStruct->contentType = $userGroupContentType;
147
        }
148
149
        $loadedParentGroup = $this->loadUserGroup($parentGroup->id);
150
151
        if ($loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
152
            throw new InvalidArgumentException('parentGroup', 'parent User Group has no main Location');
153
        }
154
155
        $locationCreateStruct = $locationService->newLocationCreateStruct(
156
            $loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId
157
        );
158
159
        $this->repository->beginTransaction();
160
        try {
161
            $contentDraft = $contentService->createContent($userGroupCreateStruct, [$locationCreateStruct]);
162
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
163
            $this->repository->commit();
164
        } catch (Exception $e) {
165
            $this->repository->rollback();
166
            throw $e;
167
        }
168
169
        return $this->buildDomainUserGroupObject($publishedContent);
170
    }
171
172
    /**
173
     * Loads a user group for the given id.
174
     *
175
     * @param mixed $id
176
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
177
     *
178
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
179
     *
180
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
181
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the user group with the given id was not found
182
     */
183
    public function loadUserGroup(int $id, array $prioritizedLanguages = []): APIUserGroup
184
    {
185
        $content = $this->repository->getContentService()->loadContent($id, $prioritizedLanguages);
186
187
        return $this->buildDomainUserGroupObject($content);
188
    }
189
190
    /**
191
     * Loads the sub groups of a user group.
192
     *
193
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
194
     * @param int $offset the start offset for paging
195
     * @param int $limit the number of user groups returned
196
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
197
     *
198
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
199
     *
200
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the user group
201
     */
202
    public function loadSubUserGroups(APIUserGroup $userGroup, int $offset = 0, int $limit = 25, array $prioritizedLanguages = []): iterable
203
    {
204
        $locationService = $this->repository->getLocationService();
205
206
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
207
        if (!$this->permissionResolver->canUser('content', 'read', $loadedUserGroup)) {
208
            throw new UnauthorizedException('content', 'read');
209
        }
210
211
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
212
            return [];
213
        }
214
215
        $mainGroupLocation = $locationService->loadLocation(
216
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
217
        );
218
219
        $searchResult = $this->searchSubGroups($mainGroupLocation, $offset, $limit);
220
        if ($searchResult->totalCount == 0) {
221
            return [];
222
        }
223
224
        $subUserGroups = [];
225
        foreach ($searchResult->searchHits as $searchHit) {
226
            $subUserGroups[] = $this->buildDomainUserGroupObject(
227
                $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...
228
                    $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...
229
                    $prioritizedLanguages
230
                )
231
            );
232
        }
233
234
        return $subUserGroups;
235
    }
236
237
    /**
238
     * Returns (searches) subgroups of a user group described by its main location.
239
     *
240
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
241
     * @param int $offset
242
     * @param int $limit
243
     *
244
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
245
     */
246
    protected function searchSubGroups(Location $location, int $offset = 0, int $limit = 25): SearchResult
247
    {
248
        $searchQuery = new LocationQuery();
249
250
        $searchQuery->offset = $offset;
251
        $searchQuery->limit = $limit;
252
253
        $searchQuery->filter = new CriterionLogicalAnd([
254
            new CriterionContentTypeId($this->settings['userGroupClassID']),
255
            new CriterionParentLocationId($location->id),
256
        ]);
257
258
        $searchQuery->sortClauses = $location->getSortClauses();
259
260
        return $this->repository->getSearchService()->findLocations($searchQuery, [], false);
261
    }
262
263
    /**
264
     * Removes a user group.
265
     *
266
     * the users which are not assigned to other groups will be deleted.
267
     *
268
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
269
     *
270
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
271
     */
272
    public function deleteUserGroup(APIUserGroup $userGroup): iterable
273
    {
274
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
275
276
        $this->repository->beginTransaction();
277
        try {
278
            //@todo: what happens to sub user groups and users below sub user groups
279
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUserGroup->getVersionInfo()->getContentInfo());
280
            $this->repository->commit();
281
        } catch (Exception $e) {
282
            $this->repository->rollback();
283
            throw $e;
284
        }
285
286
        return $affectedLocationIds;
287
    }
288
289
    /**
290
     * Moves the user group to another parent.
291
     *
292
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
293
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $newParent
294
     *
295
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
296
     */
297
    public function moveUserGroup(APIUserGroup $userGroup, APIUserGroup $newParent): void
298
    {
299
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
300
        $loadedNewParent = $this->loadUserGroup($newParent->id);
301
302
        $locationService = $this->repository->getLocationService();
303
304
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
305
            throw new BadStateException('userGroup', 'existing User Group is not stored and/or does not have any Location yet');
306
        }
307
308
        if ($loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId === null) {
309
            throw new BadStateException('newParent', 'new User Group is not stored and/or does not have any Location yet');
310
        }
311
312
        $userGroupMainLocation = $locationService->loadLocation(
313
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
314
        );
315
        $newParentMainLocation = $locationService->loadLocation(
316
            $loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId
317
        );
318
319
        $this->repository->beginTransaction();
320
        try {
321
            $locationService->moveSubtree($userGroupMainLocation, $newParentMainLocation);
322
            $this->repository->commit();
323
        } catch (Exception $e) {
324
            $this->repository->rollback();
325
            throw $e;
326
        }
327
    }
328
329
    /**
330
     * Updates the group profile with fields and meta data.
331
     *
332
     * 4.x: If the versionUpdateStruct is set in $userGroupUpdateStruct, this method internally creates a content draft, updates ts with the provided data
333
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
334
     *
335
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
336
     * @param UserGroupUpdateStruct $userGroupUpdateStruct
337
     *
338
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
339
     *
340
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user group
341
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupUpdateStruct is not valid
342
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
343
     */
344
    public function updateUserGroup(APIUserGroup $userGroup, UserGroupUpdateStruct $userGroupUpdateStruct): APIUserGroup
345
    {
346
        if ($userGroupUpdateStruct->contentUpdateStruct === null &&
347
            $userGroupUpdateStruct->contentMetadataUpdateStruct === null) {
348
            // both update structs are empty, nothing to do
349
            return $userGroup;
350
        }
351
352
        $contentService = $this->repository->getContentService();
353
354
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
355
356
        $this->repository->beginTransaction();
357
        try {
358
            $publishedContent = $loadedUserGroup;
359
            if ($userGroupUpdateStruct->contentUpdateStruct !== null) {
360
                $contentDraft = $contentService->createContentDraft($loadedUserGroup->getVersionInfo()->getContentInfo());
361
362
                $contentDraft = $contentService->updateContent(
363
                    $contentDraft->getVersionInfo(),
364
                    $userGroupUpdateStruct->contentUpdateStruct
365
                );
366
367
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
368
            }
369
370
            if ($userGroupUpdateStruct->contentMetadataUpdateStruct !== null) {
371
                $publishedContent = $contentService->updateContentMetadata(
372
                    $publishedContent->getVersionInfo()->getContentInfo(),
373
                    $userGroupUpdateStruct->contentMetadataUpdateStruct
374
                );
375
            }
376
377
            $this->repository->commit();
378
        } catch (Exception $e) {
379
            $this->repository->rollback();
380
            throw $e;
381
        }
382
383
        return $this->buildDomainUserGroupObject($publishedContent);
384
    }
385
386
    /**
387
     * Create a new user. The created user is published by this method.
388
     *
389
     * @param APIUserCreateStruct $userCreateStruct the data used for creating the user
390
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup[] $parentGroups the groups which are assigned to the user after creation
391
     *
392
     * @return \eZ\Publish\API\Repository\Values\User\User
393
     *
394
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
395
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userCreateStruct is not valid
396
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
397
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a user with provided login already exists
398
     */
399
    public function createUser(APIUserCreateStruct $userCreateStruct, array $parentGroups): APIUser
400
    {
401
        $contentService = $this->repository->getContentService();
402
        $locationService = $this->repository->getLocationService();
403
404
        $locationCreateStructs = [];
405
        foreach ($parentGroups as $parentGroup) {
406
            $parentGroup = $this->loadUserGroup($parentGroup->id);
407
            if ($parentGroup->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
408
                $locationCreateStructs[] = $locationService->newLocationCreateStruct(
409
                    $parentGroup->getVersionInfo()->getContentInfo()->mainLocationId
410
                );
411
            }
412
        }
413
414
        // Search for the first ezuser field type in content type
415
        $userFieldDefinition = $this->getUserFieldDefinition($userCreateStruct->contentType);
416
        if ($userFieldDefinition === null) {
417
            throw new ContentValidationException('the provided Content Type does not contain the ezuser Field Type');
418
        }
419
420
        $this->repository->beginTransaction();
421
        try {
422
            $contentDraft = $contentService->createContent($userCreateStruct, $locationCreateStructs);
423
            // There is no need to create user separately, just load it from SPI
424
            $spiUser = $this->userHandler->load($contentDraft->id);
425
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
426
427
            // User\Handler::create call is currently used to clear cache only
428
            $this->userHandler->create(
429
                new SPIUser(
430
                    [
431
                        'id' => $spiUser->id,
432
                        'login' => $spiUser->login,
433
                        'email' => $spiUser->email,
434
                    ]
435
                )
436
            );
437
438
            $this->repository->commit();
439
        } catch (Exception $e) {
440
            $this->repository->rollback();
441
            throw $e;
442
        }
443
444
        return $this->buildDomainUserObject($spiUser, $publishedContent);
445
    }
446
447
    /**
448
     * Loads a user.
449
     *
450
     * @param int $userId
451
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
452
     *
453
     * @return \eZ\Publish\API\Repository\Values\User\User
454
     *
455
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given id was not found
456
     */
457
    public function loadUser(int $userId, array $prioritizedLanguages = []): APIUser
458
    {
459
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
460
        $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...
461
        // Get spiUser value from Field Value
462
        foreach ($content->getFields() as $field) {
463
            if (!$field->value instanceof UserValue) {
464
                continue;
465
            }
466
467
            /** @var \eZ\Publish\Core\FieldType\User\Value $value */
468
            $value = $field->value;
469
            $spiUser = new SPIUser();
470
            $spiUser->id = $value->contentId;
471
            $spiUser->login = $value->login;
472
            $spiUser->email = $value->email;
473
            $spiUser->hashAlgorithm = $value->passwordHashType;
474
            $spiUser->passwordHash = $value->passwordHash;
475
            $spiUser->passwordUpdatedAt = $value->passwordUpdatedAt ? $value->passwordUpdatedAt->getTimestamp() : null;
476
            $spiUser->isEnabled = $value->enabled;
477
            $spiUser->maxLogin = $value->maxLogin;
478
            break;
479
        }
480
481
        // If for some reason not found, load it
482
        if (!isset($spiUser)) {
483
            $spiUser = $this->userHandler->load($userId);
484
        }
485
486
        return $this->buildDomainUserObject($spiUser, $content);
487
    }
488
489
    /**
490
     * Checks if credentials are valid for provided User.
491
     *
492
     * @param \eZ\Publish\API\Repository\Values\User\User $user
493
     * @param string $credentials
494
     *
495
     * @return bool
496
     */
497
    public function checkUserCredentials(APIUser $user, string $credentials): bool
498
    {
499
        return $this->comparePasswordHashForAPIUser($user, $credentials);
500
    }
501
502
    /**
503
     * Update password hash to the type configured for the service, if they differ.
504
     *
505
     * @param string $login User login
506
     * @param string $password User password
507
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
508
     *
509
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException if the password is not correctly saved, in which case the update is reverted
510
     */
511
    private function updatePasswordHash(string $login, string $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...
512
    {
513
        $hashType = $this->passwordHashService->getDefaultHashType();
514
        if ($spiUser->hashAlgorithm === $hashType) {
515
            return;
516
        }
517
518
        $spiUser->passwordHash = $this->passwordHashService->createPasswordHash($password, $hashType);
519
        $spiUser->hashAlgorithm = $hashType;
520
521
        $this->repository->beginTransaction();
522
        $this->userHandler->update($spiUser);
523
        $reloadedSpiUser = $this->userHandler->load($spiUser->id);
524
525
        if ($reloadedSpiUser->passwordHash === $spiUser->passwordHash) {
526
            $this->repository->commit();
527
        } else {
528
            // Password hash was not correctly saved, possible cause: EZP-28692
529
            $this->repository->rollback();
530
            if (isset($this->logger)) {
531
                $this->logger->critical('Password hash could not be updated. Please verify that your database schema is up to date.');
532
            }
533
534
            throw new BadStateException(
535
                'user',
536
                'Could not save updated password hash, reverting to previous hash. Please verify that your database schema is up to date.'
537
            );
538
        }
539
    }
540
541
    /**
542
     * Loads a user for the given login.
543
     *
544
     * {@inheritdoc}
545
     *
546
     * @param string $login
547
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
548
     *
549
     * @return \eZ\Publish\API\Repository\Values\User\User
550
     *
551
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
552
     */
553
    public function loadUserByLogin(string $login, array $prioritizedLanguages = []): APIUser
554
    {
555
        if (empty($login)) {
556
            throw new InvalidArgumentValue('login', $login);
557
        }
558
559
        $spiUser = $this->userHandler->loadByLogin($login);
560
561
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
562
    }
563
564
    /**
565
     * Loads a user for the given email.
566
     *
567
     * {@inheritdoc}
568
     *
569
     * @param string $email
570
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
571
     *
572
     * @return \eZ\Publish\API\Repository\Values\User\User
573
     *
574
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
575
     */
576
    public function loadUserByEmail(string $email, array $prioritizedLanguages = []): APIUser
577
    {
578
        if (empty($email)) {
579
            throw new InvalidArgumentValue('email', $email);
580
        }
581
582
        $spiUser = $this->userHandler->loadByEmail($email);
583
584
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
585
    }
586
587
    /**
588
     * Loads a user for the given email.
589
     *
590
     * {@inheritdoc}
591
     *
592
     * @param string $email
593
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
594
     *
595
     * @return \eZ\Publish\API\Repository\Values\User\User[]
596
     *
597
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
598
     */
599
    public function loadUsersByEmail(string $email, array $prioritizedLanguages = []): iterable
600
    {
601
        if (empty($email)) {
602
            throw new InvalidArgumentValue('email', $email);
603
        }
604
605
        $users = [];
606
        foreach ($this->userHandler->loadUsersByEmail($email) as $spiUser) {
607
            $users[] = $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
608
        }
609
610
        return $users;
611
    }
612
613
    /**
614
     * Loads a user for the given token.
615
     *
616
     * {@inheritdoc}
617
     *
618
     * @param string $hash
619
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
620
     *
621
     * @return \eZ\Publish\API\Repository\Values\User\User
622
     *
623
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
624
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
625
     */
626
    public function loadUserByToken(string $hash, array $prioritizedLanguages = []): APIUser
627
    {
628
        if (empty($hash)) {
629
            throw new InvalidArgumentValue('hash', $hash);
630
        }
631
632
        $spiUser = $this->userHandler->loadUserByToken($hash);
633
634
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
635
    }
636
637
    /**
638
     * This method deletes a user.
639
     *
640
     * @param \eZ\Publish\API\Repository\Values\User\User $user
641
     *
642
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to delete the user
643
     */
644
    public function deleteUser(APIUser $user): iterable
645
    {
646
        $loadedUser = $this->loadUser($user->id);
647
648
        $this->repository->beginTransaction();
649
        try {
650
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo());
651
652
            // User\Handler::delete call is currently used to clear cache only
653
            $this->userHandler->delete($loadedUser->id);
654
            $this->repository->commit();
655
        } catch (Exception $e) {
656
            $this->repository->rollback();
657
            throw $e;
658
        }
659
660
        return $affectedLocationIds ?? [];
661
    }
662
663
    /**
664
     * Updates a user.
665
     *
666
     * 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
667
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
668
     *
669
     * @param \eZ\Publish\API\Repository\Values\User\User $user
670
     * @param UserUpdateStruct $userUpdateStruct
671
     *
672
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userUpdateStruct is not valid
673
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
674
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user
675
     */
676
    public function updateUser(APIUser $user, UserUpdateStruct $userUpdateStruct): APIUser
677
    {
678
        $loadedUser = $this->loadUser($user->id);
679
680
        $contentService = $this->repository->getContentService();
681
682
        $canEditContent = $this->permissionResolver->canUser('content', 'edit', $loadedUser);
683
684
        if (!$canEditContent && $this->isUserProfileUpdateRequested($userUpdateStruct)) {
685
            throw new UnauthorizedException('content', 'edit');
686
        }
687
688
        $userFieldDefinition = null;
689
        foreach ($loadedUser->getContentType()->fieldDefinitions as $fieldDefinition) {
690
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
691
                $userFieldDefinition = $fieldDefinition;
692
                break;
693
            }
694
        }
695
696
        if ($userFieldDefinition === null) {
697
            throw new ContentValidationException('The provided Content Type does not contain the ezuser Field Type');
698
        }
699
700
        $userUpdateStruct->contentUpdateStruct = $userUpdateStruct->contentUpdateStruct ?? $contentService->newContentUpdateStruct();
701
702
        $providedUserUpdateDataInField = false;
703
        foreach ($userUpdateStruct->contentUpdateStruct->fields as $field) {
704
            if ($field->value instanceof UserValue) {
705
                $providedUserUpdateDataInField = true;
706
                break;
707
            }
708
        }
709
710
        if (!$providedUserUpdateDataInField) {
711
            $userUpdateStruct->contentUpdateStruct->setField(
712
                $userFieldDefinition->identifier,
713
                new UserValue([
714
                    'contentId' => $loadedUser->id,
715
                    'hasStoredLogin' => true,
716
                    'login' => $loadedUser->login,
717
                    'email' => $userUpdateStruct->email ?? $loadedUser->email,
718
                    'plainPassword' => $userUpdateStruct->password,
719
                    'enabled' => $userUpdateStruct->enabled ?? $loadedUser->enabled,
720
                    'maxLogin' => $userUpdateStruct->maxLogin ?? $loadedUser->maxLogin,
721
                    'passwordHashType' => $user->hashAlgorithm,
722
                    'passwordHash' => $user->passwordHash,
723
                ])
724
            );
725
        }
726
727
        if (!empty($userUpdateStruct->password) &&
728
            !$canEditContent &&
729
            !$this->permissionResolver->canUser('user', 'password', $loadedUser)
730
        ) {
731
            throw new UnauthorizedException('user', 'password');
732
        }
733
734
        $this->repository->beginTransaction();
735
        try {
736
            $publishedContent = $loadedUser;
737
            if ($userUpdateStruct->contentUpdateStruct !== null) {
738
                $contentDraft = $contentService->createContentDraft($loadedUser->getVersionInfo()->getContentInfo());
739
                $contentDraft = $contentService->updateContent(
740
                    $contentDraft->getVersionInfo(),
741
                    $userUpdateStruct->contentUpdateStruct
742
                );
743
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
744
            }
745
746
            if ($userUpdateStruct->contentMetadataUpdateStruct !== null) {
747
                $contentService->updateContentMetadata(
748
                    $publishedContent->getVersionInfo()->getContentInfo(),
749
                    $userUpdateStruct->contentMetadataUpdateStruct
750
                );
751
            }
752
753
            // User\Handler::update call is currently used to clear cache only
754
            $this->userHandler->update(
755
                new SPIUser(
756
                    [
757
                        'id' => $loadedUser->id,
758
                        'login' => $loadedUser->login,
759
                        'email' => $userUpdateStruct->email ?: $loadedUser->email,
760
                    ]
761
                )
762
            );
763
764
            $this->repository->commit();
765
        } catch (Exception $e) {
766
            $this->repository->rollback();
767
            throw $e;
768
        }
769
770
        return $this->loadUser($loadedUser->id);
771
    }
772
773
    /**
774
     * Update the user token information specified by the user token struct.
775
     *
776
     * @param \eZ\Publish\API\Repository\Values\User\User $user
777
     * @param \eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct $userTokenUpdateStruct
778
     *
779
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
780
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
781
     * @throws \RuntimeException
782
     * @throws \Exception
783
     *
784
     * @return \eZ\Publish\API\Repository\Values\User\User
785
     */
786
    public function updateUserToken(APIUser $user, UserTokenUpdateStruct $userTokenUpdateStruct): APIUser
787
    {
788
        $loadedUser = $this->loadUser($user->id);
789
790
        if ($userTokenUpdateStruct->hashKey !== null && (!is_string($userTokenUpdateStruct->hashKey) || empty($userTokenUpdateStruct->hashKey))) {
791
            throw new InvalidArgumentValue('hashKey', $userTokenUpdateStruct->hashKey, 'UserTokenUpdateStruct');
792
        }
793
794
        if ($userTokenUpdateStruct->time === null) {
795
            throw new InvalidArgumentValue('time', $userTokenUpdateStruct->time, 'UserTokenUpdateStruct');
796
        }
797
798
        $this->repository->beginTransaction();
799
        try {
800
            $this->userHandler->updateUserToken(
801
                new SPIUserTokenUpdateStruct(
802
                    [
803
                        'userId' => $loadedUser->id,
804
                        'hashKey' => $userTokenUpdateStruct->hashKey,
805
                        'time' => $userTokenUpdateStruct->time->getTimestamp(),
806
                    ]
807
                )
808
            );
809
            $this->repository->commit();
810
        } catch (Exception $e) {
811
            $this->repository->rollback();
812
            throw $e;
813
        }
814
815
        return $this->loadUser($loadedUser->id);
816
    }
817
818
    /**
819
     * Expires user token with user hash.
820
     *
821
     * @param string $hash
822
     */
823
    public function expireUserToken(string $hash): void
824
    {
825
        $this->repository->beginTransaction();
826
        try {
827
            $this->userHandler->expireUserToken($hash);
828
            $this->repository->commit();
829
        } catch (Exception $e) {
830
            $this->repository->rollback();
831
            throw $e;
832
        }
833
    }
834
835
    /**
836
     * Assigns a new user group to the user.
837
     *
838
     * @param \eZ\Publish\API\Repository\Values\User\User $user
839
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
840
     *
841
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to assign the user group to the user
842
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is already in the given user group
843
     */
844
    public function assignUserToUserGroup(APIUser $user, APIUserGroup $userGroup): void
845
    {
846
        $loadedUser = $this->loadUser($user->id);
847
        $loadedGroup = $this->loadUserGroup($userGroup->id);
848
        $locationService = $this->repository->getLocationService();
849
850
        $existingGroupIds = [];
851
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
852
        foreach ($userLocations as $userLocation) {
853
            $existingGroupIds[] = $userLocation->parentLocationId;
854
        }
855
856
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
857
            throw new BadStateException('userGroup', 'User Group has no main Location or no Locations');
858
        }
859
860
        if (in_array($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId, $existingGroupIds)) {
861
            // user is already assigned to the user group
862
            throw new InvalidArgumentException('user', 'User is already in the given User Group');
863
        }
864
865
        $locationCreateStruct = $locationService->newLocationCreateStruct(
866
            $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId
867
        );
868
869
        $this->repository->beginTransaction();
870
        try {
871
            $locationService->createLocation(
872
                $loadedUser->getVersionInfo()->getContentInfo(),
873
                $locationCreateStruct
874
            );
875
            $this->repository->commit();
876
        } catch (Exception $e) {
877
            $this->repository->rollback();
878
            throw $e;
879
        }
880
    }
881
882
    /**
883
     * Removes a user group from the user.
884
     *
885
     * @param \eZ\Publish\API\Repository\Values\User\User $user
886
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
887
     *
888
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to remove the user group from the user
889
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is not in the given user group
890
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $userGroup is the last assigned user group
891
     */
892
    public function unAssignUserFromUserGroup(APIUser $user, APIUserGroup $userGroup): void
893
    {
894
        $loadedUser = $this->loadUser($user->id);
895
        $loadedGroup = $this->loadUserGroup($userGroup->id);
896
        $locationService = $this->repository->getLocationService();
897
898
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
899
        if (empty($userLocations)) {
900
            throw new BadStateException('user', 'User has no Locations, cannot unassign from group');
901
        }
902
903
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
904
            throw new BadStateException('userGroup', 'User Group has no main Location or no Locations, cannot unassign');
905
        }
906
907
        foreach ($userLocations as $userLocation) {
908
            if ($userLocation->parentLocationId == $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId) {
909
                // Throw this specific BadState when we know argument is valid
910
                if (count($userLocations) === 1) {
911
                    throw new BadStateException('user', 'User only has one User Group, cannot unassign from last group');
912
                }
913
914
                $this->repository->beginTransaction();
915
                try {
916
                    $locationService->deleteLocation($userLocation);
917
                    $this->repository->commit();
918
919
                    return;
920
                } catch (Exception $e) {
921
                    $this->repository->rollback();
922
                    throw $e;
923
                }
924
            }
925
        }
926
927
        throw new InvalidArgumentException('userGroup', 'User is not in the given User Group');
928
    }
929
930
    /**
931
     * Loads the user groups the user belongs to.
932
     *
933
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed read the user or user group
934
     *
935
     * @param \eZ\Publish\API\Repository\Values\User\User $user
936
     * @param int $offset the start offset for paging
937
     * @param int $limit the number of user groups returned
938
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
939
     *
940
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
941
     */
942
    public function loadUserGroupsOfUser(APIUser $user, int $offset = 0, int $limit = 25, array $prioritizedLanguages = []): iterable
943
    {
944
        $locationService = $this->repository->getLocationService();
945
946
        if (!$this->repository->getPermissionResolver()->canUser('content', 'read', $user)) {
947
            throw new UnauthorizedException('content', 'read');
948
        }
949
950
        $userLocations = $locationService->loadLocations(
951
            $user->getVersionInfo()->getContentInfo()
952
        );
953
954
        $parentLocationIds = [];
955
        foreach ($userLocations as $userLocation) {
956
            if ($userLocation->parentLocationId !== null) {
957
                $parentLocationIds[] = $userLocation->parentLocationId;
958
            }
959
        }
960
961
        $searchQuery = new LocationQuery();
962
963
        $searchQuery->offset = $offset;
964
        $searchQuery->limit = $limit;
965
        $searchQuery->performCount = false;
966
967
        $searchQuery->filter = new CriterionLogicalAnd(
968
            [
969
                new CriterionContentTypeId($this->settings['userGroupClassID']),
970
                new CriterionLocationId($parentLocationIds),
971
            ]
972
        );
973
974
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
975
976
        $userGroups = [];
977
        foreach ($searchResult->searchHits as $resultItem) {
978
            $userGroups[] = $this->buildDomainUserGroupObject(
979
                $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...
980
                    $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...
981
                    $prioritizedLanguages
982
                )
983
            );
984
        }
985
986
        return $userGroups;
987
    }
988
989
    /**
990
     * Loads the users of a user group.
991
     *
992
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the users or user group
993
     *
994
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
995
     * @param int $offset the start offset for paging
996
     * @param int $limit the number of users returned
997
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
998
     *
999
     * @return \eZ\Publish\API\Repository\Values\User\User[]
1000
     */
1001
    public function loadUsersOfUserGroup(
1002
        APIUserGroup $userGroup,
1003
        int $offset = 0,
1004
        int $limit = 25,
1005
        array $prioritizedLanguages = []
1006
    ): iterable {
1007
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
1008
1009
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1010
            return [];
1011
        }
1012
1013
        $mainGroupLocation = $this->repository->getLocationService()->loadLocation(
1014
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
1015
        );
1016
1017
        $searchQuery = new LocationQuery();
1018
1019
        $searchQuery->filter = new CriterionLogicalAnd(
1020
            [
1021
                new CriterionContentTypeId($this->settings['userClassID']),
1022
                new CriterionParentLocationId($mainGroupLocation->id),
1023
            ]
1024
        );
1025
1026
        $searchQuery->offset = $offset;
1027
        $searchQuery->limit = $limit;
1028
        $searchQuery->performCount = false;
1029
        $searchQuery->sortClauses = $mainGroupLocation->getSortClauses();
1030
1031
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1032
1033
        $users = [];
1034
        foreach ($searchResult->searchHits as $resultItem) {
1035
            $users[] = $this->buildDomainUserObject(
1036
                $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...
1037
                $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...
1038
                    $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...
1039
                    $prioritizedLanguages
1040
                )
1041
            );
1042
        }
1043
1044
        return $users;
1045
    }
1046
1047
    /**
1048
     * {@inheritdoc}
1049
     */
1050
    public function isUser(APIContent $content): bool
1051
    {
1052
        // First check against config for fast check
1053
        if ($this->settings['userClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId) {
1054
            return true;
1055
        }
1056
1057
        // For users we ultimately need to look for ezuser type as content type id could be several for users.
1058
        // And config might be different from one SA to the next, which we don't care about here.
1059
        foreach ($content->getFields() as $field) {
1060
            if ($field->fieldTypeIdentifier === 'ezuser') {
1061
                return true;
1062
            }
1063
        }
1064
1065
        return false;
1066
    }
1067
1068
    /**
1069
     * {@inheritdoc}
1070
     */
1071
    public function isUserGroup(APIContent $content): bool
1072
    {
1073
        return $this->settings['userGroupClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId;
1074
    }
1075
1076
    /**
1077
     * Instantiate a user create class.
1078
     *
1079
     * @param string $login the login of the new user
1080
     * @param string $email the email of the new user
1081
     * @param string $password the plain password of the new user
1082
     * @param string $mainLanguageCode the main language for the underlying content object
1083
     * @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
1084
     *
1085
     * @return \eZ\Publish\API\Repository\Values\User\UserCreateStruct
1086
     */
1087
    public function newUserCreateStruct(string $login, string $email, string $password, string $mainLanguageCode, ?ContentType $contentType = null): APIUserCreateStruct
1088
    {
1089
        if ($contentType === null) {
1090
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1091
                $this->settings['userClassID']
1092
            );
1093
        }
1094
1095
        $fieldDefIdentifier = '';
1096
        foreach ($contentType->fieldDefinitions as $fieldDefinition) {
1097
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
1098
                $fieldDefIdentifier = $fieldDefinition->identifier;
1099
                break;
1100
            }
1101
        }
1102
1103
        return new UserCreateStruct(
1104
            [
1105
                'contentType' => $contentType,
1106
                'mainLanguageCode' => $mainLanguageCode,
1107
                'login' => $login,
1108
                'email' => $email,
1109
                'password' => $password,
1110
                'enabled' => true,
1111
                'fields' => [
1112
                    new Field([
1113
                        'fieldDefIdentifier' => $fieldDefIdentifier,
1114
                        'languageCode' => $mainLanguageCode,
1115
                        'fieldTypeIdentifier' => 'ezuser',
1116
                        'value' => new UserValue([
1117
                            'login' => $login,
1118
                            'email' => $email,
1119
                            'plainPassword' => $password,
1120
                            'enabled' => true,
1121
                            'passwordUpdatedAt' => new DateTime(),
1122
                        ]),
1123
                    ]),
1124
                ],
1125
            ]
1126
        );
1127
    }
1128
1129
    /**
1130
     * Instantiate a user group create class.
1131
     *
1132
     * @param string $mainLanguageCode The main language for the underlying content object
1133
     * @param \eZ\Publish\API\Repository\Values\ContentType\ContentType|null $contentType 5.x the content type for the underlying content object. In 4.x it is ignored and taken from the configuration
1134
     *
1135
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct
1136
     */
1137
    public function newUserGroupCreateStruct(string $mainLanguageCode, ?ContentType $contentType = null): APIUserGroupCreateStruct
1138
    {
1139
        if ($contentType === null) {
1140
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1141
                $this->settings['userGroupClassID']
1142
            );
1143
        }
1144
1145
        return new UserGroupCreateStruct(
1146
            [
1147
                'contentType' => $contentType,
1148
                'mainLanguageCode' => $mainLanguageCode,
1149
                'fields' => [],
1150
            ]
1151
        );
1152
    }
1153
1154
    /**
1155
     * Instantiate a new user update struct.
1156
     *
1157
     * @return \eZ\Publish\API\Repository\Values\User\UserUpdateStruct
1158
     */
1159
    public function newUserUpdateStruct(): UserUpdateStruct
1160
    {
1161
        return new UserUpdateStruct();
1162
    }
1163
1164
    /**
1165
     * Instantiate a new user group update struct.
1166
     *
1167
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
1168
     */
1169
    public function newUserGroupUpdateStruct(): UserGroupUpdateStruct
1170
    {
1171
        return new UserGroupUpdateStruct();
1172
    }
1173
1174
    /**
1175
     * {@inheritdoc}
1176
     */
1177
    public function validatePassword(string $password, PasswordValidationContext $context = null): array
1178
    {
1179
        $errors = [];
1180
1181
        if ($context === null) {
1182
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1183
                $this->settings['userClassID']
1184
            );
1185
1186
            $context = new PasswordValidationContext([
1187
                'contentType' => $contentType,
1188
            ]);
1189
        }
1190
1191
        // Search for the first ezuser field type in content type
1192
        $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...
1193
        if ($userFieldDefinition === null) {
1194
            throw new ContentValidationException('The provided Content Type does not contain the ezuser Field Type');
1195
        }
1196
1197
        $configuration = $userFieldDefinition->getValidatorConfiguration();
1198
        if (isset($configuration['PasswordValueValidator'])) {
1199
            $errors = (new UserPasswordValidator($configuration['PasswordValueValidator']))->validate($password);
1200
        }
1201
1202
        if ($context->user !== null) {
1203
            $isPasswordTTLEnabled = $this->getPasswordInfo($context->user)->hasExpirationDate();
1204
            $isNewPasswordRequired = $configuration['PasswordValueValidator']['requireNewPassword'] ?? false;
1205
1206
            if (($isPasswordTTLEnabled || $isNewPasswordRequired) &&
1207
                $this->comparePasswordHashForAPIUser($context->user, $password)
1208
            ) {
1209
                $errors[] = new ValidationError('New password cannot be the same as old password', null, [], 'password');
1210
            }
1211
        }
1212
1213
        return $errors;
1214
    }
1215
1216
    /**
1217
     * Builds the domain UserGroup object from provided Content object.
1218
     *
1219
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1220
     *
1221
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
1222
     */
1223
    protected function buildDomainUserGroupObject(APIContent $content): APIUserGroup
1224
    {
1225
        $locationService = $this->repository->getLocationService();
1226
1227
        if ($content->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
1228
            $mainLocation = $locationService->loadLocation(
1229
                $content->getVersionInfo()->getContentInfo()->mainLocationId
1230
            );
1231
            $parentLocation = $this->locationHandler->load($mainLocation->parentLocationId);
1232
        }
1233
1234
        return new UserGroup(
1235
            [
1236
                'content' => $content,
1237
                'parentId' => $parentLocation->contentId ?? null,
1238
            ]
1239
        );
1240
    }
1241
1242
    /**
1243
     * Builds the domain user object from provided persistence user object.
1244
     *
1245
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
1246
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
1247
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1248
     *
1249
     * @return \eZ\Publish\API\Repository\Values\User\User
1250
     */
1251
    protected function buildDomainUserObject(
1252
        SPIUser $spiUser,
1253
        APIContent $content = null,
1254
        array $prioritizedLanguages = []
1255
    ): APIUser {
1256
        if ($content === null) {
1257
            $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...
1258
                $spiUser->id,
1259
                $prioritizedLanguages
1260
            );
1261
        }
1262
1263
        return new User(
1264
            [
1265
                'content' => $content,
1266
                'login' => $spiUser->login,
1267
                'email' => $spiUser->email,
1268
                'passwordHash' => $spiUser->passwordHash,
1269
                'passwordUpdatedAt' => $this->getDateTime($spiUser->passwordUpdatedAt),
1270
                'hashAlgorithm' => (int)$spiUser->hashAlgorithm,
1271
                'enabled' => $spiUser->isEnabled,
1272
                'maxLogin' => (int)$spiUser->maxLogin,
1273
            ]
1274
        );
1275
    }
1276
1277
    public function getPasswordInfo(APIUser $user): PasswordInfo
1278
    {
1279
        $passwordUpdatedAt = $user->passwordUpdatedAt;
1280
        if ($passwordUpdatedAt === null) {
1281
            return new PasswordInfo();
1282
        }
1283
1284
        $definition = $this->getUserFieldDefinition($user->getContentType());
1285
        if ($definition === null) {
1286
            return new PasswordInfo();
1287
        }
1288
1289
        $expirationDate = null;
1290
        $expirationWarningDate = null;
1291
1292
        $passwordTTL = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_SETTING];
1293
        if ($passwordTTL > 0) {
1294
            if ($passwordUpdatedAt instanceof DateTime) {
1295
                $passwordUpdatedAt = DateTimeImmutable::createFromMutable($passwordUpdatedAt);
1296
            }
1297
1298
            $expirationDate = $passwordUpdatedAt->add(new DateInterval(sprintf('P%dD', $passwordTTL)));
1299
1300
            $passwordTTLWarning = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_WARNING_SETTING];
1301
            if ($passwordTTLWarning > 0) {
1302
                $expirationWarningDate = $expirationDate->sub(new DateInterval(sprintf('P%dD', $passwordTTLWarning)));
1303
            }
1304
        }
1305
1306
        return new PasswordInfo($expirationDate, $expirationWarningDate);
1307
    }
1308
1309
    private function getUserFieldDefinition(ContentType $contentType): ?FieldDefinition
1310
    {
1311
        return $contentType->getFirstFieldDefinitionOfType('ezuser');
1312
    }
1313
1314
    /**
1315
     * Verifies if the provided login and password are valid for eZ\Publish\SPI\Persistence\User.
1316
     *
1317
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1318
     */
1319
    protected function comparePasswordHashForSPIUser(SPIUser $user, string $password): bool
1320
    {
1321
        return $this->comparePasswordHashes($password, $user->passwordHash, $user->hashAlgorithm);
1322
    }
1323
1324
    /**
1325
     * Verifies if the provided login and password are valid for eZ\Publish\API\Repository\Values\User\User.
1326
     *
1327
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1328
     */
1329
    protected function comparePasswordHashForAPIUser(APIUser $user, string $password): bool
1330
    {
1331
        return $this->comparePasswordHashes($password, $user->passwordHash, $user->hashAlgorithm);
1332
    }
1333
1334
    /**
1335
     * Verifies if the provided login and password are valid against given password hash and hash type.
1336
     *
1337
     * @param string $plainPassword User password
1338
     * @param string $passwordHash User password hash
1339
     * @param int $hashAlgorithm Hash type
1340
     *
1341
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1342
     */
1343
    private function comparePasswordHashes(
1344
        string $plainPassword,
1345
        string $passwordHash,
1346
        int $hashAlgorithm
1347
    ): bool {
1348
        return $this->passwordHashService->isValidPassword($plainPassword, $passwordHash, $hashAlgorithm);
1349
    }
1350
1351
    /**
1352
     * Return true if any of the UserUpdateStruct properties refers to User Profile (Content) update.
1353
     *
1354
     * @param UserUpdateStruct $userUpdateStruct
1355
     *
1356
     * @return bool
1357
     */
1358
    private function isUserProfileUpdateRequested(UserUpdateStruct $userUpdateStruct)
1359
    {
1360
        return
1361
            !empty($userUpdateStruct->contentUpdateStruct) ||
1362
            !empty($userUpdateStruct->contentMetadataUpdateStruct) ||
1363
            !empty($userUpdateStruct->email) ||
1364
            !empty($userUpdateStruct->enabled) ||
1365
            !empty($userUpdateStruct->maxLogin);
1366
    }
1367
1368
    private function getDateTime(?int $timestamp): ?DateTimeInterface
1369
    {
1370
        if ($timestamp !== null) {
1371
            // Instead of using DateTime(ts) we use setTimeStamp() so timezone does not get set to UTC
1372
            $dateTime = new DateTime();
1373
            $dateTime->setTimestamp($timestamp);
1374
1375
            return DateTimeImmutable::createFromMutable($dateTime);
1376
        }
1377
1378
        return null;
1379
    }
1380
}
1381