Completed
Push — ezp-31079-login-by-email ( 710641...5864d1 )
by
unknown
15:55
created

UserService::checkUserCredentials()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 4
rs 10
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 = $this->getUserFieldDefinition($userCreateStruct->contentType);
418
        if ($userFieldDefinition === null) {
419
            throw new ContentValidationException('the provided Content Type does not contain the ezuser Field Type');
420
        }
421
422
        $this->repository->beginTransaction();
423
        try {
424
            $contentDraft = $contentService->createContent($userCreateStruct, $locationCreateStructs);
425
            // There is no need to create user separately, just load it from SPI
426
            $spiUser = $this->userHandler->load($contentDraft->id);
427
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
428
429
            // User\Handler::create call is currently used to clear cache only
430
            $this->userHandler->create(
431
                new SPIUser(
432
                    [
433
                        'id' => $spiUser->id,
434
                        'login' => $spiUser->login,
435
                        'email' => $spiUser->email,
436
                    ]
437
                )
438
            );
439
440
            $this->repository->commit();
441
        } catch (Exception $e) {
442
            $this->repository->rollback();
443
            throw $e;
444
        }
445
446
        return $this->buildDomainUserObject($spiUser, $publishedContent);
447
    }
448
449
    /**
450
     * Loads a user.
451
     *
452
     * @param mixed $userId
453
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
454
     *
455
     * @return \eZ\Publish\API\Repository\Values\User\User
456
     *
457
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given id was not found
458
     */
459
    public function loadUser($userId, array $prioritizedLanguages = [])
460
    {
461
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
462
        $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...
463
        // Get spiUser value from Field Value
464
        foreach ($content->getFields() as $field) {
465
            if (!$field->value instanceof UserValue) {
466
                continue;
467
            }
468
469
            /** @var \eZ\Publish\Core\FieldType\User\Value $value */
470
            $value = $field->value;
471
            $spiUser = new SPIUser();
472
            $spiUser->id = $value->contentId;
473
            $spiUser->login = $value->login;
474
            $spiUser->email = $value->email;
475
            $spiUser->hashAlgorithm = $value->passwordHashType;
476
            $spiUser->passwordHash = $value->passwordHash;
477
            $spiUser->passwordUpdatedAt = $value->passwordUpdatedAt ? $value->passwordUpdatedAt->getTimestamp() : null;
478
            $spiUser->isEnabled = $value->enabled;
479
            $spiUser->maxLogin = $value->maxLogin;
480
            break;
481
        }
482
483
        // If for some reason not found, load it
484
        if (!isset($spiUser)) {
485
            $spiUser = $this->userHandler->load($userId);
486
        }
487
488
        return $this->buildDomainUserObject($spiUser, $content);
489
    }
490
491
    public function checkUserCredentials(APIUser $user, string $credentials): bool
492
    {
493
        return $this->comparePasswordHashForAPIUser($user, $credentials);
494
    }
495
496
    /**
497
     * Update password hash to the type configured for the service, if they differ.
498
     *
499
     * @param string $login User login
500
     * @param string $password User password
501
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
502
     *
503
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException if the password is not correctly saved, in which case the update is reverted
504
     */
505
    private function updatePasswordHash($login, $password, SPIUser $spiUser)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $login is not used and could be removed.

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

Loading history...
506
    {
507
        $hashType = $this->passwordHashService->getDefaultHashType();
508
        if ($spiUser->hashAlgorithm === $hashType) {
509
            return;
510
        }
511
512
        $spiUser->passwordHash = $this->passwordHashService->createPasswordHash($password, $hashType);
513
        $spiUser->hashAlgorithm = $hashType;
514
515
        $this->repository->beginTransaction();
516
        $this->userHandler->update($spiUser);
517
        $reloadedSpiUser = $this->userHandler->load($spiUser->id);
518
519
        if ($reloadedSpiUser->passwordHash === $spiUser->passwordHash) {
520
            $this->repository->commit();
521
        } else {
522
            // Password hash was not correctly saved, possible cause: EZP-28692
523
            $this->repository->rollback();
524
            if (isset($this->logger)) {
525
                $this->logger->critical('Password hash could not be updated. Please verify that your database schema is up to date.');
526
            }
527
528
            throw new BadStateException(
529
                'user',
530
                'Could not save updated password hash, reverting to previous hash. Please verify that your database schema is up to date.'
531
            );
532
        }
533
    }
534
535
    /**
536
     * Loads a user for the given login.
537
     *
538
     * {@inheritdoc}
539
     *
540
     * @param string $login
541
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
542
     *
543
     * @return \eZ\Publish\API\Repository\Values\User\User
544
     *
545
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
546
     */
547
    public function loadUserByLogin($login, array $prioritizedLanguages = [])
548
    {
549
        if (!is_string($login) || empty($login)) {
550
            throw new InvalidArgumentValue('login', $login);
551
        }
552
553
        $spiUser = $this->userHandler->loadByLogin($login);
554
555
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
556
    }
557
558
    /**
559
     * Loads a user for the given email.
560
     *
561
     * {@inheritdoc}
562
     *
563
     * @param string $email
564
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
565
     *
566
     * @return \eZ\Publish\API\Repository\Values\User\User
567
     *
568
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
569
     */
570
    public function loadUserByEmail(string $email, array $prioritizedLanguages = []): APIUser
571
    {
572
        if (empty($email)) {
573
            throw new InvalidArgumentValue('email', $email);
574
        }
575
576
        $spiUser = $this->userHandler->loadByEmail($email);
577
578
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
579
    }
580
581
    /**
582
     * Loads a user for the given email.
583
     *
584
     * {@inheritdoc}
585
     *
586
     * @param string $email
587
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
588
     *
589
     * @return \eZ\Publish\API\Repository\Values\User\User[]
590
     *
591
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
592
     */
593
    public function loadUsersByEmail(string $email, array $prioritizedLanguages = []): array
594
    {
595
        if (empty($email)) {
596
            throw new InvalidArgumentValue('email', $email);
597
        }
598
599
        $users = [];
600
        foreach ($this->userHandler->loadUsersByEmail($email) as $spiUser) {
601
            $users[] = $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
602
        }
603
604
        return $users;
605
    }
606
607
    /**
608
     * Loads a user for the given token.
609
     *
610
     * {@inheritdoc}
611
     *
612
     * @param string $hash
613
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
614
     *
615
     * @return \eZ\Publish\API\Repository\Values\User\User
616
     *
617
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
618
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
619
     */
620
    public function loadUserByToken($hash, array $prioritizedLanguages = [])
621
    {
622
        if (!is_string($hash) || empty($hash)) {
623
            throw new InvalidArgumentValue('hash', $hash);
624
        }
625
626
        $spiUser = $this->userHandler->loadUserByToken($hash);
627
628
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
629
    }
630
631
    /**
632
     * This method deletes a user.
633
     *
634
     * @param \eZ\Publish\API\Repository\Values\User\User $user
635
     *
636
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to delete the user
637
     */
638
    public function deleteUser(APIUser $user)
639
    {
640
        $loadedUser = $this->loadUser($user->id);
641
642
        $this->repository->beginTransaction();
643
        try {
644
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo());
645
646
            // User\Handler::delete call is currently used to clear cache only
647
            $this->userHandler->delete($loadedUser->id);
648
            $this->repository->commit();
649
        } catch (Exception $e) {
650
            $this->repository->rollback();
651
            throw $e;
652
        }
653
654
        return $affectedLocationIds;
655
    }
656
657
    /**
658
     * Updates a user.
659
     *
660
     * 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
661
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
662
     *
663
     * @param \eZ\Publish\API\Repository\Values\User\User $user
664
     * @param \eZ\Publish\API\Repository\Values\User\UserUpdateStruct $userUpdateStruct
665
     *
666
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userUpdateStruct is not valid
667
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
668
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user
669
     *
670
     * @return \eZ\Publish\API\Repository\Values\User\User
671
     */
672
    public function updateUser(APIUser $user, UserUpdateStruct $userUpdateStruct)
673
    {
674
        $loadedUser = $this->loadUser($user->id);
675
676
        $contentService = $this->repository->getContentService();
677
678
        $canEditContent = $this->permissionResolver->canUser('content', 'edit', $loadedUser);
679
680
        if (!$canEditContent && $this->isUserProfileUpdateRequested($userUpdateStruct)) {
681
            throw new UnauthorizedException('content', 'edit');
682
        }
683
684
        $userFieldDefinition = null;
685
        foreach ($loadedUser->getContentType()->fieldDefinitions as $fieldDefinition) {
686
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
687
                $userFieldDefinition = $fieldDefinition;
688
                break;
689
            }
690
        }
691
692
        if ($userFieldDefinition === null) {
693
            throw new ContentValidationException('The provided Content Type does not contain the ezuser Field Type');
694
        }
695
696
        $userUpdateStruct->contentUpdateStruct = $userUpdateStruct->contentUpdateStruct ?? $contentService->newContentUpdateStruct();
697
698
        $providedUserUpdateDataInField = false;
699
        foreach ($userUpdateStruct->contentUpdateStruct->fields as $field) {
700
            if ($field->value instanceof UserValue) {
701
                $providedUserUpdateDataInField = true;
702
                break;
703
            }
704
        }
705
706
        if (!$providedUserUpdateDataInField) {
707
            $userUpdateStruct->contentUpdateStruct->setField(
708
                $userFieldDefinition->identifier,
709
                new UserValue([
710
                    'contentId' => $loadedUser->id,
711
                    'hasStoredLogin' => true,
712
                    'login' => $loadedUser->login,
713
                    'email' => $userUpdateStruct->email ?? $loadedUser->email,
714
                    'plainPassword' => $userUpdateStruct->password,
715
                    'enabled' => $userUpdateStruct->enabled ?? $loadedUser->enabled,
716
                    'maxLogin' => $userUpdateStruct->maxLogin ?? $loadedUser->maxLogin,
717
                    'passwordHashType' => $user->hashAlgorithm,
718
                    'passwordHash' => $user->passwordHash,
719
                ])
720
            );
721
        }
722
723
        if (!empty($userUpdateStruct->password) &&
724
            !$canEditContent &&
725
            !$this->permissionResolver->canUser('user', 'password', $loadedUser)
726
        ) {
727
            throw new UnauthorizedException('user', 'password');
728
        }
729
730
        $this->repository->beginTransaction();
731
        try {
732
            $publishedContent = $loadedUser;
733
            if ($userUpdateStruct->contentUpdateStruct !== null) {
734
                $contentDraft = $contentService->createContentDraft($loadedUser->getVersionInfo()->getContentInfo());
735
                $contentDraft = $contentService->updateContent(
736
                    $contentDraft->getVersionInfo(),
737
                    $userUpdateStruct->contentUpdateStruct
738
                );
739
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
740
            }
741
742
            if ($userUpdateStruct->contentMetadataUpdateStruct !== null) {
743
                $contentService->updateContentMetadata(
744
                    $publishedContent->getVersionInfo()->getContentInfo(),
745
                    $userUpdateStruct->contentMetadataUpdateStruct
746
                );
747
            }
748
749
            // User\Handler::update call is currently used to clear cache only
750
            $this->userHandler->update(
751
                new SPIUser(
752
                    [
753
                        'id' => $loadedUser->id,
754
                        'login' => $loadedUser->login,
755
                        'email' => $userUpdateStruct->email ?: $loadedUser->email,
756
                    ]
757
                )
758
            );
759
760
            $this->repository->commit();
761
        } catch (Exception $e) {
762
            $this->repository->rollback();
763
            throw $e;
764
        }
765
766
        return $this->loadUser($loadedUser->id);
767
    }
768
769
    /**
770
     * Update the user token information specified by the user token struct.
771
     *
772
     * @param \eZ\Publish\API\Repository\Values\User\User $user
773
     * @param \eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct $userTokenUpdateStruct
774
     *
775
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
776
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
777
     * @throws \RuntimeException
778
     * @throws \Exception
779
     *
780
     * @return \eZ\Publish\API\Repository\Values\User\User
781
     */
782
    public function updateUserToken(APIUser $user, UserTokenUpdateStruct $userTokenUpdateStruct)
783
    {
784
        $loadedUser = $this->loadUser($user->id);
785
786
        if ($userTokenUpdateStruct->hashKey !== null && (!is_string($userTokenUpdateStruct->hashKey) || empty($userTokenUpdateStruct->hashKey))) {
787
            throw new InvalidArgumentValue('hashKey', $userTokenUpdateStruct->hashKey, 'UserTokenUpdateStruct');
788
        }
789
790
        if ($userTokenUpdateStruct->time === null) {
791
            throw new InvalidArgumentValue('time', $userTokenUpdateStruct->time, 'UserTokenUpdateStruct');
792
        }
793
794
        $this->repository->beginTransaction();
795
        try {
796
            $this->userHandler->updateUserToken(
797
                new SPIUserTokenUpdateStruct(
798
                    [
799
                        'userId' => $loadedUser->id,
800
                        'hashKey' => $userTokenUpdateStruct->hashKey,
801
                        'time' => $userTokenUpdateStruct->time->getTimestamp(),
802
                    ]
803
                )
804
            );
805
            $this->repository->commit();
806
        } catch (Exception $e) {
807
            $this->repository->rollback();
808
            throw $e;
809
        }
810
811
        return $this->loadUser($loadedUser->id);
812
    }
813
814
    /**
815
     * Expires user token with user hash.
816
     *
817
     * @param string $hash
818
     */
819
    public function expireUserToken($hash)
820
    {
821
        $this->repository->beginTransaction();
822
        try {
823
            $this->userHandler->expireUserToken($hash);
824
            $this->repository->commit();
825
        } catch (Exception $e) {
826
            $this->repository->rollback();
827
            throw $e;
828
        }
829
    }
830
831
    /**
832
     * Assigns a new user group to the user.
833
     *
834
     * @param \eZ\Publish\API\Repository\Values\User\User $user
835
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
836
     *
837
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to assign the user group to the user
838
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is already in the given user group
839
     */
840
    public function assignUserToUserGroup(APIUser $user, APIUserGroup $userGroup)
841
    {
842
        $loadedUser = $this->loadUser($user->id);
843
        $loadedGroup = $this->loadUserGroup($userGroup->id);
844
        $locationService = $this->repository->getLocationService();
845
846
        $existingGroupIds = [];
847
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
848
        foreach ($userLocations as $userLocation) {
849
            $existingGroupIds[] = $userLocation->parentLocationId;
850
        }
851
852
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
853
            throw new BadStateException('userGroup', 'User Group has no main Location or no Locations');
854
        }
855
856
        if (in_array($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId, $existingGroupIds)) {
857
            // user is already assigned to the user group
858
            throw new InvalidArgumentException('user', 'User is already in the given User Group');
859
        }
860
861
        $locationCreateStruct = $locationService->newLocationCreateStruct(
862
            $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId
863
        );
864
865
        $this->repository->beginTransaction();
866
        try {
867
            $locationService->createLocation(
868
                $loadedUser->getVersionInfo()->getContentInfo(),
869
                $locationCreateStruct
870
            );
871
            $this->repository->commit();
872
        } catch (Exception $e) {
873
            $this->repository->rollback();
874
            throw $e;
875
        }
876
    }
877
878
    /**
879
     * Removes a user group from the user.
880
     *
881
     * @param \eZ\Publish\API\Repository\Values\User\User $user
882
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
883
     *
884
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to remove the user group from the user
885
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is not in the given user group
886
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $userGroup is the last assigned user group
887
     */
888
    public function unAssignUserFromUserGroup(APIUser $user, APIUserGroup $userGroup)
889
    {
890
        $loadedUser = $this->loadUser($user->id);
891
        $loadedGroup = $this->loadUserGroup($userGroup->id);
892
        $locationService = $this->repository->getLocationService();
893
894
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
895
        if (empty($userLocations)) {
896
            throw new BadStateException('user', 'User has no Locations, cannot unassign from group');
897
        }
898
899
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
900
            throw new BadStateException('userGroup', 'User Group has no main Location or no Locations, cannot unassign');
901
        }
902
903
        foreach ($userLocations as $userLocation) {
904
            if ($userLocation->parentLocationId == $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId) {
905
                // Throw this specific BadState when we know argument is valid
906
                if (count($userLocations) === 1) {
907
                    throw new BadStateException('user', 'User only has one User Group, cannot unassign from last group');
908
                }
909
910
                $this->repository->beginTransaction();
911
                try {
912
                    $locationService->deleteLocation($userLocation);
913
                    $this->repository->commit();
914
915
                    return;
916
                } catch (Exception $e) {
917
                    $this->repository->rollback();
918
                    throw $e;
919
                }
920
            }
921
        }
922
923
        throw new InvalidArgumentException('userGroup', 'User is not in the given User Group');
924
    }
925
926
    /**
927
     * Loads the user groups the user belongs to.
928
     *
929
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed read the user or user group
930
     *
931
     * @param \eZ\Publish\API\Repository\Values\User\User $user
932
     * @param int $offset the start offset for paging
933
     * @param int $limit the number of user groups returned
934
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
935
     *
936
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
937
     */
938
    public function loadUserGroupsOfUser(APIUser $user, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
939
    {
940
        $locationService = $this->repository->getLocationService();
941
942
        if (!$this->repository->getPermissionResolver()->canUser('content', 'read', $user)) {
943
            throw new UnauthorizedException('content', 'read');
944
        }
945
946
        $userLocations = $locationService->loadLocations(
947
            $user->getVersionInfo()->getContentInfo()
948
        );
949
950
        $parentLocationIds = [];
951
        foreach ($userLocations as $userLocation) {
952
            if ($userLocation->parentLocationId !== null) {
953
                $parentLocationIds[] = $userLocation->parentLocationId;
954
            }
955
        }
956
957
        $searchQuery = new LocationQuery();
958
959
        $searchQuery->offset = $offset;
960
        $searchQuery->limit = $limit;
961
        $searchQuery->performCount = false;
962
963
        $searchQuery->filter = new CriterionLogicalAnd(
964
            [
965
                new CriterionContentTypeId($this->settings['userGroupClassID']),
966
                new CriterionLocationId($parentLocationIds),
967
            ]
968
        );
969
970
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
971
972
        $userGroups = [];
973
        foreach ($searchResult->searchHits as $resultItem) {
974
            $userGroups[] = $this->buildDomainUserGroupObject(
975
                $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...
976
                    $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...
977
                    $prioritizedLanguages
978
                )
979
            );
980
        }
981
982
        return $userGroups;
983
    }
984
985
    /**
986
     * Loads the users of a user group.
987
     *
988
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the users or user group
989
     *
990
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
991
     * @param int $offset the start offset for paging
992
     * @param int $limit the number of users returned
993
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
994
     *
995
     * @return \eZ\Publish\API\Repository\Values\User\User[]
996
     */
997
    public function loadUsersOfUserGroup(
998
        APIUserGroup $userGroup,
999
        $offset = 0,
1000
        $limit = 25,
1001
        array $prioritizedLanguages = []
1002
    ) {
1003
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
1004
1005
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1006
            return [];
1007
        }
1008
1009
        $mainGroupLocation = $this->repository->getLocationService()->loadLocation(
1010
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
1011
        );
1012
1013
        $searchQuery = new LocationQuery();
1014
1015
        $searchQuery->filter = new CriterionLogicalAnd(
1016
            [
1017
                new CriterionContentTypeId($this->settings['userClassID']),
1018
                new CriterionParentLocationId($mainGroupLocation->id),
1019
            ]
1020
        );
1021
1022
        $searchQuery->offset = $offset;
1023
        $searchQuery->limit = $limit;
1024
        $searchQuery->performCount = false;
1025
        $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...
1026
1027
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1028
1029
        $users = [];
1030
        foreach ($searchResult->searchHits as $resultItem) {
1031
            $users[] = $this->buildDomainUserObject(
1032
                $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...
1033
                $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...
1034
                    $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...
1035
                    $prioritizedLanguages
1036
                )
1037
            );
1038
        }
1039
1040
        return $users;
1041
    }
1042
1043
    /**
1044
     * {@inheritdoc}
1045
     */
1046
    public function isUser(APIContent $content): bool
1047
    {
1048
        // First check against config for fast check
1049
        if ($this->settings['userClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId) {
1050
            return true;
1051
        }
1052
1053
        // For users we ultimately need to look for ezuser type as content type id could be several for users.
1054
        // And config might be different from one SA to the next, which we don't care about here.
1055
        foreach ($content->getFields() as $field) {
1056
            if ($field->fieldTypeIdentifier === 'ezuser') {
1057
                return true;
1058
            }
1059
        }
1060
1061
        return false;
1062
    }
1063
1064
    /**
1065
     * {@inheritdoc}
1066
     */
1067
    public function isUserGroup(APIContent $content): bool
1068
    {
1069
        return $this->settings['userGroupClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId;
1070
    }
1071
1072
    /**
1073
     * Instantiate a user create class.
1074
     *
1075
     * @param string $login the login of the new user
1076
     * @param string $email the email of the new user
1077
     * @param string $password the plain password of the new user
1078
     * @param string $mainLanguageCode the main language for the underlying content object
1079
     * @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
1080
     *
1081
     * @return \eZ\Publish\API\Repository\Values\User\UserCreateStruct
1082
     */
1083
    public function newUserCreateStruct($login, $email, $password, $mainLanguageCode, $contentType = null)
1084
    {
1085
        if ($contentType === null) {
1086
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1087
                $this->settings['userClassID']
1088
            );
1089
        }
1090
1091
        $fieldDefIdentifier = '';
1092
        foreach ($contentType->fieldDefinitions as $fieldDefinition) {
1093
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
1094
                $fieldDefIdentifier = $fieldDefinition->identifier;
1095
                break;
1096
            }
1097
        }
1098
1099
        return new UserCreateStruct(
1100
            [
1101
                'contentType' => $contentType,
1102
                'mainLanguageCode' => $mainLanguageCode,
1103
                'login' => $login,
1104
                'email' => $email,
1105
                'password' => $password,
1106
                'enabled' => true,
1107
                'fields' => [
1108
                    new Field([
1109
                        'fieldDefIdentifier' => $fieldDefIdentifier,
1110
                        'languageCode' => $mainLanguageCode,
1111
                        'fieldTypeIdentifier' => 'ezuser',
1112
                        'value' => new UserValue([
1113
                            'login' => $login,
1114
                            'email' => $email,
1115
                            'plainPassword' => $password,
1116
                            'enabled' => true,
1117
                            'passwordUpdatedAt' => new DateTime(),
1118
                        ]),
1119
                    ]),
1120
                ],
1121
            ]
1122
        );
1123
    }
1124
1125
    /**
1126
     * Instantiate a user group create class.
1127
     *
1128
     * @param string $mainLanguageCode The main language for the underlying content object
1129
     * @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
1130
     *
1131
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct
1132
     */
1133
    public function newUserGroupCreateStruct($mainLanguageCode, $contentType = null)
1134
    {
1135
        if ($contentType === null) {
1136
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1137
                $this->settings['userGroupClassID']
1138
            );
1139
        }
1140
1141
        return new UserGroupCreateStruct(
1142
            [
1143
                'contentType' => $contentType,
1144
                'mainLanguageCode' => $mainLanguageCode,
1145
                'fields' => [],
1146
            ]
1147
        );
1148
    }
1149
1150
    /**
1151
     * Instantiate a new user update struct.
1152
     *
1153
     * @return \eZ\Publish\API\Repository\Values\User\UserUpdateStruct
1154
     */
1155
    public function newUserUpdateStruct()
1156
    {
1157
        return new UserUpdateStruct();
1158
    }
1159
1160
    /**
1161
     * Instantiate a new user group update struct.
1162
     *
1163
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
1164
     */
1165
    public function newUserGroupUpdateStruct()
1166
    {
1167
        return new UserGroupUpdateStruct();
1168
    }
1169
1170
    /**
1171
     * {@inheritdoc}
1172
     */
1173
    public function validatePassword(string $password, PasswordValidationContext $context = null): array
1174
    {
1175
        $errors = [];
1176
1177
        if ($context === null) {
1178
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1179
                $this->settings['userClassID']
1180
            );
1181
1182
            $context = new PasswordValidationContext([
1183
                'contentType' => $contentType,
1184
            ]);
1185
        }
1186
1187
        // Search for the first ezuser field type in content type
1188
        $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...
1189
        if ($userFieldDefinition === null) {
1190
            throw new ContentValidationException('The provided Content Type does not contain the ezuser Field Type');
1191
        }
1192
1193
        $configuration = $userFieldDefinition->getValidatorConfiguration();
1194
        if (isset($configuration['PasswordValueValidator'])) {
1195
            $errors = (new UserPasswordValidator($configuration['PasswordValueValidator']))->validate($password);
1196
        }
1197
1198
        if ($context->user !== null) {
1199
            $isPasswordTTLEnabled = $this->getPasswordInfo($context->user)->hasExpirationDate();
1200
            $isNewPasswordRequired = $configuration['PasswordValueValidator']['requireNewPassword'] ?? false;
1201
1202
            if (($isPasswordTTLEnabled || $isNewPasswordRequired) &&
1203
                $this->comparePasswordHashForAPIUser($context->user, $password)
1204
            ) {
1205
                $errors[] = new ValidationError('New password cannot be the same as old password', null, [], 'password');
1206
            }
1207
        }
1208
1209
        return $errors;
1210
    }
1211
1212
    /**
1213
     * Builds the domain UserGroup object from provided Content object.
1214
     *
1215
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1216
     *
1217
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
1218
     */
1219
    protected function buildDomainUserGroupObject(APIContent $content)
1220
    {
1221
        $locationService = $this->repository->getLocationService();
1222
1223
        if ($content->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
1224
            $mainLocation = $locationService->loadLocation(
1225
                $content->getVersionInfo()->getContentInfo()->mainLocationId
1226
            );
1227
            $parentLocation = $this->locationHandler->load($mainLocation->parentLocationId);
1228
        }
1229
1230
        return new UserGroup(
1231
            [
1232
                'content' => $content,
1233
                'parentId' => $parentLocation->contentId ?? null,
1234
            ]
1235
        );
1236
    }
1237
1238
    /**
1239
     * Builds the domain user object from provided persistence user object.
1240
     *
1241
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
1242
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
1243
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1244
     *
1245
     * @return \eZ\Publish\API\Repository\Values\User\User
1246
     */
1247
    protected function buildDomainUserObject(
1248
        SPIUser $spiUser,
1249
        APIContent $content = null,
1250
        array $prioritizedLanguages = []
1251
    ) {
1252
        if ($content === null) {
1253
            $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...
1254
                $spiUser->id,
1255
                $prioritizedLanguages
1256
            );
1257
        }
1258
1259
        return new User(
1260
            [
1261
                'content' => $content,
1262
                'login' => $spiUser->login,
1263
                'email' => $spiUser->email,
1264
                'passwordHash' => $spiUser->passwordHash,
1265
                'passwordUpdatedAt' => $this->getDateTime($spiUser->passwordUpdatedAt),
1266
                'hashAlgorithm' => (int)$spiUser->hashAlgorithm,
1267
                'enabled' => $spiUser->isEnabled,
1268
                'maxLogin' => (int)$spiUser->maxLogin,
1269
            ]
1270
        );
1271
    }
1272
1273
    public function getPasswordInfo(APIUser $user): PasswordInfo
1274
    {
1275
        $passwordUpdatedAt = $user->passwordUpdatedAt;
1276
        if ($passwordUpdatedAt === null) {
1277
            return new PasswordInfo();
1278
        }
1279
1280
        $definition = $this->getUserFieldDefinition($user->getContentType());
1281
        if ($definition === null) {
1282
            return new PasswordInfo();
1283
        }
1284
1285
        $expirationDate = null;
1286
        $expirationWarningDate = null;
1287
1288
        $passwordTTL = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_SETTING];
1289
        if ($passwordTTL > 0) {
1290
            if ($passwordUpdatedAt instanceof DateTime) {
1291
                $passwordUpdatedAt = DateTimeImmutable::createFromMutable($passwordUpdatedAt);
1292
            }
1293
1294
            $expirationDate = $passwordUpdatedAt->add(new DateInterval(sprintf('P%dD', $passwordTTL)));
1295
1296
            $passwordTTLWarning = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_WARNING_SETTING];
1297
            if ($passwordTTLWarning > 0) {
1298
                $expirationWarningDate = $expirationDate->sub(new DateInterval(sprintf('P%dD', $passwordTTLWarning)));
1299
            }
1300
        }
1301
1302
        return new PasswordInfo($expirationDate, $expirationWarningDate);
1303
    }
1304
1305
    private function getUserFieldDefinition(ContentType $contentType): ?FieldDefinition
1306
    {
1307
        return $contentType->getFirstFieldDefinitionOfType('ezuser');
1308
    }
1309
1310
    /**
1311
     * Verifies if the provided login and password are valid for eZ\Publish\SPI\Persistence\User.
1312
     *
1313
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1314
     */
1315
    protected function comparePasswordHashForSPIUser(SPIUser $user, string $password): bool
1316
    {
1317
        return $this->comparePasswordHashes($password, $user->passwordHash, $user->hashAlgorithm);
1318
    }
1319
1320
    /**
1321
     * Verifies if the provided login and password are valid for eZ\Publish\API\Repository\Values\User\User.
1322
     *
1323
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1324
     */
1325
    protected function comparePasswordHashForAPIUser(APIUser $user, string $password): bool
1326
    {
1327
        return $this->comparePasswordHashes($password, $user->passwordHash, $user->hashAlgorithm);
1328
    }
1329
1330
    /**
1331
     * Verifies if the provided login and password are valid against given password hash and hash type.
1332
     *
1333
     * @param string $plainPassword User password
1334
     * @param string $passwordHash User password hash
1335
     * @param int $hashAlgorithm Hash type
1336
     *
1337
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1338
     */
1339
    private function comparePasswordHashes(
1340
        string $plainPassword,
1341
        string $passwordHash,
1342
        int $hashAlgorithm
1343
    ): bool {
1344
        return $this->passwordHashService->isValidPassword($plainPassword, $passwordHash, $hashAlgorithm);
1345
    }
1346
1347
    /**
1348
     * Return true if any of the UserUpdateStruct properties refers to User Profile (Content) update.
1349
     *
1350
     * @param UserUpdateStruct $userUpdateStruct
1351
     *
1352
     * @return bool
1353
     */
1354
    private function isUserProfileUpdateRequested(UserUpdateStruct $userUpdateStruct)
1355
    {
1356
        return
1357
            !empty($userUpdateStruct->contentUpdateStruct) ||
1358
            !empty($userUpdateStruct->contentMetadataUpdateStruct) ||
1359
            !empty($userUpdateStruct->email) ||
1360
            !empty($userUpdateStruct->enabled) ||
1361
            !empty($userUpdateStruct->maxLogin);
1362
    }
1363
1364
    private function getDateTime(?int $timestamp): ?DateTimeInterface
1365
    {
1366
        if ($timestamp !== null) {
1367
            // Instead of using DateTime(ts) we use setTimeStamp() so timezone does not get set to UTC
1368
            $dateTime = new DateTime();
1369
            $dateTime->setTimestamp($timestamp);
1370
1371
            return DateTimeImmutable::createFromMutable($dateTime);
1372
        }
1373
1374
        return null;
1375
    }
1376
}
1377