From ae977101b925f35b80bbaccf03923100b70b9ab2 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 4 Oct 2024 14:55:59 +0300 Subject: [PATCH 01/11] DIgital signature first version --- .../src/Element/AttachmentElement.php | 46 ++- .../WebformElement/AttachmentElement.php | 6 + .../os2forms_digital_signature.info.yml | 9 + .../os2forms_digital_signature.module | 28 ++ .../os2forms_digital_signature.routing.yml | 18 ++ .../os2forms_digital_signature.services.yml | 4 + .../Controller/DigitalSignatureController.php | 139 +++++++++ .../DigitalSignatureWebformHandler.php | 293 ++++++++++++++++++ .../src/Service/SigningService.php | 156 ++++++++++ .../src/Service/SigningUtil.php | 93 ++++++ 10 files changed, 780 insertions(+), 12 deletions(-) create mode 100644 modules/os2forms_digital_signature/os2forms_digital_signature.info.yml create mode 100644 modules/os2forms_digital_signature/os2forms_digital_signature.module create mode 100644 modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml create mode 100644 modules/os2forms_digital_signature/os2forms_digital_signature.services.yml create mode 100644 modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php create mode 100644 modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php create mode 100644 modules/os2forms_digital_signature/src/Service/SigningService.php create mode 100644 modules/os2forms_digital_signature/src/Service/SigningUtil.php diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php index 05d37e4c..43326d20 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,36 @@ 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']); + $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/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/os2forms_digital_signature.info.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml new file mode 100644 index 00000000..d744350b --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml @@ -0,0 +1,9 @@ +name: 'OS2Forms Digital Signature' +type: module +description: 'todo' +package: 'OS2Forms' +core_version_requirement: ^9 || ^10 +dependencies: + - 'webform:webform' + +configure: os2forms_digital_post.admin.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..025c20da --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -0,0 +1,28 @@ +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'); + } + } + } +} 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..54e01a74 --- /dev/null +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml @@ -0,0 +1,18 @@ +# Webform os2forms_attachment_component routes. +os2forms_digital_signature.sign_callback: + path: '/os2forms_digital_signature/{uuid}/{hash}/sign_callback' + defaults: + _controller: '\Drupal\os2forms_digital_signature\Controller\DigitalSignatureController::signCallback' + requirements: + _permission: 'access content' + +os2forms_digital_signature.test: + path: '/os2forms_digital_signature/{webform_submission}/test' + defaults: + _controller: '\Drupal\os2forms_digital_signature\Controller\DigitalSignatureController::test' +# _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title' +# view_mode: 'html' +# operation: webform_submission_view +# entity_access: 'webform_submission.view' + requirements: + _permission: 'access content' 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..fa1587e6 --- /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: [] 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..8e2ebaf4 --- /dev/null +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -0,0 +1,139 @@ +getStorage('webform_submission') + ->loadByProperties(['uuid' => $uuid]); + + // Since loadByProperties returns an array, we need to fetch the first item. + $webformSubmission = $submissions ? reset($submissions) : NULL; + if (!$webformSubmission) { + throw new NotFoundHttpException(); + } + + $webformId = $webformSubmission->getWebform()->id(); + + // Checking hash. + $salt = \Drupal::service('settings')->get('hash_salt'); + $tmpHash = Crypt::hashBase64($uuid . $webformId . $salt); + if ($hash !== $tmpHash) { + throw new NotFoundHttpException(); + } + + /** @var SigningService $signingService */ + $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); + + $signeFilename = \Drupal::request()->get('file'); + $signedFileContent = $signingService->download($signeFilename); + if (!$signedFileContent) { + throw new NotFoundHttpException(); + } + + // Prepare the directory to ensure it exists and is writable. + $file_system = \Drupal::service('file_system'); + $expectedFileUri = "private://webform/$webformId/digital_signature/$uuid.pdf"; + $directory = dirname($expectedFileUri); + + if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { + // TODO: throw error. + //\Drupal::logger('my_module')->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); + } + catch (\Exception $e) { + // TODO: throw error. + //\Drupal::logger('my_module')->error('Failed to write to file %uri: @message', ['%uri' => $uri, '@message' => $e->getMessage()]); + } + + // Updating webform submission. + $webformSubmission->setLocked(TRUE); + $webformSubmission->save(); + + // 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; + } + + + + + public function test(WebformSubmission $webform_submission) { + $webformId = $webform_submission->getWebform()->id(); + $sid = $webform_submission->id(); + + $fileUri = "private://webform/$webformId/digital_signature/$sid.pdf"; +// $webform_submission->resave(); +// dpm('Done'); + + /** @var SigningService $signingService */ + $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); + + $signeFilename = \Drupal::request()->get('file'); + $signedFileContent = $signingService->download($signeFilename); + + // Get the FileSystem service. + $file_system = \Drupal::service('file_system'); + + // Prepare the directory to ensure it exists and is writable. + $directory = dirname($fileUri); + if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { + //\Drupal::logger('my_module')->error('Failed to prepare directory %directory.', ['%directory' => $directory]); + } + + // Write the data to the file using Drupal's file system service. + try { + $result = $file_system->saveData($signedFileContent, $fileUri , FileSystemInterface::EXISTS_REPLACE); + } + catch (\Exception $e) { + //\Drupal::logger('my_module')->error('Failed to write to file %uri: @message', ['%uri' => $uri, '@message' => $e->getMessage()]); + } + + $webform_submission->setLocked(TRUE); + $webform_submission->save(); + + // Build the URL for the webform confirmation page. + $confirmation_url = Url::fromRoute('entity.webform.confirmation', [ + 'webform' => $webformId, + 'webform_submission' => $sid, + ])->toString(); + + // Redirect to the webform confirmation page. + $response = new RedirectResponse($confirmation_url); + return $response; + } +} 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..9e9d7cd3 --- /dev/null +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -0,0 +1,293 @@ +renderer = $container->get('renderer'); + $instance->moduleHandler = $container->get('module_handler'); + $instance->elementManager = $container->get('plugin.manager.webform.element'); + return $instance; + } + +// /** +// * {@inheritdoc} +// */ +// public function defaultConfiguration() { +// return [ +// 'format' => 'yaml', +// 'submission' => FALSE, +// ]; +// } + +// /** +// * {@inheritdoc} +// */ +// public function getSummary() { +// $settings = $this->getSettings(); +// switch ($settings['format']) { +// case static::FORMAT_JSON: +// $settings['format'] = $this->t('JSON'); +// break; +// +// case static::FORMAT_YAML: +// default: +// $settings['format'] = $this->t('YAML'); +// break; +// } +// return [ +// '#settings' => $settings, +// ] + parent::getSummary(); +// } +// +// /** +// * {@inheritdoc} +// */ +// public function buildConfigurationForm(array $form, FormStateInterface $form_state) { +// $form['debug_settings'] = [ +// '#type' => 'fieldset', +// '#title' => $this->t('Debug settings'), +// ]; +// $form['debug_settings']['format'] = [ +// '#type' => 'select', +// '#title' => $this->t('Data format'), +// '#options' => [ +// static::FORMAT_YAML => $this->t('YAML'), +// static::FORMAT_JSON => $this->t('JSON'), +// ], +// '#default_value' => $this->configuration['format'], +// ]; +// $form['debug_settings']['submission'] = [ +// '#type' => 'checkbox', +// '#title' => $this->t('Include submission properties'), +// '#description' => $this->t('If checked, all submission properties and values will be included in the displayed debug information. This includes sid, created, updated, completed, and more.'), +// '#return_value' => TRUE, +// '#default_value' => $this->configuration['submission'], +// ]; +// return $this->setSettingsParents($form); +// } + +// /** +// * {@inheritdoc} +// */ +// public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { +// parent::submitConfigurationForm($form, $form_state); +// $this->applyFormStateToConfiguration($form_state); +// } +// +// /** +// * {@inheritdoc} +// */ +// public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { +// $settings = $this->getSettings(); +// +// $data = ($settings['submission']) +// ? $webform_submission->toArray(TRUE) +// : $webform_submission->getData(); +// WebformElementHelper::convertRenderMarkupToStrings($data); +// +// $label = ($settings['submission']) +// ? $this->t('Submitted properties and values are:') +// : $this->t('Submitted values are:'); +// +// $build = [ +// 'label' => ['#markup' => $label], +// 'data' => [ +// '#markup' => ($settings['format'] === static::FORMAT_JSON) +// ? json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_PRETTY_PRINT) +// : WebformYaml::encode($data), +// '#prefix' => '
',
+//        '#suffix' => '
', +// ], +// ]; +// $message = $this->renderer->renderPlain($build); +// +// $this->messenger()->addWarning($message); +// } + + /** + * {@inheritdoc} + */ + public function preSave(WebformSubmissionInterface $webform_submission) { + if ($webform_submission->isLocked()) { + return; + } + + $attachments = $this->getSubmissionAttachment($webform_submission); + //$destination = 'private://webform/signing' . $webform_submission->uuid() .'.pdf'; + //$pdfToSign = file_put_contents($destination, $attachment['filecontent'], FILE_APPEND); + + // TODO: think about file URL protection. + $destinationDir = 'public://signing'; + \Drupal::service('file_system')->prepareDirectory($destinationDir, FileSystemInterface::CREATE_DIRECTORY); + + $destination = $destinationDir . '/' . $webform_submission->uuid() .'.pdf'; + + // Save the file data. + /** @var FileInterface $fileSubmissionPdf */ + $fileSubmissionPdf = \Drupal::service('file.repository')->writeData($attachments[0]['filecontent'], $destination, FileSystemInterface::EXISTS_REPLACE); + + if ($fileSubmissionPdf) { + // Set the status to permanent to prevent file deletion on cron. + //$fileSubmissionPdf->setPermanent(); + + $fileSubmissionPdf->save(); + $submissionPdfPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileSubmissionPdf->getFileUri()); + } + + if ($submissionPdfPublicUrl) { + // For testing. + //$submissionPdfPublicUrl = 'https://signering.bellcom.dk/test/test-form.pdf'; + + /** @var SigningService $signingService */ + $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); + + $cid = $signingService->get_cid(); + if (empty($cid)) { + die('Failed to obtain cid. Is server running?'); + } + + // Creating hash. + $salt = \Drupal::service('settings')->get('hash_salt'); + $hash = Crypt::hashBase64($webform_submission->uuid() . $webform_submission->getWebform()->id() . $salt); + + $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash]); + + // Starting signing + $signingService->sign($submissionPdfPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); + } + } + + + public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) { + return; + + if ($webform_submission->isLocked()) { + return; + } +// Getting attachments. + $attachments = $this->getMessageAttachments($webform_submission); + dpm($attachments); + return; +// +// // Getting attachment as file. TODO: is there a better way to do it? +// $data = $attachments[0]['filecontent']; +// $destination = 'sites/default/files/teststan' . $webform_submission->id() .'.pdf'; + $submissionPdfPublicUrl = 'https://signering.bellcom.dk/test/test-form.pdf'; + + +// // Write data to the file. +// $result = file_put_contents($destination, $data, FILE_APPEND); +//// $response = \Drupal::httpClient()->get($url, ['sink' => $destination]); +// + /** @var SigningService $signingService */ + $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); + + $cid = $signingService->get_cid(); + if(empty($cid)) { + die('Failed to obtain cid. Is server running?'); + } + + $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.test', ['webform_submission' => $webform_submission->id()]); + + // Starting signing + $signingService->sign($submissionPdfPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); + + // Making redirect. +// $response = new RedirectResponse('https://google.com'); +// $response->send(); + +// $response = new RedirectResponse($url->setAbsolute()->toString()); +// $response->send(); + +// $webform_submission->resave(); + } + + /** + * Get OS2forms file attachment. + * + * @param \Drupal\webform\WebformSubmissionInterface $webform_submission + * A webform submission. + * + * @return array|null + * Array of attachment data. + */ + protected function getSubmissionAttachment(WebformSubmissionInterface $webform_submission) { + $attachment = 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; + } + + $element = $elements[$element_attachment]; + if ($element['#type'] == 'os2forms_attachment') { + /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */ + $element_plugin = $this->elementManager->getElementInstance($element); + $attachment = $element_plugin->getEmailAttachments($element, $webform_submission); + } + } + + // 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..31b3dc5b --- /dev/null +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -0,0 +1,156 @@ +SIGN_REMOTE_SERVICE_URL . 'action=getcid'; + $curl = curl_init($url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + $result = curl_exec($curl); + + $this->reply = json_decode($result, JSON_OBJECT_AS_ARRAY); + + return $this->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. + * @param bool $leave + * Leave the pdf file on the remote server. + * + * @throws SignParameterException + * Empty url or cid given. + */ + public function sign(string $document_uri, string $cid, string $forward_url, bool $leave = FALSE) { + if(empty($document_uri) || empty($cid) || empty($forward_url)) { + //throw new SignParameterException(); + } + + $hash = SigningUtil::get_hash($forward_url); + $params = ['action' => 'sign', 'cid' => $cid, 'hash' => $hash, 'uri' => base64_encode($document_uri), 'forward_url' => base64_encode($forward_url)]; + $url = $this->SIGN_REMOTE_SERVICE_URL . http_build_query($params); + + SigningUtil::url_forward($url); + } + + /** + * Verify the document. + * + * Verifying is done by redirecting the user's browser to a url on the signing server that takes the user + * through the verify flow. + * + * This function will never return. + * + * @param string $forward_url + * A url to a file on the local server that we want to sign or the full file name on the signing server. + * In case of a local file, it must be prefixed by 'http://' or 'https://' and be readable from the signing server. + * + * @throws SignParameterException + * Empty url or cid given. + * + * @todo Verifying the pdf is yet to be implemented on the signing server. + */ + public function verify(string $document_uri, string $cid, string $forward_url) { + SigningUtil::logger('Verify unimplemented!', 'WARNING'); + if(empty($forward_url)) { + //throw new SignParameterException(); + } + + $hash = SigningUtil::get_hash($forward_url); + $params = ['action' => 'verify', 'hash' => $hash, 'uri' => base64_encode($document_uri), 'forward_url' => base64_encode($forward_url)]; + $url = $this->SIGN_REMOTE_SERVICE_URL . http_build_query($params); + + SigningUtil::url_forward($url); + } + + /** + * 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. + * + * @return mixed + * The binary data of the pdf or an array if an error occured. + */ + public function download(string $filename, $leave = FALSE) { + if (empty($filename)) { + return FALSE; + //throw new SignParameterException('Filename cannot be empty'); + } + if (!preg_match('/^[a-f0-9]{32}\.pdf$/', $filename)) { + return FALSE; + //throw new SignParameterException('Incorrect filename given'); + } + $params = ['action' => 'download', 'file' => $filename, 'leave' => $leave]; + $url = $this->SIGN_REMOTE_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; + //$return = ['error' => TRUE, 'message' => 'Empty file']; + } + elseif (substr($return, 0, 5) !== '%PDF-') { + return FALSE; + //$return = ['error' => TRUE, 'message' => 'Not a PDF file']; + } + + return $return; + } + + /** + * Download the pdf file and send it to the user's browser. + * + * @param string $filename + * The filename. + * + * @throws SignException + */ + public function view(string $filename) { + $pdf = $this->download($filename); + if(is_array($pdf)) { + print 'Unable to view file: ' . $pdf['message']; + return; + } + + header('Content-Type: application/pdf'); + header('Content-Length: ' . strlen($pdf)); + + print $pdf; + } +} diff --git a/modules/os2forms_digital_signature/src/Service/SigningUtil.php b/modules/os2forms_digital_signature/src/Service/SigningUtil.php new file mode 100644 index 00000000..89475a72 --- /dev/null +++ b/modules/os2forms_digital_signature/src/Service/SigningUtil.php @@ -0,0 +1,93 @@ +send(); + +// header("location: $url"); +// print "\n"; +// +// die(); + } + + /** + * Write a message to the log file. + * + * @param string $message + * The message to write. + * @param string $type + * One of 'INFO', 'WARNING' or 'ERROR'. + */ + public static function logger(string $message, string $type = 'INFO') { + if(SIGN_LOG_LEVEL == 'NONE') { + return; + } + + $type = in_array($type, ['INFO', 'WARNING', 'ERROR']) ? $type : 'INFO'; + $date = date('Y-m-d H:i:s'); + error_log("$date $type $message\n", 3, SIGN_LOGFILE); + } + + /** + * Takes a pathname and makes sure it ends with a slash. + * This is suitable for paths defined in the config.php file which may or may not end with a slash. + * + * @param string $path + * The path, e.g., '/tmp/' or '/tmp'. + * + * @return string + * The string with a slash suffix. + */ + public static function add_slash(string $path = '/') : string { + return rtrim($path, '/\\') . DIRECTORY_SEPARATOR; + } +} From 987b6ac6d80ee2dc9ab78167595088f5610897a8 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Tue, 15 Oct 2024 17:44:12 +0300 Subject: [PATCH 02/11] OS-110 code refactoring, security --- modules/os2forms_digital_signature/README.md | 40 ++++ .../os2forms_digital_signature.links.menu.yml | 5 + .../os2forms_digital_signature.module | 33 +++ .../os2forms_digital_signature.routing.yml | 15 +- .../os2forms_digital_signature.services.yml | 2 +- .../Controller/DigitalSignatureController.php | 65 +----- .../src/Form/SettingsForm.php | 74 ++++++ .../DigitalSignatureWebformHandler.php | 211 ++++-------------- .../src/Service/SigningService.php | 107 +++------ .../src/Service/SigningUtil.php | 93 -------- 10 files changed, 244 insertions(+), 401 deletions(-) create mode 100644 modules/os2forms_digital_signature/README.md create mode 100644 modules/os2forms_digital_signature/os2forms_digital_signature.links.menu.yml create mode 100644 modules/os2forms_digital_signature/src/Form/SettingsForm.php delete mode 100644 modules/os2forms_digital_signature/src/Service/SigningUtil.php 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.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 index 025c20da..1f686baf 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.module +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -1,6 +1,8 @@ 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 index 54e01a74..227a3a5d 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml @@ -5,14 +5,11 @@ os2forms_digital_signature.sign_callback: _controller: '\Drupal\os2forms_digital_signature\Controller\DigitalSignatureController::signCallback' requirements: _permission: 'access content' - -os2forms_digital_signature.test: - path: '/os2forms_digital_signature/{webform_submission}/test' +os2forms_digital_signature.settings: + path: '/admin/os2forms_digital_signature/settings' defaults: - _controller: '\Drupal\os2forms_digital_signature\Controller\DigitalSignatureController::test' -# _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title' -# view_mode: 'html' -# operation: webform_submission_view -# entity_access: 'webform_submission.view' + _form: '\Drupal\os2forms_digital_signature\Form\SettingsForm' + _title: 'Digital signature settings' requirements: - _permission: 'access content' + _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 index fa1587e6..d5d1b220 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml @@ -1,4 +1,4 @@ services: os2forms_digital_signature.signing_service: class: Drupal\os2forms_digital_signature\Service\SigningService - arguments: [] + arguments: ['@config.factory'] diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 8e2ebaf4..6f4db014 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -6,7 +6,6 @@ use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Url; use Drupal\os2forms_digital_signature\Service\SigningService; -use Drupal\webform\Entity\WebformSubmission; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -34,6 +33,7 @@ public function signCallback($uuid, $hash) { // Since loadByProperties returns an array, we need to fetch the first item. $webformSubmission = $submissions ? reset($submissions) : NULL; if (!$webformSubmission) { + // Submission does not exist. throw new NotFoundHttpException(); } @@ -43,6 +43,7 @@ public function signCallback($uuid, $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(); } @@ -52,6 +53,7 @@ public function signCallback($uuid, $hash) { $signeFilename = \Drupal::request()->get('file'); $signedFileContent = $signingService->download($signeFilename); if (!$signedFileContent) { + \Drupal::logger('os2forms_digital_signature')->warning('Missing file on remote server %file.', ['%file' => $signeFilename]); throw new NotFoundHttpException(); } @@ -61,23 +63,21 @@ public function signCallback($uuid, $hash) { $directory = dirname($expectedFileUri); if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { - // TODO: throw error. - //\Drupal::logger('my_module')->error('Failed to prepare directory %directory.', ['%directory' => $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(); } catch (\Exception $e) { - // TODO: throw error. - //\Drupal::logger('my_module')->error('Failed to write to file %uri: @message', ['%uri' => $uri, '@message' => $e->getMessage()]); + \Drupal::logger('os2forms_digital_signature')->error('Failed to write to file %uri: @message', ['%uri' => $expectedFileUri, '@message' => $e->getMessage()]); } - // Updating webform submission. - $webformSubmission->setLocked(TRUE); - $webformSubmission->save(); - // Build the URL for the webform submission confirmation page. $confirmation_url = Url::fromRoute('entity.webform.confirmation', [ 'webform' => $webformId, @@ -89,51 +89,4 @@ public function signCallback($uuid, $hash) { return $response; } - - - - public function test(WebformSubmission $webform_submission) { - $webformId = $webform_submission->getWebform()->id(); - $sid = $webform_submission->id(); - - $fileUri = "private://webform/$webformId/digital_signature/$sid.pdf"; -// $webform_submission->resave(); -// dpm('Done'); - - /** @var SigningService $signingService */ - $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); - - $signeFilename = \Drupal::request()->get('file'); - $signedFileContent = $signingService->download($signeFilename); - - // Get the FileSystem service. - $file_system = \Drupal::service('file_system'); - - // Prepare the directory to ensure it exists and is writable. - $directory = dirname($fileUri); - if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { - //\Drupal::logger('my_module')->error('Failed to prepare directory %directory.', ['%directory' => $directory]); - } - - // Write the data to the file using Drupal's file system service. - try { - $result = $file_system->saveData($signedFileContent, $fileUri , FileSystemInterface::EXISTS_REPLACE); - } - catch (\Exception $e) { - //\Drupal::logger('my_module')->error('Failed to write to file %uri: @message', ['%uri' => $uri, '@message' => $e->getMessage()]); - } - - $webform_submission->setLocked(TRUE); - $webform_submission->save(); - - // Build the URL for the webform confirmation page. - $confirmation_url = Url::fromRoute('entity.webform.confirmation', [ - 'webform' => $webformId, - 'webform_submission' => $sid, - ])->toString(); - - // Redirect to the webform confirmation page. - $response = new RedirectResponse($confirmation_url); - return $response; - } } diff --git a/modules/os2forms_digital_signature/src/Form/SettingsForm.php b/modules/os2forms_digital_signature/src/Form/SettingsForm.php new file mode 100644 index 00000000..473f64ec --- /dev/null +++ b/modules/os2forms_digital_signature/src/Form/SettingsForm.php @@ -0,0 +1,74 @@ + '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'), + ]; + + 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/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index 9e9d7cd3..d0c4b4a8 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -4,17 +4,12 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\file\FileInterface; use Drupal\os2forms_digital_signature\Service\SigningService; use Drupal\webform\Plugin\WebformHandlerBase; -use Drupal\webform\Utility\WebformElementHelper; -use Drupal\webform\Utility\WebformYaml; use Drupal\webform\WebformSubmissionInterface; -use phpseclib3\Crypt\Hash; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\RedirectResponse; /** * Webform submission debug handler. @@ -50,203 +45,68 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase { */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); -// $instance->renderer = $container->get('renderer'); $instance->moduleHandler = $container->get('module_handler'); $instance->elementManager = $container->get('plugin.manager.webform.element'); return $instance; } -// /** -// * {@inheritdoc} -// */ -// public function defaultConfiguration() { -// return [ -// 'format' => 'yaml', -// 'submission' => FALSE, -// ]; -// } - -// /** -// * {@inheritdoc} -// */ -// public function getSummary() { -// $settings = $this->getSettings(); -// switch ($settings['format']) { -// case static::FORMAT_JSON: -// $settings['format'] = $this->t('JSON'); -// break; -// -// case static::FORMAT_YAML: -// default: -// $settings['format'] = $this->t('YAML'); -// break; -// } -// return [ -// '#settings' => $settings, -// ] + parent::getSummary(); -// } -// -// /** -// * {@inheritdoc} -// */ -// public function buildConfigurationForm(array $form, FormStateInterface $form_state) { -// $form['debug_settings'] = [ -// '#type' => 'fieldset', -// '#title' => $this->t('Debug settings'), -// ]; -// $form['debug_settings']['format'] = [ -// '#type' => 'select', -// '#title' => $this->t('Data format'), -// '#options' => [ -// static::FORMAT_YAML => $this->t('YAML'), -// static::FORMAT_JSON => $this->t('JSON'), -// ], -// '#default_value' => $this->configuration['format'], -// ]; -// $form['debug_settings']['submission'] = [ -// '#type' => 'checkbox', -// '#title' => $this->t('Include submission properties'), -// '#description' => $this->t('If checked, all submission properties and values will be included in the displayed debug information. This includes sid, created, updated, completed, and more.'), -// '#return_value' => TRUE, -// '#default_value' => $this->configuration['submission'], -// ]; -// return $this->setSettingsParents($form); -// } - -// /** -// * {@inheritdoc} -// */ -// public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { -// parent::submitConfigurationForm($form, $form_state); -// $this->applyFormStateToConfiguration($form_state); -// } -// -// /** -// * {@inheritdoc} -// */ -// public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { -// $settings = $this->getSettings(); -// -// $data = ($settings['submission']) -// ? $webform_submission->toArray(TRUE) -// : $webform_submission->getData(); -// WebformElementHelper::convertRenderMarkupToStrings($data); -// -// $label = ($settings['submission']) -// ? $this->t('Submitted properties and values are:') -// : $this->t('Submitted values are:'); -// -// $build = [ -// 'label' => ['#markup' => $label], -// 'data' => [ -// '#markup' => ($settings['format'] === static::FORMAT_JSON) -// ? json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_PRETTY_PRINT) -// : WebformYaml::encode($data), -// '#prefix' => '
',
-//        '#suffix' => '
', -// ], -// ]; -// $message = $this->renderer->renderPlain($build); -// -// $this->messenger()->addWarning($message); -// } - /** * {@inheritdoc} */ public function preSave(WebformSubmissionInterface $webform_submission) { + $webform = $webform_submission->getWebform(); + if ($webform_submission->isLocked()) { return; } - $attachments = $this->getSubmissionAttachment($webform_submission); - //$destination = 'private://webform/signing' . $webform_submission->uuid() .'.pdf'; - //$pdfToSign = file_put_contents($destination, $attachment['filecontent'], FILE_APPEND); + $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; + } // TODO: think about file URL protection. - $destinationDir = 'public://signing'; - \Drupal::service('file_system')->prepareDirectory($destinationDir, FileSystemInterface::CREATE_DIRECTORY); - - $destination = $destinationDir . '/' . $webform_submission->uuid() .'.pdf'; - - // Save the file data. - /** @var FileInterface $fileSubmissionPdf */ - $fileSubmissionPdf = \Drupal::service('file.repository')->writeData($attachments[0]['filecontent'], $destination, FileSystemInterface::EXISTS_REPLACE); - - if ($fileSubmissionPdf) { - // Set the status to permanent to prevent file deletion on cron. - //$fileSubmissionPdf->setPermanent(); - - $fileSubmissionPdf->save(); - $submissionPdfPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileSubmissionPdf->getFileUri()); + $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; } - if ($submissionPdfPublicUrl) { - // For testing. - //$submissionPdfPublicUrl = 'https://signering.bellcom.dk/test/test-form.pdf'; - - /** @var SigningService $signingService */ - $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); - - $cid = $signingService->get_cid(); - if (empty($cid)) { - die('Failed to obtain cid. Is server running?'); - } - - // Creating hash. - $salt = \Drupal::service('settings')->get('hash_salt'); - $hash = Crypt::hashBase64($webform_submission->uuid() . $webform_submission->getWebform()->id() . $salt); + $fileUri = $destinationDir . '/' . $webform_submission->uuid() .'.pdf'; - $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash]); - - // Starting signing - $signingService->sign($submissionPdfPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); + // Save the file data. + try { + /** @var FileInterface $fileSubmissionPdf */ + $fileSubmissionPdf = \Drupal::service('file.repository')->writeData($attachment['filecontent'], $fileUri, FileSystemInterface::EXISTS_REPLACE); } - } - - - public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) { - return; - - if ($webform_submission->isLocked()) { + catch (\Exception $e) { + \Drupal::logger('os2forms_digital_signature')->error('File cannot be saved: %fileUri, error: %error', ['%fileUri' => $fileUri, '%error' => $e->getMessage()]); return; } -// Getting attachments. - $attachments = $this->getMessageAttachments($webform_submission); - dpm($attachments); - return; -// -// // Getting attachment as file. TODO: is there a better way to do it? -// $data = $attachments[0]['filecontent']; -// $destination = 'sites/default/files/teststan' . $webform_submission->id() .'.pdf'; - $submissionPdfPublicUrl = 'https://signering.bellcom.dk/test/test-form.pdf'; + // Set the status to permanent to prevent file deletion on cron. + //$fileSubmissionPdf->setPermanent(); + $fileSubmissionPdf->save(); + $submissionPdfPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileSubmissionPdf->getFileUri()); -// // Write data to the file. -// $result = file_put_contents($destination, $data, FILE_APPEND); -//// $response = \Drupal::httpClient()->get($url, ['sink' => $destination]); -// /** @var SigningService $signingService */ $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); $cid = $signingService->get_cid(); - if(empty($cid)) { - die('Failed to obtain cid. Is server running?'); + if (empty($cid)) { + \Drupal::logger('os2forms_digital_signature')->error('Failed to obtain cid. Is server running?'); + return; } - $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.test', ['webform_submission' => $webform_submission->id()]); + // Creating hash. + $salt = \Drupal::service('settings')->get('hash_salt'); + $hash = Crypt::hashBase64($webform_submission->uuid() . $webform->id() . $salt); - // Starting signing - $signingService->sign($submissionPdfPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); - - // Making redirect. -// $response = new RedirectResponse('https://google.com'); -// $response->send(); + $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash]); -// $response = new RedirectResponse($url->setAbsolute()->toString()); -// $response->send(); - -// $webform_submission->resave(); + // Starting signing, if everything is correct - this funcition will start redirect. + $signingService->sign($submissionPdfPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); } /** @@ -257,9 +117,12 @@ public function postSave(WebformSubmissionInterface $webform_submission, $update * * @return array|null * Array of attachment data. + * @throws \Exception */ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_submission) { + $attachments = NULL; $attachment = NULL; + $elements = $this->getWebform()->getElementsInitializedAndFlattened(); $element_attachments = $this->getWebform()->getElementsAttachments(); foreach ($element_attachments as $element_attachment) { @@ -272,10 +135,14 @@ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_s if ($element['#type'] == 'os2forms_attachment') { /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */ $element_plugin = $this->elementManager->getElementInstance($element); - $attachment = $element_plugin->getEmailAttachments($element, $webform_submission); + $attachments = $element_plugin->getEmailAttachments($element, $webform_submission); } } + 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 diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index 31b3dc5b..674dae17 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -2,33 +2,39 @@ namespace Drupal\os2forms_digital_signature\Service; -class SigningService { - - private $reply = []; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\ImmutableConfig; +use Drupal\os2forms_digital_signature\Form\SettingsForm; +use Symfony\Component\HttpFoundation\RedirectResponse; - private string $SIGN_REMOTE_SERVICE_URL = 'https://signering.bellcom.dk/sign.php?'; +class SigningService { /** - * Default constructor. + * The config. + * + * @var \Drupal\Core\Config\ImmutableConfig */ - public function __construct() { + private readonly ImmutableConfig $config; + + public function __construct(ConfigFactoryInterface $configFactory) { + $this->config = $configFactory->get(SettingsForm::$configName); } /** * Fetch a new cid. * - * @return string + * @return string|NULL * The correlation id. */ public function get_cid() : ?string { - $url = $this->SIGN_REMOTE_SERVICE_URL . 'action=getcid'; + $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); - $this->reply = json_decode($result, JSON_OBJECT_AS_ARRAY); + $reply = json_decode($result, JSON_OBJECT_AS_ARRAY); - return $this->reply['cid'] ?? NULL; + return $reply['cid'] ?? NULL; } /** @@ -49,49 +55,20 @@ public function get_cid() : ?string { * @param bool $leave * Leave the pdf file on the remote server. * - * @throws SignParameterException - * Empty url or cid given. + * @return void */ - public function sign(string $document_uri, string $cid, string $forward_url, bool $leave = FALSE) { - if(empty($document_uri) || empty($cid) || empty($forward_url)) { - //throw new SignParameterException(); + public function sign(string $document_uri, string $cid, string $forward_url, bool $leave = FALSE):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 = SigningUtil::get_hash($forward_url); + $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->SIGN_REMOTE_SERVICE_URL . http_build_query($params); + $url = $this->config->get('os2forms_digital_signature_remove_service_url') . http_build_query($params); - SigningUtil::url_forward($url); - } - - /** - * Verify the document. - * - * Verifying is done by redirecting the user's browser to a url on the signing server that takes the user - * through the verify flow. - * - * This function will never return. - * - * @param string $forward_url - * A url to a file on the local server that we want to sign or the full file name on the signing server. - * In case of a local file, it must be prefixed by 'http://' or 'https://' and be readable from the signing server. - * - * @throws SignParameterException - * Empty url or cid given. - * - * @todo Verifying the pdf is yet to be implemented on the signing server. - */ - public function verify(string $document_uri, string $cid, string $forward_url) { - SigningUtil::logger('Verify unimplemented!', 'WARNING'); - if(empty($forward_url)) { - //throw new SignParameterException(); - } - - $hash = SigningUtil::get_hash($forward_url); - $params = ['action' => 'verify', 'hash' => $hash, 'uri' => base64_encode($document_uri), 'forward_url' => base64_encode($forward_url)]; - $url = $this->SIGN_REMOTE_SERVICE_URL . http_build_query($params); - - SigningUtil::url_forward($url); + $response = new RedirectResponse($url); + $response->send(); } /** @@ -102,20 +79,18 @@ public function verify(string $document_uri, string $cid, string $forward_url) { * @param boolean $leave * If TRUE, leave the file on the remote server, default is to remove the file after download. * - * @return mixed - * The binary data of the pdf or an array if an error occured. + * @return mixed|bool + * The binary data of the pdf or FALSE if an error occurred. */ public function download(string $filename, $leave = FALSE) { if (empty($filename)) { return FALSE; - //throw new SignParameterException('Filename cannot be empty'); } if (!preg_match('/^[a-f0-9]{32}\.pdf$/', $filename)) { return FALSE; - //throw new SignParameterException('Incorrect filename given'); } $params = ['action' => 'download', 'file' => $filename, 'leave' => $leave]; - $url = $this->SIGN_REMOTE_SERVICE_URL . http_build_query($params); + $url = $this->config->get('os2forms_digital_signature_remove_service_url') . http_build_query($params); $curl = curl_init($url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); @@ -123,34 +98,26 @@ public function download(string $filename, $leave = FALSE) { if (empty($return)) { return FALSE; - //$return = ['error' => TRUE, 'message' => 'Empty file']; } elseif (substr($return, 0, 5) !== '%PDF-') { return FALSE; - //$return = ['error' => TRUE, 'message' => 'Not a PDF file']; } return $return; } /** - * Download the pdf file and send it to the user's browser. + * Calculate the hash value. * - * @param string $filename - * The filename. + * @param string $name + * The value to hash including salt. * - * @throws SignException + * @return string + * The hash value (sha1). */ - public function view(string $filename) { - $pdf = $this->download($filename); - if(is_array($pdf)) { - print 'Unable to view file: ' . $pdf['message']; - return; - } - - header('Content-Type: application/pdf'); - header('Content-Length: ' . strlen($pdf)); - - print $pdf; + private function getHash(string $value) : string { + $hashSalt = $this->config->get('os2forms_digital_signature_sign_hash_salt'); + return sha1($hashSalt . $value); } + } diff --git a/modules/os2forms_digital_signature/src/Service/SigningUtil.php b/modules/os2forms_digital_signature/src/Service/SigningUtil.php deleted file mode 100644 index 89475a72..00000000 --- a/modules/os2forms_digital_signature/src/Service/SigningUtil.php +++ /dev/null @@ -1,93 +0,0 @@ -send(); - -// header("location: $url"); -// print "\n"; -// -// die(); - } - - /** - * Write a message to the log file. - * - * @param string $message - * The message to write. - * @param string $type - * One of 'INFO', 'WARNING' or 'ERROR'. - */ - public static function logger(string $message, string $type = 'INFO') { - if(SIGN_LOG_LEVEL == 'NONE') { - return; - } - - $type = in_array($type, ['INFO', 'WARNING', 'ERROR']) ? $type : 'INFO'; - $date = date('Y-m-d H:i:s'); - error_log("$date $type $message\n", 3, SIGN_LOGFILE); - } - - /** - * Takes a pathname and makes sure it ends with a slash. - * This is suitable for paths defined in the config.php file which may or may not end with a slash. - * - * @param string $path - * The path, e.g., '/tmp/' or '/tmp'. - * - * @return string - * The string with a slash suffix. - */ - public static function add_slash(string $path = '/') : string { - return rtrim($path, '/\\') . DIRECTORY_SEPARATOR; - } -} From 53fb77e1598e2931ebf6b12a099ee28097008ea9 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Wed, 16 Oct 2024 16:58:37 +0300 Subject: [PATCH 03/11] OS-110 upload file for signature --- .../src/Element/DigitalSignatureDocument.php | 20 ++++++++++ .../DigitalSignatureDocument.php | 39 +++++++++++++++++++ .../DigitalSignatureWebformHandler.php | 29 ++++++++++---- 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php create mode 100644 modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php 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 @@ +t('PDF file for signature'); + return $formats; + } + + /** + * {@inheritdoc} + */ + protected function getFileExtensions(array $element = NULL) { + return 'pdf'; + } + +} diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index d0c4b4a8..9591b065 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -77,8 +77,8 @@ public function preSave(WebformSubmissionInterface $webform_submission) { // Save the file data. try { - /** @var FileInterface $fileSubmissionPdf */ - $fileSubmissionPdf = \Drupal::service('file.repository')->writeData($attachment['filecontent'], $fileUri, FileSystemInterface::EXISTS_REPLACE); + /** @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()]); @@ -86,9 +86,9 @@ public function preSave(WebformSubmissionInterface $webform_submission) { } // Set the status to permanent to prevent file deletion on cron. - //$fileSubmissionPdf->setPermanent(); - $fileSubmissionPdf->save(); - $submissionPdfPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileSubmissionPdf->getFileUri()); + //$fileToSign->setPermanent(); + $fileToSign->save(); + $fileToSignPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileToSign->getFileUri()); /** @var SigningService $signingService */ $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); @@ -106,7 +106,7 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash]); // Starting signing, if everything is correct - this funcition will start redirect. - $signingService->sign($submissionPdfPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); + $signingService->sign($fileToSignPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); } /** @@ -123,6 +123,19 @@ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_s $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) { @@ -132,10 +145,12 @@ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_s } $element = $elements[$element_attachment]; - if ($element['#type'] == 'os2forms_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); + break; } } From ff96214ff3be65df1ec7b123525d31e5c3c8b93f Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Wed, 16 Oct 2024 18:49:36 +0300 Subject: [PATCH 04/11] Upload digital document --- .../os2forms_digital_signature.routing.yml | 3 +- .../Controller/DigitalSignatureController.php | 31 ++++++++++--- .../DigitalSignatureDocument.php | 44 +++++++++++++++++++ .../DigitalSignatureWebformHandler.php | 9 +++- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml index 227a3a5d..c286ad3b 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml @@ -1,8 +1,9 @@ # Webform os2forms_attachment_component routes. os2forms_digital_signature.sign_callback: - path: '/os2forms_digital_signature/{uuid}/{hash}/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: diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 6f4db014..4c31d19c 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Url; +use Drupal\file\Entity\File; use Drupal\os2forms_digital_signature\Service\SigningService; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -18,13 +19,18 @@ class DigitalSignatureController { * * @param $uuid * Webform submission UUID. + * @param $hash + * Hash to check if the request is authentic. + * @param $fid + * File to replace (optional). + * * @return RedirectResponse * Redirect response to form submission confirmation. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ - public function signCallback($uuid, $hash) { + public function signCallback($uuid, $hash, $fid = NULL) { // Load the webform submission entity by UUID. $submissions = \Drupal::entityTypeManager() ->getStorage('webform_submission') @@ -57,13 +63,21 @@ public function signCallback($uuid, $hash) { throw new NotFoundHttpException(); } - // Prepare the directory to ensure it exists and is writable. + /** @var FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); - $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]); + // 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. @@ -73,6 +87,11 @@ public function signCallback($uuid, $hash) { // 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()]); diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php b/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php index 2b34ca10..843a63c9 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php @@ -3,6 +3,7 @@ namespace Drupal\os2forms_digital_signature\Plugin\WebformElement; use Drupal\webform\Plugin\WebformElement\WebformManagedFileBase; +use Drupal\webform\WebformSubmissionInterface; /** * Provides a 'os2forms_digital_signature_document' element. @@ -36,4 +37,47 @@ 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 index 9591b065..2c35b03d 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -66,7 +66,6 @@ public function preSave(WebformSubmissionInterface $webform_submission) { return; } - // TODO: think about file URL protection. $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]); @@ -103,7 +102,8 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $salt = \Drupal::service('settings')->get('hash_salt'); $hash = Crypt::hashBase64($webform_submission->uuid() . $webform->id() . $salt); - $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash]); + $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()); @@ -150,6 +150,11 @@ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_s /** @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; } } From 3be3b706f2bd360a6050853c0d5c061e92732b99 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Tue, 19 Nov 2024 16:38:35 +0200 Subject: [PATCH 05/11] OS-110 Signing service: Support for adding an extra page with signing info --- .../src/Service/SigningService.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index 674dae17..d72ee120 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -52,12 +52,10 @@ public function get_cid() : ?string { * The cid made available by the get_cid() function. * @param string $forward_url * The url on the local server to forward user to afterwards. - * @param bool $leave - * Leave the pdf file on the remote server. * * @return void */ - public function sign(string $document_uri, string $cid, string $forward_url, bool $leave = FALSE):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; @@ -78,18 +76,23 @@ public function sign(string $document_uri, string $cid, string $forward_url, boo * 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) { + public function download(string $filename, $leave = FALSE, $annotate = TRUE, $attributes = []) { if (empty($filename)) { return FALSE; } if (!preg_match('/^[a-f0-9]{32}\.pdf$/', $filename)) { return FALSE; } - $params = ['action' => 'download', 'file' => $filename, 'leave' => $leave]; + $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); From d06c8626be500d05c8f8118f213876e5dd39fb02 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 9 Dec 2024 15:06:09 +0200 Subject: [PATCH 06/11] OS-144 - adding return URL --- .../src/Controller/DigitalSignatureController.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 4c31d19c..aaf0979b 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -7,6 +7,7 @@ use Drupal\Core\Url; use Drupal\file\Entity\File; use Drupal\os2forms_digital_signature\Service\SigningService; +use Drupal\webform\WebformSubmissionInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -37,6 +38,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { ->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. @@ -45,6 +47,16 @@ public function signCallback($uuid, $hash, $fid = NULL) { $webformId = $webformSubmission->getWebform()->id(); + // Checking the action + $action = \Drupal::request()->query->get('name'); + 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); From 5d36f4a304e17ea667cebc77bfa2629f701fdb3b Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 27 Dec 2024 15:46:13 +0200 Subject: [PATCH 07/11] OS-145 adding websubmissions automatic cleaning --- .../os2forms_digital_signature.module | 11 +++ .../src/Form/SettingsForm.php | 6 ++ .../src/Service/SigningService.php | 68 +++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.module b/modules/os2forms_digital_signature/os2forms_digital_signature.module index 1f686baf..994aeba3 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.module +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -4,6 +4,17 @@ 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(). * diff --git a/modules/os2forms_digital_signature/src/Form/SettingsForm.php b/modules/os2forms_digital_signature/src/Form/SettingsForm.php index 473f64ec..f40fbffb 100644 --- a/modules/os2forms_digital_signature/src/Form/SettingsForm.php +++ b/modules/os2forms_digital_signature/src/Form/SettingsForm.php @@ -53,6 +53,12 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#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); } diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index d72ee120..ba7a67ee 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -5,7 +5,10 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ImmutableConfig; use Drupal\os2forms_digital_signature\Form\SettingsForm; +use Drupal\os2forms_digital_signature\Plugin\WebformHandler\DigitalSignatureWebformHandler; use Symfony\Component\HttpFoundation\RedirectResponse; +use Drupal\webform\Entity\Webform; +use Drupal\webform\Entity\WebformSubmission; class SigningService { @@ -123,4 +126,69 @@ private function getHash(string $value) : string { 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(); + } + } + } From 8d3b2a71afa2db0727b83cb49d02dc40dbb5ebf7 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Thu, 30 Jan 2025 15:16:03 +0200 Subject: [PATCH 08/11] OS-161 Disabling annotation page by default --- .../src/Controller/DigitalSignatureController.php | 6 +++--- .../src/Service/SigningService.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index aaf0979b..9ade1c01 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -68,10 +68,10 @@ public function signCallback($uuid, $hash, $fid = NULL) { /** @var SigningService $signingService */ $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); - $signeFilename = \Drupal::request()->get('file'); - $signedFileContent = $signingService->download($signeFilename); + $signedFilename = \Drupal::request()->get('file'); + $signedFileContent = $signingService->download($signedFilename); if (!$signedFileContent) { - \Drupal::logger('os2forms_digital_signature')->warning('Missing file on remote server %file.', ['%file' => $signeFilename]); + \Drupal::logger('os2forms_digital_signature')->warning('Missing file on remote server %file.', ['%file' => $signedFilename]); throw new NotFoundHttpException(); } diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index ba7a67ee..b38fee7f 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -88,7 +88,7 @@ public function sign(string $document_uri, string $cid, string $forward_url):voi * @return mixed|bool * The binary data of the pdf or FALSE if an error occurred. */ - public function download(string $filename, $leave = FALSE, $annotate = TRUE, $attributes = []) { + public function download(string $filename, $leave = FALSE, $annotate = FALSE, $attributes = []) { if (empty($filename)) { return FALSE; } From 43e5a0497f9703342f3902affd8bbb172c7ce346 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Thu, 30 Jan 2025 15:31:51 +0200 Subject: [PATCH 09/11] OS-144 Fixing cancel digital signature --- .../src/Controller/DigitalSignatureController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 9ade1c01..2ee637c6 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -48,7 +48,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { $webformId = $webformSubmission->getWebform()->id(); // Checking the action - $action = \Drupal::request()->query->get('name'); + $action = \Drupal::request()->query->get('action'); if ($action == 'cancel') { $cancelUrl = $webformSubmission->getWebform()->toUrl()->toString(); From 0db0714f9d4323b9cb4d38862ce6d97e71b443f2 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 17 Feb 2025 17:26:33 +0200 Subject: [PATCH 10/11] OS-167 adding Digital signature validation text --- .../src/Element/AttachmentElement.php | 9 +++- .../src/Os2formsAttachmentPrintBuilder.php | 54 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php index 43326d20..8136ea43 100644 --- a/modules/os2forms_attachment/src/Element/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Element/AttachmentElement.php @@ -75,7 +75,14 @@ public static function getFileContent(array $element, WebformSubmissionInterface // Save printable document. $print_engine = $print_engine_manager->createSelectedInstance($element['#export_type']); - $file_path = $print_builder->savePrintable([$webform_submission], $print_engine, $scheme, $file_name); + + // 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) { 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); From 6d6af9f613f7d837362d3979cfeb2fa0ec1e4254 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Wed, 23 Apr 2025 13:33:03 +0300 Subject: [PATCH 11/11] Adding module description --- CHANGELOG.md | 1 + .../os2forms_digital_signature.info.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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_digital_signature/os2forms_digital_signature.info.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml index d744350b..29547e43 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.info.yml @@ -1,9 +1,9 @@ name: 'OS2Forms Digital Signature' type: module -description: 'todo' +description: 'Provides digital signature functionality' package: 'OS2Forms' core_version_requirement: ^9 || ^10 dependencies: - 'webform:webform' -configure: os2forms_digital_post.admin.settings +configure: os2forms_digital_signature.settings