Completed
Push — ezp_30797 ( b39740...a92ef0 )
by
unknown
17:49
created

UserService::getPasswordInfo()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 7
nop 1
dl 0
loc 31
rs 8.8017
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\Repository as RepositoryInterface;
17
use eZ\Publish\API\Repository\UserService as UserServiceInterface;
18
use eZ\Publish\API\Repository\Values\Content\Content as APIContent;
19
use eZ\Publish\API\Repository\Values\Content\Location;
20
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
21
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\ContentTypeId as CriterionContentTypeId;
22
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LocationId as CriterionLocationId;
23
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd as CriterionLogicalAnd;
24
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\ParentLocationId as CriterionParentLocationId;
25
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
26
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
27
use eZ\Publish\API\Repository\Values\User\PasswordInfo;
28
use eZ\Publish\API\Repository\Values\User\PasswordValidationContext;
29
use eZ\Publish\API\Repository\Values\User\User as APIUser;
30
use eZ\Publish\API\Repository\Values\User\UserCreateStruct as APIUserCreateStruct;
31
use eZ\Publish\API\Repository\Values\User\UserGroup as APIUserGroup;
32
use eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct as APIUserGroupCreateStruct;
33
use eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct;
34
use eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct;
35
use eZ\Publish\API\Repository\Values\User\UserUpdateStruct;
36
use eZ\Publish\Core\Base\Exceptions\BadStateException;
37
use eZ\Publish\Core\Base\Exceptions\ContentValidationException;
38
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
39
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue;
40
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
41
use eZ\Publish\Core\Base\Exceptions\UnauthorizedException;
42
use eZ\Publish\Core\Base\Exceptions\UserPasswordValidationException;
43
use eZ\Publish\Core\FieldType\User\Value as UserValue;
44
use eZ\Publish\Core\FieldType\User\Type as UserType;
45
use eZ\Publish\Core\Repository\Validator\UserPasswordValidator;
46
use eZ\Publish\Core\Repository\Values\User\User;
47
use eZ\Publish\Core\Repository\Values\User\UserCreateStruct;
48
use eZ\Publish\Core\Repository\Values\User\UserGroup;
49
use eZ\Publish\Core\Repository\Values\User\UserGroupCreateStruct;
50
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
51
use eZ\Publish\SPI\Persistence\User as SPIUser;
52
use eZ\Publish\SPI\Persistence\User\Handler;
53
use eZ\Publish\SPI\Persistence\User\UserTokenUpdateStruct as SPIUserTokenUpdateStruct;
54
use Psr\Log\LoggerInterface;
55
56
/**
57
 * This service provides methods for managing users and user groups.
58
 *
59
 * @example Examples/user.php
60
 */
61
class UserService implements UserServiceInterface
62
{
63
    /** @var \eZ\Publish\API\Repository\Repository */
64
    protected $repository;
65
66
    /** @var \eZ\Publish\SPI\Persistence\User\Handler */
67
    protected $userHandler;
68
69
    /** @var \eZ\Publish\SPI\Persistence\Content\Location\Handler */
70
    private $locationHandler;
71
72
    /** @var array */
73
    protected $settings;
74
75
    /** @var \Psr\Log\LoggerInterface|null */
76
    protected $logger;
77
78
    /** @var \eZ\Publish\API\Repository\PermissionResolver */
79
    private $permissionResolver;
80
81
    public function setLogger(LoggerInterface $logger = null)
82
    {
83
        $this->logger = $logger;
84
    }
85
86
    /**
87
     * Setups service with reference to repository object that created it & corresponding handler.
88
     *
89
     * @param \eZ\Publish\API\Repository\Repository $repository
90
     * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
91
     * @param \eZ\Publish\SPI\Persistence\Content\Location\Handler $locationHandler
92
     * @param array $settings
93
     */
94
    public function __construct(
95
        RepositoryInterface $repository,
96
        Handler $userHandler,
97
        LocationHandler $locationHandler,
98
        array $settings = []
99
    ) {
100
        $this->repository = $repository;
101
        $this->permissionResolver = $repository->getPermissionResolver();
102
        $this->userHandler = $userHandler;
103
        $this->locationHandler = $locationHandler;
104
        // Union makes sure default settings are ignored if provided in argument
105
        $this->settings = $settings + [
106
            'defaultUserPlacement' => 12,
107
            'userClassID' => 4, // @todo Rename this settings to swap out "Class" for "Type"
108
            'userGroupClassID' => 3,
109
            'hashType' => APIUser::DEFAULT_PASSWORD_HASH,
110
            'siteName' => 'ez.no',
111
        ];
112
    }
113
114
    /**
115
     * Creates a new user group using the data provided in the ContentCreateStruct parameter.
116
     *
117
     * In 4.x in the content type parameter in the profile is ignored
118
     * - the content type is determined via configuration and can be set to null.
119
     * The returned version is published.
120
     *
121
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct $userGroupCreateStruct a structure for setting all necessary data to create this user group
122
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $parentGroup
123
     *
124
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
125
     *
126
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
127
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the input structure has invalid data
128
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupCreateStruct is not valid
129
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
130
     */
131
    public function createUserGroup(APIUserGroupCreateStruct $userGroupCreateStruct, APIUserGroup $parentGroup)
132
    {
133
        $contentService = $this->repository->getContentService();
134
        $locationService = $this->repository->getLocationService();
135
        $contentTypeService = $this->repository->getContentTypeService();
136
137
        if ($userGroupCreateStruct->contentType === null) {
138
            $userGroupContentType = $contentTypeService->loadContentType($this->settings['userGroupClassID']);
139
            $userGroupCreateStruct->contentType = $userGroupContentType;
140
        }
141
142
        $loadedParentGroup = $this->loadUserGroup($parentGroup->id);
143
144
        if ($loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
145
            throw new InvalidArgumentException('parentGroup', 'parent user group has no main location');
146
        }
147
148
        $locationCreateStruct = $locationService->newLocationCreateStruct(
149
            $loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId
150
        );
151
152
        $this->repository->beginTransaction();
153
        try {
154
            $contentDraft = $contentService->createContent($userGroupCreateStruct, [$locationCreateStruct]);
155
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
156
            $this->repository->commit();
157
        } catch (Exception $e) {
158
            $this->repository->rollback();
159
            throw $e;
160
        }
161
162
        return $this->buildDomainUserGroupObject($publishedContent);
163
    }
164
165
    /**
166
     * Loads a user group for the given id.
167
     *
168
     * @param mixed $id
169
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
170
     *
171
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
172
     *
173
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
174
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the user group with the given id was not found
175
     */
176
    public function loadUserGroup($id, array $prioritizedLanguages = [])
177
    {
178
        $content = $this->repository->getContentService()->loadContent($id, $prioritizedLanguages);
179
180
        return $this->buildDomainUserGroupObject($content);
181
    }
182
183
    /**
184
     * Loads the sub groups of a user group.
185
     *
186
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
187
     * @param int $offset the start offset for paging
188
     * @param int $limit the number of user groups returned
189
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
190
     *
191
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
192
     *
193
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the user group
194
     */
195
    public function loadSubUserGroups(APIUserGroup $userGroup, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
196
    {
197
        $locationService = $this->repository->getLocationService();
198
199
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
200
        if (!$this->repository->canUser('content', 'read', $loadedUserGroup)) {
0 ignored issues
show
Deprecated Code introduced by
The method eZ\Publish\API\Repository\Repository::canUser() has been deprecated with message: since 6.6, to be removed. Use PermissionResolver::canUser() instead. Indicates if the current user is allowed to perform an action given by the function on the given
objects. Example: canUser( 'content', 'edit', $content, $location ); This will check edit permission on content given the specific location, if skipped if will check on all locations. Example2: canUser( 'section', 'assign', $content, $section ); Check if user has access to assign $content to $section.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
201
            throw new UnauthorizedException('content', 'read');
202
        }
203
204
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
205
            return [];
206
        }
207
208
        $mainGroupLocation = $locationService->loadLocation(
209
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
210
        );
211
212
        $searchResult = $this->searchSubGroups($mainGroupLocation, $offset, $limit);
213
        if ($searchResult->totalCount == 0) {
214
            return [];
215
        }
216
217
        $subUserGroups = [];
218
        foreach ($searchResult->searchHits as $searchHit) {
219
            $subUserGroups[] = $this->buildDomainUserGroupObject(
220
                $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...
221
                    $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...
222
                    $prioritizedLanguages
223
                )
224
            );
225
        }
226
227
        return $subUserGroups;
228
    }
229
230
    /**
231
     * Returns (searches) subgroups of a user group described by its main location.
232
     *
233
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
234
     * @param int $offset
235
     * @param int $limit
236
     *
237
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
238
     */
239
    protected function searchSubGroups(Location $location, $offset = 0, $limit = 25)
240
    {
241
        $searchQuery = new LocationQuery();
242
243
        $searchQuery->offset = $offset;
244
        $searchQuery->limit = $limit;
245
246
        $searchQuery->filter = new CriterionLogicalAnd([
247
            new CriterionContentTypeId($this->settings['userGroupClassID']),
248
            new CriterionParentLocationId($location->id),
249
        ]);
250
251
        $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...
252
253
        return $this->repository->getSearchService()->findLocations($searchQuery, [], false);
254
    }
255
256
    /**
257
     * Removes a user group.
258
     *
259
     * the users which are not assigned to other groups will be deleted.
260
     *
261
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
262
     *
263
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
264
     */
265
    public function deleteUserGroup(APIUserGroup $userGroup)
266
    {
267
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
268
269
        $this->repository->beginTransaction();
270
        try {
271
            //@todo: what happens to sub user groups and users below sub user groups
272
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUserGroup->getVersionInfo()->getContentInfo());
273
            $this->repository->commit();
274
        } catch (Exception $e) {
275
            $this->repository->rollback();
276
            throw $e;
277
        }
278
279
        return $affectedLocationIds;
280
    }
281
282
    /**
283
     * Moves the user group to another parent.
284
     *
285
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
286
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $newParent
287
     *
288
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
289
     */
290
    public function moveUserGroup(APIUserGroup $userGroup, APIUserGroup $newParent)
291
    {
292
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
293
        $loadedNewParent = $this->loadUserGroup($newParent->id);
294
295
        $locationService = $this->repository->getLocationService();
296
297
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
298
            throw new BadStateException('userGroup', 'existing user group is not stored and/or does not have any location yet');
299
        }
300
301
        if ($loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId === null) {
302
            throw new BadStateException('newParent', 'new user group is not stored and/or does not have any location yet');
303
        }
304
305
        $userGroupMainLocation = $locationService->loadLocation(
306
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
307
        );
308
        $newParentMainLocation = $locationService->loadLocation(
309
            $loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId
310
        );
311
312
        $this->repository->beginTransaction();
313
        try {
314
            $locationService->moveSubtree($userGroupMainLocation, $newParentMainLocation);
315
            $this->repository->commit();
316
        } catch (Exception $e) {
317
            $this->repository->rollback();
318
            throw $e;
319
        }
320
    }
321
322
    /**
323
     * Updates the group profile with fields and meta data.
324
     *
325
     * 4.x: If the versionUpdateStruct is set in $userGroupUpdateStruct, this method internally creates a content draft, updates ts with the provided data
326
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
327
     *
328
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
329
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct $userGroupUpdateStruct
330
     *
331
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
332
     *
333
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user group
334
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupUpdateStruct is not valid
335
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
336
     */
337
    public function updateUserGroup(APIUserGroup $userGroup, UserGroupUpdateStruct $userGroupUpdateStruct)
338
    {
339
        if ($userGroupUpdateStruct->contentUpdateStruct === null &&
340
            $userGroupUpdateStruct->contentMetadataUpdateStruct === null) {
341
            // both update structs are empty, nothing to do
342
            return $userGroup;
343
        }
344
345
        $contentService = $this->repository->getContentService();
346
347
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
348
349
        $this->repository->beginTransaction();
350
        try {
351
            $publishedContent = $loadedUserGroup;
352
            if ($userGroupUpdateStruct->contentUpdateStruct !== null) {
353
                $contentDraft = $contentService->createContentDraft($loadedUserGroup->getVersionInfo()->getContentInfo());
354
355
                $contentDraft = $contentService->updateContent(
356
                    $contentDraft->getVersionInfo(),
357
                    $userGroupUpdateStruct->contentUpdateStruct
358
                );
359
360
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
361
            }
362
363
            if ($userGroupUpdateStruct->contentMetadataUpdateStruct !== null) {
364
                $publishedContent = $contentService->updateContentMetadata(
365
                    $publishedContent->getVersionInfo()->getContentInfo(),
366
                    $userGroupUpdateStruct->contentMetadataUpdateStruct
367
                );
368
            }
369
370
            $this->repository->commit();
371
        } catch (Exception $e) {
372
            $this->repository->rollback();
373
            throw $e;
374
        }
375
376
        return $this->buildDomainUserGroupObject($publishedContent);
377
    }
378
379
    /**
380
     * Create a new user. The created user is published by this method.
381
     *
382
     * @param \eZ\Publish\API\Repository\Values\User\UserCreateStruct $userCreateStruct the data used for creating the user
383
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup[] $parentGroups the groups which are assigned to the user after creation
384
     *
385
     * @return \eZ\Publish\API\Repository\Values\User\User
386
     *
387
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
388
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userCreateStruct is not valid
389
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
390
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a user with provided login already exists
391
     */
392
    public function createUser(APIUserCreateStruct $userCreateStruct, array $parentGroups)
393
    {
394
        if (empty($parentGroups)) {
395
            throw new InvalidArgumentValue('parentGroups', $parentGroups);
396
        }
397
398
        if (!is_string($userCreateStruct->login) || empty($userCreateStruct->login)) {
399
            throw new InvalidArgumentValue('login', $userCreateStruct->login, 'UserCreateStruct');
400
        }
401
402
        if (!is_string($userCreateStruct->email) || empty($userCreateStruct->email)) {
403
            throw new InvalidArgumentValue('email', $userCreateStruct->email, 'UserCreateStruct');
404
        }
405
406
        if (!preg_match('/^.+@.+\..+$/', $userCreateStruct->email)) {
407
            throw new InvalidArgumentValue('email', $userCreateStruct->email, 'UserCreateStruct');
408
        }
409
410
        if (!is_string($userCreateStruct->password) || empty($userCreateStruct->password)) {
411
            throw new InvalidArgumentValue('password', $userCreateStruct->password, 'UserCreateStruct');
412
        }
413
414
        if (!is_bool($userCreateStruct->enabled)) {
415
            throw new InvalidArgumentValue('enabled', $userCreateStruct->enabled, 'UserCreateStruct');
416
        }
417
418
        try {
419
            $this->userHandler->loadByLogin($userCreateStruct->login);
420
            throw new InvalidArgumentException('userCreateStruct', 'User with provided login already exists');
421
        } catch (NotFoundException $e) {
422
            // Do nothing
423
        }
424
425
        $contentService = $this->repository->getContentService();
426
        $locationService = $this->repository->getLocationService();
427
        $contentTypeService = $this->repository->getContentTypeService();
428
429
        if ($userCreateStruct->contentType === null) {
430
            $userCreateStruct->contentType = $contentTypeService->loadContentType($this->settings['userClassID']);
431
        }
432
433
        $errors = $this->validatePassword($userCreateStruct->password, new PasswordValidationContext([
434
            'contentType' => $userCreateStruct->contentType,
435
        ]));
436
        if (!empty($errors)) {
437
            throw new UserPasswordValidationException('password', $errors);
438
        }
439
440
        $locationCreateStructs = [];
441
        foreach ($parentGroups as $parentGroup) {
442
            $parentGroup = $this->loadUserGroup($parentGroup->id);
443
            if ($parentGroup->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
444
                $locationCreateStructs[] = $locationService->newLocationCreateStruct(
445
                    $parentGroup->getVersionInfo()->getContentInfo()->mainLocationId
446
                );
447
            }
448
        }
449
450
        // Search for the first ezuser field type in content type
451
        $userFieldDefinition = null;
452
        foreach ($userCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
453
            if ($fieldDefinition->fieldTypeIdentifier == 'ezuser') {
454
                $userFieldDefinition = $fieldDefinition;
455
                break;
456
            }
457
        }
458
459
        if ($userFieldDefinition === null) {
460
            throw new ContentValidationException('Provided content type does not contain ezuser field type');
461
        }
462
463
        $fixUserFieldType = true;
464
        foreach ($userCreateStruct->fields as $index => $field) {
465
            if ($field->fieldDefIdentifier == $userFieldDefinition->identifier) {
466
                if ($field->value instanceof UserValue) {
467
                    $userCreateStruct->fields[$index]->value->login = $userCreateStruct->login;
468
                } else {
469
                    $userCreateStruct->fields[$index]->value = new UserValue(
470
                        [
471
                            'login' => $userCreateStruct->login,
472
                        ]
473
                    );
474
                }
475
476
                $fixUserFieldType = false;
477
            }
478
        }
479
480
        if ($fixUserFieldType) {
481
            $userCreateStruct->setField(
482
                $userFieldDefinition->identifier,
483
                new UserValue(
484
                    [
485
                        'login' => $userCreateStruct->login,
486
                    ]
487
                )
488
            );
489
        }
490
491
        $this->repository->beginTransaction();
492
        try {
493
            $contentDraft = $contentService->createContent($userCreateStruct, $locationCreateStructs);
494
            // Create user before publishing, so that external data can be returned
495
            $spiUser = $this->userHandler->create(
496
                new SPIUser(
497
                    [
498
                        'id' => $contentDraft->id,
499
                        'login' => $userCreateStruct->login,
500
                        'email' => $userCreateStruct->email,
501
                        'passwordHash' => $this->createPasswordHash(
502
                            $userCreateStruct->login,
503
                            $userCreateStruct->password,
504
                            $this->settings['siteName'],
505
                            $this->settings['hashType']
506
                        ),
507
                        'hashAlgorithm' => $this->settings['hashType'],
508
                        'passwordUpdatedAt' => time(),
509
                        'isEnabled' => $userCreateStruct->enabled,
510
                        'maxLogin' => 0,
511
                    ]
512
                )
513
            );
514
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
515
516
            $this->repository->commit();
517
        } catch (Exception $e) {
518
            $this->repository->rollback();
519
            throw $e;
520
        }
521
522
        return $this->buildDomainUserObject($spiUser, $publishedContent);
523
    }
524
525
    /**
526
     * Loads a user.
527
     *
528
     * @param mixed $userId
529
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
530
     *
531
     * @return \eZ\Publish\API\Repository\Values\User\User
532
     *
533
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given id was not found
534
     */
535
    public function loadUser($userId, array $prioritizedLanguages = [])
536
    {
537
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
538
        $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...
539
        // Get spiUser value from Field Value
540
        foreach ($content->getFields() as $field) {
541
            if (!$field->value instanceof UserValue) {
542
                continue;
543
            }
544
545
            /** @var \eZ\Publish\Core\FieldType\User\Value $value */
546
            $value = $field->value;
547
            $spiUser = new SPIUser();
548
            $spiUser->id = $value->contentId;
549
            $spiUser->login = $value->login;
550
            $spiUser->email = $value->email;
551
            $spiUser->hashAlgorithm = $value->passwordHashType;
552
            $spiUser->passwordHash = $value->passwordHash;
553
            $spiUser->passwordUpdatedAt = $value->passwordUpdatedAt ? $value->passwordUpdatedAt->getTimestamp() : null;
554
            $spiUser->isEnabled = $value->enabled;
555
            $spiUser->maxLogin = $value->maxLogin;
556
            break;
557
        }
558
559
        // If for some reason not found, load it
560
        if (!isset($spiUser)) {
561
            $spiUser = $this->userHandler->load($userId);
562
        }
563
564
        return $this->buildDomainUserObject($spiUser, $content);
565
    }
566
567
    /**
568
     * Loads anonymous user.
569
     *
570
     * @deprecated since 5.3, use loadUser( $anonymousUserId ) instead
571
     *
572
     * @uses ::loadUser()
573
     *
574
     * @return \eZ\Publish\API\Repository\Values\User\User
575
     */
576
    public function loadAnonymousUser()
577
    {
578
        return $this->loadUser($this->settings['anonymousUserID']);
579
    }
580
581
    /**
582
     * Loads a user for the given login and password.
583
     *
584
     * If the password hash type differs from that configured for the service, it will be updated to the configured one.
585
     *
586
     * {@inheritdoc}
587
     *
588
     * @param string $login
589
     * @param string $password the plain password
590
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
591
     *
592
     * @return \eZ\Publish\API\Repository\Values\User\User
593
     *
594
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if credentials are invalid
595
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
596
     */
597
    public function loadUserByCredentials($login, $password, array $prioritizedLanguages = [])
598
    {
599
        if (!is_string($login) || empty($login)) {
600
            throw new InvalidArgumentValue('login', $login);
601
        }
602
603
        if (!is_string($password)) {
604
            throw new InvalidArgumentValue('password', $password);
605
        }
606
607
        $spiUser = $this->userHandler->loadByLogin($login);
608
        if (!$this->verifyPassword($login, $password, $spiUser)) {
609
            throw new NotFoundException('user', $login);
610
        }
611
612
        // Don't catch BadStateException, on purpose, to avoid broken hashes.
613
        $this->updatePasswordHash($login, $password, $spiUser);
614
615
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
616
    }
617
618
    /**
619
     * Update password hash to the type configured for the service, if they differ.
620
     *
621
     * @param string $login User login
622
     * @param string $password User password
623
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
624
     *
625
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException if the password is not correctly saved, in which case the update is reverted
626
     */
627
    private function updatePasswordHash($login, $password, SPIUser $spiUser)
628
    {
629
        if ($spiUser->hashAlgorithm === $this->settings['hashType']) {
630
            return;
631
        }
632
633
        $spiUser->passwordHash = $this->createPasswordHash($login, $password, null, $this->settings['hashType']);
634
        $spiUser->hashAlgorithm = $this->settings['hashType'];
635
636
        $this->repository->beginTransaction();
637
        $this->userHandler->update($spiUser);
638
        $reloadedSpiUser = $this->userHandler->load($spiUser->id);
639
640
        if ($reloadedSpiUser->passwordHash === $spiUser->passwordHash) {
641
            $this->repository->commit();
642
        } else {
643
            // Password hash was not correctly saved, possible cause: EZP-28692
644
            $this->repository->rollback();
645
            if (isset($this->logger)) {
646
                $this->logger->critical('Password hash could not be updated. Please verify that your database schema is up to date.');
647
            }
648
649
            throw new BadStateException(
650
                'user',
651
                'Could not save updated password hash, reverting to previous hash. Please verify that your database schema is up to date.'
652
            );
653
        }
654
    }
655
656
    /**
657
     * Loads a user for the given login.
658
     *
659
     * {@inheritdoc}
660
     *
661
     * @param string $login
662
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
663
     *
664
     * @return \eZ\Publish\API\Repository\Values\User\User
665
     *
666
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
667
     */
668
    public function loadUserByLogin($login, array $prioritizedLanguages = [])
669
    {
670
        if (!is_string($login) || empty($login)) {
671
            throw new InvalidArgumentValue('login', $login);
672
        }
673
674
        $spiUser = $this->userHandler->loadByLogin($login);
675
676
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
677
    }
678
679
    /**
680
     * Loads a user for the given email.
681
     *
682
     * {@inheritdoc}
683
     *
684
     * @param string $email
685
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
686
     *
687
     * @return \eZ\Publish\API\Repository\Values\User\User[]
688
     */
689
    public function loadUsersByEmail($email, array $prioritizedLanguages = [])
690
    {
691
        if (!is_string($email) || empty($email)) {
692
            throw new InvalidArgumentValue('email', $email);
693
        }
694
695
        $users = [];
696
        foreach ($this->userHandler->loadByEmail($email) as $spiUser) {
697
            $users[] = $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
698
        }
699
700
        return $users;
701
    }
702
703
    /**
704
     * Loads a user for the given token.
705
     *
706
     * {@inheritdoc}
707
     *
708
     * @param string $hash
709
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
710
     *
711
     * @return \eZ\Publish\API\Repository\Values\User\User
712
     *
713
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
714
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
715
     */
716
    public function loadUserByToken($hash, array $prioritizedLanguages = [])
717
    {
718
        if (!is_string($hash) || empty($hash)) {
719
            throw new InvalidArgumentValue('hash', $hash);
720
        }
721
722
        $spiUser = $this->userHandler->loadUserByToken($hash);
723
724
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
725
    }
726
727
    /**
728
     * This method deletes a user.
729
     *
730
     * @param \eZ\Publish\API\Repository\Values\User\User $user
731
     *
732
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to delete the user
733
     */
734
    public function deleteUser(APIUser $user)
735
    {
736
        $loadedUser = $this->loadUser($user->id);
737
738
        $this->repository->beginTransaction();
739
        try {
740
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo());
741
            $this->userHandler->delete($loadedUser->id);
742
            $this->repository->commit();
743
        } catch (Exception $e) {
744
            $this->repository->rollback();
745
            throw $e;
746
        }
747
748
        return $affectedLocationIds;
749
    }
750
751
    /**
752
     * Updates a user.
753
     *
754
     * 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
755
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
756
     *
757
     * @param \eZ\Publish\API\Repository\Values\User\User $user
758
     * @param \eZ\Publish\API\Repository\Values\User\UserUpdateStruct $userUpdateStruct
759
     *
760
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userUpdateStruct is not valid
761
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
762
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user
763
     *
764
     * @return \eZ\Publish\API\Repository\Values\User\User
765
     */
766
    public function updateUser(APIUser $user, UserUpdateStruct $userUpdateStruct)
767
    {
768
        $loadedUser = $this->loadUser($user->id);
769
770
        // We need to determine if we have anything to update.
771
        // UserUpdateStruct is specific as some of the new content is in
772
        // content update struct and some of it is in additional fields like
773
        // email, password and so on
774
        $doUpdate = false;
775
        foreach ($userUpdateStruct as $propertyValue) {
0 ignored issues
show
Bug introduced by
The expression $userUpdateStruct of type object<eZ\Publish\API\Re...\User\UserUpdateStruct> is not traversable.
Loading history...
776
            if ($propertyValue !== null) {
777
                $doUpdate = true;
778
                break;
779
            }
780
        }
781
782
        if (!$doUpdate) {
783
            // Nothing to update, so we just quit
784
            return $user;
785
        }
786
787
        if ($userUpdateStruct->email !== null) {
788
            if (!is_string($userUpdateStruct->email) || empty($userUpdateStruct->email)) {
789
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
790
            }
791
792
            if (!preg_match('/^.+@.+\..+$/', $userUpdateStruct->email)) {
793
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
794
            }
795
        }
796
797
        if ($userUpdateStruct->enabled !== null && !is_bool($userUpdateStruct->enabled)) {
798
            throw new InvalidArgumentValue('enabled', $userUpdateStruct->enabled, 'UserUpdateStruct');
799
        }
800
801
        if ($userUpdateStruct->maxLogin !== null && !is_int($userUpdateStruct->maxLogin)) {
802
            throw new InvalidArgumentValue('maxLogin', $userUpdateStruct->maxLogin, 'UserUpdateStruct');
803
        }
804
805
        if ($userUpdateStruct->password !== null) {
806
            if (!is_string($userUpdateStruct->password) || empty($userUpdateStruct->password)) {
807
                throw new InvalidArgumentValue('password', $userUpdateStruct->password, 'UserUpdateStruct');
808
            }
809
810
            $userContentType = $this->repository->getContentTypeService()->loadContentType(
811
                $user->contentInfo->contentTypeId
812
            );
813
814
            $errors = $this->validatePassword($userUpdateStruct->password, new PasswordValidationContext([
815
                'contentType' => $userContentType,
816
                'user' => $user,
817
            ]));
818
819
            if (!empty($errors)) {
820
                throw new UserPasswordValidationException('password', $errors);
821
            }
822
        }
823
824
        $contentService = $this->repository->getContentService();
825
826
        $canEditContent = $this->permissionResolver->canUser('content', 'edit', $loadedUser);
827
828
        if (!$canEditContent && $this->isUserProfileUpdateRequested($userUpdateStruct)) {
829
            throw new UnauthorizedException('content', 'edit');
830
        }
831
832
        if (!empty($userUpdateStruct->password) &&
833
            !$canEditContent &&
834
            !$this->permissionResolver->canUser('user', 'password', $loadedUser)
835
        ) {
836
            throw new UnauthorizedException('user', 'password');
837
        }
838
839
        $this->repository->beginTransaction();
840
        try {
841
            $publishedContent = $loadedUser;
842
            if ($userUpdateStruct->contentUpdateStruct !== null) {
843
                $contentDraft = $contentService->createContentDraft($loadedUser->getVersionInfo()->getContentInfo());
844
                $contentDraft = $contentService->updateContent(
845
                    $contentDraft->getVersionInfo(),
846
                    $userUpdateStruct->contentUpdateStruct
847
                );
848
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
849
            }
850
851
            if ($userUpdateStruct->contentMetadataUpdateStruct !== null) {
852
                $contentService->updateContentMetadata(
853
                    $publishedContent->getVersionInfo()->getContentInfo(),
854
                    $userUpdateStruct->contentMetadataUpdateStruct
855
                );
856
            }
857
858
            $spiUser = new SPIUser([
859
                'id' => $loadedUser->id,
860
                'login' => $loadedUser->login,
861
                'email' => $userUpdateStruct->email ?: $loadedUser->email,
862
                'isEnabled' => $userUpdateStruct->enabled !== null ? $userUpdateStruct->enabled : $loadedUser->enabled,
863
                'maxLogin' => $userUpdateStruct->maxLogin !== null ? (int)$userUpdateStruct->maxLogin : $loadedUser->maxLogin,
864
            ]);
865
866
            if ($userUpdateStruct->password) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userUpdateStruct->password of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
867
                $spiUser->passwordHash = $this->createPasswordHash(
868
                    $loadedUser->login,
869
                    $userUpdateStruct->password,
870
                    $this->settings['siteName'],
871
                    $this->settings['hashType']
872
                );
873
                $spiUser->hashAlgorithm = $this->settings['hashType'];
874
                $spiUser->passwordUpdatedAt = time();
875
            } else {
876
                $spiUser->passwordHash = $loadedUser->passwordHash;
877
                $spiUser->hashAlgorithm = $loadedUser->hashAlgorithm;
878
                $spiUser->passwordUpdatedAt = $loadedUser->passwordUpdatedAt ? $loadedUser->passwordUpdatedAt->getTimestamp() : null;
879
            }
880
881
            $this->userHandler->update($spiUser);
882
883
            $this->repository->commit();
884
        } catch (Exception $e) {
885
            $this->repository->rollback();
886
            throw $e;
887
        }
888
889
        return $this->loadUser($loadedUser->id);
890
    }
891
892
    /**
893
     * Update the user token information specified by the user token struct.
894
     *
895
     * @param \eZ\Publish\API\Repository\Values\User\User $user
896
     * @param \eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct $userTokenUpdateStruct
897
     *
898
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
899
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
900
     * @throws \RuntimeException
901
     * @throws \Exception
902
     *
903
     * @return \eZ\Publish\API\Repository\Values\User\User
904
     */
905
    public function updateUserToken(APIUser $user, UserTokenUpdateStruct $userTokenUpdateStruct)
906
    {
907
        $loadedUser = $this->loadUser($user->id);
908
909
        if ($userTokenUpdateStruct->hashKey !== null && (!is_string($userTokenUpdateStruct->hashKey) || empty($userTokenUpdateStruct->hashKey))) {
910
            throw new InvalidArgumentValue('hashKey', $userTokenUpdateStruct->hashKey, 'UserTokenUpdateStruct');
911
        }
912
913
        if ($userTokenUpdateStruct->time === null) {
914
            throw new InvalidArgumentValue('time', $userTokenUpdateStruct->time, 'UserTokenUpdateStruct');
915
        }
916
917
        $this->repository->beginTransaction();
918
        try {
919
            $this->userHandler->updateUserToken(
920
                new SPIUserTokenUpdateStruct(
921
                    [
922
                        'userId' => $loadedUser->id,
923
                        'hashKey' => $userTokenUpdateStruct->hashKey,
924
                        'time' => $userTokenUpdateStruct->time->getTimestamp(),
925
                    ]
926
                )
927
            );
928
            $this->repository->commit();
929
        } catch (Exception $e) {
930
            $this->repository->rollback();
931
            throw $e;
932
        }
933
934
        return $this->loadUser($loadedUser->id);
935
    }
936
937
    /**
938
     * Expires user token with user hash.
939
     *
940
     * @param string $hash
941
     */
942
    public function expireUserToken($hash)
943
    {
944
        $this->repository->beginTransaction();
945
        try {
946
            $this->userHandler->expireUserToken($hash);
947
            $this->repository->commit();
948
        } catch (Exception $e) {
949
            $this->repository->rollback();
950
            throw $e;
951
        }
952
    }
953
954
    /**
955
     * Assigns a new user group to the user.
956
     *
957
     * @param \eZ\Publish\API\Repository\Values\User\User $user
958
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
959
     *
960
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to assign the user group to the user
961
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is already in the given user group
962
     */
963
    public function assignUserToUserGroup(APIUser $user, APIUserGroup $userGroup)
964
    {
965
        $loadedUser = $this->loadUser($user->id);
966
        $loadedGroup = $this->loadUserGroup($userGroup->id);
967
        $locationService = $this->repository->getLocationService();
968
969
        $existingGroupIds = [];
970
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
971
        foreach ($userLocations as $userLocation) {
972
            $existingGroupIds[] = $userLocation->parentLocationId;
973
        }
974
975
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
976
            throw new BadStateException('userGroup', 'user group has no main location or no locations');
977
        }
978
979
        if (in_array($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId, $existingGroupIds)) {
980
            // user is already assigned to the user group
981
            throw new InvalidArgumentException('user', 'user is already in the given user group');
982
        }
983
984
        $locationCreateStruct = $locationService->newLocationCreateStruct(
985
            $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId
986
        );
987
988
        $this->repository->beginTransaction();
989
        try {
990
            $locationService->createLocation(
991
                $loadedUser->getVersionInfo()->getContentInfo(),
992
                $locationCreateStruct
993
            );
994
            $this->repository->commit();
995
        } catch (Exception $e) {
996
            $this->repository->rollback();
997
            throw $e;
998
        }
999
    }
1000
1001
    /**
1002
     * Removes a user group from the user.
1003
     *
1004
     * @param \eZ\Publish\API\Repository\Values\User\User $user
1005
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
1006
     *
1007
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to remove the user group from the user
1008
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is not in the given user group
1009
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $userGroup is the last assigned user group
1010
     */
1011
    public function unAssignUserFromUserGroup(APIUser $user, APIUserGroup $userGroup)
1012
    {
1013
        $loadedUser = $this->loadUser($user->id);
1014
        $loadedGroup = $this->loadUserGroup($userGroup->id);
1015
        $locationService = $this->repository->getLocationService();
1016
1017
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
1018
        if (empty($userLocations)) {
1019
            throw new BadStateException('user', 'user has no locations, cannot unassign from group');
1020
        }
1021
1022
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1023
            throw new BadStateException('userGroup', 'user group has no main location or no locations, cannot unassign');
1024
        }
1025
1026
        foreach ($userLocations as $userLocation) {
1027
            if ($userLocation->parentLocationId == $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId) {
1028
                // Throw this specific BadState when we know argument is valid
1029
                if (count($userLocations) === 1) {
1030
                    throw new BadStateException('user', 'user only has one user group, cannot unassign from last group');
1031
                }
1032
1033
                $this->repository->beginTransaction();
1034
                try {
1035
                    $locationService->deleteLocation($userLocation);
1036
                    $this->repository->commit();
1037
1038
                    return;
1039
                } catch (Exception $e) {
1040
                    $this->repository->rollback();
1041
                    throw $e;
1042
                }
1043
            }
1044
        }
1045
1046
        throw new InvalidArgumentException('userGroup', 'user is not in the given user group');
1047
    }
1048
1049
    /**
1050
     * Loads the user groups the user belongs to.
1051
     *
1052
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed read the user or user group
1053
     *
1054
     * @param \eZ\Publish\API\Repository\Values\User\User $user
1055
     * @param int $offset the start offset for paging
1056
     * @param int $limit the number of user groups returned
1057
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1058
     *
1059
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
1060
     */
1061
    public function loadUserGroupsOfUser(APIUser $user, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
1062
    {
1063
        $locationService = $this->repository->getLocationService();
1064
1065
        if (!$this->repository->getPermissionResolver()->canUser('content', 'read', $user)) {
1066
            throw new UnauthorizedException('content', 'read');
1067
        }
1068
1069
        $userLocations = $locationService->loadLocations(
1070
            $user->getVersionInfo()->getContentInfo()
1071
        );
1072
1073
        $parentLocationIds = [];
1074
        foreach ($userLocations as $userLocation) {
1075
            if ($userLocation->parentLocationId !== null) {
1076
                $parentLocationIds[] = $userLocation->parentLocationId;
1077
            }
1078
        }
1079
1080
        $searchQuery = new LocationQuery();
1081
1082
        $searchQuery->offset = $offset;
1083
        $searchQuery->limit = $limit;
1084
        $searchQuery->performCount = false;
1085
1086
        $searchQuery->filter = new CriterionLogicalAnd(
1087
            [
1088
                new CriterionContentTypeId($this->settings['userGroupClassID']),
1089
                new CriterionLocationId($parentLocationIds),
1090
            ]
1091
        );
1092
1093
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1094
1095
        $userGroups = [];
1096
        foreach ($searchResult->searchHits as $resultItem) {
1097
            $userGroups[] = $this->buildDomainUserGroupObject(
1098
                $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...
1099
                    $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...
1100
                    $prioritizedLanguages
1101
                )
1102
            );
1103
        }
1104
1105
        return $userGroups;
1106
    }
1107
1108
    /**
1109
     * Loads the users of a user group.
1110
     *
1111
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the users or user group
1112
     *
1113
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
1114
     * @param int $offset the start offset for paging
1115
     * @param int $limit the number of users returned
1116
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1117
     *
1118
     * @return \eZ\Publish\API\Repository\Values\User\User[]
1119
     */
1120
    public function loadUsersOfUserGroup(
1121
        APIUserGroup $userGroup,
1122
        $offset = 0,
1123
        $limit = 25,
1124
        array $prioritizedLanguages = []
1125
    ) {
1126
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
1127
1128
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1129
            return [];
1130
        }
1131
1132
        $mainGroupLocation = $this->repository->getLocationService()->loadLocation(
1133
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
1134
        );
1135
1136
        $searchQuery = new LocationQuery();
1137
1138
        $searchQuery->filter = new CriterionLogicalAnd(
1139
            [
1140
                new CriterionContentTypeId($this->settings['userClassID']),
1141
                new CriterionParentLocationId($mainGroupLocation->id),
1142
            ]
1143
        );
1144
1145
        $searchQuery->offset = $offset;
1146
        $searchQuery->limit = $limit;
1147
        $searchQuery->performCount = false;
1148
        $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...
1149
1150
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1151
1152
        $users = [];
1153
        foreach ($searchResult->searchHits as $resultItem) {
1154
            $users[] = $this->buildDomainUserObject(
1155
                $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...
1156
                $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...
1157
                    $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...
1158
                    $prioritizedLanguages
1159
                )
1160
            );
1161
        }
1162
1163
        return $users;
1164
    }
1165
1166
    /**
1167
     * {@inheritdoc}
1168
     */
1169
    public function isUser(APIContent $content): bool
1170
    {
1171
        // First check against config for fast check
1172
        if ($this->settings['userClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId) {
1173
            return true;
1174
        }
1175
1176
        // For users we ultimately need to look for ezuser type as content type id could be several for users.
1177
        // And config might be different from one SA to the next, which we don't care about here.
1178
        foreach ($content->getFields() as $field) {
1179
            if ($field->fieldTypeIdentifier === 'ezuser') {
1180
                return true;
1181
            }
1182
        }
1183
1184
        return false;
1185
    }
1186
1187
    /**
1188
     * {@inheritdoc}
1189
     */
1190
    public function isUserGroup(APIContent $content): bool
1191
    {
1192
        return $this->settings['userGroupClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId;
1193
    }
1194
1195
    /**
1196
     * Instantiate a user create class.
1197
     *
1198
     * @param string $login the login of the new user
1199
     * @param string $email the email of the new user
1200
     * @param string $password the plain password of the new user
1201
     * @param string $mainLanguageCode the main language for the underlying content object
1202
     * @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
1203
     *
1204
     * @return \eZ\Publish\API\Repository\Values\User\UserCreateStruct
1205
     */
1206
    public function newUserCreateStruct($login, $email, $password, $mainLanguageCode, $contentType = null)
1207
    {
1208
        if ($contentType === null) {
1209
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1210
                $this->settings['userClassID']
1211
            );
1212
        }
1213
1214
        return new UserCreateStruct(
1215
            [
1216
                'contentType' => $contentType,
1217
                'mainLanguageCode' => $mainLanguageCode,
1218
                'login' => $login,
1219
                'email' => $email,
1220
                'password' => $password,
1221
                'enabled' => true,
1222
                'fields' => [],
1223
            ]
1224
        );
1225
    }
1226
1227
    /**
1228
     * Instantiate a user group create class.
1229
     *
1230
     * @param string $mainLanguageCode The main language for the underlying content object
1231
     * @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
1232
     *
1233
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct
1234
     */
1235
    public function newUserGroupCreateStruct($mainLanguageCode, $contentType = null)
1236
    {
1237
        if ($contentType === null) {
1238
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1239
                $this->settings['userGroupClassID']
1240
            );
1241
        }
1242
1243
        return new UserGroupCreateStruct(
1244
            [
1245
                'contentType' => $contentType,
1246
                'mainLanguageCode' => $mainLanguageCode,
1247
                'fields' => [],
1248
            ]
1249
        );
1250
    }
1251
1252
    /**
1253
     * Instantiate a new user update struct.
1254
     *
1255
     * @return \eZ\Publish\API\Repository\Values\User\UserUpdateStruct
1256
     */
1257
    public function newUserUpdateStruct()
1258
    {
1259
        return new UserUpdateStruct();
1260
    }
1261
1262
    /**
1263
     * Instantiate a new user group update struct.
1264
     *
1265
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
1266
     */
1267
    public function newUserGroupUpdateStruct()
1268
    {
1269
        return new UserGroupUpdateStruct();
1270
    }
1271
1272
    /**
1273
     * {@inheritdoc}
1274
     */
1275
    public function validatePassword(string $password, PasswordValidationContext $context = null): array
1276
    {
1277
        if ($context === null) {
1278
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1279
                $this->settings['userClassID']
1280
            );
1281
1282
            $context = new PasswordValidationContext([
1283
                'contentType' => $contentType,
1284
            ]);
1285
        }
1286
1287
        // Search for the first ezuser field type in content type
1288
        $userFieldDefinition = null;
1289
        foreach ($context->contentType->getFieldDefinitions() as $fieldDefinition) {
1290
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
1291
                $userFieldDefinition = $fieldDefinition;
1292
                break;
1293
            }
1294
        }
1295
1296
        if ($userFieldDefinition === null) {
1297
            throw new ContentValidationException('Provided content type does not contain ezuser field type');
1298
        }
1299
1300
        $configuration = $userFieldDefinition->getValidatorConfiguration();
1301
        if (!isset($configuration['PasswordValueValidator'])) {
1302
            return [];
1303
        }
1304
1305
        return (new UserPasswordValidator($configuration['PasswordValueValidator']))->validate($password);
1306
    }
1307
1308
    /**
1309
     * Builds the domain UserGroup object from provided Content object.
1310
     *
1311
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1312
     *
1313
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
1314
     */
1315
    protected function buildDomainUserGroupObject(APIContent $content)
1316
    {
1317
        $locationService = $this->repository->getLocationService();
1318
1319
        if ($content->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
1320
            $mainLocation = $locationService->loadLocation(
1321
                $content->getVersionInfo()->getContentInfo()->mainLocationId
1322
            );
1323
            $parentLocation = $this->locationHandler->load($mainLocation->parentLocationId);
1324
        }
1325
1326
        return new UserGroup(
1327
            [
1328
                'content' => $content,
1329
                'parentId' => $parentLocation->contentId ?? null,
1330
            ]
1331
        );
1332
    }
1333
1334
    /**
1335
     * Builds the domain user object from provided persistence user object.
1336
     *
1337
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
1338
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
1339
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1340
     *
1341
     * @return \eZ\Publish\API\Repository\Values\User\User
1342
     */
1343
    protected function buildDomainUserObject(
1344
        SPIUser $spiUser,
1345
        APIContent $content = null,
1346
        array $prioritizedLanguages = []
1347
    ) {
1348
        if ($content === null) {
1349
            $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...
1350
                $spiUser->id,
1351
                $prioritizedLanguages
1352
            );
1353
        }
1354
1355
        return new User(
1356
            [
1357
                'content' => $content,
1358
                'login' => $spiUser->login,
1359
                'email' => $spiUser->email,
1360
                'passwordHash' => $spiUser->passwordHash,
1361
                'passwordUpdatedAt' => $this->getDateTime($spiUser->passwordUpdatedAt),
1362
                'hashAlgorithm' => (int)$spiUser->hashAlgorithm,
1363
                'enabled' => $spiUser->isEnabled,
1364
                'maxLogin' => (int)$spiUser->maxLogin,
1365
            ]
1366
        );
1367
    }
1368
1369
    public function getPasswordInfo(APIUser $user): PasswordInfo
1370
    {
1371
        $passwordUpdatedAt = $user->passwordUpdatedAt;
1372
        if ($passwordUpdatedAt === null) {
1373
            return new PasswordInfo();
1374
        }
1375
1376
        $definition = $this->getUserFieldDefinition($user->getContentType());
1377
        if ($definition === null) {
1378
            return new PasswordInfo();
1379
        }
1380
1381
        $expirationDate = null;
1382
        $expirationWarningDate = null;
1383
1384
        $passwordTTL = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_SETTING];
1385
        if ($passwordTTL > 0) {
1386
            if ($passwordUpdatedAt instanceof DateTime) {
1387
                $passwordUpdatedAt = DateTimeImmutable::createFromMutable($passwordUpdatedAt);
1388
            }
1389
1390
            $expirationDate = $passwordUpdatedAt->add(new DateInterval(sprintf('P%dD', $passwordTTL)));
1391
1392
            $passwordTTLWarning = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_WARNING_SETTING];
1393
            if ($passwordTTLWarning > 0) {
1394
                $expirationWarningDate = $expirationDate->sub(new DateInterval(sprintf('P%dD', $passwordTTLWarning)));
1395
            }
1396
        }
1397
1398
        return new PasswordInfo($expirationDate, $expirationWarningDate);
1399
    }
1400
1401
    private function getUserFieldDefinition(ContentType $contentType): ?FieldDefinition
1402
    {
1403
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1404
            if ($fieldDefinition->fieldTypeIdentifier == 'ezuser') {
1405
                return $fieldDefinition;
1406
            }
1407
        }
1408
1409
        return null;
1410
    }
1411
1412
    /**
1413
     * Verifies if the provided login and password are valid.
1414
     *
1415
     * @param string $login User login
1416
     * @param string $password User password
1417
     * @param \eZ\Publish\SPI\Persistence\User $spiUser Loaded user handler
1418
     *
1419
     * @return bool return true if the login and password are sucessfully
1420
     * validate and false, if not.
1421
     */
1422
    protected function verifyPassword($login, $password, $spiUser)
1423
    {
1424
        // In case of bcrypt let php's password functionality do it's magic
1425
        if ($spiUser->hashAlgorithm === APIUser::PASSWORD_HASH_BCRYPT ||
1426
            $spiUser->hashAlgorithm === APIUser::PASSWORD_HASH_PHP_DEFAULT) {
1427
            return password_verify($password, $spiUser->passwordHash);
1428
        }
1429
1430
        // Randomize login time to protect against timing attacks
1431
        usleep(random_int(0, 30000));
1432
1433
        $passwordHash = $this->createPasswordHash(
1434
            $login,
1435
            $password,
1436
            $this->settings['siteName'],
1437
            $spiUser->hashAlgorithm
1438
        );
1439
1440
        return $passwordHash === $spiUser->passwordHash;
1441
    }
1442
1443
    /**
1444
     * Returns password hash based on user data and site settings.
1445
     *
1446
     * @param string $login User login
1447
     * @param string $password User password
1448
     * @param string $site The name of the site
1449
     * @param int $type Type of password to generate
1450
     *
1451
     * @return string Generated password hash
1452
     *
1453
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the type is not recognized
1454
     */
1455
    protected function createPasswordHash($login, $password, $site, $type)
1456
    {
1457
        $deprecationWarningFormat = 'Password hash type %s is deprecated since 6.13.';
1458
1459
        switch ($type) {
1460
            case APIUser::PASSWORD_HASH_MD5_PASSWORD:
0 ignored issues
show
Deprecated Code introduced by
The constant eZ\Publish\API\Repositor...SWORD_HASH_MD5_PASSWORD has been deprecated with message: since 6.13

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1461
                @trigger_error(sprintf($deprecationWarningFormat, 'PASSWORD_HASH_MD5_PASSWORD'), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1462
1463
                return md5($password);
1464
1465
            case APIUser::PASSWORD_HASH_MD5_USER:
0 ignored issues
show
Deprecated Code introduced by
The constant eZ\Publish\API\Repositor...:PASSWORD_HASH_MD5_USER has been deprecated with message: since 6.13

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1466
                @trigger_error(sprintf($deprecationWarningFormat, 'PASSWORD_HASH_MD5_USER'), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1467
1468
                return md5("$login\n$password");
1469
1470
            case APIUser::PASSWORD_HASH_MD5_SITE:
0 ignored issues
show
Deprecated Code introduced by
The constant eZ\Publish\API\Repositor...:PASSWORD_HASH_MD5_SITE has been deprecated with message: since 6.13

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1471
                @trigger_error(sprintf($deprecationWarningFormat, 'PASSWORD_HASH_MD5_SITE'), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1472
1473
                return md5("$login\n$password\n$site");
1474
1475
            case APIUser::PASSWORD_HASH_PLAINTEXT:
0 ignored issues
show
Deprecated Code introduced by
The constant eZ\Publish\API\Repositor...PASSWORD_HASH_PLAINTEXT has been deprecated with message: since 6.13

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1476
                @trigger_error(sprintf($deprecationWarningFormat, 'PASSWORD_HASH_PLAINTEXT'), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1477
1478
                return $password;
1479
1480
            case APIUser::PASSWORD_HASH_BCRYPT:
1481
                return password_hash($password, PASSWORD_BCRYPT);
1482
1483
            case APIUser::PASSWORD_HASH_PHP_DEFAULT:
1484
                return password_hash($password, PASSWORD_DEFAULT);
1485
1486
            default:
1487
                throw new InvalidArgumentException('type', "Password hash type '$type' is not recognized");
1488
        }
1489
    }
1490
1491
    /**
1492
     * Return true if any of the UserUpdateStruct properties refers to User Profile (Content) update.
1493
     *
1494
     * @param UserUpdateStruct $userUpdateStruct
1495
     *
1496
     * @return bool
1497
     */
1498
    private function isUserProfileUpdateRequested(UserUpdateStruct $userUpdateStruct)
1499
    {
1500
        return
1501
            !empty($userUpdateStruct->contentUpdateStruct) ||
1502
            !empty($userUpdateStruct->contentMetadataUpdateStruct) ||
1503
            !empty($userUpdateStruct->email) ||
1504
            !empty($userUpdateStruct->enabled) ||
1505
            !empty($userUpdateStruct->maxLogin);
1506
    }
1507
1508
    private function getDateTime(?int $timestamp): ?DateTimeInterface
1509
    {
1510
        if ($timestamp !== null) {
1511
            // Instead of using DateTime(ts) we use setTimeStamp() so timezone does not get set to UTC
1512
            $dateTime = new DateTime();
1513
            $dateTime->setTimestamp($timestamp);
1514
1515
            return DateTimeImmutable::createFromMutable($dateTime);
1516
        }
1517
1518
        return null;
1519
    }
1520
}
1521