Skip to content

Commit 8f6f1fe

Browse files
authored
IBX-8169: OpenAPI compatible content copy endpoint
#111
1 parent c4b854a commit 8f6f1fe

File tree

12 files changed

+176
-6
lines changed

12 files changed

+176
-6
lines changed

src/bundle/Resources/config/input_parsers.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,3 +898,11 @@ services:
898898
$validator: '@validator'
899899
tags:
900900
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.CopyLocationInput }
901+
902+
Ibexa\Rest\Server\Input\Parser\CopyContentInput:
903+
parent: Ibexa\Rest\Server\Common\Parser
904+
arguments:
905+
$locationService: '@ibexa.api.service.location'
906+
$validator: '@validator'
907+
tags:
908+
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.CopyContentInput }

src/bundle/Resources/config/routing.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ ibexa.rest.refresh_session:
5757
# Content
5858

5959

60+
ibexa.rest.content.copy:
61+
path: /content/objects/{contentId}
62+
controller: Ibexa\Rest\Server\Controller\Content::copy
63+
condition: 'ibexa_get_media_type(request) === "CopyContentInput"'
64+
methods: [POST]
65+
options:
66+
options_route_suffix: 'CopyContentInput'
67+
requirements:
68+
contentId: \d+
69+
6070
ibexa.rest.content.create_draft_from_version:
6171
path: /content/objects/{contentId}/versions/{versionNumber}
6272
controller: Ibexa\Rest\Server\Controller\Content::createDraftFromVersion

src/lib/Server/Controller/Content.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,38 @@ public function copyContent($contentId, Request $request)
282282
);
283283
}
284284

285+
/**
286+
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
287+
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
288+
*/
289+
public function copy(int $contentId, Request $request): Values\ResourceCreated
290+
{
291+
$contentService = $this->repository->getContentService();
292+
$locationService = $this->repository->getLocationService();
293+
294+
$contentInfo = $contentService->loadContentInfo($contentId);
295+
296+
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Location $destinationLocation */
297+
$destinationLocation = $this->inputDispatcher->parse(
298+
new Message(
299+
['Content-Type' => $request->headers->get('Content-Type')],
300+
$request->getContent(),
301+
),
302+
);
303+
304+
$copiedContent = $contentService->copyContent(
305+
$contentInfo,
306+
$locationService->newLocationCreateStruct($destinationLocation->getId()),
307+
);
308+
309+
return new Values\ResourceCreated(
310+
$this->router->generate(
311+
'ibexa.rest.load_content',
312+
['contentId' => $copiedContent->id],
313+
)
314+
);
315+
}
316+
285317
/**
286318
* Deletes a translation from all the Versions of the given Content Object.
287319
*
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Rest\Server\Input\Parser;
10+
11+
use Ibexa\Rest\Server\Validation\Builder\Input\Parser\BaseInputParserValidatorBuilder;
12+
use Ibexa\Rest\Server\Validation\Builder\Input\Parser\SwapLocationInputValidatorBuilder;
13+
14+
final class CopyContentInput extends AbstractDestinationLocationParser
15+
{
16+
protected const string PARSER = 'CopyContentInput';
17+
18+
protected function getValidatorBuilder(): BaseInputParserValidatorBuilder
19+
{
20+
return new SwapLocationInputValidatorBuilder($this->validator);
21+
}
22+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Rest\Server\Validation\Builder\Input\Parser;
10+
11+
use Ibexa\Rest\Server\Input\Parser\CopyContentInput;
12+
use Symfony\Component\Validator\Constraint;
13+
use Symfony\Component\Validator\Constraints as Assert;
14+
15+
final class CopyContentInputValidatorBuilder extends BaseInputParserValidatorBuilder
16+
{
17+
protected function buildConstraint(): Constraint
18+
{
19+
return new Assert\Collection(
20+
[
21+
CopyContentInput::DESTINATION_KEY => [
22+
new Assert\NotBlank(),
23+
new Assert\Type('string'),
24+
new Assert\Regex('/^(\/\d+)+$/'),
25+
],
26+
],
27+
);
28+
}
29+
}

tests/bundle/Functional/ContentTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,29 @@ public function testCopyContent($restContentHref)
223223
return $response->getHeader('Location')[0];
224224
}
225225

226+
/**
227+
* Covers POST /content/objects/<contentId>.
228+
*
229+
* @depends testPublishContent
230+
*/
231+
public function testCopy(string $restContentHref): void
232+
{
233+
$request = $this->createHttpRequest(
234+
'POST',
235+
$restContentHref,
236+
'CopyContentInput+json',
237+
'',
238+
json_encode(['CopyContentInput' => ['destination' => '/1/2']], JSON_THROW_ON_ERROR),
239+
);
240+
$response = $this->sendHttpRequest($request);
241+
242+
self::assertHttpResponseCodeEquals($response, 201);
243+
self::assertStringStartsWith(
244+
'/api/ibexa/v2/content/objects/',
245+
$response->getHeader('Location')[0],
246+
);
247+
}
248+
226249
/**
227250
* Covers DELETE /content/objects/<versionNumber>.
228251
*

tests/bundle/Functional/HttpOptionsTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public function providerForTestHttpOptions(): array
5757
['/content/sections', ['GET', 'POST']],
5858
['/content/sections/1', ['GET', 'PATCH', 'DELETE']],
5959
['/content/objects', ['GET', 'POST']],
60+
['/content/objects/1', ['POST'], 'CopyContentInput+json'],
6061
['/content/objects/1', ['PATCH', 'GET', 'DELETE', 'COPY']],
6162
['/content/objects/1/translations/eng-GB', ['DELETE']],
6263
['/content/objects/1/relations', ['GET']],

tests/bundle/Functional/SearchView/Criterion/IsContainerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function getCriteriaPayloads(): iterable
2828
'is container' => [
2929
'json',
3030
$this->buildJsonCriterionQuery('"IsContainerCriterion": true'),
31-
13,
31+
14,
3232
],
3333
'is not container' => [
3434
'json',

tests/bundle/Functional/SearchView/Criterion/IsUserBasedTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function getCriteriaPayloads(): iterable
2323
'is not user based' => [
2424
'json',
2525
$this->buildJsonCriterionQuery('"IsUserBasedCriterion": false'),
26-
13,
26+
14,
2727
],
2828
];
2929
}

tests/bundle/Functional/SearchView/Criterion/ObjectStateIdentifierTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ public function getCriteriaPayloads(): iterable
1818
'identifier with target group' => [
1919
'json',
2020
$this->buildJsonCriterionQuery('"ObjectStateIdentifierCriterion": {"value": "not_locked", "target": "ez_lock"}'),
21-
15,
21+
16,
2222
],
2323
'identifier without target group' => [
2424
'json',
2525
$this->buildJsonCriterionQuery('"ObjectStateIdentifierCriterion": {"value": "not_locked", "target": null}'),
26-
15,
26+
16,
2727
],
2828
];
2929
}

0 commit comments

Comments
 (0)