Completed
Push — 6.13.7 ( b1546d )
by
unknown
14:00
created

UserService::createUser()   B

Complexity

Conditions 7
Paths 63

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 63
nop 2
dl 0
loc 54
rs 8.0703
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\PasswordHashGeneratorInterface;
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\Repository\Values\User\User;
50
use eZ\Publish\Core\Repository\Values\User\UserGroup;
51
use eZ\Publish\Core\Repository\Values\User\UserGroupCreateStruct;
52
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
53
use eZ\Publish\SPI\Persistence\User as SPIUser;
54
use eZ\Publish\SPI\Persistence\User\Handler;
55
use eZ\Publish\SPI\Persistence\User\UserTokenUpdateStruct as SPIUserTokenUpdateStruct;
56
use Psr\Log\LoggerInterface;
57
58
/**
59
 * This service provides methods for managing users and user groups.
60
 *
61
 * @example Examples/user.php
62
 */
63
class UserService implements UserServiceInterface
64
{
65
    /** @var \eZ\Publish\API\Repository\Repository */
66
    protected $repository;
67
68
    /** @var \eZ\Publish\SPI\Persistence\User\Handler */
69
    protected $userHandler;
70
71
    /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */
72
    private $locationHandler;
73
74
    /** @var array */
75
    protected $settings;
76
77
    /** @var \Psr\Log\LoggerInterface|null */
78
    protected $logger;
79
80
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
81
    private $permissionResolver;
82
83
    /** @var \eZ\Publish\Core\Repository\User\PasswordHashGeneratorInterface */
84
    private $passwordHashGenerator;
85
86
    public function setLogger(LoggerInterface $logger = null)
87
    {
88
        $this->logger = $logger;
89
    }
90
91
    /**
92
     * Setups service with reference to repository object that created it & corresponding handler.
93
     *
94
     * @param \eZ\Publish\API\Repository\Repository $repository
95
     * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
96
     * @param \eZ\Publish\SPI\Persistence\Content\Location\Handler $locationHandler
97
     * @param array $settings
98
     */
99
    public function __construct(
100
        RepositoryInterface $repository,
101
        PermissionResolver $permissionResolver,
102
        Handler $userHandler,
103
        LocationHandler $locationHandler,
104
        PasswordHashGeneratorInterface $passwordHashGenerator,
105
        array $settings = []
106
    ) {
107
        $this->repository = $repository;
108
        $this->permissionResolver = $permissionResolver;
109
        $this->userHandler = $userHandler;
110
        $this->locationHandler = $locationHandler;
111
        // Union makes sure default settings are ignored if provided in argument
112
        $this->settings = $settings + [
113
            'defaultUserPlacement' => 12,
114
            'userClassID' => 4, // @todo Rename this settings to swap out "Class" for "Type"
115
            'userGroupClassID' => 3,
116
            'hashType' => $passwordHashGenerator->getHashType(),
117
            'siteName' => 'ez.no',
118
        ];
119
        $this->passwordHashGenerator = $passwordHashGenerator;
120
    }
121
122
    /**
123
     * Creates a new user group using the data provided in the ContentCreateStruct parameter.
124
     *
125
     * In 4.x in the content type parameter in the profile is ignored
126
     * - the content type is determined via configuration and can be set to null.
127
     * The returned version is published.
128
     *
129
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct $userGroupCreateStruct a structure for setting all necessary data to create this user group
130
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $parentGroup
131
     *
132
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
133
     *
134
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
135
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the input structure has invalid data
136
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupCreateStruct is not valid
137
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
138
     */
139
    public function createUserGroup(APIUserGroupCreateStruct $userGroupCreateStruct, APIUserGroup $parentGroup)
140
    {
141
        $contentService = $this->repository->getContentService();
142
        $locationService = $this->repository->getLocationService();
143
        $contentTypeService = $this->repository->getContentTypeService();
144
145
        if ($userGroupCreateStruct->contentType === null) {
146
            $userGroupContentType = $contentTypeService->loadContentType($this->settings['userGroupClassID']);
147
            $userGroupCreateStruct->contentType = $userGroupContentType;
148
        }
149
150
        $loadedParentGroup = $this->loadUserGroup($parentGroup->id);
151
152
        if ($loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
153
            throw new InvalidArgumentException('parentGroup', 'parent user group has no main location');
154
        }
155
156
        $locationCreateStruct = $locationService->newLocationCreateStruct(
157
            $loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId
158
        );
159
160
        $this->repository->beginTransaction();
161
        try {
162
            $contentDraft = $contentService->createContent($userGroupCreateStruct, [$locationCreateStruct]);
163
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
164
            $this->repository->commit();
165
        } catch (Exception $e) {
166
            $this->repository->rollback();
167
            throw $e;
168
        }
169
170
        return $this->buildDomainUserGroupObject($publishedContent);
171
    }
172
173
    /**
174
     * Loads a user group for the given id.
175
     *
176
     * @param mixed $id
177
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
178
     *
179
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
180
     *
181
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
182
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the user group with the given id was not found
183
     */
184
    public function loadUserGroup($id, array $prioritizedLanguages = [])
185
    {
186
        $content = $this->repository->getContentService()->loadContent($id, $prioritizedLanguages);
187
188
        return $this->buildDomainUserGroupObject($content);
189
    }
190
191
    /**
192
     * Loads the sub groups of a user group.
193
     *
194
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
195
     * @param int $offset the start offset for paging
196
     * @param int $limit the number of user groups returned
197
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
198
     *
199
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
200
     *
201
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the user group
202
     */
203
    public function loadSubUserGroups(APIUserGroup $userGroup, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
204
    {
205
        $locationService = $this->repository->getLocationService();
206
207
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
208
        if (!$this->permissionResolver->canUser('content', 'read', $loadedUserGroup)) {
209
            throw new UnauthorizedException('content', 'read');
210
        }
211
212
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
213
            return [];
214
        }
215
216
        $mainGroupLocation = $locationService->loadLocation(
217
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
218
        );
219
220
        $searchResult = $this->searchSubGroups($mainGroupLocation, $offset, $limit);
221
        if ($searchResult->totalCount == 0) {
222
            return [];
223
        }
224
225
        $subUserGroups = [];
226
        foreach ($searchResult->searchHits as $searchHit) {
227
            $subUserGroups[] = $this->buildDomainUserGroupObject(
228
                $this->repository->getContentService()->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...
229
                    $searchHit->valueObject->contentInfo->id,
0 ignored issues
show
Documentation introduced by
The property contentInfo does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

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

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

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

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