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