Completed
Push — master ( e07a6c...89b9bc )
by
unknown
111:05 queued 90:45
created

UserService::updatePasswordHash()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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