Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions modules/os2forms_attachment/src/Element/AttachmentElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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' => '',
Expand Down Expand Up @@ -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);

Expand Down
14 changes: 11 additions & 3 deletions modules/os2forms_digital_signature/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ package: 'OS2Forms'
core_version_requirement: ^9 || ^10
dependencies:
- 'webform:webform'
- 'os2forms:os2forms_attachment'

configure: os2forms_digital_signature.settings
147 changes: 147 additions & 0 deletions modules/os2forms_digital_signature/os2forms_digital_signature.install
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

/**
* @file
* Install, update and uninstall for the os2forms_digital_signature module.
*/

use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder;
use Drupal\webform\Entity\Webform;

/**
* Migrate digital signature configuration onto the handler.
*
* Previously the os2forms_attachment element exposed #digital_signature and
* #digital_signature_position properties, and the handler picked the element
* to sign by type: the first os2forms_digital_signature_document element,
* falling back to the first os2forms_attachment element. The configuration
* now lives on the os2forms_digital_signature handler, so this update
* replicates that selection into the handler config of every webform using
* the handler, and strips the legacy element properties from all webforms.
*
* Processes the webforms in batches of 25 via the update sandbox.
*/
function os2forms_digital_signature_update_10001(&$sandbox) {
$logger = \Drupal::logger('os2forms_digital_signature');

if (!isset($sandbox['ids'])) {
$sandbox['ids'] = array_values(\Drupal::entityQuery('webform')->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);
}
}
Loading
Loading