Completed
Push — ezp_30973 ( feb262...9f83f6 )
by
unknown
14:39
created

UserService::assignUserToUserGroup()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

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