@amazeelabs/silverback-gutenberg 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +983 -0
- package/drupal/silverback_gutenberg/README.md +439 -0
- package/drupal/silverback_gutenberg/composer.json +20 -0
- package/drupal/silverback_gutenberg/config/install/silverback_gutenberg.settings.yml +1 -0
- package/drupal/silverback_gutenberg/config/schema/silverback_gutenberg.schema.yml +13 -0
- package/drupal/silverback_gutenberg/css/gutenberg-tweaks.css +46 -0
- package/drupal/silverback_gutenberg/directives.gql +40 -0
- package/drupal/silverback_gutenberg/directives.graphql +46 -0
- package/drupal/silverback_gutenberg/js/base.js +24 -0
- package/drupal/silverback_gutenberg/js/gutenberg-tweaks.js +154 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.api.php +76 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.info.yml +8 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.libraries.yml +14 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.module +97 -0
- package/drupal/silverback_gutenberg/silverback_gutenberg.services.yml +29 -0
- package/drupal/silverback_gutenberg/src/Annotation/GutenbergBlockMutator.php +39 -0
- package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidator.php +37 -0
- package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidatorRule.php +37 -0
- package/drupal/silverback_gutenberg/src/Attribute/GutenbergBlockMutator.php +29 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorBase.php +24 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorInterface.php +41 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManager.php +114 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManagerInterface.php +30 -0
- package/drupal/silverback_gutenberg/src/BlockMutator/EntityBlockMutatorBase.php +189 -0
- package/drupal/silverback_gutenberg/src/BlockSerializer.php +84 -0
- package/drupal/silverback_gutenberg/src/Controller/LinkitAutocomplete.php +84 -0
- package/drupal/silverback_gutenberg/src/Directives.php +74 -0
- package/drupal/silverback_gutenberg/src/EditorBlocksProcessor.php +53 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorInterface.php +19 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorTrait.php +221 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorBase.php +24 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorInterface.php +65 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorManager.php +37 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleInterface.php +20 -0
- package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleManager.php +37 -0
- package/drupal/silverback_gutenberg/src/LinkProcessor.php +405 -0
- package/drupal/silverback_gutenberg/src/LinkedContentExtractor.php +35 -0
- package/drupal/silverback_gutenberg/src/Normalizer/GutenbergContentEntityNormalizer.php +123 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergContentTrackTrait.php +51 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergLinkedContent.php +96 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergMediaEmbed.php +63 -0
- package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergReferencedContent.php +101 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockAttribute.php +42 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockChildren.php +32 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockHtml.php +30 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockMedia.php +159 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockType.php +29 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlocks.php +127 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockAttribute.php +29 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockChildren.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMarkup.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMedia.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockType.php +21 -0
- package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlocks.php +36 -0
- package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/MediaBlockMutator.php +30 -0
- package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/NodeBlockMutator.php +25 -0
- package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/TermReferenceBlockMutator.php +104 -0
- package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMatcherTrait.php +69 -0
- package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMediaMatcher.php +53 -0
- package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackNodeMatcher.php +19 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/Gutenberg.php +15 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/GutenbergValidator.php +210 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Email.php +28 -0
- package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Required.php +29 -0
- package/drupal/silverback_gutenberg/src/ReferencedContentExtractor.php +67 -0
- package/drupal/silverback_gutenberg/src/Routing/RouteSubscriber.php +17 -0
- package/drupal/silverback_gutenberg/src/Service/MediaService.php +27 -0
- package/drupal/silverback_gutenberg/src/SilverbackGutenbergServiceProvider.php +28 -0
- package/drupal/silverback_gutenberg/src/Utils.php +30 -0
- package/drupal/silverback_gutenberg/src/WebformMessageManager.php +29 -0
- package/drupal/silverback_gutenberg/tests/graphql/.graphqlrc.json +5 -0
- package/drupal/silverback_gutenberg/tests/graphql/queries/editor.gql +30 -0
- package/drupal/silverback_gutenberg/tests/graphql/schema.graphqls +37 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/silverback_gutenberg_test_validator.info.yml +9 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/ColumnValidator.php +42 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/GroupValidator.php +50 -0
- package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/LinkValidator.php +43 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/BlockValidationRuleTest.php +194 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/EditorDirectivesTest.php +255 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergLinkedContentEUTrackTest.php +133 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergReferencedContentEUTrackTest.php +225 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/LinkProcessorTest.php +284 -0
- package/drupal/silverback_gutenberg/tests/src/Kernel/MediaNormalizerTest.php +174 -0
- package/drupal/silverback_gutenberg/tests/src/Traits/SampleAssetTrait.php +15 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/BlockSerializerTest.php +27 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/BlockValidatorCardinalityTest.php +1537 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/EditorBlocksProcessorTest.php +159 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/LinkedContentExtractorTest.php +65 -0
- package/drupal/silverback_gutenberg/tests/src/Unit/ReferencedContentExtractorTest.php +248 -0
- package/drupal/silverback_gutenberg/tests/src/assets/media/data.json +4 -0
- package/drupal/silverback_gutenberg/tests/src/assets/media/source.html +71 -0
- package/drupal/silverback_gutenberg/tests/src/assets/media/target.html +71 -0
- package/package.json +16 -0
- package/turbo.json +15 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg;
|
|
4
|
+
|
|
5
|
+
use Drupal\Component\Utility\Html;
|
|
6
|
+
use Drupal\Core\Cache\CacheableMetadata;
|
|
7
|
+
use Drupal\Core\Config\ConfigFactoryInterface;
|
|
8
|
+
use Drupal\Core\Entity\EntityRepositoryInterface;
|
|
9
|
+
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
|
10
|
+
use Drupal\Core\Extension\ModuleHandlerInterface;
|
|
11
|
+
use Drupal\Core\Language\LanguageInterface;
|
|
12
|
+
use Drupal\Core\StreamWrapper\PublicStream;
|
|
13
|
+
use Drupal\Core\Url;
|
|
14
|
+
use Drupal\gutenberg\Parser\BlockParser;
|
|
15
|
+
use Drupal\path_alias\AliasManagerInterface;
|
|
16
|
+
use Symfony\Component\HttpFoundation\RequestStack;
|
|
17
|
+
|
|
18
|
+
class LinkProcessor {
|
|
19
|
+
|
|
20
|
+
protected AliasManagerInterface $pathAliasManager;
|
|
21
|
+
protected ConfigFactoryInterface $configFactory;
|
|
22
|
+
protected ModuleHandlerInterface $moduleHandler;
|
|
23
|
+
protected string $currentHost;
|
|
24
|
+
protected int $currentPort;
|
|
25
|
+
/**
|
|
26
|
+
* @var array|null
|
|
27
|
+
*/
|
|
28
|
+
protected $localHosts;
|
|
29
|
+
protected EntityRepositoryInterface $entityRepository;
|
|
30
|
+
protected EntityTypeManagerInterface $entityTypeManager;
|
|
31
|
+
protected array $linkPatterns = [];
|
|
32
|
+
protected array $idToUuidMapping = [];
|
|
33
|
+
|
|
34
|
+
protected CacheableMetadata $cacheableMetadata;
|
|
35
|
+
|
|
36
|
+
public function __construct(
|
|
37
|
+
AliasManagerInterface $pathAliasManager,
|
|
38
|
+
ConfigFactoryInterface $configFactory,
|
|
39
|
+
RequestStack $requestStack,
|
|
40
|
+
ModuleHandlerInterface $moduleHandler,
|
|
41
|
+
EntityRepositoryInterface $entityRepository,
|
|
42
|
+
EntityTypeManagerInterface $entityTypeManager
|
|
43
|
+
) {
|
|
44
|
+
$this->pathAliasManager = $pathAliasManager;
|
|
45
|
+
$this->configFactory = $configFactory;
|
|
46
|
+
$this->moduleHandler = $moduleHandler;
|
|
47
|
+
$this->currentHost = $requestStack->getCurrentRequest()->getHost();
|
|
48
|
+
$this->currentPort = (int) $requestStack->getCurrentRequest()->getPort();
|
|
49
|
+
$this->localHosts = $this->configFactory->get('silverback_gutenberg.settings')->get('local_hosts');
|
|
50
|
+
$this->entityRepository = $entityRepository;
|
|
51
|
+
$this->entityTypeManager = $entityTypeManager;
|
|
52
|
+
$this->cacheableMetadata = new CacheableMetadata();
|
|
53
|
+
|
|
54
|
+
foreach ($entityTypeManager->getDefinitions() as $entityType) {
|
|
55
|
+
$linkTemplate = $entityType->getLinkTemplate('canonical');
|
|
56
|
+
if ($linkTemplate) {
|
|
57
|
+
$this->linkPatterns[$entityType->id()] = '~(^' . preg_replace('~\{[^}]+}~', ')([^/]+)(', $linkTemplate, 1) . '$)~';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public function getCacheableMetadata(): CacheableMetadata {
|
|
63
|
+
return $this->cacheableMetadata;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public function resetCacheableMetadata() {
|
|
67
|
+
$this->cacheableMetadata = new CacheableMetadata();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public function addCacheableDependency($other_object) {
|
|
71
|
+
$this->cacheableMetadata->addCacheableDependency($other_object);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public function processLinks(string $html, string $direction, LanguageInterface $language = NULL) {
|
|
75
|
+
if (!in_array($direction, ['inbound', 'outbound'], TRUE)) {
|
|
76
|
+
throw new \Exception('Unknown direction: "' . $direction . '".');
|
|
77
|
+
}
|
|
78
|
+
if ($direction === 'outbound' && !$language) {
|
|
79
|
+
throw new \Exception('$language is required for "outbound" direction.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$document = Html::load($html);
|
|
83
|
+
|
|
84
|
+
foreach ($document->getElementsByTagName('a') as $link) {
|
|
85
|
+
$this->processLink($link, $direction, $language);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$processed = Html::serialize($document);
|
|
89
|
+
|
|
90
|
+
// This entity kills Gutenberg.
|
|
91
|
+
$processed = str_replace(' ', "\r", $processed);
|
|
92
|
+
|
|
93
|
+
if (strpos($processed, '<!-- wp:') !== FALSE) {
|
|
94
|
+
$blocks = (new BlockParser())->parse($processed);
|
|
95
|
+
$this->processBlocks($blocks, $direction, $language);
|
|
96
|
+
$processed = (new BlockSerializer())->serialize_blocks($blocks);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return $processed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected function processBlocks(&$blocks, string $direction, LanguageInterface $language = NULL): void {
|
|
103
|
+
$processUrlCallback = fn(string $url) => $this->processUrl($url, $direction, $language);
|
|
104
|
+
$processLinksCallback = fn(string $html) => $this->processLinks($html, $direction, $language);
|
|
105
|
+
foreach ($blocks as &$block) {
|
|
106
|
+
|
|
107
|
+
// First call the deprecated hook.
|
|
108
|
+
$this->moduleHandler->alter(
|
|
109
|
+
'silverback_gutenberg_link_processor_block_attributes',
|
|
110
|
+
$block['attrs'],
|
|
111
|
+
$block['blockName'],
|
|
112
|
+
$processUrlCallback
|
|
113
|
+
);
|
|
114
|
+
// Then call the new hook.
|
|
115
|
+
$context = [
|
|
116
|
+
'blockName' => $block['blockName'],
|
|
117
|
+
'processUrlCallback' => $processUrlCallback,
|
|
118
|
+
'processLinksCallback' => $processLinksCallback,
|
|
119
|
+
'direction' => $direction,
|
|
120
|
+
'language' => $language,
|
|
121
|
+
'linkProcessor' => $this,
|
|
122
|
+
];
|
|
123
|
+
$this->moduleHandler->alter(
|
|
124
|
+
'silverback_gutenberg_link_processor_block_attrs',
|
|
125
|
+
$block['attrs'],
|
|
126
|
+
$context
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (!empty($block['innerBlocks'])) {
|
|
130
|
+
$this->processBlocks($block['innerBlocks'], $direction, $language);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @deprecated Use hasSchemeOrHost
|
|
137
|
+
*/
|
|
138
|
+
public function isExternal(string $url): bool {
|
|
139
|
+
return $this->hasSchemeOrHost($url);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public function hasSchemeOrHost(string $url): bool {
|
|
143
|
+
$parts = parse_url($url);
|
|
144
|
+
return isset($parts['scheme']) || isset($parts['host']);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public function linksToCurrentHost(string $url): bool {
|
|
148
|
+
if (!$this->hasSchemeOrHost($url)) {
|
|
149
|
+
return TRUE;
|
|
150
|
+
}
|
|
151
|
+
$parts = parse_url($url);
|
|
152
|
+
return (
|
|
153
|
+
(
|
|
154
|
+
!isset($parts['scheme']) ||
|
|
155
|
+
(
|
|
156
|
+
isset($parts['scheme']) &&
|
|
157
|
+
($parts['scheme'] === 'http' || $parts['scheme'] === 'https')
|
|
158
|
+
)
|
|
159
|
+
) &&
|
|
160
|
+
isset($parts['host']) &&
|
|
161
|
+
(
|
|
162
|
+
(
|
|
163
|
+
is_array($this->localHosts) &&
|
|
164
|
+
in_array($parts['host'], $this->localHosts, TRUE)
|
|
165
|
+
) ||
|
|
166
|
+
(
|
|
167
|
+
$parts['host'] === $this->currentHost &&
|
|
168
|
+
(
|
|
169
|
+
(
|
|
170
|
+
isset($parts['port']) && $parts['port'] == $this->currentPort
|
|
171
|
+
) ||
|
|
172
|
+
!isset($parts['port'])
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public function isAsset(string $url): bool {
|
|
180
|
+
$parts = parse_url($url);
|
|
181
|
+
if (empty($parts['path'])) {
|
|
182
|
+
return FALSE;
|
|
183
|
+
}
|
|
184
|
+
if ($this->hasSchemeOrHost($url) && !$this->linksToCurrentHost($url)) {
|
|
185
|
+
// We have no reliable way to check if an external URL is an asset.
|
|
186
|
+
return FALSE;
|
|
187
|
+
}
|
|
188
|
+
if (strpos($parts['path'], '/' . PublicStream::basePath() . '/') === 0) {
|
|
189
|
+
return TRUE;
|
|
190
|
+
}
|
|
191
|
+
if (strpos($parts['path'], '/system/files') !== FALSE) {
|
|
192
|
+
return TRUE;
|
|
193
|
+
}
|
|
194
|
+
return FALSE;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
protected function cleanUrl(string $url): string {
|
|
198
|
+
if ($this->hasSchemeOrHost($url) && !$this->linksToCurrentHost($url)) {
|
|
199
|
+
return $url;
|
|
200
|
+
}
|
|
201
|
+
$parts = parse_url($url);
|
|
202
|
+
unset($parts['scheme'], $parts['host'], $parts['port'], $parts['user'], $parts['pass']);
|
|
203
|
+
return $this->buildUrl($parts);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected function processLink(\DOMElement $link, string $direction, LanguageInterface $language = NULL) {
|
|
207
|
+
if ($direction === 'outbound' && !$language) {
|
|
208
|
+
throw new \Exception('$language is required for "outbound" direction.');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$href = $link->getAttribute('href');
|
|
212
|
+
if ($href) {
|
|
213
|
+
$link->setAttribute('href', $this->processUrl($href, $direction, $language, $metadata));
|
|
214
|
+
if ($direction === 'inbound' && isset($metadata['uuid']) && $link->hasAttribute('data-id')) {
|
|
215
|
+
$link->setAttribute('data-id', $metadata['uuid']);
|
|
216
|
+
$link->setAttribute('data-entity-type', $metadata['entity_type']);
|
|
217
|
+
}
|
|
218
|
+
if ($direction === 'outbound' && isset($metadata['id']) && $link->hasAttribute('data-id')) {
|
|
219
|
+
$link->setAttribute('data-id', $metadata['id']);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if ($direction === 'inbound') {
|
|
224
|
+
$this->moduleHandler->alter('silverback_gutenberg_link_processor_inbound_link', $link, $this);
|
|
225
|
+
}
|
|
226
|
+
if ($direction === 'outbound') {
|
|
227
|
+
$this->moduleHandler->alter('silverback_gutenberg_link_processor_outbound_link', $link, $language, $this);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public function processUrl(string $url, string $direction, LanguageInterface $language = NULL, array &$metadata = NULL): string {
|
|
233
|
+
$metadata = [];
|
|
234
|
+
|
|
235
|
+
if ($direction === 'outbound' && !$language) {
|
|
236
|
+
throw new \Exception('$language is required for "outbound" direction.');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if ($this->hasSchemeOrHost($url)) {
|
|
240
|
+
if ($this->linksToCurrentHost($url)) {
|
|
241
|
+
$url = $this->cleanUrl($url);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
return $url;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
$parts = parse_url($url);
|
|
249
|
+
|
|
250
|
+
if (empty($parts['path'])) {
|
|
251
|
+
|
|
252
|
+
// Corrupted URL.
|
|
253
|
+
return $url;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if ($direction === 'outbound') {
|
|
257
|
+
// Replace UUIDs with IDs.
|
|
258
|
+
foreach ($this->linkPatterns as $entityType => $pattern) {
|
|
259
|
+
if (preg_match($pattern, $parts['path'], $matches)) {
|
|
260
|
+
$uuid = $matches[2];
|
|
261
|
+
$id = $this->getId($entityType, $uuid);
|
|
262
|
+
if ($id) {
|
|
263
|
+
$parts['path'] = preg_replace($pattern, '${1}' . $id . '${3}', $parts['path']);
|
|
264
|
+
$metadata['entity_type'] = $entityType;
|
|
265
|
+
$metadata['id'] = $id;
|
|
266
|
+
$metadata['uuid'] = $uuid;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if ($direction === 'inbound') {
|
|
274
|
+
$path = $this->pathAliasManager->getPathByAlias($parts['path']);
|
|
275
|
+
if ($path !== $parts['path']) {
|
|
276
|
+
$parts['path'] = $path;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Try to strip the language prefix.
|
|
280
|
+
$prefixes = $this->configFactory
|
|
281
|
+
->get('language.negotiation')
|
|
282
|
+
->get('url.prefixes');
|
|
283
|
+
$pathLangcode = null;
|
|
284
|
+
foreach ($prefixes as $langcode => $prefix) {
|
|
285
|
+
if ('/' . $prefix === $parts['path']) {
|
|
286
|
+
$withoutPrefix = '/';
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
elseif (strpos($parts['path'], '/' . $prefix . '/') === 0) {
|
|
290
|
+
$pathLangcode = $langcode;
|
|
291
|
+
$withoutPrefix = substr($parts['path'], strlen($prefix) + 1);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!empty($withoutPrefix)) {
|
|
296
|
+
$path = $this->pathAliasManager->getPathByAlias($withoutPrefix, $pathLangcode);
|
|
297
|
+
if ($path !== $withoutPrefix) {
|
|
298
|
+
$parts['path'] = $path;
|
|
299
|
+
} else {
|
|
300
|
+
/** @var \Drupal\Core\Routing\Router $router */
|
|
301
|
+
$router = \Drupal::service('router.no_access_checks');
|
|
302
|
+
try {
|
|
303
|
+
$router->match($withoutPrefix);
|
|
304
|
+
// This is a Drupal path, so we strip the prefix.
|
|
305
|
+
$parts['path'] = $withoutPrefix;
|
|
306
|
+
}
|
|
307
|
+
catch (\Throwable $e) { }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if ($direction === 'outbound') {
|
|
314
|
+
if ((strpos($parts['path'], '/') !== 0) && (strpos($parts['path'], '#') !== 0) && (strpos($parts['path'], '?') !== 0)) {
|
|
315
|
+
/* @see \Drupal\Core\Url::fromUserInput */
|
|
316
|
+
$parts['path'] = '/' . $parts['path'];
|
|
317
|
+
}
|
|
318
|
+
$pathAlias = Url::fromUserInput($parts['path'])
|
|
319
|
+
->setAbsolute(FALSE)
|
|
320
|
+
->setOption('language', $language)
|
|
321
|
+
->toString(TRUE)
|
|
322
|
+
->getGeneratedUrl();
|
|
323
|
+
// The toString() call above will encode the urls, so now we need to
|
|
324
|
+
// decode them to avoid double encoding upon the save operation
|
|
325
|
+
// afterwards.
|
|
326
|
+
$pathAlias = rawurldecode($pathAlias);
|
|
327
|
+
if ($pathAlias !== $parts['path']) {
|
|
328
|
+
$parts['path'] = $pathAlias;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if ($direction === 'inbound') {
|
|
333
|
+
// Replace IDs with UUIDs.
|
|
334
|
+
foreach ($this->linkPatterns as $entityType => $pattern) {
|
|
335
|
+
if (preg_match($pattern, $parts['path'], $matches)) {
|
|
336
|
+
$id = $matches[2];
|
|
337
|
+
$uuid = $this->getUuid($entityType, $id);
|
|
338
|
+
if ($uuid) {
|
|
339
|
+
$parts['path'] = preg_replace($pattern, '${1}' . $uuid . '${3}', $parts['path']);
|
|
340
|
+
$metadata['entity_type'] = $entityType;
|
|
341
|
+
$metadata['id'] = $id;
|
|
342
|
+
$metadata['uuid'] = $uuid;
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
$url = $this->buildUrl($parts);
|
|
350
|
+
|
|
351
|
+
if ($direction === 'inbound') {
|
|
352
|
+
$this->moduleHandler->alter('silverback_gutenberg_link_processor_inbound_url', $url, $this);
|
|
353
|
+
}
|
|
354
|
+
if ($direction === 'outbound') {
|
|
355
|
+
$this->moduleHandler->alter('silverback_gutenberg_link_processor_outbound_url', $url, $language, $this);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return $url;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
protected function buildUrl(array $parts): string {
|
|
362
|
+
return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') .
|
|
363
|
+
((isset($parts['user']) || isset($parts['host'])) ? '//' : '') .
|
|
364
|
+
(isset($parts['user']) ? "{$parts['user']}" : '') .
|
|
365
|
+
(isset($parts['pass']) ? ":{$parts['pass']}" : '') .
|
|
366
|
+
(isset($parts['user']) ? '@' : '') .
|
|
367
|
+
(isset($parts['host']) ? "{$parts['host']}" : '') .
|
|
368
|
+
(isset($parts['port']) ? ":{$parts['port']}" : '') .
|
|
369
|
+
(isset($parts['path']) ? "{$parts['path']}" : '') .
|
|
370
|
+
(isset($parts['query']) ? "?{$parts['query']}" : '') .
|
|
371
|
+
(isset($parts['fragment']) ? "#{$parts['fragment']}" : '');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
protected function getUuid(string $entityType, string $id): ?string {
|
|
375
|
+
if (!isset($this->idToUuidMapping[$entityType][$id])) {
|
|
376
|
+
$entity = $this->entityTypeManager->getStorage($entityType)->load($id);
|
|
377
|
+
if ($entity) {
|
|
378
|
+
$this->idToUuidMapping[$entityType][$id] = $entity->uuid();
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
$this->idToUuidMapping[$entityType][$id] = FALSE;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return $this->idToUuidMapping[$entityType][$id] ?: NULL;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
protected function getId(string $entityType, string $uuid): ?string {
|
|
388
|
+
if (!isset($this->idToUuidMapping[$entityType])) {
|
|
389
|
+
$this->idToUuidMapping[$entityType] = [];
|
|
390
|
+
}
|
|
391
|
+
$id = array_search($uuid, $this->idToUuidMapping[$entityType]);
|
|
392
|
+
if ($id) {
|
|
393
|
+
return $id;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
$entity = $this->entityRepository->loadEntityByUuid($entityType, $uuid);
|
|
397
|
+
if ($entity) {
|
|
398
|
+
$this->idToUuidMapping[$entityType][$entity->id()] = $uuid;
|
|
399
|
+
return $entity->id();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return NULL;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg;
|
|
4
|
+
|
|
5
|
+
use Drupal\Component\Utility\Html;
|
|
6
|
+
|
|
7
|
+
class LinkedContentExtractor {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Give a source (html) text, it returns all the references to content coming
|
|
11
|
+
* from html link tags. The link tags have to have the data-id attribute,
|
|
12
|
+
* identifying the uuid of the entity, and optionally the data-entity-type
|
|
13
|
+
* attribute, identifying the entity type. If not present, the
|
|
14
|
+
* data-entity-type value defaults to 'node'
|
|
15
|
+
*
|
|
16
|
+
* The above attributes (data-id and data-entity-type) should be set by the
|
|
17
|
+
* LinkProcessor class when the gutenberg blocks are saved into the database.
|
|
18
|
+
*
|
|
19
|
+
* @param string $sourceText
|
|
20
|
+
* @return array
|
|
21
|
+
*/
|
|
22
|
+
public function getTargetEntities($sourceText) {
|
|
23
|
+
$document = Html::load($sourceText);
|
|
24
|
+
$references = [];
|
|
25
|
+
foreach ($document->getElementsByTagName('a') as $link) {
|
|
26
|
+
if ($link->hasAttribute('data-id')) {
|
|
27
|
+
$uuid = $link->getAttribute('data-id');
|
|
28
|
+
// The default entity type should be, for convenience, node.
|
|
29
|
+
$entityType = $link->hasAttribute('data-entity-type') ? $link->getAttribute('data-entity-type') : 'node';
|
|
30
|
+
$references[$entityType][$uuid] = $uuid;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return $references;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Normalizer;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Entity\ContentEntityInterface;
|
|
6
|
+
use Drupal\Core\Entity\EntityRepositoryInterface;
|
|
7
|
+
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
|
8
|
+
use Drupal\Core\Extension\ModuleHandlerInterface;
|
|
9
|
+
use Drupal\Core\Language\LanguageManagerInterface;
|
|
10
|
+
use Drupal\default_content\Normalizer\ContentEntityNormalizer;
|
|
11
|
+
use Drupal\gutenberg\Parser\BlockParser;
|
|
12
|
+
use Drupal\node\Entity\Node;
|
|
13
|
+
use Drupal\silverback_gutenberg\BlockMutator\BlockMutatorManagerInterface;
|
|
14
|
+
use Drupal\silverback_gutenberg\BlockSerializer;
|
|
15
|
+
use Drupal\silverback_gutenberg\LinkedContentExtractor;
|
|
16
|
+
use Drupal\silverback_gutenberg\Utils;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Class GutenbergContentEntityNormalizer
|
|
20
|
+
*
|
|
21
|
+
* Override for the ContentEntityNormalizer in default_content, replacing referenced
|
|
22
|
+
* media entity id's with uuids, so they can be imported and exported reliably.
|
|
23
|
+
*
|
|
24
|
+
* @package Drupal\silverback_gutenberg\Normalizer
|
|
25
|
+
*/
|
|
26
|
+
class GutenbergContentEntityNormalizer extends ContentEntityNormalizer {
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The block mutator plugin manager.
|
|
30
|
+
*
|
|
31
|
+
* @var \Drupal\silverback_gutenberg\BlockMutator\BlockMutatorManagerInterface
|
|
32
|
+
*/
|
|
33
|
+
protected $blockMutatorManager;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Constructs an GutenbergContentEntityNormalizer object.
|
|
37
|
+
*
|
|
38
|
+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
|
39
|
+
* The entity type manager.
|
|
40
|
+
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
|
41
|
+
* The module handler.
|
|
42
|
+
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
|
43
|
+
* The entity repository.
|
|
44
|
+
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
|
45
|
+
* The language manager.
|
|
46
|
+
*/
|
|
47
|
+
public function __construct(
|
|
48
|
+
EntityTypeManagerInterface $entity_type_manager,
|
|
49
|
+
ModuleHandlerInterface $module_handler,
|
|
50
|
+
EntityRepositoryInterface $entity_repository,
|
|
51
|
+
LanguageManagerInterface $language_manager,
|
|
52
|
+
BlockMutatorManagerInterface $block_mutator_manager,
|
|
53
|
+
) {
|
|
54
|
+
parent::__construct($entity_type_manager, $module_handler, $entity_repository, $language_manager);
|
|
55
|
+
$this->blockMutatorManager = $block_mutator_manager;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* {@inheritDoc}
|
|
60
|
+
*/
|
|
61
|
+
protected function normalizeTranslation(ContentEntityInterface $entity, array $field_names) {
|
|
62
|
+
$normalized = parent::normalizeTranslation($entity, $field_names);
|
|
63
|
+
|
|
64
|
+
foreach (Utils::getGutenbergFields($entity) as $field) {
|
|
65
|
+
if (isset($normalized[$field][0]['value'])) {
|
|
66
|
+
// Parse the document, mutate it and re-assign it as the field value.
|
|
67
|
+
$blocks = (new BlockParser())->parse($normalized[$field][0]['value']);
|
|
68
|
+
$this->blockMutatorManager->mutateExport($blocks, $this->dependencies);
|
|
69
|
+
$normalized[$field][0]['value'] = (new BlockSerializer())->serialize_blocks($blocks);
|
|
70
|
+
|
|
71
|
+
// Register linked content as dependencies.
|
|
72
|
+
// This is required to make the entity usage tracking work during the
|
|
73
|
+
// default content import - the target entity should already exist at
|
|
74
|
+
// the moment the referencing entity is imported.
|
|
75
|
+
$linkExtractor = new LinkedContentExtractor();
|
|
76
|
+
$references = $linkExtractor->getTargetEntities($normalized[$field][0]['value']);
|
|
77
|
+
foreach ($references as $entityType => $uuids) {
|
|
78
|
+
foreach ($uuids as $uuid) {
|
|
79
|
+
$targetEntity = $this->entityRepository->loadEntityByUuid($entityType, $uuid);
|
|
80
|
+
if ($targetEntity instanceof ContentEntityInterface) {
|
|
81
|
+
$this->addDependency($targetEntity);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return $normalized;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* {@inheritDoc}
|
|
93
|
+
*/
|
|
94
|
+
public function denormalize(array $data) {
|
|
95
|
+
// Abort early if the current entity is not a node.
|
|
96
|
+
if (!isset($data['_meta']['entity_type']) || $data['_meta']['entity_type'] !== 'node') {
|
|
97
|
+
return parent::denormalize($data);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create an empty instance of that entity to easily get the relevant
|
|
101
|
+
// field names.
|
|
102
|
+
$entity = Node::create(['type' => $data['_meta']['bundle']]);
|
|
103
|
+
foreach (Utils::getGutenbergFields($entity) as $field) {
|
|
104
|
+
if (is_string($data['default'][$field][0]['value'])) {
|
|
105
|
+
// Parse the document, mutate it and re-assign it as the payload.
|
|
106
|
+
$blocks = (new BlockParser())->parse($data['default'][$field][0]['value']);
|
|
107
|
+
$this->blockMutatorManager->mutateImport($blocks);
|
|
108
|
+
$data['default'][$field][0]['value'] = (new BlockSerializer())->serialize_blocks($blocks);
|
|
109
|
+
}
|
|
110
|
+
if (isset($data['translations'])) {
|
|
111
|
+
foreach(array_keys($data['translations']) as $langcode) {
|
|
112
|
+
// Parse the translation document, mutate it and re-assign it as the payload.
|
|
113
|
+
$blocks = (new BlockParser())->parse($data['translations'][$langcode][$field][0]['value']);
|
|
114
|
+
$this->blockMutatorManager->mutateImport($blocks);
|
|
115
|
+
$data['translations'][$langcode][$field][0]['value'] = (new BlockSerializer())->serialize_blocks($blocks);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Pass the modified payload to the actual denormalization.
|
|
121
|
+
return parent::denormalize($data);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergContentTrackTrait.php
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Drupal\silverback_gutenberg\Plugin\EntityUsage\Track;
|
|
4
|
+
|
|
5
|
+
use Drupal\Core\Entity\EntityStorageException;
|
|
6
|
+
|
|
7
|
+
trait GutenbergContentTrackTrait {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts an array of references obtained using the extractor services to an
|
|
11
|
+
* entity usage list representation.
|
|
12
|
+
*
|
|
13
|
+
* The array of references looks like this:
|
|
14
|
+
* array(
|
|
15
|
+
* 'node' => array(
|
|
16
|
+
* 'uuid_1' => 'uuid_1',
|
|
17
|
+
* 'uuid_2' => 'uuid_2',
|
|
18
|
+
* ),
|
|
19
|
+
* 'user' => array(
|
|
20
|
+
* 'uuid_user_1' => 'uuid_user_1',
|
|
21
|
+
* 'uuid_user_2' => 'uuid_user_2',
|
|
22
|
+
* )
|
|
23
|
+
* ).
|
|
24
|
+
* The entity usage list representation would be:
|
|
25
|
+
* array(
|
|
26
|
+
* 'node|nid1',
|
|
27
|
+
* 'node|nid2',
|
|
28
|
+
* 'user|uid1',
|
|
29
|
+
* 'user|uid2'
|
|
30
|
+
* )
|
|
31
|
+
* @param $references
|
|
32
|
+
* @return array
|
|
33
|
+
*/
|
|
34
|
+
protected function convertReferencesToEntityUsageList($references) {
|
|
35
|
+
$targetEntities = [];
|
|
36
|
+
foreach ($references as $entityType => $uuids) {
|
|
37
|
+
foreach ($uuids as $uuid) {
|
|
38
|
+
try {
|
|
39
|
+
$entity = $this->entityRepository->loadEntityByUuid($entityType, $uuid);
|
|
40
|
+
if (!$entity) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
$targetEntities[] = implode('|', [$entityType, $entity->id()]);
|
|
44
|
+
} catch (EntityStorageException $e) {
|
|
45
|
+
// Just ignore this exception.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return $targetEntities;
|
|
50
|
+
}
|
|
51
|
+
}
|