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 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ before starting to add changes. Use example [placed in the end of the page](#exa
## [Unreleased]

- Updating the display of os2forms package on the status page
- Adding os2forms_digital_signature module

## [4.0.0] 2025-03-06

Expand Down
53 changes: 41 additions & 12 deletions modules/os2forms_attachment/src/Element/AttachmentElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Drupal\os2forms_attachment\Element;

use Drupal\Core\File\FileSystemInterface;
use Drupal\webform\Entity\WebformSubmission;
use Drupal\webform\WebformSubmissionInterface;
use Drupal\webform_attachment\Element\WebformAttachmentBase;
Expand All @@ -20,6 +21,7 @@ public function getInfo() {
return parent::getInfo() + [
'#view_mode' => 'html',
'#export_type' => 'pdf',
'#digital_signature' => FALSE,
'#template' => '',
];
}
Expand All @@ -28,6 +30,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 +55,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,6 +3,9 @@
namespace Drupal\os2forms_attachment;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\File\FileExists;
use Drupal\entity_print\Event\PreSendPrintEvent;
use Drupal\entity_print\Event\PrintEvents;
use Drupal\entity_print\Plugin\PrintEngineInterface;
use Drupal\entity_print\PrintBuilder;

Expand All @@ -27,10 +30,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 \Drupal::service('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.
*
* @see PrintBuilder::prepareRenderer
*
* @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.
*/
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 +99,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?


- **Hash Salt used for signature**

Must match hash salt on the signature server


- **List IP's 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,72 @@
<?php

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,16 @@
# 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,4 @@
services:
os2forms_digital_signature.signing_service:
class: Drupal\os2forms_digital_signature\Service\SigningService
arguments: ['@config.factory']
Loading
Loading