Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Implement privilege for hierarchical asset collections #233

Merged
merged 4 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Classes/Aspect/HierarchicalAssetCollectionAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Flowpack\Media\Ui\Domain\Model\HierarchicalAssetCollectionInterface;
use Flowpack\Media\Ui\Utility\AssetCollectionUtility;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Neos\Media\Domain\Model\AssetCollection;
Expand All @@ -42,6 +43,13 @@ class HierarchicalAssetCollectionAspect
*/
protected $parent;

/**
* @var string
* @ORM\Column(length=1000,nullable=true)
* @Flow\Introduce("class(Neos\Media\Domain\Model\AssetCollection)")
*/
protected $path = null;

/**
* @Flow\Around("method(Neos\Media\Domain\Model\AssetCollection->getParent())")
*/
Expand All @@ -64,6 +72,7 @@ public function setParent(JoinPointInterface $joinPoint): void
if (!$parentAssetCollection instanceof AssetCollection && $parentAssetCollection !== null) {
throw new \InvalidArgumentException('Parent must be an AssetCollection', 1678330583);
}

ObjectAccess::setProperty($assetCollection, 'parent', $parentAssetCollection, true);

// Throws an error if a circular dependency has been detected
Expand All @@ -77,6 +86,9 @@ public function setParent(JoinPointInterface $joinPoint): void
), 1678330856);
}
}

$path = AssetCollectionUtility::renderValidPath($assetCollection);
ObjectAccess::setProperty($assetCollection, 'path', $path, true);
}

/**
Expand All @@ -86,7 +98,21 @@ public function unsetParent(JoinPointInterface $joinPoint): void
{
/** @var HierarchicalAssetCollectionInterface $assetCollection */
$assetCollection = $joinPoint->getProxy();
$path = AssetCollectionUtility::renderValidPath($assetCollection);
ObjectAccess::setProperty($assetCollection, 'parent', null, true);
ObjectAccess::setProperty($assetCollection, 'path', $path, true);
}

/**
* @Flow\Around("method(Neos\Media\Domain\Model\AssetCollection->__construct())")
*/
public function updatePathAfterConstruct(JoinPointInterface $joinPoint): void
{
$joinPoint->getAdviceChain()->proceed($joinPoint);

/** @var HierarchicalAssetCollectionInterface $assetCollection */
$assetCollection = $joinPoint->getProxy();
$assetCollection->setPath(AssetCollectionUtility::renderValidPath($assetCollection));
}

/**
Expand All @@ -109,6 +135,18 @@ public function getTitle(JoinPointInterface $joinPoint): string
return $assetCollection->getTitle();
}

/**
* @Flow\Around("method(Neos\Media\Domain\Model\AssetCollection->setTitle())")
*/
public function setTitle(JoinPointInterface $joinPoint): void
{
$joinPoint->getAdviceChain()->proceed($joinPoint);

/** @var HierarchicalAssetCollectionInterface $assetCollection */
$assetCollection = $joinPoint->getProxy();
$assetCollection->setPath(AssetCollectionUtility::renderValidPath($assetCollection));
}

/**
* @Flow\Around("method(Neos\Media\Domain\Model\AssetCollection->getTags())")
*/
Expand All @@ -118,4 +156,27 @@ public function getTags(JoinPointInterface $joinPoint): Collection
$assetCollection = $joinPoint->getProxy();
return $assetCollection->getTags();
}

/**
* @Flow\Around("method(Neos\Media\Domain\Model\AssetCollection->getPath())")
*/
public function getPath(JoinPointInterface $joinPoint): ?string
{
/** @var HierarchicalAssetCollectionInterface $assetCollection */
$assetCollection = $joinPoint->getProxy();
return ObjectAccess::getProperty($assetCollection, 'path', true);
}

/**
* @Flow\Around("method(Neos\Media\Domain\Model\AssetCollection->setPath())")
*/
public function setPath(JoinPointInterface $joinPoint): void
{
/** @var HierarchicalAssetCollectionInterface $assetCollection */
$assetCollection = $joinPoint->getProxy();
/** @var string $path */
$path = $joinPoint->getMethodArgument('path');
ObjectAccess::setProperty($assetCollection, 'path', $path, true);
}

}
38 changes: 34 additions & 4 deletions Classes/Command/AssetCollectionsCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/

use Flowpack\Media\Ui\Domain\Model\HierarchicalAssetCollectionInterface;
use Flowpack\Media\Ui\Service\AssetCollectionService;
use Flowpack\Media\Ui\Utility\AssetCollectionUtility;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Cli\CommandController;
use Neos\Flow\Persistence\PersistenceManagerInterface;
Expand All @@ -40,6 +42,12 @@ class AssetCollectionsCommandController extends CommandController
*/
protected $persistenceManager;

/**
* @Flow\Inject
* @var AssetCollectionService
*/
protected $assetCollectionService;

public function hierarchyCommand(): void
{
$rows = array_map(function (HierarchicalAssetCollectionInterface $assetCollection) {
Expand All @@ -50,12 +58,15 @@ public function hierarchyCommand(): void
$assetCollection->getTitle(),
$assetCollection->getParent() ? $this->persistenceManager->getIdentifierByObject($assetCollection->getParent()) : 'None',
$assetCollection->getParent() ? $assetCollection->getParent()->getTitle() : 'None',
implode(', ', array_map(static fn (AssetCollection $assetCollection) => $assetCollection->getTitle(), $children)),
implode("\n", array_map(static fn (Tag $tag) => $tag->getLabel(), $assetCollection->getTags()->toArray())),
implode(', ',
array_map(static fn(AssetCollection $assetCollection) => $assetCollection->getTitle(), $children)),
implode("\n",
array_map(static fn(Tag $tag) => $tag->getLabel(), $assetCollection->getTags()->toArray())),
$assetCollection->getPath(),
];
}, $this->assetCollectionRepository->findAll()->toArray());

$this->output->outputTable($rows, ['Id', 'Title', 'ParentId', 'Parent title', 'Children', 'Tags']);
$this->output->outputTable($rows, ['Id', 'Title', 'ParentId', 'Parent title', 'Children', 'Tags', 'Path']);
}

public function setParentCommand(string $assetCollectionIdentifier, string $parentAssetCollectionIdentifier): void
Expand All @@ -67,7 +78,26 @@ public function setParentCommand(string $assetCollectionIdentifier, string $pare
/** @var HierarchicalAssetCollectionInterface $assetCollection */
$assetCollection->setParent($parentAssetCollection);
$this->assetCollectionRepository->update($assetCollection);
$this->outputLine('Asset collection "%s" has been set as child of "%s"', [$assetCollection->getTitle(), $parentAssetCollection ? $parentAssetCollection->getTitle() : 'none']);
$this->assetCollectionService->updatePathForNestedAssetCollections($assetCollection);
$this->outputLine(
'Asset collection "%s" has been set as child of "%s"',
[$assetCollection->getTitle(), $parentAssetCollection ? $parentAssetCollection->getTitle() : 'none']
);
}

/**
* Recalculates the path of each `AssetCollection` and updates the database.
*/
public function updatePathsCommand(): void
{
$assetCollections = $this->assetCollectionRepository->findAll();
/** @var HierarchicalAssetCollectionInterface $assetCollection */
foreach ($assetCollections as $assetCollection) {
$path = AssetCollectionUtility::renderValidPath($assetCollection);
$assetCollection->setPath($path);
$this->assetCollectionRepository->update($assetCollection);
}
$this->outputLine('Paths have been updated for %d asset collections', [$assetCollections->count()]);
}

}
10 changes: 10 additions & 0 deletions Classes/Domain/Model/HierarchicalAssetCollectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@ public function unsetParent();
* @return bool
*/
public function hasParent();

/**
* @return string|null
*/
public function getPath();

/**
* @return void
*/
public function setPath(?string $path);
}
12 changes: 10 additions & 2 deletions Classes/GraphQL/Resolver/Type/MutationResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@
use Flowpack\Media\Ui\Domain\Model\HierarchicalAssetCollectionInterface;
use Flowpack\Media\Ui\Exception;
use Flowpack\Media\Ui\GraphQL\Context\AssetSourceContext;
use Flowpack\Media\Ui\Service\AssetCollectionService;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\I18n\Translator;
use Neos\Flow\Persistence\Exception\IllegalObjectTypeException;
use Neos\Flow\Persistence\Exception\InvalidQueryException;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Flow\ResourceManagement\ResourceManager;
use Neos\Http\Factories\FlowUploadedFile;
use Neos\Media\Domain\Model\Asset;
use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface;
use Neos\Media\Domain\Model\AssetCollection;
use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface;
use Neos\Media\Domain\Model\Tag;
use Neos\Media\Domain\Repository\AssetCollectionRepository;
use Neos\Media\Domain\Repository\AssetRepository;
Expand Down Expand Up @@ -107,6 +107,12 @@ class MutationResolver implements ResolverInterface
*/
protected $assetService;

/**
* @Flow\Inject
* @var AssetCollectionService
*/
protected $assetCollectionService;

/**
* @throws Exception
*/
Expand Down Expand Up @@ -695,6 +701,7 @@ public function updateAssetCollection($_, array $variables): bool
}

$this->assetCollectionRepository->update($assetCollection);
$this->assetCollectionService->updatePathForNestedAssetCollections($assetCollection);

return true;
}
Expand Down Expand Up @@ -727,6 +734,7 @@ public function setAssetCollectionParent($_, array $variables): bool
$assetCollection->setParent(null);
}
$this->assetCollectionRepository->update($assetCollection);
$this->assetCollectionService->updatePathForNestedAssetCollections($assetCollection);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Flowpack\Media\Ui\Security\Authorization\Privilege\Doctrine;

/*
* This file is part of the Flowpack.Media.Ui package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Doctrine\ORM\Query\Filter\SQLFilter as DoctrineSqlFilter;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Neos\Flow\Security\Authorization\Privilege\Entity\Doctrine\PropertyConditionGenerator;
use Neos\Flow\Validation\Validator\UuidValidator;

/**
* Condition generator covering Asset <-> HierarchicalAssetCollection relations
* (M:M relations are not supported by the Flow PropertyConditionGenerator yet)
*/
class AssetAssetCollectionConditionGenerator extends
\Neos\Media\Security\Authorization\Privilege\Doctrine\AssetAssetCollectionConditionGenerator
{

/**
* @param DoctrineSqlFilter $sqlFilter
* @param ClassMetadata $targetEntity Metadata object for the target entity to create the constraint for
* @param string $targetTableAlias The target table alias used in the current query
* @return string
*/
public function getSql(DoctrineSqlFilter $sqlFilter, ClassMetadata $targetEntity, $targetTableAlias): string
{
$propertyConditionGenerator = new PropertyConditionGenerator('');
$collectionTitleOrIdentifier = $propertyConditionGenerator->getValueForOperand($this->collectionTitleOrIdentifier);
if (preg_match(UuidValidator::PATTERN_MATCH_UUID, $collectionTitleOrIdentifier) === 1) {
$whereCondition = $targetTableAlias . '_ac.persistence_object_identifier = ' . $this->entityManager->getConnection()->quote($collectionTitleOrIdentifier);
} else {
$whereCondition = $targetTableAlias . '_ac.path LIKE ' . $this->entityManager->getConnection()->quote($collectionTitleOrIdentifier . '%');
}

return $targetTableAlias . '.persistence_object_identifier IN (
SELECT ' . $targetTableAlias . '_a.persistence_object_identifier
FROM neos_media_domain_model_asset AS ' . $targetTableAlias . '_a
LEFT JOIN neos_media_domain_model_assetcollection_assets_join ' . $targetTableAlias . '_acj ON ' . $targetTableAlias . '_a.persistence_object_identifier = ' . $targetTableAlias . '_acj.media_asset
LEFT JOIN neos_media_domain_model_assetcollection ' . $targetTableAlias . '_ac ON ' . $targetTableAlias . '_ac.persistence_object_identifier = ' . $targetTableAlias . '_acj.media_assetcollection
WHERE ' . $whereCondition . ')';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Flowpack\Media\Ui\Security\Authorization\Privilege\Doctrine;

/*
* This file is part of the Flowpack.Media.Ui package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

/**
* A SQL condition generator, supporting special SQL constraints for assets in hierarchical asset collections
*/
class AssetConditionGenerator extends \Neos\Media\Security\Authorization\Privilege\Doctrine\AssetConditionGenerator
{
public function isInCollectionPath(string $collectionTitle): AssetAssetCollectionConditionGenerator
{
return new AssetAssetCollectionConditionGenerator($collectionTitle);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Flowpack\Media\Ui\Security\Authorization\Privilege\Doctrine;

/*
* This file is part of the Flowpack.Media.Ui package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Security\Authorization\Privilege\Entity\Doctrine\PropertyConditionGenerator;
use Neos\Media\Security\Authorization\Privilege\Doctrine\AssetCollectionConditionGenerator;

/**
* An extended SQL condition generator, supporting special SQL constraints for hierarchical asset collections
*/
class HierarchicalAssetCollectionConditionGenerator extends AssetCollectionConditionGenerator
{
public function isInPath(string $path): PropertyConditionGenerator
{
return (new PropertyConditionGenerator('path'))->like($path . '%');
}
}
28 changes: 28 additions & 0 deletions Classes/Security/Authorization/Privilege/ReadAssetPrivilege.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Flowpack\Media\Ui\Security\Authorization\Privilege;

/*
* This file is part of the Flowpack.Media.Ui package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Flowpack\Media\Ui\Security\Authorization\Privilege\Doctrine\AssetConditionGenerator;

/**
* Privilege for restricting reading of Assets in hierarchical asset collections
*/
class ReadAssetPrivilege extends \Neos\Media\Security\Authorization\Privilege\ReadAssetPrivilege
{
protected function getConditionGenerator(): AssetConditionGenerator
{
return new AssetConditionGenerator();
}
}
Loading
Loading