diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 00382b62..64946986 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -39,7 +39,7 @@ jobs:
- name: Check that composer file is normalized
run: |
- docker compose run --rm --user "$(id -u):$(id -g)" php composer install
+ docker compose run --rm --user "$(id -u):$(id -g)" php ./scripts/ci-composer install
docker compose run --rm --user "$(id -u):$(id -g)" php composer normalize --dry-run
test-composer-files:
@@ -64,7 +64,7 @@ jobs:
- name: Check that dependencies resolve.
run: |
rm -fr vendor/
- docker compose run --rm --user "$(id -u):$(id -g)" php composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
+ docker compose run --rm --user "$(id -u):$(id -g)" php ./scripts/ci-composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
php-coding-standards:
name: PHP coding standards
@@ -78,7 +78,7 @@ jobs:
- name: Install Dependencies
run: |
- docker compose run --rm --user "$(id -u):$(id -g)" php composer install
+ docker compose run --rm --user "$(id -u):$(id -g)" php ./scripts/ci-composer install
- name: PHPCS
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ddc05ab..7b180f58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,20 @@ before starting to add changes. Use example [placed in the end of the page](#exa
## [Unreleased]
+- Moved digital signature configuration (which attachment to sign and the
+ signature validation text position) from the *OS2Forms Attachment* element
+ onto the *Digital Signature* webform handler. Includes an automatic
+ migration update hook for existing webforms.
+- Adding children information to Attachment SBSYS
+- os2forms_webform_maps translation page fix
+
+## [6.0.0] 2026-09-30
+
+- [PR-346](https://github.com/OS2Forms/os2forms/pull/346)
+ Drupal 11 compatibility.
+- [#355](https://github.com/OS2Forms/os2forms/issues/355)
+ Children select elements warning fix.
+
## [5.1.0] 2026-06-03
- [PR-326](https://github.com/OS2Forms/os2forms/pull/326)
diff --git a/compose.yaml b/compose.yaml
index a64bf5fb..d3083932 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -9,6 +9,7 @@ services:
volumes:
- ./:/app
environment:
+ HOME: /tmp
COMPOSER_HOME: /tmp/composer
COMPOSER_CACHE_DIR: /tmp/composer-cache
entrypoint:
diff --git a/composer.json b/composer.json
index ba5f18b2..00a05602 100644
--- a/composer.json
+++ b/composer.json
@@ -8,12 +8,11 @@
"ext-dom": "*",
"ext-soap": "*",
"cweagans/composer-patches": "^1.6.5",
- "dompdf/dompdf": "^2.0",
+ "dompdf/dompdf": "^2.0 || ^3.0",
"drupal/admin_toolbar": "^3.0",
"drupal/advancedqueue": "^1.0",
"drupal/cache_control_override": "^1.1 || ^2.0",
"drupal/coc_forms_auto_export": "^3.0@alpha",
- "drupal/config_entity_revisions": "2.0.x-dev",
"drupal/diff": "^1.0",
"drupal/entity_print": "^2.1",
"drupal/eu_cookie_compliance": "^1.8",
@@ -33,8 +32,8 @@
"drupal/r4032login": "^2.1",
"drupal/redirect": "^1.4",
"drupal/simplesamlphp_auth": "^4.0",
- "drupal/smtp": "^1.0@beta",
- "drupal/sodium": "^2.4",
+ "drupal/smtp": "^1.0@dev",
+ "drupal/sodium": "^2.4 || ^3.0",
"drupal/switch_page_theme": "^4.0",
"drupal/telephone_validation": "^2.2",
"drupal/token": "^1.5",
@@ -46,7 +45,7 @@
"drupal/webform_node_element": "^1.2",
"drupal/webform_remote_handlers": "^3.0",
"drupal/webform_remote_select": "^1.0.6",
- "drupal/webform_rest": "^4.0.0-beta2",
+ "drupal/webform_rest": "^4.2",
"drupal/webform_validation": "^2.0",
"drupal/webform_views": "^5.0@alpha",
"drupal/workflow_participants": "^3.0@RC",
@@ -73,7 +72,7 @@
"mglaman/phpstan-drupal": "^1.1",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan-deprecation-rules": "^1.1",
- "phpunit/phpunit": "^9.5",
+ "phpunit/phpunit": "^9.6 || ^10.5 || ^11.5",
"wsdltophp/packagegenerator": "^4.0"
},
"repositories": {
@@ -107,7 +106,9 @@
"drupal-lenient": {
"allowed-list": [
"drupal/coc_forms_auto_export",
- "drupal/webform_node_element"
+ "drupal/webform_node_element",
+ "drupal/events_logging",
+ "drupal/switch_page_theme"
]
},
"enable-patching": true,
@@ -122,6 +123,12 @@
"drupal/entity_print": {
"2733781 - Add Export to Word Support": "https://www.drupal.org/files/issues/2019-11-22/2733781-47.patch"
},
+ "drupal/events_logging": {
+ "3430351 - Automated Drupal 11 compatibility fixes for events_logging": "https://www.drupal.org/files/issues/2024-03-17/events_logging.2.0.0-beta2.rector.patch"
+ },
+ "drupal/switch_page_theme": {
+ "3489081 - Drupal 11 compatibility for Switch Page Theme": "https://www.drupal.org/files/issues/2025-10-30/switch_page_theme-3489081-20.patch"
+ },
"drupal/webform": {
"Unlock possibility of using Entity print module export to Word": "https://www.drupal.org/files/issues/2020-02-29/3096552-6.patch",
"Webform computed element post save alter": "https://www.drupal.org/files/issues/2024-06-25/webform_computed_post_save_field_alter.patch",
@@ -132,7 +139,7 @@
"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": {
- "3290637 - Automated Drupal 10 compatibility fixes": "https://www.drupal.org/files/issues/2023-05-12/webform_node_element_d10-3290637-11.patch"
+ "3495835 - Drupal 11 compatibility fixes for Webform Node Element": "https://git.drupalcode.org/project/webform_node_element/-/merge_requests/5.diff"
}
}
},
diff --git a/modules/os2forms_attachment/os2forms_attachment.info.yml b/modules/os2forms_attachment/os2forms_attachment.info.yml
index 1687014f..6835b74e 100644
--- a/modules/os2forms_attachment/os2forms_attachment.info.yml
+++ b/modules/os2forms_attachment/os2forms_attachment.info.yml
@@ -2,7 +2,7 @@ name: 'OS2forms Attachment'
type: module
description: 'Provides OS2forms attachment field'
package: OS2Forms
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'drupal:os2forms'
diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php
index bbc33f0b..6476833f 100644
--- a/modules/os2forms_attachment/src/Element/AttachmentElement.php
+++ b/modules/os2forms_attachment/src/Element/AttachmentElement.php
@@ -58,6 +58,28 @@ public static function getFileContent(array $element, WebformSubmissionInterface
if ($element['#export_type'] === 'pdf') {
$file_path = NULL;
+ // Digital signature settings live on the digital signature handler.
+ // Resolving them here ensures all consumers of the element (email
+ // handlers, attachment downloads) serve an already signed document
+ // and render the validation text consistently.
+ // Webform skips handlers whose per-submission conditions do not match,
+ // so the same check is applied here to avoid rendering signature
+ // validation text on a document that was never signed.
+ $elementKey = $element['#webform_key'] ?? NULL;
+ if ($elementKey !== NULL) {
+ foreach ($webform_submission->getWebform()->getHandlers('os2forms_digital_signature') as $handler) {
+ $settings = $handler->getConfiguration()['settings'] ?? [];
+ if ($handler->isEnabled()
+ && $handler->checkConditions($webform_submission)
+ && ($settings['attachment_element'] ?? '') === $elementKey) {
+ $element['#digital_signature'] = TRUE;
+ $element['#digital_signature_position'] = $settings['signature_position']
+ ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT;
+ break;
+ }
+ }
+ }
+
// If attachment with digital signatur, check if we already have one.
if (isset($element['#digital_signature']) && $element['#digital_signature']) {
// Get scheme.
diff --git a/modules/os2forms_attachment/src/Form/AttachmentComponentForm.php b/modules/os2forms_attachment/src/Form/AttachmentComponentForm.php
index 2a096e31..ba32ff03 100644
--- a/modules/os2forms_attachment/src/Form/AttachmentComponentForm.php
+++ b/modules/os2forms_attachment/src/Form/AttachmentComponentForm.php
@@ -113,9 +113,11 @@ public function form(array $form, FormStateInterface $form_state) {
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
- parent::save($form, $form_state);
+ $result = parent::save($form, $form_state);
$form_state->setRedirect('entity.os2forms_attachment_component.list');
+
+ return $result;
}
/**
diff --git a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php
index e19c531a..4f2215d6 100644
--- a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php
+++ b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php
@@ -5,7 +5,6 @@
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,8 +27,6 @@ protected function defineDefaultProperties() {
'view_mode' => 'html',
'template' => '',
'export_type' => '',
- 'digital_signature' => '',
- 'digital_signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT,
'exclude_empty' => '',
'exclude_empty_checkbox' => '',
'excluded_elements' => '',
@@ -91,28 +88,6 @@ public function form(array $form, FormStateInterface $form_state) {
'html' => $this->t('HTML'),
],
];
- $form['attachment']['digital_signature'] = [
- '#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);
diff --git a/modules/os2forms_autocomplete/os2forms_autocomplete.info.yml b/modules/os2forms_autocomplete/os2forms_autocomplete.info.yml
index 22db8a02..81bdc3eb 100644
--- a/modules/os2forms_autocomplete/os2forms_autocomplete.info.yml
+++ b/modules/os2forms_autocomplete/os2forms_autocomplete.info.yml
@@ -2,7 +2,7 @@ name: 'OS2forms Autocomplete'
type: module
description: 'Provides autocomplete field'
package: OS2Forms
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'drupal:os2forms'
diff --git a/modules/os2forms_dawa/os2forms_dawa.info.yml b/modules/os2forms_dawa/os2forms_dawa.info.yml
index bab9de83..6ab1b3d8 100644
--- a/modules/os2forms_dawa/os2forms_dawa.info.yml
+++ b/modules/os2forms_dawa/os2forms_dawa.info.yml
@@ -2,7 +2,7 @@ name: 'OS2forms DAWA'
type: module
description: 'Provides integration with Danish Addresses Web API (DAWA https://dawa.aws.dk) and provide address autocomplete fields.'
package: OS2Forms
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'drupal:os2forms'
diff --git a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/os2forms_digital_post_examples.info.yml b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/os2forms_digital_post_examples.info.yml
index 3863214f..3be7eb8c 100644
--- a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/os2forms_digital_post_examples.info.yml
+++ b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/os2forms_digital_post_examples.info.yml
@@ -3,6 +3,6 @@ type: module
description: 'Example forms for OS2Forms Digital Post.'
package: 'OS2Forms'
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'os2forms_digital_post:os2forms_digital_post'
diff --git a/modules/os2forms_digital_post/os2forms_digital_post.info.yml b/modules/os2forms_digital_post/os2forms_digital_post.info.yml
index 0e1408cc..ec5a66c9 100644
--- a/modules/os2forms_digital_post/os2forms_digital_post.info.yml
+++ b/modules/os2forms_digital_post/os2forms_digital_post.info.yml
@@ -2,7 +2,7 @@ name: 'OS2Forms Digital Post'
type: module
description: 'Provides integration to Print service provided by Serviceplatformen.'
package: 'OS2Forms'
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'beskedfordeler:beskedfordeler'
- 'drupal:advancedqueue'
diff --git a/modules/os2forms_digital_post/src/Form/SettingsForm.php b/modules/os2forms_digital_post/src/Form/SettingsForm.php
index e03f2d8a..ad51220c 100644
--- a/modules/os2forms_digital_post/src/Form/SettingsForm.php
+++ b/modules/os2forms_digital_post/src/Form/SettingsForm.php
@@ -3,6 +3,7 @@
namespace Drupal\os2forms_digital_post\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -26,17 +27,18 @@ final class SettingsForm extends ConfigFormBase {
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
- private readonly EntityStorageInterface $queueStorage;
+ protected readonly EntityStorageInterface $queueStorage;
/**
* Constructor.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
+ TypedConfigManagerInterface $typedConfigManager,
EntityTypeManagerInterface $entityTypeManager,
- private readonly Settings $settings,
+ protected readonly Settings $settings,
) {
- parent::__construct($config_factory);
+ parent::__construct($config_factory, $typedConfigManager);
$this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue');
}
@@ -48,6 +50,7 @@ public function __construct(
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
+ $container->get('config.typed'),
$container->get('entity_type.manager'),
$container->get(Settings::class),
);
diff --git a/modules/os2forms_digital_post/src/Helper/BeskedfordelerHelper.php b/modules/os2forms_digital_post/src/Helper/BeskedfordelerHelper.php
index c256dc17..e92c3f53 100644
--- a/modules/os2forms_digital_post/src/Helper/BeskedfordelerHelper.php
+++ b/modules/os2forms_digital_post/src/Helper/BeskedfordelerHelper.php
@@ -61,7 +61,6 @@ public function createMessage(int $submissionId, MeMoMessage $message, string $r
* @see Message::__set()
*/
public function loadMessage(string $messageUUID): ?Message {
- // @phpstan-ignore-next-line (fetchObject invoked with 2 parameters)
return $this->database
->select(self::TABLE_NAME, 'm')
->fields('m')
diff --git a/modules/os2forms_digital_post/src/Plugin/AdvancedQueue/JobType/SendDigitalPostSF1601.php b/modules/os2forms_digital_post/src/Plugin/AdvancedQueue/JobType/SendDigitalPostSF1601.php
index 3952d333..15b3dad1 100644
--- a/modules/os2forms_digital_post/src/Plugin/AdvancedQueue/JobType/SendDigitalPostSF1601.php
+++ b/modules/os2forms_digital_post/src/Plugin/AdvancedQueue/JobType/SendDigitalPostSF1601.php
@@ -25,7 +25,7 @@ final class SendDigitalPostSF1601 extends JobTypeBase implements ContainerFactor
*
* @var \Drupal\os2forms_digital_post\Helper\WebformHelperSF1601
*/
- private WebformHelperSF1601 $helper;
+ protected WebformHelperSF1601 $helper;
/**
* {@inheritdoc}
diff --git a/modules/os2forms_digital_signature/README.md b/modules/os2forms_digital_signature/README.md
index d863182e..5dd93e2c 100644
--- a/modules/os2forms_digital_signature/README.md
+++ b/modules/os2forms_digital_signature/README.md
@@ -13,12 +13,20 @@ The signature server consists of two parts. A frontend module
### Activating Digital Signature
-1. Add the OS2forms attachment element to the form.
-2. Indicate that the OS2Forms attachment requires a digital signature.
-3. Add the Digital Signature Handler to the webform.
+1. Add an attachment-style element to the form (either *OS2Forms Attachment* — generated PDF — or
+ *OS2forms digital signature document* — uploaded PDF).
+2. Add the **Digital Signature** handler to the webform.
+3. In the handler configuration, pick the attachment element to sign from the **Attachment element to sign**
+ dropdown, and choose where the signature validation text should be placed in the generated PDF
+ (the position only applies to *OS2Forms Attachment* elements; it is ignored for uploaded PDFs).
4. If the form requires an email handler, ensure the trigger is set to **...when submission is locked** in the handler’s
*Additional settings*.
+> [!NOTE]
+> Prior to this release, the attachment element exposed `Digital signature` and `Digital signature position` properties
+> directly. These settings have moved to the handler. Existing forms are migrated automatically by
+> `os2forms_digital_signature_update_10001()`.
+
### Flow Explained
1. Upon form submission, a PDF is generated, saved in the private directory, and sent to the signature service via URL.
diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml
index 29547e43..974986f9 100644
--- a/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml
+++ b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml
@@ -2,8 +2,9 @@ name: 'OS2Forms Digital Signature'
type: module
description: 'Provides digital signature functionality'
package: 'OS2Forms'
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'webform:webform'
+ - 'os2forms:os2forms_attachment'
configure: os2forms_digital_signature.settings
diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.install b/modules/os2forms_digital_signature/os2forms_digital_signature.install
new file mode 100644
index 00000000..5c360940
--- /dev/null
+++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install
@@ -0,0 +1,147 @@
+accessCheck(FALSE)->execute());
+ $sandbox['migrated'] = 0;
+ }
+
+ // array_splice() removes the chunk from the sandbox, so the next pass
+ // continues with the remaining ids.
+ /** @var \Drupal\webform\WebformInterface[] $webforms */
+ $webforms = Webform::loadMultiple(array_splice($sandbox['ids'], 0, 25));
+ foreach ($webforms as $webform) {
+ $handlers = $webform->getHandlers();
+ $signatureHandler = NULL;
+ foreach ($handlers as $handler) {
+ if ($handler->getPluginId() === 'os2forms_digital_signature') {
+ $signatureHandler = $handler;
+ break;
+ }
+ }
+
+ $elements = $webform->getElementsDecodedAndFlattened();
+ $needsSave = FALSE;
+
+ if ($signatureHandler) {
+ // Mirror the pre-update runtime selection: the handler signed the
+ // first os2forms_digital_signature_document element, falling back to
+ // the first os2forms_attachment element. The #digital_signature
+ // property never influenced the selection, only the rendering of the
+ // validation text.
+ $signedKey = NULL;
+ $signedPosition = Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT;
+ $candidates = [];
+ foreach (['os2forms_digital_signature_document', 'os2forms_attachment'] as $type) {
+ foreach ($elements as $key => $element) {
+ if (($element['#type'] ?? NULL) === $type) {
+ $candidates[] = $key;
+ if ($signedKey === NULL) {
+ $signedKey = $key;
+ if ($type === 'os2forms_attachment') {
+ $signedPosition = $element['#digital_signature_position'] ?? $signedPosition;
+ }
+ }
+ }
+ }
+ }
+
+ if ($signedKey === NULL) {
+ $logger->warning('Skipped webform @id during digital signature config migration: the handler is enabled but the webform has no os2forms_digital_signature_document or os2forms_attachment element. Configure the handler manually.', [
+ '@id' => $webform->id(),
+ ]);
+ }
+ else {
+ if (count($candidates) > 1) {
+ $logger->warning('Webform @id has multiple signable elements (@all); migrating @chosen to match the pre-update type-priority selection.', [
+ '@id' => $webform->id(),
+ '@all' => implode(', ', $candidates),
+ '@chosen' => $signedKey,
+ ]);
+ }
+ foreach ($elements as $key => $element) {
+ if (!empty($element['#digital_signature']) && $key !== $signedKey) {
+ $logger->warning('Webform @id: element @key had digital signature enabled, but @chosen is migrated instead to match the pre-update behavior.', [
+ '@id' => $webform->id(),
+ '@key' => $key,
+ '@chosen' => $signedKey,
+ ]);
+ }
+ }
+
+ // Write the new config onto the handler.
+ $config = $signatureHandler->getConfiguration();
+ $config['settings']['attachment_element'] = $signedKey;
+ $config['settings']['signature_position'] = $signedPosition;
+ $signatureHandler->setConfiguration($config);
+ $needsSave = TRUE;
+ $sandbox['migrated']++;
+ }
+ }
+
+ // Strip the legacy properties whether or not the handler is present, so
+ // no webform is left with inert digital signature configuration.
+ foreach ($elements as $element) {
+ if (array_key_exists('#digital_signature', $element) || array_key_exists('#digital_signature_position', $element)) {
+ $rawElements = $webform->getElementsDecoded();
+ _os2forms_digital_signature_strip_legacy_props($rawElements);
+ $webform->setElements($rawElements);
+ $needsSave = TRUE;
+ break;
+ }
+ }
+
+ if ($needsSave) {
+ $webform->save();
+ }
+ }
+
+ // Anything below 1 makes the runner call us again. Done when no ids remain.
+ $sandbox['#finished'] = empty($sandbox['ids']) ? 1 : 0;
+
+ if ($sandbox['#finished'] >= 1) {
+ return t('Migrated digital signature config on @count webform(s).', ['@count' => $sandbox['migrated']]);
+ }
+}
+
+/**
+ * Recursively remove legacy digital signature element properties.
+ *
+ * @param array $elements
+ * The decoded elements tree, modified in place.
+ */
+function _os2forms_digital_signature_strip_legacy_props(array &$elements) {
+ foreach ($elements as $key => &$value) {
+ if (!is_array($value)) {
+ continue;
+ }
+ if (str_starts_with((string) $key, '#')) {
+ continue;
+ }
+ unset($value['#digital_signature'], $value['#digital_signature_position']);
+ _os2forms_digital_signature_strip_legacy_props($value);
+ }
+}
diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php
index 9a616bfc..f2a572cd 100644
--- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php
+++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php
@@ -7,9 +7,11 @@
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
+use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\file\FileRepositoryInterface;
+use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder;
use Drupal\os2forms_digital_signature\Service\SigningService;
use Drupal\webform\Plugin\WebformElementManagerInterface;
use Drupal\webform\Plugin\WebformHandlerBase;
@@ -37,56 +39,175 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase {
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
- private readonly ModuleHandlerInterface $moduleHandler;
+ protected readonly ModuleHandlerInterface $moduleHandler;
/**
* The webform element plugin manager.
*
* @var \Drupal\webform\Plugin\WebformElementManagerInterface
*/
- private readonly WebformElementManagerInterface $elementManager;
+ protected readonly WebformElementManagerInterface $elementManager;
/**
* Logger for channel - os2forms_digital_signature.
*
* @var \Psr\Log\LoggerInterface
*/
- private readonly LoggerInterface $logger;
+ protected readonly LoggerInterface $logger;
/**
* File system interface.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
- private readonly FileSystemInterface $fileSystem;
+ protected readonly FileSystemInterface $fileSystem;
/**
* File repository.
*
* @var \Drupal\file\FileRepositoryInterface
*/
- private readonly FileRepositoryInterface $fileRepository;
+ protected readonly FileRepositoryInterface $fileRepository;
/**
* File URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
- private readonly FileUrlGeneratorInterface $fileUrlGenerator;
+ protected readonly FileUrlGeneratorInterface $fileUrlGenerator;
/**
* OS2Forms signing service.
*
* @var \Drupal\os2forms_digital_signature\Service\SigningService
*/
- private readonly SigningService $signingService;
+ protected readonly SigningService $signingService;
/**
* Settings service.
*
* @var \Drupal\Core\Site\Settings
*/
- private readonly Settings $settings;
+ protected readonly Settings $settings;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'attachment_element' => '',
+ 'signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ $form['attachment_element'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Attachment element to sign'),
+ '#description' => $this->t('Select the webform element whose generated or uploaded PDF should be signed. Only OS2Forms Attachment elements with export type PDF and Digital Signature Document elements are listed.'),
+ '#options' => $this->getAttachmentElementOptions(),
+ '#empty_option' => $this->t('- Select -'),
+ '#default_value' => $this->configuration['attachment_element'],
+ '#required' => TRUE,
+ ];
+
+ $form['signature_position'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Signature validation text position'),
+ '#description' => $this->t('Where the digital signature validation text is placed in the generated PDF. Only applies when the selected element is an OS2Forms Attachment; ignored for uploaded PDF documents.'),
+ '#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' => $this->configuration['signature_position'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ parent::submitConfigurationForm($form, $form_state);
+
+ $values = $form_state->getValues();
+ $this->configuration['attachment_element'] = $values['attachment_element'] ?? '';
+ $this->configuration['signature_position'] = $values['signature_position']
+ ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSummary() {
+ $elementKey = $this->configuration['attachment_element'] ?? '';
+ $position = $this->configuration['signature_position'] ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT;
+
+ return [
+ '#markup' => $this->t('Sign attachment: @element
Signature position: @position', [
+ '@element' => $elementKey !== '' ? $elementKey : $this->t('not configured'),
+ '@position' => $position,
+ ]),
+ ];
+ }
+
+ /**
+ * Build the dropdown options for the attachment element selector.
+ *
+ * The signing flow requires a PDF, so only the two element types that
+ * provide one are listed: os2forms_digital_signature_document (uploaded
+ * PDF) and os2forms_attachment (generated PDF).
+ *
+ * @return array
+ * Map of element key => human label.
+ *
+ * @see self::isSignableElement()
+ */
+ protected function getAttachmentElementOptions(): array {
+ $webform = $this->getWebform();
+ if (!$webform) {
+ return [];
+ }
+
+ $elements = $webform->getElementsInitializedAndFlattened();
+ $options = [];
+ foreach ($webform->getElementsAttachments() as $key) {
+ $element = $elements[$key] ?? NULL;
+ if (!$element || !$this->isSignableElement($element)) {
+ continue;
+ }
+ $type = $element['#type'] ?? '';
+ $title = $element['#title'] ?? $key;
+ $options[$key] = sprintf('%s (%s) [%s]', $title, $key, $type);
+ }
+ return $options;
+ }
+
+ /**
+ * Check whether an element provides a PDF that can be signed.
+ *
+ * @param array $element
+ * The flattened webform element.
+ *
+ * @return bool
+ * TRUE if the element can be signed.
+ */
+ protected function isSignableElement(array $element): bool {
+ return match ($element['#type'] ?? '') {
+ 'os2forms_digital_signature_document' => TRUE,
+ // List only os2forms_attachment elements with PDF export type.
+ 'os2forms_attachment' => ($element['#export_type'] ?? '') === 'pdf',
+ default => FALSE,
+ };
+ }
/**
* {@inheritdoc}
@@ -177,59 +298,62 @@ public function preSave(WebformSubmissionInterface $webform_submission) {
/**
* Get OS2forms file attachment.
*
+ * Resolves the attachment element configured on the handler, asks its
+ * plugin for the email attachment payload, and returns the first item.
+ *
* @param \Drupal\webform\WebformSubmissionInterface $webform_submission
* A webform submission.
*
* @return array|null
- * Array of attachment data.
+ * Array of attachment data, or NULL when no attachment is available.
*
* @throws \Exception
*/
protected function getSubmissionAttachment(WebformSubmissionInterface $webform_submission) {
- $attachments = NULL;
- $attachment = NULL;
-
- // Getting all element types that are added to the webform.
- //
- // Priority is the following: check for os2forms_digital_signature_document,
- // is not found try serving os2forms_attachment.
- $elementTypes = array_column($this->getWebform()->getElementsDecodedAndFlattened(), '#type');
- $attachmentType = '';
- if (in_array('os2forms_digital_signature_document', $elementTypes)) {
- $attachmentType = 'os2forms_digital_signature_document';
- }
- elseif (in_array('os2forms_attachment', $elementTypes)) {
- $attachmentType = 'os2forms_attachment';
+ $elementKey = $this->configuration['attachment_element'] ?? '';
+ if ($elementKey === '') {
+ $this->logger->error('Digital signature handler has no attachment_element configured for webform %webform.', [
+ '%webform' => $this->getWebform()->id(),
+ ]);
+ return NULL;
}
$elements = $this->getWebform()->getElementsInitializedAndFlattened();
- $element_attachments = $this->getWebform()->getElementsAttachments();
- foreach ($element_attachments as $element_attachment) {
- // Check if the element attachment key is excluded and should not attach
- // any files.
- if (isset($this->configuration['excluded_elements'][$element_attachment])) {
- continue;
- }
+ if (!isset($elements[$elementKey])) {
+ $this->logger->error('Configured attachment element %element does not exist on webform %webform.', [
+ '%element' => $elementKey,
+ '%webform' => $this->getWebform()->id(),
+ ]);
+ return NULL;
+ }
+
+ $element = $elements[$elementKey];
- $element = $elements[$element_attachment];
+ // Ensure that the element provides a PDF that can be signed during runtime.
+ if (!$this->isSignableElement($element)) {
+ $this->logger->error('Configured attachment element %element on webform %webform does not provide a PDF and cannot be signed.', [
+ '%element' => $elementKey,
+ '%webform' => $this->getWebform()->id(),
+ ]);
+ return NULL;
+ }
- if ($element['#type'] == $attachmentType) {
- /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */
- $element_plugin = $this->elementManager->getElementInstance($element);
- $attachments = $element_plugin->getEmailAttachments($element, $webform_submission);
+ /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */
+ $element_plugin = $this->elementManager->getElementInstance($element);
+ $attachments = $element_plugin->getEmailAttachments($element, $webform_submission);
- // If we are dealing with an uploaded file, attach the FID.
- if ($fid = $webform_submission->getElementData($element_attachment)) {
- $attachments[0]['fid'] = $fid;
- }
- break;
- }
+ if (empty($attachments)) {
+ return NULL;
}
- if (!empty($attachments)) {
- $attachment = reset($attachments);
+ // If the source is an uploaded managed file, attach the FID so the
+ // signed file can replace the upload rather than creating a new file.
+ if ($fid = $webform_submission->getElementData($elementKey)) {
+ $attachments[0]['fid'] = $fid;
}
+ $attachment = reset($attachments);
+
// For SwiftMailer && Mime Mail use filecontent and not the filepath.
// @see \Drupal\swiftmailer\Plugin\Mail\SwiftMailer::attachAsMimeMail
// @see \Drupal\mimemail\Utility\MimeMailFormatHelper::mimeMailFile
diff --git a/modules/os2forms_encrypt/os2forms_encrypt.info.yml b/modules/os2forms_encrypt/os2forms_encrypt.info.yml
index 5e1e7edf..049b10de 100644
--- a/modules/os2forms_encrypt/os2forms_encrypt.info.yml
+++ b/modules/os2forms_encrypt/os2forms_encrypt.info.yml
@@ -3,7 +3,7 @@ description: Encryption functionality for OS2Forms web-forms
package: OS2Forms
type: module
version: 1.0.0
-core_version_requirement: ^8 || ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'drupal:webform_encrypt'
- 'drupal:sodium'
diff --git a/modules/os2forms_encrypt/src/Form/SettingsForm.php b/modules/os2forms_encrypt/src/Form/SettingsForm.php
index e80341a8..0820c0eb 100644
--- a/modules/os2forms_encrypt/src/Form/SettingsForm.php
+++ b/modules/os2forms_encrypt/src/Form/SettingsForm.php
@@ -3,6 +3,7 @@
namespace Drupal\os2forms_encrypt\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
@@ -28,10 +29,10 @@ class SettingsForm extends ConfigFormBase {
*
* @var \Drupal\encrypt\EncryptionProfileManager
*/
- private EncryptionProfileManager $encryptionProfileManager;
+ protected EncryptionProfileManager $encryptionProfileManager;
- public function __construct(ConfigFactoryInterface $config_factory, EncryptionProfileManager $encryptionProfileManager) {
- parent::__construct($config_factory);
+ public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, EncryptionProfileManager $encryptionProfileManager) {
+ parent::__construct($config_factory, $typedConfigManager);
$this->encryptionProfileManager = $encryptionProfileManager;
}
@@ -41,6 +42,7 @@ public function __construct(ConfigFactoryInterface $config_factory, EncryptionPr
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
+ $container->get('config.typed'),
$container->get('encrypt.encryption_profile.manager')
);
}
diff --git a/modules/os2forms_fasit/os2forms_fasit.info.yml b/modules/os2forms_fasit/os2forms_fasit.info.yml
index fe55c94a..c32678bc 100644
--- a/modules/os2forms_fasit/os2forms_fasit.info.yml
+++ b/modules/os2forms_fasit/os2forms_fasit.info.yml
@@ -2,7 +2,7 @@ name: 'OS2Forms Fasit'
type: module
description: 'Fasit integration'
package: OS2Forms
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- drupal:webform
- drupal:advancedqueue
diff --git a/modules/os2forms_fasit/src/Form/SettingsForm.php b/modules/os2forms_fasit/src/Form/SettingsForm.php
index 2cfed651..89cf1752 100644
--- a/modules/os2forms_fasit/src/Form/SettingsForm.php
+++ b/modules/os2forms_fasit/src/Form/SettingsForm.php
@@ -3,6 +3,7 @@
namespace Drupal\os2forms_fasit\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -33,9 +34,10 @@ final class SettingsForm extends ConfigFormBase {
*/
public function __construct(
ConfigFactoryInterface $config_factory,
- private readonly FasitHelper $helper,
+ TypedConfigManagerInterface $typedConfigManager,
+ protected readonly FasitHelper $helper,
) {
- parent::__construct($config_factory);
+ parent::__construct($config_factory, $typedConfigManager);
}
/**
@@ -46,6 +48,7 @@ public function __construct(
public static function create(ContainerInterface $container): self {
return new static(
$container->get('config.factory'),
+ $container->get('config.typed'),
$container->get(FasitHelper::class)
);
}
diff --git a/modules/os2forms_fasit/src/Plugin/AdvancedQueue/JobType/Fasit.php b/modules/os2forms_fasit/src/Plugin/AdvancedQueue/JobType/Fasit.php
index efc8b2bd..d7d6a20d 100644
--- a/modules/os2forms_fasit/src/Plugin/AdvancedQueue/JobType/Fasit.php
+++ b/modules/os2forms_fasit/src/Plugin/AdvancedQueue/JobType/Fasit.php
@@ -26,7 +26,7 @@ class Fasit extends JobTypeBase implements ContainerFactoryPluginInterface {
*
* @var \Drupal\os2forms_fasit\Helper\FasitHelper
*/
- private FasitHelper $helper;
+ protected FasitHelper $helper;
/**
* The submission logger.
diff --git a/modules/os2forms_fbs_handler/os2forms_fbs_handler.info.yml b/modules/os2forms_fbs_handler/os2forms_fbs_handler.info.yml
index e6251ac6..abf35943 100644
--- a/modules/os2forms_fbs_handler/os2forms_fbs_handler.info.yml
+++ b/modules/os2forms_fbs_handler/os2forms_fbs_handler.info.yml
@@ -2,7 +2,7 @@ name: 'FBS Handler'
type: module
description: 'Provides integration to FBS.'
package: 'OS2Forms'
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'webform:webform'
- 'advancedqueue:advancedqueue'
diff --git a/modules/os2forms_forloeb/os2forms_forloeb.info.yml b/modules/os2forms_forloeb/os2forms_forloeb.info.yml
index 9aee352b..04c27a6e 100644
--- a/modules/os2forms_forloeb/os2forms_forloeb.info.yml
+++ b/modules/os2forms_forloeb/os2forms_forloeb.info.yml
@@ -2,7 +2,7 @@ name: 'OS2forms med Forløb Module'
description: 'Adds a Maestro workflow engine and advanced workflow functionality to OS2forms.'
package: OS2Forms
type: module
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
configure: os2forms_forloeb.settings
diff --git a/modules/os2forms_forloeb/src/Form/SettingsForm.php b/modules/os2forms_forloeb/src/Form/SettingsForm.php
index deb91bb6..0b43c15b 100644
--- a/modules/os2forms_forloeb/src/Form/SettingsForm.php
+++ b/modules/os2forms_forloeb/src/Form/SettingsForm.php
@@ -3,6 +3,7 @@
namespace Drupal\os2forms_forloeb\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Extension\ModuleExtensionList;
@@ -28,11 +29,12 @@ class SettingsForm extends ConfigFormBase {
*/
public function __construct(
ConfigFactoryInterface $configFactory,
- private readonly RoleStorageInterface $roleStorage,
- private readonly EntityStorageInterface $queueStorage,
- private readonly ModuleExtensionList $moduleHandler,
+ TypedConfigManagerInterface $typedConfigManager,
+ protected readonly RoleStorageInterface $roleStorage,
+ protected readonly EntityStorageInterface $queueStorage,
+ protected readonly ModuleExtensionList $moduleHandler,
) {
- parent::__construct($configFactory);
+ parent::__construct($configFactory, $typedConfigManager);
}
/**
@@ -41,6 +43,7 @@ public function __construct(
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
+ $container->get('config.typed'),
$container->get('entity_type.manager')->getStorage('user_role'),
$container->get('entity_type.manager')->getStorage('advancedqueue_queue'),
$container->get('extension.list.module'),
diff --git a/modules/os2forms_forloeb/src/Plugin/AdvancedQueue/JobType/SendMeastroNotification.php b/modules/os2forms_forloeb/src/Plugin/AdvancedQueue/JobType/SendMeastroNotification.php
index 1cf267de..5c67aa61 100644
--- a/modules/os2forms_forloeb/src/Plugin/AdvancedQueue/JobType/SendMeastroNotification.php
+++ b/modules/os2forms_forloeb/src/Plugin/AdvancedQueue/JobType/SendMeastroNotification.php
@@ -40,7 +40,7 @@ public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
- private readonly MaestroHelper $helper,
+ protected readonly MaestroHelper $helper,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
diff --git a/modules/os2forms_nemid/os2forms_nemid.info.yml b/modules/os2forms_nemid/os2forms_nemid.info.yml
index 3523807f..f9b7ceaf 100644
--- a/modules/os2forms_nemid/os2forms_nemid.info.yml
+++ b/modules/os2forms_nemid/os2forms_nemid.info.yml
@@ -2,7 +2,7 @@ name: 'OS2forms NemID'
type: module
description: 'Provides custom NemID fields and integration with NemID'
package: OS2Forms
-core_version_requirement: ^9 || ^10
+core_version_requirement: ^10 || ^11
dependencies:
- 'drupal:os2forms'
diff --git a/modules/os2forms_nemid/src/Plugin/WebformElement/MitidChildElementBase.php b/modules/os2forms_nemid/src/Plugin/WebformElement/MitidChildElementBase.php
index fbdd7dca..79d78cc4 100644
--- a/modules/os2forms_nemid/src/Plugin/WebformElement/MitidChildElementBase.php
+++ b/modules/os2forms_nemid/src/Plugin/WebformElement/MitidChildElementBase.php
@@ -12,7 +12,7 @@
* @see \Drupal\webform\Plugin\WebformElementInterface
* @see \Drupal\webform\Annotation\WebformElement
*/
-abstract class MitidChildElementBase extends WebformElementBase {
+abstract class MitidChildElementBase extends WebformElementBase implements NemidChildElementInterface {
/**
* {@inheritdoc}
diff --git a/modules/os2forms_nemid/src/Plugin/WebformElement/NemidChildElementInterface.php b/modules/os2forms_nemid/src/Plugin/WebformElement/NemidChildElementInterface.php
new file mode 100644
index 00000000..967b3c77
--- /dev/null
+++ b/modules/os2forms_nemid/src/Plugin/WebformElement/NemidChildElementInterface.php
@@ -0,0 +1,10 @@
+' . $element['#address_protection_help_text'] . '';
}
}
diff --git a/modules/os2forms_nemid/src/Plugin/WebformElement/NemidChildrenSelect.php b/modules/os2forms_nemid/src/Plugin/WebformElement/NemidChildrenSelect.php
index 9d2e0497..18b3144f 100644
--- a/modules/os2forms_nemid/src/Plugin/WebformElement/NemidChildrenSelect.php
+++ b/modules/os2forms_nemid/src/Plugin/WebformElement/NemidChildrenSelect.php
@@ -20,7 +20,7 @@
* @see \Drupal\os2forms_nemid\Plugin\NemidElementBase
* @see \Drupal\os2forms_nemid\Element\NemidChildrenRadios
*/
-class NemidChildrenSelect extends Select implements NemidElementPersonalInterface, NemidPrepopulateFieldInterface {
+class NemidChildrenSelect extends Select implements NemidElementPersonalInterface, NemidPrepopulateFieldInterface, NemidChildElementInterface {
/**
* {@inheritdoc}
@@ -155,7 +155,7 @@ public function handleElementPrepopulate(array &$element, FormStateInterface &$f
$element['#options'] = $options;
- if ($showAddressNameProtectionMessage) {
+ if ($showAddressNameProtectionMessage && isset($element['#address_protection_help_text'])) {
$element['#suffix'] = '