From 5efb744a21cdb75569a18771d81f1bbfd1e76771 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 3 Feb 2026 10:34:38 +0100 Subject: [PATCH 01/52] Added required "Zoom control position" to map element --- .../src/Element/WebformLeafletMapField.php | 1 + .../WebformElement/WebformLeafletMapField.php | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php b/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php index 87b079d0..84f7506c 100644 --- a/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php +++ b/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php @@ -90,6 +90,7 @@ public static function processWebformMapElement(&$element, FormStateInterface $f 'zoomFiner' => $element['#zoomFiner'], 'minZoom' => $element['#minZoom'], 'maxZoom' => $element['#maxZoom'], + 'zoomControlPosition' => $element['#zoomControlPosition'] ?? 'topleft', 'center' => [ 'lat' => (float) $element['#lat'], 'lon' => (float) $element['#lon'], diff --git a/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php b/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php index d6a5afea..31374ba7 100644 --- a/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php +++ b/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php @@ -20,6 +20,13 @@ class WebformLeafletMapField extends WebformElementBase { use LeafletSettingsElementsTrait; + // Valid Leaflet control positions (cf. + // https://github.com/Leaflet/Leaflet/blob/main/src/control/Control.js). + private const string LEAFLET_POSITION_TOP_LEFT = 'topleft'; + private const string LEAFLET_POSITION_TOP_RIGHT = 'topright'; + private const string LEAFLET_POSITION_BOTTOM_LEFT = 'bottomleft'; + private const string LEAFLET_POSITION_BOTTOM_RIGHT = 'bottomright'; + /** * {@inheritdoc} */ @@ -33,10 +40,11 @@ public function defineDefaultProperties(): array { 'minZoom' => 1, 'maxZoom' => 18, 'zoomFiner' => 0, + 'zoomControlPosition' => self::LEAFLET_POSITION_TOP_LEFT, 'scrollWheelZoom' => 0, 'doubleClickZoom' => 1, - 'position' => 'topleft', + 'position' => self::LEAFLET_POSITION_TOP_LEFT, 'marker' => 'defaultMarker', 'drawPolyline' => 0, 'drawRectangle' => 0, @@ -72,6 +80,13 @@ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); $map_keys = array_keys(leaflet_map_get_info()); + $positionOptions = [ + self::LEAFLET_POSITION_TOP_LEFT => $this->t('topleft'), + self::LEAFLET_POSITION_TOP_RIGHT => $this->t('topright'), + self::LEAFLET_POSITION_BOTTOM_LEFT => $this->t('bottomleft'), + self::LEAFLET_POSITION_BOTTOM_RIGHT => $this->t('bottomright'), + ]; + $form['mapstyles'] = [ '#type' => 'fieldset', '#title' => $this->t('Map settings'), @@ -139,6 +154,11 @@ public function form(array $form, FormStateInterface $form_state) { '#step' => 1, '#description' => $this->t('Value that might/will be added to default Fit Elements Bounds Zoom. (-5 / +5)'), ], + 'zoomControlPosition' => [ + '#type' => 'select', + '#title' => $this->t('Zoom control position'), + '#options' => $positionOptions, + ], 'scrollWheelZoom' => [ '#type' => 'checkbox', '#title' => $this->t('Enable Scroll Wheel Zoom on click'), @@ -159,12 +179,7 @@ public function form(array $form, FormStateInterface $form_state) { 'position' => [ '#type' => 'select', '#title' => $this->t('Toolbar position.'), - '#options' => [ - 'topleft' => $this->t('topleft'), - 'topright' => $this->t('topright'), - 'bottomleft' => $this->t('bottomleft'), - 'bottomright' => $this->t('bottomright'), - ], + '#options' => $positionOptions, ], 'marker' => [ '#type' => 'radios', From 76a63ba263ab1eb304e1d8f0408e39277b91a676 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 3 Feb 2026 10:38:47 +0100 Subject: [PATCH 02/52] Removed reference to nonexistent CSS file --- .../os2forms_webform_maps/os2forms_webform_maps.libraries.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/os2forms_webform_maps/os2forms_webform_maps.libraries.yml b/modules/os2forms_webform_maps/os2forms_webform_maps.libraries.yml index c223f6c3..78ce35dd 100644 --- a/modules/os2forms_webform_maps/os2forms_webform_maps.libraries.yml +++ b/modules/os2forms_webform_maps/os2forms_webform_maps.libraries.yml @@ -1,8 +1,5 @@ webformmap: version: 1.x - css: - theme: - css/webform_map.css: {} js: js/webform_map.js: {} dependencies: From c46ffda776ac31d32c9e2d2ea5847eafc8d68ee2 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 3 Feb 2026 10:41:06 +0100 Subject: [PATCH 03/52] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f3e756..53ef51fa 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-289](https://github.com/OS2Forms/os2forms/pull/289) + Added required "Zoom control position" to map element + ## [5.0.0] 2025-11-18 - [PR-192](https://github.com/OS2Forms/os2forms/pull/192) From 0d8224a6d389775fa5f0417efe9ff083ca5a57da Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Fri, 6 Feb 2026 07:55:09 +0100 Subject: [PATCH 04/52] 288: Add address element to generated html during digital post --- modules/os2forms_digital_post/README.md | 19 ++++ ...arve_ej_til_kant_demo_ny_rudeplacering.pdf | Bin 0 -> 42079 bytes .../os2forms_digital_post.services.yml | 7 ++ .../Os2formsDigitalPostSubscriber.php | 99 ++++++++++++++++++ .../src/Helper/WebformHelperSF1601.php | 35 +++++++ 5 files changed, 160 insertions(+) create mode 100644 modules/os2forms_digital_post/docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf create mode 100644 modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index 999d87c7..e9591ff0 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -80,3 +80,22 @@ of recipients: ``` shell drush os2forms-digital-post:test:send --help ``` + +## Fjernprint (physical digital post) + +To comply with the address placement in the envelope window (kuvert-rude) an +[event subscriber](src/EventSubscriber/Os2formsDigitalPostSubscriber.php) is +used to inject an address information element into generated HTML before it is +converted to a PDF. + +We are only guaranteed to have the necessary information when in a digital +post context. For that reason, the injection of address information is only +done when in a digital post context. Note also that the information is only +injected – it is not styled. This allows flexibility across installations but +also means that it is up to individual installations to style it correctly. +This should be done in OS2Forms Attachment-templates, see +[Overwriting templates](https://github.com/OS2Forms/os2forms/tree/develop/modules/os2forms_attachment#overwriting-templates). + +To see the exact requirements for address placement, see +[digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf](docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf). + diff --git a/modules/os2forms_digital_post/docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf b/modules/os2forms_digital_post/docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9f381c7a0246469067fa3d444e7f84614d3a95ef GIT binary patch literal 42079 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 gLnO2tD2G8X*n`UX2dtp+`Tzg` literal 0 HcmV?d00001 diff --git a/modules/os2forms_digital_post/os2forms_digital_post.services.yml b/modules/os2forms_digital_post/os2forms_digital_post.services.yml index 745b88d2..990e6020 100644 --- a/modules/os2forms_digital_post/os2forms_digital_post.services.yml +++ b/modules/os2forms_digital_post/os2forms_digital_post.services.yml @@ -52,6 +52,7 @@ services: - "@logger.channel.os2forms_digital_post" - "@logger.channel.os2forms_digital_post_submission" - "@Drupal\\os2forms_digital_post\\Helper\\DigitalPostHelper" + - "@Drupal\\os2forms_digital_post\\EventSubscriber\\Os2formsDigitalPostSubscriber" Drupal\os2forms_digital_post\Helper\SF1461Helper: @@ -69,3 +70,9 @@ services: - '@database' - '@Drupal\os2forms_digital_post\Helper\MeMoHelper' - '@logger.channel.os2forms_digital_post' + + Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber: + arguments: + - '@request_stack' + tags: + - { name: 'event_subscriber' } diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php new file mode 100644 index 00000000..331e244e --- /dev/null +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -0,0 +1,99 @@ +getHtml(); + + // Only modify HTML if there is exactly one submission. + if (count($event->getEntities()) === 1) { + $submission = $event->getEntities()[0]; + if ($submission instanceof WebformSubmissionInterface) { + // Check whether generation is for digital post. + if ($digitalPostContext = $this->getDigitalPostContext($submission)) { + + $name = $digitalPostContext['name']; + $address = $digitalPostContext['address']; + $zipAndCity = $digitalPostContext['zipAndCity']; + + $addressHtml = << +
+ $name
+ $address
+ $zipAndCity +
+ +HTML; + $html = preg_replace('@]*>@', '${0}' . $addressHtml, $html); + $this->deleteDigitalPostContext($submission); + } + } + } + + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + PrintEvents::POST_RENDER => ['onPrintRender'], + ]; + } + + /** + * Indicate Digital Post context in the current request. + */ + public function setDigitalPostContext(WebformSubmissionInterface $submission, array $digitalPostContext): void { + $key = $this->createSessionKeyFromSubmission($submission); + $this->requestStack->getCurrentRequest()->getSession()->set($key, $digitalPostContext); + } + + /** + * Check for Digital Post context in the current request. + */ + public function getDigitalPostContext(WebformSubmissionInterface $submission): array { + $key = $this->createSessionKeyFromSubmission($submission); + return $this->requestStack->getCurrentRequest()->getSession()->get($key, []); + } + + /** + * Delete Digital Post context from the current request. + */ + public function deleteDigitalPostContext(WebformSubmissionInterface $submission): bool { + $key = $this->createSessionKeyFromSubmission($submission); + return (bool) $this->requestStack->getCurrentRequest()->getSession()->remove($key); + } + + /** + * Create a session key from a submission that is unique to the submission. + */ + public function createSessionKeyFromSubmission(WebformSubmissionInterface $submission): string { + // Due to cloning of submission during attachment logic, we cannot use + // submission id or uuid. Webform serial, however, is copied along, so a + // combination of webform id and serial is used for uniqueness. + // @see \Drupal\os2forms_attachment\Element\AttachmentElement::overrideWebformSettings + return 'digital_post_context_' . $submission->getWebform()->id() . '_' . $submission->serial(); + } + +} diff --git a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php index 60fae6a2..860e5e0d 100644 --- a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php +++ b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php @@ -2,12 +2,15 @@ namespace Drupal\os2forms_digital_post\Helper; +use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult; +use Drupal\os2web_datalookup\LookupResult\CprLookupResult; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\advancedqueue\Entity\QueueInterface; use Drupal\advancedqueue\Job; use Drupal\advancedqueue\JobResult; +use Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber; use Drupal\os2forms_digital_post\Exception\InvalidRecipientIdentifierElementException; use Drupal\os2forms_digital_post\Exception\RuntimeException; use Drupal\os2forms_digital_post\Exception\SubmissionNotFoundException; @@ -62,6 +65,7 @@ public function __construct( #[Autowire(service: 'logger.channel.os2forms_digital_post_submission')] private readonly LoggerChannelInterface $submissionLogger, private readonly DigitalPostHelper $digitalPostHelper, + private readonly Os2formsDigitalPostSubscriber $digitalPostSubscriber, ) { $this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission'); $this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue'); @@ -152,6 +156,10 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h $recipientIdentifierType = 'CPR'; } + $digitalPostAddressData = $this->getAddressForDigitalPost($lookupResult); + + $this->digitalPostSubscriber->setDigitalPostContext($submission, $digitalPostAddressData); + $senderSettings = $this->settings->getSender(); $messageOptions = [ self::RECIPIENT_IDENTIFIER_TYPE => $recipientIdentifierType, @@ -342,4 +350,31 @@ public function deleteMessages(array $webformSubmissions): void { $this->beskedfordelerHelper->deleteMessages($webformSubmissions); } + /** + * Gets lookup results addresses in the format needed for SF1601. + */ + private function getAddressForDigitalPost(CprLookupResult|CompanyLookupResult $lookupResult): array { + $name = $lookupResult->getName(); + + $address = $lookupResult->getStreet(); + + if ($lookupResult->getHouseNr()) { + $address .= ' ' . $lookupResult->getHouseNr(); + } + if ($lookupResult->getFloor()) { + $address .= ' ' . $lookupResult->getFloor(); + } + if ($lookupResult->getApartmentNr()) { + $address .= ' ' . $lookupResult->getApartmentNr(); + } + + $zipAndCity = $lookupResult->getPostalCode() . ' ' . $lookupResult->getCity(); + + return [ + 'name' => $name, + 'address' => $address, + 'zipAndCity' => $zipAndCity, + ]; + } + } From ba26c190b8f76cdf4fbf532f1b9214261b5684e7 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Fri, 6 Feb 2026 08:03:57 +0100 Subject: [PATCH 05/52] 288: Updated CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f3e756..200bbc16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-301](https://github.com/OS2Forms/os2forms/pull/301) + Add address information to Digital Post shipments to ensure "*fjernprint*" + can be sent. + ## [5.0.0] 2025-11-18 - [PR-192](https://github.com/OS2Forms/os2forms/pull/192) From d85e11228008a1a95f7894012b639c8d12131cef Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 6 Feb 2026 13:11:51 +0200 Subject: [PATCH 06/52] #251 webform encrypt uninstall fix --- CHANGELOG.md | 3 +++ composer.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f3e756..cc01b0d3 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] +- [#251](https://github.com/OS2Forms/os2forms/issues/251) + Webform encrypt uninstall problem fix + ## [5.0.0] 2025-11-18 - [PR-192](https://github.com/OS2Forms/os2forms/pull/192) diff --git a/composer.json b/composer.json index 17968e2f..e2413ffe 100644 --- a/composer.json +++ b/composer.json @@ -128,7 +128,7 @@ "Add custom hook (hook_webform_post_load_data) for audit logging": "https://gist.githubusercontent.com/cableman/d26898fc8f65ee0a31001bf391583b59/raw/6189dc4c2ceaabb19d25cc4b98b0b3028a6b0e1e/gistfile1.txt" }, "drupal/webform_encrypt": { - "Ensure data is base64 encoded (https://www.drupal.org/project/webform_encrypt/issues/3399414)": "https://git.drupalcode.org/project/webform_encrypt/-/merge_requests/4.patch", + "Ensure data is base64 encoded (https://www.drupal.org/project/webform_encrypt/issues/3399414)": "https://www.drupal.org/files/issues/2026-02-06/patch.diff", "PHP Warning if unserialize fails (https://www.drupal.org/project/webform_encrypt/issues/3292305)": "https://www.drupal.org/files/issues/2022-06-23/unserialize-php-notice.patch" }, "drupal/webform_node_element": { From e0815dc87fd152b6764e7bd2e5f3190a2201fdf1 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 10 Feb 2026 13:19:01 +0100 Subject: [PATCH 07/52] 288: Added comment to preg_replace --- .../src/EventSubscriber/Os2formsDigitalPostSubscriber.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index 331e244e..eac2dafa 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -44,6 +44,8 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { HTML; + + // Insert address HTML immediately after body opening tag. $html = preg_replace('@]*>@', '${0}' . $addressHtml, $html); $this->deleteDigitalPostContext($submission); } From 1fca0a27d9b44f452ff42722f1a8b2b60d877ce8 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 10 Feb 2026 13:26:15 +0100 Subject: [PATCH 08/52] 288: Ensured required fields are set before accessing them --- .../src/EventSubscriber/Os2formsDigitalPostSubscriber.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index eac2dafa..be19fa7b 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -30,6 +30,11 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { if ($submission instanceof WebformSubmissionInterface) { // Check whether generation is for digital post. if ($digitalPostContext = $this->getDigitalPostContext($submission)) { + // Check whether required fields are present. + if (!isset($digitalPostContext['name'], $digitalPostContext['address'], $digitalPostContext['zipAndCity'])) { + // Do nothing if they are not. + return; + } $name = $digitalPostContext['name']; $address = $digitalPostContext['address']; From d25ba3eb6569e7da49a628b407af9dd71bb9fe82 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 10 Feb 2026 15:41:16 +0100 Subject: [PATCH 09/52] 288: Switched to h-card and saving lookup result in session --- modules/os2forms_digital_post/README.md | 54 ++++++++++++ .../os2forms_digital_post.services.yml | 2 +- .../Os2formsDigitalPostSubscriber.php | 83 +++++++++++-------- .../src/Helper/WebformHelperSF1601.php | 31 +------ 4 files changed, 106 insertions(+), 64 deletions(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index e9591ff0..9267cb5f 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -99,3 +99,57 @@ This should be done in OS2Forms Attachment-templates, see To see the exact requirements for address placement, see [digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf](docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf). +The injected HTML is on the form: + +Without extended address information: + +```html +
+
+
Jeppe
+
Test vej HouseNr
+
2100 Copenhagen
+
+
+``` + +With extended address information: + +```html +
+
+
Jeppe
+
Test vej HouseNr Floor AppartmentNr
+
2100 Copenhagen
+
+
+``` + +Without c/o: + +```html + +
+
+
Jeppe
+
Test vej HouseNr Floor AppartmentNr
+
2100 Copenhagen
+
+
+``` + +With c/o: + +```html +
+
+
Jeppe
+
c/o Mikkel
+
Test vej HouseNr Floor AppartmentNr
+
2100 Copenhagen
+
+
+``` + + + diff --git a/modules/os2forms_digital_post/os2forms_digital_post.services.yml b/modules/os2forms_digital_post/os2forms_digital_post.services.yml index 990e6020..32fdb740 100644 --- a/modules/os2forms_digital_post/os2forms_digital_post.services.yml +++ b/modules/os2forms_digital_post/os2forms_digital_post.services.yml @@ -73,6 +73,6 @@ services: Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber: arguments: - - '@request_stack' + - '@session' tags: - { name: 'event_subscriber' } diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index be19fa7b..4fefcffa 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -4,16 +4,18 @@ use Drupal\entity_print\Event\PrintEvents; use Drupal\entity_print\Event\PrintHtmlAlterEvent; +use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult; +use Drupal\os2web_datalookup\LookupResult\CprLookupResult; use Drupal\webform\WebformSubmissionInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\SessionInterface; /** * Used to alter the generated PDF to align with digital post requirements. */ final class Os2formsDigitalPostSubscriber implements EventSubscriberInterface { - public function __construct(private readonly RequestStack $requestStack) { + public function __construct(private readonly SessionInterface $session) { } /** @@ -29,30 +31,45 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { $submission = $event->getEntities()[0]; if ($submission instanceof WebformSubmissionInterface) { // Check whether generation is for digital post. - if ($digitalPostContext = $this->getDigitalPostContext($submission)) { - // Check whether required fields are present. - if (!isset($digitalPostContext['name'], $digitalPostContext['address'], $digitalPostContext['zipAndCity'])) { - // Do nothing if they are not. - return; + if ($lookupResult = $this->getDigitalPostContext($submission)) { + + // Combine address parts. + $streetAddress = $lookupResult->getStreet(); + + if ($lookupResult->getHouseNr()) { + $streetAddress .= ' ' . $lookupResult->getHouseNr(); } - $name = $digitalPostContext['name']; - $address = $digitalPostContext['address']; - $zipAndCity = $digitalPostContext['zipAndCity']; + $extendedAddress = ''; - $addressHtml = << -
- $name
- $address
- $zipAndCity -
- -HTML; + if ($lookupResult->getFloor()) { + $extendedAddress = $lookupResult->getFloor(); + } + if ($lookupResult->getApartmentNr()) { + $extendedAddress .= ' ' . $lookupResult->getApartmentNr(); + } + + // Generate address HTML. + $addressHtml = '
'; + $addressHtml .= '
' . htmlspecialchars($lookupResult->getName()) . '
'; + if ($lookupResult->getCoName()) { + $addressHtml .= '
c/o ' . htmlspecialchars($lookupResult->getCoName()) . '
'; + } + $addressHtml .= '
'; + $addressHtml .= '' . htmlspecialchars($streetAddress) . ''; + if (!empty($extendedAddress)) { + $addressHtml .= ' ' . htmlspecialchars($extendedAddress) . ''; + } + $addressHtml .= '
'; + $addressHtml .= '
'; + $addressHtml .= '' . htmlspecialchars($lookupResult->getPostalCode()) . ''; + $addressHtml .= ' ' . htmlspecialchars($lookupResult->getCity()) . ''; + $addressHtml .= '
'; + $addressHtml .= '
'; + $addressHtml .= '
'; // Insert address HTML immediately after body opening tag. $html = preg_replace('@]*>@', '${0}' . $addressHtml, $html); - $this->deleteDigitalPostContext($submission); } } } @@ -69,27 +86,27 @@ public static function getSubscribedEvents(): array { } /** - * Indicate Digital Post context in the current request. + * Indicate Digital Post context in the current session. */ - public function setDigitalPostContext(WebformSubmissionInterface $submission, array $digitalPostContext): void { + public function setDigitalPostContext(WebformSubmissionInterface $submission, CompanyLookupResult|CprLookupResult $lookupResult): void { $key = $this->createSessionKeyFromSubmission($submission); - $this->requestStack->getCurrentRequest()->getSession()->set($key, $digitalPostContext); + $this->session->set($key, $lookupResult); } /** - * Check for Digital Post context in the current request. + * Check for Digital Post context in the current session. */ - public function getDigitalPostContext(WebformSubmissionInterface $submission): array { + public function getDigitalPostContext(WebformSubmissionInterface $submission): CompanyLookupResult|CprLookupResult|null { $key = $this->createSessionKeyFromSubmission($submission); - return $this->requestStack->getCurrentRequest()->getSession()->get($key, []); - } - /** - * Delete Digital Post context from the current request. - */ - public function deleteDigitalPostContext(WebformSubmissionInterface $submission): bool { - $key = $this->createSessionKeyFromSubmission($submission); - return (bool) $this->requestStack->getCurrentRequest()->getSession()->remove($key); + $digitalPostContext = $this->session->get($key); + + // We only need/use it once, so just remove it after fetching it. + if ($digitalPostContext) { + $this->session->remove($key); + } + + return $digitalPostContext; } /** diff --git a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php index 860e5e0d..e26e0b1b 100644 --- a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php +++ b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php @@ -156,9 +156,7 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h $recipientIdentifierType = 'CPR'; } - $digitalPostAddressData = $this->getAddressForDigitalPost($lookupResult); - - $this->digitalPostSubscriber->setDigitalPostContext($submission, $digitalPostAddressData); + $this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult); $senderSettings = $this->settings->getSender(); $messageOptions = [ @@ -350,31 +348,4 @@ public function deleteMessages(array $webformSubmissions): void { $this->beskedfordelerHelper->deleteMessages($webformSubmissions); } - /** - * Gets lookup results addresses in the format needed for SF1601. - */ - private function getAddressForDigitalPost(CprLookupResult|CompanyLookupResult $lookupResult): array { - $name = $lookupResult->getName(); - - $address = $lookupResult->getStreet(); - - if ($lookupResult->getHouseNr()) { - $address .= ' ' . $lookupResult->getHouseNr(); - } - if ($lookupResult->getFloor()) { - $address .= ' ' . $lookupResult->getFloor(); - } - if ($lookupResult->getApartmentNr()) { - $address .= ' ' . $lookupResult->getApartmentNr(); - } - - $zipAndCity = $lookupResult->getPostalCode() . ' ' . $lookupResult->getCity(); - - return [ - 'name' => $name, - 'address' => $address, - 'zipAndCity' => $zipAndCity, - ]; - } - } From cfdd08f9e1e75f20b5560d611e30dadfc6bdde03 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 10 Feb 2026 16:11:07 +0100 Subject: [PATCH 10/52] 288: Updated README with injected html and example styling --- modules/os2forms_digital_post/README.md | 103 +++++++++++++++++++++--- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index 9267cb5f..805e94b4 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -99,11 +99,14 @@ This should be done in OS2Forms Attachment-templates, see To see the exact requirements for address placement, see [digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf](docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf). -The injected HTML is on the form: +### The injected HTML -Without extended address information: +Variations of the injected HTML include extended addresses and c/o. + +Without extended address information or c/o: ```html +
Jeppe
@@ -113,32 +116,32 @@ Without extended address information:
``` -With extended address information: +With just an extended address: ```html
-
-
Jeppe
-
Test vej HouseNr Floor AppartmentNr
-
2100 Copenhagen
-
+
+
Jeppe
+
Test vej HouseNr Floor AppartmentNr
+
2100 Copenhagen
+
``` -Without c/o: +With just c/o: ```html
Jeppe
-
Test vej HouseNr Floor AppartmentNr
+
c/o Mikkel
Test vej HouseNr
2100 Copenhagen
``` -With c/o: +With extended address information and c/o: ```html
@@ -153,3 +156,81 @@ With c/o: +### Styling of the HTML + +The following SCSS can be used to style the injected HTML accordingly: + +```scss +$margin-top: 25mm; +// There is no exact measurement for margin right in the specifications +$margin-right: 10mm; +$margin-bottom: 20mm; +$margin-left: 17mm; +$page-width: 210mm; +$page-height: 297mm; +$envelope-window-height: 89mm; +$envelope-window-width: 115mm; +$recipient-window-height: 21mm; +$recipient-window-width: 59mm; + +@page { + size: A4; + margin: 0; +} + +html, body { + font-family:"DejaVu Sans",Helvetica,Arial,sans-serif; +} + +body { + margin-top: $margin-top; + margin-right: $margin-right; + margin-bottom: $margin-bottom; + margin-left: $margin-left; +} + +header { + position: fixed; + top: 0; + height: $margin-top; + width: calc($page-width - $margin-left - $margin-right); + font-size: 12px; +} + +footer { + position: fixed; + bottom: 0; + height: $margin-bottom; + width: calc($page-width - $margin-left - $margin-right); + font-size: 12px; +} + +// Style the envelope window that may be injected by Digital Post. +// Note that top/left is made from the assumption that @page has margin 0. +// @see \Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber:onPrintRender +#envelope-window-digital-post { + position: absolute; + top: $margin-top; + left: $margin-left; + height: $envelope-window-height; + width: $envelope-window-width; + background: white +} + +// If envelope window is present, move webform content down +// @see os2forms_digital_post +#envelope-window-digital-post ~ * .webform-entity-print-body { + margin-top: $envelope-window-height; +} + +#envelope-window-digital-post > div { + position: absolute; + top: 16mm; + left: 4mm; + font-size: 10px; + height: $recipient-window-height; + width: $recipient-window-width; +} + +// More custom styling... +``` From e3a719a48b9e96e0151b0c89966d54d3675d40b9 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 10 Feb 2026 16:17:59 +0100 Subject: [PATCH 11/52] 288: Cleaned up example styling --- modules/os2forms_digital_post/README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index 805e94b4..16e83513 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -178,10 +178,6 @@ $recipient-window-width: 59mm; margin: 0; } -html, body { - font-family:"DejaVu Sans",Helvetica,Arial,sans-serif; -} - body { margin-top: $margin-top; margin-right: $margin-right; @@ -223,6 +219,7 @@ footer { margin-top: $envelope-window-height; } +// Style the h-card div #envelope-window-digital-post > div { position: absolute; top: 16mm; From fdaafd5ca437fd61bd9ffdded019750ed6fb4f9c Mon Sep 17 00:00:00 2001 From: Jeppe Kuhlmann Andersen <78410897+jekuaitk@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:09:35 +0100 Subject: [PATCH 12/52] Update modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php Co-authored-by: Mikkel Ricky --- .../src/EventSubscriber/Os2formsDigitalPostSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index 4fefcffa..81ca5ef0 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -52,7 +52,7 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { // Generate address HTML. $addressHtml = '
'; $addressHtml .= '
' . htmlspecialchars($lookupResult->getName()) . '
'; - if ($lookupResult->getCoName()) { + if ($lookupResult instanceof CprLookupResult && $lookupResult->getCoName()) { $addressHtml .= '
c/o ' . htmlspecialchars($lookupResult->getCoName()) . '
'; } $addressHtml .= '
'; From a552bebc9fb7d6b1dc6265ff581a390ac54633fb Mon Sep 17 00:00:00 2001 From: Jeppe Kuhlmann Andersen <78410897+jekuaitk@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:09:53 +0100 Subject: [PATCH 13/52] Update modules/os2forms_digital_post/README.md Co-authored-by: Mikkel Ricky --- modules/os2forms_digital_post/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index 16e83513..2c550502 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -85,7 +85,7 @@ drush os2forms-digital-post:test:send --help To comply with the address placement in the envelope window (kuvert-rude) an [event subscriber](src/EventSubscriber/Os2formsDigitalPostSubscriber.php) is -used to inject an address information element into generated HTML before it is +used to inject an address information element into the generated HTML before it is converted to a PDF. We are only guaranteed to have the necessary information when in a digital From 7ed8d8f43a6fea0be3605bf6f09f9230cab3a7d8 Mon Sep 17 00:00:00 2001 From: Jeppe Kuhlmann Andersen <78410897+jekuaitk@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:10:05 +0100 Subject: [PATCH 14/52] Update modules/os2forms_digital_post/README.md Co-authored-by: Mikkel Ricky --- modules/os2forms_digital_post/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index 2c550502..851e6342 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -158,7 +158,7 @@ With extended address information and c/o: ### Styling of the HTML -The following SCSS can be used to style the injected HTML accordingly: +The following [SCSS](https://sass-lang.com/) can be used to style the injected HTML accordingly: ```scss $margin-top: 25mm; From 0c397392b8a4d8debbb89a06e5bd700dbd6416eb Mon Sep 17 00:00:00 2001 From: Jeppe Kuhlmann Andersen <78410897+jekuaitk@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:26:03 +0100 Subject: [PATCH 15/52] Update modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php Co-authored-by: Mikkel Ricky --- .../src/EventSubscriber/Os2formsDigitalPostSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index 81ca5ef0..8c95f5c7 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -53,7 +53,7 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { $addressHtml = '
'; $addressHtml .= '
' . htmlspecialchars($lookupResult->getName()) . '
'; if ($lookupResult instanceof CprLookupResult && $lookupResult->getCoName()) { - $addressHtml .= '
c/o ' . htmlspecialchars($lookupResult->getCoName()) . '
'; + $addressHtml .= '
c/o ' . htmlspecialchars($lookupResult->getCoName()) . '
'; } $addressHtml .= '
'; $addressHtml .= '' . htmlspecialchars($streetAddress) . ''; From 423219b20c233ad3c0b29524bde8e4bed73327c4 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Mon, 16 Feb 2026 10:47:52 +0100 Subject: [PATCH 16/52] Used constants for positions --- .../src/Element/WebformLeafletMapField.php | 5 +++-- .../src/Plugin/WebformElement/WebformLeafletMapField.php | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php b/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php index 84f7506c..5d36829f 100644 --- a/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php +++ b/modules/os2forms_webform_maps/src/Element/WebformLeafletMapField.php @@ -5,6 +5,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\FormElement; use Drupal\webform\Element\WebformCompositeFormElementTrait; +use Drupal\os2forms_webform_maps\Plugin\WebformElement\WebformLeafletMapField as WebformLeafletMapElement; /** * Provides a webform_map_field. @@ -36,7 +37,7 @@ public function getInfo() { '#minZoom' => 1, '#maxZoom' => 18, '#zoomFiner' => 0, - '#position' => 'topleft', + '#position' => WebformLeafletMapElement::LEAFLET_POSITION_TOP_LEFT, '#marker' => 'defaultMarker', '#drawPolyline' => 0, '#drawRectangle' => 0, @@ -90,7 +91,7 @@ public static function processWebformMapElement(&$element, FormStateInterface $f 'zoomFiner' => $element['#zoomFiner'], 'minZoom' => $element['#minZoom'], 'maxZoom' => $element['#maxZoom'], - 'zoomControlPosition' => $element['#zoomControlPosition'] ?? 'topleft', + 'zoomControlPosition' => $element['#zoomControlPosition'] ?? WebformLeafletMapElement::LEAFLET_POSITION_TOP_LEFT, 'center' => [ 'lat' => (float) $element['#lat'], 'lon' => (float) $element['#lon'], diff --git a/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php b/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php index 31374ba7..3b6aa405 100644 --- a/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php +++ b/modules/os2forms_webform_maps/src/Plugin/WebformElement/WebformLeafletMapField.php @@ -22,10 +22,10 @@ class WebformLeafletMapField extends WebformElementBase { // Valid Leaflet control positions (cf. // https://github.com/Leaflet/Leaflet/blob/main/src/control/Control.js). - private const string LEAFLET_POSITION_TOP_LEFT = 'topleft'; - private const string LEAFLET_POSITION_TOP_RIGHT = 'topright'; - private const string LEAFLET_POSITION_BOTTOM_LEFT = 'bottomleft'; - private const string LEAFLET_POSITION_BOTTOM_RIGHT = 'bottomright'; + const string LEAFLET_POSITION_TOP_LEFT = 'topleft'; + const string LEAFLET_POSITION_TOP_RIGHT = 'topright'; + const string LEAFLET_POSITION_BOTTOM_LEFT = 'bottomleft'; + const string LEAFLET_POSITION_BOTTOM_RIGHT = 'bottomright'; /** * {@inheritdoc} From de2282a3ad6edc60fc6c18ed8d6585d568c3fbde Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 17 Feb 2026 16:28:55 +0100 Subject: [PATCH 17/52] Fix IP validation in digital signature file download (CIDR support) --- CHANGELOG.md | 2 + .../os2forms_digital_signature.module | 59 +++++++++++++++++-- .../os2forms_digital_signature.services.yml | 13 ++++ .../src/Form/SettingsForm.php | 5 +- 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f3e756..89a90989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- Fix IP validation in digital signature file download (CIDR support) + ## [5.0.0] 2025-11-18 - [PR-192](https://github.com/OS2Forms/os2forms/pull/192) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.module b/modules/os2forms_digital_signature/os2forms_digital_signature.module index 6e12a210..49f9a496 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.module +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -57,21 +57,70 @@ function os2forms_digital_signature_file_download($uri) { $config = \Drupal::config(SettingsForm::$configName); $allowedIps = $config->get('os2forms_digital_signature_submission_allowed_ips'); - $allowedIpsArr = explode(',', $allowedIps); - $remoteIp = Drupal::request()->getClientIp(); + $allowedIpsArr = array_map('trim', explode(',', $allowedIps)); + // Remove empty entries (e.g. from trailing comma or empty config). + $allowedIpsArr = array_filter($allowedIpsArr); + $remoteIp = \Drupal::request()->getClientIp(); - // IP list is empty, or request IP is allowed. - if (empty($allowedIpsArr) || in_array($remoteIp, $allowedIpsArr)) { + // IP list is empty, allow access. + if (empty($allowedIpsArr)) { $basename = basename($uri); return [ 'Content-disposition' => 'attachment; filename="' . $basename . '"', ]; } - // Otherwise - Deny access. + // Check if remote IP matches any allowed IP or CIDR range. + foreach ($allowedIpsArr as $allowedIp) { + if ($remoteIp === $allowedIp || os2forms_digital_signature_ip_in_cidr($remoteIp, $allowedIp)) { + $basename = basename($uri); + return [ + 'Content-disposition' => 'attachment; filename="' . $basename . '"', + ]; + } + } + + // Deny access and log warning. + \Drupal::logger('os2forms_digital_signature')->warning('File download denied for IP @ip on URI @uri. Allowed IPs: @allowed', [ + '@ip' => $remoteIp, + '@uri' => $uri, + '@allowed' => $allowedIps, + ]); + return -1; } // Not submission file, allow normal access. return NULL; } + +/** + * Check if an IP address is within a CIDR range. + * + * @param string $ip + * The IP address to check. + * @param string $cidr + * The CIDR range (e.g. "172.16.0.0/16"). + * + * @return bool + * TRUE if the IP is within the CIDR range, FALSE otherwise. + */ +function os2forms_digital_signature_ip_in_cidr(string $ip, string $cidr): bool { + if (!str_contains($cidr, '/')) { + return FALSE; + } + + [$subnet, $bits] = explode('/', $cidr, 2); + $bits = (int) $bits; + + $ip = ip2long($ip); + $subnet = ip2long($subnet); + + if ($ip === FALSE || $subnet === FALSE || $bits < 0 || $bits > 32) { + return FALSE; + } + + $mask = -1 << (32 - $bits); + + return ($ip & $mask) === ($subnet & $mask); +} diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml index 33848830..47d0fd68 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml @@ -11,3 +11,16 @@ services: - '@config.factory' - '@entity_type.manager' - '@logger.channel.os2forms_digital_signature' +services: + logger.channel.os2forms_digital_signature: + parent: logger.channel_base + arguments: [ 'os2forms_digital_signature' ] + + os2forms_digital_signature.signing_service: + class: Drupal\os2forms_digital_signature\Service\SigningService + arguments: + - '@http_client' + - '@datetime.time' + - '@config.factory' + - '@entity_type.manager' + - '@logger.channel.os2forms_digital_signature' diff --git a/modules/os2forms_digital_signature/src/Form/SettingsForm.php b/modules/os2forms_digital_signature/src/Form/SettingsForm.php index a05ec886..ab821c26 100644 --- a/modules/os2forms_digital_signature/src/Form/SettingsForm.php +++ b/modules/os2forms_digital_signature/src/Form/SettingsForm.php @@ -40,12 +40,14 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['os2forms_digital_signature_remote_service_url'] = [ '#type' => 'textfield', '#title' => $this->t('Signature server URL'), + '#required' => TRUE, '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_remote_service_url'), '#description' => $this->t('E.g. https://signering.bellcom.dk/sign.php?'), ]; $form['os2forms_digital_signature_sign_hash_salt'] = [ '#type' => 'textfield', '#title' => $this->t('Hash Salt used for signature'), + '#required' => TRUE, '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_sign_hash_salt'), '#description' => $this->t('Must match hash salt on the signature server'), ]; @@ -53,11 +55,12 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'textfield', '#title' => $this->t('List IPs which can download unsigned PDF submissions'), '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_submission_allowed_ips'), - '#description' => $this->t('Comma separated. e.g. 192.168.1.1,192.168.2.1'), + '#description' => $this->t('Comma separated. e.g. 192.168.1.1,192.168.2.1 or 172.16.0.0/16. If left empty no restrictions will be applied.'), ]; $form['os2forms_digital_signature_submission_retention_period'] = [ '#type' => 'textfield', '#title' => $this->t('Unsigned submission timespan (s)'), + '#required' => TRUE, '#default_value' => ($this->config(self::$configName)->get('os2forms_digital_signature_submission_retention_period')) ?? 300, '#description' => $this->t('How many seconds can unsigned submission exist before being automatically deleted'), ]; From a95445d6e90eda165acdd58c4ebe128ed3490b3b Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 18 Feb 2026 09:13:57 +0100 Subject: [PATCH 18/52] Switch digital signatur ip range check to use symfony function --- .../os2forms_digital_signature.module | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.module b/modules/os2forms_digital_signature/os2forms_digital_signature.module index 49f9a496..35a72ef6 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.module +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -8,6 +8,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StreamWrapper\StreamWrapperManager; use Drupal\os2forms_digital_signature\Form\SettingsForm; +use Symfony\Component\HttpFoundation\IpUtils; /** * Implements hook_cron(). @@ -106,21 +107,5 @@ function os2forms_digital_signature_file_download($uri) { * TRUE if the IP is within the CIDR range, FALSE otherwise. */ function os2forms_digital_signature_ip_in_cidr(string $ip, string $cidr): bool { - if (!str_contains($cidr, '/')) { - return FALSE; - } - - [$subnet, $bits] = explode('/', $cidr, 2); - $bits = (int) $bits; - - $ip = ip2long($ip); - $subnet = ip2long($subnet); - - if ($ip === FALSE || $subnet === FALSE || $bits < 0 || $bits > 32) { - return FALSE; - } - - $mask = -1 << (32 - $bits); - - return ($ip & $mask) === ($subnet & $mask); + return IpUtils::checkIp($ip, $cidr); } From 7fb53321996b7f252b839bb8f633e62d3b6eb11b Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 19 Feb 2026 10:45:35 +0100 Subject: [PATCH 19/52] Made digital signature text placement on attachment configurable --- .../src/Element/AttachmentElement.php | 5 ++- .../src/Os2formsAttachmentPrintBuilder.php | 44 ++++++++++++++----- .../WebformElement/AttachmentElement.php | 19 ++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php index cdf3ae44..bbc33f0b 100644 --- a/modules/os2forms_attachment/src/Element/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Element/AttachmentElement.php @@ -2,6 +2,7 @@ namespace Drupal\os2forms_attachment\Element; +use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder; use Drupal\webform\Entity\WebformSubmission; use Drupal\webform\WebformSubmissionInterface; use Drupal\webform_attachment\Element\WebformAttachmentBase; @@ -21,6 +22,7 @@ public function getInfo() { '#view_mode' => 'html', '#export_type' => 'pdf', '#digital_signature' => FALSE, + '#digital_signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, '#template' => '', ]; } @@ -77,7 +79,8 @@ public static function getFileContent(array $element, WebformSubmissionInterface // Adding digital signature. if (isset($element['#digital_signature']) && $element['#digital_signature']) { - $file_path = $print_builder->savePrintableDigitalSignature([$webform_submission], $print_engine, $scheme, $file_name); + $signaturePosition = $element['#digital_signature_position'] ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; + $file_path = $print_builder->savePrintableDigitalSignature([$webform_submission], $print_engine, $scheme, $file_name, TRUE, $signaturePosition); } else { $file_path = $print_builder->savePrintable([$webform_submission], $print_engine, $scheme, $file_name); diff --git a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php index 1f8a9f38..6698b165 100644 --- a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php +++ b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php @@ -18,6 +18,11 @@ */ class Os2formsAttachmentPrintBuilder extends PrintBuilder { + public const SIGNATURE_POSITION_FOOTER = 'footer'; + public const SIGNATURE_POSITION_HEADER = 'header'; + public const SIGNATURE_POSITION_AFTER_CONTENT = 'after_content'; + public const SIGNATURE_POSITION_BEFORE_CONTENT = 'before_content'; + /** * {@inheritdoc} */ @@ -52,8 +57,8 @@ public function printHtml(EntityInterface $entity, $use_default_css = TRUE, $opt * @return string * FALSE or the URI to the file. E.g. public://my-file.pdf. */ - public function savePrintableDigitalSignature(array $entities, PrintEngineInterface $print_engine, $scheme = 'public', $filename = FALSE, $use_default_css = TRUE) { - $renderer = $this->prepareRenderer($entities, $print_engine, $use_default_css, TRUE); + public function savePrintableDigitalSignature(array $entities, PrintEngineInterface $print_engine, $scheme = 'public', $filename = FALSE, $use_default_css = TRUE, string $signaturePosition = self::SIGNATURE_POSITION_AFTER_CONTENT) { + $renderer = $this->prepareRenderer($entities, $print_engine, $use_default_css, $signaturePosition); // Allow other modules to alter the generated Print object. $this->dispatcher->dispatch(new PreSendPrintEvent($print_engine, $entities), PrintEvents::PRE_SEND); @@ -82,15 +87,16 @@ public function savePrintableDigitalSignature(array $entities, PrintEngineInterf * The print engine. * @param bool $use_default_css * TRUE if we want the default CSS included. - * @param bool $digitalSignature - * If the digital signature message needs to be added. + * @param string $signaturePosition + * The position for the digital signature validation text. Empty string + * means no signature. Use the SIGNATURE_POSITION_* class constants. * * @return \Drupal\entity_print\Renderer\RendererInterface * A print renderer. * * @see PrintBuilder::prepareRenderer */ - protected function prepareRenderer(array $entities, PrintEngineInterface $print_engine, $use_default_css, $digitalSignature = FALSE) { + protected function prepareRenderer(array $entities, PrintEngineInterface $print_engine, $use_default_css, string $signaturePosition = '') { if (empty($entities)) { throw new \InvalidArgumentException('You must pass at least 1 entity'); } @@ -106,12 +112,30 @@ protected function prepareRenderer(array $entities, PrintEngineInterface $print_ '#attached' => [], ]; - // Adding hardcoded negative margin to avoid margins in
- // structure. That margin is automatically added in PDF and PDF only. $generatedHtml = (string) $renderer->generateHtml($entities, $render, $use_default_css, TRUE); - $generatedHtml .= ""; - if ($digitalSignature) { - $generatedHtml .= $this->t('You can validate the signature on this PDF file via validering.nemlog-in.dk.'); + + // Place signature according to the passed parameter. + if ($signaturePosition) { + $signatureHtml = '

' . $this->t('You can validate the signature on this PDF file via validering.nemlog-in.dk.') . '

'; + + switch ($signaturePosition) { + case self::SIGNATURE_POSITION_HEADER: + $generatedHtml = str_replace('', $signatureHtml . '', $generatedHtml); + break; + + case self::SIGNATURE_POSITION_BEFORE_CONTENT: + $generatedHtml = str_replace('
', '
' . $signatureHtml, $generatedHtml); + break; + + case self::SIGNATURE_POSITION_FOOTER: + $generatedHtml = str_replace('', $signatureHtml . '', $generatedHtml); + break; + + case self::SIGNATURE_POSITION_AFTER_CONTENT: + default: + $generatedHtml = str_replace('', $signatureHtml . '', $generatedHtml); + break; + } } $print_engine->addPage($generatedHtml); diff --git a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php index 7ec580bd..3f6a11da 100644 --- a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php @@ -5,6 +5,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\webform\Twig\WebformTwigExtension; use Drupal\webform\Utility\WebformElementHelper; +use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder; use Drupal\webform_attachment\Plugin\WebformElement\WebformAttachmentBase; /** @@ -28,6 +29,7 @@ protected function defineDefaultProperties() { 'template' => '', 'export_type' => '', 'digital_signature' => '', + 'digital_signature_position' => 'after_content', 'exclude_empty' => '', 'exclude_empty_checkbox' => '', 'excluded_elements' => '', @@ -93,6 +95,23 @@ public function form(array $form, FormStateInterface $form_state) { '#type' => 'checkbox', '#title' => $this->t('Digital signature'), ]; + $form['attachment']['digital_signature_position'] = [ + '#type' => 'select', + '#title' => $this->t('Digital signature position'), + '#description' => $this->t('Select where the digital signature validation text should be placed in the PDF document.'), + '#options' => [ + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_FOOTER => $this->t('Footer (repeats on every page)'), + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_HEADER => $this->t('Header (repeats on every page)'), + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT => $this->t('After content (end of document)'), + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_BEFORE_CONTENT => $this->t('Before content (start of document)'), + ], + '#default_value' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, + '#states' => [ + 'visible' => [ + ':input[name="properties[digital_signature]"]' => ['checked' => TRUE], + ], + ], + ]; // Set #access so that help is always visible. WebformElementHelper::setPropertyRecursive($form['attachment']['help'], '#access', TRUE); From 96c578c8c21e92ce5b388c50009a75ae5dd4f83d Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 19 Feb 2026 10:51:57 +0100 Subject: [PATCH 20/52] Comment and CHANGELOG --- CHANGELOG.md | 3 +++ .../src/Os2formsAttachmentPrintBuilder.php | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f3e756..14bd0633 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-306](https://github.com/OS2Forms/os2forms/pull/306) + Made digital signature text placement configurable. + ## [5.0.0] 2025-11-18 - [PR-192](https://github.com/OS2Forms/os2forms/pull/192) diff --git a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php index 6698b165..c72f098c 100644 --- a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php +++ b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php @@ -49,8 +49,8 @@ public function printHtml(EntityInterface $entity, $use_default_css = TRUE, $opt /** * Modified version of the original savePrintable() function. * - * The only difference is modified call to prepareRenderer with digitalPost - * flag TRUE. + * The only difference is modified call to prepareRenderer with a + * signature position parameter. * * @see PrintBuilder::savePrintable() * From f972417f8d7f9172c1d6779d39e2201d467c94b1 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 19 Feb 2026 10:56:17 +0100 Subject: [PATCH 21/52] Use constants --- .../src/Plugin/WebformElement/AttachmentElement.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php index 3f6a11da..e19c531a 100644 --- a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php @@ -29,7 +29,7 @@ protected function defineDefaultProperties() { 'template' => '', 'export_type' => '', 'digital_signature' => '', - 'digital_signature_position' => 'after_content', + 'digital_signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, 'exclude_empty' => '', 'exclude_empty_checkbox' => '', 'excluded_elements' => '', From a6e6f1fd5ebd89c70645307cc4bb73607665ce99 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 19 Feb 2026 15:19:51 +0100 Subject: [PATCH 22/52] Added missing comma in address --- .../Os2formsDigitalPostSubscriber.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index 8c95f5c7..c30a21e8 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -43,6 +43,8 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { $extendedAddress = ''; if ($lookupResult->getFloor()) { + // Add a comma to align with danish address specifications. + $streetAddress .= ','; $extendedAddress = $lookupResult->getFloor(); } if ($lookupResult->getApartmentNr()) { @@ -97,6 +99,19 @@ public function setDigitalPostContext(WebformSubmissionInterface $submission, Co * Check for Digital Post context in the current session. */ public function getDigitalPostContext(WebformSubmissionInterface $submission): CompanyLookupResult|CprLookupResult|null { + $result = new CprLookupResult(); + + $result->setCpr('2611740000'); + $result->setName('Jeppe Kuhlmann Andersen'); + $result->setPostalCode('8200'); + $result->setCity('Aarhus N'); + $result->setStreet('Jens Baggesens vej'); + $result->setHouseNr('45'); + $result->setFloor('st.'); + $result->setApartmentNr('7'); + $result->setCoName('Mikkel'); + + return $result; $key = $this->createSessionKeyFromSubmission($submission); $digitalPostContext = $this->session->get($key); From 7c057aab145f7843a5435012b668eb16ee7b0b06 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 19 Feb 2026 15:23:04 +0100 Subject: [PATCH 23/52] Updated README --- modules/os2forms_digital_post/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index 851e6342..a8f5f638 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -154,8 +154,6 @@ With extended address information and c/o:
``` - - ### Styling of the HTML The following [SCSS](https://sass-lang.com/) can be used to style the injected HTML accordingly: From a99952cba985fe85b1efbc14cfc9f3743443af40 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Feb 2026 13:42:02 +0100 Subject: [PATCH 24/52] Cleanup --- .../Os2formsDigitalPostSubscriber.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index c30a21e8..6efe2419 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -99,19 +99,6 @@ public function setDigitalPostContext(WebformSubmissionInterface $submission, Co * Check for Digital Post context in the current session. */ public function getDigitalPostContext(WebformSubmissionInterface $submission): CompanyLookupResult|CprLookupResult|null { - $result = new CprLookupResult(); - - $result->setCpr('2611740000'); - $result->setName('Jeppe Kuhlmann Andersen'); - $result->setPostalCode('8200'); - $result->setCity('Aarhus N'); - $result->setStreet('Jens Baggesens vej'); - $result->setHouseNr('45'); - $result->setFloor('st.'); - $result->setApartmentNr('7'); - $result->setCoName('Mikkel'); - - return $result; $key = $this->createSessionKeyFromSubmission($submission); $digitalPostContext = $this->session->get($key); From cac654d72ec1e94c395e54d132f6271cafaae853 Mon Sep 17 00:00:00 2001 From: skifter Date: Wed, 25 Feb 2026 10:44:00 +0100 Subject: [PATCH 25/52] Change to git file permissions error on build --- .github/workflows/pr.yml | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 194b59f3..416364ed 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,17 +23,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Fix ownership (container) + run: | + docker compose run --rm --user 0 -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) php bash -lc 'chown -R ${HOST_UID}:${HOST_GID} /app' + - name: Validate composer files run: | - # Note that we don't use the --strict flag on validate due to the - # package drupal/config_entity_revisions 2.0.x-dev being considered a - # version cf. - # https://getcomposer.org/doc/articles/versions.md#branches - docker compose run --rm php composer validate composer.json + docker compose run --rm --user "$(id -u):$(id -g)" php composer validate composer.json + - name: Check that composer file is normalized run: | - docker compose run --rm php composer install - docker compose run --rm php composer normalize --dry-run + docker compose run --rm --user "$(id -u):$(id -g)" php composer install + docker compose run --rm --user "$(id -u):$(id -g)" php composer normalize --dry-run test-composer-files: name: Test composer files @@ -43,29 +45,44 @@ jobs: dependency-version: [ prefer-lowest, prefer-stable ] steps: - uses: actions/checkout@v4 + + - name: Fix ownership (container) + run: | + docker compose run --rm --user 0 -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) php bash -lc 'chown -R ${HOST_UID}:${HOST_GID} /app' + - name: Check that dependencies resolve. run: | - # Clean up before update (cf. https://www.drupal.org/project/simplesamlphp_auth/issues/3350773) rm -fr vendor/ - docker compose run --rm php composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + docker compose run --rm --user "$(id -u):$(id -g)" php composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction php-coding-standards: name: PHP coding standards runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Fix ownership (container) + run: | + docker compose run --rm --user 0 -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) php bash -lc 'chown -R ${HOST_UID}:${HOST_GID} /app' + - name: Install Dependencies run: | - docker compose run --rm php composer install + docker compose run --rm --user "$(id -u):$(id -g)" php composer install + - name: PHPCS run: | - docker compose run --rm php composer coding-standards-check/phpcs + docker compose run --rm --user "$(id -u):$(id -g)" php composer coding-standards-check/phpcs php-code-analysis: name: PHP code analysis runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Fix ownership (container) + run: | + docker compose run --rm --user 0 -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) php bash -lc 'chown -R ${HOST_UID}:${HOST_GID} /app' + - name: Code analysis run: | ./scripts/code-analysis From 4f782d9d0545c69a537ac04a443e2ad133725dd8 Mon Sep 17 00:00:00 2001 From: skifter Date: Wed, 25 Feb 2026 11:06:09 +0100 Subject: [PATCH 26/52] Change 2 to make repo safe for git --- .github/workflows/pr.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 416364ed..d2965bb9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,6 +23,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Mark repo as safe for git (container) + run: | + docker compose run --rm --user 0 php bash -lc \ + 'git config --system --add safe.directory /app || git config --global --add safe.directory /app' - name: Fix ownership (container) run: | From 300ffa39af0d10bcbf7e84aabf8b5f97a94c4d5d Mon Sep 17 00:00:00 2001 From: skifter Date: Wed, 25 Feb 2026 12:17:59 +0100 Subject: [PATCH 27/52] Changing image to use older wersion with php-3 --- compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yaml b/compose.yaml index d606c09c..0448b12f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ services: php: - image: ddev/ddev-php-base + image: ddev/ddev-php-base:v1.24.10-old profiles: - dev working_dir: /app From 6b3e0b5f1d4227845b2a851a61dac557d01cfafd Mon Sep 17 00:00:00 2001 From: skifter Date: Wed, 25 Feb 2026 12:42:32 +0100 Subject: [PATCH 28/52] Debug --- .github/workflows/pr.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d2965bb9..00382b62 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -55,6 +55,12 @@ jobs: run: | docker compose run --rm --user 0 -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) php bash -lc 'chown -R ${HOST_UID}:${HOST_GID} /app' + - name: Debug compose + php + run: | + ls -la + docker compose config + docker compose run --rm php php -v + - name: Check that dependencies resolve. run: | rm -fr vendor/ From 18f91db1a67f9afbe8cf9b160cf1f55afce9f973 Mon Sep 17 00:00:00 2001 From: skifter Date: Wed, 25 Feb 2026 14:01:32 +0100 Subject: [PATCH 29/52] CI: reset pipeline --- CHANGELOG.md | 2 ++ compose.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ef51fa..cfe1d60f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- git actions check + - [PR-289](https://github.com/OS2Forms/os2forms/pull/289) Added required "Zoom control position" to map element diff --git a/compose.yaml b/compose.yaml index 0448b12f..a64bf5fb 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,6 +8,16 @@ services: working_dir: /app volumes: - ./:/app + environment: + COMPOSER_HOME: /tmp/composer + COMPOSER_CACHE_DIR: /tmp/composer-cache + entrypoint: + - bash + - -lc + - | + git config --global --add safe.directory /app + exec "$@" + - bash markdownlint: image: itkdev/markdownlint From eeba74bcb5045682dada63be5650329c7897cb70 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 9 Mar 2026 09:38:05 +0100 Subject: [PATCH 30/52] Ensure address is added to both digital post pdf and fjernprint pdf --- .../Os2formsDigitalPostSubscriber.php | 14 ++++++++------ .../src/Helper/WebformHelperSF1601.php | 13 ++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index 6efe2419..e0677a38 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -101,14 +101,16 @@ public function setDigitalPostContext(WebformSubmissionInterface $submission, Co public function getDigitalPostContext(WebformSubmissionInterface $submission): CompanyLookupResult|CprLookupResult|null { $key = $this->createSessionKeyFromSubmission($submission); - $digitalPostContext = $this->session->get($key); + return $this->session->get($key); + } - // We only need/use it once, so just remove it after fetching it. - if ($digitalPostContext) { - $this->session->remove($key); - } + /** + * Delete Digital Post context from the current request. + */ + public function deleteDigitalPostContext(WebformSubmissionInterface $submission): bool { + $key = $this->createSessionKeyFromSubmission($submission); - return $digitalPostContext; + return (bool) $this->session->remove($key); } /** diff --git a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php index e26e0b1b..dd869462 100644 --- a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php +++ b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php @@ -156,7 +156,7 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h $recipientIdentifierType = 'CPR'; } - $this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult); + $senderSettings = $this->settings->getSender(); $messageOptions = [ @@ -169,7 +169,13 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h WebformHandlerSF1601::SENDER_LABEL => $handlerMessageSettings[WebformHandlerSF1601::SENDER_LABEL], WebformHandlerSF1601::MESSAGE_HEADER_LABEL => $handlerMessageSettings[WebformHandlerSF1601::MESSAGE_HEADER_LABEL], ]; + + // Set flag indicating digital post context. + $this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult); $message = $this->meMoHelper->buildWebformSubmissionMessage($submission, $messageOptions, $handlerSettings, $lookupResult); + // Remove flag. + $this->digitalPostSubscriber->deleteDigitalPostContext($submission); + $forsendelseOptions = [ self::RECIPIENT_IDENTIFIER_TYPE => $recipientIdentifierType, self::RECIPIENT_IDENTIFIER => $recipientIdentifier, @@ -181,7 +187,12 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h WebformHandlerSF1601::SENDER_LABEL => $handlerMessageSettings[WebformHandlerSF1601::SENDER_LABEL], WebformHandlerSF1601::MESSAGE_HEADER_LABEL => $handlerMessageSettings[WebformHandlerSF1601::MESSAGE_HEADER_LABEL], ]; + + // Set flag indicating digital post context. + $this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult); $forsendelse = $this->forsendelseHelper->buildSubmissionForsendelse($submission, $forsendelseOptions, $handlerSettings, $lookupResult); + // Remove flag. + $this->digitalPostSubscriber->deleteDigitalPostContext($submission); $type = $handlerMessageSettings[WebformHandlerSF1601::TYPE] ?? SF1601::TYPE_DIGITAL_POST; From 5fdc5d4d44bc2605c4b54ab44f22609724214b3e Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 12 Mar 2026 14:12:11 +0100 Subject: [PATCH 31/52] Adjusted when context is set and removed --- .../os2forms_digital_post.services.yml | 3 ++- .../src/Helper/AbstractMessageHelper.php | 14 +++++++++++++- .../src/Helper/ForsendelseHelper.php | 2 +- .../src/Helper/MeMoHelper.php | 5 +++-- .../src/Helper/WebformHelperSF1601.php | 14 -------------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/modules/os2forms_digital_post/os2forms_digital_post.services.yml b/modules/os2forms_digital_post/os2forms_digital_post.services.yml index 32fdb740..aefb2d97 100644 --- a/modules/os2forms_digital_post/os2forms_digital_post.services.yml +++ b/modules/os2forms_digital_post/os2forms_digital_post.services.yml @@ -21,12 +21,14 @@ services: - "@Drupal\\os2forms_digital_post\\Helper\\Settings" - "@plugin.manager.element_info" - "@webform.token_manager" + - "@Drupal\\os2forms_digital_post\\EventSubscriber\\Os2formsDigitalPostSubscriber" Drupal\os2forms_digital_post\Helper\ForsendelseHelper: arguments: - "@Drupal\\os2forms_digital_post\\Helper\\Settings" - "@plugin.manager.element_info" - "@webform.token_manager" + - "@Drupal\\os2forms_digital_post\\EventSubscriber\\Os2formsDigitalPostSubscriber" Drupal\os2forms_digital_post\Helper\DigitalPostHelper: arguments: @@ -52,7 +54,6 @@ services: - "@logger.channel.os2forms_digital_post" - "@logger.channel.os2forms_digital_post_submission" - "@Drupal\\os2forms_digital_post\\Helper\\DigitalPostHelper" - - "@Drupal\\os2forms_digital_post\\EventSubscriber\\Os2formsDigitalPostSubscriber" Drupal\os2forms_digital_post\Helper\SF1461Helper: diff --git a/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php b/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php index c2f2b990..0746f20e 100644 --- a/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php +++ b/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php @@ -4,9 +4,12 @@ use DigitalPost\MeMo\Message; use Drupal\Core\Render\ElementInfoManager; +use Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber; use Drupal\os2forms_digital_post\Exception\InvalidAttachmentElementException; use Drupal\os2forms_digital_post\Model\Document; use Drupal\os2forms_digital_post\Plugin\WebformHandler\WebformHandlerSF1601; +use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult; +use Drupal\os2web_datalookup\LookupResult\CprLookupResult; use Drupal\webform\WebformSubmissionInterface; use Drupal\webform\WebformTokenManagerInterface; use Drupal\webform_attachment\Element\WebformAttachmentBase; @@ -28,6 +31,7 @@ public function __construct( readonly protected ElementInfoManager $elementInfoManager, #[Autowire(service: 'webform.token_manager')] readonly protected WebformTokenManagerInterface $webformTokenManager, + readonly protected Os2formsDigitalPostSubscriber $digitalPostSubscriber, ) { } @@ -38,7 +42,7 @@ public function __construct( * * @phpstan-param array $handlerSettings */ - protected function getMainDocument(WebformSubmissionInterface $submission, array $handlerSettings): Document { + protected function getMainDocument(WebformSubmissionInterface $submission, array $handlerSettings, CprLookupResult|CompanyLookupResult $recipientData): Document { // Lifted from Drupal\webform_attachment\Controller\WebformAttachmentController::download. $element = $handlerSettings[WebformHandlerSF1601::MEMO_MESSAGE][WebformHandlerSF1601::ATTACHMENT_ELEMENT]; $element = $submission->getWebform()->getElement($element) ?: []; @@ -51,7 +55,15 @@ protected function getMainDocument(WebformSubmissionInterface $submission, array $fileName = $instance::getFileName($element, $submission); $mimeType = $instance::getFileMimeType($element, $submission); + + // The way to alter html generated from entities is through the + // PrintEvents::POST_RENDER event. See: + // @Drupal\entity_print\Renderer::generateHtml, + // To indicate digital post context and get the necessary information, + // we add a flag to the session. + $this->digitalPostSubscriber->setDigitalPostContext($submission, $recipientData); $content = $instance::getFileContent($element, $submission); + $this->digitalPostSubscriber->deleteDigitalPostContext($submission); return new Document( $content, diff --git a/modules/os2forms_digital_post/src/Helper/ForsendelseHelper.php b/modules/os2forms_digital_post/src/Helper/ForsendelseHelper.php index 42cbbea1..6254b63a 100644 --- a/modules/os2forms_digital_post/src/Helper/ForsendelseHelper.php +++ b/modules/os2forms_digital_post/src/Helper/ForsendelseHelper.php @@ -61,7 +61,7 @@ public function buildForsendelse(CprLookupResult|CompanyLookupResult $recipientD */ public function buildSubmissionForsendelse(WebformSubmissionInterface $submission, array $options, array $handlerSettings, CprLookupResult|CompanyLookupResult $recipientData): ForsendelseI { $label = $this->replaceTokens($options[WebformHandlerSF1601::MESSAGE_HEADER_LABEL], $submission); - $document = $this->getMainDocument($submission, $handlerSettings); + $document = $this->getMainDocument($submission, $handlerSettings, $recipientData); return $this->buildForsendelse($recipientData, $label, $document); } diff --git a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php index f32c567d..b3b66749 100644 --- a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php +++ b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php @@ -97,10 +97,11 @@ public function buildMessage(CprLookupResult|CompanyLookupResult $recipientData, * @phpstan-param array $options * @phpstan-param array $handlerSettings */ - public function buildWebformSubmissionMessage(WebformSubmissionInterface $submission, array $options, array $handlerSettings, CprLookupResult|CompanyLookupResult|null $recipientData = NULL): Message { + public function buildWebformSubmissionMessage(WebformSubmissionInterface $submission, array $options, array $handlerSettings, CprLookupResult|CompanyLookupResult $recipientData): Message { $senderLabel = $this->replaceTokens($options[WebformHandlerSF1601::SENDER_LABEL], $submission); $messageLabel = $this->replaceTokens($options[WebformHandlerSF1601::MESSAGE_HEADER_LABEL], $submission); - $document = $this->getMainDocument($submission, $handlerSettings); + $document = $this->getMainDocument($submission, $handlerSettings, $recipientData); + $actions = []; if (isset($handlerSettings[WebformHandlerSF1601::MEMO_ACTIONS]['actions'])) { foreach ($handlerSettings[WebformHandlerSF1601::MEMO_ACTIONS]['actions'] as $spec) { diff --git a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php index dd869462..3d079fe1 100644 --- a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php +++ b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php @@ -2,15 +2,12 @@ namespace Drupal\os2forms_digital_post\Helper; -use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult; -use Drupal\os2web_datalookup\LookupResult\CprLookupResult; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\advancedqueue\Entity\QueueInterface; use Drupal\advancedqueue\Job; use Drupal\advancedqueue\JobResult; -use Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber; use Drupal\os2forms_digital_post\Exception\InvalidRecipientIdentifierElementException; use Drupal\os2forms_digital_post\Exception\RuntimeException; use Drupal\os2forms_digital_post\Exception\SubmissionNotFoundException; @@ -65,7 +62,6 @@ public function __construct( #[Autowire(service: 'logger.channel.os2forms_digital_post_submission')] private readonly LoggerChannelInterface $submissionLogger, private readonly DigitalPostHelper $digitalPostHelper, - private readonly Os2formsDigitalPostSubscriber $digitalPostSubscriber, ) { $this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission'); $this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue'); @@ -156,8 +152,6 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h $recipientIdentifierType = 'CPR'; } - - $senderSettings = $this->settings->getSender(); $messageOptions = [ self::RECIPIENT_IDENTIFIER_TYPE => $recipientIdentifierType, @@ -170,11 +164,7 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h WebformHandlerSF1601::MESSAGE_HEADER_LABEL => $handlerMessageSettings[WebformHandlerSF1601::MESSAGE_HEADER_LABEL], ]; - // Set flag indicating digital post context. - $this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult); $message = $this->meMoHelper->buildWebformSubmissionMessage($submission, $messageOptions, $handlerSettings, $lookupResult); - // Remove flag. - $this->digitalPostSubscriber->deleteDigitalPostContext($submission); $forsendelseOptions = [ self::RECIPIENT_IDENTIFIER_TYPE => $recipientIdentifierType, @@ -188,11 +178,7 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h WebformHandlerSF1601::MESSAGE_HEADER_LABEL => $handlerMessageSettings[WebformHandlerSF1601::MESSAGE_HEADER_LABEL], ]; - // Set flag indicating digital post context. - $this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult); $forsendelse = $this->forsendelseHelper->buildSubmissionForsendelse($submission, $forsendelseOptions, $handlerSettings, $lookupResult); - // Remove flag. - $this->digitalPostSubscriber->deleteDigitalPostContext($submission); $type = $handlerMessageSettings[WebformHandlerSF1601::TYPE] ?? SF1601::TYPE_DIGITAL_POST; From ed4c707a00880cfb7bfa5613de0cb0d25026fc7a Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Fri, 13 Mar 2026 14:19:21 +0100 Subject: [PATCH 32/52] Added option to add sender address to digital post --- CHANGELOG.md | 1 + .../Os2formsDigitalPostSubscriber.php | 19 ++++++++++++++----- .../src/Helper/AbstractMessageHelper.php | 3 ++- .../WebformHandler/WebformHandlerSF1601.php | 9 +++++++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 200bbc16..b0e40249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ before starting to add changes. Use example [placed in the end of the page](#exa - [PR-301](https://github.com/OS2Forms/os2forms/pull/301) Add address information to Digital Post shipments to ensure "*fjernprint*" can be sent. +- Add option to add sender (address) to Digital Post shipments. ## [5.0.0] 2025-11-18 diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index e0677a38..dd17545a 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -31,7 +31,9 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { $submission = $event->getEntities()[0]; if ($submission instanceof WebformSubmissionInterface) { // Check whether generation is for digital post. - if ($lookupResult = $this->getDigitalPostContext($submission)) { + if ($context = $this->getDigitalPostContext($submission)) { + $lookupResult = $context['lookupResult']; + $senderAddress = $context['senderAddress']; // Combine address parts. $streetAddress = $lookupResult->getStreet(); @@ -52,7 +54,11 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { } // Generate address HTML. - $addressHtml = '
'; + $addressHtml = '
'; + if (!empty($senderAddress)) { + $addressHtml .= '
' . htmlspecialchars($senderAddress) . '
'; + } + $addressHtml .= '
'; $addressHtml .= '
' . htmlspecialchars($lookupResult->getName()) . '
'; if ($lookupResult instanceof CprLookupResult && $lookupResult->getCoName()) { $addressHtml .= '
c/o ' . htmlspecialchars($lookupResult->getCoName()) . '
'; @@ -90,15 +96,18 @@ public static function getSubscribedEvents(): array { /** * Indicate Digital Post context in the current session. */ - public function setDigitalPostContext(WebformSubmissionInterface $submission, CompanyLookupResult|CprLookupResult $lookupResult): void { + public function setDigitalPostContext(WebformSubmissionInterface $submission, CompanyLookupResult|CprLookupResult $lookupResult, string $senderAddress = ''): void { $key = $this->createSessionKeyFromSubmission($submission); - $this->session->set($key, $lookupResult); + $this->session->set($key, [ + 'lookupResult' => $lookupResult, + 'senderAddress' => $senderAddress, + ]); } /** * Check for Digital Post context in the current session. */ - public function getDigitalPostContext(WebformSubmissionInterface $submission): CompanyLookupResult|CprLookupResult|null { + public function getDigitalPostContext(WebformSubmissionInterface $submission): ?array { $key = $this->createSessionKeyFromSubmission($submission); return $this->session->get($key); diff --git a/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php b/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php index 0746f20e..368ac1ac 100644 --- a/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php +++ b/modules/os2forms_digital_post/src/Helper/AbstractMessageHelper.php @@ -61,7 +61,8 @@ protected function getMainDocument(WebformSubmissionInterface $submission, array // @Drupal\entity_print\Renderer::generateHtml, // To indicate digital post context and get the necessary information, // we add a flag to the session. - $this->digitalPostSubscriber->setDigitalPostContext($submission, $recipientData); + $senderAddress = $handlerSettings[WebformHandlerSF1601::MEMO_MESSAGE][WebformHandlerSF1601::SENDER_ADDRESS] ?? ''; + $this->digitalPostSubscriber->setDigitalPostContext($submission, $recipientData, $senderAddress); $content = $instance::getFileContent($element, $submission); $this->digitalPostSubscriber->deleteDigitalPostContext($submission); diff --git a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php index 0b1dc219..bba377a2 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -31,6 +31,7 @@ final class WebformHandlerSF1601 extends WebformHandlerBase { public const MESSAGE_HEADER_LABEL = 'message_header_label'; public const RECIPIENT_ELEMENT = 'recipient_element'; public const ATTACHMENT_ELEMENT = 'attachment_element'; + public const SENDER_ADDRESS = 'sender_address'; /** * Maximum length of sender label. @@ -131,6 +132,14 @@ public function buildConfigurationForm(array $form, FormStateInterface $formStat '#maxlength' => self::MESSAGE_HEADER_LABEL_MAX_LENGTH, ]; + $form[self::MEMO_MESSAGE][self::SENDER_ADDRESS] = [ + '#type' => 'textfield', + '#title' => $this->t('Sender address'), + '#description' => $this->t('Optional sender address shown on the printed document. Displayed as a single line above the recipient name.'), + '#required' => FALSE, + '#default_value' => $this->configuration[self::MEMO_MESSAGE][self::SENDER_ADDRESS] ?? NULL, + ]; + $form[self::MEMO_ACTIONS] = [ '#type' => 'fieldset', '#title' => $this->t('Actions'), From 8fbeeaa3f79ccd7583b6d699be824b2bf819e344 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Fri, 13 Mar 2026 14:20:51 +0100 Subject: [PATCH 33/52] Updated README with styling of sender adress --- modules/os2forms_digital_post/README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index a8f5f638..d9642356 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -218,7 +218,7 @@ footer { } // Style the h-card div -#envelope-window-digital-post > div { +#envelope-window-digital-post > .h-card { position: absolute; top: 16mm; left: 4mm; @@ -227,5 +227,15 @@ footer { width: $recipient-window-width; } +// Style the sender address div +#envelope-window-digital-post > #sender-address-digital-post { + position: absolute; + top: 12mm; + left: 4mm; + font-size: 8px; + height: 4mm; + width: 71mm; +} + // More custom styling... ``` From 381f438bd856a0d9f25ac1410b7eec4f9873c5be Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 16 Mar 2026 10:35:29 +0100 Subject: [PATCH 34/52] Added max length for sender and checks for context being correctly set --- .../Os2formsDigitalPostSubscriber.php | 12 ++++++++++-- .../Plugin/WebformHandler/WebformHandlerSF1601.php | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index dd17545a..836714e0 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -32,8 +32,16 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { if ($submission instanceof WebformSubmissionInterface) { // Check whether generation is for digital post. if ($context = $this->getDigitalPostContext($submission)) { - $lookupResult = $context['lookupResult']; - $senderAddress = $context['senderAddress']; + $lookupResult = $context['lookupResult'] ?? NULL; + $senderAddress = $context['senderAddress'] ?? ''; + + if (!$lookupResult instanceof CprLookupResult && !$lookupResult instanceof CompanyLookupResult) { + return; + } + + if (!is_string($senderAddress)) { + $senderAddress = ''; + } // Combine address parts. $streetAddress = $lookupResult->getStreet(); diff --git a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php index bba377a2..6c0e59e8 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -38,6 +38,11 @@ final class WebformHandlerSF1601 extends WebformHandlerBase { */ private const SENDER_LABEL_MAX_LENGTH = 64; + /** + * Maximum length of sender address. + */ + private const SENDER_ADDRESS_MAX_LENGTH = 70; + /** * Maximum length of header label. */ @@ -135,9 +140,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $formStat $form[self::MEMO_MESSAGE][self::SENDER_ADDRESS] = [ '#type' => 'textfield', '#title' => $this->t('Sender address'), - '#description' => $this->t('Optional sender address shown on the printed document. Displayed as a single line above the recipient name.'), + '#description' => $this->t('Optional sender address shown on the printed document. Displayed as a single line above the recipient name. Maximum 70 characters.'), '#required' => FALSE, '#default_value' => $this->configuration[self::MEMO_MESSAGE][self::SENDER_ADDRESS] ?? NULL, + '#maxlength' => self::SENDER_ADDRESS_MAX_LENGTH, ]; $form[self::MEMO_ACTIONS] = [ From ee98943bdd6e570f7aab2a4a71cfb5cbbceab856 Mon Sep 17 00:00:00 2001 From: Jeppe Kuhlmann Andersen <78410897+jekuaitk@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:37:39 +0100 Subject: [PATCH 35/52] Update modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php Co-authored-by: Mikkel Ricky --- .../src/EventSubscriber/Os2formsDigitalPostSubscriber.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index 836714e0..e4b686bb 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -33,12 +33,11 @@ public function onPrintRender(PrintHtmlAlterEvent $event): void { // Check whether generation is for digital post. if ($context = $this->getDigitalPostContext($submission)) { $lookupResult = $context['lookupResult'] ?? NULL; - $senderAddress = $context['senderAddress'] ?? ''; - if (!$lookupResult instanceof CprLookupResult && !$lookupResult instanceof CompanyLookupResult) { return; } + $senderAddress = $context['senderAddress'] ?? ''; if (!is_string($senderAddress)) { $senderAddress = ''; } From 5a644a1b1f48a84d96d680d1dc80e9b105b20e9f Mon Sep 17 00:00:00 2001 From: Jeppe Kuhlmann Andersen <78410897+jekuaitk@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:37:49 +0100 Subject: [PATCH 36/52] Update modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php Co-authored-by: Mikkel Ricky --- .../src/Plugin/WebformHandler/WebformHandlerSF1601.php | 2 +- 1 file changed, 1 insertion(+), 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 6c0e59e8..6f0e06a9 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -140,7 +140,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $formStat $form[self::MEMO_MESSAGE][self::SENDER_ADDRESS] = [ '#type' => 'textfield', '#title' => $this->t('Sender address'), - '#description' => $this->t('Optional sender address shown on the printed document. Displayed as a single line above the recipient name. Maximum 70 characters.'), + '#description' => $this->t('Optional sender address shown on the printed document. Displayed as a single line above the recipient name. Maximum @max characters.', ['@max' => self::SENDER_ADDRESS_MAX_LENGTH]), '#required' => FALSE, '#default_value' => $this->configuration[self::MEMO_MESSAGE][self::SENDER_ADDRESS] ?? NULL, '#maxlength' => self::SENDER_ADDRESS_MAX_LENGTH, From 4335ee35b28aa383ab298fd1c83cdd4021c9d49f Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 16 Mar 2026 12:55:51 +0100 Subject: [PATCH 37/52] Updated digital post README with sender address --- modules/os2forms_digital_post/README.md | 32 ++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index d9642356..471bd6e6 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -96,17 +96,23 @@ also means that it is up to individual installations to style it correctly. This should be done in OS2Forms Attachment-templates, see [Overwriting templates](https://github.com/OS2Forms/os2forms/tree/develop/modules/os2forms_attachment#overwriting-templates). -To see the exact requirements for address placement, see +Furthermore, a single-line sender address may be configured on the handler. +The value of this field will be injected into the HTML as a sender address, +which should be placed within the envelope window just above the recipient +address. As with the recipient information, it is up to individual +installations to style it correctly. + +To see the exact requirements for address and sender placement, see [digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf](docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf). ### The injected HTML -Variations of the injected HTML include extended addresses and c/o. +Variations of the injected HTML include extended addresses, c/o and sender +address. -Without extended address information or c/o: +Without extended address information, c/o or sender address: ```html -
Jeppe
@@ -131,7 +137,6 @@ With just an extended address: With just c/o: ```html -
Jeppe
@@ -141,10 +146,25 @@ With just c/o:
``` -With extended address information and c/o: +With just the sender address: + +```html +
+
Dokk1, Hack Kampmanns Plads 2, 8000 Aarhus C
+
+
Jeppe
+
Test vej HouseNr
+
2100 Copenhagen
+
+
+``` + + +With extended address information, c/o and sender address: ```html
+
Dokk1, Hack Kampmanns Plads 2, 8000 Aarhus C
Jeppe
c/o Mikkel
From e0794f6a0a5ea9d758a39ab6a2938a73dc82c2b7 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Mar 2026 09:49:52 +0100 Subject: [PATCH 38/52] Updated CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e40249..ff58d468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ before starting to add changes. Use example [placed in the end of the page](#exa - [PR-301](https://github.com/OS2Forms/os2forms/pull/301) Add address information to Digital Post shipments to ensure "*fjernprint*" can be sent. -- Add option to add sender (address) to Digital Post shipments. +- Add option to add return address to Digital Post shipments. ## [5.0.0] 2025-11-18 From e7c58e52789b0139a4a753e5c7eb8effa5185ddc Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Mar 2026 12:50:56 +0100 Subject: [PATCH 39/52] Updated Digital Post README --- modules/os2forms_digital_post/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index 471bd6e6..e70143cb 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -159,7 +159,6 @@ With just the sender address:
``` - With extended address information, c/o and sender address: ```html From 929fd9584cd5d513767435631e7b25cf742bfa8f Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Mar 2026 13:00:07 +0100 Subject: [PATCH 40/52] Fixed code analysis script --- scripts/code-analysis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/code-analysis b/scripts/code-analysis index ace9e282..a0bdb083 100755 --- a/scripts/code-analysis +++ b/scripts/code-analysis @@ -40,7 +40,7 @@ drupal_composer config --no-plugins allow-plugins true # Making Drupal 10 compatible drupal_composer require psr/http-message:^1.0 -drupal_composer require mglaman/composer-drupal-lenient +drupal_composer require mglaman/composer-drupal-lenient:^1.0 drupal_composer config --merge --json extra.drupal-lenient.allowed-list '["drupal/coc_forms_auto_export", "drupal/webform_node_element"]' drupal_composer require wikimedia/composer-merge-plugin From 5092830c44c0be9937cbdd9516ce58afb599f4bb Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Mar 2026 13:02:13 +0100 Subject: [PATCH 41/52] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69034355..f5613cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-317](https://github.com/OS2Forms/os2forms/pull/317) + Updated code analysis script. - [PR-306](https://github.com/OS2Forms/os2forms/pull/306) Made digital signature text placement configurable. - [#251](https://github.com/OS2Forms/os2forms/issues/251) From 54d995051b960e2e15d7c97cf184e30acf67d240 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Mar 2026 13:38:23 +0100 Subject: [PATCH 42/52] Added missing array return declaration --- .../src/EventSubscriber/Os2formsDigitalPostSubscriber.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index e4b686bb..ab6c4d04 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -113,6 +113,8 @@ public function setDigitalPostContext(WebformSubmissionInterface $submission, Co /** * Check for Digital Post context in the current session. + * + * @return array|null */ public function getDigitalPostContext(WebformSubmissionInterface $submission): ?array { $key = $this->createSessionKeyFromSubmission($submission); From 579d11e77de09cdd7fb636f96ca34e7bd9ef9e9e Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Mar 2026 13:53:07 +0100 Subject: [PATCH 43/52] Applied coding standards --- .../src/EventSubscriber/Os2formsDigitalPostSubscriber.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php index ab6c4d04..647aacc7 100644 --- a/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php +++ b/modules/os2forms_digital_post/src/EventSubscriber/Os2formsDigitalPostSubscriber.php @@ -115,6 +115,8 @@ public function setDigitalPostContext(WebformSubmissionInterface $submission, Co * Check for Digital Post context in the current session. * * @return array|null + * - 'lookupResult': the lookup result + * - 'senderAddress': the sender address */ public function getDigitalPostContext(WebformSubmissionInterface $submission): ?array { $key = $this->createSessionKeyFromSubmission($submission); From f5fac0209841b66adcf45ada580fafbe63432667 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 23 Mar 2026 14:49:39 +0100 Subject: [PATCH 44/52] Code cleanup --- .../os2forms_digital_signature.module | 29 ++----------------- .../os2forms_digital_signature.services.yml | 13 --------- 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.module b/modules/os2forms_digital_signature/os2forms_digital_signature.module index 35a72ef6..a3618fda 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.module +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -63,24 +63,14 @@ function os2forms_digital_signature_file_download($uri) { $allowedIpsArr = array_filter($allowedIpsArr); $remoteIp = \Drupal::request()->getClientIp(); - // IP list is empty, allow access. - if (empty($allowedIpsArr)) { + // Check if remote IP matches any allowed IP or CIDR range. + if (empty($allowedIpsArr) || IpUtils::checkIp($remoteIp, $allowedIpsArr)) { $basename = basename($uri); return [ 'Content-disposition' => 'attachment; filename="' . $basename . '"', ]; } - // Check if remote IP matches any allowed IP or CIDR range. - foreach ($allowedIpsArr as $allowedIp) { - if ($remoteIp === $allowedIp || os2forms_digital_signature_ip_in_cidr($remoteIp, $allowedIp)) { - $basename = basename($uri); - return [ - 'Content-disposition' => 'attachment; filename="' . $basename . '"', - ]; - } - } - // Deny access and log warning. \Drupal::logger('os2forms_digital_signature')->warning('File download denied for IP @ip on URI @uri. Allowed IPs: @allowed', [ '@ip' => $remoteIp, @@ -94,18 +84,3 @@ function os2forms_digital_signature_file_download($uri) { // Not submission file, allow normal access. return NULL; } - -/** - * Check if an IP address is within a CIDR range. - * - * @param string $ip - * The IP address to check. - * @param string $cidr - * The CIDR range (e.g. "172.16.0.0/16"). - * - * @return bool - * TRUE if the IP is within the CIDR range, FALSE otherwise. - */ -function os2forms_digital_signature_ip_in_cidr(string $ip, string $cidr): bool { - return IpUtils::checkIp($ip, $cidr); -} diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml index 47d0fd68..33848830 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml @@ -11,16 +11,3 @@ services: - '@config.factory' - '@entity_type.manager' - '@logger.channel.os2forms_digital_signature' -services: - logger.channel.os2forms_digital_signature: - parent: logger.channel_base - arguments: [ 'os2forms_digital_signature' ] - - os2forms_digital_signature.signing_service: - class: Drupal\os2forms_digital_signature\Service\SigningService - arguments: - - '@http_client' - - '@datetime.time' - - '@config.factory' - - '@entity_type.manager' - - '@logger.channel.os2forms_digital_signature' From 65e419a7686a665f5195788754e827343883a885 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 31 Mar 2026 16:11:15 +0200 Subject: [PATCH 45/52] Update README --- README.md | 91 +++++++++++++++++++++++++-------------------------- composer.json | 2 +- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index f4a52e28..13ff01fb 100644 --- a/README.md +++ b/README.md @@ -2,42 +2,31 @@ ## Install -OS2Forms Drupal 8 module is available to download via composer. +OS2Forms Drupal 10 module is available to download via composer. ```sh composer require os2forms/os2forms -drush en os2forms +drush pm:install os2forms ``` If you don't have Drupal installed on you server, you will to need install it first. -Read more about [how to install drupal core](https://www.drupal.org/docs/8/install). +Read more about [how to install drupal core](https://www.drupal.org/docs/getting-started/installing-drupal). -We are recommending to install drupal via composer by using -[OS2Forms composer project](https://github.com/OS2Forms/composer-project). -By this way you will get standalone project with OS2Forms module on board, plus -all the other contrib modules you will probably need to configure OS2Forms to -your specific demands. - -```sh -composer create-project os2forms/composer-project:8.x-dev some-dir --no-interaction -``` - -To get more benefits on your Drupal project we are offering you to use +To get more benefits on your Drupal project we recommend you use [OS2web](https://packagist.org/packages/os2web/os2web) as installation -profile for Drupal. This profile is a part of OS2Forms composer project -mentioned above. +profile for Drupal. You can easy download and install OS2web installation profile to your composer based Drupal project with commands: ```sh composer require os2web/os2web -drush si os2web --db-url=mysql://db_user:db_pass@mysql_host/db_name --locale=da --site-name="OS2Forms" --account-pass=admin -y +drush site:install os2web --db-url=mysql://db_user:db_pass@mysql_host/db_name --locale=da --site-name="OS2Forms" --account-pass=admin -y ``` ## Update -Updating process for OS2forms module is similar to usual Drupal 8 module. +Updating process for OS2forms module is similar to usual Drupal 10 module. Use Composer's built-in command for listing packages that have updates available: ```sh @@ -46,53 +35,62 @@ composer outdated os2forms/os2forms ## Automated testing and code quality -See [OS2Forms testing and CI information](https://github.com/OS2Forms/docs#testing-and-ci) +See [OS2Forms testing and CI information](https://os2forms.github.io/os2forms-docs/for-developers.html#testing-and-ci) ## Contribution -OS2Forms project is opened for new features and os course bugfixes. -If you have any suggestion or you found a bug in project, you are very welcome -to create an issue in github repository issue tracker. -For issue description there is expected that you will provide clear and -sufficient information about your feature request or bug report. +The OS2Forms project is open for new features and bugfixes. If you have any +suggestion, or you found a bug in the project, you are very welcome to create +an issue in github repository issue tracker. For issue description was ask +that you will provide clear and sufficient information about your feature +request or bug report. ### Code review policy -See [OS2Forms code review policy](https://github.com/OS2Forms/docs#code-review) +See [OS2Forms code review policy](https://os2forms.github.io/os2forms-docs/for-developers.html#code-review) ### Git name convention -See [OS2Forms git name convention](https://github.com/OS2Forms/docs#git-guideline) +See [OS2Forms git name convention](https://os2forms.github.io/os2forms-docs/for-developers.html#git-guideline) ## Important notes ### Webforms -Each webform, including all its settings, is stored as configuration in db and -will(could) be exported as `yml` file via Drupal configuration management -system. And afterwards could be tracked by `git`. +Each webform, along with all its settings, is stored as configuration in the +database and can be exported as a `yml` file through Drupal's configuration +management system, making it trackable via `git`. -It means that all webform settings from drupal database will -be syncronized (exported/imported) with state stored in `yml` files from -configuration folder stored in git repository. Without proper actions webforms -could be deleted or reverted to state in `yml` during synchronization. +This means that webform settings in the Drupal database will be synchronized +(exported/imported) with the state defined in `yml` files located in the +configuration folder of your git repository. Without taking the appropriate +precautions, webforms may be deleted or reverted to the state captured in +those `yml` files during synchronization. -To avoid/prevent this behavior we recommend use `Config ignore` module, where -you can add all settings you do not want to export/import via configuration -management system. +To prevent this, we recommend using the +`Config ignore`-[module](https://www.drupal.org/project/config_ignore), which +allows you to exclude specific settings from the configuration management +export/import process. ### Serviceplatformen plugins -Settings for CPR and CVR serviceplantormen plugins are storing as configuration -in db and will(could) be exported as `yml` file via Drupal configuration -management system. And afterwards could be tracked by `git`. +Similar to webforms, settings for the CPR and CVR Serviceplatformen plugins +are stored as configuration in the database and can be exported as `yml` files +through Drupal's configuration management system, making them trackable via +`git`. + +Note that if your git repository is publicly accessible, these plugin settings +— which may contain sensitive information — will be exposed. As with webforms, +we recommend using the `Config ignore`-module to exclude them from the +export/import process. -If case you have public access to your git repository all setting from plugins -will be exposed for third persons. +### Other configuration -To avoid/prevent this behavior we recommend use `Config ignore` module, where -you can add all settings you do not want to export/import via configuration -management system. +The two cases above are just some examples of configuration that may be +sensitive or subject to unintended changes during synchronization. In general, +any configuration that is environment-specific, contains sensitive data, or is +managed directly in the database rather than through code should be considered +for exclusion via the `Config ignore`-module. ## Unstable features @@ -148,11 +146,12 @@ docker compose run --rm markdownlint markdownlint '**/*.md' We use [PHPStan](https://phpstan.org/) for static code analysis. -Running statis code analysis on a standalone Drupal module is a bit tricky, so we use a helper script to run the +Running static code analysis on a standalone Drupal module is a bit tricky, so we use a helper script to run the analysis: ```shell docker compose run --rm php ./scripts/code-analysis ``` -**Note**: Currently the code analysis is only run on the `os2forms_digital_post` sub-module (cf. [`phpstan.neon`](./phpstan.neon)). +**Note**: Currently the code analysis is only run on the `os2forms_digital_post` and `os2forms_fbs_handler` sub-modules +(cf. [`phpstan.neon`](./phpstan.neon)). diff --git a/composer.json b/composer.json index e2413ffe..ba5f18b2 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "os2forms/os2forms", - "description": "Drupal 8 OS2Form module provides advanced webform functionality for Danish Municipalities", + "description": "Drupal 10 OS2Form module provides advanced webform functionality for Danish Municipalities", "license": "EUPL-1.2", "type": "drupal-module", "require": { From f1531f7e525653e5a9957030cd732a6951920ff6 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 31 Mar 2026 16:15:14 +0200 Subject: [PATCH 46/52] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f5f248..ababfd3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-320](https://github.com/OS2Forms/os2forms/pull/320) + Update README. - [PR-301](https://github.com/OS2Forms/os2forms/pull/301) Add address information to Digital Post shipments to ensure "*fjernprint*" can be sent. From c3f8309b33515ccc656587b7019d38471fbe50e9 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Wed, 8 Apr 2026 13:27:03 +0200 Subject: [PATCH 47/52] Adjust Digital Post error messages --- CHANGELOG.md | 3 ++- .../os2forms_digital_post/src/Helper/WebformHelperSF1601.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ababfd3d..8a62b88b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -424,7 +424,8 @@ f/OS-115_dawa_address - Security in case of vulnerabilities. ``` -[Unreleased]: https://github.com/OS2Forms/os2forms/compare/4.1.0...HEAD +[Unreleased]: https://github.com/OS2Forms/os2forms/compare/5.0.0...HEAD +[5.0.0]: https://github.com/OS2Forms/os2forms/compare/4.1.0...5.0.0 [4.1.0]: https://github.com/OS2Forms/os2forms/compare/4.0.0...4.1.0 [4.0.0]: https://github.com/OS2Forms/os2forms/compare/3.22.2...4.0.0 [3.22.2]: https://github.com/OS2Forms/os2forms/compare/3.22.1...3.22.2 diff --git a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php index 3d079fe1..807af274 100644 --- a/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php +++ b/modules/os2forms_digital_post/src/Helper/WebformHelperSF1601.php @@ -90,7 +90,7 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h $handlerMessageSettings = $handlerSettings[WebformHandlerSF1601::MEMO_MESSAGE]; $recipientIdentifierKey = $handlerMessageSettings[WebformHandlerSF1601::RECIPIENT_ELEMENT] ?? NULL; if (NULL === $recipientIdentifierKey) { - $message = 'Recipient identifier element (key: @element_key) not found in submission'; + $message = 'Recipient identifier element (key: @element_key) not found in handler settings'; $context = [ '@element_key' => WebformHandlerSF1601::RECIPIENT_ELEMENT, ]; @@ -115,7 +115,7 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h if (NULL === $recipientIdentifier) { $message = 'Recipient identifier element (key: @element_key) not found in submission'; $context = [ - '@element_key' => WebformHandlerSF1601::RECIPIENT_ELEMENT, + '@element_key' => $recipientIdentifierKey, ]; $this->error($message, $context); From 433a83a2eee5c20b2621df8c639add6fab2e4184 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Wed, 8 Apr 2026 13:37:53 +0200 Subject: [PATCH 48/52] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a62b88b..ff239913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-322](https://github.com/OS2Forms/os2forms/pull/322) + Update Digital Post handler error messages. - [PR-320](https://github.com/OS2Forms/os2forms/pull/320) Update README. - [PR-301](https://github.com/OS2Forms/os2forms/pull/301) From d0301c61c35705dc5f48bf9f3290b9e59fd9f40e Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 8 May 2026 12:44:21 +0300 Subject: [PATCH 49/52] #200 updating ckeditor -> ckeditor5 (#326) * #200 updating ckeditor -> ckeditor5 * #200 adding PR link into readme * #200 ignoring empty var * #200 ignoring empty var --- CHANGELOG.md | 2 ++ os2forms.info.yml | 1 - os2forms.install | 28 ++++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff239913..1f722099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-326](https://github.com/OS2Forms/os2forms/pull/326) + Updating ckeditor -> ckeditor5. - [PR-322](https://github.com/OS2Forms/os2forms/pull/322) Update Digital Post handler error messages. - [PR-320](https://github.com/OS2Forms/os2forms/pull/320) diff --git a/os2forms.info.yml b/os2forms.info.yml index d08e8e9c..d7e530d0 100644 --- a/os2forms.info.yml +++ b/os2forms.info.yml @@ -5,7 +5,6 @@ package: OS2Forms core_version_requirement: ^9 || ^10 dependencies: - - 'drupal:ckeditor' - 'drupal:editor' - 'drupal:entity_print' - 'drupal:eu_cookie_compliance' diff --git a/os2forms.install b/os2forms.install index b6085142..9d1becee 100644 --- a/os2forms.install +++ b/os2forms.install @@ -8,6 +8,7 @@ use Composer\InstalledVersions; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\filter\Entity\FilterFormat; use Drupal\taxonomy\Entity\Term; use Symfony\Component\Yaml\Yaml; @@ -237,3 +238,30 @@ function os2forms_update_103001() { function os2forms_update_103002() { \Drupal::service('module_installer')->install(['os2web_key']); } + +/** + * Implements hook_update_N(). + * + * Updating ckeditor -> ckeditor5. + */ +function os2forms_update_103003() { + \Drupal::service('module_installer')->install(['ckeditor5']); + + $editor_storage = \Drupal::entityTypeManager()->getStorage('editor'); + /** @var \Drupal\ckeditor5\SmartDefaultSettings $smart_defaults */ + $smart_defaults = \Drupal::service('ckeditor5.smart_default_settings'); + + foreach (FilterFormat::loadMultiple() as $format) { + /** @var \Drupal\editor\EditorInterface $editor */ + $editor = $editor_storage->load($format->id()); + if (!$editor || $editor->getEditor() !== 'ckeditor') { + continue; + } + + [$new_editor] = $smart_defaults + ->computeSmartDefaultSettings($editor, $format); + $new_editor->save(); + } + + \Drupal::service('module_installer')->uninstall(['ckeditor']); +} From dcc5155617d0d42bcfd6eccb3244c3b426f974d8 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 18 May 2026 17:19:26 +0300 Subject: [PATCH 50/52] OS-219 adding config object delete on uninstall --- CHANGELOG.md | 4 ++-- .../os2forms_encrypt/os2forms_encrypt.install | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f722099..cf8e8a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,11 +27,11 @@ before starting to add changes. Use example [placed in the end of the page](#exa Updated code analysis script. - [PR-306](https://github.com/OS2Forms/os2forms/pull/306) Made digital signature text placement configurable. -- [#251](https://github.com/OS2Forms/os2forms/issues/251) - Webform encrypt uninstall problem fix - git actions check - [PR-289](https://github.com/OS2Forms/os2forms/pull/289) Added required "Zoom control position" to map element +- [#251](https://github.com/OS2Forms/os2forms/issues/251) + Updated Webform encrypt uninstall problem fix ## [5.0.0] 2025-11-18 diff --git a/modules/os2forms_encrypt/os2forms_encrypt.install b/modules/os2forms_encrypt/os2forms_encrypt.install index 0a42e4ee..010722a4 100644 --- a/modules/os2forms_encrypt/os2forms_encrypt.install +++ b/modules/os2forms_encrypt/os2forms_encrypt.install @@ -17,3 +17,19 @@ function os2forms_encrypt_install() { module_set_weight('os2forms_encrypt', 9999); } + +/** + * Implements hook_uninstall(). + */ +function os2forms_encrypt_uninstall(): void { + $config_factory = \Drupal::configFactory(); + + $configs = [ + 'encrypt.profile.webform', + 'key.key.webform', + ]; + + foreach ($configs as $config_name) { + $config_factory->getEditable($config_name)->delete(); + } +} From 33106971df5dc6a84f2d0db7b11d072096e54835 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 29 May 2026 11:13:33 +0300 Subject: [PATCH 51/52] OS-244 #246 Adding Datafordeler address lookup (#327) --- CHANGELOG.md | 2 + .../js/dawa_address_autocomplete.js | 17 ++ .../os2forms_dawa/os2forms_dawa.libraries.yml | 7 + modules/os2forms_dawa/os2forms_dawa.module | 11 + .../os2forms_dawa/os2forms_dawa.services.yml | 4 - .../src/Controller/DawaElementController.php | 32 +-- .../Element/DawaElementAddressMatrikula.php | 19 +- .../src/Element/DawaElementBase.php | 25 +- .../src/Entity/DatafordelerMatrikula.php | 79 ------- .../os2forms_dawa/src/Entity/DawaAddress.php | 135 ----------- .../WebformElement/DawaElementAddress.php | 6 +- .../DawaElementAddressMatrikula.php | 6 +- .../WebformElement/DawaElementBlock.php | 4 +- .../WebformElement/DawaElementMatrikula.php | 4 +- .../DataLookup/DatafordelerDataLookup.php | 162 ------------- .../DatafordelerDataLookupInterface.php | 38 --- .../os2forms_dawa/src/Service/DawaService.php | 219 ------------------ 17 files changed, 76 insertions(+), 694 deletions(-) create mode 100644 modules/os2forms_dawa/js/dawa_address_autocomplete.js create mode 100644 modules/os2forms_dawa/os2forms_dawa.libraries.yml delete mode 100644 modules/os2forms_dawa/os2forms_dawa.services.yml delete mode 100644 modules/os2forms_dawa/src/Entity/DatafordelerMatrikula.php delete mode 100644 modules/os2forms_dawa/src/Entity/DawaAddress.php delete mode 100644 modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookup.php delete mode 100644 modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookupInterface.php delete mode 100644 modules/os2forms_dawa/src/Service/DawaService.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8e8a0c..edf75bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa - git actions check - [PR-289](https://github.com/OS2Forms/os2forms/pull/289) Added required "Zoom control position" to map element +- [#246](https://github.com/OS2Forms/os2forms/issues/246) + Adding Datafordeler address lookup - [#251](https://github.com/OS2Forms/os2forms/issues/251) Updated Webform encrypt uninstall problem fix diff --git a/modules/os2forms_dawa/js/dawa_address_autocomplete.js b/modules/os2forms_dawa/js/dawa_address_autocomplete.js new file mode 100644 index 00000000..12d0b053 --- /dev/null +++ b/modules/os2forms_dawa/js/dawa_address_autocomplete.js @@ -0,0 +1,17 @@ +(function ($, Drupal) { + // Overrides splitValues() function found in core/misc/autocomplete.js for + // DAWA elements. Danish addresses contain commas. Original splitValues() + // method splits upon commas. Combined with default extractLastTerm this + // results in unexpected behavior when using autocomplete with respect to + // danish addresses. Inspired by + // https://www.drupal.org/project/drupal/issues/2821181#comment-12012538. + Drupal.autocomplete.splitValues = function (value) { + // Check if the current autocomplete is for a focused DAWA address field + if ($('.ui-autocomplete-input:focus').closest('.os2forms-dawa-address').length) { + // For DAWA address fields, return the entire value as a single value + return [ value.trim() ]; + } + // For other fields, use the original behavior + return Drupal.autocomplete.splitValues(value); + }; +})(jQuery, Drupal); diff --git a/modules/os2forms_dawa/os2forms_dawa.libraries.yml b/modules/os2forms_dawa/os2forms_dawa.libraries.yml new file mode 100644 index 00000000..d2cf517b --- /dev/null +++ b/modules/os2forms_dawa/os2forms_dawa.libraries.yml @@ -0,0 +1,7 @@ +dawa_address_autocomplete: + version: 1.x + js: + js/dawa_address_autocomplete.js: {} + dependencies: + - core/jquery + - core/drupal diff --git a/modules/os2forms_dawa/os2forms_dawa.module b/modules/os2forms_dawa/os2forms_dawa.module index b6644c4d..f851fd9c 100644 --- a/modules/os2forms_dawa/os2forms_dawa.module +++ b/modules/os2forms_dawa/os2forms_dawa.module @@ -5,6 +5,8 @@ * OS2Forms Address autocomplete functionality module. */ +use Drupal\Core\Form\FormStateInterface; + /** * Implements hook_webform_migrate_d7_webform_element_ELEMENT_TYPE_alter(). * @@ -26,3 +28,12 @@ function os2forms_dawa_webform_migrate_d7_webform_element_address_autocomp_alter function os2forms_dawa_webform_migrate_d7_webform_element_addrs_autocomp_l_alter(&$markup, $indent, array $element) { $markup .= "$indent '#type': os2forms_dawa_address_matrikula\n"; } + +/** + * Implements hook_webform_element_alter(). + */ +function os2forms_dawa_webform_element_alter(array &$element, FormStateInterface $form_state, array $context) { + if ('os2forms_dawa_address' === $element['#type']) { + $element['#attached']['library'][] = 'os2forms_dawa/dawa_address_autocomplete'; + } +} diff --git a/modules/os2forms_dawa/os2forms_dawa.services.yml b/modules/os2forms_dawa/os2forms_dawa.services.yml deleted file mode 100644 index c8c63c95..00000000 --- a/modules/os2forms_dawa/os2forms_dawa.services.yml +++ /dev/null @@ -1,4 +0,0 @@ -services: - os2forms_dawa.service: - class: Drupal\os2forms_dawa\Service\DawaService - arguments: ['@plugin.manager.os2web_datalookup'] diff --git a/modules/os2forms_dawa/src/Controller/DawaElementController.php b/modules/os2forms_dawa/src/Controller/DawaElementController.php index 93787e68..5f513686 100644 --- a/modules/os2forms_dawa/src/Controller/DawaElementController.php +++ b/modules/os2forms_dawa/src/Controller/DawaElementController.php @@ -3,7 +3,7 @@ namespace Drupal\os2forms_dawa\Controller; use Drupal\Core\Controller\ControllerBase; -use Drupal\os2forms_dawa\Service\DawaService; +use Drupal\os2web_datalookup\Plugin\DataLookupManager; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -14,20 +14,20 @@ class DawaElementController extends ControllerBase { /** - * The DAWA service object. + * Datafordeler address lookup. * - * @var \Drupal\os2forms_dawa\Service\DawaService + * @var \Drupal\os2web_datalookup\Plugin\os2web\DataLookup\DatafordelerAddressLookupInterface */ - protected $dawaService; + protected $datafordelerAddressLookup; /** * Constructs a DawaElementController object. * - * @param \Drupal\os2forms_dawa\Service\DawaService $os2forms_dawa_service - * The DAWA service object. + * @param \Drupal\os2web_datalookup\Plugin\DataLookupManager $dataLookupManager + * Datalookup manager. */ - public function __construct(DawaService $os2forms_dawa_service) { - $this->dawaService = $os2forms_dawa_service; + public function __construct(DataLookupManager $dataLookupManager) { + $this->datafordelerAddressLookup = $dataLookupManager->createInstance('datafordeler_address_lookup'); } /** @@ -35,7 +35,7 @@ public function __construct(DawaService $os2forms_dawa_service) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('os2forms_dawa.service') + $container->get('plugin.manager.os2web_datalookup') ); } @@ -61,18 +61,8 @@ public function autocomplete(Request $request, $element_type) { $matches = []; // Get the matches based on the element type. - switch ($element_type) { - case 'os2forms_dawa_address': - $matches = $this->dawaService->getAddressMatches($query); - break; - - case 'os2forms_dawa_block': - $matches = $this->dawaService->getBlockMatches($query); - break; - - case 'os2forms_dawa_matrikula': - $matches = $this->dawaService->getMatrikulaMatches($query); - break; + if ($element_type == 'os2forms_dawa_address') { + $matches = $this->datafordelerAddressLookup->getAddressMatches($query); } return new JsonResponse($matches); diff --git a/modules/os2forms_dawa/src/Element/DawaElementAddressMatrikula.php b/modules/os2forms_dawa/src/Element/DawaElementAddressMatrikula.php index fdfc0ea9..bd6d3167 100644 --- a/modules/os2forms_dawa/src/Element/DawaElementAddressMatrikula.php +++ b/modules/os2forms_dawa/src/Element/DawaElementAddressMatrikula.php @@ -96,11 +96,14 @@ public static function getCompositeElements(array $element) { private static function getMatrikulaOptions($addressValue, array $element) { $options = []; - /** @var \Drupal\os2forms_dawa\Service\DawaService $dawaService */ - $dawaService = \Drupal::service('os2forms_dawa.service'); + /** @var \Drupal\os2web_datalookup\Plugin\DataLookupManager $datalookupManager */ + $datalookupManager = \Drupal::service('plugin.manager.os2web_datalookup'); - /** @var \Drupal\os2forms_dawa\Plugin\os2web\DataLookup\DatafordelerDataLookupInterface $datafordelerLookup */ - $datafordelerLookup = \Drupal::service('plugin.manager.os2web_datalookup')->createInstance('datafordeler_data_lookup'); + /** @var \Drupal\os2web_datalookup\Plugin\os2web\DataLookup\DatafordelerAddressLookupInterface $addressLookup */ + $addressLookup = $datalookupManager->createInstance('datafordeler_address_lookup'); + + /** @var \Drupal\os2web_datalookup\Plugin\os2web\DataLookup\DatafordelerMatrikulaLookupInterface $matrikulaLookup */ + $matrikulaLookup = $datalookupManager->createInstance('datafordeler_matrikula_lookup'); // Getting address. $addressParams = new ParameterBag(); @@ -108,17 +111,17 @@ private static function getMatrikulaOptions($addressValue, array $element) { if (isset($element['#limit_by_municipality'])) { $addressParams->set('limit_by_municipality', $element['#limit_by_municipality']); } - $address = $dawaService->getSingleAddress($addressParams); + $address = $addressLookup->getSingleAddress($addressParams); if ($address) { - $addressAccessId = $address->getAccessAddressId(); + $addressHouseId = $address->getHouseId(); // Find matrikula list from the houseid (husnummer): - $matrikulaId = $datafordelerLookup->getMatrikulaId($addressAccessId); + $matrikulaId = $matrikulaLookup->getMatrikulaId($addressHouseId); // Find Matrikula entries from matrikulas ID. if ($matrikulaId) { - $matrikulaEnties = $datafordelerLookup->getMatrikulaEntries($matrikulaId); + $matrikulaEnties = $matrikulaLookup->getMatrikulaEntries($matrikulaId); foreach ($matrikulaEnties as $matrikula) { $matrikulaOption = $matrikula->getMatrikulaNumber() . ' ' . $matrikula->getOwnershipName(); diff --git a/modules/os2forms_dawa/src/Element/DawaElementBase.php b/modules/os2forms_dawa/src/Element/DawaElementBase.php index 6c723e47..b6e57453 100644 --- a/modules/os2forms_dawa/src/Element/DawaElementBase.php +++ b/modules/os2forms_dawa/src/Element/DawaElementBase.php @@ -34,26 +34,15 @@ public static function validateDawaElementBase(&$element, FormStateInterface $fo } if (!empty($value)) { - /** @var \Drupal\os2forms_dawa\Service\DawaService $dawaService*/ - $dawaService = \Drupal::service('os2forms_dawa.service'); + $matches = []; - $element_type = $element['#type']; + if ($element['#type'] == 'os2forms_dawa_address') { + /** @var \Drupal\os2web_datalookup\Plugin\os2web\DataLookup\DatafordelerAddressLookupInterface $datafordelerAddressLookup */ + $datafordelerAddressLookup = \Drupal::service('plugin.manager.os2web_datalookup')->createInstance('datafordeler_address_lookup'); - $parameters = new ParameterBag($element['#autocomplete_route_parameters']); - $parameters->set('q', $value); - - switch ($element_type) { - case 'os2forms_dawa_address': - $matches = $dawaService->getAddressMatches($parameters); - break; - - case 'os2forms_dawa_block': - $matches = $dawaService->getBlockMatches($parameters); - break; - - case 'os2forms_dawa_matrikula': - $matches = $dawaService->getMatrikulaMatches($parameters); - break; + $parameters = new ParameterBag($element['#autocomplete_route_parameters']); + $parameters->set('q', $value); + $matches = $datafordelerAddressLookup->getAddressMatches($parameters); } // Checking if the current value is within the list of the values from an diff --git a/modules/os2forms_dawa/src/Entity/DatafordelerMatrikula.php b/modules/os2forms_dawa/src/Entity/DatafordelerMatrikula.php deleted file mode 100644 index 41be22ac..00000000 --- a/modules/os2forms_dawa/src/Entity/DatafordelerMatrikula.php +++ /dev/null @@ -1,79 +0,0 @@ -ownerLicenseCode = $json['properties']['ejerlavskode']; - $this->ownershipName = $json['properties']['ejerlavsnavn']; - $this->matrikulaNumber = $json['properties']['matrikelnummer']; - } - - /** - * Returns owner licence code. - * - * @return string - * Owners licence code. - */ - public function getOwnerLicenseCode(): string { - return $this->ownerLicenseCode; - } - - /** - * Returns ownership name. - * - * @return string - * ownership name. - */ - public function getOwnershipName(): string { - return $this->ownershipName; - } - - /** - * Returns makrikula number. - * - * @return string - * Matrikula number - */ - public function getMatrikulaNumber(): string { - return $this->matrikulaNumber; - } - -} diff --git a/modules/os2forms_dawa/src/Entity/DawaAddress.php b/modules/os2forms_dawa/src/Entity/DawaAddress.php deleted file mode 100644 index 89fe92a7..00000000 --- a/modules/os2forms_dawa/src/Entity/DawaAddress.php +++ /dev/null @@ -1,135 +0,0 @@ -id = $json['id']; - - if (isset($json['adgangsadresse']) && is_array($json['adgangsadresse'])) { - $this->municipalityCode = $json['adgangsadresse']['kommune']['kode']; - $this->propertyNumber = $json['adgangsadresse']['esrejendomsnr']; - $this->longitude = $json['adgangsadresse']['adgangspunkt']['koordinater'][0]; - $this->latitude = $json['adgangsadresse']['adgangspunkt']['koordinater'][1]; - $this->accessAddressId = $json['adgangsadresse']['id']; - } - } - - /** - * Gets address ID. - * - * @return string - * ID of the address. - */ - public function id() { - return $this->id; - } - - /** - * Gets municipality code. - * - * @return string - * Municipality code of the address. - */ - public function getMunicipalityCode() { - return $this->municipalityCode; - } - - /** - * Gets property number. - * - * @return string - * property number of the address. - */ - public function getPropertyNumber() { - return $this->propertyNumber; - } - - /** - * Gets latitude. - * - * @return float - * property latitude. - */ - public function getLatitude() { - return $this->latitude; - } - - /** - * Gets longitude. - * - * @return float - * property longitude. - */ - public function getLongitude() { - return $this->longitude; - } - - /** - * Gets Address access ID. - * - * @return string - * Address access ID. - */ - public function getAccessAddressId() { - return $this->accessAddressId; - } - -} diff --git a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddress.php b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddress.php index 93f809da..21a7779d 100644 --- a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddress.php +++ b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddress.php @@ -51,9 +51,9 @@ public function form(array $form, FormStateInterface $form_state) { ]; $form['autocomplete']['limit_by_municipality'] = [ '#type' => 'textfield', - '#title' => $this->t('Limit by municipality (-es)'), - '#pattern' => '^(\d{3},?)*$', - '#description' => $this->t('CSV list of municipalities codes, what will limit the address lookup.'), + '#title' => $this->t('Limit by municipality'), + '#pattern' => '^(\d{4})$', + '#description' => $this->t('Municipality code, what will limit the address lookup. Single number, 4 digits, e.g. 0661'), ]; return $form; diff --git a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddressMatrikula.php b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddressMatrikula.php index 5d211db0..f7d770d2 100644 --- a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddressMatrikula.php +++ b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementAddressMatrikula.php @@ -76,9 +76,9 @@ public function form(array $form, FormStateInterface $form_state) { ]; $form['autocomplete']['limit_by_municipality'] = [ '#type' => 'textfield', - '#title' => $this->t('Limit by municipality (-es)'), - '#pattern' => '^(\d{3},?)*$', - '#description' => $this->t('CSV list of municipalities codes, what will limit the address lookup.'), + '#title' => $this->t('Limit by municipality'), + '#pattern' => '^(\d{4})$', + '#description' => $this->t('Municipality code, what will limit the address lookup. Single number, 4 digits, e.g. 0661'), ]; return $form; diff --git a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementBlock.php b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementBlock.php index cb0b832f..45a20502 100644 --- a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementBlock.php +++ b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementBlock.php @@ -10,8 +10,8 @@ * * @WebformElement( * id = "os2forms_dawa_block", - * label = @Translation("DAWA Block (autocomplete)"), - * description = @Translation("Provides a DAWA Block Autocomplete element."), + * label = @Translation("DAWA Block (autocomplete) - DEPRECATED"), + * description = @Translation("Provides a DAWA Block Autocomplete element. This element is deprecated due to the API being phased out. There is no provided alternative for this element."), * category = @Translation("DAWA"), * ) */ diff --git a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementMatrikula.php b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementMatrikula.php index 34654769..464a45aa 100644 --- a/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementMatrikula.php +++ b/modules/os2forms_dawa/src/Plugin/WebformElement/DawaElementMatrikula.php @@ -10,8 +10,8 @@ * * @WebformElement( * id = "os2forms_dawa_matrikula", - * label = @Translation("DAWA Matrikula (autocomplete)"), - * description = @Translation("Provides a DAWA Matrikula Autocomplete element."), + * label = @Translation("DAWA Matrikula (autocomplete) - DEPRECATED"), + * description = @Translation("Provides a DAWA Matrikula Autocomplete element. This element is deprecated due to the API being phased out. There is no provided alternative for this element."), * category = @Translation("DAWA"), * ) */ diff --git a/modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookup.php b/modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookup.php deleted file mode 100644 index 00df83a3..00000000 --- a/modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookup.php +++ /dev/null @@ -1,162 +0,0 @@ -get('os2web_audit.logger'); - /** @var \Drupal\key\KeyRepositoryInterface $keyRepository */ - $keyRepository = $container->get('key.repository'); - /** @var \Drupal\Core\File\FileSystem $fileSystem */ - $fileSystem = $container->get('file_system'); - - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('http_client'), - $auditLogger, - $keyRepository, - $fileSystem, - ); - } - - /** - * {@inheritdoc} - */ - public function getMatrikulaId(string $addressAccessId) : ?string { - $url = "https://services.datafordeler.dk/DAR/DAR/3.0.0/rest/husnummerTilJordstykke"; - - $json = $this->httpClient->request('GET', $url, [ - 'query' => [ - 'husnummerid' => $addressAccessId, - ], - ])->getBody(); - - $jsonDecoded = json_decode($json, TRUE); - if (is_array($jsonDecoded)) { - if (NestedArray::keyExists($jsonDecoded, ['gældendeJordstykke', 'jordstykkeLokalId'])) { - return NestedArray::getValue($jsonDecoded, ['gældendeJordstykke', 'jordstykkeLokalId']); - } - } - - return NULL; - } - - /** - * {@inheritdoc} - */ - public function getMatrikulaEntries(string $matrikulaId) : array { - $matrikulaEntries = []; - $url = "https://services.datafordeler.dk/Matriklen2/Matrikel/2.0.0/rest/SamletFastEjendom"; - - $configuration = $this->getConfiguration(); - $json = $this->httpClient->request('GET', $url, [ - 'query' => [ - 'jordstykkeid' => $matrikulaId, - 'username' => $configuration['username'], - 'password' => $configuration['password'], - ], - ])->getBody(); - - $jsonDecoded = json_decode($json, TRUE); - - if (is_array($jsonDecoded)) { - if (NestedArray::keyExists($jsonDecoded, ['features', 0, 'properties', 'jordstykke'])) { - $jordstykker = NestedArray::getValue($jsonDecoded, ['features', 0, 'properties', 'jordstykke']); - foreach ($jordstykker as $jordstyk) { - try { - $matrikulaEntries[] = new DatafordelerMatrikula($jordstyk); - } - catch (\TypeError $e) { - // Could not create matrikula object. - } - } - } - } - - return $matrikulaEntries; - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration(): array { - return [ - 'username' => '', - 'password' => '', - ] + parent::defaultConfiguration(); - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form['username'] = [ - '#type' => 'textfield', - '#title' => $this->t('Username for service calls'), - '#default_value' => $this->configuration['username'], - '#required' => TRUE, - '#description' => $this->t('Username required for performing API requests'), - ]; - $form['password'] = [ - '#type' => 'textfield', - '#title' => $this->t('Password for service calls'), - '#default_value' => $this->configuration['password'], - '#required' => TRUE, - '#description' => $this->t('Password required for performing API requests'), - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - $configuration = $this->getConfiguration(); - $configuration['username'] = $form_state->getValue('username'); - $configuration['password'] = $form_state->getValue('password'); - $this->setConfiguration($configuration); - } - -} diff --git a/modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookupInterface.php b/modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookupInterface.php deleted file mode 100644 index 16947249..00000000 --- a/modules/os2forms_dawa/src/Plugin/os2web/DataLookup/DatafordelerDataLookupInterface.php +++ /dev/null @@ -1,38 +0,0 @@ -dawaDataLookup = $manager->createInstance('dawa_data_lookup'); - } - - /** - * Returns response for 'os2forms_dawa_address' element autocomplete route. - * - * @param \Symfony\Component\HttpFoundation\ParameterBag $params - * The query params. - * @param string $fetchColumn - * The name of the column to return, set to NULL to get all columns. - * - * @return array - * Array of matches. - */ - public function getAddressMatches(ParameterBag $params, $fetchColumn = 'tekst') { - // Get autocomplete query. - $q = $params->get('q') ?: ''; - - $matches = []; - - $autocompletePath = $this->dawaDataLookup->getAddressAutocompletePath(); - $requestUrl = $autocompletePath . '?q=' . urlencode($q); - - // Adding limit by municipality limit, if present. - $limitByMunicipality = $params->get('limit_by_municipality') ?: ''; - if (!empty($limitByMunicipality)) { - $limit_by_municipality_arr = str_getcsv($limitByMunicipality); - $requestUrl .= '&kommunekode=' . implode('|', $limit_by_municipality_arr); - } - - $json = file_get_contents($requestUrl); - $jsonDecoded = json_decode($json, TRUE); - - if ($fetchColumn) { - if (is_array($jsonDecoded)) { - // Checking if remove_place_name is enabled. - $removePlaceName = $params->get('remove_place_name') ?: ''; - if ($removePlaceName) { - foreach ($jsonDecoded as $entry) { - $supplerendebynavn = $entry['adresse']['supplerendebynavn']; - - $text = $entry[$fetchColumn]; - if (!empty($supplerendebynavn)) { - $text = preg_replace("/$supplerendebynavn,/", '', $text); - } - - $matches[] = $text; - } - } - else { - $matches = array_column($jsonDecoded, $fetchColumn); - } - } - } - else { - $matches = $jsonDecoded; - } - - return $matches; - } - - /** - * Returns single address from address API. - * - * @param \Symfony\Component\HttpFoundation\ParameterBag $params - * The query params. - * - * @return \Drupal\os2forms_dawa\Entity\DawaAddress|null - * The found address. - */ - public function getSingleAddress(ParameterBag $params) { - $address = NULL; - - // Getting address_id. - $matches = $this->getAddressMatches($params, NULL); - if (!empty($matches) && isset($matches[0]['adresse'])) { - $address = new DawaAddress($matches[0]['adresse']); - } - - // Fetching address. - if ($address && $address->id()) { - $requestUrl = $this->dawaDataLookup->getAddressApiPath() . '/' . $address->id(); - - $json = file_get_contents($requestUrl); - $jsonDecoded = json_decode($json, TRUE); - - if (is_array($jsonDecoded) && !empty($jsonDecoded)) { - $address = new DawaAddress($jsonDecoded); - } - } - - return $address; - } - - /** - * Returns response for 'os2forms_dawa_block' element autocomplete route. - * - * @param \Symfony\Component\HttpFoundation\ParameterBag $params - * The query params. - * - * @return array - * Array of matches. - */ - public function getBlockMatches(ParameterBag $params) { - // Get autocomplete query. - $q = $params->get('q') ?: ''; - - $matches = []; - - $autocompletePath = $this->dawaDataLookup->getBlockAutocompletePath(); - $requestUrl = $autocompletePath . '?q=' . urlencode($q); - - $json = file_get_contents($requestUrl); - $jsonDecoded = json_decode($json, TRUE); - if (is_array($jsonDecoded)) { - // Checking if remove_code is enabled. - $removeCode = $params->get('remove_code') ?: ''; - if ($removeCode) { - foreach ($jsonDecoded as $entry) { - $code = $entry['ejerlav']['kode']; - - $text = $entry['tekst']; - if (!empty($code)) { - $text = preg_replace("/$code /", '', $text); - } - - $matches[] = $text; - } - } - else { - $matches = array_column($jsonDecoded, 'tekst'); - } - } - - return $matches; - } - - /** - * Returns response for 'os2forms_dawa_matrikula' element autocomplete route. - * - * @param \Symfony\Component\HttpFoundation\ParameterBag $params - * The query params. - * - * @return array - * Array of matches. - */ - public function getMatrikulaMatches(ParameterBag $params) { - // Get autocomplete query. - $q = $params->get('q') ?: ''; - - $matches = []; - - $autocompletePath = $this->dawaDataLookup->getMatrikulaAutocompletePath(); - $requestUrl = $autocompletePath . '?q=' . urlencode($q); - - // Adding limit by municipality limit, if present. - $limitByMunicipality = $params->get('limit_by_municipality') ?: ''; - if (!empty($limitByMunicipality)) { - $requestUrl .= '&kommunekode=' . $limitByMunicipality; - } - - // Adding property number, if present. - $limitByProperty = $params->get('limit_by_property') ?: ''; - if (!empty($limitByProperty)) { - $requestUrl .= '&esrejendomsnr=' . $limitByProperty; - } - - $json = file_get_contents($requestUrl); - $jsonDecoded = json_decode($json, TRUE); - if (is_array($jsonDecoded)) { - // Checking if remove_code is enabled. - $removeCode = $params->get('remove_code') ?: ''; - if ($removeCode) { - foreach ($jsonDecoded as $entry) { - $code = $entry['jordstykke']['ejerlav']['kode']; - - $text = $entry['tekst']; - if (!empty($code)) { - $text = preg_replace("/ \($code\)/", '', $text); - } - - $matches[] = $text; - } - } - else { - $matches = array_column($jsonDecoded, 'tekst'); - } - } - - return $matches; - } - -} From 2bf2ec6720c62e7a0199d6917cbbaa12171d54b5 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Wed, 3 Jun 2026 12:23:48 +0300 Subject: [PATCH 52/52] Adding 5.1.0 version --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edf75bb5..b0609571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +## [5.1.0] 2026-06-03 + - [PR-326](https://github.com/OS2Forms/os2forms/pull/326) Updating ckeditor -> ckeditor5. - [PR-322](https://github.com/OS2Forms/os2forms/pull/322)