Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "^1.7.1",
"mglaman/composer-drupal-lenient": "^1.0",
"os2web/os2web_audit": "^1.0",
"os2web/os2web_datalookup": "^2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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<string> $recipients
* @phpstan-param array<string, mixed> $options
Expand All @@ -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());
Expand All @@ -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'];
Expand All @@ -98,21 +116,40 @@ 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'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we leave a comment explaining that if no memo-version is provided, 1.2 is the default?

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],
['Subject' => $subject],
['Message' => $message]
['Message' => $message],
['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);
$actions = [];

$meMoMessage = $this->digitalPostHelper->getMeMoHelper()->buildMessage($recipientLookupResult, $senderLabel,
$messageLabel, $document, $actions);
if ($meMoVersion) {
$meMoMessage->setMemoVersion($meMoVersion);
}
$forsendelse = $this->digitalPostHelper->getForsendelseHelper()->buildForsendelse($recipientLookupResult,
$messageLabel, $document);

Expand All @@ -122,7 +159,13 @@ public function send(
$forsendelse
);

$io->success(sprintf('Digital post sent to %s', $recipient));
$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) {
$io->error(sprintf('Error sending digital post to %s:', $recipient));
Expand Down Expand Up @@ -158,4 +201,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('[email protected]')
->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;
}

}
75 changes: 75 additions & 0 deletions modules/os2forms_digital_post/src/Helper/MeMoHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -184,4 +189,74 @@ 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.
*
* @phpstan-param array<string, mixed> $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)
if (!UrlHelper::isValid($url, absolute: TRUE)) {
return new TranslatableMarkup('URL <code>@url</code> for action %action must be absolute, i.e. start with <code>https://</code>.', [
'@url' => $url,
'%action' => self::getTranslatedActionName($options['action']),
]);
}
elseif ('https' !== parse_url($url, PHP_URL_SCHEME)) {
return new TranslatableMarkup('URL <code>@url</code> for action %action must use the <code>https</code> scheme, i.e. start with <code>https://</code>.', [
'@url' => $url,
'%action' => self::getTranslatedActionName($options['action']),
]);
}

return NULL;
}

/**
* Translated action names.
*
* @var array|null
*
* @phpstan-var array<string, string>
*/
private static ?array $translatedActionNames = NULL;

/**
* Get translated action names.
*
* @return array<string, string>
* The 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;
}

}
Loading