Skip to content

Commit

Permalink
IBX-7717: [REST] Implemented extended-info endpoint for UDW (#1197)
Browse files Browse the repository at this point in the history
Key changes:

* Added REST endpoint with location permission info for UDW (`GET /location/tree/{locationId}/extended-info`)

* [Tests] Added REST integration coverage for the endpoint

---------

Co-Authored-By: Michał Grabowski <GrabowskiM@users.noreply.github.com>
Co-authored-by: Andrew Longosz <alongosz@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 10, 2024
1 parent 44388a7 commit 3f23e91
Show file tree
Hide file tree
Showing 17 changed files with 606 additions and 11 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"twig/string-extra": "^3.0"
},
"require-dev": {
"dama/doctrine-test-bundle": "^v6.7",
"ibexa/ci-scripts": "^0.2@dev",
"ibexa/behat": "~4.5.0@dev",
"friendsofphp/php-cs-fixer": "^3.0",
Expand Down
3 changes: 3 additions & 0 deletions phpunit.integration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@
<directory>tests/integration</directory>
</testsuite>
</testsuites>
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
</extensions>
</phpunit>
152 changes: 143 additions & 9 deletions src/bundle/Controller/Content/ContentTreeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,56 @@

namespace Ibexa\Bundle\AdminUi\Controller\Content;

use Ibexa\AdminUi\Permission\LookupLimitationsTransformer;
use Ibexa\AdminUi\REST\Value\ContentTree\LoadSubtreeRequestNode;
use Ibexa\AdminUi\REST\Value\ContentTree\Node;
use Ibexa\AdminUi\REST\Value\ContentTree\NodeExtendedInfo;
use Ibexa\AdminUi\REST\Value\ContentTree\Root;
use Ibexa\AdminUi\Specification\ContentType\ContentTypeIsUser;
use Ibexa\AdminUi\UI\Module\ContentTree\NodeFactory;
use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
use Ibexa\Contracts\Core\Limitation\Target;
use Ibexa\Contracts\Core\Repository\LocationService;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\Values\Content\Location;
use Ibexa\Contracts\Core\Repository\Values\Content\Query;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
use Ibexa\Rest\Message;
use Ibexa\Rest\Server\Controller as RestController;
use Symfony\Component\HttpFoundation\Request;

/**
* @phpstan-import-type TPermissionRestrictions from \Ibexa\AdminUi\REST\Value\ContentTree\NodeExtendedInfo
*/
class ContentTreeController extends RestController
{
/** @var \Ibexa\Contracts\Core\Repository\LocationService */
private $locationService;
private LocationService $locationService;

/** @var \Ibexa\AdminUi\UI\Module\ContentTree\NodeFactory */
private $contentTreeNodeFactory;
private PermissionCheckerInterface $permissionChecker;

private LookupLimitationsTransformer $lookupLimitationsTransformer;

private NodeFactory $contentTreeNodeFactory;

private PermissionResolver $permissionResolver;

private ConfigResolverInterface $configResolver;

/**
* @param \Ibexa\Contracts\Core\Repository\LocationService $locationService
* @param \Ibexa\AdminUi\UI\Module\ContentTree\NodeFactory $contentTreeNodeFactory
*/
public function __construct(
LocationService $locationService,
NodeFactory $contentTreeNodeFactory
PermissionCheckerInterface $permissionChecker,
LookupLimitationsTransformer $lookupLimitationsTransformer,
NodeFactory $contentTreeNodeFactory,
PermissionResolver $permissionResolver,
ConfigResolverInterface $configResolver
) {
$this->locationService = $locationService;
$this->permissionChecker = $permissionChecker;
$this->lookupLimitationsTransformer = $lookupLimitationsTransformer;
$this->contentTreeNodeFactory = $contentTreeNodeFactory;
$this->permissionResolver = $permissionResolver;
$this->configResolver = $configResolver;
}

/**
Expand Down Expand Up @@ -110,6 +132,118 @@ public function loadSubtreeAction(Request $request): Root

return new Root($elements);
}

/**
* @internal for internal use by this package
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
public function loadNodeExtendedInfoAction(Location $location): NodeExtendedInfo
{
$locationPermissionRestrictions = $this->getLocationPermissionRestrictions($location);

return new NodeExtendedInfo($locationPermissionRestrictions);
}

/**
* @return TPermissionRestrictions
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
private function getLocationPermissionRestrictions(Location $location): array
{
$lookupCreateLimitationsResult = $this->permissionChecker->getContentCreateLimitations($location);
$lookupUpdateLimitationsResult = $this->permissionChecker->getContentUpdateLimitations($location);

$createLimitationsValues = $this->lookupLimitationsTransformer->getGroupedLimitationValues(
$lookupCreateLimitationsResult,
[Limitation::CONTENTTYPE, Limitation::LANGUAGE]
);

$updateLimitationsValues = $this->lookupLimitationsTransformer->getGroupedLimitationValues(
$lookupUpdateLimitationsResult,
[Limitation::LANGUAGE]
);

return [
'create' => [
'hasAccess' => $lookupCreateLimitationsResult->hasAccess(),
'restrictedContentTypeIds' => $createLimitationsValues[Limitation::CONTENTTYPE],
'restrictedLanguageCodes' => $createLimitationsValues[Limitation::LANGUAGE],
],
'edit' => [
'hasAccess' => $lookupUpdateLimitationsResult->hasAccess(),
// skipped content type limitation values as in this case it can be inferred from "hasAccess" above
'restrictedLanguageCodes' => $updateLimitationsValues[Limitation::LANGUAGE],
],
'delete' => [
'hasAccess' => $this->canUserRemoveContent($location),
// skipped other limitation values due to performance, until really needed
],
'hide' => [
'hasAccess' => $this->canUserHideContent($location),
// skipped other limitation values due to performance, until really needed
],
];
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
*/
private function canUserRemoveContent(Location $location): bool
{
$content = $location->getContent();
$contentType = $content->getContentType();
$contentIsUser = (new ContentTypeIsUser($this->configResolver->getParameter('user_content_type_identifier')))
->isSatisfiedBy($contentType);

$translations = $content->getVersionInfo()->getLanguageCodes();
$target = (new Target\Version())->deleteTranslations($translations);

if ($contentIsUser) {
return $this->permissionResolver->canUser(
'content',
'remove',
$content,
[$target]
);
}

if ($location->depth > 1) {
return $this->permissionResolver->canUser(
'content',
'remove',
$location->getContentInfo(),
[$location, $target]
);
}

return false;
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
*/
private function canUserHideContent(Location $location): bool
{
$content = $location->getContent();

$translations = $content->getVersionInfo()->getLanguageCodes();
$target = (new Target\Version())->deleteTranslations($translations);

return $this->permissionResolver->canUser(
'content',
'hide',
$content,
[$target]
);
}
}

class_alias(ContentTreeController::class, 'EzSystems\EzPlatformAdminUiBundle\Controller\Content\ContentTreeController');
7 changes: 7 additions & 0 deletions src/bundle/Resources/config/routing_rest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ ibexa.rest.location.tree.load_subtree:
defaults:
_controller: 'Ibexa\Bundle\AdminUi\Controller\Content\ContentTreeController::loadSubtreeAction'

ibexa.rest.location.tree.load_node_extended_info:
path: /location/tree/{locationId}/extended-info
methods: ['GET']
options:
expose: true
controller: 'Ibexa\Bundle\AdminUi\Controller\Content\ContentTreeController::loadNodeExtendedInfoAction'

#
# Content type create/edit form
#
Expand Down
5 changes: 5 additions & 0 deletions src/bundle/Resources/config/services/rest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ services:
tags:
- { name: ibexa.rest.output.value_object.visitor, type: Ibexa\AdminUi\REST\Value\ContentTree\Root }

Ibexa\AdminUi\REST\Output\ValueObjectVisitor\ContentTree\NodeExtendedInfoVisitor:
parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor
tags:
- { name: ibexa.rest.output.value_object.visitor, type: Ibexa\AdminUi\REST\Value\ContentTree\NodeExtendedInfo }

#
# Content type create/edit form
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@
}
}
}

.ibexa-label--checkbox-radio {
padding-left: calculateRem(4px);
}
}
2 changes: 1 addition & 1 deletion src/contracts/Permission/PermissionCheckerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function getContentCreateLimitations(Location $parentLocation): LookupLim
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
public function getContentUpdateLimitations(Location $parentLocation): LookupLimitationResult;
public function getContentUpdateLimitations(Location $location): LookupLimitationResult;
}

class_alias(PermissionCheckerInterface::class, 'EzSystems\EzPlatformAdminUi\Permission\PermissionCheckerInterface');
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\AdminUi\REST\Output\ValueObjectVisitor\ContentTree;

use Ibexa\Contracts\Rest\Output\Generator;
use Ibexa\Contracts\Rest\Output\ValueObjectVisitor;
use Ibexa\Contracts\Rest\Output\Visitor;
use Symfony\Component\HttpFoundation\Response;

/**
* @phpstan-import-type TPermissionRestrictions from \Ibexa\AdminUi\REST\Value\ContentTree\NodeExtendedInfo
*/
final class NodeExtendedInfoVisitor extends ValueObjectVisitor
{
public const MAIN_ELEMENT = 'ContentTreeNodeExtendedInfo';

/**
* @param \Ibexa\AdminUi\REST\Value\ContentTree\NodeExtendedInfo $data
*/
public function visit(Visitor $visitor, Generator $generator, $data): void
{
$generator->startObjectElement(self::MAIN_ELEMENT);
$visitor->setHeader('Content-Type', $generator->getMediaType(self::MAIN_ELEMENT));
$visitor->setStatus(Response::HTTP_OK);

$this->buildPermissionNode($data->getPermissionRestrictions(), $generator);

$generator->endObjectElement(self::MAIN_ELEMENT);
}

/**
* @phpstan-param TPermissionRestrictions $permissionRestrictions
*/
protected function buildPermissionNode(
?array $permissionRestrictions,
Generator $generator
): void {
if (null === $permissionRestrictions) {
return;
}

$generator->startList('permissions');

foreach ($permissionRestrictions as $function => $restrictions) {
$generator->startHashElement('function');
$generator->attribute('name', $function);
foreach ($restrictions as $restrictionKey => $restrictionValue) {
if (is_array($restrictionValue)) {
$generator->startHashElement($restrictionKey . 'List');
$generator->startList($restrictionKey);
foreach ($restrictionValue as $value) {
$generator->valueElement('value', $value);
}
$generator->endList($restrictionKey);
$generator->endHashElement($restrictionKey . 'List');
} elseif (is_bool($restrictionValue)) {
$generator->valueElement($restrictionKey, $generator->serializeBool($restrictionValue));
}
}
$generator->endHashElement('function');
}

$generator->endList('permissions');
}
}
48 changes: 48 additions & 0 deletions src/lib/REST/Value/ContentTree/NodeExtendedInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\AdminUi\REST\Value\ContentTree;

use Ibexa\Rest\Value as RestValue;

/**
* @phpstan-type TRestrictions array{
* hasAccess: bool,
* restrictedContentTypeIds?: array<int>,
* restrictedLanguageCodes?: array<string>,
* }
*
* @phpstan-type TPermissionRestrictions array{
* create: TRestrictions,
* edit: TRestrictions,
* delete: TRestrictions,
* hide: TRestrictions,
* }
*/
final class NodeExtendedInfo extends RestValue
{
/** @phpstan-var TPermissionRestrictions|null */
private ?array $permissions;

/**
* @phpstan-param TPermissionRestrictions|null $permissions
*/
public function __construct(
?array $permissions = null
) {
$this->permissions = $permissions;
}

/**
* @return TPermissionRestrictions|null
*/
public function getPermissionRestrictions(): ?array
{
return $this->permissions;
}
}
2 changes: 1 addition & 1 deletion src/lib/UniversalDiscovery/UniversalDiscoveryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public function getLocationPermissionRestrictions(Location $location): array
);

$updateLimitationsValues = $this->lookupLimitationsTransformer->getGroupedLimitationValues(
$lookupCreateLimitationsResult,
$lookupUpdateLimitationsResult,
[Limitation::CONTENTTYPE, Limitation::LANGUAGE]
);

Expand Down
4 changes: 4 additions & 0 deletions tests/integration/AdminUiIbexaTestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace Ibexa\Tests\Integration\AdminUi;

use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle;
use Hautelook\TemplatedUriBundle\HautelookTemplatedUriBundle;
use Ibexa\Bundle\AdminUi\IbexaAdminUiBundle;
use Ibexa\Bundle\ContentForms\IbexaContentFormsBundle;
Expand All @@ -20,6 +21,7 @@
use Ibexa\Contracts\Test\Core\IbexaTestKernel;
use Ibexa\Rest\Server\Controller\JWT;
use Knp\Bundle\MenuBundle\KnpMenuBundle;
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
use Swift_Mailer;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
Expand All @@ -39,6 +41,8 @@ public function registerBundles(): iterable
yield new HautelookTemplatedUriBundle();
yield new KnpMenuBundle();
yield new WebpackEncoreBundle();
yield new SensioFrameworkExtraBundle();
yield new DAMADoctrineTestBundle();

yield new IbexaContentFormsBundle();
yield new IbexaDesignEngineBundle();
Expand Down
Loading

0 comments on commit 3f23e91

Please sign in to comment.