diff --git a/CHANGELOG.md b/CHANGELOG.md index eb688943..374e91ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php index 05d37e4c..8136ea43 100644 --- a/modules/os2forms_attachment/src/Element/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Element/AttachmentElement.php @@ -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; @@ -20,6 +21,7 @@ public function getInfo() { return parent::getInfo() + [ '#view_mode' => 'html', '#export_type' => 'pdf', + '#digital_signature' => FALSE, '#template' => '', ]; } @@ -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); @@ -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. diff --git a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php index 67a0c99a..e9d96dd7 100644 --- a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php +++ b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php @@ -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; @@ -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'); } @@ -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 .= ""; + if ($digitalSignature) { + $generatedHtml .= $this->t('You can validate the signature on this PDF file via validering.nemlog-in.dk.'); + } $print_engine->addPage($generatedHtml); diff --git a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php index 4f2215d6..7ec580bd 100644 --- a/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Plugin/WebformElement/AttachmentElement.php @@ -27,6 +27,7 @@ protected function defineDefaultProperties() { 'view_mode' => 'html', 'template' => '', 'export_type' => '', + 'digital_signature' => '', 'exclude_empty' => '', 'exclude_empty_checkbox' => '', 'excluded_elements' => '', @@ -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); diff --git a/modules/os2forms_digital_signature/README.md b/modules/os2forms_digital_signature/README.md new file mode 100644 index 00000000..1217d6e9 --- /dev/null +++ b/modules/os2forms_digital_signature/README.md @@ -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. diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml new file mode 100644 index 00000000..29547e43 --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml @@ -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 diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.links.menu.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.links.menu.yml new file mode 100644 index 00000000..2fc07d22 --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.links.menu.yml @@ -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 diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.module b/modules/os2forms_digital_signature/os2forms_digital_signature.module new file mode 100644 index 00000000..994aeba3 --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -0,0 +1,72 @@ +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; +} diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml new file mode 100644 index 00000000..c286ad3b --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml @@ -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' + diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml new file mode 100644 index 00000000..d5d1b220 --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml @@ -0,0 +1,4 @@ +services: + os2forms_digital_signature.signing_service: + class: Drupal\os2forms_digital_signature\Service\SigningService + arguments: ['@config.factory'] diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php new file mode 100644 index 00000000..2ee637c6 --- /dev/null +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -0,0 +1,123 @@ +getStorage('webform_submission') + ->loadByProperties(['uuid' => $uuid]); + + // Since loadByProperties returns an array, we need to fetch the first item. + /** @var WebformSubmissionInterface $webformSubmission */ + $webformSubmission = $submissions ? reset($submissions) : NULL; + if (!$webformSubmission) { + // Submission does not exist. + throw new NotFoundHttpException(); + } + + $webformId = $webformSubmission->getWebform()->id(); + + // Checking the action + $action = \Drupal::request()->query->get('action'); + if ($action == 'cancel') { + $cancelUrl = $webformSubmission->getWebform()->toUrl()->toString(); + + // Redirect to the webform confirmation page. + $response = new RedirectResponse($cancelUrl); + return $response; + } + + // Checking hash. + $salt = \Drupal::service('settings')->get('hash_salt'); + $tmpHash = Crypt::hashBase64($uuid . $webformId . $salt); + if ($hash !== $tmpHash) { + // Submission exist, but the provided hash is incorrect. + throw new NotFoundHttpException(); + } + + /** @var SigningService $signingService */ + $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); + + $signedFilename = \Drupal::request()->get('file'); + $signedFileContent = $signingService->download($signedFilename); + if (!$signedFileContent) { + \Drupal::logger('os2forms_digital_signature')->warning('Missing file on remote server %file.', ['%file' => $signedFilename]); + throw new NotFoundHttpException(); + } + + /** @var FileSystemInterface $file_system */ + $file_system = \Drupal::service('file_system'); + + // If $fid is present - we are replacing uploaded/managed file, otherwise creating a new one. + if ($fid) { + $file = File::load($fid); + $expectedFileUri = $file->getFileUri(); + } else { + // Prepare the directory to ensure it exists and is writable. + $expectedFileUri = "private://webform/$webformId/digital_signature/$uuid.pdf"; + $directory = dirname($expectedFileUri); + + if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { + \Drupal::logger('os2forms_digital_signature')->error('Failed to prepare directory %directory.', ['%directory' => $directory]); + } + } + + // Write the data to the file using Drupal's file system service. + try { + $file_system->saveData($signedFileContent, $expectedFileUri , FileSystemInterface::EXISTS_REPLACE); + + // Updating webform submission. + $webformSubmission->setLocked(TRUE); + $webformSubmission->save(); + + // If file existing, resave the file to update the size and etc. + if ($fid) { + File::load($fid)->save(); + } + } + catch (\Exception $e) { + \Drupal::logger('os2forms_digital_signature')->error('Failed to write to file %uri: @message', ['%uri' => $expectedFileUri, '@message' => $e->getMessage()]); + } + + // Build the URL for the webform submission confirmation page. + $confirmation_url = Url::fromRoute('entity.webform.confirmation', [ + 'webform' => $webformId, + 'webform_submission' => $webformSubmission->id(), + ])->toString(); + + // Redirect to the webform confirmation page. + $response = new RedirectResponse($confirmation_url); + return $response; + } + +} diff --git a/modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php b/modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php new file mode 100644 index 00000000..5f27ff79 --- /dev/null +++ b/modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php @@ -0,0 +1,20 @@ + 'textfield', + '#title' => t("Signature server URL"), + '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_remove_service_url'), + '#description' => t('E.g. https://signering.bellcom.dk/sign.php?'), + ]; + $form['os2forms_digital_signature_sign_hash_salt'] = [ + '#type' => 'textfield', + '#title' => t("Hash Salt used for signature"), + '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_sign_hash_salt'), + '#description' => t('Must match hash salt on the signature server'), + ]; + $form['os2forms_digital_signature_submission_allowed_ips'] = [ + '#type' => 'textfield', + '#title' => t("List IP's which can download unsigned PDF submissions"), + '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_submission_allowed_ips'), + '#description' => t('Comma separated. Ex. 192.168.1.1,192.168.2.1'), + ]; + $form['os2forms_digital_signature_submission_retention_period'] = [ + '#type' => 'textfield', + '#title' => t('Unsigned submission timespan (s)'), + '#default_value' => ($this->config(self::$configName)->get('os2forms_digital_signature_submission_retention_period')) ?? 300, + '#description' => t('How many seconds can unsigned submission exist before being automatically deleted'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + + $config = $this->config(self::$configName); + foreach ($values as $key => $value) { + $config->set($key, $value); + } + $config->save(); + + parent::submitForm($form, $form_state); + } +} diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php b/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php new file mode 100644 index 00000000..843a63c9 --- /dev/null +++ b/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php @@ -0,0 +1,83 @@ +t('PDF file for signature'); + return $formats; + } + + /** + * {@inheritdoc} + */ + protected function getFileExtensions(array $element = NULL) { + return 'pdf'; + } + + + /** + * {@inheritdoc} + */ + protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) { + $value = $this->getValue($element, $webform_submission, $options); + $file = $this->getFile($element, $value, $options); + + if (empty($file)) { + return ''; + } + + $format = $this->getItemFormat($element); + switch ($format) { + case 'basename': + case 'extension': + case 'data': + case 'id': + case 'mime': + case 'name': + case 'raw': + case 'size': + case 'url': + case 'value': + return $this->formatTextItem($element, $webform_submission, $options); + + case 'link': + return [ + '#theme' => 'file_link', + '#file' => $file, + ]; + + default: + return [ + '#theme' => 'webform_element_document_file', + '#element' => $element, + '#value' => $value, + '#options' => $options, + '#file' => $file, + ]; + } + } + +} diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php new file mode 100644 index 00000000..2c35b03d --- /dev/null +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -0,0 +1,180 @@ +moduleHandler = $container->get('module_handler'); + $instance->elementManager = $container->get('plugin.manager.webform.element'); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function preSave(WebformSubmissionInterface $webform_submission) { + $webform = $webform_submission->getWebform(); + + if ($webform_submission->isLocked()) { + return; + } + + $attachment = $this->getSubmissionAttachment($webform_submission); + if (!$attachment) { + \Drupal::logger('os2forms_digital_signature')->error('Attachment cannot be created webform: %webform, webform_submission: %webform_submission', ['%webform' => $webform->id(), '%webform_submission' => $webform_submission->uuid()]); + return; + } + + $destinationDir = 'private://signing'; + if (!\Drupal::service('file_system')->prepareDirectory($destinationDir, FileSystemInterface::CREATE_DIRECTORY)) { + \Drupal::logger('os2forms_digital_signature')->error('File directory cannot be created: %filedirectory', ['%filedirectory' => $destinationDir]); + return; + } + + $fileUri = $destinationDir . '/' . $webform_submission->uuid() .'.pdf'; + + // Save the file data. + try { + /** @var FileInterface $fileToSign */ + $fileToSign = \Drupal::service('file.repository')->writeData($attachment['filecontent'], $fileUri, FileSystemInterface::EXISTS_REPLACE); + } + catch (\Exception $e) { + \Drupal::logger('os2forms_digital_signature')->error('File cannot be saved: %fileUri, error: %error', ['%fileUri' => $fileUri, '%error' => $e->getMessage()]); + return; + } + + // Set the status to permanent to prevent file deletion on cron. + //$fileToSign->setPermanent(); + $fileToSign->save(); + $fileToSignPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileToSign->getFileUri()); + + /** @var SigningService $signingService */ + $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); + + $cid = $signingService->get_cid(); + if (empty($cid)) { + \Drupal::logger('os2forms_digital_signature')->error('Failed to obtain cid. Is server running?'); + return; + } + + // Creating hash. + $salt = \Drupal::service('settings')->get('hash_salt'); + $hash = Crypt::hashBase64($webform_submission->uuid() . $webform->id() . $salt); + + $attahchmentFid = $attachment['fid'] ?? NULL; + $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash, 'fid' => $attahchmentFid]); + + // Starting signing, if everything is correct - this funcition will start redirect. + $signingService->sign($fileToSignPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); + } + + /** + * Get OS2forms file attachment. + * + * @param \Drupal\webform\WebformSubmissionInterface $webform_submission + * A webform submission. + * + * @return array|null + * Array of attachment data. + * @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'; + } + + $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; + } + + $element = $elements[$element_attachment]; + + if ($element['#type'] == $attachmentType) { + /** @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)) { + $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 + // @see https://www.drupal.org/project/webform/issues/3232756 + if ($this->moduleHandler->moduleExists('swiftmailer') + || $this->moduleHandler->moduleExists('mimemail')) { + if (isset($attachment['filecontent']) && isset($attachment['filepath'])) { + unset($attachment['filepath']); + } + } + + return $attachment; + } + +} diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php new file mode 100644 index 00000000..b38fee7f --- /dev/null +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -0,0 +1,194 @@ +config = $configFactory->get(SettingsForm::$configName); + } + + /** + * Fetch a new cid. + * + * @return string|NULL + * The correlation id. + */ + public function get_cid() : ?string { + $url = $this->config->get('os2forms_digital_signature_remove_service_url') . 'action=getcid'; + $curl = curl_init($url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + $result = curl_exec($curl); + + $reply = json_decode($result, JSON_OBJECT_AS_ARRAY); + + return $reply['cid'] ?? NULL; + } + + /** + * Sign the document. + * + * Signing is done by redirecting the user's browser to a url on the signing server that takes the user + * through the signing flow. + * + * This function will never return. + * + * @param string $document_uri + * A uri to a file on the local server that we want to sign or the file name on the signing server in the SIGN_PDF_UPLOAD_DIR. + * In case of a local file, it must be prefixed by 'http://' or 'https://' and be readable from the signing server. + * @param string $cid + * The cid made available by the get_cid() function. + * @param string $forward_url + * The url on the local server to forward user to afterwards. + * + * @return void + */ + public function sign(string $document_uri, string $cid, string $forward_url):void { + if (empty($document_uri) || empty($cid) || empty($forward_url)) { + \Drupal::logger('os2forms_digital_signature')->error('Cannot initiate signing process, check params: document_uri: %document_uri, cid: %cid, forward_url: %forward_url', ['%document_uri' => $document_uri, '%cid' => $cid, '%forward_url' => $forward_url]); + return; + } + + $hash = $this->getHash($forward_url); + $params = ['action' => 'sign', 'cid' => $cid, 'hash' => $hash, 'uri' => base64_encode($document_uri), 'forward_url' => base64_encode($forward_url)]; + $url = $this->config->get('os2forms_digital_signature_remove_service_url') . http_build_query($params); + + $response = new RedirectResponse($url); + $response->send(); + } + + /** + * Download the pdf file and return it as a binary string. + * + * @param string $filename + * The filename as given by the signing server. + * @param boolean $leave + * If TRUE, leave the file on the remote server, default is to remove the file after download. + * @param boolean $annotate + * If TRUE, download a pdf with an annotation page. + * @param array $attributes + * An array of pairs of prompts and values that will be added to the annotation box, e.g., + * ['IP' => $_SERVER['REMOTE_ADDR'], 'Region' => 'Capital Region Copenhagen']. + * + * @return mixed|bool + * The binary data of the pdf or FALSE if an error occurred. + */ + public function download(string $filename, $leave = FALSE, $annotate = FALSE, $attributes = []) { + if (empty($filename)) { + return FALSE; + } + if (!preg_match('/^[a-f0-9]{32}\.pdf$/', $filename)) { + return FALSE; + } + $params = ['action' => 'download', 'file' => $filename, 'leave' => $leave, 'annotate' => $annotate, 'attributes' => $attributes]; + $url = $this->config->get('os2forms_digital_signature_remove_service_url') . http_build_query($params); + + $curl = curl_init($url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + $return = curl_exec($curl); + + if (empty($return)) { + return FALSE; + } + elseif (substr($return, 0, 5) !== '%PDF-') { + return FALSE; + } + + return $return; + } + + /** + * Calculate the hash value. + * + * @param string $name + * The value to hash including salt. + * + * @return string + * The hash value (sha1). + */ + private function getHash(string $value) : string { + $hashSalt = $this->config->get('os2forms_digital_signature_sign_hash_salt'); + return sha1($hashSalt . $value); + } + + /** + * Deletes stalled webform submissions that were left unsigned. + * + * Only checked the webforms that have digital_signature handler enabled and the submission is older that a specified + * period. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function deleteStalledSubmissions() : void { + $digitalSignatureWebforms = []; + + // Finding webforms that have any handler. + $query = \Drupal::entityQuery('webform') + ->exists('handlers'); // Only webforms with handlers configured. + $handler_webform_ids = $query->execute(); + + // No webforms with handlers, aborting. + if (empty($handler_webform_ids)) { + return; + } + + // Find all with os2forms_digital_signature handlers enabled. + foreach ($handler_webform_ids as $webform_id) { + $webform = Webform::load($webform_id); + if (!$webform) { + continue; + } + + $handlers = $webform->getHandlers(); + foreach ($handlers as $handler) { + // Check if the handler is of type 'os2forms_digital_signature'. + if ($handler->getPluginId() === 'os2forms_digital_signature' && $handler->isEnabled()) { + $digitalSignatureWebforms[] = $webform->id(); + break; + } + } + } + + // No webforms, aborting. + if (empty($digitalSignatureWebforms)) { + return; + } + + // Find all stalled webform submissions of digital signature forms. + $retention_period = ($this->config->get('os2forms_digital_signature_submission_retention_period')) ?? 300; + $timestamp_threshold = \Drupal::time()->getRequestTime() - $retention_period; + $query = \Drupal::entityQuery('webform_submission') + ->accessCheck(FALSE) + ->condition('webform_id', $digitalSignatureWebforms, 'IN') + ->condition('locked', 0) + ->condition('created', $timestamp_threshold, '<'); + $submission_ids = $query->execute(); + + // No submissions, aborting. + if (empty($submission_ids)) { + return; + } + + // Deleting all stalled webform submissions. + foreach ($submission_ids as $submission_id) { + $submission = WebformSubmission::load($submission_id); + $submission->delete(); + } + } + +}