diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ddc05ab..cb36739b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,11 @@ 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.
+
## [5.1.0] 2026-06-03
- [PR-326](https://github.com/OS2Forms/os2forms/pull/326)
diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php
index bbc33f0b..96f9a91e 100644
--- a/modules/os2forms_attachment/src/Element/AttachmentElement.php
+++ b/modules/os2forms_attachment/src/Element/AttachmentElement.php
@@ -58,6 +58,23 @@ 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.
+ $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() && ($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/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_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..99abd7c5 100644
--- a/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml
+++ b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml
@@ -5,5 +5,6 @@ package: 'OS2Forms'
core_version_requirement: ^9 || ^10
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..e36046c3 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;
@@ -88,6 +90,108 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase {
*/
private 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.'),
+ '#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.
+ */
+ 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) {
+ continue;
+ }
+ $type = $element['#type'] ?? '';
+ if (!in_array($type, ['os2forms_digital_signature_document', 'os2forms_attachment'], TRUE)) {
+ continue;
+ }
+ $title = $element['#title'] ?? $key;
+ $options[$key] = sprintf('%s (%s) [%s]', $title, $key, $type);
+ }
+ return $options;
+ }
+
/**
* {@inheritdoc}
*/
@@ -177,59 +281,53 @@ 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[$element_attachment];
+ $element = $elements[$elementKey];
- 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