From 0943c5cfb477ca2c41d53cc02467ab65bb4f17af Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 19 Aug 2025 15:26:52 +0200 Subject: [PATCH 01/12] 5189: Sanitized MeMo filename --- composer.json | 6 +++++- modules/os2forms_digital_post/src/Helper/MeMoHelper.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5f20db3..6232068 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "fig/http-message-util": "^1.1", "http-interop/http-factory-guzzle": "^1.0.0", "itk-dev/beskedfordeler-drupal": "^1.0", - "itk-dev/serviceplatformen": "^1.5", + "itk-dev/serviceplatformen": "dev-feature/5189-memo-1.2 as 1.7.0", "mglaman/composer-drupal-lenient": "^1.0", "os2web/os2web_audit": "^1.0", "os2web/os2web_datalookup": "^2.0", @@ -79,6 +79,10 @@ "wsdltophp/packagegenerator": "^4.0" }, "repositories": { + "itk-dev/serviceplatformen": { + "type": "vcs", + "url": "https://github.com/itk-dev/serviceplatformen" + }, "drupal": { "type": "composer", "url": "https://packages.drupal.org/8" diff --git a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php index 3ec8b7e..23e480b 100644 --- a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php +++ b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php @@ -74,7 +74,7 @@ public function buildMessage(CprLookupResult|CompanyLookupResult $recipientData, (new File()) ->setEncodingFormat($document->mimeType) ->setLanguage($document->language) - ->setFilename($document->filename) + ->setFilename(SF1601::sanitizeFilename($document->filename)) ->setContent($document->content), ]); From 3191624c019bf48121834dc741f3698ca989bb30 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 19 Aug 2025 15:27:29 +0200 Subject: [PATCH 02/12] 5189: Added check for absolute and secure URLs in MeMo actions --- .../WebformHandler/WebformHandlerSF1601.php | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php index ccb61a0..5f0e1b0 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -2,6 +2,7 @@ namespace Drupal\os2forms_digital_post\Plugin\WebformHandler; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Form\FormStateInterface; use Drupal\os2forms_digital_post\Helper\WebformHelperSF1601; use Drupal\webform\Plugin\WebformHandlerBase; @@ -270,10 +271,31 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form self::MEMO_ACTIONS . '][actions][' . $index . '][url', $this->t('Url for action %action is required.', [ '%action' => $this->getTranslatedActionName($action['action']), - '%url' => $action['url'] ?? '', ]) ); } + else { + $url = $action['url']; + // URL must be absolute and use https (cf. https://digitaliser.dk/digital-post/nyhedsarkiv/2024/nov/oeget-validering-i-digital-post) + if (!UrlHelper::isValid($url, absolute: TRUE)) { + $formState->setErrorByName( + self::MEMO_ACTIONS . '][actions][' . $index . '][url', + $this->t('Url @url for action %action must be absolute, i.e. start with https://.', [ + '@url' => $url, + '%action' => $this->getTranslatedActionName($action['action']), + ]) + ); + } + elseif ('https' !== parse_url($url, PHP_URL_SCHEME)) { + $formState->setErrorByName( + self::MEMO_ACTIONS . '][actions][' . $index . '][url', + $this->t('Url @url for action %action must use the https scheme, i.e. start with https://.', [ + '@url' => $url, + '%action' => $this->getTranslatedActionName($action['action']), + ]) + ); + } + } if (isset($definedActions[$action['action']])) { $formState->setErrorByName( self::MEMO_ACTIONS . '][actions][' . $index . '][action', From fdb5ee6101c0f44929737663cbc9e4515453e116 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 19 Aug 2025 15:27:51 +0200 Subject: [PATCH 03/12] 5189: Improved digital post test command --- .../Commands/DigitalPostTestCommands.php | 149 ++++++++++++++++-- 1 file changed, 137 insertions(+), 12 deletions(-) diff --git a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php index 201581c..ec2c0bc 100644 --- a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php +++ b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php @@ -2,6 +2,9 @@ namespace Drupal\os2forms_digital_post\Drush\Commands; +use DigitalPost\MeMo\Action; +use DigitalPost\MeMo\EntryPoint; +use DigitalPost\MeMo\Reservation; use Drupal\Component\Serialization\Yaml; use Drupal\Core\DependencyInjection\AutowireTrait; use Drupal\Core\Utility\Token; @@ -10,10 +13,16 @@ use Drupal\os2forms_digital_post\Helper\Settings; use Drupal\os2forms_digital_post\Model\Document; use Drush\Commands\DrushCommands; +use ItkDev\Serviceplatformen\Service\SF1601\Serializer; use ItkDev\Serviceplatformen\Service\SF1601\SF1601; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; /** @@ -44,14 +53,20 @@ public function __construct( * @param array $options * The options. * - * @option string subject - * The subject. Can contain HTML. - * @option string message - * The message to send. Can contain HTML. - * @option string digital-post-type - * The digital post type to use. - * @option bool dump-digital-post-settings - * Dump digital post settings. + * @option subject + * The subject. Can contain HTML. + * @option message + * The message to send. Can contain HTML. + * @option digital-post-type + * The digital post type to use. + * @option dump-digital-post-settings + * Dump digital post settings. + * @option memo-version + * MeMo version (1.1 or 1.2). If not set, a proper default will be used. + * @option action + * MeMo actions, e.g. 'action=INFORMATION&label=Vigtig%20information&entrypoint=https://example.com' + * @option filename + * The main document filename (used to test invalid filenames (cf. https://digitaliser.dk/digital-post/nyhedsarkiv/2024/nov/oeget-validering-i-digital-post)) * * @phpstan-param array $recipients * @phpstan-param array $options @@ -66,6 +81,9 @@ public function send( 'message' => 'This is a test message from os2forms_digital_post sent on [current-date:html_datetime].', 'digital-post-type' => SF1601::TYPE_AUTOMATISK_VALG, 'dump-digital-post-settings' => FALSE, + 'memo-version' => NULL, + 'action' => [], + 'filename' => 'os2forms_digital_post', ], ): void { $io = new SymfonyStyle($this->input(), $this->output()); @@ -89,7 +107,7 @@ public function send( $document = new Document( $content, Document::MIME_TYPE_PDF, - 'os2forms_digital_post.pdf' + $options['filename'] . '.pdf', ); $type = $options['digital-post-type']; @@ -98,21 +116,41 @@ public function send( throw new InvalidArgumentException(sprintf('Invalid type: %s. Must be one of %s.', $quote($type), implode(', ', array_map($quote, SF1601::TYPES)))); } + $meMoVersion = $options['memo-version']; + if ($meMoVersion) { + $meMoVersion = (float) $meMoVersion; + $allowedValues = [SF1601::MEMO_1_1, SF1601::MEMO_1_2]; + if (!in_array($meMoVersion, $allowedValues, TRUE)) { + $quote = static fn($value) => var_export($value, TRUE); + throw new InvalidArgumentException(sprintf( + 'Invalid MeMo version: %s. Must be one of %s.', + $quote($meMoVersion), + implode(', ', array_map($quote, $allowedValues)) + )); + } + } + $io->section('Digital post'); $io->definitionList( ['Type' => $type], + ['Document' => sprintf('%s (%s) (sanitized: %s)', $document->filename, $document->mimeType, SF1601::sanitizeFilename($document->filename))], ['Subject' => $subject], - ['Message' => $message] + ['Message' => $message], + ['MeMe version' => $meMoVersion], ); + $actions = array_map($this->buildAction(...), $options['action']); + foreach ($recipients as $recipient) { try { $io->writeln(sprintf('Recipient: %s', $recipient)); $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipient); - $actions = []; $meMoMessage = $this->digitalPostHelper->getMeMoHelper()->buildMessage($recipientLookupResult, $senderLabel, $messageLabel, $document, $actions); + if ($meMoVersion) { + $meMoMessage->setMemoVersion($meMoVersion); + } $forsendelse = $this->digitalPostHelper->getForsendelseHelper()->buildForsendelse($recipientLookupResult, $messageLabel, $document); @@ -122,7 +160,7 @@ public function send( $forsendelse ); - $io->success(sprintf('Digital post sent to %s', $recipient)); + $io->success(sprintf('Digital post sent to %s (MeMo %s)', $recipient, $meMoMessage->getMemoVersion())); } catch (\Throwable $throwable) { $io->error(sprintf('Error sending digital post to %s:', $recipient)); @@ -158,4 +196,91 @@ private function dumpDigitalPostSettings(SymfonyStyle $io): void { ]); } + /** + * Build MeMo action. + * + * Lifted from KombiPostAfsendCommand::buildAction(). + * + * @see KombiPostAfsendCommand::buildAction() + */ + private function buildAction(string $spec): Action { + parse_str($spec, $options); + $resolver = $this->getActionOptionsResolver(); + try { + $options = $resolver->resolve($options); + } + catch (ExceptionInterface $exception) { + throw new InvalidOptionException(sprintf( + 'Invalid action %s: %s', + json_encode($spec), + $exception->getMessage() + )); + } + + $action = (new Action()) + ->setActionCode($options['action']) + ->setLabel($options['label']); + if (SF1601::ACTION_AFTALE === $options['action']) { + $reservation = (new Reservation()) + ->setStartDateTime(new \DateTime('+2 days')) + ->setEndDateTime(new \DateTime('+2 days 1 hour')) + ->setLocation('Meeting room 1') + ->setAbstract('Abstract') + ->setDescription('Description') + ->setOrganizerName('Organizer') + ->setOrganizerMail('organizer@example.com') + ->setReservationUUID(Serializer::createUuid()); + $action->setReservation($reservation); + } + elseif ($options['entrypoint']) { + $action->setEntryPoint( + (new EntryPoint()) + ->setUrl($options['entrypoint']) + ); + } + + if ($options['endDateTime']) { + $action->setEndDateTime(new \DateTime($options['endDateTime'])); + } + + return $action; + } + + /** + * Get actions options resolver. + * + * @see KombiPostAfsendCommand::getActionOptionsResolver() + */ + private function getActionOptionsResolver(): OptionsResolver { + $resolver = new OptionsResolver(); + $resolver + ->setRequired([ + 'action', + 'label', + ]) + ->setDefaults([ + 'endDateTime' => NULL, + 'entrypoint' => NULL, + ]) + ->setInfo('action', sprintf('The action name (one of %s)', implode(', ', SF1601::ACTIONS))) + ->setInfo('label', 'The action label') + ->setInfo('endDateTime', 'The end time e.g. "2022-12-02" or "14 days"') + ->setInfo('entrypoint', 'The entry point (an URL)') + ->setAllowedValues('action', static function ($value) { + return in_array($value, SF1601::ACTIONS, TRUE); + }) + ->setNormalizer('entrypoint', static function (Options $options, $value) { + if (NULL === $value && SF1601::ACTION_AFTALE !== $options['action']) { + throw new InvalidOptionsException(sprintf( + 'Action entrypoint is required for all actions but %s', + SF1601::ACTION_AFTALE + )); + } + + return $value; + }); + + return $resolver; + } + } From c7762ba5b747267e4b0ff2ad923004017ade42dd Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 19 Aug 2025 22:40:15 +0200 Subject: [PATCH 04/12] 5189: Refactored action URL validation. Added runtime check. --- .../src/Helper/MeMoHelper.php | 68 ++++++++++++++++ .../WebformHandler/WebformHandlerSF1601.php | 77 +++++-------------- 2 files changed, 86 insertions(+), 59 deletions(-) diff --git a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php index 23e480b..0773053 100644 --- a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php +++ b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php @@ -14,6 +14,8 @@ use DigitalPost\MeMo\MessageHeader; use DigitalPost\MeMo\Recipient; use DigitalPost\MeMo\Sender; +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\os2forms_digital_post\Model\Document; use Drupal\os2forms_digital_post\Plugin\WebformHandler\WebformHandlerSF1601; use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult; @@ -175,6 +177,9 @@ private function buildAction(array $options, WebformSubmissionInterface $submiss } elseif ($options['url']) { $url = $this->replaceTokens($options['url'], $submission); + if ($message = self::validateActionUrl($url, $options)) { + throw new \RuntimeException((string) $message); + } $action->setEntryPoint( (new EntryPoint()) ->setUrl($url) @@ -184,4 +189,67 @@ private function buildAction(array $options, WebformSubmissionInterface $submiss return $action; } + /** + * Validate an action URL. + * + * @param string $url + * The URL. + * @param array $options + * The options. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup|null + * A message if the URL is not valid for an action. + */ + public static function validateActionUrl(string $url, array $options): ?TranslatableMarkup { + // URL must be absolute and use https (cf. https://digitaliser.dk/digital-post/nyhedsarkiv/2024/nov/oeget-validering-i-digital-post) + if (!UrlHelper::isValid($url, absolute: TRUE)) { + return new TranslatableMarkup('URL @url for action %action must be absolute, i.e. start with https://.', [ + '@url' => $url, + '%action' => self::getTranslatedActionName($options['action']), + ]); + } + elseif ('https' !== parse_url($url, PHP_URL_SCHEME)) { + return new TranslatableMarkup('URL @url for action %action must use the https scheme, i.e. start with https://.', [ + '@url' => $url, + '%action' => self::getTranslatedActionName($options['action']), + ]); + } + } + + /** + * Translated action names. + * + * @var array|null + * + * @phpstan-var array + */ + private static ?array $translatedActionNames = NULL; + + /** + * Get translated action names. + */ + public static function getTranslatedActionNames(): array { + if (NULL === self::$translatedActionNames) { + self::$translatedActionNames = [ + SF1601::ACTION_AFTALE => (string) new TranslatableMarkup('Aftale', [], ['context' => 'memo action']), + SF1601::ACTION_BEKRAEFT => (string) new TranslatableMarkup('Bekræft', [], ['context' => 'memo action']), + SF1601::ACTION_BETALING => (string) new TranslatableMarkup('Betaling', [], ['context' => 'memo action']), + SF1601::ACTION_FORBEREDELSE => (string) new TranslatableMarkup('Forberedelse', [], ['context' => 'memo action']), + SF1601::ACTION_INFORMATION => (string) new TranslatableMarkup('Information', [], ['context' => 'memo action']), + SF1601::ACTION_SELVBETJENING => (string) new TranslatableMarkup('Selvbetjening', [], ['context' => 'memo action']), + SF1601::ACTION_TILMELDING => (string) new TranslatableMarkup('Tilmelding', [], ['context' => 'memo action']), + SF1601::ACTION_UNDERSKRIV => (string) new TranslatableMarkup('Underskriv', [], ['context' => 'memo action']), + ]; + } + + return self::$translatedActionNames; + } + + /** + * Get translated action name. + */ + public static function getTranslatedActionName(string $action): string { + return self::$translatedActionNames[$action] ?? $action; + } + } diff --git a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php index 5f0e1b0..b0f78b0 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -2,8 +2,8 @@ namespace Drupal\os2forms_digital_post\Plugin\WebformHandler; -use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Form\FormStateInterface; +use Drupal\os2forms_digital_post\Helper\MeMoHelper; use Drupal\os2forms_digital_post\Helper\WebformHelperSF1601; use Drupal\webform\Plugin\WebformHandlerBase; use Drupal\webform\WebformSubmissionInterface; @@ -137,23 +137,18 @@ public function buildConfigurationForm(array $form, FormStateInterface $formStat '#description' => $this->t('Remove an action by clearing %action and saving.', ['%action' => (string) $this->t('Action')]), ]; + $form[self::MEMO_ACTIONS]['message'] = [ + '#markup' => $this->t('Important: All action URLs must be absolute and secure, i.e. start with https://.'), + ]; + $form[self::MEMO_ACTIONS]['actions'] = [ '#type' => 'table', ]; - $actionOptions = [ - // @todo Handle SF1601::ACTION_AFTALE. - SF1601::ACTION_BEKRAEFT => $this->getTranslatedActionName(SF1601::ACTION_BEKRAEFT), - SF1601::ACTION_BETALING => $this->getTranslatedActionName(SF1601::ACTION_BETALING), - SF1601::ACTION_FORBEREDELSE => $this->getTranslatedActionName(SF1601::ACTION_FORBEREDELSE), - SF1601::ACTION_INFORMATION => $this->getTranslatedActionName(SF1601::ACTION_INFORMATION), - SF1601::ACTION_SELVBETJENING => $this->getTranslatedActionName(SF1601::ACTION_SELVBETJENING), - SF1601::ACTION_TILMELDING => $this->getTranslatedActionName(SF1601::ACTION_TILMELDING), - SF1601::ACTION_UNDERSKRIV => $this->getTranslatedActionName(SF1601::ACTION_UNDERSKRIV), - ]; + $actionOptions = MeMoHelper::getTranslatedActionNames(); $actions = $this->configuration[self::MEMO_ACTIONS]['actions'] ?? []; for ($i = 0; $i <= count($actions); $i++) { - $action = $actions[$i]; + $action = $actions[$i] ?? []; $form[self::MEMO_ACTIONS]['actions'][$i]['action'] = [ '#type' => 'select', '#title' => $this->t('Action'), @@ -276,31 +271,24 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form } else { $url = $action['url']; - // URL must be absolute and use https (cf. https://digitaliser.dk/digital-post/nyhedsarkiv/2024/nov/oeget-validering-i-digital-post) - if (!UrlHelper::isValid($url, absolute: TRUE)) { - $formState->setErrorByName( - self::MEMO_ACTIONS . '][actions][' . $index . '][url', - $this->t('Url @url for action %action must be absolute, i.e. start with https://.', [ - '@url' => $url, - '%action' => $this->getTranslatedActionName($action['action']), - ]) - ); + // Add warning if URL contains tokens. + if (preg_match('/\[[a-z_:]+/', $url)) { + $message = $this->t('Make sure that the tokens in the URL @url for action %action expands to an absolute URL, i.e. something starting with https://.', [ + '@url' => $url, + '%action' => MeMoHelper::getTranslatedActionName($action['action']), + ]); + $this->messenger()->addWarning($message); + continue; } - elseif ('https' !== parse_url($url, PHP_URL_SCHEME)) { - $formState->setErrorByName( - self::MEMO_ACTIONS . '][actions][' . $index . '][url', - $this->t('Url @url for action %action must use the https scheme, i.e. start with https://.', [ - '@url' => $url, - '%action' => $this->getTranslatedActionName($action['action']), - ]) - ); + if ($message = MeMoHelper::validateActionUrl($url, $action)) { + $formState->setErrorByName(self::MEMO_ACTIONS . '][actions][' . $index . '][url', $message); } } if (isset($definedActions[$action['action']])) { $formState->setErrorByName( self::MEMO_ACTIONS . '][actions][' . $index . '][action', $this->t('Action %action already defined.', [ - '%action' => $this->getTranslatedActionName($action['action']), + '%action' => MeMoHelper::getTranslatedActionName($action['action']), ]) ); } @@ -359,33 +347,4 @@ public function postPurge(array $webformSubmissions) { $this->helper->deleteMessages($webformSubmissions); } - /** - * Translated action names. - * - * @var array|null - * - * @phpstan-var array - */ - private ?array $translatedActionNames = NULL; - - /** - * Get translated action name. - */ - private function getTranslatedActionName(string $action): string { - if (NULL === $this->translatedActionNames) { - $this->translatedActionNames = [ - SF1601::ACTION_AFTALE => (string) $this->t('Aftale', [], ['context' => 'memo action']), - SF1601::ACTION_BEKRAEFT => (string) $this->t('Bekræft', [], ['context' => 'memo action']), - SF1601::ACTION_BETALING => (string) $this->t('Betaling', [], ['context' => 'memo action']), - SF1601::ACTION_FORBEREDELSE => (string) $this->t('Forberedelse', [], ['context' => 'memo action']), - SF1601::ACTION_INFORMATION => (string) $this->t('Information', [], ['context' => 'memo action']), - SF1601::ACTION_SELVBETJENING => (string) $this->t('Selvbetjening', [], ['context' => 'memo action']), - SF1601::ACTION_TILMELDING => (string) $this->t('Tilmelding', [], ['context' => 'memo action']), - SF1601::ACTION_UNDERSKRIV => (string) $this->t('Underskriv', [], ['context' => 'memo action']), - ]; - } - - return $this->translatedActionNames[$action] ?? $action; - } - } From 7533b146e5a8ac4ba8fc2c854d698229917e5f4a Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 20 Aug 2025 10:21:11 +0200 Subject: [PATCH 05/12] 5189: Cleaned up --- CHANGELOG.md | 3 +++ modules/os2forms_digital_post/src/Helper/MeMoHelper.php | 7 +++++++ .../src/Plugin/WebformHandler/WebformHandlerSF1601.php | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbaf66e..e7d7f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-189](https://github.com/OS2Forms/os2forms/pull/189) + - Added support for MeMo 1.2 and added additional validation of MeMo actions. + ## [4.1.0] 2025-06-03 - [PR-176](https://github.com/OS2Forms/os2forms/pull/176) diff --git a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php index 0773053..dc3c680 100644 --- a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php +++ b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php @@ -199,6 +199,8 @@ private function buildAction(array $options, WebformSubmissionInterface $submiss * * @return \Drupal\Core\StringTranslation\TranslatableMarkup|null * A message if the URL is not valid for an action. + * + * @phpstan-param array $options */ public static function validateActionUrl(string $url, array $options): ?TranslatableMarkup { // URL must be absolute and use https (cf. https://digitaliser.dk/digital-post/nyhedsarkiv/2024/nov/oeget-validering-i-digital-post) @@ -214,6 +216,8 @@ public static function validateActionUrl(string $url, array $options): ?Translat '%action' => self::getTranslatedActionName($options['action']), ]); } + + return NULL; } /** @@ -227,6 +231,9 @@ public static function validateActionUrl(string $url, array $options): ?Translat /** * Get translated action names. + * + * @return array + * The translated action names. */ public static function getTranslatedActionNames(): array { if (NULL === self::$translatedActionNames) { diff --git a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php index b0f78b0..0b1dc21 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -265,7 +265,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form $formState->setErrorByName( self::MEMO_ACTIONS . '][actions][' . $index . '][url', $this->t('Url for action %action is required.', [ - '%action' => $this->getTranslatedActionName($action['action']), + '%action' => MeMoHelper::getTranslatedActionName($action['action']), ]) ); } From 336fe2eaf19c55790ef458d802e3c1f3430751b7 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Wed, 20 Aug 2025 12:38:35 +0200 Subject: [PATCH 06/12] #190: Rethrow exception to ensure failed job status in Maestro notification --- modules/os2forms_forloeb/src/MaestroHelper.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/os2forms_forloeb/src/MaestroHelper.php b/modules/os2forms_forloeb/src/MaestroHelper.php index d2dd0c5..eaa6a5b 100644 --- a/modules/os2forms_forloeb/src/MaestroHelper.php +++ b/modules/os2forms_forloeb/src/MaestroHelper.php @@ -205,7 +205,14 @@ public function processJob(Job $job): JobResult { $submission = $this->webformSubmissionStorage->load($submissionID); - $this->sendNotification($notificationType, $submission, $templateTask, $maestroQueueID); + try { + $this->sendNotification($notificationType, $submission, $templateTask, $maestroQueueID); + } + catch (\Exception $e) { + // Logging is done by the sendNotification method. + // The job should be considered failed. + return JobResult::failure($e->getMessage()); + } return JobResult::success(); } @@ -261,12 +268,15 @@ private function sendNotification( } } catch (\Exception $exception) { + // Log with context and rethrow exception. $this->error('Error sending notification: @message', $context + [ '@message' => $exception->getMessage(), 'handler_id' => 'os2forms_forloeb', 'operation' => 'notification failed', 'exception' => $exception, ]); + + throw $exception; } } From 7480dbc439a9bb982f1e294149f1e567a669e9a8 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Wed, 20 Aug 2025 12:43:22 +0200 Subject: [PATCH 07/12] #190: Updated CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbaf66e..3dbddf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-191](https://github.com/OS2Forms/os2forms/pull/191) + Re-throws exception to ensure failed status during Maestro notification job. + ## [4.1.0] 2025-06-03 - [PR-176](https://github.com/OS2Forms/os2forms/pull/176) From 7bd7041ad5af3a312d860aa763e5503d1f185ec3 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Wed, 20 Aug 2025 15:32:02 +0200 Subject: [PATCH 08/12] 5189: Updated itk-dev/serviceplatformen --- composer.json | 6 +----- .../src/Drush/Commands/DigitalPostTestCommands.php | 11 ++++++++--- .../os2forms_digital_post/src/Helper/MeMoHelper.php | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 6232068..8a95071 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "fig/http-message-util": "^1.1", "http-interop/http-factory-guzzle": "^1.0.0", "itk-dev/beskedfordeler-drupal": "^1.0", - "itk-dev/serviceplatformen": "dev-feature/5189-memo-1.2 as 1.7.0", + "itk-dev/serviceplatformen": "^1.7.1", "mglaman/composer-drupal-lenient": "^1.0", "os2web/os2web_audit": "^1.0", "os2web/os2web_datalookup": "^2.0", @@ -79,10 +79,6 @@ "wsdltophp/packagegenerator": "^4.0" }, "repositories": { - "itk-dev/serviceplatformen": { - "type": "vcs", - "url": "https://github.com/itk-dev/serviceplatformen" - }, "drupal": { "type": "composer", "url": "https://packages.drupal.org/8" diff --git a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php index ec2c0bc..f68af76 100644 --- a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php +++ b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php @@ -133,17 +133,16 @@ public function send( $io->section('Digital post'); $io->definitionList( ['Type' => $type], - ['Document' => sprintf('%s (%s) (sanitized: %s)', $document->filename, $document->mimeType, SF1601::sanitizeFilename($document->filename))], ['Subject' => $subject], ['Message' => $message], - ['MeMe version' => $meMoVersion], + ['Document' => sprintf('%s (%s)', $document->filename, $document->mimeType)], + ['MeMo version' => $meMoVersion ?? '–'], ); $actions = array_map($this->buildAction(...), $options['action']); foreach ($recipients as $recipient) { try { - $io->writeln(sprintf('Recipient: %s', $recipient)); $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipient); $meMoMessage = $this->digitalPostHelper->getMeMoHelper()->buildMessage($recipientLookupResult, $senderLabel, @@ -160,6 +159,12 @@ public function send( $forsendelse ); + $io->definitionList( + ['Recipient' => $recipient], + ['Document' => sprintf('%s (%s)', $document->filename, $document->mimeType)], + ['MeMo version' => $meMoMessage->getMemoVersion()], + ); + $io->success(sprintf('Digital post sent to %s (MeMo %s)', $recipient, $meMoMessage->getMemoVersion())); } catch (\Throwable $throwable) { diff --git a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php index dc3c680..f32c567 100644 --- a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php +++ b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php @@ -76,7 +76,7 @@ public function buildMessage(CprLookupResult|CompanyLookupResult $recipientData, (new File()) ->setEncodingFormat($document->mimeType) ->setLanguage($document->language) - ->setFilename(SF1601::sanitizeFilename($document->filename)) + ->setFilename($document->filename) ->setContent($document->content), ]); From a802c2f1e7e4d212cbe554c138c8e775fcef0bbe Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 22 Aug 2025 11:21:14 +0200 Subject: [PATCH 09/12] 5189: Updated example forms --- ....webform.os2forms_digital_post_example.yml | 10 +- ...ebform.os2forms_digital_post_example_2.yml | 245 ++++++++++++++++++ 2 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example_2.yml diff --git a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml index 6a18f4d..a942a2d 100644 --- a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml +++ b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml @@ -240,14 +240,10 @@ handlers: actions: - action: INFORMATION - url: 'http://dr.dk' + url: '[site:url]' label: 'Se her!' - action: SELVBETJENING - url: 'https://selvbetjening.aarhuskommune.dk/da/content/book-aarhus' - label: 'Book ressource' - - - action: FORBEREDELSE - url: 'http://tv2.dk' - label: 'Forbered dig med' + url: 'https://eksempel.dk' + label: 'Eksempel' variants: { } diff --git a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example_2.yml b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example_2.yml new file mode 100644 index 0000000..3994ece --- /dev/null +++ b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example_2.yml @@ -0,0 +1,245 @@ +langcode: da +status: open +dependencies: + enforced: + module: + - os2forms_digital_post_examples + module: + - os2forms_digital_post_examples +third_party_settings: + webform_revisions: + contentEntity_id: null +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_digital_post_example_2 +title: 'OS2Forms Digital post example with invalid handler URL' +description: 'Simple example form with a digital post handler with invalid handler URL' +category: Example +elements: |- + message: + '#type': textarea + '#title': Message + '#required': true + '#default_value': |- + [current-date:long] + + [random:hash:sha512] + recipient_cpr: + '#type': textfield + '#title': 'Recipient cpr' + '#required': true + '#default_value': '1705880000' + digital_post_content_pdf: + '#type': 'webform_entity_print_attachment:pdf' + '#title': 'Digital post (PDF)' + '#display_on': view + '#filename': hat-og-briller.pdf +css: '' +javascript: '' +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: '' + ajax_effect: '' + ajax_speed: null + page: true + page_submit_path: '' + page_confirm_path: '' + page_theme_name: '' + form_title: both + form_submit_once: false + form_open_message: '' + form_close_message: '' + form_exception_message: '' + form_previous_submissions: true + form_confidential: false + form_confidential_message: '' + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: '' + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: '' + form_access_denied_message: '' + form_access_denied_attributes: { } + form_file_limit: '' + form_attributes: { } + form_method: '' + form_action: '' + share: false + share_node: false + share_theme_name: '' + share_title: true + share_page_body_attributes: { } + submission_label: '' + submission_exception_message: '' + submission_locked_message: '' + submission_log: false + submission_excluded_elements: { } + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: { } + submission_views_replace: { } + submission_user_columns: { } + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: '' + submission_access_denied_message: '' + submission_access_denied_attributes: { } + previous_submission_message: '' + previous_submissions_message: '' + autofill: false + autofill_message: '' + autofill_excluded_elements: { } + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: '' + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: '' + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: '' + wizard_prev_button_label: '' + wizard_next_button_label: '' + wizard_toggle: false + wizard_toggle_show_label: '' + wizard_toggle_hide_label: '' + preview: 0 + preview_label: '' + preview_title: '' + preview_message: '' + preview_attributes: { } + preview_excluded_elements: { } + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: '' + draft_loaded_message: '' + draft_pending_single_message: '' + draft_pending_multiple_message: '' + confirmation_type: message + confirmation_url: '' + confirmation_title: '' + confirmation_message: '' + confirmation_attributes: { } + confirmation_back: true + confirmation_back_label: '' + confirmation_back_attributes: { } + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: '' + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: '' + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: { } + permissions: { } + view_any: + roles: { } + users: { } + permissions: { } + update_any: + roles: { } + users: { } + permissions: { } + delete_any: + roles: { } + users: { } + permissions: { } + purge_any: + roles: { } + users: { } + permissions: { } + view_own: + roles: { } + users: { } + permissions: { } + update_own: + roles: { } + users: { } + permissions: { } + delete_own: + roles: { } + users: { } + permissions: { } + administer: + roles: { } + users: { } + permissions: { } + test: + roles: { } + users: { } + permissions: { } + configuration: + roles: { } + users: { } + permissions: { } +handlers: + digital_post_sf1601: + id: digital_post_sf1601 + handler_id: digital_post_sf1601 + label: 'Digital post (sf1601)' + notes: '' + status: true + conditions: { } + weight: 0 + settings: + debug: false + memo_message: + type: 'Automatisk Valg' + recipient_element: recipient_cpr + attachment_element: digital_post_content_pdf + sender_label: 'Hilsen fra [site:url-brief]' + message_header_label: SF1601 + memo_actions: + actions: + - + action: INFORMATION + url: 'http://eksempel.dk' + label: 'Se her!' +variants: { } From 62652e12eb0f9f614c18a9d80e05f671e2108183 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 29 Aug 2025 11:01:32 +0200 Subject: [PATCH 10/12] 5189: Added clarifying comment --- .../src/Drush/Commands/DigitalPostTestCommands.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php index f68af76..b0efb2b 100644 --- a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php +++ b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php @@ -147,6 +147,8 @@ public function send( $meMoMessage = $this->digitalPostHelper->getMeMoHelper()->buildMessage($recipientLookupResult, $senderLabel, $messageLabel, $document, $actions); + // If a valid memo-version option has been provided, set that version on + // the message. if ($meMoVersion) { $meMoMessage->setMemoVersion($meMoVersion); } From fb247a0cf84411595bd8a7b3fdb486501ca5a615 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Fri, 29 Aug 2025 14:53:26 +0200 Subject: [PATCH 11/12] #195: Sanitize recipient identifier in Maestro notification --- modules/os2forms_forloeb/src/MaestroHelper.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/os2forms_forloeb/src/MaestroHelper.php b/modules/os2forms_forloeb/src/MaestroHelper.php index d2dd0c5..80f3748 100644 --- a/modules/os2forms_forloeb/src/MaestroHelper.php +++ b/modules/os2forms_forloeb/src/MaestroHelper.php @@ -389,7 +389,10 @@ private function sendNotificationDigitalPost( $senderLabel = $subject; $messageLabel = $subject; - $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipient); + // Remove all non-digits from recipient identifier. + $recipientIdentifier = preg_replace('/[^\d]+/', '', $recipient); + + $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipientIdentifier); $actions = [ (new Action()) ->setActionCode(SF1601::ACTION_SELVBETJENING) From 6e83da36ccef46813abb774dcbd2d19e15cc252c Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Fri, 29 Aug 2025 14:58:52 +0200 Subject: [PATCH 12/12] #195: Updated CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbaf66e..150a4d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-202](https://github.com/OS2Forms/os2forms/pull/202) + - Removed non-digits from recipient id in Maestro digital post notifications. + ## [4.1.0] 2025-06-03 - [PR-176](https://github.com/OS2Forms/os2forms/pull/176)