Completed
Push — 6.13 ( 468aa6...b38df1 )
by
unknown
78:43 queued 54:57
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 ezcMailTools;
40
use Exception;
41
use Psr\Log\LoggerInterface;
42
43
/**
44
 * This service provides methods for managing users and user groups.
45
 *
46
 * @example Examples/user.php
47
 */
48
class UserService implements UserServiceInterface
49
{
50
    /**
51
     * @var \eZ\Publish\API\Repository\Repository
52
     */
53
    protected $repository;
54
55
    /**
56
     * @var \eZ\Publish\SPI\Persistence\User\Handler
57
     */
58
    protected $userHandler;
59
60
    /**
61
     * @var array
62
     */
63
    protected $settings;
64
65
    /** @var \Psr\Log\LoggerInterface|null */
66
    protected $logger;
67
68
    public function setLogger(LoggerInterface $logger = null)
69
    {
70
        $this->logger = $logger;
71
    }
72
73
    /**
74
     * Setups service with reference to repository object that created it & corresponding handler.
75
     *
76
     * @param \eZ\Publish\API\Repository\Repository $repository
77
     * @param \eZ\Publish\SPI\Persistence\User\Handler $userHandler
78
     * @param array $settings
79
     */
80
    public function __construct(RepositoryInterface $repository, Handler $userHandler, array $settings = array())
81
    {
82
        $this->repository = $repository;
83
        $this->userHandler = $userHandler;
84
        // Union makes sure default settings are ignored if provided in argument
85
        $this->settings = $settings + array(
86
            'defaultUserPlacement' => 12,
87
            'userClassID' => 4, // @todo Rename this settings to swap out "Class" for "Type"
88
            'userGroupClassID' => 3,
89
            'hashType' => APIUser::DEFAULT_PASSWORD_HASH,
90
            'siteName' => 'ez.no',
91
        );
92
    }
93
94
    /**
95
     * Creates a new user group using the data provided in the ContentCreateStruct parameter.
96
     *
97
     * In 4.x in the content type parameter in the profile is ignored
98
     * - the content type is determined via configuration and can be set to null.
99
     * The returned version is published.
100
     *
101
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct $userGroupCreateStruct a structure for setting all necessary data to create this user group
102
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $parentGroup
103
     *
104
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
105
     *
106
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
107
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the input structure has invalid data
108
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupCreateStruct is not valid
109
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
110
     */
111
    public function createUserGroup(APIUserGroupCreateStruct $userGroupCreateStruct, APIUserGroup $parentGroup)
112
    {
113
        $contentService = $this->repository->getContentService();
114
        $locationService = $this->repository->getLocationService();
115
        $contentTypeService = $this->repository->getContentTypeService();
116
117
        if ($userGroupCreateStruct->contentType === null) {
118
            $userGroupContentType = $contentTypeService->loadContentType($this->settings['userGroupClassID']);
119
            $userGroupCreateStruct->contentType = $userGroupContentType;
120
        }
121
122
        $loadedParentGroup = $this->loadUserGroup($parentGroup->id);
123
124
        if ($loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
125
            throw new InvalidArgumentException('parentGroup', 'parent user group has no main location');
126
        }
127
128
        $locationCreateStruct = $locationService->newLocationCreateStruct(
129
            $loadedParentGroup->getVersionInfo()->getContentInfo()->mainLocationId
130
        );
131
132
        $this->repository->beginTransaction();
133
        try {
134
            $contentDraft = $contentService->createContent($userGroupCreateStruct, array($locationCreateStruct));
135
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
136
            $this->repository->commit();
137
        } catch (Exception $e) {
138
            $this->repository->rollback();
139
            throw $e;
140
        }
141
142
        return $this->buildDomainUserGroupObject($publishedContent);
143
    }
144
145
    /**
146
     * Loads a user group for the given id.
147
     *
148
     * @param mixed $id
149
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
150
     *
151
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
152
     *
153
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
154
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the user group with the given id was not found
155
     */
156
    public function loadUserGroup($id, array $prioritizedLanguages = [])
157
    {
158
        $content = $this->repository->getContentService()->loadContent($id, $prioritizedLanguages);
159
160
        return $this->buildDomainUserGroupObject($content);
161
    }
162
163
    /**
164
     * Loads the sub groups of a user group.
165
     *
166
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
167
     * @param int $offset the start offset for paging
168
     * @param int $limit the number of user groups returned
169
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
170
     *
171
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
172
     *
173
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the user group
174
     */
175
    public function loadSubUserGroups(APIUserGroup $userGroup, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
176
    {
177
        $locationService = $this->repository->getLocationService();
178
179
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
180
        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...
181
            throw new UnauthorizedException('content', 'read');
182
        }
183
184
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
185
            return array();
186
        }
187
188
        $mainGroupLocation = $locationService->loadLocation(
189
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
190
        );
191
192
        $searchResult = $this->searchSubGroups($mainGroupLocation, $offset, $limit);
193
        if ($searchResult->totalCount == 0) {
194
            return array();
195
        }
196
197
        $subUserGroups = array();
198 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...
199
            $subUserGroups[] = $this->buildDomainUserGroupObject(
200
                $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...
201
                    $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...
202
                    $prioritizedLanguages
203
                )
204
            );
205
        }
206
207
        return $subUserGroups;
208
    }
209
210
    /**
211
     * Returns (searches) subgroups of a user group described by its main location.
212
     *
213
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
214
     * @param int $offset
215
     * @param int $limit
216
     *
217
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
218
     */
219
    protected function searchSubGroups(Location $location, $offset = 0, $limit = 25)
220
    {
221
        $searchQuery = new LocationQuery();
222
223
        $searchQuery->offset = $offset;
224
        $searchQuery->limit = $limit;
225
226
        $searchQuery->filter = new CriterionLogicalAnd([
227
            new CriterionContentTypeId($this->settings['userGroupClassID']),
228
            new CriterionParentLocationId($location->id),
229
        ]);
230
231
        $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...
232
233
        return $this->repository->getSearchService()->findLocations($searchQuery, array(), false);
234
    }
235
236
    /**
237
     * Removes a user group.
238
     *
239
     * the users which are not assigned to other groups will be deleted.
240
     *
241
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
242
     *
243
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to create a user group
244
     */
245 View Code Duplication
    public function deleteUserGroup(APIUserGroup $userGroup)
246
    {
247
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
248
249
        $this->repository->beginTransaction();
250
        try {
251
            //@todo: what happens to sub user groups and users below sub user groups
252
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUserGroup->getVersionInfo()->getContentInfo());
253
            $this->repository->commit();
254
        } catch (Exception $e) {
255
            $this->repository->rollback();
256
            throw $e;
257
        }
258
259
        return $affectedLocationIds;
260
    }
261
262
    /**
263
     * Moves the user group to another parent.
264
     *
265
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
266
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $newParent
267
     *
268
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
269
     */
270
    public function moveUserGroup(APIUserGroup $userGroup, APIUserGroup $newParent)
271
    {
272
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
273
        $loadedNewParent = $this->loadUserGroup($newParent->id);
274
275
        $locationService = $this->repository->getLocationService();
276
277
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
278
            throw new BadStateException('userGroup', 'existing user group is not stored and/or does not have any location yet');
279
        }
280
281
        if ($loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId === null) {
282
            throw new BadStateException('newParent', 'new user group is not stored and/or does not have any location yet');
283
        }
284
285
        $userGroupMainLocation = $locationService->loadLocation(
286
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
287
        );
288
        $newParentMainLocation = $locationService->loadLocation(
289
            $loadedNewParent->getVersionInfo()->getContentInfo()->mainLocationId
290
        );
291
292
        $this->repository->beginTransaction();
293
        try {
294
            $locationService->moveSubtree($userGroupMainLocation, $newParentMainLocation);
295
            $this->repository->commit();
296
        } catch (Exception $e) {
297
            $this->repository->rollback();
298
            throw $e;
299
        }
300
    }
301
302
    /**
303
     * Updates the group profile with fields and meta data.
304
     *
305
     * 4.x: If the versionUpdateStruct is set in $userGroupUpdateStruct, this method internally creates a content draft, updates ts with the provided data
306
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
307
     *
308
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
309
     * @param \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct $userGroupUpdateStruct
310
     *
311
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
312
     *
313
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user group
314
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userGroupUpdateStruct is not valid
315
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
316
     */
317
    public function updateUserGroup(APIUserGroup $userGroup, UserGroupUpdateStruct $userGroupUpdateStruct)
318
    {
319
        if ($userGroupUpdateStruct->contentUpdateStruct === null &&
320
            $userGroupUpdateStruct->contentMetadataUpdateStruct === null) {
321
            // both update structs are empty, nothing to do
322
            return $userGroup;
323
        }
324
325
        $contentService = $this->repository->getContentService();
326
327
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
328
329
        $this->repository->beginTransaction();
330
        try {
331
            $publishedContent = $loadedUserGroup;
332 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...
333
                $contentDraft = $contentService->createContentDraft($loadedUserGroup->getVersionInfo()->getContentInfo());
334
335
                $contentDraft = $contentService->updateContent(
336
                    $contentDraft->getVersionInfo(),
337
                    $userGroupUpdateStruct->contentUpdateStruct
338
                );
339
340
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
341
            }
342
343
            if ($userGroupUpdateStruct->contentMetadataUpdateStruct !== null) {
344
                $publishedContent = $contentService->updateContentMetadata(
345
                    $publishedContent->getVersionInfo()->getContentInfo(),
346
                    $userGroupUpdateStruct->contentMetadataUpdateStruct
347
                );
348
            }
349
350
            $this->repository->commit();
351
        } catch (Exception $e) {
352
            $this->repository->rollback();
353
            throw $e;
354
        }
355
356
        return $this->buildDomainUserGroupObject($publishedContent);
357
    }
358
359
    /**
360
     * Create a new user. The created user is published by this method.
361
     *
362
     * @param \eZ\Publish\API\Repository\Values\User\UserCreateStruct $userCreateStruct the data used for creating the user
363
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup[] $parentGroups the groups which are assigned to the user after creation
364
     *
365
     * @return \eZ\Publish\API\Repository\Values\User\User
366
     *
367
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to move the user group
368
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userCreateStruct is not valid
369
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is missing or set to an empty value
370
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a user with provided login already exists
371
     */
372
    public function createUser(APIUserCreateStruct $userCreateStruct, array $parentGroups)
373
    {
374
        if (empty($parentGroups)) {
375
            throw new InvalidArgumentValue('parentGroups', $parentGroups);
376
        }
377
378
        if (!is_string($userCreateStruct->login) || empty($userCreateStruct->login)) {
379
            throw new InvalidArgumentValue('login', $userCreateStruct->login, 'UserCreateStruct');
380
        }
381
382 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...
383
            throw new InvalidArgumentValue('email', $userCreateStruct->email, 'UserCreateStruct');
384
        }
385
386
        if (!ezcMailTools::validateEmailAddress($userCreateStruct->email)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \ezcMailTools::validateE...serCreateStruct->email) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
387
            throw new InvalidArgumentValue('email', $userCreateStruct->email, 'UserCreateStruct');
388
        }
389
390
        if (!is_string($userCreateStruct->password) || empty($userCreateStruct->password)) {
391
            throw new InvalidArgumentValue('password', $userCreateStruct->password, 'UserCreateStruct');
392
        }
393
394
        if (!is_bool($userCreateStruct->enabled)) {
395
            throw new InvalidArgumentValue('enabled', $userCreateStruct->enabled, 'UserCreateStruct');
396
        }
397
398
        try {
399
            $this->userHandler->loadByLogin($userCreateStruct->login);
400
            throw new InvalidArgumentException('userCreateStruct', 'User with provided login already exists');
401
        } catch (NotFoundException $e) {
402
            // Do nothing
403
        }
404
405
        $contentService = $this->repository->getContentService();
406
        $locationService = $this->repository->getLocationService();
407
        $contentTypeService = $this->repository->getContentTypeService();
408
409
        if ($userCreateStruct->contentType === null) {
410
            $userContentType = $contentTypeService->loadContentType($this->settings['userClassID']);
411
            $userCreateStruct->contentType = $userContentType;
412
        }
413
414
        $locationCreateStructs = array();
415
        foreach ($parentGroups as $parentGroup) {
416
            $parentGroup = $this->loadUserGroup($parentGroup->id);
417
            if ($parentGroup->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
418
                $locationCreateStructs[] = $locationService->newLocationCreateStruct(
419
                    $parentGroup->getVersionInfo()->getContentInfo()->mainLocationId
420
                );
421
            }
422
        }
423
424
        // Search for the first ezuser field type in content type
425
        $userFieldDefinition = null;
426
        foreach ($userCreateStruct->contentType->getFieldDefinitions() as $fieldDefinition) {
427
            if ($fieldDefinition->fieldTypeIdentifier == 'ezuser') {
428
                $userFieldDefinition = $fieldDefinition;
429
                break;
430
            }
431
        }
432
433
        if ($userFieldDefinition === null) {
434
            throw new ContentValidationException('Provided content type does not contain ezuser field type');
435
        }
436
437
        $fixUserFieldType = true;
438
        foreach ($userCreateStruct->fields as $index => $field) {
439
            if ($field->fieldDefIdentifier == $userFieldDefinition->identifier) {
440
                if ($field->value instanceof UserValue) {
441
                    $userCreateStruct->fields[$index]->value->login = $userCreateStruct->login;
442
                } else {
443
                    $userCreateStruct->fields[$index]->value = new UserValue(
444
                        array(
445
                            'login' => $userCreateStruct->login,
446
                        )
447
                    );
448
                }
449
450
                $fixUserFieldType = false;
451
            }
452
        }
453
454
        if ($fixUserFieldType) {
455
            $userCreateStruct->setField(
456
                $userFieldDefinition->identifier,
457
                new UserValue(
458
                    array(
459
                        'login' => $userCreateStruct->login,
460
                    )
461
                )
462
            );
463
        }
464
465
        $this->repository->beginTransaction();
466
        try {
467
            $contentDraft = $contentService->createContent($userCreateStruct, $locationCreateStructs);
468
            // Create user before publishing, so that external data can be returned
469
            $spiUser = $this->userHandler->create(
470
                new SPIUser(
471
                    array(
472
                        'id' => $contentDraft->id,
473
                        'login' => $userCreateStruct->login,
474
                        'email' => $userCreateStruct->email,
475
                        'passwordHash' => $this->createPasswordHash(
476
                            $userCreateStruct->login,
477
                            $userCreateStruct->password,
478
                            $this->settings['siteName'],
479
                            $this->settings['hashType']
480
                        ),
481
                        'hashAlgorithm' => $this->settings['hashType'],
482
                        'isEnabled' => $userCreateStruct->enabled,
483
                        'maxLogin' => 0,
484
                    )
485
                )
486
            );
487
            $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
488
489
            $this->repository->commit();
490
        } catch (Exception $e) {
491
            $this->repository->rollback();
492
            throw $e;
493
        }
494
495
        return $this->buildDomainUserObject($spiUser, $publishedContent);
496
    }
497
498
    /**
499
     * Loads a user.
500
     *
501
     * @param mixed $userId
502
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
503
     *
504
     * @return \eZ\Publish\API\Repository\Values\User\User
505
     *
506
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given id was not found
507
     */
508
    public function loadUser($userId, array $prioritizedLanguages = [])
509
    {
510
        /** @var \eZ\Publish\API\Repository\Values\Content\Content $content */
511
        $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...
512
        // Get spiUser value from Field Value
513
        foreach ($content->getFields() as $field) {
514
            if (!$field->value instanceof UserValue) {
515
                continue;
516
            }
517
518
            /** @var \eZ\Publish\Core\FieldType\User\Value $value */
519
            $value = $field->value;
520
            $spiUser = new SPIUser();
521
            $spiUser->id = $value->contentId;
522
            $spiUser->login = $value->login;
523
            $spiUser->email = $value->email;
524
            $spiUser->hashAlgorithm = $value->passwordHashType;
525
            $spiUser->passwordHash = $value->passwordHash;
526
            $spiUser->isEnabled = $value->enabled;
527
            $spiUser->maxLogin = $value->maxLogin;
528
            break;
529
        }
530
531
        // If for some reason not found, load it
532
        if (!isset($spiUser)) {
533
            $spiUser = $this->userHandler->load($userId);
534
        }
535
536
        return $this->buildDomainUserObject($spiUser, $content);
537
    }
538
539
    /**
540
     * Loads anonymous user.
541
     *
542
     * @deprecated since 5.3, use loadUser( $anonymousUserId ) instead
543
     *
544
     * @uses ::loadUser()
545
     *
546
     * @return \eZ\Publish\API\Repository\Values\User\User
547
     */
548
    public function loadAnonymousUser()
549
    {
550
        return $this->loadUser($this->settings['anonymousUserID']);
551
    }
552
553
    /**
554
     * Loads a user for the given login and password.
555
     *
556
     * If the password hash type differs from that configured for the service, it will be updated to the configured one.
557
     *
558
     * {@inheritdoc}
559
     *
560
     * @param string $login
561
     * @param string $password the plain password
562
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
563
     *
564
     * @return \eZ\Publish\API\Repository\Values\User\User
565
     *
566
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if credentials are invalid
567
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
568
     */
569
    public function loadUserByCredentials($login, $password, array $prioritizedLanguages = [])
570
    {
571
        if (!is_string($login) || empty($login)) {
572
            throw new InvalidArgumentValue('login', $login);
573
        }
574
575
        if (!is_string($password)) {
576
            throw new InvalidArgumentValue('password', $password);
577
        }
578
579
        $spiUser = $this->userHandler->loadByLogin($login);
580
        if (!$this->verifyPassword($login, $password, $spiUser)) {
581
            throw new NotFoundException('user', $login);
582
        }
583
584
        // Don't catch BadStateException, on purpose, to avoid broken hashes.
585
        $this->updatePasswordHash($login, $password, $spiUser);
586
587
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
588
    }
589
590
    /**
591
     * Update password hash to the type configured for the service, if they differ.
592
     *
593
     * @param string $login User login
594
     * @param string $password User password
595
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
596
     *
597
     * @throws \eZ\Publish\Core\Base\Exceptions\BadStateException if the password is not correctly saved, in which case the update is reverted
598
     */
599
    private function updatePasswordHash($login, $password, SPIUser $spiUser)
600
    {
601
        if ($spiUser->hashAlgorithm === $this->settings['hashType']) {
602
            return;
603
        }
604
605
        $spiUser->passwordHash = $this->createPasswordHash($login, $password, null, $this->settings['hashType']);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->createPasswordHas...->settings['hashType']) can also be of type false. However, the property $passwordHash is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
606
        $spiUser->hashAlgorithm = $this->settings['hashType'];
607
608
        $this->repository->beginTransaction();
609
        $this->userHandler->update($spiUser);
610
        $reloadedSpiUser = $this->userHandler->load($spiUser->id);
611
612
        if ($reloadedSpiUser->passwordHash === $spiUser->passwordHash) {
613
            $this->repository->commit();
614
        } else {
615
            // Password hash was not correctly saved, possible cause: EZP-28692
616
            $this->repository->rollback();
617
            if (isset($this->logger)) {
618
                $this->logger->critical('Password hash could not be updated. Please verify that your database schema is up to date.');
619
            }
620
621
            throw new BadStateException(
622
                'user',
623
                'Could not save updated password hash, reverting to previous hash. Please verify that your database schema is up to date.'
624
            );
625
        }
626
    }
627
628
    /**
629
     * Loads a user for the given login.
630
     *
631
     * {@inheritdoc}
632
     *
633
     * @param string $login
634
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
635
     *
636
     * @return \eZ\Publish\API\Repository\Values\User\User
637
     *
638
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if a user with the given credentials was not found
639
     */
640
    public function loadUserByLogin($login, array $prioritizedLanguages = [])
641
    {
642
        if (!is_string($login) || empty($login)) {
643
            throw new InvalidArgumentValue('login', $login);
644
        }
645
646
        $spiUser = $this->userHandler->loadByLogin($login);
647
648
        return $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
649
    }
650
651
    /**
652
     * Loads a user for the given email.
653
     *
654
     * {@inheritdoc}
655
     *
656
     * @param string $email
657
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
658
     *
659
     * @return \eZ\Publish\API\Repository\Values\User\User[]
660
     */
661
    public function loadUsersByEmail($email, array $prioritizedLanguages = [])
662
    {
663
        if (!is_string($email) || empty($email)) {
664
            throw new InvalidArgumentValue('email', $email);
665
        }
666
667
        $users = array();
668
        foreach ($this->userHandler->loadByEmail($email) as $spiUser) {
669
            $users[] = $this->buildDomainUserObject($spiUser, null, $prioritizedLanguages);
670
        }
671
672
        return $users;
673
    }
674
675
    /**
676
     * This method deletes a user.
677
     *
678
     * @param \eZ\Publish\API\Repository\Values\User\User $user
679
     *
680
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to delete the user
681
     */
682 View Code Duplication
    public function deleteUser(APIUser $user)
683
    {
684
        $loadedUser = $this->loadUser($user->id);
685
686
        $this->repository->beginTransaction();
687
        try {
688
            $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo());
689
            $this->userHandler->delete($loadedUser->id);
690
            $this->repository->commit();
691
        } catch (Exception $e) {
692
            $this->repository->rollback();
693
            throw $e;
694
        }
695
696
        return $affectedLocationIds;
697
    }
698
699
    /**
700
     * Updates a user.
701
     *
702
     * 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
703
     * and publishes the draft. If a draft is explicitly required, the user group can be updated via the content service methods.
704
     *
705
     * @param \eZ\Publish\API\Repository\Values\User\User $user
706
     * @param \eZ\Publish\API\Repository\Values\User\UserUpdateStruct $userUpdateStruct
707
     *
708
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to update the user
709
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $userUpdateStruct is not valid
710
     * @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException if a required field is set empty
711
     *
712
     * @return \eZ\Publish\API\Repository\Values\User\User
713
     */
714
    public function updateUser(APIUser $user, UserUpdateStruct $userUpdateStruct)
715
    {
716
        $loadedUser = $this->loadUser($user->id);
717
718
        // We need to determine if we have anything to update.
719
        // UserUpdateStruct is specific as some of the new content is in
720
        // content update struct and some of it is in additional fields like
721
        // email, password and so on
722
        $doUpdate = false;
723
        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...
724
            if ($propertyValue !== null) {
725
                $doUpdate = true;
726
                break;
727
            }
728
        }
729
730
        if (!$doUpdate) {
731
            // Nothing to update, so we just quit
732
            return $user;
733
        }
734
735
        if ($userUpdateStruct->email !== null) {
736 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...
737
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
738
            }
739
740
            if (!ezcMailTools::validateEmailAddress($userUpdateStruct->email)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \ezcMailTools::validateE...serUpdateStruct->email) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
741
                throw new InvalidArgumentValue('email', $userUpdateStruct->email, 'UserUpdateStruct');
742
            }
743
        }
744
745 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...
746
            throw new InvalidArgumentValue('password', $userUpdateStruct->password, 'UserUpdateStruct');
747
        }
748
749
        if ($userUpdateStruct->enabled !== null && !is_bool($userUpdateStruct->enabled)) {
750
            throw new InvalidArgumentValue('enabled', $userUpdateStruct->enabled, 'UserUpdateStruct');
751
        }
752
753
        if ($userUpdateStruct->maxLogin !== null && !is_int($userUpdateStruct->maxLogin)) {
754
            throw new InvalidArgumentValue('maxLogin', $userUpdateStruct->maxLogin, 'UserUpdateStruct');
755
        }
756
757
        $contentService = $this->repository->getContentService();
758
759
        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...
760
            throw new UnauthorizedException('content', 'edit');
761
        }
762
763
        $this->repository->beginTransaction();
764
        try {
765
            $publishedContent = $loadedUser;
766 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...
767
                $contentDraft = $contentService->createContentDraft($loadedUser->getVersionInfo()->getContentInfo());
768
                $contentDraft = $contentService->updateContent(
769
                    $contentDraft->getVersionInfo(),
770
                    $userUpdateStruct->contentUpdateStruct
771
                );
772
                $publishedContent = $contentService->publishVersion($contentDraft->getVersionInfo());
773
            }
774
775
            if ($userUpdateStruct->contentMetadataUpdateStruct !== null) {
776
                $contentService->updateContentMetadata(
777
                    $publishedContent->getVersionInfo()->getContentInfo(),
778
                    $userUpdateStruct->contentMetadataUpdateStruct
779
                );
780
            }
781
782
            $this->userHandler->update(
783
                new SPIUser(
784
                    array(
785
                        'id' => $loadedUser->id,
786
                        'login' => $loadedUser->login,
787
                        'email' => $userUpdateStruct->email ?: $loadedUser->email,
788
                        'passwordHash' => $userUpdateStruct->password ?
789
                            $this->createPasswordHash(
790
                                $loadedUser->login,
791
                                $userUpdateStruct->password,
792
                                $this->settings['siteName'],
793
                                $this->settings['hashType']
794
                            ) :
795
                            $loadedUser->passwordHash,
796
                        'hashAlgorithm' => $userUpdateStruct->password ?
797
                            $this->settings['hashType'] :
798
                            $loadedUser->hashAlgorithm,
799
                        'isEnabled' => $userUpdateStruct->enabled !== null ? $userUpdateStruct->enabled : $loadedUser->enabled,
800
                        'maxLogin' => $userUpdateStruct->maxLogin !== null ? (int)$userUpdateStruct->maxLogin : $loadedUser->maxLogin,
801
                    )
802
                )
803
            );
804
805
            $this->repository->commit();
806
        } catch (Exception $e) {
807
            $this->repository->rollback();
808
            throw $e;
809
        }
810
811
        return $this->loadUser($loadedUser->id);
812
    }
813
814
    /**
815
     * Assigns a new user group to the user.
816
     *
817
     * @param \eZ\Publish\API\Repository\Values\User\User $user
818
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
819
     *
820
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to assign the user group to the user
821
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is already in the given user group
822
     */
823
    public function assignUserToUserGroup(APIUser $user, APIUserGroup $userGroup)
824
    {
825
        $loadedUser = $this->loadUser($user->id);
826
        $loadedGroup = $this->loadUserGroup($userGroup->id);
827
        $locationService = $this->repository->getLocationService();
828
829
        $existingGroupIds = array();
830
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
831
        foreach ($userLocations as $userLocation) {
832
            $existingGroupIds[] = $userLocation->parentLocationId;
833
        }
834
835
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
836
            throw new BadStateException('userGroup', 'user group has no main location or no locations');
837
        }
838
839
        if (in_array($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId, $existingGroupIds)) {
840
            // user is already assigned to the user group
841
            throw new InvalidArgumentException('user', 'user is already in the given user group');
842
        }
843
844
        $locationCreateStruct = $locationService->newLocationCreateStruct(
845
            $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId
846
        );
847
848
        $this->repository->beginTransaction();
849
        try {
850
            $locationService->createLocation(
851
                $loadedUser->getVersionInfo()->getContentInfo(),
852
                $locationCreateStruct
853
            );
854
            $this->repository->commit();
855
        } catch (Exception $e) {
856
            $this->repository->rollback();
857
            throw $e;
858
        }
859
    }
860
861
    /**
862
     * Removes a user group from the user.
863
     *
864
     * @param \eZ\Publish\API\Repository\Values\User\User $user
865
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
866
     *
867
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to remove the user group from the user
868
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the user is not in the given user group
869
     * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $userGroup is the last assigned user group
870
     */
871
    public function unAssignUserFromUserGroup(APIUser $user, APIUserGroup $userGroup)
872
    {
873
        $loadedUser = $this->loadUser($user->id);
874
        $loadedGroup = $this->loadUserGroup($userGroup->id);
875
        $locationService = $this->repository->getLocationService();
876
877
        $userLocations = $locationService->loadLocations($loadedUser->getVersionInfo()->getContentInfo());
878
        if (empty($userLocations)) {
879
            throw new BadStateException('user', 'user has no locations, cannot unassign from group');
880
        }
881
882
        if ($loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
883
            throw new BadStateException('userGroup', 'user group has no main location or no locations, cannot unassign');
884
        }
885
886
        foreach ($userLocations as $userLocation) {
887
            if ($userLocation->parentLocationId == $loadedGroup->getVersionInfo()->getContentInfo()->mainLocationId) {
888
                // Throw this specific BadState when we know argument is valid
889
                if (count($userLocations) === 1) {
890
                    throw new BadStateException('user', 'user only has one user group, cannot unassign from last group');
891
                }
892
893
                $this->repository->beginTransaction();
894
                try {
895
                    $locationService->deleteLocation($userLocation);
896
                    $this->repository->commit();
897
898
                    return;
899
                } catch (Exception $e) {
900
                    $this->repository->rollback();
901
                    throw $e;
902
                }
903
            }
904
        }
905
906
        throw new InvalidArgumentException('userGroup', 'user is not in the given user group');
907
    }
908
909
    /**
910
     * Loads the user groups the user belongs to.
911
     *
912
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed read the user or user group
913
     *
914
     * @param \eZ\Publish\API\Repository\Values\User\User $user
915
     * @param int $offset the start offset for paging
916
     * @param int $limit the number of user groups returned
917
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
918
     *
919
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup[]
920
     */
921
    public function loadUserGroupsOfUser(APIUser $user, $offset = 0, $limit = 25, array $prioritizedLanguages = [])
922
    {
923
        $locationService = $this->repository->getLocationService();
924
925
        if (!$this->repository->getPermissionResolver()->canUser('content', 'read', $user)) {
926
            throw new UnauthorizedException('content', 'read');
927
        }
928
929
        $userLocations = $locationService->loadLocations(
930
            $user->getVersionInfo()->getContentInfo()
931
        );
932
933
        $parentLocationIds = array();
934
        foreach ($userLocations as $userLocation) {
935
            if ($userLocation->parentLocationId !== null) {
936
                $parentLocationIds[] = $userLocation->parentLocationId;
937
            }
938
        }
939
940
        $searchQuery = new LocationQuery();
941
942
        $searchQuery->offset = $offset;
943
        $searchQuery->limit = $limit;
944
        $searchQuery->performCount = false;
945
946
        $searchQuery->filter = new CriterionLogicalAnd(
947
            [
948
                new CriterionContentTypeId($this->settings['userGroupClassID']),
949
                new CriterionLocationId($parentLocationIds),
950
            ]
951
        );
952
953
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
954
955
        $userGroups = [];
956 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...
957
            $userGroups[] = $this->buildDomainUserGroupObject(
958
                $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...
959
                    $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...
960
                    $prioritizedLanguages
961
                )
962
            );
963
        }
964
965
        return $userGroups;
966
    }
967
968
    /**
969
     * Loads the users of a user group.
970
     *
971
     * @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read the users or user group
972
     *
973
     * @param \eZ\Publish\API\Repository\Values\User\UserGroup $userGroup
974
     * @param int $offset the start offset for paging
975
     * @param int $limit the number of users returned
976
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
977
     *
978
     * @return \eZ\Publish\API\Repository\Values\User\User[]
979
     */
980
    public function loadUsersOfUserGroup(
981
        APIUserGroup $userGroup,
982
        $offset = 0,
983
        $limit = 25,
984
        array $prioritizedLanguages = []
985
    ) {
986
        $loadedUserGroup = $this->loadUserGroup($userGroup->id);
987
988
        if ($loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId === null) {
989
            return [];
990
        }
991
992
        $mainGroupLocation = $this->repository->getLocationService()->loadLocation(
993
            $loadedUserGroup->getVersionInfo()->getContentInfo()->mainLocationId
994
        );
995
996
        $searchQuery = new LocationQuery();
997
998
        $searchQuery->filter = new CriterionLogicalAnd(
999
            [
1000
                new CriterionContentTypeId($this->settings['userClassID']),
1001
                new CriterionParentLocationId($mainGroupLocation->id),
1002
            ]
1003
        );
1004
1005
        $searchQuery->offset = $offset;
1006
        $searchQuery->limit = $limit;
1007
        $searchQuery->performCount = false;
1008
        $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...
1009
1010
        $searchResult = $this->repository->getSearchService()->findLocations($searchQuery);
1011
1012
        $users = [];
1013
        foreach ($searchResult->searchHits as $resultItem) {
1014
            $users[] = $this->buildDomainUserObject(
1015
                $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...
1016
                $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...
1017
                    $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...
1018
                    $prioritizedLanguages
1019
                )
1020
            );
1021
        }
1022
1023
        return $users;
1024
    }
1025
1026
    /**
1027
     * Instantiate a user create class.
1028
     *
1029
     * @param string $login the login of the new user
1030
     * @param string $email the email of the new user
1031
     * @param string $password the plain password of the new user
1032
     * @param string $mainLanguageCode the main language for the underlying content object
1033
     * @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
1034
     *
1035
     * @return \eZ\Publish\API\Repository\Values\User\UserCreateStruct
1036
     */
1037
    public function newUserCreateStruct($login, $email, $password, $mainLanguageCode, $contentType = null)
1038
    {
1039
        if ($contentType === null) {
1040
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1041
                $this->settings['userClassID']
1042
            );
1043
        }
1044
1045
        return new UserCreateStruct(
1046
            array(
1047
                'contentType' => $contentType,
1048
                'mainLanguageCode' => $mainLanguageCode,
1049
                'login' => $login,
1050
                'email' => $email,
1051
                'password' => $password,
1052
                'enabled' => true,
1053
                'fields' => array(),
1054
            )
1055
        );
1056
    }
1057
1058
    /**
1059
     * Instantiate a user group create class.
1060
     *
1061
     * @param string $mainLanguageCode The main language for the underlying content object
1062
     * @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
1063
     *
1064
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupCreateStruct
1065
     */
1066
    public function newUserGroupCreateStruct($mainLanguageCode, $contentType = null)
1067
    {
1068
        if ($contentType === null) {
1069
            $contentType = $this->repository->getContentTypeService()->loadContentType(
1070
                $this->settings['userGroupClassID']
1071
            );
1072
        }
1073
1074
        return new UserGroupCreateStruct(
1075
            array(
1076
                'contentType' => $contentType,
1077
                'mainLanguageCode' => $mainLanguageCode,
1078
                'fields' => array(),
1079
            )
1080
        );
1081
    }
1082
1083
    /**
1084
     * Instantiate a new user update struct.
1085
     *
1086
     * @return \eZ\Publish\API\Repository\Values\User\UserUpdateStruct
1087
     */
1088
    public function newUserUpdateStruct()
1089
    {
1090
        return new UserUpdateStruct();
1091
    }
1092
1093
    /**
1094
     * Instantiate a new user group update struct.
1095
     *
1096
     * @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
1097
     */
1098
    public function newUserGroupUpdateStruct()
1099
    {
1100
        return new UserGroupUpdateStruct();
1101
    }
1102
1103
    /**
1104
     * Builds the domain UserGroup object from provided Content object.
1105
     *
1106
     * @param \eZ\Publish\API\Repository\Values\Content\Content $content
1107
     *
1108
     * @return \eZ\Publish\API\Repository\Values\User\UserGroup
1109
     */
1110
    protected function buildDomainUserGroupObject(APIContent $content)
1111
    {
1112
        $locationService = $this->repository->getLocationService();
1113
1114
        $subGroupCount = 0;
1115
        if ($content->getVersionInfo()->getContentInfo()->mainLocationId !== null) {
1116
            $mainLocation = $locationService->loadLocation(
1117
                $content->getVersionInfo()->getContentInfo()->mainLocationId
1118
            );
1119
            $parentLocation = $locationService->loadLocation($mainLocation->parentLocationId);
1120
            $subGroups = $this->searchSubGroups($mainLocation, 0, 0);
1121
            $subGroupCount = $subGroups->totalCount;
1122
        }
1123
1124
        return new UserGroup(
1125
            array(
1126
                'content' => $content,
1127
                'parentId' => isset($parentLocation) ? $parentLocation->contentId : null,
1128
                'subGroupCount' => $subGroupCount,
1129
            )
1130
        );
1131
    }
1132
1133
    /**
1134
     * Builds the domain user object from provided persistence user object.
1135
     *
1136
     * @param \eZ\Publish\SPI\Persistence\User $spiUser
1137
     * @param \eZ\Publish\API\Repository\Values\Content\Content|null $content
1138
     * @param string[] $prioritizedLanguages Used as prioritized language code on translated properties of returned object.
1139
     *
1140
     * @return \eZ\Publish\API\Repository\Values\User\User
1141
     */
1142
    protected function buildDomainUserObject(
1143
        SPIUser $spiUser,
1144
        APIContent $content = null,
1145
        array $prioritizedLanguages = []
1146
    ) {
1147
        if ($content === null) {
1148
            $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...
1149
                $spiUser->id,
1150
                $prioritizedLanguages
1151
            );
1152
        }
1153
1154
        return new User(
1155
            array(
1156
                'content' => $content,
1157
                'login' => $spiUser->login,
1158
                'email' => $spiUser->email,
1159
                'passwordHash' => $spiUser->passwordHash,
1160
                'hashAlgorithm' => (int)$spiUser->hashAlgorithm,
1161
                'enabled' => $spiUser->isEnabled,
1162
                'maxLogin' => (int)$spiUser->maxLogin,
1163
            )
1164
        );
1165
    }
1166
1167
    /**
1168
     * Verifies if the provided login and password are valid.
1169
     *
1170
     * @param string $login User login
1171
     * @param string $password User password
1172
     * @param \eZ\Publish\SPI\Persistence\User $spiUser Loaded user handler
1173
     *
1174
     * @return bool return true if the login and password are sucessfully
1175
     * validate and false, if not.
1176
     */
1177
    protected function verifyPassword($login, $password, $spiUser)
1178
    {
1179
        // In case of bcrypt let php's password functionality do it's magic
1180
        if ($spiUser->hashAlgorithm === APIUser::PASSWORD_HASH_BCRYPT ||
1181
            $spiUser->hashAlgorithm === APIUser::PASSWORD_HASH_PHP_DEFAULT) {
1182
            return password_verify($password, $spiUser->passwordHash);
1183
        }
1184
1185
        // Randomize login time to protect against timing attacks
1186
        usleep(mt_rand(0, 30000));
1187
1188
        $passwordHash = $this->createPasswordHash(
1189
            $login,
1190
            $password,
1191
            $this->settings['siteName'],
1192
            $spiUser->hashAlgorithm
1193
        );
1194
1195
        return $passwordHash === $spiUser->passwordHash;
1196
    }
1197
1198
    /**
1199
     * Returns password hash based on user data and site settings.
1200
     *
1201
     * @param string $login User login
1202
     * @param string $password User password
1203
     * @param string $site The name of the site
1204
     * @param int $type Type of password to generate
1205
     *
1206
     * @return string Generated password hash
1207
     *
1208
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if the type is not recognized
1209
     */
1210
    protected function createPasswordHash($login, $password, $site, $type)
1211
    {
1212
        $deprecationWarningFormat = 'Password hash type %s is deprecated since 6.13.';
1213
1214
        switch ($type) {
1215
            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...
1216
                @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...
1217
1218
                return md5($password);
1219
1220 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...
1221
                @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...
1222
1223
                return md5("$login\n$password");
1224
1225 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...
1226
                @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...
1227
1228
                return md5("$login\n$password\n$site");
1229
1230
            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...
1231
                @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...
1232
1233
                return $password;
1234
1235
            case APIUser::PASSWORD_HASH_BCRYPT:
1236
                return password_hash($password, PASSWORD_BCRYPT);
1237
1238
            case APIUser::PASSWORD_HASH_PHP_DEFAULT:
1239
                return password_hash($password, PASSWORD_DEFAULT);
1240
1241
            default:
1242
                throw new InvalidArgumentException('type', "Password hash type '$type' is not recognized");
1243
        }
1244
    }
1245
}
1246