Completed
Push — ezp_30797 ( 8198f2...1e476a )
by
unknown
18:51
created

UserService::getPasswordInfo()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

Loading history...
201
            throw new UnauthorizedException('content', 'read');
202
        }
203
204
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
205
            return [];
206
        }
207
208
        $mainGroupLocation = $locationService->loadLocation(
209
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
210
        );
211
212
        $searchResult = $this->searchSubGroups($mainGroupLocation, $offset, $limit);
213
        if ($searchResult->totalCount == 0) {
214
            return [];
215
        }
216
217
        $subUserGroups = [];
218
        foreach ($searchResult->searchHits as $searchHit) {
219
            $subUserGroups[] = $this->buildDomainUserGroupObject(
220
                $this->repository->getContentService()->internalLoadContent(
0 ignored issues
show
Bug introduced by
The method internalLoadContent() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
221
                    $searchHit->valueObject->contentInfo->id,
0 ignored issues
show
Documentation introduced by
The property contentInfo does not exist on object<eZ\Publish\API\Re...ory\Values\ValueObject>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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