Completed
Push — ezp_30797 ( 45158f )
by
unknown
19:25
created

UserService::getPasswordExpirationDate()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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