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
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ before starting to add changes. Use example [placed in the end of the page](#exa

## [Unreleased]

- [PR-325](https://github.com/OS2Forms/os2forms/pull/325)
Added NemID first name, middle name and last name webform elements.
- 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

Expand Down
11 changes: 11 additions & 0 deletions build_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ git merge --no-ff origin/feature/add-first-middle-and-last-name-nemid-elements
git checkout --theirs CHANGELOG.md
git add CHANGELOG.md
git commit -m "Merge remote-tracking branch 'origin/feature/add-first-middle-and-last-name-nemid-elements' into selvbetjening.aarhuskommune.dk"
git merge --no-ff origin/feature/add-states-configuration-to-handlers
git checkout --theirs CHANGELOG.md
git add CHANGELOG.md
git commit -m "Merge remote-tracking branch 'origin/feature/add-states-configuration-to-handlers' into selvbetjening.aarhuskommune.dk"
git merge --no-ff origin/feature/move-digital-signature-configuration-to-handler
# Manually handle conflicts in modules/os2forms_digital_signature/os2forms_digital_signature.install
# git add modules/os2forms_digital_signature/os2forms_digital_signature.install
git checkout --theirs CHANGELOG.md
git add CHANGELOG.md
git commit -m "Merge remote-tracking branch 'origin/feature/move-digital-signature-configuration-to-handler' into selvbetjening.aarhuskommune.dk"


docker run --rm --volume .:/app itkdev/php8.4-fpm:latest sed --in-place '1i This is a hack so the CHANGELOG is messed up. \nGenerated by build_release.sh' CHANGELOG.md
git add CHANGELOG.md
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
32 changes: 32 additions & 0 deletions modules/os2forms_digital_post/os2forms_digital_post.install
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

use Drupal\os2forms_digital_post\Helper\BeskedfordelerHelper;
use Drupal\webform\WebformSubmissionInterface;

/**
* Implements hook_schema().
Expand All @@ -26,3 +27,34 @@ function os2forms_digital_post_update_9001(): void {
'os2web_key',
], TRUE);
}

/**
* Set states config to completed on existing digital post handlers.
*/
function os2forms_digital_post_update_10001(): void {
// To avoid having to load full webforms we load and update webform configs.
$configFactory = \Drupal::configFactory();

foreach ($configFactory->listAll('webform.webform.') as $name) {
$config = $configFactory->getEditable($name);
$handlers = $config->get('handlers');
if (!is_array($handlers)) {
continue;
}

$changed = FALSE;

foreach ($handlers as $handlerKey => $handler) {
// $handler['id'] is the handler plugin id.
if (($handler['id'] ?? NULL) !== 'digital_post_sf1601') {
continue;
}
$handlers[$handlerKey]['settings']['additional']['states'] = [WebformSubmissionInterface::STATE_COMPLETED];
$changed = TRUE;
}

if ($changed) {
$config->set('handlers', $handlers)->save();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ final class WebformHandlerSF1601 extends WebformHandlerBase {
public const RECIPIENT_ELEMENT = 'recipient_element';
public const ATTACHMENT_ELEMENT = 'attachment_element';
public const SENDER_ADDRESS = 'sender_address';
private const string ADDITIONAL = 'additional';
private const string STATES = 'states';

/**
* Maximum length of sender label.
Expand Down Expand Up @@ -76,6 +78,9 @@ public static function create(ContainerInterface $container, array $configuratio
public function defaultConfiguration() {
return [
'debug' => FALSE,
self::ADDITIONAL => [
self::STATES => [WebformSubmissionInterface::STATE_COMPLETED],
],
];
}

Expand Down Expand Up @@ -203,6 +208,31 @@ public function buildConfigurationForm(array $form, FormStateInterface $formStat
'#default_value' => $this->configuration['debug'] ?? NULL,
];

// Additional.
// Lifted from EmailWebformHandler::buildConfigurationForm().
$resultsDisabled = (bool) $this->getWebform()->getSetting('results_disabled');
$form[self::ADDITIONAL] = [
'#type' => 'fieldset',
'#title' => $this->t('Additional settings'),
];
// Settings: States.
$states = (array) ($this->configuration[self::ADDITIONAL][self::STATES] ?? NULL);
$form[self::ADDITIONAL][self::STATES] = [
'#type' => 'checkboxes',
'#title' => $this->t('Run handler when …'),
'#options' => [
WebformSubmissionInterface::STATE_DRAFT_CREATED => $this->t('<b>draft is created</b>.'),
WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('<b>draft is updated</b>.'),
WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous <b>submission is converted</b> to authenticated.'),
WebformSubmissionInterface::STATE_COMPLETED => $this->t('<b>submission is completed</b>.'),
WebformSubmissionInterface::STATE_UPDATED => $this->t('<b>submission is updated</b>.'),
WebformSubmissionInterface::STATE_DELETED => $this->t('<b>submission is deleted</b>.'),
WebformSubmissionInterface::STATE_LOCKED => $this->t('<b>submission is locked</b>.'),
],
'#access' => !$resultsDisabled,
'#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states,
];

return $this->setSettingsParents($form);
}

Expand Down Expand Up @@ -333,6 +363,11 @@ static function (array $action) {
$this->configuration[self::MEMO_ACTIONS] = $actions;

$this->configuration['debug'] = (bool) $formState->getValue('debug');

$additional = $formState->getValue(self::ADDITIONAL);
// Clean up states.
$additional[self::STATES] = array_values(array_filter($additional[self::STATES]));
$this->configuration[self::ADDITIONAL] = $additional;
}

/**
Expand All @@ -341,6 +376,12 @@ static function (array $action) {
* @phpstan-return void
*/
public function postSave(WebformSubmissionInterface $webformSubmission, $update = TRUE) {
$submissionState = $webformSubmission->getWebform()->getSetting('results_disabled') ? WebformSubmissionInterface::STATE_COMPLETED : $webformSubmission->getState();
$enabledStates = (array) ($this->configuration[self::ADDITIONAL][self::STATES] ?? NULL);
if (!in_array($submissionState, $enabledStates)) {
return;
}

$this->helper->createJob($webformSubmission, $this->configuration);
}

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 hooks for os2forms_digital_signature.
*/

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_10002(&$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