Completed
Push — master ( 59965c...4339a8 )
by
unknown
114:08 queued 95:41
created

UserService::newUserCreateStruct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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