Completed
Push — 7.5 ( faf482...14c55e )
by
unknown
191:03 queued 173:36
created

UserService::checkUserCredentials()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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