@amazeelabs/silverback-preview-link 1.6.15

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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/drupal/silverback_preview_link/CHANGELOG.md +195 -0
  3. package/drupal/silverback_preview_link/README.md +30 -0
  4. package/drupal/silverback_preview_link/composer.json +13 -0
  5. package/drupal/silverback_preview_link/config/install/silverback_preview_link.settings.yml +3 -0
  6. package/drupal/silverback_preview_link/config/schema/silverback_preview_link.schema.yml +14 -0
  7. package/drupal/silverback_preview_link/css/modal.css +17 -0
  8. package/drupal/silverback_preview_link/js/copy.js +39 -0
  9. package/drupal/silverback_preview_link/silverback_preview_link.info.yml +12 -0
  10. package/drupal/silverback_preview_link/silverback_preview_link.install +6 -0
  11. package/drupal/silverback_preview_link/silverback_preview_link.libraries.yml +9 -0
  12. package/drupal/silverback_preview_link/silverback_preview_link.links.menu.yml +5 -0
  13. package/drupal/silverback_preview_link/silverback_preview_link.module +79 -0
  14. package/drupal/silverback_preview_link/silverback_preview_link.permissions.yml +9 -0
  15. package/drupal/silverback_preview_link/silverback_preview_link.routing.yml +40 -0
  16. package/drupal/silverback_preview_link/silverback_preview_link.services.yml +31 -0
  17. package/drupal/silverback_preview_link/src/Access/PreviewEnabledAccessCheck.php +80 -0
  18. package/drupal/silverback_preview_link/src/Access/PreviewLinkAccessCheck.php +49 -0
  19. package/drupal/silverback_preview_link/src/Authentication/Provider/PreviewToken.php +118 -0
  20. package/drupal/silverback_preview_link/src/Controller/PreviewController.php +83 -0
  21. package/drupal/silverback_preview_link/src/Entity/SilverbackPreviewLink.php +224 -0
  22. package/drupal/silverback_preview_link/src/Entity/SilverbackPreviewLinkInterface.php +110 -0
  23. package/drupal/silverback_preview_link/src/Form/PreviewLinkForm.php +320 -0
  24. package/drupal/silverback_preview_link/src/Form/SettingsForm.php +204 -0
  25. package/drupal/silverback_preview_link/src/Plugin/EntityReferenceSelection/SilverbackPreviewLinkSelection.php +25 -0
  26. package/drupal/silverback_preview_link/src/Plugin/Field/FieldWidget/PreviewLinkEntitiesWidget.php +103 -0
  27. package/drupal/silverback_preview_link/src/Plugin/Validation/Constraint/SilverbackPreviewLinkEntitiesUniqueConstraint.php +26 -0
  28. package/drupal/silverback_preview_link/src/Plugin/Validation/Constraint/SilverbackPreviewLinkEntitiesUniqueConstraintValidator.php +44 -0
  29. package/drupal/silverback_preview_link/src/PreviewLinkExpiry.php +55 -0
  30. package/drupal/silverback_preview_link/src/PreviewLinkHooks.php +61 -0
  31. package/drupal/silverback_preview_link/src/PreviewLinkHost.php +70 -0
  32. package/drupal/silverback_preview_link/src/PreviewLinkHostInterface.php +49 -0
  33. package/drupal/silverback_preview_link/src/PreviewLinkStorage.php +99 -0
  34. package/drupal/silverback_preview_link/src/PreviewLinkStorageInterface.php +19 -0
  35. package/drupal/silverback_preview_link/src/PreviewLinkUtility.php +27 -0
  36. package/drupal/silverback_preview_link/src/QRCodeWithLogo.php +84 -0
  37. package/drupal/silverback_preview_link/src/QRMarkupSVGWithLogo.php +51 -0
  38. package/drupal/silverback_preview_link/src/Routing/PreviewLinkRouteProvider.php +61 -0
  39. package/drupal/silverback_preview_link/src/images/amazee-labs_logo-square-green.svg +13 -0
  40. package/drupal/silverback_preview_link/templates/preview-link.html.twig +39 -0
  41. package/package.json +13 -0
@@ -0,0 +1,320 @@
1
+ <?php
2
+
3
+ declare(strict_types = 1);
4
+
5
+ namespace Drupal\silverback_preview_link\Form;
6
+
7
+ use chillerlan\QRCode\QRCode;
8
+ use chillerlan\QRCode\QROptions;
9
+ use Drupal\Component\Datetime\TimeInterface;
10
+ use Drupal\Component\Utility\Html;
11
+ use Drupal\Component\Utility\NestedArray;
12
+ use Drupal\Core\Ajax\AjaxResponse;
13
+ use Drupal\Core\Ajax\PrependCommand;
14
+ use Drupal\Core\Ajax\ReplaceCommand;
15
+ use Drupal\Core\Datetime\DateFormatterInterface;
16
+ use Drupal\Core\Entity\ContentEntityForm;
17
+ use Drupal\Core\Entity\EntityInterface;
18
+ use Drupal\Core\Entity\EntityRepositoryInterface;
19
+ use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
20
+ use Drupal\Core\Form\FormStateInterface;
21
+ use Drupal\Core\Messenger\MessengerInterface;
22
+ use Drupal\Core\Routing\RouteMatchInterface;
23
+ use Drupal\Core\Url;
24
+ use Drupal\node\NodeInterface;
25
+ use Drupal\silverback_preview_link\Entity\SilverbackPreviewLink;
26
+ use Drupal\silverback_preview_link\PreviewLinkExpiry;
27
+ use Drupal\silverback_preview_link\PreviewLinkHostInterface;
28
+ use Drupal\silverback_preview_link\PreviewLinkStorageInterface;
29
+ use Symfony\Component\DependencyInjection\ContainerInterface;
30
+ use Drupal\silverback_preview_link\QRCodeLogo;
31
+ use Drupal\silverback_preview_link\QRCodeWithLogo;
32
+
33
+ /**
34
+ * Preview link form.
35
+ *
36
+ * @internal
37
+ *
38
+ * @property \Drupal\silverback_preview_link\Entity\SilverbackPreviewLinkInterface $entity
39
+ */
40
+ final class PreviewLinkForm extends ContentEntityForm {
41
+
42
+ /**
43
+ * PreviewLinkForm constructor.
44
+ */
45
+ public function __construct(
46
+ EntityRepositoryInterface $entity_repository,
47
+ EntityTypeBundleInfoInterface $entity_type_bundle_info,
48
+ TimeInterface $time,
49
+ protected DateFormatterInterface $dateFormatter,
50
+ protected PreviewLinkExpiry $linkExpiry,
51
+ MessengerInterface $messenger,
52
+ protected PreviewLinkHostInterface $previewLinkHost,
53
+ ) {
54
+ parent::__construct($entity_repository, $entity_type_bundle_info, $time);
55
+ $this->messenger = $messenger;
56
+ }
57
+
58
+ /**
59
+ * {@inheritdoc}
60
+ */
61
+ public static function create(ContainerInterface $container): self {
62
+ return new static(
63
+ $container->get('entity.repository'),
64
+ $container->get('entity_type.bundle.info'),
65
+ $container->get('datetime.time'),
66
+ $container->get('date.formatter'),
67
+ $container->get('silverback_preview_link.link_expiry'),
68
+ $container->get('messenger'),
69
+ $container->get('silverback_preview_link.host'),
70
+ );
71
+ }
72
+
73
+ /**
74
+ * {@inheritdoc}
75
+ */
76
+ public function getFormId() {
77
+ return 'silverback_preview_link_entity_form';
78
+ }
79
+
80
+ /**
81
+ * {@inheritdoc}
82
+ */
83
+ public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
84
+ $host = $this->getHostEntity($route_match);
85
+ $previewLinks = $this->previewLinkHost->getPreviewLinks($host);
86
+ if (count($previewLinks) > 0) {
87
+ return reset($previewLinks);
88
+ }
89
+ else {
90
+ $storage = $this->entityTypeManager->getStorage('silverback_preview_link');
91
+ assert($storage instanceof PreviewLinkStorageInterface);
92
+ $previewLink = SilverbackPreviewLink::create()->addEntity($host);
93
+ $previewLink->save();
94
+ return $previewLink;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get the entity referencing this Preview Link.
100
+ *
101
+ * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
102
+ * A route match.
103
+ *
104
+ * @return \Drupal\Core\Entity\EntityInterface
105
+ * The host entity.
106
+ */
107
+ public function getHostEntity(RouteMatchInterface $routeMatch): EntityInterface {
108
+ return parent::getEntityFromRouteMatch($routeMatch, $routeMatch->getRouteObject()->getOption('silverback_preview_link.entity_type_id'));
109
+ }
110
+
111
+ /**
112
+ * {@inheritdoc}
113
+ */
114
+ public function buildForm(array $form, FormStateInterface $form_state, RouteMatchInterface $routeMatch = NULL) {
115
+ if (!isset($routeMatch)) {
116
+ throw new \LogicException('Route match not populated from argument resolver');
117
+ }
118
+
119
+ $host = $this->getHostEntity($routeMatch);
120
+ $form = parent::buildForm($form, $form_state);
121
+ // See https://www.drupal.org/project/drupal/issues/2897377
122
+ $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']);
123
+
124
+ if ($host instanceof NodeInterface) {
125
+ /** @var \Drupal\silverback_preview_link\Entity\SilverbackPreviewLinkInterface $silverbackPreviewLink */
126
+ $silverbackPreviewLink = $this->getEntity();
127
+ /** @var \Drupal\silverback_external_preview\ExternalPreviewLink $externalPreviewLink */
128
+ $externalPreviewLink = \Drupal::service('silverback_external_preview.external_preview_link');
129
+ $externalPreviewUrl = $externalPreviewLink->createPreviewUrlFromEntity($host);
130
+ $query = $externalPreviewUrl->getOption('query') ?? [];
131
+ $query['preview_access_token'] = $silverbackPreviewLink->getToken();
132
+ $externalPreviewUrl->setOption('query', $query);
133
+ $externalPreviewUrlString = $externalPreviewUrl->setAbsolute()->toString();
134
+ }
135
+ else {
136
+ \Drupal::messenger()->addError('Preview link is only available for nodes.');
137
+ // This could be refactored to get the storage.
138
+ // Implement nodes for now as we are still using Drupal Gutenberg 2.x that
139
+ // is not entity type agnostic.
140
+ // $link = Url::fromRoute('entity.' . $host->getEntityTypeId() . '.silverback_preview_link', [
141
+ // $host->getEntityTypeId() => $host->id(),
142
+ // 'preview_token' => $previewLink->getToken(),
143
+ // ]);
144
+ return $form;
145
+ }
146
+
147
+ $originalAgeFormatted = $this->dateFormatter->formatInterval($this->linkExpiry->getLifetime(), 1);
148
+ $remainingSeconds = max(0, ($this->entity->getExpiry()?->getTimestamp() ?? 0) - $this->time->getRequestTime());
149
+ $remainingAgeFormatted = $this->dateFormatter->formatInterval($remainingSeconds);
150
+ $isNewToken = $this->linkExpiry->getLifetime() === $remainingSeconds;
151
+ $displayQRCode = TRUE;
152
+ $qrFallback = NULL;
153
+ $qrCodeUrlString = NULL;
154
+ $actionsDescription = NULL;
155
+ $previewLinkHasExpired = $remainingSeconds === 0;
156
+ $displayGif = \Drupal::state()->get('silverback_easter_mode') === '↑↑↓↓←→←→BA';
157
+
158
+ if ($isNewToken) {
159
+ $expiryDescription = $this->t('Expires @lifetime after creation.', [
160
+ '@lifetime' => $originalAgeFormatted,
161
+ ]);
162
+ $qrCode = (new QRCode)->render($externalPreviewUrlString);
163
+ }
164
+ else {
165
+ if ($previewLinkHasExpired) {
166
+ $expiryDescription = $this->t('⌛ <strong>Live preview link</a> for <em>@entity_label</em> has expired</strong>, reset link expiry or generate a new one.', [
167
+ ':url' => $externalPreviewUrlString,
168
+ '@entity_label' => $host->label(),
169
+ ]);
170
+ $displayQRCode = FALSE;
171
+ }
172
+ else {
173
+ $expiryDescription = $this->t('Live preview link for <em>@entity_label</em> expires in @lifetime.</p>', [
174
+ ':url' => $externalPreviewUrlString,
175
+ '@entity_label' => $host->label(),
176
+ '@lifetime' => $remainingAgeFormatted,
177
+ ]);
178
+ }
179
+ $actionsDescription = $this->t('If a new link is generated, the active link becomes invalid.');
180
+ }
181
+
182
+ if ($displayQRCode) {
183
+ $qrCodeEncodedUrl = str_replace(['/'], ['_'], base64_encode($externalPreviewUrlString));
184
+ try {
185
+ $qrCodeUrlString = Url::fromRoute('silverback_preview_link.qr_code', ['base64_url' => $qrCodeEncodedUrl])->toString();
186
+ }
187
+ catch (\Exception $e) {
188
+ $this->logger('silverback_preview_link')->error('Failed to generate branded QR code: @message', ['@message' => $e->getMessage()]);
189
+ try {
190
+ $qrFallback = (new QRCode)->render($externalPreviewUrlString);
191
+ }
192
+ catch (\Exception $e) {
193
+ $this->logger('silverback_preview_link')->error('Failed to generate fallback QR code: @message', ['@message' => $e->getMessage()]);
194
+ }
195
+ }
196
+ }
197
+
198
+ $form['preview_link'] = [
199
+ '#theme' => 'preview_link',
200
+ '#title' => $this->t('Preview link'),
201
+ '#weight' => -9999,
202
+ '#preview_link_has_expired' => $previewLinkHasExpired,
203
+ '#preview_url' => $externalPreviewUrlString,
204
+ '#preview_qr_code_url' => $qrCodeUrlString,
205
+ '#preview_qr_code_fallback' => $qrFallback,
206
+ '#expiry_description' => $expiryDescription,
207
+ '#actions_description' => $actionsDescription,
208
+ '#display_gif' => $displayGif,
209
+ ];
210
+
211
+ if (!$isNewToken) {
212
+ $form['actions']['regenerate_submit'] = $form['actions']['submit'];
213
+ $form['actions']['regenerate_submit']['#value'] = $this->t('Generate new link');
214
+ // Shift ::save to after ::regenerateToken.
215
+ $form['actions']['regenerate_submit']['#submit'] = array_diff($form['actions']['regenerate_submit']['#submit'], ['::save']);
216
+ $form['actions']['regenerate_submit']['#submit'][] = '::regenerateToken';
217
+ $form['actions']['regenerate_submit']['#submit'][] = '::save';
218
+ $form['actions']['regenerate_submit']['#ajax'] = [
219
+ 'callback' => [get_called_class(), 'ajaxRefreshForm'],
220
+ ];
221
+
222
+ $form['actions']['reset'] = [
223
+ '#type' => 'submit',
224
+ '#value' => $this->t('Reset current link expiry to @lifetime', ['@lifetime' => $originalAgeFormatted]),
225
+ '#submit' => ['::resetLifetime', '::save'],
226
+ '#ajax' => [
227
+ 'callback' => [get_called_class(), 'ajaxRefreshForm'],
228
+ ],
229
+ '#weight' => 100,
230
+ ];
231
+ }
232
+ unset($form['actions']['submit']);
233
+ $form['#attached']['library'][] = 'silverback_preview_link/copy';
234
+ $form['#attached']['library'][] = 'silverback_preview_link/modal';
235
+
236
+ return $form;
237
+ }
238
+
239
+ public static function ajaxRefreshForm(array $form, FormStateInterface $form_state) {
240
+ $triggering_element = $form_state->getTriggeringElement();
241
+ $element = NULL;
242
+ if (!empty($triggering_element['#ajax']['element'])) {
243
+ $element = NestedArray::getValue($form, $triggering_element['#ajax']['element']);
244
+ }
245
+ // Element not specified or not found. Show messages on top of the form.
246
+ if (!$element) {
247
+ $element = $form;
248
+ }
249
+ $response = new AjaxResponse();
250
+ $response->addCommand(new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form));
251
+ $response->addCommand(new PrependCommand('[data-drupal-selector="' . $element['#attributes']['data-drupal-selector'] . '"]', ['#type' => 'status_messages']));
252
+
253
+ return $response;
254
+ }
255
+
256
+ /**
257
+ * {@inheritdoc}
258
+ */
259
+ public function submitForm(array &$form, FormStateInterface $form_state) {
260
+ // For ajax.
261
+ $form_state->setRebuild();
262
+ parent::submitForm($form, $form_state);
263
+ // Update the changed timestamp of the entity.
264
+ $this->updateChangedTime($this->entity);
265
+ }
266
+
267
+ /**
268
+ * {@inheritdoc}
269
+ */
270
+ public function save(array $form, FormStateInterface $form_state) {
271
+ $result = parent::save($form, $form_state);
272
+ return $result;
273
+ }
274
+
275
+ /**
276
+ * Regenerates preview link token.
277
+ *
278
+ * @param array $form
279
+ * An associative array containing the structure of the form.
280
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
281
+ * The current state of the form.
282
+ */
283
+ public function regenerateToken(array &$form, FormStateInterface $form_state): void {
284
+ $this->entity->regenerateToken(TRUE);
285
+ $expiry = $this->getExpiry();
286
+ $this->entity->setExpiry($expiry);
287
+ $this->messenger()->addMessage($this->t('The live preview link token has been regenerated.'));
288
+ }
289
+
290
+ /**
291
+ * Resets the lifetime of the preview link.
292
+ *
293
+ * @param array $form
294
+ * An associative array containing the structure of the form.
295
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
296
+ * The current state of the form.
297
+ */
298
+ public function resetLifetime(array &$form, FormStateInterface $form_state): void {
299
+ $form_state->setRebuild();
300
+ $expiry = $this->getExpiry();
301
+ $this->entity->setExpiry($expiry);
302
+ $timezone = date_default_timezone_get();
303
+ $this->messenger()->addMessage($this->t('Preview link will now expire at %time.', [
304
+ '%time' => $this->dateFormatter->format($expiry->getTimestamp(), 'custom', 'd/m/y H:i', $timezone) . ' (' . $timezone . ')',
305
+ ]));
306
+ }
307
+
308
+ /**
309
+ * Helper to reset the expiry.
310
+ *
311
+ * @return \DateTimeImmutable|false
312
+ *
313
+ * @throws \Exception
314
+ */
315
+ private function getExpiry() {
316
+ $expiry = new \DateTimeImmutable('@' . $this->time->getRequestTime());
317
+ return $expiry->modify('+' . $this->linkExpiry->getLifetime() . ' seconds');
318
+ }
319
+
320
+ }
@@ -0,0 +1,204 @@
1
+ <?php
2
+
3
+ declare(strict_types = 1);
4
+
5
+ namespace Drupal\silverback_preview_link\Form;
6
+
7
+ use Drupal\Core\Config\ConfigFactoryInterface;
8
+ use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
9
+ use Drupal\Core\Entity\EntityTypeManagerInterface;
10
+ use Drupal\Core\Form\ConfigFormBase;
11
+ use Drupal\Core\Form\FormStateInterface;
12
+ use Drupal\Core\State\StateInterface;
13
+ use Drupal\silverback_preview_link\PreviewLinkUtility;
14
+ use Drupal\user\Entity\User;
15
+ use Symfony\Component\DependencyInjection\ContainerInterface;
16
+
17
+ /**
18
+ * Allow settings to be changed from the UI for Silverback preview link.
19
+ */
20
+ class SettingsForm extends ConfigFormBase {
21
+
22
+ /**
23
+ * Constructs a SettingsForm.
24
+ */
25
+ public function __construct(ConfigFactoryInterface $configFactory,
26
+ protected EntityTypeBundleInfoInterface $bundleInfo,
27
+ protected EntityTypeManagerInterface $entityTypeManager,
28
+ protected StateInterface $state,
29
+ ) {
30
+ parent::__construct($configFactory);
31
+ }
32
+
33
+ /**
34
+ * {@inheritdoc}
35
+ */
36
+ public static function create(ContainerInterface $container) {
37
+ return new static(
38
+ $container->get('config.factory'),
39
+ $container->get('entity_type.bundle.info'),
40
+ $container->get('entity_type.manager'),
41
+ $container->get('state'),
42
+ );
43
+ }
44
+
45
+ /**
46
+ * {@inheritdoc}
47
+ */
48
+ protected function getEditableConfigNames(): array {
49
+ return [
50
+ $this->getConfigName(),
51
+ ];
52
+ }
53
+
54
+ /**
55
+ * A method to get the config name.
56
+ *
57
+ * @return string
58
+ * The config name.
59
+ */
60
+ private function getConfigName(): string {
61
+ return 'silverback_preview_link.settings';
62
+ }
63
+
64
+ /**
65
+ * {@inheritdoc}
66
+ */
67
+ public function getFormId(): string {
68
+ return 'silverback_preview_link_settings_form';
69
+ }
70
+
71
+ /**
72
+ * {@inheritdoc}
73
+ */
74
+ public function buildForm(array $form, FormStateInterface $form_state): array {
75
+ $config = $this->config($this->getConfigName());
76
+ $form = parent::buildForm($form, $form_state);
77
+
78
+ $form['bundles'] = [
79
+ '#type' => 'details',
80
+ ];
81
+ $form['bundles']['help'] = [
82
+ '#markup' => $this->t('Enable entity type/bundles for use with preview link.'),
83
+ ];
84
+ $selectedOptions = $this->getSelectedEntityTypeOptions();
85
+ $form['bundles']['enabled_entity_types'] = [
86
+ '#type' => 'tableselect',
87
+ '#header' => [
88
+ $this->t('Entity type'),
89
+ $this->t('Bundle'),
90
+ ],
91
+ '#options' => $this->getEntityTypeOptions(),
92
+ '#default_value' => array_fill_keys($selectedOptions, TRUE),
93
+ ];
94
+ // Collapse the details element if anything is enabled.
95
+ $form['bundles']['#title'] = $this->t('Enabled types (@count)', [
96
+ '@count' => count($selectedOptions),
97
+ ]);
98
+ $form['bundles']['#open'] = count($selectedOptions) === 0;
99
+
100
+ $form['multiple_entities'] = [
101
+ '#type' => 'checkbox',
102
+ '#title' => 'Multiple entities',
103
+ '#description' => $this->t('Whether preview links can reference multiple entities.'),
104
+ '#default_value' => $config->get('multiple_entities'),
105
+ ];
106
+ $form['expiry_seconds'] = [
107
+ '#type' => 'number',
108
+ '#title' => 'Expiry seconds',
109
+ '#description' => $this->t('The number of seconds before a preview link expires.'),
110
+ '#default_value' => $config->get('expiry_seconds') ?: 604800,
111
+ '#min' => 1,
112
+ ];
113
+ $preview_user_id = $this->state->get('silverback_preview_link.default_preview_user');
114
+ $form['default_preview_user'] = [
115
+ '#type' => 'entity_autocomplete',
116
+ '#target_type' => 'user',
117
+ '#default_value' => $preview_user_id ? User::load($preview_user_id) : NULL,
118
+ '#selection_settings' => ['include_anonymous' => FALSE],
119
+ '#title' => $this->t('Default preview user'),
120
+ '#description' => $this->t('Attach a default user to all the preview links. By choosing an user, when the preview link will be accessed, the request will be authenticated using that user.'),
121
+ ];
122
+
123
+ return $form;
124
+ }
125
+
126
+ /**
127
+ * {@inheritdoc}
128
+ */
129
+ public function submitForm(array &$form, FormStateInterface $form_state) {
130
+ $config = $this->config($this->getConfigName());
131
+
132
+ $config->set('multiple_entities', $form_state->getValue('multiple_entities'));
133
+ $config->set('expiry_seconds', $form_state->getValue('expiry_seconds'));
134
+ $this->state->set('silverback_preview_link.default_preview_user', $form_state->getValue('default_preview_user'));
135
+
136
+ $config->clear('enabled_entity_types');
137
+ foreach (array_keys(array_filter($form_state->getValue('enabled_entity_types'))) as $enabledBundle) {
138
+ if (strpos($enabledBundle, ':') !== FALSE) {
139
+ [$entityTypeId, $bundle] = explode(':', $enabledBundle);
140
+ $bundles = $config->get('enabled_entity_types.' . $entityTypeId) ?: [];
141
+ $bundles[] = $bundle;
142
+ $config->set('enabled_entity_types.' . $entityTypeId, $bundles);
143
+ }
144
+ else {
145
+ $entityTypeId = $enabledBundle;
146
+ $config->set('enabled_entity_types.' . $entityTypeId, []);
147
+ }
148
+ }
149
+
150
+ $config->save();
151
+ parent::submitForm($form, $form_state);
152
+ }
153
+
154
+ /**
155
+ * The options available for the user to select for bundle types.
156
+ *
157
+ * @return array
158
+ * A 'entity_id:bundle' style array of possible options.
159
+ */
160
+ protected function getEntityTypeOptions(): array {
161
+ $options = [];
162
+ $entityTypes = $this->entityTypeManager->getDefinitions();
163
+ $entityTypes = array_filter($entityTypes, [
164
+ PreviewLinkUtility::class,
165
+ 'isEntityTypeSupported',
166
+ ]);
167
+ foreach ($entityTypes as $entityTypeId => $info) {
168
+ $options[$entityTypeId] = [
169
+ ['data' => ['#markup' => '<strong>' . $info->getLabel() . '</strong>']],
170
+ ['data' => ['#markup' => $this->t('<em>If selected and no bundles are selected, all bundles will be enabled.</em>')]],
171
+ ];
172
+ foreach ($this->bundleInfo->getBundleInfo($entityTypeId) as $bundle => $bundleInfo) {
173
+ if ($entityTypeId === $bundle) {
174
+ continue;
175
+ }
176
+ $options[sprintf('%s:%s', $entityTypeId, $bundle)] = [
177
+ $info->getLabel() ?: '',
178
+ $bundleInfo['label'] ?: '',
179
+ ];
180
+ }
181
+ }
182
+ return $options;
183
+ }
184
+
185
+ /**
186
+ * The enabled entities and bundles for preview link to apply to.
187
+ *
188
+ * @return array
189
+ * A 'entity_id:bundle' style array of selected options.
190
+ */
191
+ protected function getSelectedEntityTypeOptions(): array {
192
+ $config = $this->config($this->getConfigName());
193
+ $configured = $config->get('enabled_entity_types') ?: [];
194
+ $selected = [];
195
+ foreach ($configured as $entityTypeId => $bundles) {
196
+ $selected[] = $entityTypeId;
197
+ foreach ($bundles as $bundle) {
198
+ $selected[] = sprintf('%s:%s', $entityTypeId, $bundle);
199
+ }
200
+ }
201
+ return $selected;
202
+ }
203
+
204
+ }
@@ -0,0 +1,25 @@
1
+ <?php
2
+
3
+ declare(strict_types = 1);
4
+
5
+ namespace Drupal\silverback_preview_link\Plugin\EntityReferenceSelection;
6
+
7
+ use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
8
+
9
+ /**
10
+ * Provides specific access control for the node entity type.
11
+ *
12
+ * This selection plugin can be changed by altering EntityReferenceSelection
13
+ * manager definitions or by altering base field definitions.
14
+ *
15
+ * @EntityReferenceSelection(
16
+ * id = "silverback_preview_link",
17
+ * label = @Translation("Preview Link Default"),
18
+ * group = "silverback_preview_link",
19
+ * weight = 0,
20
+ * deriver = "Drupal\Core\Entity\Plugin\Derivative\DefaultSelectionDeriver"
21
+ * )
22
+ */
23
+ final class SilverbackPreviewLinkSelection extends DefaultSelection {
24
+
25
+ }
@@ -0,0 +1,103 @@
1
+ <?php
2
+
3
+ declare(strict_types = 1);
4
+
5
+ namespace Drupal\silverback_preview_link\Plugin\Field\FieldWidget;
6
+
7
+ use Drupal\Core\Field\FieldDefinitionInterface;
8
+ use Drupal\Core\Field\FieldItemListInterface;
9
+ use Drupal\Core\Form\FormStateInterface;
10
+ use Drupal\Core\Routing\RouteMatchInterface;
11
+ use Drupal\dynamic_entity_reference\Plugin\Field\FieldWidget\DynamicEntityReferenceWidget;
12
+ use Drupal\silverback_preview_link\Form\PreviewLinkForm;
13
+ use Symfony\Component\DependencyInjection\ContainerInterface;
14
+
15
+ /**
16
+ * Form widget for entities field on Preview Link.
17
+ *
18
+ * Prevents mixing referenced entity types, unless they were created
19
+ * programmatically.
20
+ *
21
+ * @FieldWidget(
22
+ * id = "silverback_preview_link_entities_widget",
23
+ * label = @Translation("Preview Link Entities Widget"),
24
+ * description = @Translation("Widget for selecting entities related to a Preview Link"),
25
+ * field_types = {
26
+ * "dynamic_entity_reference"
27
+ * }
28
+ * )
29
+ *
30
+ * @internal
31
+ */
32
+ final class PreviewLinkEntitiesWidget extends DynamicEntityReferenceWidget {
33
+
34
+ /**
35
+ * The current route match.
36
+ *
37
+ * @var \Drupal\Core\Routing\RouteMatchInterface
38
+ */
39
+ protected RouteMatchInterface $routeMatch;
40
+
41
+ /**
42
+ * {@inheritdoc}
43
+ */
44
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
45
+ $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
46
+ $instance->routeMatch = $container->get('current_route_match');
47
+ return $instance;
48
+ }
49
+
50
+ /**
51
+ * {@inheritdoc}
52
+ */
53
+ public static function isApplicable(FieldDefinitionInterface $field_definition) {
54
+ $storageDefinition = $field_definition->getFieldStorageDefinition();
55
+ return $storageDefinition->getTargetEntityTypeId() === 'silverback_preview_link' && $storageDefinition->getName() === 'entities';
56
+ }
57
+
58
+ /**
59
+ * {@inheritdoc}
60
+ */
61
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
62
+ $element = parent::formElement($items, $delta, $element, $form, $form_state);
63
+ $formObject = $form_state->getFormObject();
64
+ if (!$formObject instanceof PreviewLinkForm) {
65
+ throw new \LogicException('Can only be used with PreviewLinkForm');
66
+ }
67
+
68
+ /** @var \Drupal\dynamic_entity_reference\Plugin\Field\FieldType\DynamicEntityReferenceItem $item */
69
+ $item = $items->get($delta);
70
+ $targetType = $item->target_type;
71
+ $targetId = $item->target_id;
72
+ $host = $formObject->getHostEntity($this->routeMatch);
73
+ $hostEntityTypeId = $host->getEntityTypeId();
74
+
75
+ // Swap select field to value.
76
+ if ($element['target_type']['#type'] !== 'value') {
77
+ $element['target_type'] = [
78
+ '#type' => 'value',
79
+ '#value' => $targetType,
80
+ ];
81
+ }
82
+
83
+ // If target type not set yet (e.g for new items). Set the value.
84
+ if (empty($targetType)) {
85
+ // Force new items to be the same as host entity type.
86
+ $element['target_type']['#value'] = $hostEntityTypeId;
87
+
88
+ // Set otherwise autocomplete will use the wrong route.
89
+ $settings = $this->getFieldSettings();
90
+ $element['target_id']['#target_type'] = $hostEntityTypeId;
91
+ $element['target_id']['#selection_handler'] = $settings[$hostEntityTypeId]['handler'];
92
+ $element['target_id']['#selection_settings'] = $settings[$hostEntityTypeId]['handler_settings'];
93
+ }
94
+
95
+ // Protect host entity from modification.
96
+ if ($targetType == $hostEntityTypeId && $targetId == $host->id()) {
97
+ $element['target_id']['#disabled'] = TRUE;
98
+ }
99
+
100
+ return $element;
101
+ }
102
+
103
+ }
@@ -0,0 +1,26 @@
1
+ <?php
2
+
3
+ declare(strict_types = 1);
4
+
5
+ namespace Drupal\silverback_preview_link\Plugin\Validation\Constraint;
6
+
7
+ use Symfony\Component\Validator\Constraint;
8
+
9
+ /**
10
+ * Ensures each entity is referenced at most once.
11
+ *
12
+ * @Constraint(
13
+ * id = "SilverbackPreviewLinkEntitiesUniqueConstraint",
14
+ * label = @Translation("Validates referenced value uniqueness", context = "Validation"),
15
+ * )
16
+ */
17
+ class SilverbackPreviewLinkEntitiesUniqueConstraint extends Constraint {
18
+
19
+ /**
20
+ * Violation message for when an entity is referenced multiple times.
21
+ *
22
+ * @var string
23
+ */
24
+ public $multipleReferences = '%entity_type is already referenced by item #%other_delta.';
25
+
26
+ }