diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f722099..a9a587d1 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]
+- 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.
- [PR-326](https://github.com/OS2Forms/os2forms/pull/326)
Updating ckeditor -> ckeditor5.
- [PR-322](https://github.com/OS2Forms/os2forms/pull/322)
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..b66e19f9
--- /dev/null
+++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install
@@ -0,0 +1,114 @@
+getHandlers();
+ $signatureHandler = NULL;
+ foreach ($handlers as $handler) {
+ if ($handler->getPluginId() === 'os2forms_digital_signature') {
+ $signatureHandler = $handler;
+ break;
+ }
+ }
+ if (!$signatureHandler) {
+ continue;
+ }
+
+ $elements = $webform->getElementsDecodedAndFlattened();
+ $signedKey = NULL;
+ $signedPosition = NULL;
+ $multiple = [];
+ foreach ($elements as $key => $element) {
+ if (!empty($element['#digital_signature'])) {
+ if ($signedKey === NULL) {
+ $signedKey = $key;
+ $signedPosition = $element['#digital_signature_position']
+ ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT;
+ }
+ else {
+ $multiple[] = $key;
+ }
+ }
+ }
+
+ if ($signedKey === NULL) {
+ $skipped[$webform->id()] = 'no element had #digital_signature set';
+ continue;
+ }
+
+ if ($multiple) {
+ $logger->warning('Webform @id has multiple elements flagged for signing (@all); migrating @chosen and ignoring the rest.', [
+ '@id' => $webform->id(),
+ '@all' => implode(', ', array_merge([$signedKey], $multiple)),
+ '@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);
+
+ // Strip the legacy properties from every element.
+ $rawElements = $webform->getElementsDecoded();
+ _os2forms_digital_signature_strip_legacy_props($rawElements);
+ $webform->setElements($rawElements);
+
+ $webform->save();
+ $migrated++;
+ }
+
+ if ($skipped) {
+ foreach ($skipped as $id => $reason) {
+ $logger->warning('Skipped webform @id during digital signature config migration: @reason. Configure the handler manually.', [
+ '@id' => $id,
+ '@reason' => $reason,
+ ]);
+ }
+ }
+
+ return t('Migrated digital signature config on @count webform(s).', ['@count' => $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..6b01f33c 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,104 @@ 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.
+ *
+ * Lists every element implementing WebformElementAttachmentInterface
+ * (covers os2forms_attachment and os2forms_digital_signature_document).
+ *
+ * @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;
+ }
+ $title = $element['#title'] ?? $key;
+ $type = $element['#type'] ?? '';
+ $options[$key] = sprintf('%s (%s) [%s]', $title, $key, $type);
+ }
+ return $options;
+ }
+
/**
* {@inheritdoc}
*/
@@ -177,59 +277,56 @@ 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];
+ $element['#digital_signature'] = TRUE;
+ $element['#digital_signature_position'] = $this->configuration['signature_position']
+ ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT;
- 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