@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.
- package/CHANGELOG.md +8 -0
- package/drupal/silverback_preview_link/CHANGELOG.md +195 -0
- package/drupal/silverback_preview_link/README.md +30 -0
- package/drupal/silverback_preview_link/composer.json +13 -0
- package/drupal/silverback_preview_link/config/install/silverback_preview_link.settings.yml +3 -0
- package/drupal/silverback_preview_link/config/schema/silverback_preview_link.schema.yml +14 -0
- package/drupal/silverback_preview_link/css/modal.css +17 -0
- package/drupal/silverback_preview_link/js/copy.js +39 -0
- package/drupal/silverback_preview_link/silverback_preview_link.info.yml +12 -0
- package/drupal/silverback_preview_link/silverback_preview_link.install +6 -0
- package/drupal/silverback_preview_link/silverback_preview_link.libraries.yml +9 -0
- package/drupal/silverback_preview_link/silverback_preview_link.links.menu.yml +5 -0
- package/drupal/silverback_preview_link/silverback_preview_link.module +79 -0
- package/drupal/silverback_preview_link/silverback_preview_link.permissions.yml +9 -0
- package/drupal/silverback_preview_link/silverback_preview_link.routing.yml +40 -0
- package/drupal/silverback_preview_link/silverback_preview_link.services.yml +31 -0
- package/drupal/silverback_preview_link/src/Access/PreviewEnabledAccessCheck.php +80 -0
- package/drupal/silverback_preview_link/src/Access/PreviewLinkAccessCheck.php +49 -0
- package/drupal/silverback_preview_link/src/Authentication/Provider/PreviewToken.php +118 -0
- package/drupal/silverback_preview_link/src/Controller/PreviewController.php +83 -0
- package/drupal/silverback_preview_link/src/Entity/SilverbackPreviewLink.php +224 -0
- package/drupal/silverback_preview_link/src/Entity/SilverbackPreviewLinkInterface.php +110 -0
- package/drupal/silverback_preview_link/src/Form/PreviewLinkForm.php +320 -0
- package/drupal/silverback_preview_link/src/Form/SettingsForm.php +204 -0
- package/drupal/silverback_preview_link/src/Plugin/EntityReferenceSelection/SilverbackPreviewLinkSelection.php +25 -0
- package/drupal/silverback_preview_link/src/Plugin/Field/FieldWidget/PreviewLinkEntitiesWidget.php +103 -0
- package/drupal/silverback_preview_link/src/Plugin/Validation/Constraint/SilverbackPreviewLinkEntitiesUniqueConstraint.php +26 -0
- package/drupal/silverback_preview_link/src/Plugin/Validation/Constraint/SilverbackPreviewLinkEntitiesUniqueConstraintValidator.php +44 -0
- package/drupal/silverback_preview_link/src/PreviewLinkExpiry.php +55 -0
- package/drupal/silverback_preview_link/src/PreviewLinkHooks.php +61 -0
- package/drupal/silverback_preview_link/src/PreviewLinkHost.php +70 -0
- package/drupal/silverback_preview_link/src/PreviewLinkHostInterface.php +49 -0
- package/drupal/silverback_preview_link/src/PreviewLinkStorage.php +99 -0
- package/drupal/silverback_preview_link/src/PreviewLinkStorageInterface.php +19 -0
- package/drupal/silverback_preview_link/src/PreviewLinkUtility.php +27 -0
- package/drupal/silverback_preview_link/src/QRCodeWithLogo.php +84 -0
- package/drupal/silverback_preview_link/src/QRMarkupSVGWithLogo.php +51 -0
- package/drupal/silverback_preview_link/src/Routing/PreviewLinkRouteProvider.php +61 -0
- package/drupal/silverback_preview_link/src/images/amazee-labs_logo-square-green.svg +13 -0
- package/drupal/silverback_preview_link/templates/preview-link.html.twig +39 -0
- 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
|
+
}
|
package/drupal/silverback_preview_link/src/Plugin/Field/FieldWidget/PreviewLinkEntitiesWidget.php
ADDED
|
@@ -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
|
+
}
|