From 0cb2f9c0ccef4a9f2d096d4332c9b3e8db12c0ff Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 27 Feb 2023 15:33:42 +0100 Subject: [PATCH 1/2] Added linked data --- .github/workflows/pr.yaml | 4 + README.md | 32 +++++ composer.json | 12 +- os2forms_rest_api.services.yml | 13 +- patches/webform_rest_submission.patch | 107 +++++++++++++++ ...r.php => WebformAccessEventSubscriber.php} | 4 +- .../WebformSubmissionDataEventSubscriber.php | 126 ++++++++++++++++++ src/WebformHelper.php | 4 +- 8 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 patches/webform_rest_submission.patch rename src/EventSubscriber/{EventSubscriber.php => WebformAccessEventSubscriber.php} (96%) create mode 100644 src/EventSubscriber/WebformSubmissionDataEventSubscriber.php diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 59c34f6..4ec48a1 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -105,6 +105,10 @@ jobs: # Restore Drupal composer repository. composer --no-interaction --working-dir=drupal config repositories.drupal composer https://packages.drupal.org/8 + composer --no-interaction --working-dir=drupal config --no-plugins allow-plugins.cweagans/composer-patches true + # @see https://getcomposer.org/doc/03-cli.md#modifying-extra-values + composer --no-interaction --working-dir=drupal config --no-plugins --json extra.enable-patching true + # Require our module. composer --no-interaction --working-dir=drupal require 'os2forms/os2forms_rest_api:*' diff --git a/README.md b/README.md index 09fc73c..0d5ad8f 100644 --- a/README.md +++ b/README.md @@ -144,3 +144,35 @@ details. In order to make documents accessible for api users the Key auth `authentication_provider` service has been overwritten to be global. See [os2forms_rest_api.services](os2forms_rest_api.services.yml). + +## Linked data + +To make using the REST API easier we add linked data to `GET` responses: + +```json +{ + … + "data": { + "file": "87", + "name": "The book", + "linked": { + "file": { + "87": { + "id": "87", + "url": "http://os2forms.example.com/system/files/webform/os2forms/1/cover.jpg", + "mime_type": "image/jpeg", + "size": "96757" + } + } + } + } +} +``` + +### Technical details on linked data + +In order to add linked data, we apply a patch, +[webform_rest_submission.patch](patches/webform_rest_submission.patch), to the +Webform REST module and implement an event subscriber, +[WebformSubmissionDataEventSubscriber](src/EventSubscriber/WebformSubmissionDataEventSubscriber.php), +to add the linked data. diff --git a/composer.json b/composer.json index 95ee865..da1e527 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ } ], "require": { + "cweagans/composer-patches": "^1.7", "drupal/key_auth": "^2.0", "drupal/webform_rest": "^4.0" }, @@ -49,7 +50,16 @@ "config": { "sort-packages": true, "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "cweagans/composer-patches": true + } + }, + "extra": { + "enable-patching": true, + "patches": { + "drupal/webform_rest": { + "Added ability to modify response data sent from Webform Submission endpoint": "https://raw.githubusercontent.com/itk-dev/os2forms_rest_api/feature/linked-data/patches/webform_rest_submission.patch" + } } } } diff --git a/os2forms_rest_api.services.yml b/os2forms_rest_api.services.yml index 1d0772e..cdb65be 100644 --- a/os2forms_rest_api.services.yml +++ b/os2forms_rest_api.services.yml @@ -1,4 +1,8 @@ services: + logger.channel.os2forms_rest_api: + parent: logger.channel_base + arguments: [ 'os2forms_rest_api' ] + Drupal\os2forms_rest_api\WebformHelper: arguments: - '@entity_type.manager' @@ -6,7 +10,7 @@ services: - '@key_auth.authentication.key_auth' - '@request_stack' - Drupal\os2forms_rest_api\EventSubscriber\EventSubscriber: + Drupal\os2forms_rest_api\EventSubscriber\WebformAccessEventSubscriber: arguments: - '@current_route_match' - '@current_user' @@ -14,6 +18,13 @@ services: tags: - { name: 'event_subscriber' } + Drupal\os2forms_rest_api\EventSubscriber\WebformSubmissionDataEventSubscriber: + arguments: + - '@entity_type.manager' + - '@logger.channel.os2forms_rest_api' + tags: + - { name: 'event_subscriber' } + # Overwrite, adding global tag # @see https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/altering-existing-services-providing-dynamic-services key_auth.authentication.key_auth: diff --git a/patches/webform_rest_submission.patch b/patches/webform_rest_submission.patch new file mode 100644 index 0000000..0ee1fc3 --- /dev/null +++ b/patches/webform_rest_submission.patch @@ -0,0 +1,107 @@ +diff --git a/src/Event/WebformSubmissionDataEvent.php b/src/Event/WebformSubmissionDataEvent.php +new file mode 100644 +index 0000000..c378f45 +--- /dev/null ++++ b/src/Event/WebformSubmissionDataEvent.php +@@ -0,0 +1,52 @@ ++webformSubmission = $webformSubmission; ++ $this->setData($data); ++ } ++ ++ /** ++ * @return WebformSubmissionInterface ++ */ ++ public function getWebformSubmission(): WebformSubmissionInterface ++ { ++ return $this->webformSubmission; ++ } ++ ++ public function getData(): array ++ { ++ return $this->data; ++ } ++ ++ public function setData(array $data) ++ { ++ $this->data = $data; ++ return $this; ++ } ++} +diff --git a/src/Plugin/rest/resource/WebformSubmissionResource.php b/src/Plugin/rest/resource/WebformSubmissionResource.php +index d2e08c5..a9cacf7 100644 +--- a/src/Plugin/rest/resource/WebformSubmissionResource.php ++++ b/src/Plugin/rest/resource/WebformSubmissionResource.php +@@ -5,6 +5,7 @@ namespace Drupal\webform_rest\Plugin\rest\resource; + use Drupal\webform\WebformSubmissionForm; + use Drupal\rest\Plugin\ResourceBase; + use Drupal\rest\ModifiedResourceResponse; ++use Drupal\webform_rest\Event\WebformSubmissionDataEvent; + use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + use Symfony\Component\DependencyInjection\ContainerInterface; + +@@ -35,6 +36,13 @@ class WebformSubmissionResource extends ResourceBase { + */ + protected $request; + ++ /** ++ * An event dispatcher instance to use for dispatching events. ++ * ++ * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface ++ */ ++ protected $eventDispatcher; ++ + /** + * {@inheritdoc} + */ +@@ -42,6 +50,7 @@ class WebformSubmissionResource extends ResourceBase { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->entityTypeManager = $container->get('entity_type.manager'); + $instance->request = $container->get('request_stack'); ++ $instance->eventDispatcher = $container->get('event_dispatcher'); + return $instance; + } + +@@ -91,9 +100,13 @@ class WebformSubmissionResource extends ResourceBase { + // Grab submission data. + $data = $webform_submission->getData(); + ++ // Dispatch WebformSubmissionDataEvent to allow modification of data. ++ $event = new WebformSubmissionDataEvent($webform_submission, $data); ++ $this->eventDispatcher->dispatch($event); ++ + $response = [ + 'entity' => $webform_submission, +- 'data' => $data, ++ 'data' => $event->getData(), + ]; + + // Return the submission. diff --git a/src/EventSubscriber/EventSubscriber.php b/src/EventSubscriber/WebformAccessEventSubscriber.php similarity index 96% rename from src/EventSubscriber/EventSubscriber.php rename to src/EventSubscriber/WebformAccessEventSubscriber.php index e67ecdf..6039d27 100644 --- a/src/EventSubscriber/EventSubscriber.php +++ b/src/EventSubscriber/WebformAccessEventSubscriber.php @@ -12,9 +12,9 @@ use Symfony\Component\HttpKernel\KernelEvents; /** - * Event subscriber. + * Webform access event subscriber. */ -class EventSubscriber implements EventSubscriberInterface { +class WebformAccessEventSubscriber implements EventSubscriberInterface { /** * The route match. * diff --git a/src/EventSubscriber/WebformSubmissionDataEventSubscriber.php b/src/EventSubscriber/WebformSubmissionDataEventSubscriber.php new file mode 100644 index 0000000..991c485 --- /dev/null +++ b/src/EventSubscriber/WebformSubmissionDataEventSubscriber.php @@ -0,0 +1,126 @@ + [ + 'webform_image_file', + 'webform_document_file', + 'webform_video_file', + 'webform_audio_file', + 'managed_file', + ], + ]; + + /** + * Constructor. + */ + public function __construct(EntityTypeManagerInterface $entityTypeManager, LoggerInterface $logger) { + $this->entityTypeManager = $entityTypeManager; + $this->setLogger($logger); + } + + /** + * Event handler. + */ + public function onWebformSubmissionDataEvent(WebformSubmissionDataEvent $event): void { + $linkedData = $this->buildLinked($event->getWebformSubmission()->getWebform(), $event->getData()); + + if (!empty($linkedData)) { + $event->setData($event->getData() + ['linked' => $linkedData]); + } + } + + /** + * Builds linked entity data. + * + * @see https://support.deskpro.com/en/guides/developers/deskpro-api/basics/sideloading + * + * @phpstan-param array $data + * @phpstan-return array + */ + private function buildLinked(WebformInterface $webform, array $data): array { + $linked = []; + $elements = $webform->getElementsDecodedAndFlattened(); + + foreach ($elements as $name => $element) { + if (!isset($data[$name])) { + continue; + } + + $linkedEntityType = NULL; + if (isset($element['#target_type'])) { + $linkedEntityType = $element['#target_type']; + } + else { + foreach (self::LINKED_ELEMENT_TYPES as $entityType => $elementTypes) { + if (in_array($element['#type'], $elementTypes, TRUE)) { + $linkedEntityType = $entityType; + break; + } + } + } + + if (NULL !== $linkedEntityType) { + // $data[$name] is either a string id i.e. '127', + // or an array of string ids i.e. ['127', '128']. + // Casting to array allow us to handle both cases the same way. + $values = (array) $data[$name]; + $entities = $this->entityTypeManager->getStorage($linkedEntityType)->loadMultiple($values); + + foreach ($entities as $value => $entity) { + $link = []; + if ($entity instanceof FileInterface) { + $link = [ + 'id' => $entity->id(), + 'url' => $entity->createFileUrl(FALSE), + 'mime_type' => $entity->getMimeType(), + 'size' => $entity->getSize(), + ]; + } + else { + $this->logger->warning(sprintf('Unhandled linked entity type %s', $linkedEntityType)); + } + if (!empty($link)) { + $linked[$name][$value] = $link; + } + } + } + } + return $linked; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + WebformSubmissionDataEvent::class => ['onWebformSubmissionDataEvent'], + ]; + } + +} diff --git a/src/WebformHelper.php b/src/WebformHelper.php index 740534f..fc91ba0 100644 --- a/src/WebformHelper.php +++ b/src/WebformHelper.php @@ -251,7 +251,9 @@ private function getAllowedUsers(WebformInterface $webform): array { * True if user has access to the webform. */ public function hasWebformAccess(WebformInterface $webform, $user): bool { - $userId = $user instanceof AccountInterface ? $user->id() : $user; + // AccountInterface::id() should return an `int` but actually returns a + // `string`. + $userId = (int) ($user instanceof AccountInterface ? $user->id() : $user); assert(is_int($userId)); $allowedUsers = $this->getAllowedUsers($webform); From 9138097522066a426a99a123fbcfe7ba5a636072 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 27 Feb 2023 20:38:07 +0100 Subject: [PATCH 2/2] Added attachments data --- README.md | 23 +++++++++++- .../WebformSubmissionDataEventSubscriber.php | 36 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d5ad8f..00e99d7 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,28 @@ To make using the REST API easier we add linked data to `GET` responses: } ``` -### Technical details on linked data +## Attachments + +Attachment elements are added to `GET` responses: + +```json +{ + … + "data": { + … + "attachments": { + "attachment_pdf": { + "name": "Attachment (pdf)", + "type": "pdf", + "url": "http://os2forms.example.com/da/webform/os2forms/submissions/42/attachment/pdf/pdf.pdf" + }, + … + } + } +} +``` + +### Technical details on linked data and attachments In order to add linked data, we apply a patch, [webform_rest_submission.patch](patches/webform_rest_submission.patch), to the diff --git a/src/EventSubscriber/WebformSubmissionDataEventSubscriber.php b/src/EventSubscriber/WebformSubmissionDataEventSubscriber.php index 991c485..227151d 100644 --- a/src/EventSubscriber/WebformSubmissionDataEventSubscriber.php +++ b/src/EventSubscriber/WebformSubmissionDataEventSubscriber.php @@ -5,6 +5,8 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\file\FileInterface; use Drupal\webform\WebformInterface; +use Drupal\webform\WebformSubmissionInterface; +use Drupal\webform_entity_print_attachment\Element\WebformEntityPrintAttachment; use Drupal\webform_rest\Event\WebformSubmissionDataEvent; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; @@ -53,6 +55,11 @@ public function onWebformSubmissionDataEvent(WebformSubmissionDataEvent $event): if (!empty($linkedData)) { $event->setData($event->getData() + ['linked' => $linkedData]); } + + $attachments = $this->buildAttachments($event->getWebformSubmission(), $event->getData()); + if (!empty($attachments)) { + $event->setData($event->getData() + ['attachments' => $attachments]); + } } /** @@ -114,6 +121,35 @@ private function buildLinked(WebformInterface $webform, array $data): array { return $linked; } + /** + * Builds attachment data. + * + * @phpstan-param array $data + * @phpstan-return array + */ + private function buildAttachments(WebformSubmissionInterface $submission, array $data): array { + $attachments = []; + + $webform = $submission->getWebform(); + $attachmentElements = $webform->getElementsAttachments(); + + foreach ($attachmentElements as $key => $name) { + $element = $webform->getElement($key); + + if (preg_match('/^webform_entity_print_attachment:(?.+)/', $element['#type'] ?? '', $matches)) { + $type = $matches['type']; + $url = WebformEntityPrintAttachment::getFileUrl($element, $submission); + $attachments[$key] = [ + 'name' => $element['#title'] ?? $name, + 'type' => $type, + 'url' => $url->toString(TRUE)->getGeneratedUrl(), + ]; + } + } + + return $attachments; + } + /** * {@inheritdoc} */