Completed
Push — 7.5 ( 497ec7...132d3e )
by Łukasz
57:42 queued 36:01
created

UserService::comparePasswordHashes()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 4
dl 0
loc 23
rs 9.552
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
        if (!is_string($login) || empty($login)) {
601
            throw new InvalidArgumentValue('login', $login);
602
        }
603
604
        if (!is_string($password)) {
605
            throw new InvalidArgumentValue('password', $password);
606
        }
607
608
        $spiUser = $this->userHandler->loadByLogin($login);
609
        if (!$this->comparePasswordHashForSPIUser($login, $password, $spiUser)) {
610
            throw new NotFoundException('user', $login);
611
        }
612
613
        // Don't catch BadStateException, on purpose, to avoid broken hashes.
614
        $this->updatePasswordHash($login, $password, $spiUser);
615
616
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
617
    }
618
619
    /**
620
     * Update password hash to the type configured for the service, if they differ.
621
     *
622
     * @param string $login User login
623
     * @param string $password User password
624
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
625
     *
626
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException if the password is not correctly saved, in which case the update is reverted
627
     */
628
    private function updatePasswordHash($login, $password, SPIUser $spiUser)
629
    {
630
        if ($spiUser->hashAlgorithm === $this->settings['hashType']) {
631
            return;
632
        }
633
634
        $spiUser->passwordHash = $this->createPasswordHash($login, $password, null, $this->settings['hashType']);
635
        $spiUser->hashAlgorithm = $this->settings['hashType'];
636
637
        $this->repository->beginTransaction();
638
        $this->userHandler->update($spiUser);
639
        $reloadedSpiUser = $this->userHandler->load($spiUser->id);
640
641
        if ($reloadedSpiUser->passwordHash === $spiUser->passwordHash) {
642
            $this->repository->commit();
643
        } else {
644
            // Password hash was not correctly saved, possible cause: EZP-28692
645
            $this->repository->rollback();
646
            if (isset($this->logger)) {
647
                $this->logger->critical('Password hash could not be updated. Please verify that your database schema is up to date.');
648
            }
649
650
            throw new BadStateException(
651
                'user',
652
                'Could not save updated password hash, reverting to previous hash. Please verify that your database schema is up to date.'
653
            );
654
        }
655
    }
656
657
    /**
658
     * Loads a user for the given login.
659
     *
660
     * {@inheritdoc}
661
     *
662
     * @param string $login
663
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
664
     *
665
     * @return \eZ\Publish\API\Repository\Values\User\User
666
     *
667
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
668
     */
669
    public function loadUserByLogin($login, array $prioritizedLanguages = [])
670
    {
671
        if (!is_string($login) || empty($login)) {
672
            throw new InvalidArgumentValue('login', $login);
673
        }
674
675
        $spiUser = $this->userHandler->loadByLogin($login);
676
677
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
678
    }
679
680
    /**
681
     * Loads a user for the given email.
682
     *
683
     * {@inheritdoc}
684
     *
685
     * @param string $email
686
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
687
     *
688
     * @return \eZ\Publish\API\Repository\Values\User\User[]
689
     */
690
    public function loadUsersByEmail($email, array $prioritizedLanguages = [])
691
    {
692
        if (!is_string($email) || empty($email)) {
693
            throw new InvalidArgumentValue('email', $email);
694
        }
695
696
        $users = [];
697
        foreach ($this->userHandler->loadByEmail($email) as $spiUser) {
698
            $users[] = $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
699
        }
700
701
        return $users;
702
    }
703
704
    /**
705
     * Loads a user for the given token.
706
     *
707
     * {@inheritdoc}
708
     *
709
     * @param string $hash
710
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
711
     *
712
     * @return \eZ\Publish\API\Repository\Values\User\User
713
     *
714
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
715
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
716
     */
717
    public function loadUserByToken($hash, array $prioritizedLanguages = [])
718
    {
719
        if (!is_string($hash) || empty($hash)) {
720
            throw new InvalidArgumentValue('hash', $hash);
721
        }
722
723
        $spiUser = $this->userHandler->loadUserByToken($hash);
724
725
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
726
    }
727
728
    /**
729
     * This method deletes a user.
730
     *
731
     * @param \eZ\Publish\API\Repository\Values\User\User $user
732
     *
733
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to delete the user
734
     */
735
    public function deleteUser(APIUser $user)
736
    {
737
        $loadedUser = $this->loadUser($user->id);
738
739
        $this->repository->beginTransaction();
740
        try {
741
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo());
742
            $this->userHandler->delete($loadedUser->id);
743
            $this->repository->commit();
744
        } catch (Exception $e) {
745
            $this->repository->rollback();
746
            throw $e;
747
        }
748
749
        return $affectedLocationIds;
750
    }
751
752
    /**
753
     * Updates a user.
754
     *
755
     * 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
756
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
757
     *
758
     * @param \eZ\Publish\API\Repository\Values\User\User $user
759
     * @param \eZ\Publish\API\Repository\Values\User\UserUpdateStruct $userUpdateStruct
760
     *
761
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userUpdateStruct is not valid
762
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
763
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user
764
     *
765
     * @return \eZ\Publish\API\Repository\Values\User\User
766
     */
767
    public function updateUser(APIUser $user, UserUpdateStruct $userUpdateStruct)
768
    {
769
        $loadedUser = $this->loadUser($user->id);
770
771
        // We need to determine if we have anything to update.
772
        // UserUpdateStruct is specific as some of the new content is in
773
        // content update struct and some of it is in additional fields like
774
        // email, password and so on
775
        $doUpdate = false;
776
        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...
777
            if ($propertyValue !== null) {
778
                $doUpdate = true;
779
                break;
780
            }
781
        }
782
783
        if (!$doUpdate) {
784
            // Nothing to update, so we just quit
785
            return $user;
786
        }
787
788
        if ($userUpdateStruct->email !== null) {
789
            if (!is_string($userUpdateStruct->email) || empty($userUpdateStruct->email)) {
790
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
791
            }
792
793
            if (!preg_match('/^.+@.+\..+$/', $userUpdateStruct->email)) {
794
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
795
            }
796
        }
797
798
        if ($userUpdateStruct->enabled !== null && !is_bool($userUpdateStruct->enabled)) {
799
            throw new InvalidArgumentValue('enabled', $userUpdateStruct->enabled, 'UserUpdateStruct');
800
        }
801
802
        if ($userUpdateStruct->maxLogin !== null && !is_int($userUpdateStruct->maxLogin)) {
803
            throw new InvalidArgumentValue('maxLogin', $userUpdateStruct->maxLogin, 'UserUpdateStruct');
804
        }
805
806
        if ($userUpdateStruct->password !== null) {
807
            if (!is_string($userUpdateStruct->password) || empty($userUpdateStruct->password)) {
808
                throw new InvalidArgumentValue('password', $userUpdateStruct->password, 'UserUpdateStruct');
809
            }
810
811
            $userContentType = $this->repository->getContentTypeService()->loadContentType(
812
                $user->contentInfo->contentTypeId
813
            );
814
815
            $errors = $this->validatePassword($userUpdateStruct->password, new PasswordValidationContext([
816
                'contentType' => $userContentType,
817
                'user' => $user,
818
            ]));
819
820
            if (!empty($errors)) {
821
                throw new UserPasswordValidationException('password', $errors);
822
            }
823
        }
824
825
        $contentService = $this->repository->getContentService();
826
827
        $canEditContent = $this->permissionResolver->canUser('content', 'edit', $loadedUser);
828
829
        if (!$canEditContent && $this->isUserProfileUpdateRequested($userUpdateStruct)) {
830
            throw new UnauthorizedException('content', 'edit');
831
        }
832
833
        if (!empty($userUpdateStruct->password) &&
834
            !$canEditContent &&
835
            !$this->permissionResolver->canUser('user', 'password', $loadedUser)
836
        ) {
837
            throw new UnauthorizedException('user', 'password');
838
        }
839
840
        $this->repository->beginTransaction();
841
        try {
842
            $publishedContent = $loadedUser;
843
            if ($userUpdateStruct->contentUpdateStruct !== null) {
844
                $contentDraft = $contentService->createContentDraft($loadedUser->getVersionInfo()->getContentInfo());
845
                $contentDraft = $contentService->updateContent(
846
                    $contentDraft->getVersionInfo(),
847
                    $userUpdateStruct->contentUpdateStruct
848
                );
849
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
850
            }
851
852
            if ($userUpdateStruct->contentMetadataUpdateStruct !== null) {
853
                $contentService->updateContentMetadata(
854
                    $publishedContent->getVersionInfo()->getContentInfo(),
855
                    $userUpdateStruct->contentMetadataUpdateStruct
856
                );
857
            }
858
859
            $spiUser = new SPIUser([
860
                'id' => $loadedUser->id,
861
                'login' => $loadedUser->login,
862
                'email' => $userUpdateStruct->email ?: $loadedUser->email,
863
                'isEnabled' => $userUpdateStruct->enabled !== null ? $userUpdateStruct->enabled : $loadedUser->enabled,
864
                'maxLogin' => $userUpdateStruct->maxLogin !== null ? (int)$userUpdateStruct->maxLogin : $loadedUser->maxLogin,
865
            ]);
866
867
            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...
868
                $spiUser->passwordHash = $this->createPasswordHash(
869
                    $loadedUser->login,
870
                    $userUpdateStruct->password,
871
                    $this->settings['siteName'],
872
                    $this->settings['hashType']
873
                );
874
                $spiUser->hashAlgorithm = $this->settings['hashType'];
875
                $spiUser->passwordUpdatedAt = time();
876
            } else {
877
                $spiUser->passwordHash = $loadedUser->passwordHash;
878
                $spiUser->hashAlgorithm = $loadedUser->hashAlgorithm;
879
                $spiUser->passwordUpdatedAt = $loadedUser->passwordUpdatedAt ? $loadedUser->passwordUpdatedAt->getTimestamp() : null;
880
            }
881
882
            $this->userHandler->update($spiUser);
883
884
            $this->repository->commit();
885
        } catch (Exception $e) {
886
            $this->repository->rollback();
887
            throw $e;
888
        }
889
890
        return $this->loadUser($loadedUser->id);
891
    }
892
893
    /**
894
     * Update the user token information specified by the user token struct.
895
     *
896
     * @param \eZ\Publish\API\Repository\Values\User\User $user
897
     * @param \eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct $userTokenUpdateStruct
898
     *
899
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
900
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
901
     * @throws \RuntimeException
902
     * @throws \Exception
903
     *
904
     * @return \eZ\Publish\API\Repository\Values\User\User
905
     */
906
    public function updateUserToken(APIUser $user, UserTokenUpdateStruct $userTokenUpdateStruct)
907
    {
908
        $loadedUser = $this->loadUser($user->id);
909
910
        if ($userTokenUpdateStruct->hashKey !== null && (!is_string($userTokenUpdateStruct->hashKey) || empty($userTokenUpdateStruct->hashKey))) {
911
            throw new InvalidArgumentValue('hashKey', $userTokenUpdateStruct->hashKey, 'UserTokenUpdateStruct');
912
        }
913
914
        if ($userTokenUpdateStruct->time === null) {
915
            throw new InvalidArgumentValue('time', $userTokenUpdateStruct->time, 'UserTokenUpdateStruct');
916
        }
917
918
        $this->repository->beginTransaction();
919
        try {
920
            $this->userHandler->updateUserToken(
921
                new SPIUserTokenUpdateStruct(
922
                    [
923
                        'userId' => $loadedUser->id,
924
                        'hashKey' => $userTokenUpdateStruct->hashKey,
925
                        'time' => $userTokenUpdateStruct->time->getTimestamp(),
926
                    ]
927
                )
928
            );
929
            $this->repository->commit();
930
        } catch (Exception $e) {
931
            $this->repository->rollback();
932
            throw $e;
933
        }
934
935
        return $this->loadUser($loadedUser->id);
936
    }
937
938
    /**
939
     * Expires user token with user hash.
940
     *
941
     * @param string $hash
942
     */
943
    public function expireUserToken($hash)
944
    {
945
        $this->repository->beginTransaction();
946
        try {
947
            $this->userHandler->expireUserToken($hash);
948
            $this->repository->commit();
949
        } catch (Exception $e) {
950
            $this->repository->rollback();
951
            throw $e;
952
        }
953
    }
954
955
    /**
956
     * Assigns a new user group to the user.
957
     *
958
     * @param \eZ\Publish\API\Repository\Values\User\User $user
959
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
960
     *
961
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to assign the user group to the user
962
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is already in the given user group
963
     */
964
    public function assignUserToUserGroup(APIUser $user, APIUserGroup $userGroup)
965
    {
966
        $loadedUser = $this->loadUser($user->id);
967
        $loadedGroup = $this->loadUserGroup($userGroup->id);
968
        $locationService = $this->repository->getLocationService();
969
970
        $existingGroupIds = [];
971
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
972
        foreach ($userLocations as $userLocation) {
973
            $existingGroupIds[] = $userLocation->parentLocationId;
974
        }
975
976
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
977
            throw new BadStateException('userGroup', 'user group has no main location or no locations');
978
        }
979
980
        if (in_array($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId, $existingGroupIds)) {
981
            // user is already assigned to the user group
982
            throw new InvalidArgumentException('user', 'user is already in the given user group');
983
        }
984
985
        $locationCreateStruct = $locationService->newLocationCreateStruct(
986
            $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId
987
        );
988
989
        $this->repository->beginTransaction();
990
        try {
991
            $locationService->createLocation(
992
                $loadedUser->getVersionInfo()->getContentInfo(),
993
                $locationCreateStruct
994
            );
995
            $this->repository->commit();
996
        } catch (Exception $e) {
997
            $this->repository->rollback();
998
            throw $e;
999
        }
1000
    }
1001
1002
    /**
1003
     * Removes a user group from the user.
1004
     *
1005
     * @param \eZ\Publish\API\Repository\Values\User\User $user
1006
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
1007
     *
1008
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to remove the user group from the user
1009
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is not in the given user group
1010
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $userGroup is the last assigned user group
1011
     */
1012
    public function unAssignUserFromUserGroup(APIUser $user, APIUserGroup $userGroup)
1013
    {
1014
        $loadedUser = $this->loadUser($user->id);
1015
        $loadedGroup = $this->loadUserGroup($userGroup->id);
1016
        $locationService = $this->repository->getLocationService();
1017
1018
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
1019
        if (empty($userLocations)) {
1020
            throw new BadStateException('user', 'user has no locations, cannot unassign from group');
1021
        }
1022
1023
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1024
            throw new BadStateException('userGroup', 'user group has no main location or no locations, cannot unassign');
1025
        }
1026
1027
        foreach ($userLocations as $userLocation) {
1028
            if ($userLocation->parentLocationId == $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId) {
1029
                // Throw this specific BadState when we know argument is valid
1030
                if (count($userLocations) === 1) {
1031
                    throw new BadStateException('user', 'user only has one user group, cannot unassign from last group');
1032
                }
1033
1034
                $this->repository->beginTransaction();
1035
                try {
1036
                    $locationService->deleteLocation($userLocation);
1037
                    $this->repository->commit();
1038
1039
                    return;
1040
                } catch (Exception $e) {
1041
                    $this->repository->rollback();
1042
                    throw $e;
1043
                }
1044
            }
1045
        }
1046
1047
        throw new InvalidArgumentException('userGroup', 'user is not in the given user group');
1048
    }
1049
1050
    /**
1051
     * Loads the user groups the user belongs to.
1052
     *
1053
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed read the user or user group
1054
     *
1055
     * @param \eZ\Publish\API\Repository\Values\User\User $user
1056
     * @param int $offset the start offset for paging
1057
     * @param int $limit the number of user groups returned
1058
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1059
     *
1060
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
1061
     */
1062
    public function loadUserGroupsOfUser(APIUser $user, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
1063
    {
1064
        $locationService = $this->repository->getLocationService();
1065
1066
        if (!$this->repository->getPermissionResolver()->canUser('content', 'read', $user)) {
1067
            throw new UnauthorizedException('content', 'read');
1068
        }
1069
1070
        $userLocations = $locationService->loadLocations(
1071
            $user->getVersionInfo()->getContentInfo()
1072
        );
1073
1074
        $parentLocationIds = [];
1075
        foreach ($userLocations as $userLocation) {
1076
            if ($userLocation->parentLocationId !== null) {
1077
                $parentLocationIds[] = $userLocation->parentLocationId;
1078
            }
1079
        }
1080
1081
        $searchQuery = new LocationQuery();
1082
1083
        $searchQuery->offset = $offset;
1084
        $searchQuery->limit = $limit;
1085
        $searchQuery->performCount = false;
1086
1087
        $searchQuery->filter = new CriterionLogicalAnd(
1088
            [
1089
                new CriterionContentTypeId($this->settings['userGroupClassID']),
1090
                new CriterionLocationId($parentLocationIds),
1091
            ]
1092
        );
1093
1094
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1095
1096
        $userGroups = [];
1097
        foreach ($searchResult->searchHits as $resultItem) {
1098
            $userGroups[] = $this->buildDomainUserGroupObject(
1099
                $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...
1100
                    $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...
1101
                    $prioritizedLanguages
1102
                )
1103
            );
1104
        }
1105
1106
        return $userGroups;
1107
    }
1108
1109
    /**
1110
     * Loads the users of a user group.
1111
     *
1112
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the users or user group
1113
     *
1114
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
1115
     * @param int $offset the start offset for paging
1116
     * @param int $limit the number of users returned
1117
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1118
     *
1119
     * @return \eZ\Publish\API\Repository\Values\User\User[]
1120
     */
1121
    public function loadUsersOfUserGroup(
1122
        APIUserGroup $userGroup,
1123
        $offset = 0,
1124
        $limit = 25,
1125
        array $prioritizedLanguages = []
1126
    ) {
1127
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
1128
1129
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1130
            return [];
1131
        }
1132
1133
        $mainGroupLocation = $this->repository->getLocationService()->loadLocation(
1134
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
1135
        );
1136
1137
        $searchQuery = new LocationQuery();
1138
1139
        $searchQuery->filter = new CriterionLogicalAnd(
1140
            [
1141
                new CriterionContentTypeId($this->settings['userClassID']),
1142
                new CriterionParentLocationId($mainGroupLocation->id),
1143
            ]
1144
        );
1145
1146
        $searchQuery->offset = $offset;
1147
        $searchQuery->limit = $limit;
1148
        $searchQuery->performCount = false;
1149
        $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...
1150
1151
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1152
1153
        $users = [];
1154
        foreach ($searchResult->searchHits as $resultItem) {
1155
            $users[] = $this->buildDomainUserObject(
1156
                $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...
1157
                $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...
1158
                    $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...
1159
                    $prioritizedLanguages
1160
                )
1161
            );
1162
        }
1163
1164
        return $users;
1165
    }
1166
1167
    /**
1168
     * {@inheritdoc}
1169
     */
1170
    public function isUser(APIContent $content): bool
1171
    {
1172
        // First check against config for fast check
1173
        if ($this->settings['userClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId) {
1174
            return true;
1175
        }
1176
1177
        // For users we ultimately need to look for ezuser type as content type id could be several for users.
1178
        // And config might be different from one SA to the next, which we don't care about here.
1179
        foreach ($content->getFields() as $field) {
1180
            if ($field->fieldTypeIdentifier === 'ezuser') {
1181
                return true;
1182
            }
1183
        }
1184
1185
        return false;
1186
    }
1187
1188
    /**
1189
     * {@inheritdoc}
1190
     */
1191
    public function isUserGroup(APIContent $content): bool
1192
    {
1193
        return $this->settings['userGroupClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId;
1194
    }
1195
1196
    /**
1197
     * Instantiate a user create class.
1198
     *
1199
     * @param string $login the login of the new user
1200
     * @param string $email the email of the new user
1201
     * @param string $password the plain password of the new user
1202
     * @param string $mainLanguageCode the main language for the underlying content object
1203
     * @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
1204
     *
1205
     * @return \eZ\Publish\API\Repository\Values\User\UserCreateStruct
1206
     */
1207
    public function newUserCreateStruct($login, $email, $password, $mainLanguageCode, $contentType = null)
1208
    {
1209
        if ($contentType === null) {
1210
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1211
                $this->settings['userClassID']
1212
            );
1213
        }
1214
1215
        return new UserCreateStruct(
1216
            [
1217
                'contentType' => $contentType,
1218
                'mainLanguageCode' => $mainLanguageCode,
1219
                'login' => $login,
1220
                'email' => $email,
1221
                'password' => $password,
1222
                'enabled' => true,
1223
                'fields' => [],
1224
            ]
1225
        );
1226
    }
1227
1228
    /**
1229
     * Instantiate a user group create class.
1230
     *
1231
     * @param string $mainLanguageCode The main language for the underlying content object
1232
     * @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
1233
     *
1234
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct
1235
     */
1236
    public function newUserGroupCreateStruct($mainLanguageCode, $contentType = null)
1237
    {
1238
        if ($contentType === null) {
1239
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1240
                $this->settings['userGroupClassID']
1241
            );
1242
        }
1243
1244
        return new UserGroupCreateStruct(
1245
            [
1246
                'contentType' => $contentType,
1247
                'mainLanguageCode' => $mainLanguageCode,
1248
                'fields' => [],
1249
            ]
1250
        );
1251
    }
1252
1253
    /**
1254
     * Instantiate a new user update struct.
1255
     *
1256
     * @return \eZ\Publish\API\Repository\Values\User\UserUpdateStruct
1257
     */
1258
    public function newUserUpdateStruct()
1259
    {
1260
        return new UserUpdateStruct();
1261
    }
1262
1263
    /**
1264
     * Instantiate a new user group update struct.
1265
     *
1266
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
1267
     */
1268
    public function newUserGroupUpdateStruct()
1269
    {
1270
        return new UserGroupUpdateStruct();
1271
    }
1272
1273
    /**
1274
     * {@inheritdoc}
1275
     */
1276
    public function validatePassword(string $password, PasswordValidationContext $context = null): array
1277
    {
1278
        $errors = [];
1279
1280
        if ($context === null) {
1281
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1282
                $this->settings['userClassID']
1283
            );
1284
1285
            $context = new PasswordValidationContext([
1286
                'contentType' => $contentType,
1287
            ]);
1288
        }
1289
1290
        // Search for the first ezuser field type in content type
1291
        $userFieldDefinition = null;
1292
        foreach ($context->contentType->getFieldDefinitions() as $fieldDefinition) {
1293
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
1294
                $userFieldDefinition = $fieldDefinition;
1295
                break;
1296
            }
1297
        }
1298
1299
        if ($userFieldDefinition === null) {
1300
            throw new ContentValidationException('Provided content type does not contain ezuser field type');
1301
        }
1302
1303
        $configuration = $userFieldDefinition->getValidatorConfiguration();
1304
        if (isset($configuration['PasswordValueValidator'])) {
1305
            $errors = (new UserPasswordValidator($configuration['PasswordValueValidator']))->validate($password);
1306
        }
1307
1308
        if ($context->user !== null) {
1309
            $isPasswordTTLEnabled = $this->getPasswordInfo($context->user)->hasExpirationDate();
1310
            $isNewPasswordRequired = $configuration['PasswordValueValidator']['requireNewPassword'] ?? false;
1311
1312
            if (($isPasswordTTLEnabled || $isNewPasswordRequired) &&
1313
                $this->comparePasswordHashForAPIUser($context->user->login, $password, $context->user)
1314
            ) {
1315
                $errors[] = new ValidationError('New password cannot be the same as old password', null, [], 'password');
1316
            }
1317
        }
1318
1319
        return $errors;
1320
    }
1321
1322
    /**
1323
     * Builds the domain UserGroup object from provided Content object.
1324
     *
1325
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1326
     *
1327
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
1328
     */
1329
    protected function buildDomainUserGroupObject(APIContent $content)
1330
    {
1331
        $locationService = $this->repository->getLocationService();
1332
1333
        if ($content->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
1334
            $mainLocation = $locationService->loadLocation(
1335
                $content->getVersionInfo()->getContentInfo()->mainLocationId
1336
            );
1337
            $parentLocation = $this->locationHandler->load($mainLocation->parentLocationId);
1338
        }
1339
1340
        return new UserGroup(
1341
            [
1342
                'content' => $content,
1343
                'parentId' => $parentLocation->contentId ?? null,
1344
            ]
1345
        );
1346
    }
1347
1348
    /**
1349
     * Builds the domain user object from provided persistence user object.
1350
     *
1351
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
1352
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
1353
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1354
     *
1355
     * @return \eZ\Publish\API\Repository\Values\User\User
1356
     */
1357
    protected function buildDomainUserObject(
1358
        SPIUser $spiUser,
1359
        APIContent $content = null,
1360
        array $prioritizedLanguages = []
1361
    ) {
1362
        if ($content === null) {
1363
            $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...
1364
                $spiUser->id,
1365
                $prioritizedLanguages
1366
            );
1367
        }
1368
1369
        return new User(
1370
            [
1371
                'content' => $content,
1372
                'login' => $spiUser->login,
1373
                'email' => $spiUser->email,
1374
                'passwordHash' => $spiUser->passwordHash,
1375
                'passwordUpdatedAt' => $this->getDateTime($spiUser->passwordUpdatedAt),
1376
                'hashAlgorithm' => (int)$spiUser->hashAlgorithm,
1377
                'enabled' => $spiUser->isEnabled,
1378
                'maxLogin' => (int)$spiUser->maxLogin,
1379
            ]
1380
        );
1381
    }
1382
1383
    public function getPasswordInfo(APIUser $user): PasswordInfo
1384
    {
1385
        $passwordUpdatedAt = $user->passwordUpdatedAt;
1386
        if ($passwordUpdatedAt === null) {
1387
            return new PasswordInfo();
1388
        }
1389
1390
        $definition = $this->getUserFieldDefinition($user->getContentType());
1391
        if ($definition === null) {
1392
            return new PasswordInfo();
1393
        }
1394
1395
        $expirationDate = null;
1396
        $expirationWarningDate = null;
1397
1398
        $passwordTTL = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_SETTING];
1399
        if ($passwordTTL > 0) {
1400
            if ($passwordUpdatedAt instanceof DateTime) {
1401
                $passwordUpdatedAt = DateTimeImmutable::createFromMutable($passwordUpdatedAt);
1402
            }
1403
1404
            $expirationDate = $passwordUpdatedAt->add(new DateInterval(sprintf('P%dD', $passwordTTL)));
1405
1406
            $passwordTTLWarning = (int)$definition->fieldSettings[UserType::PASSWORD_TTL_WARNING_SETTING];
1407
            if ($passwordTTLWarning > 0) {
1408
                $expirationWarningDate = $expirationDate->sub(new DateInterval(sprintf('P%dD', $passwordTTLWarning)));
1409
            }
1410
        }
1411
1412
        return new PasswordInfo($expirationDate, $expirationWarningDate);
1413
    }
1414
1415
    private function getUserFieldDefinition(ContentType $contentType): ?FieldDefinition
1416
    {
1417
        foreach ($contentType->getFieldDefinitions() as $fieldDefinition) {
1418
            if ($fieldDefinition->fieldTypeIdentifier == 'ezuser') {
1419
                return $fieldDefinition;
1420
            }
1421
        }
1422
1423
        return null;
1424
    }
1425
1426
    /**
1427
     * Verifies if the provided login and password are valid for eZ\Publish\SPI\Persistence\User.
1428
     *
1429
     * @param string $login User login
1430
     * @param string $password User password
1431
     * @param \eZ\Publish\SPI\Persistence\User $spiUser Loaded user handler
1432
     *
1433
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1434
     */
1435
    protected function comparePasswordHashForSPIUser(string $login, string $password, SPIUser $spiUser): bool
1436
    {
1437
        return $this->comparePasswordHashes($login, $password, $spiUser->passwordHash, $spiUser->hashAlgorithm);
1438
    }
1439
1440
    /**
1441
     * Verifies if the provided login and password are valid for eZ\Publish\API\Repository\Values\User\User.
1442
     *
1443
     * @param string $login User login
1444
     * @param string $password User password
1445
     * @param \eZ\Publish\API\Repository\Values\User\User $apiUser Loaded user
1446
     *
1447
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1448
     */
1449
    protected function comparePasswordHashForAPIUser(string $login, string $password, APIUser $apiUser): bool
1450
    {
1451
        return $this->comparePasswordHashes($login, $password, $apiUser->passwordHash, $apiUser->hashAlgorithm);
1452
    }
1453
1454
    /**
1455
     * Verifies if the provided login and password are valid.
1456
     *
1457
     * @deprecated since v7.5.5 in favour of verifyPasswordForSPIUser
1458
     *
1459
     * @param string $login User login
1460
     * @param string $password User password
1461
     * @param \eZ\Publish\SPI\Persistence\User $spiUser Loaded user handler
1462
     *
1463
     * @return bool return true if the login and password are sucessfully
1464
     * validate and false, if not.
1465
     */
1466
    protected function verifyPassword($login, $password, $spiUser)
1467
    {
1468
        return $this->comparePasswordHashForSPIUser($login, $password, $spiUser);
1469
    }
1470
1471
    /**
1472
     * Verifies if the provided login and password are valid against given password hash and hash type.
1473
     *
1474
     * @param string $login User login
1475
     * @param string $plainPassword User password
1476
     * @param string $passwordHash User password hash
1477
     * @param int $hashAlgorithm Hash type
1478
     *
1479
     * @return bool return true if the login and password are sucessfully validated and false, if not.
1480
     */
1481
    private function comparePasswordHashes(
1482
        string $login,
1483
        string $plainPassword,
1484
        string $passwordHash,
1485
        int $hashAlgorithm
1486
    ): bool {
1487
        // In case of bcrypt let php's password functionality do it's magic
1488
        if ($hashAlgorithm === APIUser::PASSWORD_HASH_BCRYPT ||
1489
            $hashAlgorithm === APIUser::PASSWORD_HASH_PHP_DEFAULT
1490
        ) {
1491
            return password_verify($plainPassword, $passwordHash);
1492
        }
1493
1494
        // Randomize login time to protect against timing attacks
1495
        usleep(random_int(0, 30000));
1496
1497
        return $passwordHash === $this->createPasswordHash(
1498
            $login,
1499
            $plainPassword,
1500
            $this->settings['siteName'],
1501
            $hashAlgorithm
1502
        );
1503
    }
1504
1505
    /**
1506
     * Returns password hash based on user data and site settings.
1507
     *
1508
     * @param string $login User login
1509
     * @param string $password User password
1510
     * @param string $site The name of the site
1511
     * @param int $type Type of password to generate
1512
     *
1513
     * @return string Generated password hash
1514
     *
1515
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the type is not recognized
1516
     */
1517
    protected function createPasswordHash($login, $password, $site, $type)
1518
    {
1519
        $deprecationWarningFormat = 'Password hash type %s is deprecated since 6.13.';
1520
1521
        switch ($type) {
1522
            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...
1523
                @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...
1524
1525
                return md5($password);
1526
1527
            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...
1528
                @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...
1529
1530
                return md5("$login\n$password");
1531
1532
            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...
1533
                @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...
1534
1535
                return md5("$login\n$password\n$site");
1536
1537
            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...
1538
                @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...
1539
1540
                return $password;
1541
1542
            case APIUser::PASSWORD_HASH_BCRYPT:
1543
                return password_hash($password, PASSWORD_BCRYPT);
1544
1545
            case APIUser::PASSWORD_HASH_PHP_DEFAULT:
1546
                return password_hash($password, PASSWORD_DEFAULT);
1547
1548
            default:
1549
                throw new InvalidArgumentException('type', "Password hash type '$type' is not recognized");
1550
        }
1551
    }
1552
1553
    /**
1554
     * Return true if any of the UserUpdateStruct properties refers to User Profile (Content) update.
1555
     *
1556
     * @param UserUpdateStruct $userUpdateStruct
1557
     *
1558
     * @return bool
1559
     */
1560
    private function isUserProfileUpdateRequested(UserUpdateStruct $userUpdateStruct)
1561
    {
1562
        return
1563
            !empty($userUpdateStruct->contentUpdateStruct) ||
1564
            !empty($userUpdateStruct->contentMetadataUpdateStruct) ||
1565
            !empty($userUpdateStruct->email) ||
1566
            !empty($userUpdateStruct->enabled) ||
1567
            !empty($userUpdateStruct->maxLogin);
1568
    }
1569
1570
    private function getDateTime(?int $timestamp): ?DateTimeInterface
1571
    {
1572
        if ($timestamp !== null) {
1573
            // Instead of using DateTime(ts) we use setTimeStamp() so timezone does not get set to UTC
1574
            $dateTime = new DateTime();
1575
            $dateTime->setTimestamp($timestamp);
1576
1577
            return DateTimeImmutable::createFromMutable($dateTime);
1578
        }
1579
1580
        return null;
1581
    }
1582
}
1583