@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +983 -0
  2. package/drupal/silverback_gutenberg/README.md +439 -0
  3. package/drupal/silverback_gutenberg/composer.json +20 -0
  4. package/drupal/silverback_gutenberg/config/install/silverback_gutenberg.settings.yml +1 -0
  5. package/drupal/silverback_gutenberg/config/schema/silverback_gutenberg.schema.yml +13 -0
  6. package/drupal/silverback_gutenberg/css/gutenberg-tweaks.css +46 -0
  7. package/drupal/silverback_gutenberg/directives.gql +40 -0
  8. package/drupal/silverback_gutenberg/directives.graphql +46 -0
  9. package/drupal/silverback_gutenberg/js/base.js +24 -0
  10. package/drupal/silverback_gutenberg/js/gutenberg-tweaks.js +154 -0
  11. package/drupal/silverback_gutenberg/silverback_gutenberg.api.php +76 -0
  12. package/drupal/silverback_gutenberg/silverback_gutenberg.info.yml +8 -0
  13. package/drupal/silverback_gutenberg/silverback_gutenberg.libraries.yml +14 -0
  14. package/drupal/silverback_gutenberg/silverback_gutenberg.module +97 -0
  15. package/drupal/silverback_gutenberg/silverback_gutenberg.services.yml +29 -0
  16. package/drupal/silverback_gutenberg/src/Annotation/GutenbergBlockMutator.php +39 -0
  17. package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidator.php +37 -0
  18. package/drupal/silverback_gutenberg/src/Annotation/GutenbergValidatorRule.php +37 -0
  19. package/drupal/silverback_gutenberg/src/Attribute/GutenbergBlockMutator.php +29 -0
  20. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorBase.php +24 -0
  21. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorInterface.php +41 -0
  22. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManager.php +114 -0
  23. package/drupal/silverback_gutenberg/src/BlockMutator/BlockMutatorManagerInterface.php +30 -0
  24. package/drupal/silverback_gutenberg/src/BlockMutator/EntityBlockMutatorBase.php +189 -0
  25. package/drupal/silverback_gutenberg/src/BlockSerializer.php +84 -0
  26. package/drupal/silverback_gutenberg/src/Controller/LinkitAutocomplete.php +84 -0
  27. package/drupal/silverback_gutenberg/src/Directives.php +74 -0
  28. package/drupal/silverback_gutenberg/src/EditorBlocksProcessor.php +53 -0
  29. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorInterface.php +19 -0
  30. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergCardinalityValidatorTrait.php +221 -0
  31. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorBase.php +24 -0
  32. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorInterface.php +65 -0
  33. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorManager.php +37 -0
  34. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleInterface.php +20 -0
  35. package/drupal/silverback_gutenberg/src/GutenbergValidation/GutenbergValidatorRuleManager.php +37 -0
  36. package/drupal/silverback_gutenberg/src/LinkProcessor.php +405 -0
  37. package/drupal/silverback_gutenberg/src/LinkedContentExtractor.php +35 -0
  38. package/drupal/silverback_gutenberg/src/Normalizer/GutenbergContentEntityNormalizer.php +123 -0
  39. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergContentTrackTrait.php +51 -0
  40. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergLinkedContent.php +96 -0
  41. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergMediaEmbed.php +63 -0
  42. package/drupal/silverback_gutenberg/src/Plugin/EntityUsage/Track/GutenbergReferencedContent.php +101 -0
  43. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockAttribute.php +42 -0
  44. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockChildren.php +32 -0
  45. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockHtml.php +30 -0
  46. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockMedia.php +159 -0
  47. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlockType.php +29 -0
  48. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/DataProducer/EditorBlocks.php +127 -0
  49. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockAttribute.php +29 -0
  50. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockChildren.php +21 -0
  51. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMarkup.php +21 -0
  52. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockMedia.php +21 -0
  53. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlockType.php +21 -0
  54. package/drupal/silverback_gutenberg/src/Plugin/GraphQL/Directive/EditorBlocks.php +36 -0
  55. package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/MediaBlockMutator.php +30 -0
  56. package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/NodeBlockMutator.php +25 -0
  57. package/drupal/silverback_gutenberg/src/Plugin/GutenbergBlockMutator/TermReferenceBlockMutator.php +104 -0
  58. package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMatcherTrait.php +69 -0
  59. package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackMediaMatcher.php +53 -0
  60. package/drupal/silverback_gutenberg/src/Plugin/Linkit/Matcher/SilverbackNodeMatcher.php +19 -0
  61. package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/Gutenberg.php +15 -0
  62. package/drupal/silverback_gutenberg/src/Plugin/Validation/Constraint/GutenbergValidator.php +210 -0
  63. package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Email.php +28 -0
  64. package/drupal/silverback_gutenberg/src/Plugin/Validation/GutenbergValidatorRule/Required.php +29 -0
  65. package/drupal/silverback_gutenberg/src/ReferencedContentExtractor.php +67 -0
  66. package/drupal/silverback_gutenberg/src/Routing/RouteSubscriber.php +17 -0
  67. package/drupal/silverback_gutenberg/src/Service/MediaService.php +27 -0
  68. package/drupal/silverback_gutenberg/src/SilverbackGutenbergServiceProvider.php +28 -0
  69. package/drupal/silverback_gutenberg/src/Utils.php +30 -0
  70. package/drupal/silverback_gutenberg/src/WebformMessageManager.php +29 -0
  71. package/drupal/silverback_gutenberg/tests/graphql/.graphqlrc.json +5 -0
  72. package/drupal/silverback_gutenberg/tests/graphql/queries/editor.gql +30 -0
  73. package/drupal/silverback_gutenberg/tests/graphql/schema.graphqls +37 -0
  74. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/silverback_gutenberg_test_validator.info.yml +9 -0
  75. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/ColumnValidator.php +42 -0
  76. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/GroupValidator.php +50 -0
  77. package/drupal/silverback_gutenberg/tests/modules/silverback_gutenberg_test_validator/src/Plugin/Validation/GutenbergValidator/LinkValidator.php +43 -0
  78. package/drupal/silverback_gutenberg/tests/src/Kernel/BlockValidationRuleTest.php +194 -0
  79. package/drupal/silverback_gutenberg/tests/src/Kernel/EditorDirectivesTest.php +255 -0
  80. package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergLinkedContentEUTrackTest.php +133 -0
  81. package/drupal/silverback_gutenberg/tests/src/Kernel/GutenbergReferencedContentEUTrackTest.php +225 -0
  82. package/drupal/silverback_gutenberg/tests/src/Kernel/LinkProcessorTest.php +284 -0
  83. package/drupal/silverback_gutenberg/tests/src/Kernel/MediaNormalizerTest.php +174 -0
  84. package/drupal/silverback_gutenberg/tests/src/Traits/SampleAssetTrait.php +15 -0
  85. package/drupal/silverback_gutenberg/tests/src/Unit/BlockSerializerTest.php +27 -0
  86. package/drupal/silverback_gutenberg/tests/src/Unit/BlockValidatorCardinalityTest.php +1537 -0
  87. package/drupal/silverback_gutenberg/tests/src/Unit/EditorBlocksProcessorTest.php +159 -0
  88. package/drupal/silverback_gutenberg/tests/src/Unit/LinkedContentExtractorTest.php +65 -0
  89. package/drupal/silverback_gutenberg/tests/src/Unit/ReferencedContentExtractorTest.php +248 -0
  90. package/drupal/silverback_gutenberg/tests/src/assets/media/data.json +4 -0
  91. package/drupal/silverback_gutenberg/tests/src/assets/media/source.html +71 -0
  92. package/drupal/silverback_gutenberg/tests/src/assets/media/target.html +71 -0
  93. package/package.json +16 -0
  94. 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('&#13;', "\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
+ }
@@ -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
+ }