Skip to content

Commit 1b771fa

Browse files
authored
Merge pull request #167 from OS2Forms/f/OS-110
Digital Signature
2 parents 1bb0188 + 7346ebc commit 1b771fa

17 files changed

+1127
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ before starting to add changes. Use example [placed in the end of the page](#exa
2525
- Fix digital post commands
2626
- Updated versions in GitHub Actions `uses` steps
2727
- Updating the display of os2forms package on the status page
28+
- Adding os2forms_digital_signature module
2829

2930
## [4.0.0] 2025-03-06
3031

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
services:
22
os2forms_attachment.print_builder:
33
class: Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder
4-
arguments: ['@entity_print.renderer_factory', '@event_dispatcher', '@string_translation']
4+
arguments: ['@entity_print.renderer_factory', '@event_dispatcher', '@string_translation', '@file_system']

modules/os2forms_attachment/src/Element/AttachmentElement.php

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public function getInfo() {
2020
return parent::getInfo() + [
2121
'#view_mode' => 'html',
2222
'#export_type' => 'pdf',
23+
'#digital_signature' => FALSE,
2324
'#template' => '',
2425
];
2526
}
@@ -28,6 +29,8 @@ public function getInfo() {
2829
* {@inheritdoc}
2930
*/
3031
public static function getFileContent(array $element, WebformSubmissionInterface $webform_submission) {
32+
$submissionUuid = $webform_submission->uuid();
33+
3134
// Override webform settings.
3235
static::overrideWebformSettings($element, $webform_submission);
3336

@@ -51,18 +54,43 @@ public static function getFileContent(array $element, WebformSubmissionInterface
5154
\Drupal::request()->request->set('_webform_submissions_view_mode', $view_mode);
5255

5356
if ($element['#export_type'] === 'pdf') {
54-
// Get scheme.
55-
$scheme = 'temporary';
56-
57-
// Get filename.
58-
$file_name = 'webform-entity-print-attachment--' . $webform_submission->getWebform()->id() . '-' . $webform_submission->id() . '.pdf';
59-
60-
// Save printable document.
61-
$print_engine = $print_engine_manager->createSelectedInstance($element['#export_type']);
62-
$temporary_file_path = $print_builder->savePrintable([$webform_submission], $print_engine, $scheme, $file_name);
63-
if ($temporary_file_path) {
64-
$contents = file_get_contents($temporary_file_path);
65-
\Drupal::service('file_system')->delete($temporary_file_path);
57+
$file_path = NULL;
58+
59+
// If attachment with digital signatur, check if we already have one.
60+
if (isset($element['#digital_signature']) && $element['#digital_signature']) {
61+
// Get scheme.
62+
$scheme = 'private';
63+
64+
// Get filename.
65+
$file_name = 'webform/' . $webform_submission->getWebform()->id() . '/digital_signature/' . $submissionUuid . '.pdf';
66+
$file_path = "$scheme://$file_name";
67+
}
68+
69+
if (!$file_path || !file_exists($file_path)) {
70+
// Get scheme.
71+
$scheme = 'temporary';
72+
// Get filename.
73+
$file_name = 'webform-entity-print-attachment--' . $webform_submission->getWebform()->id() . '-' . $webform_submission->id() . '.pdf';
74+
75+
// Save printable document.
76+
$print_engine = $print_engine_manager->createSelectedInstance($element['#export_type']);
77+
78+
// Adding digital signature.
79+
if (isset($element['#digital_signature']) && $element['#digital_signature']) {
80+
$file_path = $print_builder->savePrintableDigitalSignature([$webform_submission], $print_engine, $scheme, $file_name);
81+
}
82+
else {
83+
$file_path = $print_builder->savePrintable([$webform_submission], $print_engine, $scheme, $file_name);
84+
}
85+
}
86+
87+
if ($file_path) {
88+
$contents = file_get_contents($file_path);
89+
90+
// Deleting temporary file.
91+
if ($scheme == 'temporary') {
92+
\Drupal::service('file_system')->delete($file_path);
93+
}
6694
}
6795
else {
6896
// Log error.

modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,28 @@
33
namespace Drupal\os2forms_attachment;
44

55
use Drupal\Core\Entity\EntityInterface;
6+
use Drupal\Core\File\FileExists;
7+
use Drupal\Core\File\FileSystemInterface;
8+
use Drupal\Core\StringTranslation\TranslationInterface;
9+
use Drupal\entity_print\Event\PreSendPrintEvent;
10+
use Drupal\entity_print\Event\PrintEvents;
611
use Drupal\entity_print\Plugin\PrintEngineInterface;
712
use Drupal\entity_print\PrintBuilder;
13+
use Drupal\entity_print\Renderer\RendererFactoryInterface;
14+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
815

916
/**
1017
* The OS2Forms attachment print builder service.
1118
*/
1219
class Os2formsAttachmentPrintBuilder extends PrintBuilder {
1320

21+
/**
22+
* {@inheritdoc}
23+
*/
24+
public function __construct(RendererFactoryInterface $renderer_factory, EventDispatcherInterface $event_dispatcher, TranslationInterface $string_translation, protected readonly FileSystemInterface $file_system) {
25+
parent::__construct($renderer_factory, $event_dispatcher, $string_translation);
26+
}
27+
1428
/**
1529
* {@inheritdoc}
1630
*/
@@ -27,10 +41,56 @@ public function printHtml(EntityInterface $entity, $use_default_css = TRUE, $opt
2741
return $renderer->generateHtml([$entity], $render, $use_default_css, $optimize_css);
2842
}
2943

44+
/**
45+
* Modified version of the original savePrintable() function.
46+
*
47+
* The only difference is modified call to prepareRenderer with digitalPost
48+
* flag TRUE.
49+
*
50+
* @see PrintBuilder::savePrintable()
51+
*
52+
* @return string
53+
* FALSE or the URI to the file. E.g. public://my-file.pdf.
54+
*/
55+
public function savePrintableDigitalSignature(array $entities, PrintEngineInterface $print_engine, $scheme = 'public', $filename = FALSE, $use_default_css = TRUE) {
56+
$renderer = $this->prepareRenderer($entities, $print_engine, $use_default_css, TRUE);
57+
58+
// Allow other modules to alter the generated Print object.
59+
$this->dispatcher->dispatch(new PreSendPrintEvent($print_engine, $entities), PrintEvents::PRE_SEND);
60+
61+
// If we didn't have a URI passed in the generate one.
62+
if (!$filename) {
63+
$filename = $renderer->getFilename($entities) . '.' . $print_engine->getExportType()->getFileExtension();
64+
}
65+
66+
$uri = "$scheme://$filename";
67+
68+
// Save the file.
69+
return $this->file_system->saveData($print_engine->getBlob(), $uri, FileExists::Replace);
70+
}
71+
3072
/**
3173
* {@inheritdoc}
3274
*/
33-
protected function prepareRenderer(array $entities, PrintEngineInterface $print_engine, $use_default_css) {
75+
76+
/**
77+
* Override prepareRenderer() the print engine with the passed entities.
78+
*
79+
* @param array $entities
80+
* An array of entities.
81+
* @param \Drupal\entity_print\Plugin\PrintEngineInterface $print_engine
82+
* The print engine.
83+
* @param bool $use_default_css
84+
* TRUE if we want the default CSS included.
85+
* @param bool $digitalSignature
86+
* If the digital signature message needs to be added.
87+
*
88+
* @return \Drupal\entity_print\Renderer\RendererInterface
89+
* A print renderer.
90+
*
91+
* @see PrintBuilder::prepareRenderer
92+
*/
93+
protected function prepareRenderer(array $entities, PrintEngineInterface $print_engine, $use_default_css, $digitalSignature = FALSE) {
3494
if (empty($entities)) {
3595
throw new \InvalidArgumentException('You must pass at least 1 entity');
3696
}
@@ -50,6 +110,9 @@ protected function prepareRenderer(array $entities, PrintEngineInterface $print_
50110
// structure. That margin is automatically added in PDF and PDF only.
51111
$generatedHtml = (string) $renderer->generateHtml($entities, $render, $use_default_css, TRUE);
52112
$generatedHtml .= "<style>fieldset legend {margin-left: -12px;}</style>";
113+
if ($digitalSignature) {
114+
$generatedHtml .= $this->t('You can validate the signature on this PDF file via validering.nemlog-in.dk.');
115+
}
53116

54117
$print_engine->addPage($generatedHtml);
55118

modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ protected function defineDefaultProperties() {
2727
'view_mode' => 'html',
2828
'template' => '',
2929
'export_type' => '',
30+
'digital_signature' => '',
3031
'exclude_empty' => '',
3132
'exclude_empty_checkbox' => '',
3233
'excluded_elements' => '',
@@ -88,6 +89,11 @@ public function form(array $form, FormStateInterface $form_state) {
8889
'html' => $this->t('HTML'),
8990
],
9091
];
92+
$form['attachment']['digital_signature'] = [
93+
'#type' => 'checkbox',
94+
'#title' => $this->t('Digital signature'),
95+
];
96+
9197
// Set #access so that help is always visible.
9298
WebformElementHelper::setPropertyRecursive($form['attachment']['help'], '#access', TRUE);
9399

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# OS2Forms Digital Signature module
2+
3+
## Module purpose
4+
5+
This module provides functionality for adding digital signature to the webform PDF submissions.
6+
7+
## How does it work
8+
9+
### Activating Digital Signature
10+
11+
1. Add the OS2forms attachment element to the form.
12+
2. Indicate that the OS2Forms attachment requires a digital signature.
13+
3. Add the Digital Signature Handler to the webform.
14+
4. If the form requires an email handler, ensure the trigger is set to **...when submission is locked** in the handler’s
15+
*Additional settings*.
16+
17+
### Flow Explained
18+
19+
1. Upon form submission, a PDF is generated, saved in the private directory, and sent to the signature service via URL.
20+
2. The user is redirected to the signature service to provide their signature.
21+
3. After signing, the user is redirected back to the webform solution.
22+
4. The signed PDF is downloaded and stored in Drupal’s private directory.
23+
5. When a submission PDF is requested (e.g., via download link or email), the signed PDF is served instead of generating
24+
a new one on the fly.
25+
26+
## Settings page
27+
28+
URL: `admin/os2forms_digital_signature/settings`
29+
30+
- **Signature server URL**
31+
32+
The URL of the service providing digital signature. This is the example of a known service [https://signering.bellcom.dk/sign.php?](https://signering.bellcom.dk/sign.php?)
33+
34+
- **Hash Salt used for signature**
35+
36+
Must match hash salt on the signature server
37+
38+
- **List IPs which can download unsigned PDF submissions**
39+
40+
Only requests from this IP will be able to download PDF which are to be signed.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: 'OS2Forms Digital Signature'
2+
type: module
3+
description: 'Provides digital signature functionality'
4+
package: 'OS2Forms'
5+
core_version_requirement: ^9 || ^10
6+
dependencies:
7+
- 'webform:webform'
8+
9+
configure: os2forms_digital_signature.settings
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
os2forms_digital_signature.admin.settings:
2+
title: OS2Forms digital signature
3+
description: Configure the OS2Forms digital signature module
4+
parent: system.admin_config_system
5+
route_name: os2forms_digital_signature.settings
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* This module enables Digital Signature functionality for Webforms.
6+
*/
7+
8+
use Drupal\Core\Form\FormStateInterface;
9+
use Drupal\Core\StreamWrapper\StreamWrapperManager;
10+
use Drupal\os2forms_digital_signature\Form\SettingsForm;
11+
12+
/**
13+
* Implements hook_cron().
14+
*
15+
* Deletes stalled webform submissions that were left unsigned.
16+
*/
17+
function os2forms_digital_signature_cron() {
18+
/** @var \Drupal\os2forms_digital_signature\Service\SigningService $service */
19+
$service = \Drupal::service('os2forms_digital_signature.signing_service');
20+
$service->deleteStalledSubmissions();
21+
}
22+
23+
/**
24+
* Implements hook_webform_submission_form_alter().
25+
*
26+
* Replaces submit button title, if digital signature present.
27+
*/
28+
function os2forms_digital_signature_webform_submission_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
29+
/** @var \Drupal\webform\WebformSubmissionInterface Interface $webformSubmission */
30+
$webformSubmission = $form_state->getFormObject()->getEntity();
31+
/** @var \Drupal\webform\WebformInterface $webform */
32+
$webform = $webformSubmission->getWebform();
33+
34+
// Checking for os2forms_digital_signature handler presence.
35+
foreach ($webform->getHandlers()->getConfiguration() as $handlerConf) {
36+
if ($handlerConf['id'] == 'os2forms_digital_signature') {
37+
$config = \Drupal::config('webform.settings');
38+
$settings = $config->get('settings');
39+
40+
// Checking if the title has not been overridden.
41+
if ($settings['default_submit_button_label'] == $form['actions']['submit']['#value']) {
42+
$form['actions']['submit']['#value'] = t('Sign and submit');
43+
}
44+
}
45+
}
46+
}
47+
48+
/**
49+
* Implements hook_file_download().
50+
*
51+
* Custom access control for private files.
52+
*/
53+
function os2forms_digital_signature_file_download($uri) {
54+
// Only operate on files in the private directory.
55+
if (StreamWrapperManager::getScheme($uri) === 'private' && str_starts_with(StreamWrapperManager::getTarget($uri), 'signing/')) {
56+
// Get allowed IPs settings.
57+
$config = \Drupal::config(SettingsForm::$configName);
58+
$allowedIps = $config->get('os2forms_digital_signature_submission_allowed_ips');
59+
60+
$allowedIpsArr = explode(',', $allowedIps);
61+
$remoteIp = Drupal::request()->getClientIp();
62+
63+
// IP list is empty, or request IP is allowed.
64+
if (empty($allowedIpsArr) || in_array($remoteIp, $allowedIpsArr)) {
65+
$basename = basename($uri);
66+
return [
67+
'Content-disposition' => 'attachment; filename="' . $basename . '"',
68+
];
69+
}
70+
71+
// Otherwise - Deny access.
72+
return -1;
73+
}
74+
75+
// Not submission file, allow normal access.
76+
return NULL;
77+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Webform os2forms_attachment_component routes.
2+
os2forms_digital_signature.sign_callback:
3+
path: '/os2forms_digital_signature/{uuid}/{hash}/sign_callback/{fid}'
4+
defaults:
5+
_controller: '\Drupal\os2forms_digital_signature\Controller\DigitalSignatureController::signCallback'
6+
fid: ''
7+
requirements:
8+
_permission: 'access content'
9+
os2forms_digital_signature.settings:
10+
path: '/admin/os2forms_digital_signature/settings'
11+
defaults:
12+
_form: '\Drupal\os2forms_digital_signature\Form\SettingsForm'
13+
_title: 'Digital signature settings'
14+
requirements:
15+
_permission: 'administer site configuration'

0 commit comments

Comments
 (0)