Completed
Push — ezp_30797 ( cae135...f173ab )
by
unknown
18:10
created

UserService::getUserFieldDefinition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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