diff --git a/src/bundle/Resources/config/input_parsers.yml b/src/bundle/Resources/config/input_parsers.yml index 011711041..1781120cc 100644 --- a/src/bundle/Resources/config/input_parsers.yml +++ b/src/bundle/Resources/config/input_parsers.yml @@ -882,3 +882,11 @@ services: $validator: '@validator' tags: - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.RestoreTrashItemInput } + + Ibexa\Rest\Server\Input\Parser\MoveUserGroupInput: + parent: Ibexa\Rest\Server\Common\Parser + arguments: + $locationService: '@ibexa.api.service.location' + $validator: '@validator' + tags: + - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.MoveUserGroupInput } diff --git a/src/bundle/Resources/config/routing.yml b/src/bundle/Resources/config/routing.yml index 26c79d905..7108991c6 100644 --- a/src/bundle/Resources/config/routing.yml +++ b/src/bundle/Resources/config/routing.yml @@ -1008,6 +1008,15 @@ ibexa.rest.delete_policy: # Users +ibexa.rest.user_group.move: + path: /user/groups/{groupPath} + controller: Ibexa\Rest\Server\Controller\User::moveGroup + condition: 'ibexa_get_media_type(request) === "MoveUserGroupInput"' + methods: [POST] + options: + options_route_suffix: 'MoveUserGroupInput' + requirements: + groupPath: "[0-9/]+" ibexa.rest.verify_users: path: /user/users diff --git a/src/lib/Server/Controller/User.php b/src/lib/Server/Controller/User.php index a75ca5b7b..a472eb6df 100644 --- a/src/lib/Server/Controller/User.php +++ b/src/lib/Server/Controller/User.php @@ -631,6 +631,50 @@ public function moveUserGroup(string $groupPath, Request $request): Values\Resou ); } + /** + * @throws \Ibexa\Contracts\Rest\Exceptions\ForbiddenException + * @throws \Ibexa\Core\Base\Exceptions\UnauthorizedException + */ + public function moveGroup(string $groupPath, Request $request): Values\ResourceCreated + { + $userGroupLocation = $this->locationService->loadLocation( + $this->extractLocationIdFromPath($groupPath) + ); + + $userGroup = $this->userService->loadUserGroup( + $userGroupLocation->contentId, + ); + + try { + /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Location $destinationLocation */ + $destinationLocation = $this->inputDispatcher->parse( + new Message( + ['Content-Type' => $request->headers->get('Content-Type')], + $request->getContent(), + ), + ); + } catch (ApiExceptions\NotFoundException $e) { + throw new ForbiddenException(/** @Ignore */ $e->getMessage(), 1, $e); + } + + $destinationGroup = $this->userService->loadUserGroup( + $destinationLocation->getContent()->getId(), + ); + + $this->userService->moveUserGroup($userGroup, $destinationGroup); + + return new Values\ResourceCreated( + $this->router->generate( + 'ibexa.rest.load_user_group', + [ + 'groupPath' => trim($destinationLocation->pathString, '/') + . '/' + . $userGroupLocation->getId(), + ], + ) + ); + } + /** * Returns a list of the sub groups. */ diff --git a/src/lib/Server/Input/Parser/MoveUserGroupInput.php b/src/lib/Server/Input/Parser/MoveUserGroupInput.php new file mode 100644 index 000000000..7f20ef290 --- /dev/null +++ b/src/lib/Server/Input/Parser/MoveUserGroupInput.php @@ -0,0 +1,22 @@ +validator); + } +} diff --git a/src/lib/Server/Validation/Builder/Input/Parser/MoveUserGroupInputValidatorBuilder.php b/src/lib/Server/Validation/Builder/Input/Parser/MoveUserGroupInputValidatorBuilder.php new file mode 100644 index 000000000..56b8b2bc4 --- /dev/null +++ b/src/lib/Server/Validation/Builder/Input/Parser/MoveUserGroupInputValidatorBuilder.php @@ -0,0 +1,29 @@ + [ + new Assert\NotBlank(), + new Assert\Type('string'), + new Assert\Regex('/^(\/\d+)+$/'), + ], + ], + ); + } +} diff --git a/tests/bundle/Functional/HttpOptionsTest.php b/tests/bundle/Functional/HttpOptionsTest.php index 1cc4dc619..f0a4deda9 100644 --- a/tests/bundle/Functional/HttpOptionsTest.php +++ b/tests/bundle/Functional/HttpOptionsTest.php @@ -123,6 +123,7 @@ public function providerForTestHttpOptions(): array ['/user/groups/root', ['GET']], ['/user/groups/subgroups', ['POST']], ['/user/groups/4', ['GET', 'PATCH', 'DELETE', 'MOVE']], + ['/user/groups/4', ['POST'], 'MoveUserGroupInput+json'], ['/user/groups/4/subgroups', ['GET', 'POST']], ['/user/groups/4/users', ['GET', 'POST']], ['/user/groups/4/roles', ['GET', 'POST']], diff --git a/tests/bundle/Functional/UserTest.php b/tests/bundle/Functional/UserTest.php index 3f101063c..d7c1a57d8 100644 --- a/tests/bundle/Functional/UserTest.php +++ b/tests/bundle/Functional/UserTest.php @@ -9,6 +9,7 @@ namespace Ibexa\Tests\Bundle\Rest\Functional; use Ibexa\Tests\Bundle\Rest\Functional\TestCase as RESTFunctionalTestCase; +use Psr\Http\Message\ResponseInterface; final class UserTest extends RESTFunctionalTestCase { @@ -379,7 +380,7 @@ public function testUnassignUserFromUserGroup(string $userHref): void * * @depends testCreateUserGroup */ - public function testMoveUserGroup(string $groupHref): void + public function testMoveUserGroup(string $groupHref): ResponseInterface { $request = $this->createHttpRequest( 'MOVE', @@ -392,6 +393,57 @@ public function testMoveUserGroup(string $groupHref): void $response = $this->sendHttpRequest($request); self::assertHttpResponseCodeEquals($response, 201); + + return $response; + } + + /** + * @depends testMoveUserGroup + */ + public function testMoveGroup(ResponseInterface $response): ResponseInterface + { + $userGroupHref = $response->getHeader('Location')[0]; + + $request = $this->createHttpRequest( + 'POST', + $userGroupHref, + 'MoveUserGroupInput+json', + '', + json_encode( + ['MoveUserGroupInput' => ['destination' => '/1/5']], + JSON_THROW_ON_ERROR, + ), + ); + + $response = $this->sendHttpRequest($request); + + self::assertHttpResponseCodeEquals($response, 201); + self::assertHttpResponseHasHeader($response, 'Location'); + + return $response; + } + + /** + * @depends testMoveGroup + */ + public function testMoveGroupToMissingLocationThrowsForbiddenException(ResponseInterface $response): void + { + $userGroupHref = $response->getHeader('Location')[0]; + + $request = $this->createHttpRequest( + 'POST', + $userGroupHref, + 'MoveUserGroupInput+json', + '', + json_encode( + ['MoveUserGroupInput' => ['destination' => '/1/5/333999']], + JSON_THROW_ON_ERROR, + ), + ); + + $response = $this->sendHttpRequest($request); + + self::assertHttpResponseCodeEquals($response, 403); } /** diff --git a/tests/lib/Server/Input/Parser/MoveUserGroupInputTest.php b/tests/lib/Server/Input/Parser/MoveUserGroupInputTest.php new file mode 100644 index 000000000..dcb23dc95 --- /dev/null +++ b/tests/lib/Server/Input/Parser/MoveUserGroupInputTest.php @@ -0,0 +1,45 @@ +parse(); + } + + public function testParseExceptionOnMissingDestinationElement(): void + { + $this->parseExceptionOnMissingDestinationElement(self::PARSER); + } + + public function testParseExceptionOnInvalidDestinationElement(): void + { + $this->parseExceptionOnInvalidDestinationElement(self::PARSER); + } + + protected function internalGetParser(): MoveUserGroupInput + { + $locationService = $this->createMock(LocationService::class); + $this->locationService = $locationService; + $this->validator = Validation::createValidator(); + + return new MoveUserGroupInput( + $this->locationService, + $this->validator, + ); + } +}