diff --git a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php index c1ec8e103f9..b3d4aaf0cc4 100644 --- a/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php +++ b/Neos.ContentRepository.TestSuite/Classes/Behavior/Features/Bootstrap/CRTestSuiteTrait.php @@ -296,7 +296,7 @@ protected function deserializeProperties(array $properties): PropertyValuesToWri { return PropertyValuesToWrite::fromArray( array_map( - static fn (mixed $value) => is_array($value) && isset($value['__type']) ? new $value['__type']($value['value']) : $value, + static fn (mixed $value) => is_array($value) && isset($value['__type']) ? (is_array($value['value']) ? $value['__type']::fromArray($value['value']) : new $value['__type']($value['value'])) : $value, $properties ) ); diff --git a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php index f61b26c42d9..88d42214cef 100644 --- a/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php +++ b/Neos.Neos/Classes/AssetUsage/Service/AssetUsageIndexingService.php @@ -24,6 +24,7 @@ use Neos\Neos\AssetUsage\Domain\AssetUsageRepository; use Neos\Neos\AssetUsage\Dto\AssetIdAndOriginalAssetId; use Neos\Neos\AssetUsage\Dto\AssetIdsByProperty; +use Neos\Neos\Domain\Link\Link; use Neos\Utility\TypeHandling; /** @@ -217,6 +218,14 @@ private function extractAssetIds(string $type, mixed $value): array preg_match_all('/asset:\/\/(?[\w-]*)/i', $value, $matches, PREG_SET_ORDER); return array_map(static fn (array $match) => $match['assetId'], $matches); } + + if ($value instanceof Link) { + if ($value->href->getScheme() === 'asset') { + return [$value->href->getHost() . $value->href->getPath()]; + } + return []; + } + if (is_subclass_of($type, ResourceBasedInterface::class)) { return [$this->persistenceManager->getIdentifierByObject($value)]; } diff --git a/Neos.Neos/Classes/Domain/Link/Link.php b/Neos.Neos/Classes/Domain/Link/Link.php new file mode 100644 index 00000000000..6b4899445af --- /dev/null +++ b/Neos.Neos/Classes/Domain/Link/Link.php @@ -0,0 +1,146 @@ + tags. + * + * Note that currently the link editor can only handle and write to + * the property {@see Link::$target} "_blank" | null + * and to {@see Link::$rel} ["noopener"] | null + * + * The Link values can be accessed in Fusion the following: + * + * ```fusion + * href = ${q(node).property("link").href} + * title = ${q(node).property("link").title} + * # ... + * ``` + * + * In case you need to cast the uri in {@see Link::$href} explicitly to a string + * you can use: `String.toString(link.href)` + * + * @Flow\Proxy(false) + */ +final readonly class Link implements \JsonSerializable +{ + /** + * A selection of frequently used target attribute values + */ + public const TARGET_SELF = '_self'; + public const TARGET_BLANK = '_blank'; + + /** + * A selection of frequently used rel attribute values + */ + public const REL_NOOPENER = 'noopener'; + public const REL_NOFOLLOW = 'nofollow'; + + /** + * @param array $rel + */ + private function __construct( + public UriInterface $href, + public ?string $title, + public ?string $target, + public array $rel, + public bool $download + ) { + } + + /** + * Note: The signature of this method might be extended in the future, so it should always be used with named arguments + * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments + * + * @param array $rel + */ + public static function create( + UriInterface $href, + ?string $title = null, + ?string $target = null, + array $rel = [], + bool $download = false, + ): self { + $relMap = []; + foreach ($rel as $value) { + $relMap[strtolower($value)] = true; + } + return new self( + $href, + ($title === '' || $title === null) ? null : $title, + ($target === '' || $target === null) ? null : strtolower($target), + array_keys($relMap), + $download + ); + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return self::create( + new Uri($array['href']), + $array['title'] ?? null, + $array['target'] ?? null, + $array['rel'] ?? [], + $array['download'] ?? false, + ); + } + + public static function fromString(string $string): self + { + return self::create( + href: new Uri($string) + ); + } + + /** + * Note: The signature of this method might be extended in the future, so it should always be used with named arguments + * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments + * + * @param array|null $rel + */ + public function with( + ?UriInterface $href = null, + ?string $title = null, + ?string $target = null, + ?array $rel = null, + ?bool $download = null, + ): self { + return self::create( + $href ?? $this->href, + $title ?? $this->title, + $target ?? $this->target, + $rel ?? $this->rel, + $download ?? $this->download, + ); + } + + public function jsonSerialize(): mixed + { + return [ + 'href' => $this->href->__toString(), + 'title' => $this->title, + 'target' => $this->target, + 'rel' => $this->rel, + 'download' => $this->download, + ]; + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/01-CreateNodeAggregateWithNode_WithoutDimensions.feature b/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/01-CreateNodeAggregateWithNode_WithoutDimensions.feature index e1078c44183..07840bec38c 100644 --- a/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/01-CreateNodeAggregateWithNode_WithoutDimensions.feature +++ b/Neos.Neos/Tests/Behavior/Features/AssetUsage/01-NodeCreation/01-CreateNodeAggregateWithNode_WithoutDimensions.feature @@ -10,6 +10,10 @@ Feature: Create node aggregate with node without dimensions properties: text: type: string + assetLinkString: + type: string + assetLinkObject: + type: Neos\Neos\Domain\Link\Link asset: type: Neos\Media\Domain\Model\Asset assets: @@ -35,6 +39,8 @@ Feature: Create node aggregate with node without dimensions When an asset exists with id "asset-1" And an asset exists with id "asset-2" And an asset exists with id "asset-3" + And an asset exists with id "asset-4" + And an asset exists with id "asset-5" When the command CreateWorkspace is executed with payload: | Key | Value | @@ -46,16 +52,20 @@ Feature: Create node aggregate with node without dimensions Given I am in workspace "live" And the following CreateNodeAggregateWithNode commands are executed: - | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | - | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | - | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | - | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | nodeAggregateId | nodeName | parentNodeAggregateId | nodeTypeName | initialPropertyValues | + | sir-david-nodenborough | node | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"asset": "Asset:asset-1"} | + | nody-mc-nodeface | child-node | sir-david-nodenborough | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assets": ["Asset:asset-2"]} | + | sir-nodeward-nodington-iii | esquire | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"text": "Link to asset://asset-3."} | + | sir-nodeward-lincoln | | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assetLinkString": "asset://asset-4"} | + | sir-objectward-lincoln | | lady-eleonode-rootford | Neos.ContentRepository.Testing:NodeWithAssetProperties | {"assetLinkObject": {"__type": "Neos\\\\Neos\\\\Domain\\\\Link\\\\Link", "value": { "href": "asset://asset-5" }}} | Then I expect the AssetUsageService to have the following AssetUsages: - | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | - | asset-1 | sir-david-nodenborough | asset | live | {} | - | asset-2 | nody-mc-nodeface | assets | live | {} | - | asset-3 | sir-nodeward-nodington-iii | text | live | {} | + | assetId | nodeAggregateId | propertyName | workspaceName | originDimensionSpacePoint | + | asset-1 | sir-david-nodenborough | asset | live | {} | + | asset-2 | nody-mc-nodeface | assets | live | {} | + | asset-3 | sir-nodeward-nodington-iii | text | live | {} | + | asset-4 | sir-nodeward-lincoln | assetLinkString | live | {} | + | asset-5 | sir-objectward-lincoln | assetLinkObject | live | {} | Scenario: Nodes on user workspace have been created Given I am in workspace "user-workspace" diff --git a/Neos.Neos/Tests/Unit/Domain/Link/LinkTest.php b/Neos.Neos/Tests/Unit/Domain/Link/LinkTest.php new file mode 100644 index 00000000000..8f631014eb1 --- /dev/null +++ b/Neos.Neos/Tests/Unit/Domain/Link/LinkTest.php @@ -0,0 +1,124 @@ +href); + self::assertEquals(null, $link->title); + self::assertEquals(null, $link->target); + self::assertEquals([], $link->rel); + self::assertEquals(false, $link->download); + + // another way to create it + self::assertEquals( + $link, + Link::fromString('#') + ); + } + + /** + * @test + */ + public function linkWithAttributes() + { + $link = Link::create( + href: new Uri('/bar'), + title: 'My link', + target: Link::TARGET_BLANK, + rel: [Link::REL_NOFOLLOW], + download: true + ); + + self::assertEquals('/bar', (string)$link->href); + self::assertEquals('My link', $link->title); + self::assertEquals('_blank', $link->target); + self::assertEquals(['nofollow'], $link->rel); + self::assertEquals(true, $link->download); + } + + /** + * @test + */ + public function linkWithAttributesViaWither() + { + $emptyLink = Link::create( + href: new Uri('#') + ); + + $link = $emptyLink->with( + href: new Uri('/bar'), + title: 'My link', + target: Link::TARGET_BLANK, + rel: [Link::REL_NOFOLLOW], + download: true + ); + + self::assertEquals('/bar', (string)$link->href); + self::assertEquals('My link', $link->title); + self::assertEquals('_blank', $link->target); + self::assertEquals(['nofollow'], $link->rel); + self::assertEquals(true, $link->download); + } + + /** + * @test + */ + public function linkWithAttributesOneUpdated() + { + $link = Link::create( + href: new Uri('/bar'), + title: 'My link', + target: Link::TARGET_BLANK, + rel: [Link::REL_NOFOLLOW], + download: true + )->with( + title: 'My updated link' + ); + + self::assertEquals('/bar', (string)$link->href); + self::assertEquals('My updated link', $link->title); + self::assertEquals('_blank', $link->target); + self::assertEquals(['nofollow'], $link->rel); + self::assertEquals(true, $link->download); + } + + /** + * @test + */ + public function linkWithAttributesCanBeResetViaWith() + { + $link = Link::create( + href: new Uri('/bar'), + title: 'My link', + target: Link::TARGET_BLANK, + rel: [Link::REL_NOFOLLOW], + download: true + )->with( + title: '', + target: '', + rel: [], + download: false + ); + + self::assertEquals('/bar', (string)$link->href); + self::assertEquals(null, $link->title); + self::assertEquals(null, $link->target); + self::assertEquals([], $link->rel); + self::assertEquals(false, $link->download); + } +}