From 09f9b1acb1dd78e3382d53f4ddae12e68208e38a Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 18 May 2026 11:25:41 +0200 Subject: [PATCH 01/12] Move digital signature confgurations to handler --- CHANGELOG.md | 4 + .../WebformElement/AttachmentElement.php | 25 --- modules/os2forms_digital_signature/README.md | 14 +- .../os2forms_digital_signature.info.yml | 1 + .../os2forms_digital_signature.install | 114 ++++++++++++ .../DigitalSignatureWebformHandler.php | 167 ++++++++++++++---- 6 files changed, 262 insertions(+), 63 deletions(-) create mode 100644 modules/os2forms_digital_signature/os2forms_digital_signature.install diff --git a/CHANGELOG.md b/CHANGELOG.md index ff239913..b8c7d67c 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-322](https://github.com/OS2Forms/os2forms/pull/322) Update Digital Post handler error messages. - [PR-320](https://github.com/OS2Forms/os2forms/pull/320) 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 From 70fe8d34463b36ae577a7bdb7aaaf1b82d3918e6 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 23 Jun 2026 14:02:58 +0200 Subject: [PATCH 02/12] Allow configurating on which submission states handlers should run --- CHANGELOG.md | 8 ++ .../os2forms_digital_post.install | 32 ++++++++ .../WebformHandler/WebformHandlerSF1601.php | 41 ++++++++++ .../os2forms_digital_signature.install | 39 ++++++++++ .../DigitalSignatureWebformHandler.php | 74 ++++++++++++++++++- modules/os2forms_fasit/os2forms_fasit.install | 33 +++++++++ .../WebformHandler/FasitWebformHandler.php | 51 +++++++++++++ .../os2forms_fbs_handler.install | 39 ++++++++++ .../WebformHandler/FbsWebformHandler.php | 51 +++++++++++++ 9 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 modules/os2forms_digital_signature/os2forms_digital_signature.install create mode 100644 modules/os2forms_fbs_handler/os2forms_fbs_handler.install diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ddc05ab..e57aca61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- Added the ability to configure on which submission states handlers should run. + The default option is to run on the completed state. Changes was made to the + following handlers: + - Digital post + - Fasit + - FBS + - Digital signature + ## [5.1.0] 2026-06-03 - [PR-326](https://github.com/OS2Forms/os2forms/pull/326) diff --git a/modules/os2forms_digital_post/os2forms_digital_post.install b/modules/os2forms_digital_post/os2forms_digital_post.install index 48768e21..cb410b1d 100644 --- a/modules/os2forms_digital_post/os2forms_digital_post.install +++ b/modules/os2forms_digital_post/os2forms_digital_post.install @@ -6,6 +6,7 @@ */ use Drupal\os2forms_digital_post\Helper\BeskedfordelerHelper; +use Drupal\webform\WebformSubmissionInterface; /** * Implements hook_schema(). @@ -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(); + } + } +} diff --git a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php index 6f0e06a9..ae633c01 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -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. @@ -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], + ], ]; } @@ -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('draft is created.'), + WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('draft is updated.'), + WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous submission is converted to authenticated.'), + WebformSubmissionInterface::STATE_COMPLETED => $this->t('submission is completed.'), + WebformSubmissionInterface::STATE_UPDATED => $this->t('submission is updated.'), + WebformSubmissionInterface::STATE_DELETED => $this->t('submission is deleted.'), + WebformSubmissionInterface::STATE_LOCKED => $this->t('submission is locked.'), + ], + '#access' => !$resultsDisabled, + '#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states, + ]; + return $this->setSettingsParents($form); } @@ -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; } /** @@ -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); } 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..4e5cb76a --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install @@ -0,0 +1,39 @@ +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) !== 'os2forms_digital_signature') { + continue; + } + $handlers[$handlerKey]['settings']['additional']['states'] = [WebformSubmissionInterface::STATE_COMPLETED]; + $changed = TRUE; + } + + if ($changed) { + $config->set('handlers', $handlers)->save(); + } + } +} diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index 9a616bfc..78252c00 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -7,6 +7,7 @@ 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; @@ -88,6 +89,9 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase { */ private readonly Settings $settings; + private const string ADDITIONAL = 'additional'; + private const string STATES = 'states'; + /** * {@inheritdoc} */ @@ -107,14 +111,78 @@ public static function create(ContainerInterface $container, array $configuratio /** * {@inheritdoc} + * + * @phpstan-return array */ - public function preSave(WebformSubmissionInterface $webform_submission) { - $webform = $webform_submission->getWebform(); + public function defaultConfiguration() { + return [ + self::ADDITIONAL => [ + self::STATES => [WebformSubmissionInterface::STATE_COMPLETED], + ], + ]; + } + + /** + * {@inheritdoc} + * + * @phpstan-param array $form + * @phpstan-return array + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + // 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('draft is created.'), + WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('draft is updated.'), + WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous submission is converted to authenticated.'), + WebformSubmissionInterface::STATE_COMPLETED => $this->t('submission is completed.'), + WebformSubmissionInterface::STATE_UPDATED => $this->t('submission is updated.'), + WebformSubmissionInterface::STATE_DELETED => $this->t('submission is deleted.'), + // The digital signature logic locks after the first signing. Resigning + // does not make sense, so 'locked' is not an option here. + ], + '#access' => !$resultsDisabled, + '#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states, + ]; + + return $this->setSettingsParents($form); + } - if ($webform_submission->isLocked()) { + /** + * {@inheritdoc} + * + * @phpstan-param array $form + * @phpstan-return void + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $additional = $form_state->getValue(self::ADDITIONAL); + // Clean up states. + $additional[self::STATES] = array_values(array_filter($additional[self::STATES])); + $this->configuration[self::ADDITIONAL] = $additional; + } + + /** + * {@inheritdoc} + */ + public function preSave(WebformSubmissionInterface $webform_submission) { + $submissionState = $webform_submission->getWebform()->getSetting('results_disabled') ? WebformSubmissionInterface::STATE_COMPLETED : $webform_submission->getState(); + $enabledStates = (array) ($this->configuration[self::ADDITIONAL][self::STATES] ?? NULL); + if (!in_array($submissionState, $enabledStates)) { return; } + $webform = $webform_submission->getWebform(); + $attachment = $this->getSubmissionAttachment($webform_submission); if (!$attachment) { $this->logger->error('Attachment cannot be created webform: %webform, webform_submission: %webform_submission', diff --git a/modules/os2forms_fasit/os2forms_fasit.install b/modules/os2forms_fasit/os2forms_fasit.install index f862bafb..8d471f49 100644 --- a/modules/os2forms_fasit/os2forms_fasit.install +++ b/modules/os2forms_fasit/os2forms_fasit.install @@ -5,6 +5,8 @@ * Install hooks for os2forms_fasit. */ +use Drupal\webform\WebformSubmissionInterface; + /** * Install Key module. */ @@ -13,3 +15,34 @@ function os2forms_fasit_update_9001(): void { 'key', ], TRUE); } + +/** + * Set states config to completed on existing fasit handlers. + */ +function os2forms_fasit_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) !== 'os2forms_fasit') { + continue; + } + $handlers[$handlerKey]['settings']['additional']['states'] = [WebformSubmissionInterface::STATE_COMPLETED]; + $changed = TRUE; + } + + if ($changed) { + $config->set('handlers', $handlers)->save(); + } + } +} diff --git a/modules/os2forms_fasit/src/Plugin/WebformHandler/FasitWebformHandler.php b/modules/os2forms_fasit/src/Plugin/WebformHandler/FasitWebformHandler.php index 84eef379..e726bf44 100644 --- a/modules/os2forms_fasit/src/Plugin/WebformHandler/FasitWebformHandler.php +++ b/modules/os2forms_fasit/src/Plugin/WebformHandler/FasitWebformHandler.php @@ -35,6 +35,8 @@ class FasitWebformHandler extends WebformHandlerBase { public const FASIT_HANDLER_DOCUMENT_DESCRIPTION = 'document_description'; public const FASIT_HANDLER_CPR_ELEMENT = 'cpr_element'; public const FASIT_HANDLER_ATTACHMENT_ELEMENT = 'attachment_element'; + private const string ADDITIONAL = 'additional'; + private const string STATES = 'states'; /** * The submission logger. @@ -79,6 +81,19 @@ public static function create(ContainerInterface $container, array $configuratio ); } + /** + * {@inheritdoc} + * + * @phpstan-return array + */ + public function defaultConfiguration() { + return [ + self::ADDITIONAL => [ + self::STATES => [WebformSubmissionInterface::STATE_COMPLETED], + ], + ]; + } + /** * {@inheritdoc} * @@ -129,6 +144,31 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#size' => 5, ]; + // 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('draft is created.'), + WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('draft is updated.'), + WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous submission is converted to authenticated.'), + WebformSubmissionInterface::STATE_COMPLETED => $this->t('submission is completed.'), + WebformSubmissionInterface::STATE_UPDATED => $this->t('submission is updated.'), + WebformSubmissionInterface::STATE_DELETED => $this->t('submission is deleted.'), + WebformSubmissionInterface::STATE_LOCKED => $this->t('submission is locked.'), + ], + '#access' => !$resultsDisabled, + '#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states, + ]; + return $this->setSettingsParents($form); } @@ -143,12 +183,23 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration[self::FASIT_HANDLER_GENERAL][self::FASIT_HANDLER_DOCUMENT_DESCRIPTION] = $form_state->getValue(self::FASIT_HANDLER_GENERAL)[self::FASIT_HANDLER_DOCUMENT_DESCRIPTION]; $this->configuration[self::FASIT_HANDLER_GENERAL][self::FASIT_HANDLER_CPR_ELEMENT] = $form_state->getValue(self::FASIT_HANDLER_GENERAL)[self::FASIT_HANDLER_CPR_ELEMENT]; $this->configuration[self::FASIT_HANDLER_GENERAL][self::FASIT_HANDLER_ATTACHMENT_ELEMENT] = $form_state->getValue(self::FASIT_HANDLER_GENERAL)[self::FASIT_HANDLER_ATTACHMENT_ELEMENT]; + + $additional = $form_state->getValue(self::ADDITIONAL); + // Clean up states. + $additional[self::STATES] = array_values(array_filter($additional[self::STATES])); + $this->configuration[self::ADDITIONAL] = $additional; } /** * {@inheritdoc} */ public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE): void { + $submissionState = $webform_submission->getWebform()->getSetting('results_disabled') ? WebformSubmissionInterface::STATE_COMPLETED : $webform_submission->getState(); + $enabledStates = (array) ($this->configuration[self::ADDITIONAL][self::STATES] ?? NULL); + if (!in_array($submissionState, $enabledStates)) { + return; + } + $queueStorage = $this->entityTypeManager->getStorage('advancedqueue_queue'); /** @var \Drupal\advancedqueue\Entity\Queue $queue */ $queue = $queueStorage->load('fasit_queue'); diff --git a/modules/os2forms_fbs_handler/os2forms_fbs_handler.install b/modules/os2forms_fbs_handler/os2forms_fbs_handler.install new file mode 100644 index 00000000..4fa0422f --- /dev/null +++ b/modules/os2forms_fbs_handler/os2forms_fbs_handler.install @@ -0,0 +1,39 @@ +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) !== 'os2forms_fbs') { + continue; + } + $handlers[$handlerKey]['settings']['additional']['states'] = [WebformSubmissionInterface::STATE_COMPLETED]; + $changed = TRUE; + } + + if ($changed) { + $config->set('handlers', $handlers)->save(); + } + } +} diff --git a/modules/os2forms_fbs_handler/src/Plugin/WebformHandler/FbsWebformHandler.php b/modules/os2forms_fbs_handler/src/Plugin/WebformHandler/FbsWebformHandler.php index aa1ae330..33c479c5 100644 --- a/modules/os2forms_fbs_handler/src/Plugin/WebformHandler/FbsWebformHandler.php +++ b/modules/os2forms_fbs_handler/src/Plugin/WebformHandler/FbsWebformHandler.php @@ -42,6 +42,8 @@ final class FbsWebformHandler extends WebformHandlerBase { * The queue id. */ private const QUEUE_ID = 'os2forms_fbs_handler'; + private const string ADDITIONAL = 'additional'; + private const string STATES = 'states'; /** * Constructs an FbsWebformHandler object. @@ -89,6 +91,19 @@ public static function create(ContainerInterface $container, array $configuratio ); } + /** + * {@inheritdoc} + * + * @phpstan-return array + */ + public function defaultConfiguration() { + return [ + self::ADDITIONAL => [ + self::STATES => [WebformSubmissionInterface::STATE_COMPLETED], + ], + ]; + } + /** * {@inheritdoc} * @@ -146,6 +161,31 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#default_value' => $this->configuration['password'] ?? '', ]; + // 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('draft is created.'), + WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('draft is updated.'), + WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous submission is converted to authenticated.'), + WebformSubmissionInterface::STATE_COMPLETED => $this->t('submission is completed.'), + WebformSubmissionInterface::STATE_UPDATED => $this->t('submission is updated.'), + WebformSubmissionInterface::STATE_DELETED => $this->t('submission is deleted.'), + WebformSubmissionInterface::STATE_LOCKED => $this->t('submission is locked.'), + ], + '#access' => !$resultsDisabled, + '#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states, + ]; + return $this->setSettingsParents($form); } @@ -164,12 +204,23 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s ->getValue(['wrapper', 'username']); $this->configuration['password'] = $form_state ->getValue(['wrapper', 'password']); + + $additional = $form_state->getValue(self::ADDITIONAL); + // Clean up states. + $additional[self::STATES] = array_values(array_filter($additional[self::STATES])); + $this->configuration[self::ADDITIONAL] = $additional; } /** * {@inheritdoc} */ public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE): void { + $submissionState = $webform_submission->getWebform()->getSetting('results_disabled') ? WebformSubmissionInterface::STATE_COMPLETED : $webform_submission->getState(); + $enabledStates = (array) ($this->configuration[self::ADDITIONAL][self::STATES] ?? NULL); + if (!in_array($submissionState, $enabledStates)) { + return; + } + $logger_context = [ 'handler_id' => 'os2forms_fbs', 'channel' => 'webform_submission', From 48d83071a0c404f773477707711459e7ea1d9d4f Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 23 Jun 2026 14:08:20 +0200 Subject: [PATCH 03/12] Updated CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e57aca61..e5b2a42c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] -- Added the ability to configure on which submission states handlers should run. +- [PR-333](https://github.com/OS2Forms/os2forms/pull/333) + Added the ability to configure on which submission states handlers should run. The default option is to run on the completed state. Changes was made to the following handlers: - Digital post From ee0a2a2c4df15550d0e08fe67f44be5958a03437 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 20 Jul 2026 10:52:52 +0200 Subject: [PATCH 04/12] Ensured signed documents are fetched when getting file content --- .../src/Element/AttachmentElement.php | 17 +++++++++++++++++ .../DigitalSignatureWebformHandler.php | 3 --- 2 files changed, 17 insertions(+), 3 deletions(-) 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_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index 6b01f33c..782270b7 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -307,9 +307,6 @@ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_s } $element = $elements[$elementKey]; - $element['#digital_signature'] = TRUE; - $element['#digital_signature_position'] = $this->configuration['signature_position'] - ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */ $element_plugin = $this->elementManager->getElementInstance($element); From 8c9e4b32c8f7fd6a95a4b68aa28c4fa89728137a Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 20 Jul 2026 11:05:07 +0200 Subject: [PATCH 05/12] Mimic original behaviour when migrating --- .../os2forms_digital_signature.install | 112 +++++++++++------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.install b/modules/os2forms_digital_signature/os2forms_digital_signature.install index b66e19f9..e9f38504 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.install +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install @@ -9,13 +9,15 @@ use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder; use Drupal\webform\Entity\Webform; /** - * Migrate per-element digital signature config onto the handler. + * Migrate digital signature configuration onto the handler. * * Previously the os2forms_attachment element exposed #digital_signature and - * #digital_signature_position properties. Both now live on the - * os2forms_digital_signature handler. This update walks every webform that - * has the handler enabled, lifts the values off the element into the handler - * config, and strips the element properties. + * #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. */ function os2forms_digital_signature_update_10001(&$sandbox) { $logger = \Drupal::logger('os2forms_digital_signature'); @@ -33,53 +35,79 @@ function os2forms_digital_signature_update_10001(&$sandbox) { 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; + $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) { + $skipped[$webform->id()] = 'the handler is enabled but the webform has no os2forms_digital_signature_document or os2forms_attachment element'; + } + 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, + ]); } - else { - $multiple[] = $key; + 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; + $migrated++; } } - if ($signedKey === NULL) { - $skipped[$webform->id()] = 'no element had #digital_signature set'; - continue; + // 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 ($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, - ]); + if ($needsSave) { + $webform->save(); } - - // 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) { From e1abc9d2fb7c349f1cbce06e944ddca5b36503f7 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 20 Jul 2026 11:08:13 +0200 Subject: [PATCH 06/12] Ensured only os2forms_digital_signature_document and os2forms_attachment are up for selection in the signature handler --- .../WebformHandler/DigitalSignatureWebformHandler.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index 782270b7..e36046c3 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -162,8 +162,9 @@ public function getSummary() { /** * Build the dropdown options for the attachment element selector. * - * Lists every element implementing WebformElementAttachmentInterface - * (covers os2forms_attachment and os2forms_digital_signature_document). + * 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. @@ -181,8 +182,11 @@ protected function getAttachmentElementOptions(): array { if (!$element) { continue; } - $title = $element['#title'] ?? $key; $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; From f45f90081b34ef2cbb1a2332e3db9379760b91d5 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 20 Jul 2026 11:28:19 +0200 Subject: [PATCH 07/12] Added batching to the migration --- .../os2forms_digital_signature.install | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.install b/modules/os2forms_digital_signature/os2forms_digital_signature.install index e9f38504..5c360940 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.install +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install @@ -18,14 +18,21 @@ use Drupal\webform\Entity\Webform; * 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'); - $migrated = 0; - $skipped = []; + 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(); + $webforms = Webform::loadMultiple(array_splice($sandbox['ids'], 0, 25)); foreach ($webforms as $webform) { $handlers = $webform->getHandlers(); $signatureHandler = NULL; @@ -63,7 +70,9 @@ function os2forms_digital_signature_update_10001(&$sandbox) { } if ($signedKey === NULL) { - $skipped[$webform->id()] = 'the handler is enabled but the webform has no os2forms_digital_signature_document or os2forms_attachment element'; + $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) { @@ -89,7 +98,7 @@ function os2forms_digital_signature_update_10001(&$sandbox) { $config['settings']['signature_position'] = $signedPosition; $signatureHandler->setConfiguration($config); $needsSave = TRUE; - $migrated++; + $sandbox['migrated']++; } } @@ -110,16 +119,12 @@ function os2forms_digital_signature_update_10001(&$sandbox) { } } - 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, - ]); - } - } + // Anything below 1 makes the runner call us again. Done when no ids remain. + $sandbox['#finished'] = empty($sandbox['ids']) ? 1 : 0; - return t('Migrated digital signature config on @count webform(s).', ['@count' => $migrated]); + if ($sandbox['#finished'] >= 1) { + return t('Migrated digital signature config on @count webform(s).', ['@count' => $sandbox['migrated']]); + } } /** From 00a4e6a0c4bbe8645191d58f7157dcf9daf77225 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 31 Aug 2026 14:22:24 +0200 Subject: [PATCH 08/12] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb36739b..fbba0e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +This is a hack so the CHANGELOG is messed up. +Generated by build_release.sh # OS2Forms Change Log From 5a1441b8ab31a62172ff1484d01505597f815bea Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 31 Aug 2026 14:22:46 +0200 Subject: [PATCH 09/12] Update build_release script --- build_release.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/build_release.sh b/build_release.sh index c1f358a2..0ae94a1e 100755 --- a/build_release.sh +++ b/build_release.sh @@ -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 From 5038fc64cc4e98d205bb049914534d4c81f26b75 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Mon, 31 Aug 2026 16:34:58 +0200 Subject: [PATCH 10/12] Fix dublicated update hook name --- .../os2forms_digital_signature.install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.install b/modules/os2forms_digital_signature/os2forms_digital_signature.install index a3d7b89f..e9712078 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.install +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install @@ -53,7 +53,7 @@ function os2forms_digital_signature_update_10001(): void { * * Processes the webforms in batches of 25 via the update sandbox. */ -function os2forms_digital_signature_update_10001(&$sandbox) { +function os2forms_digital_signature_update_10002(&$sandbox) { $logger = \Drupal::logger('os2forms_digital_signature'); if (!isset($sandbox['ids'])) { From 84b3506c6a7a3e52a5e43c6f474c386eb5bbecb2 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 1 Sep 2026 08:56:33 +0200 Subject: [PATCH 11/12] Fixed further merge conflicts --- .../DigitalSignatureWebformHandler.php | 132 +++++++----------- 1 file changed, 52 insertions(+), 80 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index aff783b9..523fe1f7 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -93,6 +93,23 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase { private const string ADDITIONAL = 'additional'; private const string STATES = 'states'; + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->moduleHandler = $container->get('module_handler'); + $instance->elementManager = $container->get('plugin.manager.webform.element'); + $instance->logger = $container->get('logger.channel.os2forms_digital_signature'); + $instance->fileSystem = $container->get('file_system'); + $instance->fileRepository = $container->get('file.repository'); + $instance->fileUrlGenerator = $container->get('file_url_generator'); + $instance->signingService = $container->get('os2forms_digital_signature.signing_service'); + $instance->settings = $container->get('settings'); + + return $instance; + } + /** * {@inheritdoc} */ @@ -100,6 +117,9 @@ public function defaultConfiguration() { return [ 'attachment_element' => '', 'signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, + self::ADDITIONAL => [ + self::STATES => [WebformSubmissionInterface::STATE_COMPLETED], + ], ]; } @@ -132,7 +152,33 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#default_value' => $this->configuration['signature_position'], ]; - return $form; + // 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('draft is created.'), + WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('draft is updated.'), + WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous submission is converted to authenticated.'), + WebformSubmissionInterface::STATE_COMPLETED => $this->t('submission is completed.'), + WebformSubmissionInterface::STATE_UPDATED => $this->t('submission is updated.'), + WebformSubmissionInterface::STATE_DELETED => $this->t('submission is deleted.'), + // The digital signature logic locks after the first signing. Resigning + // does not make sense, so 'locked' is not an option here. + ], + '#access' => !$resultsDisabled, + '#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states, + ]; + + return $this->setSettingsParents($form); } /** @@ -145,6 +191,11 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['attachment_element'] = $values['attachment_element'] ?? ''; $this->configuration['signature_position'] = $values['signature_position'] ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; + + $additional = $form_state->getValue(self::ADDITIONAL); + // Clean up states. + $additional[self::STATES] = array_values(array_filter($additional[self::STATES])); + $this->configuration[self::ADDITIONAL] = $additional; } /** @@ -195,85 +246,6 @@ protected function getAttachmentElementOptions(): array { return $options; } - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); - $instance->moduleHandler = $container->get('module_handler'); - $instance->elementManager = $container->get('plugin.manager.webform.element'); - $instance->logger = $container->get('logger.channel.os2forms_digital_signature'); - $instance->fileSystem = $container->get('file_system'); - $instance->fileRepository = $container->get('file.repository'); - $instance->fileUrlGenerator = $container->get('file_url_generator'); - $instance->signingService = $container->get('os2forms_digital_signature.signing_service'); - $instance->settings = $container->get('settings'); - - return $instance; - } - - /** - * {@inheritdoc} - * - * @phpstan-return array - */ - public function defaultConfiguration() { - return [ - self::ADDITIONAL => [ - self::STATES => [WebformSubmissionInterface::STATE_COMPLETED], - ], - ]; - } - - /** - * {@inheritdoc} - * - * @phpstan-param array $form - * @phpstan-return array - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - // 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('draft is created.'), - WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('draft is updated.'), - WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous submission is converted to authenticated.'), - WebformSubmissionInterface::STATE_COMPLETED => $this->t('submission is completed.'), - WebformSubmissionInterface::STATE_UPDATED => $this->t('submission is updated.'), - WebformSubmissionInterface::STATE_DELETED => $this->t('submission is deleted.'), - // The digital signature logic locks after the first signing. Resigning - // does not make sense, so 'locked' is not an option here. - ], - '#access' => !$resultsDisabled, - '#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states, - ]; - - return $this->setSettingsParents($form); - } - - /** - * {@inheritdoc} - * - * @phpstan-param array $form - * @phpstan-return void - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - $additional = $form_state->getValue(self::ADDITIONAL); - // Clean up states. - $additional[self::STATES] = array_values(array_filter($additional[self::STATES])); - $this->configuration[self::ADDITIONAL] = $additional; - } - /** * {@inheritdoc} */ From 931725e747cbf9e004f0dbd6fcaa194543d7fea7 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Tue, 1 Sep 2026 09:41:07 +0200 Subject: [PATCH 12/12] Reverted handler states from digital signature handler --- .../os2forms_digital_signature.install | 32 --------- .../DigitalSignatureWebformHandler.php | 68 +++++-------------- 2 files changed, 16 insertions(+), 84 deletions(-) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.install b/modules/os2forms_digital_signature/os2forms_digital_signature.install index e9712078..37b57d85 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.install +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install @@ -7,38 +7,6 @@ use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder; use Drupal\webform\Entity\Webform; -use Drupal\webform\WebformSubmissionInterface; - -/** - * Set states config to completed on existing digital signature handlers. - */ -function os2forms_digital_signature_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) !== 'os2forms_digital_signature') { - continue; - } - $handlers[$handlerKey]['settings']['additional']['states'] = [WebformSubmissionInterface::STATE_COMPLETED]; - $changed = TRUE; - } - - if ($changed) { - $config->set('handlers', $handlers)->save(); - } - } -} /** * Migrate digital signature configuration onto the handler. diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index 523fe1f7..85d57f16 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -90,24 +90,21 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase { */ private readonly Settings $settings; - private const string ADDITIONAL = 'additional'; - private const string STATES = 'states'; - /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); - $instance->moduleHandler = $container->get('module_handler'); - $instance->elementManager = $container->get('plugin.manager.webform.element'); - $instance->logger = $container->get('logger.channel.os2forms_digital_signature'); - $instance->fileSystem = $container->get('file_system'); - $instance->fileRepository = $container->get('file.repository'); - $instance->fileUrlGenerator = $container->get('file_url_generator'); - $instance->signingService = $container->get('os2forms_digital_signature.signing_service'); - $instance->settings = $container->get('settings'); - - return $instance; + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->moduleHandler = $container->get('module_handler'); + $instance->elementManager = $container->get('plugin.manager.webform.element'); + $instance->logger = $container->get('logger.channel.os2forms_digital_signature'); + $instance->fileSystem = $container->get('file_system'); + $instance->fileRepository = $container->get('file.repository'); + $instance->fileUrlGenerator = $container->get('file_url_generator'); + $instance->signingService = $container->get('os2forms_digital_signature.signing_service'); + $instance->settings = $container->get('settings'); + + return $instance; } /** @@ -117,9 +114,6 @@ public function defaultConfiguration() { return [ 'attachment_element' => '', 'signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, - self::ADDITIONAL => [ - self::STATES => [WebformSubmissionInterface::STATE_COMPLETED], - ], ]; } @@ -152,33 +146,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#default_value' => $this->configuration['signature_position'], ]; - // 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('draft is created.'), - WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this->t('draft is updated.'), - WebformSubmissionInterface::STATE_CONVERTED => $this->t('anonymous submission is converted to authenticated.'), - WebformSubmissionInterface::STATE_COMPLETED => $this->t('submission is completed.'), - WebformSubmissionInterface::STATE_UPDATED => $this->t('submission is updated.'), - WebformSubmissionInterface::STATE_DELETED => $this->t('submission is deleted.'), - // The digital signature logic locks after the first signing. Resigning - // does not make sense, so 'locked' is not an option here. - ], - '#access' => !$resultsDisabled, - '#default_value' => $resultsDisabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $states, - ]; - - return $this->setSettingsParents($form); + return $this->setSettingsParents($form); } /** @@ -191,11 +159,6 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['attachment_element'] = $values['attachment_element'] ?? ''; $this->configuration['signature_position'] = $values['signature_position'] ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; - - $additional = $form_state->getValue(self::ADDITIONAL); - // Clean up states. - $additional[self::STATES] = array_values(array_filter($additional[self::STATES])); - $this->configuration[self::ADDITIONAL] = $additional; } /** @@ -250,9 +213,10 @@ protected function getAttachmentElementOptions(): array { * {@inheritdoc} */ public function preSave(WebformSubmissionInterface $webform_submission) { - $submissionState = $webform_submission->getWebform()->getSetting('results_disabled') ? WebformSubmissionInterface::STATE_COMPLETED : $webform_submission->getState(); - $enabledStates = (array) ($this->configuration[self::ADDITIONAL][self::STATES] ?? NULL); - if (!in_array($submissionState, $enabledStates)) { + // Signing redirects the user to the signing flow, so it only makes sense + // on an actual submission. Drafts are skipped, and a locked submission is + // already signed (signing locks it). + if ($webform_submission->isDraft() || $webform_submission->isLocked()) { return; }