Completed
Push — master ( 8fa866...f2aec6 )
by André
44:30 queued 23:23
created

UserService::isUserGroup()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
208
            $subUserGroups[] = $this->buildDomainUserGroupObject(
209
                $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...
210
                    $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...
211
                    $prioritizedLanguages
212
                )
213
            );
214
        }
215
216
        return $subUserGroups;
217
    }
218
219
    /**
220
     * Returns (searches) subgroups of a user group described by its main location.
221
     *
222
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
223
     * @param int $offset
224
     * @param int $limit
225
     *
226
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
227
     */
228
    protected function searchSubGroups(Location $location, $offset = 0, $limit = 25)
229
    {
230
        $searchQuery = new LocationQuery();
231
232
        $searchQuery->offset = $offset;
233
        $searchQuery->limit = $limit;
234
235
        $searchQuery->filter = new CriterionLogicalAnd([
236
            new CriterionContentTypeId($this->settings['userGroupClassID']),
237
            new CriterionParentLocationId($location->id),
238
        ]);
239
240
        $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...
241
242
        return $this->repository->getSearchService()->findLocations($searchQuery, array(), false);
243
    }
244
245
    /**
246
     * Removes a user group.
247
     *
248
     * the users which are not assigned to other groups will be deleted.
249
     *
250
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
251
     *
252
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
253
     */
254 View Code Duplication
    public function deleteUserGroup(APIUserGroup $userGroup)
255
    {
256
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
257
258
        $this->repository->beginTransaction();
259
        try {
260
            //@todo: what happens to sub user groups and users below sub user groups
261
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUserGroup->getVersionInfo()->getContentInfo());
262
            $this->repository->commit();
263
        } catch (Exception $e) {
264
            $this->repository->rollback();
265
            throw $e;
266
        }
267
268
        return $affectedLocationIds;
269
    }
270
271
    /**
272
     * Moves the user group to another parent.
273
     *
274
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
275
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $newParent
276
     *
277
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
278
     */
279
    public function moveUserGroup(APIUserGroup $userGroup, APIUserGroup $newParent)
280
    {
281
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
282
        $loadedNewParent = $this->loadUserGroup($newParent->id);
283
284
        $locationService = $this->repository->getLocationService();
285
286
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
287
            throw new BadStateException('userGroup', 'existing user group is not stored and/or does not have any location yet');
288
        }
289
290
        if ($loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId === null) {
291
            throw new BadStateException('newParent', 'new user group is not stored and/or does not have any location yet');
292
        }
293
294
        $userGroupMainLocation = $locationService->loadLocation(
295
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
296
        );
297
        $newParentMainLocation = $locationService->loadLocation(
298
            $loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId
299
        );
300
301
        $this->repository->beginTransaction();
302
        try {
303
            $locationService->moveSubtree($userGroupMainLocation, $newParentMainLocation);
304
            $this->repository->commit();
305
        } catch (Exception $e) {
306
            $this->repository->rollback();
307
            throw $e;
308
        }
309
    }
310
311
    /**
312
     * Updates the group profile with fields and meta data.
313
     *
314
     * 4.x: If the versionUpdateStruct is set in $userGroupUpdateStruct, this method internally creates a content draft, updates ts with the provided data
315
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
316
     *
317
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
318
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct $userGroupUpdateStruct
319
     *
320
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
321
     *
322
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user group
323
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupUpdateStruct is not valid
324
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
325
     */
326
    public function updateUserGroup(APIUserGroup $userGroup, UserGroupUpdateStruct $userGroupUpdateStruct)
327
    {
328
        if ($userGroupUpdateStruct->contentUpdateStruct === null &&
329
            $userGroupUpdateStruct->contentMetadataUpdateStruct === null) {
330
            // both update structs are empty, nothing to do
331
            return $userGroup;
332
        }
333
334
        $contentService = $this->repository->getContentService();
335
336
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
337
338
        $this->repository->beginTransaction();
339
        try {
340
            $publishedContent = $loadedUserGroup;
341 View Code Duplication
            if ($userGroupUpdateStruct->contentUpdateStruct !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
342
                $contentDraft = $contentService->createContentDraft($loadedUserGroup->getVersionInfo()->getContentInfo());
343
344
                $contentDraft = $contentService->updateContent(
345
                    $contentDraft->getVersionInfo(),
346
                    $userGroupUpdateStruct->contentUpdateStruct
347
                );
348
349
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
350
            }
351
352
            if ($userGroupUpdateStruct->contentMetadataUpdateStruct !== null) {
353
                $publishedContent = $contentService->updateContentMetadata(
354
                    $publishedContent->getVersionInfo()->getContentInfo(),
355
                    $userGroupUpdateStruct->contentMetadataUpdateStruct
356
                );
357
            }
358
359
            $this->repository->commit();
360
        } catch (Exception $e) {
361
            $this->repository->rollback();
362
            throw $e;
363
        }
364
365
        return $this->buildDomainUserGroupObject($publishedContent);
366
    }
367
368
    /**
369
     * Create a new user. The created user is published by this method.
370
     *
371
     * @param \eZ\Publish\API\Repository\Values\User\UserCreateStruct $userCreateStruct the data used for creating the user
372
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup[] $parentGroups the groups which are assigned to the user after creation
373
     *
374
     * @return \eZ\Publish\API\Repository\Values\User\User
375
     *
376
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
377
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userCreateStruct is not valid
378
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
379
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a user with provided login already exists
380
     */
381
    public function createUser(APIUserCreateStruct $userCreateStruct, array $parentGroups)
382
    {
383
        if (empty($parentGroups)) {
384
            throw new InvalidArgumentValue('parentGroups', $parentGroups);
385
        }
386
387
        if (!is_string($userCreateStruct->login) || empty($userCreateStruct->login)) {
388
            throw new InvalidArgumentValue('login', $userCreateStruct->login, 'UserCreateStruct');
389
        }
390
391 View Code Duplication
        if (!is_string($userCreateStruct->email) || empty($userCreateStruct->email)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
392
            throw new InvalidArgumentValue('email', $userCreateStruct->email, 'UserCreateStruct');
393
        }
394
395
        if (!preg_match('/^.+@.+\..+$/', $userCreateStruct->email)) {
396
            throw new InvalidArgumentValue('email', $userCreateStruct->email, 'UserCreateStruct');
397
        }
398
399
        if (!is_string($userCreateStruct->password) || empty($userCreateStruct->password)) {
400
            throw new InvalidArgumentValue('password', $userCreateStruct->password, 'UserCreateStruct');
401
        }
402
403
        if (!is_bool($userCreateStruct->enabled)) {
404
            throw new InvalidArgumentValue('enabled', $userCreateStruct->enabled, 'UserCreateStruct');
405
        }
406
407
        try {
408
            $this->userHandler->loadByLogin($userCreateStruct->login);
409
            throw new InvalidArgumentException('userCreateStruct', 'User with provided login already exists');
410
        } catch (NotFoundException $e) {
411
            // Do nothing
412
        }
413
414
        $contentService = $this->repository->getContentService();
415
        $locationService = $this->repository->getLocationService();
416
        $contentTypeService = $this->repository->getContentTypeService();
417
418
        if ($userCreateStruct->contentType === null) {
419
            $userCreateStruct->contentType = $contentTypeService->loadContentType($this->settings['userClassID']);
420
        }
421
422
        $errors = $this->validatePassword($userCreateStruct->password, new PasswordValidationContext([
423
            'contentType' => $userCreateStruct->contentType,
424
        ]));
425
        if (!empty($errors)) {
426
            throw new UserPasswordValidationException('password', $errors);
427
        }
428
429
        $locationCreateStructs = array();
430
        foreach ($parentGroups as $parentGroup) {
431
            $parentGroup = $this->loadUserGroup($parentGroup->id);
432
            if ($parentGroup->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
433
                $locationCreateStructs[] = $locationService->newLocationCreateStruct(
434
                    $parentGroup->getVersionInfo()->getContentInfo()->mainLocationId
435
                );
436
            }
437
        }
438
439
        // Search for the first ezuser field type in content type
440
        $userFieldDefinition = null;
441
        foreach ($userCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
442
            if ($fieldDefinition->fieldTypeIdentifier == 'ezuser') {
443
                $userFieldDefinition = $fieldDefinition;
444
                break;
445
            }
446
        }
447
448
        if ($userFieldDefinition === null) {
449
            throw new ContentValidationException('Provided content type does not contain ezuser field type');
450
        }
451
452
        $fixUserFieldType = true;
453
        foreach ($userCreateStruct->fields as $index => $field) {
454
            if ($field->fieldDefIdentifier == $userFieldDefinition->identifier) {
455
                if ($field->value instanceof UserValue) {
456
                    $userCreateStruct->fields[$index]->value->login = $userCreateStruct->login;
457
                } else {
458
                    $userCreateStruct->fields[$index]->value = new UserValue(
459
                        array(
460
                            'login' => $userCreateStruct->login,
461
                        )
462
                    );
463
                }
464
465
                $fixUserFieldType = false;
466
            }
467
        }
468
469
        if ($fixUserFieldType) {
470
            $userCreateStruct->setField(
471
                $userFieldDefinition->identifier,
472
                new UserValue(
473
                    array(
474
                        'login' => $userCreateStruct->login,
475
                    )
476
                )
477
            );
478
        }
479
480
        $this->repository->beginTransaction();
481
        try {
482
            $contentDraft = $contentService->createContent($userCreateStruct, $locationCreateStructs);
483
            // Create user before publishing, so that external data can be returned
484
            $spiUser = $this->userHandler->create(
485
                new SPIUser(
486
                    array(
487
                        'id' => $contentDraft->id,
488
                        'login' => $userCreateStruct->login,
489
                        'email' => $userCreateStruct->email,
490
                        'passwordHash' => $this->createPasswordHash(
491
                            $userCreateStruct->login,
492
                            $userCreateStruct->password,
493
                            $this->settings['siteName'],
494
                            $this->settings['hashType']
495
                        ),
496
                        'hashAlgorithm' => $this->settings['hashType'],
497
                        'isEnabled' => $userCreateStruct->enabled,
498
                        'maxLogin' => 0,
499
                    )
500
                )
501
            );
502
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
503
504
            $this->repository->commit();
505
        } catch (Exception $e) {
506
            $this->repository->rollback();
507
            throw $e;
508
        }
509
510
        return $this->buildDomainUserObject($spiUser, $publishedContent);
511
    }
512
513
    /**
514
     * Loads a user.
515
     *
516
     * @param mixed $userId
517
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
518
     *
519
     * @return \eZ\Publish\API\Repository\Values\User\User
520
     *
521
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given id was not found
522
     */
523
    public function loadUser($userId, array $prioritizedLanguages = [])
524
    {
525
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
526
        $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...
527
        // Get spiUser value from Field Value
528
        foreach ($content->getFields() as $field) {
529
            if (!$field->value instanceof UserValue) {
530
                continue;
531
            }
532
533
            /** @var \eZ\Publish\Core\FieldType\User\Value $value */
534
            $value = $field->value;
535
            $spiUser = new SPIUser();
536
            $spiUser->id = $value->contentId;
537
            $spiUser->login = $value->login;
538
            $spiUser->email = $value->email;
539
            $spiUser->hashAlgorithm = $value->passwordHashType;
540
            $spiUser->passwordHash = $value->passwordHash;
541
            $spiUser->isEnabled = $value->enabled;
542
            $spiUser->maxLogin = $value->maxLogin;
543
            break;
544
        }
545
546
        // If for some reason not found, load it
547
        if (!isset($spiUser)) {
548
            $spiUser = $this->userHandler->load($userId);
549
        }
550
551
        return $this->buildDomainUserObject($spiUser, $content);
552
    }
553
554
    /**
555
     * Loads anonymous user.
556
     *
557
     * @deprecated since 5.3, use loadUser( $anonymousUserId ) instead
558
     *
559
     * @uses ::loadUser()
560
     *
561
     * @return \eZ\Publish\API\Repository\Values\User\User
562
     */
563
    public function loadAnonymousUser()
564
    {
565
        return $this->loadUser($this->settings['anonymousUserID']);
566
    }
567
568
    /**
569
     * Loads a user for the given login and password.
570
     *
571
     * If the password hash type differs from that configured for the service, it will be updated to the configured one.
572
     *
573
     * {@inheritdoc}
574
     *
575
     * @param string $login
576
     * @param string $password the plain password
577
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
578
     *
579
     * @return \eZ\Publish\API\Repository\Values\User\User
580
     *
581
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if credentials are invalid
582
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
583
     */
584
    public function loadUserByCredentials($login, $password, array $prioritizedLanguages = [])
585
    {
586
        if (!is_string($login) || empty($login)) {
587
            throw new InvalidArgumentValue('login', $login);
588
        }
589
590
        if (!is_string($password)) {
591
            throw new InvalidArgumentValue('password', $password);
592
        }
593
594
        $spiUser = $this->userHandler->loadByLogin($login);
595
        if (!$this->verifyPassword($login, $password, $spiUser)) {
596
            throw new NotFoundException('user', $login);
597
        }
598
599
        // Don't catch BadStateException, on purpose, to avoid broken hashes.
600
        $this->updatePasswordHash($login, $password, $spiUser);
601
602
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
603
    }
604
605
    /**
606
     * Update password hash to the type configured for the service, if they differ.
607
     *
608
     * @param string $login User login
609
     * @param string $password User password
610
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
611
     *
612
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException if the password is not correctly saved, in which case the update is reverted
613
     */
614
    private function updatePasswordHash($login, $password, SPIUser $spiUser)
615
    {
616
        if ($spiUser->hashAlgorithm === $this->settings['hashType']) {
617
            return;
618
        }
619
620
        $spiUser->passwordHash = $this->createPasswordHash($login, $password, null, $this->settings['hashType']);
621
        $spiUser->hashAlgorithm = $this->settings['hashType'];
622
623
        $this->repository->beginTransaction();
624
        $this->userHandler->update($spiUser);
625
        $reloadedSpiUser = $this->userHandler->load($spiUser->id);
626
627
        if ($reloadedSpiUser->passwordHash === $spiUser->passwordHash) {
628
            $this->repository->commit();
629
        } else {
630
            // Password hash was not correctly saved, possible cause: EZP-28692
631
            $this->repository->rollback();
632
            if (isset($this->logger)) {
633
                $this->logger->critical('Password hash could not be updated. Please verify that your database schema is up to date.');
634
            }
635
636
            throw new BadStateException(
637
                'user',
638
                'Could not save updated password hash, reverting to previous hash. Please verify that your database schema is up to date.'
639
            );
640
        }
641
    }
642
643
    /**
644
     * Loads a user for the given login.
645
     *
646
     * {@inheritdoc}
647
     *
648
     * @param string $login
649
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
650
     *
651
     * @return \eZ\Publish\API\Repository\Values\User\User
652
     *
653
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
654
     */
655 View Code Duplication
    public function loadUserByLogin($login, array $prioritizedLanguages = [])
656
    {
657
        if (!is_string($login) || empty($login)) {
658
            throw new InvalidArgumentValue('login', $login);
659
        }
660
661
        $spiUser = $this->userHandler->loadByLogin($login);
662
663
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
664
    }
665
666
    /**
667
     * Loads a user for the given email.
668
     *
669
     * {@inheritdoc}
670
     *
671
     * @param string $email
672
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
673
     *
674
     * @return \eZ\Publish\API\Repository\Values\User\User[]
675
     */
676
    public function loadUsersByEmail($email, array $prioritizedLanguages = [])
677
    {
678
        if (!is_string($email) || empty($email)) {
679
            throw new InvalidArgumentValue('email', $email);
680
        }
681
682
        $users = array();
683
        foreach ($this->userHandler->loadByEmail($email) as $spiUser) {
684
            $users[] = $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
685
        }
686
687
        return $users;
688
    }
689
690
    /**
691
     * Loads a user for the given token.
692
     *
693
     * {@inheritdoc}
694
     *
695
     * @param string $hash
696
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
697
     *
698
     * @return \eZ\Publish\API\Repository\Values\User\User
699
     *
700
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
701
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
702
     */
703 View Code Duplication
    public function loadUserByToken($hash, array $prioritizedLanguages = [])
704
    {
705
        if (!is_string($hash) || empty($hash)) {
706
            throw new InvalidArgumentValue('hash', $hash);
707
        }
708
709
        $spiUser = $this->userHandler->loadUserByToken($hash);
710
711
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
712
    }
713
714
    /**
715
     * This method deletes a user.
716
     *
717
     * @param \eZ\Publish\API\Repository\Values\User\User $user
718
     *
719
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to delete the user
720
     */
721 View Code Duplication
    public function deleteUser(APIUser $user)
722
    {
723
        $loadedUser = $this->loadUser($user->id);
724
725
        $this->repository->beginTransaction();
726
        try {
727
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo());
728
            $this->userHandler->delete($loadedUser->id);
729
            $this->repository->commit();
730
        } catch (Exception $e) {
731
            $this->repository->rollback();
732
            throw $e;
733
        }
734
735
        return $affectedLocationIds;
736
    }
737
738
    /**
739
     * Updates a user.
740
     *
741
     * 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
742
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
743
     *
744
     * @param \eZ\Publish\API\Repository\Values\User\User $user
745
     * @param \eZ\Publish\API\Repository\Values\User\UserUpdateStruct $userUpdateStruct
746
     *
747
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user
748
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userUpdateStruct is not valid
749
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
750
     *
751
     * @return \eZ\Publish\API\Repository\Values\User\User
752
     */
753
    public function updateUser(APIUser $user, UserUpdateStruct $userUpdateStruct)
754
    {
755
        $loadedUser = $this->loadUser($user->id);
756
757
        // We need to determine if we have anything to update.
758
        // UserUpdateStruct is specific as some of the new content is in
759
        // content update struct and some of it is in additional fields like
760
        // email, password and so on
761
        $doUpdate = false;
762
        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...
763
            if ($propertyValue !== null) {
764
                $doUpdate = true;
765
                break;
766
            }
767
        }
768
769
        if (!$doUpdate) {
770
            // Nothing to update, so we just quit
771
            return $user;
772
        }
773
774
        if ($userUpdateStruct->email !== null) {
775 View Code Duplication
            if (!is_string($userUpdateStruct->email) || empty($userUpdateStruct->email)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
776
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
777
            }
778
779
            if (!preg_match('/^.+@.+\..+$/', $userUpdateStruct->email)) {
780
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
781
            }
782
        }
783
784
        if ($userUpdateStruct->enabled !== null && !is_bool($userUpdateStruct->enabled)) {
785
            throw new InvalidArgumentValue('enabled', $userUpdateStruct->enabled, 'UserUpdateStruct');
786
        }
787
788
        if ($userUpdateStruct->maxLogin !== null && !is_int($userUpdateStruct->maxLogin)) {
789
            throw new InvalidArgumentValue('maxLogin', $userUpdateStruct->maxLogin, 'UserUpdateStruct');
790
        }
791
792
        if ($userUpdateStruct->password !== null) {
793
            if (!is_string($userUpdateStruct->password) || empty($userUpdateStruct->password)) {
794
                throw new InvalidArgumentValue('password', $userUpdateStruct->password, 'UserUpdateStruct');
795
            }
796
797
            $userContentType = $this->repository->getContentTypeService()->loadContentType(
798
                $user->contentInfo->contentTypeId
799
            );
800
801
            $errors = $this->validatePassword($userUpdateStruct->password, new PasswordValidationContext([
802
                'contentType' => $userContentType,
803
                'user' => $user,
804
            ]));
805
806
            if (!empty($errors)) {
807
                throw new UserPasswordValidationException('password', $errors);
808
            }
809
        }
810
811
        $contentService = $this->repository->getContentService();
812
813
        $canEditContent = $this->permissionResolver->canUser('content', 'edit', $loadedUser);
814
815
        if (!$canEditContent && $this->isUserProfileUpdateRequested($userUpdateStruct)) {
816
            throw new UnauthorizedException('content', 'edit');
817
        }
818
819
        if (!empty($userUpdateStruct->password) &&
820
            !$canEditContent &&
821
            !$this->permissionResolver->canUser('user', 'password', $loadedUser)
822
        ) {
823
            throw new UnauthorizedException('user', 'password');
824
        }
825
826
        $this->repository->beginTransaction();
827
        try {
828
            $publishedContent = $loadedUser;
829 View Code Duplication
            if ($userUpdateStruct->contentUpdateStruct !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
830
                $contentDraft = $contentService->createContentDraft($loadedUser->getVersionInfo()->getContentInfo());
831
                $contentDraft = $contentService->updateContent(
832
                    $contentDraft->getVersionInfo(),
833
                    $userUpdateStruct->contentUpdateStruct
834
                );
835
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
836
            }
837
838
            if ($userUpdateStruct->contentMetadataUpdateStruct !== null) {
839
                $contentService->updateContentMetadata(
840
                    $publishedContent->getVersionInfo()->getContentInfo(),
841
                    $userUpdateStruct->contentMetadataUpdateStruct
842
                );
843
            }
844
845
            $this->userHandler->update(
846
                new SPIUser(
847
                    array(
848
                        'id' => $loadedUser->id,
849
                        'login' => $loadedUser->login,
850
                        'email' => $userUpdateStruct->email ?: $loadedUser->email,
851
                        'passwordHash' => $userUpdateStruct->password ?
852
                            $this->createPasswordHash(
853
                                $loadedUser->login,
854
                                $userUpdateStruct->password,
855
                                $this->settings['siteName'],
856
                                $this->settings['hashType']
857
                            ) :
858
                            $loadedUser->passwordHash,
859
                        'hashAlgorithm' => $userUpdateStruct->password ?
860
                            $this->settings['hashType'] :
861
                            $loadedUser->hashAlgorithm,
862
                        'isEnabled' => $userUpdateStruct->enabled !== null ? $userUpdateStruct->enabled : $loadedUser->enabled,
863
                        'maxLogin' => $userUpdateStruct->maxLogin !== null ? (int)$userUpdateStruct->maxLogin : $loadedUser->maxLogin,
864
                    )
865
                )
866
            );
867
868
            $this->repository->commit();
869
        } catch (Exception $e) {
870
            $this->repository->rollback();
871
            throw $e;
872
        }
873
874
        return $this->loadUser($loadedUser->id);
875
    }
876
877
    /**
878
     * Update the user token information specified by the user token struct.
879
     *
880
     * @param \eZ\Publish\API\Repository\Values\User\User $user
881
     * @param \eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct $userTokenUpdateStruct
882
     *
883
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue
884
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
885
     * @throws \RuntimeException
886
     * @throws \Exception
887
     *
888
     * @return \eZ\Publish\API\Repository\Values\User\User
889
     */
890
    public function updateUserToken(APIUser $user, UserTokenUpdateStruct $userTokenUpdateStruct)
891
    {
892
        $loadedUser = $this->loadUser($user->id);
893
894 View Code Duplication
        if ($userTokenUpdateStruct->hashKey !== null && (!is_string($userTokenUpdateStruct->hashKey) || empty($userTokenUpdateStruct->hashKey))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
895
            throw new InvalidArgumentValue('hashKey', $userTokenUpdateStruct->hashKey, 'UserTokenUpdateStruct');
896
        }
897
898
        if ($userTokenUpdateStruct->time === null) {
899
            throw new InvalidArgumentValue('time', $userTokenUpdateStruct->time, 'UserTokenUpdateStruct');
900
        }
901
902
        $this->repository->beginTransaction();
903
        try {
904
            $this->userHandler->updateUserToken(
905
                new SPIUserTokenUpdateStruct(
906
                    array(
907
                        'userId' => $loadedUser->id,
908
                        'hashKey' => $userTokenUpdateStruct->hashKey,
909
                        'time' => $userTokenUpdateStruct->time->getTimestamp(),
910
                    )
911
                )
912
            );
913
            $this->repository->commit();
914
        } catch (Exception $e) {
915
            $this->repository->rollback();
916
            throw $e;
917
        }
918
919
        return $this->loadUser($loadedUser->id);
920
    }
921
922
    /**
923
     * Expires user token with user hash.
924
     *
925
     * @param string $hash
926
     */
927
    public function expireUserToken($hash)
928
    {
929
        $this->repository->beginTransaction();
930
        try {
931
            $this->userHandler->expireUserToken($hash);
932
            $this->repository->commit();
933
        } catch (Exception $e) {
934
            $this->repository->rollback();
935
            throw $e;
936
        }
937
    }
938
939
    /**
940
     * Assigns a new user group to the user.
941
     *
942
     * @param \eZ\Publish\API\Repository\Values\User\User $user
943
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
944
     *
945
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to assign the user group to the user
946
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is already in the given user group
947
     */
948
    public function assignUserToUserGroup(APIUser $user, APIUserGroup $userGroup)
949
    {
950
        $loadedUser = $this->loadUser($user->id);
951
        $loadedGroup = $this->loadUserGroup($userGroup->id);
952
        $locationService = $this->repository->getLocationService();
953
954
        $existingGroupIds = array();
955
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
956
        foreach ($userLocations as $userLocation) {
957
            $existingGroupIds[] = $userLocation->parentLocationId;
958
        }
959
960
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
961
            throw new BadStateException('userGroup', 'user group has no main location or no locations');
962
        }
963
964
        if (in_array($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId, $existingGroupIds)) {
965
            // user is already assigned to the user group
966
            throw new InvalidArgumentException('user', 'user is already in the given user group');
967
        }
968
969
        $locationCreateStruct = $locationService->newLocationCreateStruct(
970
            $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId
971
        );
972
973
        $this->repository->beginTransaction();
974
        try {
975
            $locationService->createLocation(
976
                $loadedUser->getVersionInfo()->getContentInfo(),
977
                $locationCreateStruct
978
            );
979
            $this->repository->commit();
980
        } catch (Exception $e) {
981
            $this->repository->rollback();
982
            throw $e;
983
        }
984
    }
985
986
    /**
987
     * Removes a user group from the user.
988
     *
989
     * @param \eZ\Publish\API\Repository\Values\User\User $user
990
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
991
     *
992
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to remove the user group from the user
993
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is not in the given user group
994
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $userGroup is the last assigned user group
995
     */
996
    public function unAssignUserFromUserGroup(APIUser $user, APIUserGroup $userGroup)
997
    {
998
        $loadedUser = $this->loadUser($user->id);
999
        $loadedGroup = $this->loadUserGroup($userGroup->id);
1000
        $locationService = $this->repository->getLocationService();
1001
1002
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
1003
        if (empty($userLocations)) {
1004
            throw new BadStateException('user', 'user has no locations, cannot unassign from group');
1005
        }
1006
1007
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1008
            throw new BadStateException('userGroup', 'user group has no main location or no locations, cannot unassign');
1009
        }
1010
1011
        foreach ($userLocations as $userLocation) {
1012
            if ($userLocation->parentLocationId == $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId) {
1013
                // Throw this specific BadState when we know argument is valid
1014
                if (count($userLocations) === 1) {
1015
                    throw new BadStateException('user', 'user only has one user group, cannot unassign from last group');
1016
                }
1017
1018
                $this->repository->beginTransaction();
1019
                try {
1020
                    $locationService->deleteLocation($userLocation);
1021
                    $this->repository->commit();
1022
1023
                    return;
1024
                } catch (Exception $e) {
1025
                    $this->repository->rollback();
1026
                    throw $e;
1027
                }
1028
            }
1029
        }
1030
1031
        throw new InvalidArgumentException('userGroup', 'user is not in the given user group');
1032
    }
1033
1034
    /**
1035
     * Loads the user groups the user belongs to.
1036
     *
1037
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed read the user or user group
1038
     *
1039
     * @param \eZ\Publish\API\Repository\Values\User\User $user
1040
     * @param int $offset the start offset for paging
1041
     * @param int $limit the number of user groups returned
1042
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1043
     *
1044
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
1045
     */
1046
    public function loadUserGroupsOfUser(APIUser $user, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
1047
    {
1048
        $locationService = $this->repository->getLocationService();
1049
1050
        if (!$this->repository->getPermissionResolver()->canUser('content', 'read', $user)) {
1051
            throw new UnauthorizedException('content', 'read');
1052
        }
1053
1054
        $userLocations = $locationService->loadLocations(
1055
            $user->getVersionInfo()->getContentInfo()
1056
        );
1057
1058
        $parentLocationIds = array();
1059
        foreach ($userLocations as $userLocation) {
1060
            if ($userLocation->parentLocationId !== null) {
1061
                $parentLocationIds[] = $userLocation->parentLocationId;
1062
            }
1063
        }
1064
1065
        $searchQuery = new LocationQuery();
1066
1067
        $searchQuery->offset = $offset;
1068
        $searchQuery->limit = $limit;
1069
        $searchQuery->performCount = false;
1070
1071
        $searchQuery->filter = new CriterionLogicalAnd(
1072
            [
1073
                new CriterionContentTypeId($this->settings['userGroupClassID']),
1074
                new CriterionLocationId($parentLocationIds),
1075
            ]
1076
        );
1077
1078
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1079
1080
        $userGroups = [];
1081 View Code Duplication
        foreach ($searchResult->searchHits as $resultItem) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1082
            $userGroups[] = $this->buildDomainUserGroupObject(
1083
                $this->repository->getContentService()->internalLoadContent(
0 ignored issues
show
Bug introduced by
The method internalLoadContent() does not exist on eZ\Publish\API\Repository\ContentService. Did you maybe mean loadContent()?

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1085
                    $prioritizedLanguages
1086
                )
1087
            );
1088
        }
1089
1090
        return $userGroups;
1091
    }
1092
1093
    /**
1094
     * Loads the users of a user group.
1095
     *
1096
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the users or user group
1097
     *
1098
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
1099
     * @param int $offset the start offset for paging
1100
     * @param int $limit the number of users returned
1101
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1102
     *
1103
     * @return \eZ\Publish\API\Repository\Values\User\User[]
1104
     */
1105
    public function loadUsersOfUserGroup(
1106
        APIUserGroup $userGroup,
1107
        $offset = 0,
1108
        $limit = 25,
1109
        array $prioritizedLanguages = []
1110
    ) {
1111
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
1112
1113
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
1114
            return [];
1115
        }
1116
1117
        $mainGroupLocation = $this->repository->getLocationService()->loadLocation(
1118
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
1119
        );
1120
1121
        $searchQuery = new LocationQuery();
1122
1123
        $searchQuery->filter = new CriterionLogicalAnd(
1124
            [
1125
                new CriterionContentTypeId($this->settings['userClassID']),
1126
                new CriterionParentLocationId($mainGroupLocation->id),
1127
            ]
1128
        );
1129
1130
        $searchQuery->offset = $offset;
1131
        $searchQuery->limit = $limit;
1132
        $searchQuery->performCount = false;
1133
        $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...
1134
1135
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1136
1137
        $users = [];
1138
        foreach ($searchResult->searchHits as $resultItem) {
1139
            $users[] = $this->buildDomainUserObject(
1140
                $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...
1141
                $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...
1142
                    $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...
1143
                    $prioritizedLanguages
1144
                )
1145
            );
1146
        }
1147
1148
        return $users;
1149
    }
1150
1151
    /**
1152
     * {@inheritdoc}
1153
     */
1154
    public function isUser(APIContent $content): bool
1155
    {
1156
        // First check against config for fast check
1157
        if ($this->settings['userClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId) {
1158
            return true;
1159
        }
1160
1161
        // For users we ultimately need to look for ezuser type as content type id could be several for users.
1162
        // And config might be different from one SA to the next, which we don't care about here.
1163
        foreach ($content->getFields() as $field) {
1164
            if ($field->fieldTypeIdentifier === 'ezuser') {
1165
                return true;
1166
            }
1167
        }
1168
1169
        return false;
1170
    }
1171
1172
    /**
1173
     * {@inheritdoc}
1174
     */
1175
    public function isUserGroup(APIContent $content): bool
1176
    {
1177
        return $this->settings['userGroupClassID'] == $content->getVersionInfo()->getContentInfo()->contentTypeId;
1178
    }
1179
1180
    /**
1181
     * Instantiate a user create class.
1182
     *
1183
     * @param string $login the login of the new user
1184
     * @param string $email the email of the new user
1185
     * @param string $password the plain password of the new user
1186
     * @param string $mainLanguageCode the main language for the underlying content object
1187
     * @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
1188
     *
1189
     * @return \eZ\Publish\API\Repository\Values\User\UserCreateStruct
1190
     */
1191
    public function newUserCreateStruct($login, $email, $password, $mainLanguageCode, $contentType = null)
1192
    {
1193
        if ($contentType === null) {
1194
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1195
                $this->settings['userClassID']
1196
            );
1197
        }
1198
1199
        return new UserCreateStruct(
1200
            array(
1201
                'contentType' => $contentType,
1202
                'mainLanguageCode' => $mainLanguageCode,
1203
                'login' => $login,
1204
                'email' => $email,
1205
                'password' => $password,
1206
                'enabled' => true,
1207
                'fields' => array(),
1208
            )
1209
        );
1210
    }
1211
1212
    /**
1213
     * Instantiate a user group create class.
1214
     *
1215
     * @param string $mainLanguageCode The main language for the underlying content object
1216
     * @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
1217
     *
1218
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct
1219
     */
1220
    public function newUserGroupCreateStruct($mainLanguageCode, $contentType = null)
1221
    {
1222
        if ($contentType === null) {
1223
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1224
                $this->settings['userGroupClassID']
1225
            );
1226
        }
1227
1228
        return new UserGroupCreateStruct(
1229
            array(
1230
                'contentType' => $contentType,
1231
                'mainLanguageCode' => $mainLanguageCode,
1232
                'fields' => array(),
1233
            )
1234
        );
1235
    }
1236
1237
    /**
1238
     * Instantiate a new user update struct.
1239
     *
1240
     * @return \eZ\Publish\API\Repository\Values\User\UserUpdateStruct
1241
     */
1242
    public function newUserUpdateStruct()
1243
    {
1244
        return new UserUpdateStruct();
1245
    }
1246
1247
    /**
1248
     * Instantiate a new user group update struct.
1249
     *
1250
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
1251
     */
1252
    public function newUserGroupUpdateStruct()
1253
    {
1254
        return new UserGroupUpdateStruct();
1255
    }
1256
1257
    /**
1258
     * {@inheritdoc}
1259
     */
1260
    public function validatePassword(string $password, PasswordValidationContext $context = null): array
1261
    {
1262
        if ($context === null) {
1263
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1264
                $this->settings['userClassID']
1265
            );
1266
1267
            $context = new PasswordValidationContext([
1268
                'contentType' => $contentType,
1269
            ]);
1270
        }
1271
1272
        // Search for the first ezuser field type in content type
1273
        $userFieldDefinition = null;
1274
        foreach ($context->contentType->getFieldDefinitions() as $fieldDefinition) {
1275
            if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') {
1276
                $userFieldDefinition = $fieldDefinition;
1277
                break;
1278
            }
1279
        }
1280
1281
        if ($userFieldDefinition === null) {
1282
            throw new ContentValidationException('Provided content type does not contain ezuser field type');
1283
        }
1284
1285
        $configuration = $userFieldDefinition->getValidatorConfiguration();
1286
        if (!isset($configuration['PasswordValueValidator'])) {
1287
            return [];
1288
        }
1289
1290
        return (new UserPasswordValidator($configuration['PasswordValueValidator']))->validate($password);
1291
    }
1292
1293
    /**
1294
     * Builds the domain UserGroup object from provided Content object.
1295
     *
1296
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1297
     *
1298
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
1299
     */
1300
    protected function buildDomainUserGroupObject(APIContent $content)
1301
    {
1302
        $locationService = $this->repository->getLocationService();
1303
1304
        if ($content->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
1305
            $mainLocation = $locationService->loadLocation(
1306
                $content->getVersionInfo()->getContentInfo()->mainLocationId
1307
            );
1308
            $parentLocation = $locationService->loadLocation($mainLocation->parentLocationId);
1309
        }
1310
1311
        return new UserGroup(
1312
            array(
1313
                'content' => $content,
1314
                'parentId' => isset($parentLocation) ? $parentLocation->contentId : null,
1315
            )
1316
        );
1317
    }
1318
1319
    /**
1320
     * Builds the domain user object from provided persistence user object.
1321
     *
1322
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
1323
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
1324
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1325
     *
1326
     * @return \eZ\Publish\API\Repository\Values\User\User
1327
     */
1328
    protected function buildDomainUserObject(
1329
        SPIUser $spiUser,
1330
        APIContent $content = null,
1331
        array $prioritizedLanguages = []
1332
    ) {
1333
        if ($content === null) {
1334
            $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...
1335
                $spiUser->id,
1336
                $prioritizedLanguages
1337
            );
1338
        }
1339
1340
        return new User(
1341
            array(
1342
                'content' => $content,
1343
                'login' => $spiUser->login,
1344
                'email' => $spiUser->email,
1345
                'passwordHash' => $spiUser->passwordHash,
1346
                'hashAlgorithm' => (int)$spiUser->hashAlgorithm,
1347
                'enabled' => $spiUser->isEnabled,
1348
                'maxLogin' => (int)$spiUser->maxLogin,
1349
            )
1350
        );
1351
    }
1352
1353
    /**
1354
     * Verifies if the provided login and password are valid.
1355
     *
1356
     * @param string $login User login
1357
     * @param string $password User password
1358
     * @param \eZ\Publish\SPI\Persistence\User $spiUser Loaded user handler
1359
     *
1360
     * @return bool return true if the login and password are sucessfully
1361
     * validate and false, if not.
1362
     */
1363
    protected function verifyPassword($login, $password, $spiUser)
1364
    {
1365
        // In case of bcrypt let php's password functionality do it's magic
1366
        if ($spiUser->hashAlgorithm === APIUser::PASSWORD_HASH_BCRYPT ||
1367
            $spiUser->hashAlgorithm === APIUser::PASSWORD_HASH_PHP_DEFAULT) {
1368
            return password_verify($password, $spiUser->passwordHash);
1369
        }
1370
1371
        // Randomize login time to protect against timing attacks
1372
        usleep(random_int(0, 30000));
1373
1374
        $passwordHash = $this->createPasswordHash(
1375
            $login,
1376
            $password,
1377
            $this->settings['siteName'],
1378
            $spiUser->hashAlgorithm
1379
        );
1380
1381
        return $passwordHash === $spiUser->passwordHash;
1382
    }
1383
1384
    /**
1385
     * Returns password hash based on user data and site settings.
1386
     *
1387
     * @param string $login User login
1388
     * @param string $password User password
1389
     * @param string $site The name of the site
1390
     * @param int $type Type of password to generate
1391
     *
1392
     * @return string Generated password hash
1393
     *
1394
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the type is not recognized
1395
     */
1396
    protected function createPasswordHash($login, $password, $site, $type)
1397
    {
1398
        $deprecationWarningFormat = 'Password hash type %s is deprecated since 6.13.';
1399
1400
        switch ($type) {
1401
            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...
1402
                @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...
1403
1404
                return md5($password);
1405
1406 View Code Duplication
            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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1407
                @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...
1408
1409
                return md5("$login\n$password");
1410
1411 View Code Duplication
            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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1412
                @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...
1413
1414
                return md5("$login\n$password\n$site");
1415
1416
            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...
1417
                @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...
1418
1419
                return $password;
1420
1421
            case APIUser::PASSWORD_HASH_BCRYPT:
1422
                return password_hash($password, PASSWORD_BCRYPT);
1423
1424
            case APIUser::PASSWORD_HASH_PHP_DEFAULT:
1425
                return password_hash($password, PASSWORD_DEFAULT);
1426
1427
            default:
1428
                throw new InvalidArgumentException('type', "Password hash type '$type' is not recognized");
1429
        }
1430
    }
1431
1432
    /**
1433
     * Return true if any of the UserUpdateStruct properties refers to User Profile (Content) update.
1434
     *
1435
     * @param UserUpdateStruct $userUpdateStruct
1436
     *
1437
     * @return bool
1438
     */
1439
    private function isUserProfileUpdateRequested(UserUpdateStruct $userUpdateStruct)
1440
    {
1441
        return
1442
            !empty($userUpdateStruct->contentUpdateStruct) ||
1443
            !empty($userUpdateStruct->contentMetadataUpdateStruct) ||
1444
            !empty($userUpdateStruct->email) ||
1445
            !empty($userUpdateStruct->enabled) ||
1446
            !empty($userUpdateStruct->maxLogin);
1447
    }
1448
}
1449