@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,118 @@
1
+ <?php
2
+
3
+ namespace Drupal\silverback_preview_link\Authentication\Provider;
4
+
5
+ use Drupal\Core\Authentication\AuthenticationProviderInterface;
6
+ use Drupal\Core\PageCache\ResponsePolicy\KillSwitch;
7
+ use Drupal\Core\Session\AccountInterface;
8
+ use Drupal\silverback_preview_link\PreviewLinkExpiry;
9
+ use Drupal\silverback_preview_link\PreviewLinkStorageInterface;
10
+ use Symfony\Component\HttpFoundation\Request;
11
+
12
+ /**
13
+ * Authentication provider based on a token from a preview link.
14
+ */
15
+ class PreviewToken implements AuthenticationProviderInterface {
16
+
17
+ /**
18
+ * The preview link storage service.
19
+ *
20
+ * @var \Drupal\silverback_preview_link\PreviewLinkStorageInterface
21
+ */
22
+ protected PreviewLinkStorageInterface $previewLinkStorage;
23
+
24
+ /**
25
+ * The page cache kill switch service.
26
+ *
27
+ * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch
28
+ */
29
+ protected KillSwitch $killSwitch;
30
+
31
+ /**
32
+ * The preview link expiry service.
33
+ * @var PreviewLinkExpiry
34
+ */
35
+ protected PreviewLinkExpiry $previewLinkExpiry;
36
+
37
+ /**
38
+ * Constructs a new token authentication provider.
39
+ */
40
+ public function __construct() {
41
+ // If we inject the entity entity type manager service, we get a circular
42
+ // dependency error, that is why we access the entity type manager service
43
+ // from here.
44
+ /** @var \Drupal\silverback_preview_link\PreviewLinkStorageInterface $storage */
45
+ $storage = \Drupal::entityTypeManager()->getStorage('silverback_preview_link');
46
+ $this->previewLinkStorage = $storage;
47
+ $this->killSwitch = \Drupal::service('page_cache_kill_switch');
48
+ $this->previewLinkExpiry = \Drupal::service('silverback_preview_link.link_expiry');
49
+ }
50
+
51
+ /**
52
+ * {@inheritdoc}
53
+ */
54
+ public function applies(Request $request) {
55
+ $previewToken = $this->getPreviewTokenFromRequest($request);
56
+ return !empty($previewToken);
57
+ }
58
+
59
+ /**
60
+ * {@inheritdoc}
61
+ */
62
+ public function authenticate(Request $request) {
63
+ // If we authenticate the user with a token, we do not want to cache the
64
+ // page.
65
+ $this->killSwitch->trigger();
66
+ $previewToken = $this->getPreviewTokenFromRequest($request);
67
+ return $this->getUserFromPreviewToken($previewToken);
68
+ }
69
+
70
+ /**
71
+ * Returns a preview token from the request (the preview_access_token query
72
+ * parameter).
73
+ *
74
+ * @param \Symfony\Component\HttpFoundation\Request $request
75
+ * @return bool|float|int|string|null
76
+ */
77
+ protected function getPreviewTokenFromRequest(Request $request) {
78
+ return $request->query->get('preview_access_token');
79
+ }
80
+
81
+ /**
82
+ * Returns the User object for a given preview token.
83
+ *
84
+ * The preview link entity which corresponds to the token will be loaded and
85
+ * if there is a user associated with that token it will be returned.
86
+ *
87
+ * @param string $previewToken
88
+ * The preview token.
89
+ *
90
+ * @return \Drupal\Core\Session\AccountInterface|null
91
+ * The User object for the current user, or NULL for anonymous.
92
+ */
93
+ protected function getUserFromPreviewToken(string $previewToken): AccountInterface|null {
94
+ $previewLink = $this->previewLinkStorage->loadByProperties(['token' => $previewToken]);
95
+ if (empty($previewLink)) {
96
+ return NULL;
97
+ }
98
+ // The loadByProperties() method returns the result as an array, so just
99
+ // take the first element.
100
+ $previewLink = reset($previewLink);
101
+ $referencedEntities = $previewLink->get('entities')->referencedEntities();
102
+ if (empty($referencedEntities)) {
103
+ return NULL;
104
+ }
105
+ $referencedUser = array_reduce($referencedEntities, function ($carry, $entity) {
106
+ if ($entity->getEntityTypeId() === 'user' && $entity->isActive()) {
107
+ $carry = $entity;
108
+ }
109
+ return $carry;
110
+ }, NULL);
111
+ // No active user entity reference found for the preview link, just return
112
+ // NULL.
113
+ if (empty($referencedUser)) {
114
+ return NULL;
115
+ }
116
+ return $referencedUser;
117
+ }
118
+ }
@@ -0,0 +1,83 @@
1
+ <?php
2
+
3
+ namespace Drupal\silverback_preview_link\Controller;
4
+
5
+ use Drupal\Core\Cache\CacheableResponse;
6
+ use Drupal\Core\Controller\ControllerBase;
7
+ use Drupal\silverback_preview_link\QRCodeWithLogo;
8
+ use Drupal\user\Entity\User;
9
+ use Symfony\Component\HttpFoundation\JsonResponse;
10
+
11
+ class PreviewController extends ControllerBase {
12
+
13
+ /**
14
+ * Checks if the current user has access to the Preview app.
15
+ */
16
+ public function hasAccess() {
17
+ /** @var \Drupal\Core\Session\AccountProxyInterface $userAccount */
18
+ $userAccount = $this->currentUser();
19
+ // Verify permission against User entity.
20
+ $userEntity = User::load($userAccount->id());
21
+ if ($userEntity->hasPermission('use external preview')) {
22
+ return new JsonResponse([
23
+ 'access' => TRUE,
24
+ ], 200);
25
+ }
26
+ else {
27
+ return new JsonResponse([
28
+ 'access' => FALSE,
29
+ ], 403);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Skip Drupal authentication if there is a valid preview token.
35
+ *
36
+ * @todo: previously, this method used to also check if the preview access
37
+ * token has been attached to an entity (the entity type and entity ids were
38
+ * sent as parameters). This approach will change in the future (as part of a
39
+ * bigger refactoring) where preview links won't be attached to content
40
+ * entities anymore, so this method might change again.
41
+ */
42
+ public function hasLinkAccess() {
43
+ $requestContent = \Drupal::request()->getContent();
44
+ $body = json_decode($requestContent, TRUE);
45
+ if (!empty($body['preview_access_token'])) {
46
+ try {
47
+ $storage = \Drupal::entityTypeManager()->getStorage('silverback_preview_link');
48
+ $previewLink = $storage->loadByProperties(['token' => $body['preview_access_token']]);
49
+ if (empty($previewLink)) {
50
+ return new JsonResponse([
51
+ 'access' => FALSE,
52
+ ], 403);
53
+ }
54
+
55
+ // @todo: optionally, we could also check if the link has expired.
56
+ // Expired links should be, however, deleted by the cron job. As this
57
+ // part of the code will probably suffer modifications during the next
58
+ // bigger refactoring (see the todo in the method's description), we
59
+ // will just check for now if the link simply exists.
60
+ return new JsonResponse([
61
+ 'access' => TRUE,
62
+ ], 200);
63
+ }
64
+ catch (\Exception $e) {
65
+ $this->getLogger('silverback_preview_link')->error($e->getMessage());
66
+ }
67
+ }
68
+ return new JsonResponse([
69
+ 'access' => FALSE,
70
+ ], 403);
71
+ }
72
+
73
+ /**
74
+ * Returns the QR SVG file.
75
+ */
76
+ public function getQRCode(string $base64_url): CacheableResponse {
77
+ $decodedUrl = base64_decode(str_replace(['_'], ['/'], $base64_url));
78
+ $qrCode = new QRCodeWithLogo();
79
+ $result = $qrCode->getQRCode($decodedUrl);
80
+ return new CacheableResponse($result, 200, ['Content-Type' => 'image/svg+xml']);
81
+ }
82
+
83
+ }
@@ -0,0 +1,224 @@
1
+ <?php
2
+
3
+ declare(strict_types = 1);
4
+
5
+ namespace Drupal\silverback_preview_link\Entity;
6
+
7
+ use Drupal\Component\Assertion\Inspector;
8
+ use Drupal\Core\Entity\ContentEntityBase;
9
+ use Drupal\Core\Entity\EntityInterface;
10
+ use Drupal\Core\Entity\EntityStorageInterface;
11
+ use Drupal\Core\Entity\EntityTypeInterface;
12
+ use Drupal\Core\Field\BaseFieldDefinition;
13
+ use Drupal\Core\Field\FieldStorageDefinitionInterface;
14
+ use Drupal\Core\Url;
15
+ use Drupal\user\Entity\User;
16
+
17
+ /**
18
+ * Defines the Silverback preview link entity class.
19
+ *
20
+ * @ContentEntityType(
21
+ * id = "silverback_preview_link",
22
+ * label = @Translation("Preview Link"),
23
+ * base_table = "silverback_preview_link",
24
+ * handlers = {
25
+ * "views_data" = "Drupal\views\EntityViewsData",
26
+ * "storage" = "Drupal\silverback_preview_link\PreviewLinkStorage",
27
+ * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
28
+ * "form" = {
29
+ * "silverback_preview_link" = "Drupal\silverback_preview_link\Form\PreviewLinkForm",
30
+ * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
31
+ * },
32
+ * "route_provider" = {
33
+ * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
34
+ * },
35
+ * },
36
+ * links = {
37
+ * "canonical" = "/admin/content/preview-link/{silverback_preview_link}",
38
+ * "delete-form" = "/admin/content/preview-link/{silverback_preview_link}/delete",
39
+ * },
40
+ * admin_permission = "administer site configuration",
41
+ * entity_keys = {
42
+ * "id" = "id"
43
+ * }
44
+ * )
45
+ *
46
+ * @property \Drupal\dynamic_entity_reference\Plugin\Field\FieldType\DynamicEntityReferenceFieldItemList $entities
47
+ */
48
+ class SilverbackPreviewLink extends ContentEntityBase implements SilverbackPreviewLinkInterface {
49
+
50
+ /**
51
+ * Keep track on whether we need a new token upon save.
52
+ *
53
+ * @var bool
54
+ */
55
+ protected $needsNewToken = FALSE;
56
+ /**
57
+ * {@inheritDoc}
58
+ */
59
+ public function postCreate(EntityStorageInterface $storage) {
60
+ parent::postCreate($storage);
61
+ // If there is a default preview user set, then we add that user to the list
62
+ // of entities attached to the link.
63
+ $state = \Drupal::state();
64
+ $default_preview_user = $state->get('silverback_preview_link.default_preview_user');
65
+ if ($default_preview_user) {
66
+ $this->addEntity(User::load($default_preview_user));
67
+ }
68
+ }
69
+
70
+ /**
71
+ * {@inheritdoc}
72
+ */
73
+ public function getUrl(EntityInterface $entity): Url {
74
+ return Url::fromRoute(sprintf('entity.%s.silverback_preview_link', $entity->getEntityTypeId()), [
75
+ $entity->getEntityTypeId() => $entity->id(),
76
+ 'preview_token' => $this->getToken(),
77
+ ]);
78
+ }
79
+
80
+ /**
81
+ * {@inheritdoc}
82
+ */
83
+ public function getToken(): string {
84
+ return $this->get('token')->value;
85
+ }
86
+
87
+ /**
88
+ * {@inheritdoc}
89
+ */
90
+ public function setToken($token) {
91
+ $this->set('token', $token);
92
+ // Add a second so our testing always works.
93
+ $this->set('generated_timestamp', time() + 1);
94
+ return $this;
95
+ }
96
+
97
+ /**
98
+ * {@inheritdoc}
99
+ */
100
+ public function regenerateToken($needs_new_token = FALSE): bool {
101
+ $current_value = $this->needsNewToken;
102
+ $this->needsNewToken = $needs_new_token;
103
+ return $current_value;
104
+ }
105
+
106
+ /**
107
+ * {@inheritdoc}
108
+ */
109
+ public function getGeneratedTimestamp(): int {
110
+ return (int) $this->get('generated_timestamp')->value;
111
+ }
112
+
113
+ /**
114
+ * {@inheritdoc}
115
+ */
116
+ public function getEntities(): array {
117
+ return $this->entities->referencedEntities();
118
+ }
119
+
120
+ /**
121
+ * {@inheritdoc}
122
+ */
123
+ public function setEntities(array $entities) {
124
+ assert(Inspector::assertAllObjects($entities, EntityInterface::class));
125
+ return $this->set('entities', $entities);
126
+ }
127
+
128
+ /**
129
+ * {@inheritdoc}
130
+ */
131
+ public function addEntity(EntityInterface $entity) {
132
+ $this->entities->appendItem($entity);
133
+ return $this;
134
+ }
135
+
136
+ /**
137
+ * {@inheritdoc}
138
+ */
139
+ public function getExpiry(): ?\DateTimeImmutable {
140
+ $value = $this->expiry->value ?? NULL;
141
+ if (!is_numeric($value)) {
142
+ return NULL;
143
+ }
144
+
145
+ return new \DateTimeImmutable('@' . $value);
146
+ }
147
+
148
+ /**
149
+ * {@inheritdoc}
150
+ */
151
+ public function setExpiry(\DateTimeInterface $expiry) {
152
+ return $this->set('expiry', $expiry->getTimestamp());
153
+ }
154
+
155
+ /**
156
+ * {@inheritdoc}
157
+ */
158
+ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
159
+ $fields = parent::baseFieldDefinitions($entity_type);
160
+ $fields['token'] = BaseFieldDefinition::create('string')
161
+ ->setLabel(t('Preview Token'))
162
+ ->setDescription(t('A token that allows any user to view a preview of this entity.'))
163
+ ->setRequired(TRUE);
164
+
165
+ $fields['entities'] = BaseFieldDefinition::create('dynamic_entity_reference')
166
+ ->setLabel(t('Entities'))
167
+ ->setDescription(t('The associated entities this preview link unlocks.'))
168
+ ->setRequired(TRUE)
169
+ ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
170
+ ->addConstraint('SilverbackPreviewLinkEntitiesUniqueConstraint', [])
171
+ ->setSettings(static::entitiesDefaultFieldSettings())
172
+ ->setDisplayOptions('form', [
173
+ 'type' => 'silverback_preview_link_entities_widget',
174
+ 'weight' => 10,
175
+ ]);
176
+
177
+ $fields['generated_timestamp'] = BaseFieldDefinition::create('timestamp')
178
+ ->setLabel(t('Generated Timestamp'))
179
+ ->setDescription(t('The time the link was generated'))
180
+ ->setRequired(TRUE);
181
+
182
+ $fields['expiry'] = BaseFieldDefinition::create('timestamp')
183
+ ->setLabel(t('Time this Preview Link expires.'))
184
+ ->setDescription(t('The time after which the preview link is no longer valid.'))
185
+ ->setDefaultValueCallback(static::class . '::expiryDefaultValue')
186
+ ->setRequired(TRUE);
187
+
188
+ return $fields;
189
+ }
190
+
191
+ /**
192
+ * Rewrites settings for 'entities' dynamic_entity_reference field.
193
+ *
194
+ * DynamicEntityReferenceItem::defaultFieldSettings doesn't receive any context,
195
+ * so we need to change the default handlers manually.
196
+ */
197
+ public static function entitiesDefaultFieldSettings(): array {
198
+ $labels = \Drupal::service('entity_type.repository')->getEntityTypeLabels(TRUE);
199
+ $options = $labels[(string) t('Content', [], ['context' => 'Entity type group'])];
200
+ $settings = [
201
+ 'exclude_entity_types' => TRUE,
202
+ 'entity_type_ids' => [],
203
+ ];
204
+ $settings += array_fill_keys(array_keys($options), [
205
+ 'handler' => 'silverback_preview_link',
206
+ 'handler_settings' => [],
207
+ ]);
208
+ return $settings;
209
+ }
210
+
211
+ /**
212
+ * Get default value for 'expiry' field.
213
+ *
214
+ * @return int<0, max>
215
+ * A timestamp.
216
+ */
217
+ public static function expiryDefaultValue(): int {
218
+ $time = \Drupal::time();
219
+ /** @var \Drupal\silverback_preview_link\PreviewLinkExpiry $linkExpiry */
220
+ $linkExpiry = \Drupal::service('silverback_preview_link.link_expiry');
221
+ return max(0, $time->getRequestTime() + $linkExpiry->getLifetime());
222
+ }
223
+
224
+ }
@@ -0,0 +1,110 @@
1
+ <?php
2
+
3
+ declare(strict_types = 1);
4
+
5
+ namespace Drupal\silverback_preview_link\Entity;
6
+
7
+ use Drupal\Core\Entity\ContentEntityInterface;
8
+ use Drupal\Core\Entity\EntityInterface;
9
+ use Drupal\Core\Url;
10
+
11
+ /**
12
+ * Interface for the preview link entity.
13
+ */
14
+ interface SilverbackPreviewLinkInterface extends ContentEntityInterface {
15
+
16
+ /**
17
+ * The URL for this preview link for an entity.
18
+ *
19
+ * @param \Drupal\Core\Entity\EntityInterface $entity
20
+ * A host entity.
21
+ *
22
+ * @return \Drupal\Core\Url
23
+ * The url object.
24
+ */
25
+ public function getUrl(EntityInterface $entity): Url;
26
+
27
+ /**
28
+ * Gets thew new token.
29
+ *
30
+ * @return string
31
+ * The token.
32
+ */
33
+ public function getToken(): string;
34
+
35
+ /**
36
+ * Set the new token.
37
+ *
38
+ * @param string $token
39
+ * The new token.
40
+ *
41
+ * @return $this
42
+ * Return this for chaining.
43
+ */
44
+ public function setToken(string $token);
45
+
46
+ /**
47
+ * Mark the entity needing a new token. Only updated upon save.
48
+ *
49
+ * @param bool $needs_new_token
50
+ * Tell this entity to generate a new token.
51
+ *
52
+ * @return bool
53
+ * TRUE if it was currently marked to generate otherwise FALSE.
54
+ */
55
+ public function regenerateToken($needs_new_token = FALSE): bool;
56
+
57
+ /**
58
+ * Gets the timestamp stamp of when the token was generated.
59
+ *
60
+ * @return int
61
+ * The timestamp.
62
+ */
63
+ public function getGeneratedTimestamp(): int;
64
+
65
+ /**
66
+ * Get entities this preview link unlocks.
67
+ *
68
+ * Ideally preview link access is determined via PreviewLinkHost service.
69
+ *
70
+ * @return \Drupal\Core\Entity\EntityInterface[]
71
+ * Associated entities.
72
+ */
73
+ public function getEntities(): array;
74
+
75
+ /**
76
+ * Set the entity this preview link unlocks.
77
+ *
78
+ * @return $this
79
+ * Return this for chaining.
80
+ */
81
+ public function setEntities(array $entities);
82
+
83
+ /**
84
+ * Add an entity for this preview link to unlock.
85
+ *
86
+ * @return $this
87
+ * Return this for chaining.
88
+ */
89
+ public function addEntity(EntityInterface $entity);
90
+
91
+ /**
92
+ * Get expiration date.
93
+ *
94
+ * @return \DateTimeImmutable|null
95
+ * The expiration date, or NULL if not set.
96
+ */
97
+ public function getExpiry(): ?\DateTimeImmutable;
98
+
99
+ /**
100
+ * Set the expiration date.
101
+ *
102
+ * @param \DateTimeInterface $expiry
103
+ * The expiration date.
104
+ *
105
+ * @return $this
106
+ * Return this for chaining.
107
+ */
108
+ public function setExpiry(\DateTimeInterface $expiry);
109
+
110
+ }