diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 620361bf..f0acc7c1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1884,12 +1884,6 @@ parameters: count: 1 path: src/lib/Form/EventSubscriber/UserFieldsSubscriber.php - - - message: '#^Method Ibexa\\ContentForms\\Form\\EventSubscriber\\UserFieldsSubscriber\:\:handleUserUpdateData\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/lib/Form/EventSubscriber/UserFieldsSubscriber.php - - message: '#^Method Ibexa\\ContentForms\\Form\\EventSubscriber\\UserFieldsSubscriber\:\:handleUserUpdateData\(\) has parameter \$languageCode with no type specified\.$#' identifier: missingType.parameter diff --git a/src/bundle/Controller/UserController.php b/src/bundle/Controller/UserController.php index 5755588b..d28d382d 100644 --- a/src/bundle/Controller/UserController.php +++ b/src/bundle/Controller/UserController.php @@ -168,7 +168,7 @@ public function editAction( string $language, Request $request ) { - $user = $this->userService->loadUser($contentId); + $user = $this->userService->loadUser($contentId, [$language]); if (!$this->permissionResolver->canUser('content', 'edit', $user)) { throw new CoreUnauthorizedException('content', 'edit', ['userId' => $contentId]); } @@ -199,7 +199,7 @@ public function editAction( $userUpdate, [ 'location' => $location, - 'content' => $this->contentService->loadContent($contentId), + 'content' => $this->contentService->loadContent($contentId, [$language]), 'languageCode' => $language, 'mainLanguageCode' => $user->contentInfo->mainLanguageCode, 'struct' => $userUpdate, diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index 5f71dba6..335d78d7 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -81,6 +81,13 @@ services: tags: - { name: kernel.event_subscriber } + Ibexa\ContentForms\Form\Processor\User\UserTranslationSecureListener: + arguments: + - '@ibexa.api.service.user' + - '@ibexa.api.service.content' + tags: + - { name: kernel.event_subscriber } + Ibexa\ContentForms\Form\Processor\SystemUrlRedirectProcessor: autowire: true autoconfigure: true diff --git a/src/lib/Data/Mapper/UserUpdateMapper.php b/src/lib/Data/Mapper/UserUpdateMapper.php index 5b9f7784..91cdf4f7 100644 --- a/src/lib/Data/Mapper/UserUpdateMapper.php +++ b/src/lib/Data/Mapper/UserUpdateMapper.php @@ -50,8 +50,9 @@ public function mapToFormData(User $user, ContentType $contentType, array $param $filter = $params['filter']; $fields = $user->getFieldsByLanguage($params['languageCode']); + $mainLanguageCode = $user->contentInfo->mainLanguageCode; foreach ($contentType->fieldDefinitions as $fieldDef) { - $field = $fields[$fieldDef->identifier]; + $field = $fields[$fieldDef->identifier] ?? $user->getField($fieldDef->identifier, $mainLanguageCode); if (is_callable($filter) && !($filter)($field)) { continue; diff --git a/src/lib/FieldType/Mapper/UserAccountFieldValueFormMapper.php b/src/lib/FieldType/Mapper/UserAccountFieldValueFormMapper.php index 0bc1ed9f..737e2ba1 100644 --- a/src/lib/FieldType/Mapper/UserAccountFieldValueFormMapper.php +++ b/src/lib/FieldType/Mapper/UserAccountFieldValueFormMapper.php @@ -8,8 +8,9 @@ namespace Ibexa\ContentForms\FieldType\Mapper; -use Ibexa\ContentForms\Data\ContentTranslationData; use Ibexa\ContentForms\Data\User\UserAccountFieldData; +use Ibexa\ContentForms\Data\User\UserCreateData; +use Ibexa\ContentForms\Data\User\UserUpdateData; use Ibexa\ContentForms\Form\Type\FieldType\UserAccountFieldType; use Ibexa\Contracts\ContentForms\Data\Content\FieldData; use Ibexa\Contracts\ContentForms\FieldType\FieldValueFormMapperInterface; @@ -41,12 +42,18 @@ public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data) $formConfig = $fieldForm->getConfig(); $rootForm = $fieldForm->getRoot()->getRoot(); $formIntent = $rootForm->getConfig()->getOption('intent'); - $isTranslation = $rootForm->getData() instanceof ContentTranslationData; + $rootData = $rootForm->getData(); + $isTranslation = !$rootData instanceof UserCreateData && !$rootData instanceof UserUpdateData; + $isNonMainLanguageTranslation = $isTranslation + && !$fieldDefinition->isTranslatable + && $formConfig->getOption('languageCode') !== $formConfig->getOption('mainLanguageCode'); + $formBuilder = $formConfig->getFormFactory()->createBuilder() ->create('value', UserAccountFieldType::class, [ 'required' => true, 'label' => $fieldDefinition->getName(), 'intent' => $formIntent, + 'disabled' => $isNonMainLanguageTranslation, ]); if ($isTranslation) { @@ -82,13 +89,17 @@ public function configureOptions(OptionsResolver $resolver) */ public function getModelTransformerForTranslation(FieldDefinition $fieldDefinition): CallbackTransformer { + /** @var \Ibexa\Core\FieldType\User\Value $currentValue */ + $currentValue = clone $fieldDefinition->defaultValue; + return new CallbackTransformer( - static function (ApiUserValue $data) { + static function (ApiUserValue $data) use (&$currentValue) { + $currentValue = $data; + return new UserAccountFieldData($data->login, null, $data->email, $data->enabled); }, - static function (UserAccountFieldData $submittedData) use ($fieldDefinition) { - /** @var \Ibexa\Core\FieldType\User\Value $userValue */ - $userValue = clone $fieldDefinition->defaultValue; + static function (UserAccountFieldData $submittedData) use (&$currentValue) { + $userValue = clone $currentValue; $userValue->login = $submittedData->username; $userValue->email = $submittedData->email; diff --git a/src/lib/Form/EventSubscriber/UserFieldsSubscriber.php b/src/lib/Form/EventSubscriber/UserFieldsSubscriber.php index 5909e7b0..6cb130e6 100644 --- a/src/lib/Form/EventSubscriber/UserFieldsSubscriber.php +++ b/src/lib/Form/EventSubscriber/UserFieldsSubscriber.php @@ -10,7 +10,6 @@ use Ibexa\ContentForms\Data\User\UserCreateData; use Ibexa\ContentForms\Data\User\UserUpdateData; -use Ibexa\Core\FieldType\User\Value; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -84,7 +83,7 @@ private function handleUserCreateData(UserCreateData $data) * @param \Ibexa\ContentForms\Data\User\UserUpdateData $data * @param $languageCode */ - private function handleUserUpdateData(UserUpdateData $data, $languageCode) + private function handleUserUpdateData(UserUpdateData $data, $languageCode): void { foreach ($data->fieldsData as $fieldData) { if ('ezuser' !== $fieldData->getFieldTypeIdentifier()) { @@ -96,9 +95,16 @@ private function handleUserUpdateData(UserUpdateData $data, $languageCode) $data->email = $userAccountFieldData->email; $data->password = $userAccountFieldData->password; $data->enabled = $userAccountFieldData->enabled; + $fieldIdentifier = $fieldData->field->fieldDefIdentifier; + + // The "user_account" field is non-translatable and therefore + // never stored under a language other than the main one: fall + // back to it when editing/translating in a different language. + $userField = $data->user->getField($fieldIdentifier, $languageCode) + ?? $data->user->getField($fieldIdentifier, $data->user->contentInfo->mainLanguageCode); /** @var \Ibexa\Core\FieldType\User\Value $userValue */ - $userValue = clone $data->user->getField($fieldData->field->fieldDefIdentifier, $languageCode)->value; + $userValue = clone $userField->value; $userValue->email = $data->email; $userValue->enabled = $data->enabled; $userValue->plainPassword = $data->password; diff --git a/src/lib/Form/Processor/User/UserTranslationSecureListener.php b/src/lib/Form/Processor/User/UserTranslationSecureListener.php new file mode 100644 index 00000000..f4981e45 --- /dev/null +++ b/src/lib/Form/Processor/User/UserTranslationSecureListener.php @@ -0,0 +1,80 @@ +userService = $userService; + $this->contentService = $contentService; + } + + public static function getSubscribedEvents(): array + { + return [ + ContentFormEvents::CONTENT_PUBLISH => ['onPublish', 5], + ContentFormEvents::CONTENT_PUBLISH_AND_EDIT => ['onPublish', 5], + ]; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\ContentValidationException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\ContentFieldValidationException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException + */ + public function onPublish(FormActionEvent $event): void + { + $content = $event->getPayload('content'); + if (null === $content || !$this->userService->isUser($content)) { + return; + } + + $languageCode = $event->getForm()->getConfig()->getOption('languageCode'); + if ($languageCode === $content->contentInfo->mainLanguageCode) { + return; + } + + $user = $this->userService->loadUser($content->id, [$languageCode]); + $userStruct = $this->userService->newUserUpdateStruct(); + $userStruct->contentUpdateStruct = $this->contentService->newContentUpdateStruct(); + $userStruct->contentUpdateStruct->initialLanguageCode = $languageCode; + + $this->userService->updateUser($user, $userStruct); + } +} diff --git a/src/lib/Form/Processor/User/UserUpdateFormProcessor.php b/src/lib/Form/Processor/User/UserUpdateFormProcessor.php index 4dc3b251..7c5c76a0 100644 --- a/src/lib/Form/Processor/User/UserUpdateFormProcessor.php +++ b/src/lib/Form/Processor/User/UserUpdateFormProcessor.php @@ -80,8 +80,14 @@ public function processUpdate(FormActionEvent $event) private function setContentFields(UserUpdateData $data, string $languageCode): void { $data->contentUpdateStruct = $this->contentService->newContentUpdateStruct(); + $data->contentUpdateStruct->initialLanguageCode = $languageCode; + $mainLanguageCode = $data->user->contentInfo->mainLanguageCode; foreach ($data->fieldsData as $fieldDefIdentifier => $fieldData) { + if ($mainLanguageCode !== $languageCode && !$fieldData->fieldDefinition->isTranslatable) { + continue; + } + $data->contentUpdateStruct->setField($fieldDefIdentifier, $fieldData->value, $languageCode); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a50889fe..49b01a86 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -7,15 +7,9 @@ declare(strict_types=1); use Ibexa\Contracts\Test\Core\Bootstrapper\Bootstrapper; -use Ibexa\Contracts\Test\Core\Bootstrapper\DatabaseSchemaHook; -use Ibexa\Contracts\Test\Core\Bootstrapper\FixtureHook; require_once dirname(__DIR__) . '/vendor/autoload.php'; chdir(dirname(__DIR__)); -(new Bootstrapper())->bootstrap(null, [ - Bootstrapper::class => [Bootstrapper::OPTION_PREPARE_DATABASE => false], - DatabaseSchemaHook::class => [DatabaseSchemaHook::OPTION_LOAD_SCHEMA => false], - FixtureHook::class => [FixtureHook::OPTION_LOAD_FIXTURES => false], -]); +(new Bootstrapper())->bootstrap(); diff --git a/tests/integration/UserTranslationSecureListenerTest.php b/tests/integration/UserTranslationSecureListenerTest.php new file mode 100644 index 00000000..1fe789e3 --- /dev/null +++ b/tests/integration/UserTranslationSecureListenerTest.php @@ -0,0 +1,110 @@ +getIbexaTestCore(); + $userService = $ibexaTestCore->getUserService(); + $contentService = $ibexaTestCore->getContentService(); + $contentTypeService = $ibexaTestCore->getContentTypeService(); + + $user = $this->createUser($login, 'John', 'Doe'); + $mainLanguageCode = $user->contentInfo->mainLanguageCode; + + $userContentType = $contentTypeService->loadContentTypeByIdentifier('user'); + $translatableFieldDefinitions = []; + foreach ($userContentType->fieldDefinitions as $fieldDefinition) { + if ( + $fieldDefinition->identifier !== 'user_account' + && $fieldDefinition->isTranslatable + && \in_array($fieldDefinition->fieldTypeIdentifier, ['ezstring', 'eztext'], true) + ) { + $translatableFieldDefinitions[] = $fieldDefinition; + } + } + self::assertNotEmpty($translatableFieldDefinitions, 'Expected the "user" content type to have translatable fields besides "user_account".'); + + $draft = $contentService->createContentDraft($user->contentInfo); + $updateStruct = $contentService->newContentUpdateStruct(); + $updateStruct->initialLanguageCode = self::NEW_LANGUAGE; + foreach ($translatableFieldDefinitions as $fieldDefinition) { + $updateStruct->setField($fieldDefinition->identifier, 'German value', self::NEW_LANGUAGE); + } + $updatedDraft = $contentService->updateContent($draft->versionInfo, $updateStruct); + $publishedContent = $contentService->publishVersion($updatedDraft->versionInfo); + + $form = new Form( + (new FormConfigBuilder('form', null, new EventDispatcher(), [ + 'languageCode' => self::NEW_LANGUAGE, + ]))->getFormConfig() + ); + + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber(new UserTranslationSecureListener($userService, $contentService)); + $eventDispatcher->dispatch( + new FormActionEvent( + $form, + null, + 'publish', + [], + ['content' => $publishedContent] + ), + $eventName + ); + + $userAfterTranslation = $userService->loadUserByLogin($login); + self::assertSame($login, $userAfterTranslation->login); + self::assertSame($email, $userAfterTranslation->email); + self::assertTrue($userAfterTranslation->enabled); + + $contentService->deleteTranslation($publishedContent->contentInfo, self::NEW_LANGUAGE); + $userAfterDeletion = $userService->loadUserByLogin($login); + + self::assertSame($login, $userAfterDeletion->login); + self::assertSame($email, $userAfterDeletion->email); + self::assertTrue($userAfterDeletion->enabled); + self::assertSame($userAfterTranslation->passwordHash, $userAfterDeletion->passwordHash); + + $contentAfterDeletion = $contentService->loadContent($publishedContent->id, [$mainLanguageCode]); + self::assertSame( + [$mainLanguageCode], + $contentAfterDeletion->versionInfo->languageCodes + ); + } + + /** + * @return iterable + */ + public static function providePublishEventNames(): iterable + { + return [ + 'publish' => [ContentFormEvents::CONTENT_PUBLISH, 'publish'], + 'publish and edit' => [ContentFormEvents::CONTENT_PUBLISH_AND_EDIT, 'publish_and_edit'], + ]; + } +} diff --git a/tests/lib/FieldType/Mapper/UserAccountFieldValueFormMapperTransformerTest.php b/tests/lib/FieldType/Mapper/UserAccountFieldValueFormMapperTransformerTest.php new file mode 100644 index 00000000..9b7907c0 --- /dev/null +++ b/tests/lib/FieldType/Mapper/UserAccountFieldValueFormMapperTransformerTest.php @@ -0,0 +1,179 @@ + [], + 'isTranslatable' => false, + 'defaultValue' => new ApiUserValue(), + ]); + + $data = $this->createMock(FieldData::class); + $data->expects(self::once()) + ->method('__get') + ->with('fieldDefinition') + ->willReturn($fieldDefinition); + + $config = $this->createStub(FormConfigInterface::class); + $config->method('getOption') + ->willReturnMap([ + ['languageCode', null, 'ger-DE'], + ['mainLanguageCode', null, 'eng-GB'], + ]); + + $formFactory = $this->getMockBuilder(FormFactoryInterface::class) + ->setMethods(['addModelTransformer', 'setAutoInitialize', 'getForm']) + ->getMockForAbstractClass(); + $formFactory->method('createBuilder')->willReturn($formFactory); + $formFactory->expects(self::once()) + ->method('addModelTransformer') + ->with(self::isInstanceOf(CallbackTransformer::class)) + ->willReturn($formFactory); + $formFactory->expects(self::once()) + ->method('setAutoInitialize') + ->with(false) + ->willReturn($formFactory); + $formFactory->expects(self::once()) + ->method('create') + ->with( + $this->anything(), + $this->anything(), + $this->callback(static function (array $options): bool { + return true === $options['disabled'] && 'update' === $options['intent']; + }) + ) + ->willReturn($formFactory); + + $builtForm = $this->createStub(FormInterface::class); + $formFactory->expects(self::once()) + ->method('getForm') + ->willReturn($builtForm); + + $config->method('getFormFactory')->willReturn($formFactory); + + $fieldForm = $this->createMock(FormInterface::class); + $fieldForm->method('getConfig')->willReturn($config); + $fieldForm->expects(self::once()) + ->method('add') + ->with($builtForm); + + $rootData = new ContentTranslationData(); + $formRoot = $this->createMock(FormInterface::class); + $formRoot->expects(self::once()) + ->method('getData') + ->willReturn($rootData); + $rootConfig = $this->createMock(FormConfigInterface::class); + $rootConfig->expects(self::once()) + ->method('getOption') + ->with('intent') + ->willReturn('update'); + $formRoot->method('getConfig')->willReturn($rootConfig); + + $userEditForm = $this->createStub(FormInterface::class); + $userEditForm->method('getRoot')->willReturn($formRoot); + $fieldForm->method('getRoot')->willReturn($userEditForm); + + $mapper->mapFieldValueForm($fieldForm, $data); + } + + /** + * @dataProvider provideReverseTransformCases + * + * @param array{login: string, email: string, enabled: bool, maxLogin: int} $expected + */ + public function testModelTransformerForTranslationReverseTransform( + ApiUserValue $defaultValue, + ?ApiUserValue $currentValue, + UserAccountFieldData $submittedData, + ApiUserValue $baseValueForComparison, + array $expected + ): void { + $mapper = new UserAccountFieldValueFormMapper(); + $fieldDefinition = new FieldDefinition(['names' => [], 'defaultValue' => $defaultValue]); + + $transformer = $mapper->getModelTransformerForTranslation($fieldDefinition); + + if (null !== $currentValue) { + $transformer->transform($currentValue); + } + + $result = $transformer->reverseTransform($submittedData); + + self::assertNotSame($baseValueForComparison, $result); + self::assertSame($expected['login'], $result->login); + self::assertSame($expected['email'], $result->email); + self::assertSame($expected['enabled'], $result->enabled); + self::assertSame($expected['maxLogin'], $result->maxLogin); + } + + /** + * @return iterable + */ + public function provideReverseTransformCases(): iterable + { + $currentValue = new ApiUserValue([ + 'login' => 'current-login', + 'email' => 'current@example.com', + 'enabled' => true, + 'maxLogin' => 5, + ]); + + yield 'clones last transformed value on reverse transform' => [ + new ApiUserValue(['login' => 'default-login', 'email' => 'default@example.com']), + $currentValue, + new UserAccountFieldData('new-login', '', 'new@example.com', false), + $currentValue, + [ + 'login' => 'new-login', + 'email' => 'new@example.com', + 'enabled' => false, + 'maxLogin' => 5, + ], + ]; + + $defaultValue = new ApiUserValue(['login' => 'default-login', 'maxLogin' => 3]); + + yield 'falls back to field definition default value' => [ + $defaultValue, + null, + new UserAccountFieldData('new-login', '', 'new@example.com', true), + $defaultValue, + [ + 'login' => 'new-login', + 'email' => 'new@example.com', + 'enabled' => true, + 'maxLogin' => 3, + ], + ]; + } +} diff --git a/tests/lib/Form/EventSubscriber/UserFieldsSubscriberTest.php b/tests/lib/Form/EventSubscriber/UserFieldsSubscriberTest.php new file mode 100644 index 00000000..57d16e84 --- /dev/null +++ b/tests/lib/Form/EventSubscriber/UserFieldsSubscriberTest.php @@ -0,0 +1,126 @@ + $fields + */ + private function createUser(string $mainLanguageCode, array $fields): User + { + $contentInfo = new ContentInfo(['mainLanguageCode' => $mainLanguageCode]); + $versionInfo = new VersionInfo(['contentInfo' => $contentInfo]); + $content = new Content(['versionInfo' => $versionInfo, 'internalFields' => $fields]); + + return new User(['content' => $content]); + } + + private function createForm(string $languageCode): Form + { + return new Form( + (new FormConfigBuilder('form', null, new EventDispatcher(), [ + 'languageCode' => $languageCode, + ]))->getFormConfig() + ); + } + + /** + * @dataProvider provideUserAccountValueResolutionCases + * + * @param list $existingFields + */ + public function testHandleUserAccountFieldResolvesBaseValueByLanguage( + array $existingFields, + string $mainLanguageCode, + string $formLanguageCode, + string $expectedBaseLogin + ): void { + $user = $this->createUser($mainLanguageCode, $existingFields); + + $data = new UserUpdateData(); + $data->user = $user; + + $fieldData = new FieldData([ + 'field' => new Field(['fieldDefIdentifier' => 'user_account']), + 'fieldDefinition' => new FieldDefinition([ + 'identifier' => 'user_account', + 'fieldTypeIdentifier' => 'ezuser', + ]), + 'value' => new UserAccountFieldData('submitted-login', 'new-password', 'new@example.com', false), + ]); + $data->addFieldData($fieldData); + + $event = new FormEvent($this->createForm($formLanguageCode), $data); + + (new UserFieldsSubscriber())->handleUserAccountField($event); + + /** @var \Ibexa\Core\FieldType\User\Value $result */ + $result = $fieldData->value; + + self::assertSame($expectedBaseLogin, $result->login); + self::assertSame('new@example.com', $result->email); + self::assertFalse($result->enabled); + self::assertSame('new-password', $result->plainPassword); + } + + /** + * @return iterable, 1: string, 2: string, 3: string}> + */ + public function provideUserAccountValueResolutionCases(): iterable + { + yield 'falls back to main language field when translation has none' => [ + [ + new Field([ + 'fieldDefIdentifier' => 'user_account', + 'languageCode' => 'eng-GB', + 'value' => new ApiUserValue(['login' => 'main-login']), + ]), + ], + 'eng-GB', + 'ger-DE', + 'main-login', + ]; + + yield 'uses the field for the requested language directly when present' => [ + [ + new Field([ + 'fieldDefIdentifier' => 'user_account', + 'languageCode' => 'eng-GB', + 'value' => new ApiUserValue(['login' => 'main-login']), + ]), + new Field([ + 'fieldDefIdentifier' => 'user_account', + 'languageCode' => 'ger-DE', + 'value' => new ApiUserValue(['login' => 'translated-login']), + ]), + ], + 'eng-GB', + 'ger-DE', + 'translated-login', + ]; + } +} diff --git a/tests/lib/Form/Processor/User/UserTranslationSecureListenerTest.php b/tests/lib/Form/Processor/User/UserTranslationSecureListenerTest.php new file mode 100644 index 00000000..8c73be7c --- /dev/null +++ b/tests/lib/Form/Processor/User/UserTranslationSecureListenerTest.php @@ -0,0 +1,89 @@ + 42, 'mainLanguageCode' => $mainLanguageCode]); + $versionInfo = new VersionInfo(['contentInfo' => $contentInfo]); + + return new Content(['versionInfo' => $versionInfo]); + } + + private function createForm(string $languageCode): Form + { + return new Form( + (new FormConfigBuilder('form', null, new EventDispatcher(), [ + 'languageCode' => $languageCode, + ]))->getFormConfig() + ); + } + + private function createEvent(string $formLanguageCode, string $contentMainLanguageCode): FormActionEvent + { + return new FormActionEvent( + $this->createForm($formLanguageCode), + null, + 'publish', + [], + ['content' => $this->createContent($contentMainLanguageCode)] + ); + } + + public function testOnPublishDoesNothingForNonUserContent(): void + { + $event = $this->createEvent('ger-DE', 'eng-GB'); + + $userService = $this->createMock(UserService::class); + $userService->expects(self::once()) + ->method('isUser') + ->with($event->getPayload('content')) + ->willReturn(false); + $userService->expects(self::never())->method('loadUser'); + $userService->expects(self::never())->method('updateUser'); + + $contentService = $this->createMock(ContentService::class); + $contentService->expects(self::never())->method('newContentUpdateStruct'); + + $listener = new UserTranslationSecureListener($userService, $contentService); + + $listener->onPublish($event); + } + + public function testOnPublishDoesNothingWhenPublishingMainLanguage(): void + { + $event = $this->createEvent('eng-GB', 'eng-GB'); + + $userService = $this->createMock(UserService::class); + $userService->method('isUser')->willReturn(true); + $userService->expects(self::never())->method('loadUser'); + $userService->expects(self::never())->method('updateUser'); + + $contentService = $this->createMock(ContentService::class); + $contentService->expects(self::never())->method('newContentUpdateStruct'); + + $listener = new UserTranslationSecureListener($userService, $contentService); + + $listener->onPublish($event); + } +} diff --git a/tests/lib/Form/Processor/User/UserUpdateFormProcessorTest.php b/tests/lib/Form/Processor/User/UserUpdateFormProcessorTest.php new file mode 100644 index 00000000..efd51ff7 --- /dev/null +++ b/tests/lib/Form/Processor/User/UserUpdateFormProcessorTest.php @@ -0,0 +1,120 @@ + 42, 'mainLanguageCode' => $mainLanguageCode]); + $versionInfo = new VersionInfo(['contentInfo' => $contentInfo]); + $content = new Content(['versionInfo' => $versionInfo]); + + return new User(['content' => $content]); + } + + private function createField(string $identifier, bool $isTranslatable, string $value): FieldData + { + $field = $this->createStub(FieldData::class); + $field->value = $value; + $field->method('__get') + ->willReturn(new FieldDefinition(['identifier' => $identifier, 'isTranslatable' => $isTranslatable])); + + return $field; + } + + private function callSetContentFields(UserUpdateFormProcessor $processor, UserUpdateData $data, string $languageCode): void + { + $method = new ReflectionMethod(UserUpdateFormProcessor::class, 'setContentFields'); + $method->setAccessible(true); + $method->invoke($processor, $data, $languageCode); + } + + private function createUserUpdateData(): UserUpdateData + { + $data = new UserUpdateData(); + $data->user = $this->createUser('eng-GB'); + $data->addFieldData($this->createField('title', true, 'translatable-value')); + $data->addFieldData($this->createField('user_account', false, 'non-translatable-value')); + + return $data; + } + + private function createProcessor(ContentUpdateStruct $contentUpdateStruct): UserUpdateFormProcessor + { + $contentService = $this->createMock(ContentService::class); + $contentService->expects(self::once()) + ->method('newContentUpdateStruct') + ->willReturn($contentUpdateStruct); + + return new UserUpdateFormProcessor( + $this->createStub(UserService::class), + $contentService, + $this->createStub(UrlGeneratorInterface::class) + ); + } + + /** + * @dataProvider provideFieldUpdatesForLanguage + * + * @param list $expectedFieldUpdates + */ + public function testSetContentFieldsUpdatesFieldsForLanguage(string $languageCode, array $expectedFieldUpdates): void + { + $data = $this->createUserUpdateData(); + + $contentUpdateStruct = $this->getMockBuilder(ContentUpdateStruct::class)->getMock(); + $contentUpdateStruct + ->expects(self::exactly(count($expectedFieldUpdates))) + ->method('setField') + ->withConsecutive(...$expectedFieldUpdates); + + $processor = $this->createProcessor($contentUpdateStruct); + + $this->callSetContentFields($processor, $data, $languageCode); + + self::assertSame($languageCode, $contentUpdateStruct->initialLanguageCode); + } + + /** + * @return iterable}> + */ + public function provideFieldUpdatesForLanguage(): iterable + { + yield 'non-translatable field is skipped on non-main language update' => [ + 'ger-DE', + [ + ['title', 'translatable-value', 'ger-DE'], + ], + ]; + + yield 'all fields are updated on main language update' => [ + 'eng-GB', + [ + ['title', 'translatable-value', 'eng-GB'], + ['user_account', 'non-translatable-value', 'eng-GB'], + ], + ]; + } +}