diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ddc05ab..cb36739b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- Moved digital signature configuration (which attachment to sign and the + signature validation text position) from the *OS2Forms Attachment* element + onto the *Digital Signature* webform handler. Includes an automatic + migration update hook for existing webforms. + ## [5.1.0] 2026-06-03 - [PR-326](https://github.com/OS2Forms/os2forms/pull/326) diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php index bbc33f0b..96f9a91e 100644 --- a/modules/os2forms_attachment/src/Element/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Element/AttachmentElement.php @@ -58,6 +58,23 @@ public static function getFileContent(array $element, WebformSubmissionInterface if ($element['#export_type'] === 'pdf') { $file_path = NULL; + // Digital signature settings live on the digital signature handler. + // Resolving them here ensures all consumers of the element (email + // handlers, attachment downloads) serve an already signed document + // and render the validation text consistently. + $elementKey = $element['#webform_key'] ?? NULL; + if ($elementKey !== NULL) { + foreach ($webform_submission->getWebform()->getHandlers('os2forms_digital_signature') as $handler) { + $settings = $handler->getConfiguration()['settings'] ?? []; + if ($handler->isEnabled() && ($settings['attachment_element'] ?? '') === $elementKey) { + $element['#digital_signature'] = TRUE; + $element['#digital_signature_position'] = $settings['signature_position'] + ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; + break; + } + } + } + // If attachment with digital signatur, check if we already have one. if (isset($element['#digital_signature']) && $element['#digital_signature']) { // Get scheme. diff --git a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php index e19c531a..4f2215d6 100644 --- a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php @@ -5,7 +5,6 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\webform\Twig\WebformTwigExtension; use Drupal\webform\Utility\WebformElementHelper; -use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder; use Drupal\webform_attachment\Plugin\WebformElement\WebformAttachmentBase; /** @@ -28,8 +27,6 @@ protected function defineDefaultProperties() { 'view_mode' => 'html', 'template' => '', 'export_type' => '', - 'digital_signature' => '', - 'digital_signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, 'exclude_empty' => '', 'exclude_empty_checkbox' => '', 'excluded_elements' => '', @@ -91,28 +88,6 @@ public function form(array $form, FormStateInterface $form_state) { 'html' => $this->t('HTML'), ], ]; - $form['attachment']['digital_signature'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Digital signature'), - ]; - $form['attachment']['digital_signature_position'] = [ - '#type' => 'select', - '#title' => $this->t('Digital signature position'), - '#description' => $this->t('Select where the digital signature validation text should be placed in the PDF document.'), - '#options' => [ - Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_FOOTER => $this->t('Footer (repeats on every page)'), - Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_HEADER => $this->t('Header (repeats on every page)'), - Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT => $this->t('After content (end of document)'), - Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_BEFORE_CONTENT => $this->t('Before content (start of document)'), - ], - '#default_value' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, - '#states' => [ - 'visible' => [ - ':input[name="properties[digital_signature]"]' => ['checked' => TRUE], - ], - ], - ]; - // Set #access so that help is always visible. WebformElementHelper::setPropertyRecursive($form['attachment']['help'], '#access', TRUE); diff --git a/modules/os2forms_digital_signature/README.md b/modules/os2forms_digital_signature/README.md index d863182e..5dd93e2c 100644 --- a/modules/os2forms_digital_signature/README.md +++ b/modules/os2forms_digital_signature/README.md @@ -13,12 +13,20 @@ The signature server consists of two parts. A frontend module ### Activating Digital Signature -1. Add the OS2forms attachment element to the form. -2. Indicate that the OS2Forms attachment requires a digital signature. -3. Add the Digital Signature Handler to the webform. +1. Add an attachment-style element to the form (either *OS2Forms Attachment* — generated PDF — or + *OS2forms digital signature document* — uploaded PDF). +2. Add the **Digital Signature** handler to the webform. +3. In the handler configuration, pick the attachment element to sign from the **Attachment element to sign** + dropdown, and choose where the signature validation text should be placed in the generated PDF + (the position only applies to *OS2Forms Attachment* elements; it is ignored for uploaded PDFs). 4. If the form requires an email handler, ensure the trigger is set to **...when submission is locked** in the handler’s *Additional settings*. +> [!NOTE] +> Prior to this release, the attachment element exposed `Digital signature` and `Digital signature position` properties +> directly. These settings have moved to the handler. Existing forms are migrated automatically by +> `os2forms_digital_signature_update_10001()`. + ### Flow Explained 1. Upon form submission, a PDF is generated, saved in the private directory, and sent to the signature service via URL. diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml index 29547e43..99abd7c5 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml @@ -5,5 +5,6 @@ package: 'OS2Forms' core_version_requirement: ^9 || ^10 dependencies: - 'webform:webform' + - 'os2forms:os2forms_attachment' configure: os2forms_digital_signature.settings diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.install b/modules/os2forms_digital_signature/os2forms_digital_signature.install new file mode 100644 index 00000000..5c360940 --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.install @@ -0,0 +1,147 @@ +accessCheck(FALSE)->execute()); + $sandbox['migrated'] = 0; + } + + // array_splice() removes the chunk from the sandbox, so the next pass + // continues with the remaining ids. + /** @var \Drupal\webform\WebformInterface[] $webforms */ + $webforms = Webform::loadMultiple(array_splice($sandbox['ids'], 0, 25)); + foreach ($webforms as $webform) { + $handlers = $webform->getHandlers(); + $signatureHandler = NULL; + foreach ($handlers as $handler) { + if ($handler->getPluginId() === 'os2forms_digital_signature') { + $signatureHandler = $handler; + break; + } + } + + $elements = $webform->getElementsDecodedAndFlattened(); + $needsSave = FALSE; + + if ($signatureHandler) { + // Mirror the pre-update runtime selection: the handler signed the + // first os2forms_digital_signature_document element, falling back to + // the first os2forms_attachment element. The #digital_signature + // property never influenced the selection, only the rendering of the + // validation text. + $signedKey = NULL; + $signedPosition = Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; + $candidates = []; + foreach (['os2forms_digital_signature_document', 'os2forms_attachment'] as $type) { + foreach ($elements as $key => $element) { + if (($element['#type'] ?? NULL) === $type) { + $candidates[] = $key; + if ($signedKey === NULL) { + $signedKey = $key; + if ($type === 'os2forms_attachment') { + $signedPosition = $element['#digital_signature_position'] ?? $signedPosition; + } + } + } + } + } + + if ($signedKey === NULL) { + $logger->warning('Skipped webform @id during digital signature config migration: the handler is enabled but the webform has no os2forms_digital_signature_document or os2forms_attachment element. Configure the handler manually.', [ + '@id' => $webform->id(), + ]); + } + else { + if (count($candidates) > 1) { + $logger->warning('Webform @id has multiple signable elements (@all); migrating @chosen to match the pre-update type-priority selection.', [ + '@id' => $webform->id(), + '@all' => implode(', ', $candidates), + '@chosen' => $signedKey, + ]); + } + foreach ($elements as $key => $element) { + if (!empty($element['#digital_signature']) && $key !== $signedKey) { + $logger->warning('Webform @id: element @key had digital signature enabled, but @chosen is migrated instead to match the pre-update behavior.', [ + '@id' => $webform->id(), + '@key' => $key, + '@chosen' => $signedKey, + ]); + } + } + + // Write the new config onto the handler. + $config = $signatureHandler->getConfiguration(); + $config['settings']['attachment_element'] = $signedKey; + $config['settings']['signature_position'] = $signedPosition; + $signatureHandler->setConfiguration($config); + $needsSave = TRUE; + $sandbox['migrated']++; + } + } + + // Strip the legacy properties whether or not the handler is present, so + // no webform is left with inert digital signature configuration. + foreach ($elements as $element) { + if (array_key_exists('#digital_signature', $element) || array_key_exists('#digital_signature_position', $element)) { + $rawElements = $webform->getElementsDecoded(); + _os2forms_digital_signature_strip_legacy_props($rawElements); + $webform->setElements($rawElements); + $needsSave = TRUE; + break; + } + } + + if ($needsSave) { + $webform->save(); + } + } + + // Anything below 1 makes the runner call us again. Done when no ids remain. + $sandbox['#finished'] = empty($sandbox['ids']) ? 1 : 0; + + if ($sandbox['#finished'] >= 1) { + return t('Migrated digital signature config on @count webform(s).', ['@count' => $sandbox['migrated']]); + } +} + +/** + * Recursively remove legacy digital signature element properties. + * + * @param array $elements + * The decoded elements tree, modified in place. + */ +function _os2forms_digital_signature_strip_legacy_props(array &$elements) { + foreach ($elements as $key => &$value) { + if (!is_array($value)) { + continue; + } + if (str_starts_with((string) $key, '#')) { + continue; + } + unset($value['#digital_signature'], $value['#digital_signature_position']); + _os2forms_digital_signature_strip_legacy_props($value); + } +} diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index 9a616bfc..e36046c3 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -7,9 +7,11 @@ use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileUrlGeneratorInterface; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Site\Settings; use Drupal\Core\Url; use Drupal\file\FileRepositoryInterface; +use Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder; use Drupal\os2forms_digital_signature\Service\SigningService; use Drupal\webform\Plugin\WebformElementManagerInterface; use Drupal\webform\Plugin\WebformHandlerBase; @@ -88,6 +90,108 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase { */ private readonly Settings $settings; + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'attachment_element' => '', + 'signature_position' => Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT, + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + $form['attachment_element'] = [ + '#type' => 'select', + '#title' => $this->t('Attachment element to sign'), + '#description' => $this->t('Select the webform element whose generated or uploaded PDF should be signed.'), + '#options' => $this->getAttachmentElementOptions(), + '#empty_option' => $this->t('- Select -'), + '#default_value' => $this->configuration['attachment_element'], + '#required' => TRUE, + ]; + + $form['signature_position'] = [ + '#type' => 'select', + '#title' => $this->t('Signature validation text position'), + '#description' => $this->t('Where the digital signature validation text is placed in the generated PDF. Only applies when the selected element is an OS2Forms Attachment; ignored for uploaded PDF documents.'), + '#options' => [ + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_FOOTER => $this->t('Footer (repeats on every page)'), + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_HEADER => $this->t('Header (repeats on every page)'), + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT => $this->t('After content (end of document)'), + Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_BEFORE_CONTENT => $this->t('Before content (start of document)'), + ], + '#default_value' => $this->configuration['signature_position'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + $values = $form_state->getValues(); + $this->configuration['attachment_element'] = $values['attachment_element'] ?? ''; + $this->configuration['signature_position'] = $values['signature_position'] + ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + $elementKey = $this->configuration['attachment_element'] ?? ''; + $position = $this->configuration['signature_position'] ?? Os2formsAttachmentPrintBuilder::SIGNATURE_POSITION_AFTER_CONTENT; + + return [ + '#markup' => $this->t('Sign attachment: @element
Signature position: @position', [ + '@element' => $elementKey !== '' ? $elementKey : $this->t('not configured'), + '@position' => $position, + ]), + ]; + } + + /** + * Build the dropdown options for the attachment element selector. + * + * The signing flow requires a PDF, so only the two element types that + * provide one are listed: os2forms_digital_signature_document (uploaded + * PDF) and os2forms_attachment (generated PDF). + * + * @return array + * Map of element key => human label. + */ + protected function getAttachmentElementOptions(): array { + $webform = $this->getWebform(); + if (!$webform) { + return []; + } + + $elements = $webform->getElementsInitializedAndFlattened(); + $options = []; + foreach ($webform->getElementsAttachments() as $key) { + $element = $elements[$key] ?? NULL; + if (!$element) { + continue; + } + $type = $element['#type'] ?? ''; + if (!in_array($type, ['os2forms_digital_signature_document', 'os2forms_attachment'], TRUE)) { + continue; + } + $title = $element['#title'] ?? $key; + $options[$key] = sprintf('%s (%s) [%s]', $title, $key, $type); + } + return $options; + } + /** * {@inheritdoc} */ @@ -177,59 +281,53 @@ public function preSave(WebformSubmissionInterface $webform_submission) { /** * Get OS2forms file attachment. * + * Resolves the attachment element configured on the handler, asks its + * plugin for the email attachment payload, and returns the first item. + * * @param \Drupal\webform\WebformSubmissionInterface $webform_submission * A webform submission. * * @return array|null - * Array of attachment data. + * Array of attachment data, or NULL when no attachment is available. * * @throws \Exception */ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_submission) { - $attachments = NULL; - $attachment = NULL; - - // Getting all element types that are added to the webform. - // - // Priority is the following: check for os2forms_digital_signature_document, - // is not found try serving os2forms_attachment. - $elementTypes = array_column($this->getWebform()->getElementsDecodedAndFlattened(), '#type'); - $attachmentType = ''; - if (in_array('os2forms_digital_signature_document', $elementTypes)) { - $attachmentType = 'os2forms_digital_signature_document'; - } - elseif (in_array('os2forms_attachment', $elementTypes)) { - $attachmentType = 'os2forms_attachment'; + $elementKey = $this->configuration['attachment_element'] ?? ''; + if ($elementKey === '') { + $this->logger->error('Digital signature handler has no attachment_element configured for webform %webform.', [ + '%webform' => $this->getWebform()->id(), + ]); + return NULL; } $elements = $this->getWebform()->getElementsInitializedAndFlattened(); - $element_attachments = $this->getWebform()->getElementsAttachments(); - foreach ($element_attachments as $element_attachment) { - // Check if the element attachment key is excluded and should not attach - // any files. - if (isset($this->configuration['excluded_elements'][$element_attachment])) { - continue; - } + if (!isset($elements[$elementKey])) { + $this->logger->error('Configured attachment element %element does not exist on webform %webform.', [ + '%element' => $elementKey, + '%webform' => $this->getWebform()->id(), + ]); + return NULL; + } - $element = $elements[$element_attachment]; + $element = $elements[$elementKey]; - if ($element['#type'] == $attachmentType) { - /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */ - $element_plugin = $this->elementManager->getElementInstance($element); - $attachments = $element_plugin->getEmailAttachments($element, $webform_submission); + /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */ + $element_plugin = $this->elementManager->getElementInstance($element); + $attachments = $element_plugin->getEmailAttachments($element, $webform_submission); - // If we are dealing with an uploaded file, attach the FID. - if ($fid = $webform_submission->getElementData($element_attachment)) { - $attachments[0]['fid'] = $fid; - } - break; - } + if (empty($attachments)) { + return NULL; } - if (!empty($attachments)) { - $attachment = reset($attachments); + // If the source is an uploaded managed file, attach the FID so the + // signed file can replace the upload rather than creating a new file. + if ($fid = $webform_submission->getElementData($elementKey)) { + $attachments[0]['fid'] = $fid; } + $attachment = reset($attachments); + // For SwiftMailer && Mime Mail use filecontent and not the filepath. // @see \Drupal\swiftmailer\Plugin\Mail\SwiftMailer::attachAsMimeMail // @see \Drupal\mimemail\Utility\MimeMailFormatHelper::mimeMailFile