Skip to content

Digital Signature #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ae97710
DIgital signature first version
stankut Oct 4, 2024
987b6ac
OS-110 code refactoring, security
stankut Oct 15, 2024
53fb77e
OS-110 upload file for signature
stankut Oct 16, 2024
ff96214
Upload digital document
stankut Oct 16, 2024
3be3b70
OS-110 Signing service: Support for adding an extra page with signing…
stankut Nov 19, 2024
f60bfbc
Merge commit '841076066c102e5236808e7a9c020ca6092bc5cd' into f/OS-110
stankut Dec 9, 2024
d06c862
OS-144 - adding return URL
stankut Dec 9, 2024
5d36f4a
OS-145 adding websubmissions automatic cleaning
stankut Dec 27, 2024
8d3b2a7
OS-161 Disabling annotation page by default
stankut Jan 30, 2025
43e5a04
OS-144 Fixing cancel digital signature
stankut Jan 30, 2025
2b4a5a1
Merge commit '7ace38332357344358eca1935f43ae2a30c23fa4' into f/OS-110
stankut Feb 17, 2025
0db0714
OS-167 adding Digital signature validation text
stankut Feb 17, 2025
592ddd1
Merge commit 'eecb47a101ea651f749c7f1bce1ea7313ae3cdce' into f/OS-110
stankut Apr 23, 2025
6d6af9f
Adding module description
stankut Apr 23, 2025
b7e53b9
OS-110 proper dependency injections
stankut May 30, 2025
51706b5
OS-110 refactoring, removing comment lines
stankut May 30, 2025
9abfcbd
OS-110 phpcs formatting
stankut May 30, 2025
3130081
OS-110 phpcs formatting
stankut May 30, 2025
1599ed0
Merge branch 'develop' into f/OS-110
stankut Jun 2, 2025
9ae977f
OS-110 injecting dependency
stankut Jun 2, 2025
e01f0c1
OS-110 refactoring
stankut Jun 9, 2025
23fa488
OS-110 refactoring, service injections
stankut Jun 9, 2025
4681c85
OS-110 refactoring, service injections
stankut Jun 9, 2025
2482846
OS-110 refactoring, phpcs
stankut Jun 9, 2025
7346ebc
Merge branch 'develop' into f/OS-110
stankut Jun 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ before starting to add changes. Use example [placed in the end of the page](#exa
- Fix digital post commands
- Updated versions in GitHub Actions `uses` steps
- Updating the display of os2forms package on the status page
- Adding os2forms_digital_signature module

## [4.0.0] 2025-03-06

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
services:
os2forms_attachment.print_builder:
class: Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder
arguments: ['@entity_print.renderer_factory', '@event_dispatcher', '@string_translation']
arguments: ['@entity_print.renderer_factory', '@event_dispatcher', '@string_translation', '@file_system']
52 changes: 40 additions & 12 deletions modules/os2forms_attachment/src/Element/AttachmentElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public function getInfo() {
return parent::getInfo() + [
'#view_mode' => 'html',
'#export_type' => 'pdf',
'#digital_signature' => FALSE,
'#template' => '',
];
}
Expand All @@ -28,6 +29,8 @@ public function getInfo() {
* {@inheritdoc}
*/
public static function getFileContent(array $element, WebformSubmissionInterface $webform_submission) {
$submissionUuid = $webform_submission->uuid();

// Override webform settings.
static::overrideWebformSettings($element, $webform_submission);

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

if ($element['#export_type'] === 'pdf') {
// Get scheme.
$scheme = 'temporary';

// Get filename.
$file_name = 'webform-entity-print-attachment--' . $webform_submission->getWebform()->id() . '-' . $webform_submission->id() . '.pdf';

// Save printable document.
$print_engine = $print_engine_manager->createSelectedInstance($element['#export_type']);
$temporary_file_path = $print_builder->savePrintable([$webform_submission], $print_engine, $scheme, $file_name);
if ($temporary_file_path) {
$contents = file_get_contents($temporary_file_path);
\Drupal::service('file_system')->delete($temporary_file_path);
$file_path = NULL;

// If attachment with digital signatur, check if we already have one.
if (isset($element['#digital_signature']) && $element['#digital_signature']) {
// Get scheme.
$scheme = 'private';

// Get filename.
$file_name = 'webform/' . $webform_submission->getWebform()->id() . '/digital_signature/' . $submissionUuid . '.pdf';
$file_path = "$scheme://$file_name";
}

if (!$file_path || !file_exists($file_path)) {
// Get scheme.
$scheme = 'temporary';
// Get filename.
$file_name = 'webform-entity-print-attachment--' . $webform_submission->getWebform()->id() . '-' . $webform_submission->id() . '.pdf';

// Save printable document.
$print_engine = $print_engine_manager->createSelectedInstance($element['#export_type']);

// Adding digital signature.
if (isset($element['#digital_signature']) && $element['#digital_signature']) {
$file_path = $print_builder->savePrintableDigitalSignature([$webform_submission], $print_engine, $scheme, $file_name);
}
else {
$file_path = $print_builder->savePrintable([$webform_submission], $print_engine, $scheme, $file_name);
}
}

if ($file_path) {
$contents = file_get_contents($file_path);

// Deleting temporary file.
if ($scheme == 'temporary') {
\Drupal::service('file_system')->delete($file_path);
}
}
else {
// Log error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@
namespace Drupal\os2forms_attachment;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\entity_print\Event\PreSendPrintEvent;
use Drupal\entity_print\Event\PrintEvents;
use Drupal\entity_print\Plugin\PrintEngineInterface;
use Drupal\entity_print\PrintBuilder;
use Drupal\entity_print\Renderer\RendererFactoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
* The OS2Forms attachment print builder service.
*/
class Os2formsAttachmentPrintBuilder extends PrintBuilder {

/**
* {@inheritdoc}
*/
public function __construct(RendererFactoryInterface $renderer_factory, EventDispatcherInterface $event_dispatcher, TranslationInterface $string_translation, protected readonly FileSystemInterface $file_system) {
parent::__construct($renderer_factory, $event_dispatcher, $string_translation);
}

/**
* {@inheritdoc}
*/
Expand All @@ -27,10 +41,56 @@ public function printHtml(EntityInterface $entity, $use_default_css = TRUE, $opt
return $renderer->generateHtml([$entity], $render, $use_default_css, $optimize_css);
}

/**
* Modified version of the original savePrintable() function.
*
* The only difference is modified call to prepareRenderer with digitalPost
* flag TRUE.
*
* @see PrintBuilder::savePrintable()
*
* @return string
* FALSE or the URI to the file. E.g. public://my-file.pdf.
*/
public function savePrintableDigitalSignature(array $entities, PrintEngineInterface $print_engine, $scheme = 'public', $filename = FALSE, $use_default_css = TRUE) {
$renderer = $this->prepareRenderer($entities, $print_engine, $use_default_css, TRUE);

// Allow other modules to alter the generated Print object.
$this->dispatcher->dispatch(new PreSendPrintEvent($print_engine, $entities), PrintEvents::PRE_SEND);

// If we didn't have a URI passed in the generate one.
if (!$filename) {
$filename = $renderer->getFilename($entities) . '.' . $print_engine->getExportType()->getFileExtension();
}

$uri = "$scheme://$filename";

// Save the file.
return $this->file_system->saveData($print_engine->getBlob(), $uri, FileExists::Replace);
}

/**
* {@inheritdoc}
*/
protected function prepareRenderer(array $entities, PrintEngineInterface $print_engine, $use_default_css) {

/**
* Override prepareRenderer() the print engine with the passed entities.
*
* @param array $entities
* An array of entities.
* @param \Drupal\entity_print\Plugin\PrintEngineInterface $print_engine
* The print engine.
* @param bool $use_default_css
* TRUE if we want the default CSS included.
* @param bool $digitalSignature
* If the digital signature message needs to be added.
*
* @return \Drupal\entity_print\Renderer\RendererInterface
* A print renderer.
*
* @see PrintBuilder::prepareRenderer
*/
protected function prepareRenderer(array $entities, PrintEngineInterface $print_engine, $use_default_css, $digitalSignature = FALSE) {
if (empty($entities)) {
throw new \InvalidArgumentException('You must pass at least 1 entity');
}
Expand All @@ -50,6 +110,9 @@ protected function prepareRenderer(array $entities, PrintEngineInterface $print_
// structure. That margin is automatically added in PDF and PDF only.
$generatedHtml = (string) $renderer->generateHtml($entities, $render, $use_default_css, TRUE);
$generatedHtml .= "<style>fieldset legend {margin-left: -12px;}</style>";
if ($digitalSignature) {
$generatedHtml .= $this->t('You can validate the signature on this PDF file via validering.nemlog-in.dk.');
}

$print_engine->addPage($generatedHtml);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ protected function defineDefaultProperties() {
'view_mode' => 'html',
'template' => '',
'export_type' => '',
'digital_signature' => '',
'exclude_empty' => '',
'exclude_empty_checkbox' => '',
'excluded_elements' => '',
Expand Down Expand Up @@ -88,6 +89,11 @@ public function form(array $form, FormStateInterface $form_state) {
'html' => $this->t('HTML'),
],
];
$form['attachment']['digital_signature'] = [
'#type' => 'checkbox',
'#title' => $this->t('Digital signature'),
];

// Set #access so that help is always visible.
WebformElementHelper::setPropertyRecursive($form['attachment']['help'], '#access', TRUE);

Expand Down
40 changes: 40 additions & 0 deletions modules/os2forms_digital_signature/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# OS2Forms Digital Signature module

## Module purpose

This module provides functionality for adding digital signature to the webform PDF submissions.

## How does it work

### 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.
4. If the form requires an email handler, ensure the trigger is set to **...when submission is locked** in the handler’s
*Additional settings*.

### Flow Explained

1. Upon form submission, a PDF is generated, saved in the private directory, and sent to the signature service via URL.
2. The user is redirected to the signature service to provide their signature.
3. After signing, the user is redirected back to the webform solution.
4. The signed PDF is downloaded and stored in Drupal’s private directory.
5. When a submission PDF is requested (e.g., via download link or email), the signed PDF is served instead of generating
a new one on the fly.

## Settings page

URL: `admin/os2forms_digital_signature/settings`

- **Signature server URL**

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?)

- **Hash Salt used for signature**

Must match hash salt on the signature server

- **List IPs which can download unsigned PDF submissions**

Only requests from this IP will be able to download PDF which are to be signed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: 'OS2Forms Digital Signature'
type: module
description: 'Provides digital signature functionality'
package: 'OS2Forms'
core_version_requirement: ^9 || ^10
dependencies:
- 'webform:webform'

configure: os2forms_digital_signature.settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
os2forms_digital_signature.admin.settings:
title: OS2Forms digital signature
description: Configure the OS2Forms digital signature module
parent: system.admin_config_system
route_name: os2forms_digital_signature.settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/**
* @file
* This module enables Digital Signature functionality for Webforms.
*/

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\os2forms_digital_signature\Form\SettingsForm;

/**
* Implements hook_cron().
*
* Deletes stalled webform submissions that were left unsigned.
*/
function os2forms_digital_signature_cron() {
/** @var \Drupal\os2forms_digital_signature\Service\SigningService $service */
$service = \Drupal::service('os2forms_digital_signature.signing_service');
$service->deleteStalledSubmissions();
}

/**
* Implements hook_webform_submission_form_alter().
*
* Replaces submit button title, if digital signature present.
*/
function os2forms_digital_signature_webform_submission_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\webform\WebformSubmissionInterface Interface $webformSubmission */
$webformSubmission = $form_state->getFormObject()->getEntity();
/** @var \Drupal\webform\WebformInterface $webform */
$webform = $webformSubmission->getWebform();

// Checking for os2forms_digital_signature handler presence.
foreach ($webform->getHandlers()->getConfiguration() as $handlerConf) {
if ($handlerConf['id'] == 'os2forms_digital_signature') {
$config = \Drupal::config('webform.settings');
$settings = $config->get('settings');

// Checking if the title has not been overridden.
if ($settings['default_submit_button_label'] == $form['actions']['submit']['#value']) {
$form['actions']['submit']['#value'] = t('Sign and submit');
}
}
}
}

/**
* Implements hook_file_download().
*
* Custom access control for private files.
*/
function os2forms_digital_signature_file_download($uri) {
// Only operate on files in the private directory.
if (StreamWrapperManager::getScheme($uri) === 'private' && str_starts_with(StreamWrapperManager::getTarget($uri), 'signing/')) {
// Get allowed IPs settings.
$config = \Drupal::config(SettingsForm::$configName);
$allowedIps = $config->get('os2forms_digital_signature_submission_allowed_ips');

$allowedIpsArr = explode(',', $allowedIps);
$remoteIp = Drupal::request()->getClientIp();

// IP list is empty, or request IP is allowed.
if (empty($allowedIpsArr) || in_array($remoteIp, $allowedIpsArr)) {
$basename = basename($uri);
return [
'Content-disposition' => 'attachment; filename="' . $basename . '"',
];
}

// Otherwise - Deny access.
return -1;
}

// Not submission file, allow normal access.
return NULL;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Webform os2forms_attachment_component routes.
os2forms_digital_signature.sign_callback:
path: '/os2forms_digital_signature/{uuid}/{hash}/sign_callback/{fid}'
defaults:
_controller: '\Drupal\os2forms_digital_signature\Controller\DigitalSignatureController::signCallback'
fid: ''
requirements:
_permission: 'access content'
os2forms_digital_signature.settings:
path: '/admin/os2forms_digital_signature/settings'
defaults:
_form: '\Drupal\os2forms_digital_signature\Form\SettingsForm'
_title: 'Digital signature settings'
requirements:
_permission: 'administer site configuration'
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
logger.channel.os2forms_digital_signature:
parent: logger.channel_base
arguments: [ 'os2forms_digital_signature' ]

os2forms_digital_signature.signing_service:
class: Drupal\os2forms_digital_signature\Service\SigningService
arguments:
- '@http_client'
- '@datetime.time'
- '@config.factory'
- '@entity_type.manager'
- '@logger.channel.os2forms_digital_signature'
Loading