From ae977101b925f35b80bbaccf03923100b70b9ab2 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 4 Oct 2024 14:55:59 +0300 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 From b7e53b9be7b15b9cbc5351341da74bb35308f595 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 30 May 2025 14:36:19 +0300 Subject: [PATCH 12/20] OS-110 proper dependency injections --- modules/os2forms_digital_signature/README.md | 2 +- .../os2forms_digital_signature.services.yml | 10 +++- .../Controller/DigitalSignatureController.php | 17 +++++-- .../src/Form/SettingsForm.php | 18 ++++---- .../DigitalSignatureWebformHandler.php | 28 ++++++++--- .../src/Service/SigningService.php | 46 +++++++++++++------ 6 files changed, 89 insertions(+), 32 deletions(-) diff --git a/modules/os2forms_digital_signature/README.md b/modules/os2forms_digital_signature/README.md index 1217d6e9..d7d36d95 100644 --- a/modules/os2forms_digital_signature/README.md +++ b/modules/os2forms_digital_signature/README.md @@ -35,6 +35,6 @@ URL: `admin/os2forms_digital_signature/settings` Must match hash salt on the signature server -- **List IP's which can download unsigned PDF submissions** +- **List IPs 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.services.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml index d5d1b220..f30f3501 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml @@ -1,4 +1,12 @@ services: + logger.channel.os2forms_digital_signature: + parent: logger.channel_base + arguments: [ 'os2forms_digital_signature' ] + os2forms_digital_signature.signing_service: class: Drupal\os2forms_digital_signature\Service\SigningService - arguments: ['@config.factory'] + arguments: + - '@http_client' + - '@config.factory' + - '@entity_type.manager' + - '@logger.channel.os2forms_digital_signature' diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 2ee637c6..90268af2 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -13,6 +13,17 @@ class DigitalSignatureController { + /** + * Logger for channel - os2forms_digital_signature. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + + public function __construct() { + $this->logger = \Drupal::logger('os2forms_digital_signature'); + } + /** * Callback for the file being signed. * @@ -71,7 +82,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { $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]); + $this->logger->warning('Missing file on remote server %file.', ['%file' => $signedFilename]); throw new NotFoundHttpException(); } @@ -88,7 +99,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { $directory = dirname($expectedFileUri); if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { - \Drupal::logger('os2forms_digital_signature')->error('Failed to prepare directory %directory.', ['%directory' => $directory]); + $this->logger->error('Failed to prepare directory %directory.', ['%directory' => $directory]); } } @@ -106,7 +117,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { } } catch (\Exception $e) { - \Drupal::logger('os2forms_digital_signature')->error('Failed to write to file %uri: @message', ['%uri' => $expectedFileUri, '@message' => $e->getMessage()]); + $this->logger->error('Failed to write to file %uri: @message', ['%uri' => $expectedFileUri, '@message' => $e->getMessage()]); } // Build the URL for the webform submission confirmation page. diff --git a/modules/os2forms_digital_signature/src/Form/SettingsForm.php b/modules/os2forms_digital_signature/src/Form/SettingsForm.php index f40fbffb..5bc855a8 100644 --- a/modules/os2forms_digital_signature/src/Form/SettingsForm.php +++ b/modules/os2forms_digital_signature/src/Form/SettingsForm.php @@ -4,11 +4,13 @@ use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Digital post settings form. */ class SettingsForm extends ConfigFormBase { + use StringTranslationTrait; /** * Name of the config. @@ -37,27 +39,27 @@ protected function getEditableConfigNames() { public function buildForm(array $form, FormStateInterface $form_state) { $form['os2forms_digital_signature_remove_service_url'] = [ '#type' => 'textfield', - '#title' => t("Signature server URL"), + '#title' => $this->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?'), + '#description' => $this->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"), + '#title' => $this->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'), + '#description' => $this->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"), + '#title' => $this->t('List IPs 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'), + '#description' => $this->t('Comma separated. e.g. 192.168.1.1,192.168.2.1'), ]; $form['os2forms_digital_signature_submission_retention_period'] = [ '#type' => 'textfield', - '#title' => t('Unsigned submission timespan (s)'), + '#title' => $this->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'), + '#description' => $this->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/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index 2c35b03d..aee3c7ff 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -40,6 +40,21 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase { */ protected $elementManager; + /** + * Logger for channel - os2forms_digital_signature. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->logger = \Drupal::logger('os2forms_digital_signature'); + } + /** * {@inheritdoc} */ @@ -47,6 +62,7 @@ public static function create(ContainerInterface $container, array $configuratio $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); $instance->moduleHandler = $container->get('module_handler'); $instance->elementManager = $container->get('plugin.manager.webform.element'); + return $instance; } @@ -62,13 +78,13 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $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()]); + $this->logger->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]); + $this->logger->error('File directory cannot be created: %filedirectory', ['%filedirectory' => $destinationDir]); return; } @@ -80,7 +96,7 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $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()]); + $this->logger->error('File cannot be saved: %fileUri, error: %error', ['%fileUri' => $fileUri, '%error' => $e->getMessage()]); return; } @@ -94,7 +110,7 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $cid = $signingService->get_cid(); if (empty($cid)) { - \Drupal::logger('os2forms_digital_signature')->error('Failed to obtain cid. Is server running?'); + $this->logger->error('Failed to obtain cid. Is server running?'); return; } @@ -102,8 +118,8 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $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]); + $attachmentFid = $attachment['fid'] ?? NULL; + $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash, 'fid' => $attachmentFid]); // Starting signing, if everything is correct - this funcition will start redirect. $signingService->sign($fileToSignPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index b38fee7f..156ca550 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -4,10 +4,12 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ImmutableConfig; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\os2forms_digital_signature\Form\SettingsForm; -use Drupal\os2forms_digital_signature\Plugin\WebformHandler\DigitalSignatureWebformHandler; +use GuzzleHttp\ClientInterface; use Symfony\Component\HttpFoundation\RedirectResponse; -use Drupal\webform\Entity\Webform; use Drupal\webform\Entity\WebformSubmission; class SigningService { @@ -19,8 +21,28 @@ class SigningService { */ private readonly ImmutableConfig $config; - public function __construct(ConfigFactoryInterface $configFactory) { + /** + * Webform storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected EntityStorageInterface $webformStorage; + + /** + * WebformSubmission storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected EntityStorageInterface $webformSubmissionStorage; + + public function __construct( + private readonly ClientInterface $httpClient, + ConfigFactoryInterface $configFactory, + EntityTypeManager $entityTypeManager, + private readonly LoggerChannelInterface $logger) { $this->config = $configFactory->get(SettingsForm::$configName); + $this->webformStorage = $entityTypeManager->getStorage('webform'); + $this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission'); } /** @@ -31,9 +53,8 @@ public function __construct(ConfigFactoryInterface $configFactory) { */ 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); + $response = $this->httpClient->request('GET', $url); + $result = $response->getBody()->getContents(); $reply = json_decode($result, JSON_OBJECT_AS_ARRAY); @@ -60,7 +81,7 @@ public function get_cid() : ?string { */ 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]); + $this->logger->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; } @@ -98,9 +119,8 @@ public function download(string $filename, $leave = FALSE, $annotate = FALSE, $a $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); + $response = $this->httpClient->request('GET', $url); + $return = $response->getBody()->getContents(); if (empty($return)) { return FALSE; @@ -149,7 +169,7 @@ public function deleteStalledSubmissions() : void { // Find all with os2forms_digital_signature handlers enabled. foreach ($handler_webform_ids as $webform_id) { - $webform = Webform::load($webform_id); + $webform = $this->webformStorage->load($webform_id); if (!$webform) { continue; } @@ -172,7 +192,7 @@ public function deleteStalledSubmissions() : void { // 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') + $query = $this->webformSubmissionStorage->getQuery() ->accessCheck(FALSE) ->condition('webform_id', $digitalSignatureWebforms, 'IN') ->condition('locked', 0) @@ -186,7 +206,7 @@ public function deleteStalledSubmissions() : void { // Deleting all stalled webform submissions. foreach ($submission_ids as $submission_id) { - $submission = WebformSubmission::load($submission_id); + $submission = $this->webformSubmissionStorage->load($submission_id); $submission->delete(); } } From 51706b5bed87cb69b8914b4e7fe795766550c8bb Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 30 May 2025 14:40:50 +0300 Subject: [PATCH 13/20] OS-110 refactoring, removing comment lines --- .../Plugin/WebformHandler/DigitalSignatureWebformHandler.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index aee3c7ff..f67590f7 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -3,6 +3,7 @@ namespace Drupal\os2forms_digital_signature\Plugin\WebformHandler; use Drupal\Component\Utility\Crypt; +use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Url; use Drupal\file\FileInterface; @@ -93,15 +94,13 @@ public function preSave(WebformSubmissionInterface $webform_submission) { // Save the file data. try { /** @var FileInterface $fileToSign */ - $fileToSign = \Drupal::service('file.repository')->writeData($attachment['filecontent'], $fileUri, FileSystemInterface::EXISTS_REPLACE); + $fileToSign = \Drupal::service('file.repository')->writeData($attachment['filecontent'], $fileUri, FileExists::Replace); } catch (\Exception $e) { $this->logger->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()); From 9abfcbde7d3d231329438f12c658d641041f7059 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 30 May 2025 15:18:23 +0300 Subject: [PATCH 14/20] OS-110 phpcs formatting --- modules/os2forms_digital_signature/README.md | 8 +- .../os2forms_digital_signature.module | 7 +- .../os2forms_digital_signature.routing.yml | 1 - .../Controller/DigitalSignatureController.php | 39 ++++++---- .../src/Element/DigitalSignatureDocument.php | 3 +- .../src/Form/SettingsForm.php | 1 + .../DigitalSignatureDocument.php | 3 +- .../DigitalSignatureWebformHandler.php | 54 +++++++++----- .../src/Service/SigningService.php | 74 +++++++++++++------ 9 files changed, 121 insertions(+), 69 deletions(-) diff --git a/modules/os2forms_digital_signature/README.md b/modules/os2forms_digital_signature/README.md index d7d36d95..f9d0a090 100644 --- a/modules/os2forms_digital_signature/README.md +++ b/modules/os2forms_digital_signature/README.md @@ -11,7 +11,8 @@ This module provides functionality for adding digital signature to the webform P 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*. +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 @@ -19,7 +20,8 @@ This module provides functionality for adding digital signature to the webform P 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. +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 @@ -29,12 +31,10 @@ URL: `admin/os2forms_digital_signature/settings` 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 IPs 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.module b/modules/os2forms_digital_signature/os2forms_digital_signature.module index 994aeba3..6e12a210 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.module +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.module @@ -1,5 +1,10 @@ get('settings'); // Checking if the title has not been overridden. - if ($settings['default_submit_button_label'] == $form['actions']['submit']['#value']){ + 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 index c286ad3b..41dbc321 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.routing.yml @@ -13,4 +13,3 @@ os2forms_digital_signature.settings: _title: 'Digital signature settings' requirements: _permission: 'administer site configuration' - diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 90268af2..8fddc8b9 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -6,11 +6,12 @@ use Drupal\Core\File\FileSystemInterface; 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; +/** + * Digital Signature Controller. + */ class DigitalSignatureController { /** @@ -29,14 +30,14 @@ public function __construct() { * * Expecting the file name to be coming as GET parameter. * - * @param $uuid + * @param string $uuid * Webform submission UUID. - * @param $hash - * Hash to check if the request is authentic. - * @param $fid - * File to replace (optional). + * @param string $hash + * Hash to check if the request is authentic. + * @param int|null $fid + * File to replace (optional). * - * @return RedirectResponse + * @return \Symfony\Component\HttpFoundation\RedirectResponse * Redirect response to form submission confirmation. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException @@ -49,7 +50,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 */ + /** @var \Drupal\webform\WebformSubmissionInterface $webformSubmission */ $webformSubmission = $submissions ? reset($submissions) : NULL; if (!$webformSubmission) { // Submission does not exist. @@ -58,7 +59,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { $webformId = $webformSubmission->getWebform()->id(); - // Checking the action + // Checking the action. $action = \Drupal::request()->query->get('action'); if ($action == 'cancel') { $cancelUrl = $webformSubmission->getWebform()->toUrl()->toString(); @@ -76,7 +77,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { throw new NotFoundHttpException(); } - /** @var SigningService $signingService */ + /** @var \Drupal\os2forms_digital_signature\Service\SigningService $signingService */ $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); $signedFilename = \Drupal::request()->get('file'); @@ -86,14 +87,16 @@ public function signCallback($uuid, $hash, $fid = NULL) { throw new NotFoundHttpException(); } - /** @var FileSystemInterface $file_system */ + /** @var \Drupal\Core\File\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 is present - we are replacing uploaded/managed file, otherwise + // creating a new one. if ($fid) { $file = File::load($fid); $expectedFileUri = $file->getFileUri(); - } else { + } + else { // Prepare the directory to ensure it exists and is writable. $expectedFileUri = "private://webform/$webformId/digital_signature/$uuid.pdf"; $directory = dirname($expectedFileUri); @@ -105,7 +108,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { // Write the data to the file using Drupal's file system service. try { - $file_system->saveData($signedFileContent, $expectedFileUri , FileSystemInterface::EXISTS_REPLACE); + $file_system->saveData($signedFileContent, $expectedFileUri, FileSystemInterface::EXISTS_REPLACE); // Updating webform submission. $webformSubmission->setLocked(TRUE); @@ -117,7 +120,11 @@ public function signCallback($uuid, $hash, $fid = NULL) { } } catch (\Exception $e) { - $this->logger->error('Failed to write to file %uri: @message', ['%uri' => $expectedFileUri, '@message' => $e->getMessage()]); + $this->logger->error('Failed to write to file %uri: @message', + [ + '%uri' => $expectedFileUri, + '@message' => $e->getMessage(), + ]); } // Build the URL for the webform submission confirmation page. diff --git a/modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php b/modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php index 5f27ff79..7ac75c97 100644 --- a/modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php +++ b/modules/os2forms_digital_signature/src/Element/DigitalSignatureDocument.php @@ -2,11 +2,10 @@ namespace Drupal\os2forms_digital_signature\Element; - use Drupal\webform\Element\WebformManagedFileBase; /** - * Provides a webform element for an 'os2forms_digital_signature_document' element. + * Provides a element for an 'os2forms_digital_signature_document' element. * * @FormElement("os2forms_digital_signature_document") */ diff --git a/modules/os2forms_digital_signature/src/Form/SettingsForm.php b/modules/os2forms_digital_signature/src/Form/SettingsForm.php index 5bc855a8..cc21436a 100644 --- a/modules/os2forms_digital_signature/src/Form/SettingsForm.php +++ b/modules/os2forms_digital_signature/src/Form/SettingsForm.php @@ -79,4 +79,5 @@ public function submitForm(array &$form, FormStateInterface $form_state) { 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 index 843a63c9..dcf727d8 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformElement/DigitalSignatureDocument.php @@ -33,11 +33,10 @@ public function getItemFormats() { /** * {@inheritdoc} */ - protected function getFileExtensions(array $element = NULL) { + protected function getFileExtensions(?array $element = NULL) { return 'pdf'; } - /** * {@inheritdoc} */ diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index f67590f7..b67cfa98 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -6,14 +6,12 @@ use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Url; -use Drupal\file\FileInterface; -use Drupal\os2forms_digital_signature\Service\SigningService; use Drupal\webform\Plugin\WebformHandlerBase; use Drupal\webform\WebformSubmissionInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Webform submission debug handler. + * Digital signature webform handler. * * @WebformHandler( * id = "os2forms_digital_signature", @@ -68,8 +66,8 @@ public static function create(ContainerInterface $container, array $configuratio } /** - * {@inheritdoc} - */ + * {@inheritdoc} + */ public function preSave(WebformSubmissionInterface $webform_submission) { $webform = $webform_submission->getWebform(); @@ -79,7 +77,12 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $attachment = $this->getSubmissionAttachment($webform_submission); if (!$attachment) { - $this->logger->error('Attachment cannot be created webform: %webform, webform_submission: %webform_submission', ['%webform' => $webform->id(), '%webform_submission' => $webform_submission->uuid()]); + $this->logger->error('Attachment cannot be created webform: %webform, webform_submission: %webform_submission', + [ + '%webform' => $webform->id(), + '%webform_submission' => $webform_submission->uuid(), + ] + ); return; } @@ -89,25 +92,29 @@ public function preSave(WebformSubmissionInterface $webform_submission) { return; } - $fileUri = $destinationDir . '/' . $webform_submission->uuid() .'.pdf'; + $fileUri = $destinationDir . '/' . $webform_submission->uuid() . '.pdf'; // Save the file data. try { - /** @var FileInterface $fileToSign */ + /** @var \Drupal\file\FileInterface $fileToSign */ $fileToSign = \Drupal::service('file.repository')->writeData($attachment['filecontent'], $fileUri, FileExists::Replace); } catch (\Exception $e) { - $this->logger->error('File cannot be saved: %fileUri, error: %error', ['%fileUri' => $fileUri, '%error' => $e->getMessage()]); + $this->logger->error('File cannot be saved: %fileUri, error: %error', + [ + '%fileUri' => $fileUri, + '%error' => $e->getMessage(), + ]); return; } $fileToSign->save(); $fileToSignPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileToSign->getFileUri()); - /** @var SigningService $signingService */ + /** @var \Drupal\os2forms_digital_signature\Service\SigningService $signingService */ $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); - $cid = $signingService->get_cid(); + $cid = $signingService->getCid(); if (empty($cid)) { $this->logger->error('Failed to obtain cid. Is server running?'); return; @@ -118,9 +125,16 @@ public function preSave(WebformSubmissionInterface $webform_submission) { $hash = Crypt::hashBase64($webform_submission->uuid() . $webform->id() . $salt); $attachmentFid = $attachment['fid'] ?? NULL; - $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', ['uuid' => $webform_submission->uuid(), 'hash' => $hash, 'fid' => $attachmentFid]); - - // Starting signing, if everything is correct - this funcition will start redirect. + $signatureCallbackUrl = Url::fromRoute('os2forms_digital_signature.sign_callback', + [ + 'uuid' => $webform_submission->uuid(), + 'hash' => $hash, + 'fid' => $attachmentFid, + ] + ); + + // Starting signing, if everything is correct - this funcition will start + // redirect. $signingService->sign($fileToSignPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); } @@ -128,10 +142,11 @@ public function preSave(WebformSubmissionInterface $webform_submission) { * Get OS2forms file attachment. * * @param \Drupal\webform\WebformSubmissionInterface $webform_submission - * A webform submission. + * A webform submission. * * @return array|null - * Array of attachment data. + * Array of attachment data. + * * @throws \Exception */ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_submission) { @@ -140,8 +155,8 @@ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_s // 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 + // 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)) { @@ -154,7 +169,8 @@ protected function getSubmissionAttachment(WebformSubmissionInterface $webform_s $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. + // Check if the element attachment key is excluded and should not attach + // any files. if (isset($this->configuration['excluded_elements'][$element_attachment])) { continue; } diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index 156ca550..17073f2a 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -10,8 +10,10 @@ use Drupal\os2forms_digital_signature\Form\SettingsForm; use GuzzleHttp\ClientInterface; use Symfony\Component\HttpFoundation\RedirectResponse; -use Drupal\webform\Entity\WebformSubmission; +/** + * Digital signing service. + */ class SigningService { /** @@ -39,7 +41,8 @@ public function __construct( private readonly ClientInterface $httpClient, ConfigFactoryInterface $configFactory, EntityTypeManager $entityTypeManager, - private readonly LoggerChannelInterface $logger) { + private readonly LoggerChannelInterface $logger, + ) { $this->config = $configFactory->get(SettingsForm::$configName); $this->webformStorage = $entityTypeManager->getStorage('webform'); $this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission'); @@ -48,10 +51,10 @@ public function __construct( /** * Fetch a new cid. * - * @return string|NULL + * @return string|null * The correlation id. */ - public function get_cid() : ?string { + public function getCid() : ?string { $url = $this->config->get('os2forms_digital_signature_remove_service_url') . 'action=getcid'; $response = $this->httpClient->request('GET', $url); $result = $response->getBody()->getContents(); @@ -64,29 +67,41 @@ public function get_cid() : ?string { /** * 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. + * 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. + * 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. + * The cid made available by the getCid() 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)) { - $this->logger->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]); + $this->logger->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)]; + $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); @@ -98,13 +113,18 @@ public function sign(string $document_uri, string $cid, string $forward_url):voi * * @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 bool $leave + * If TRUE, leave the file on the remote server, default is to remove the + * file after download. + * @param bool $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']. + * 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. @@ -116,7 +136,13 @@ public function download(string $filename, $leave = FALSE, $annotate = FALSE, $a if (!preg_match('/^[a-f0-9]{32}\.pdf$/', $filename)) { return FALSE; } - $params = ['action' => 'download', 'file' => $filename, 'leave' => $leave, 'annotate' => $annotate, 'attributes' => $attributes]; + $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); $response = $this->httpClient->request('GET', $url); @@ -135,7 +161,7 @@ public function download(string $filename, $leave = FALSE, $annotate = FALSE, $a /** * Calculate the hash value. * - * @param string $name + * @param string $value * The value to hash including salt. * * @return string @@ -149,8 +175,8 @@ private function getHash(string $value) : string { /** * 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. + * Only checked the webforms that have digital_signature handler enabled and + * the submission is older that a specified period. * * @throws \Drupal\Core\Entity\EntityStorageException */ @@ -159,7 +185,7 @@ public function deleteStalledSubmissions() : void { // Finding webforms that have any handler. $query = \Drupal::entityQuery('webform') - ->exists('handlers'); // Only webforms with handlers configured. + ->exists('handlers'); $handler_webform_ids = $query->execute(); // No webforms with handlers, aborting. From 31300811dcc7f6b9386deaf791c3b87ad8a902dc Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Fri, 30 May 2025 16:39:07 +0300 Subject: [PATCH 15/20] OS-110 phpcs formatting --- .../src/Element/AttachmentElement.php | 3 +-- .../src/Os2formsAttachmentPrintBuilder.php | 10 +++++----- modules/os2forms_digital_signature/README.md | 2 +- .../os2forms_digital_signature.services.yml | 1 + .../src/Service/SigningService.php | 6 ++++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/os2forms_attachment/src/Element/AttachmentElement.php b/modules/os2forms_attachment/src/Element/AttachmentElement.php index 8136ea43..cdf3ae44 100644 --- a/modules/os2forms_attachment/src/Element/AttachmentElement.php +++ b/modules/os2forms_attachment/src/Element/AttachmentElement.php @@ -2,7 +2,6 @@ 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; @@ -76,7 +75,7 @@ public static function getFileContent(array $element, WebformSubmissionInterface // Save printable document. $print_engine = $print_engine_manager->createSelectedInstance($element['#export_type']); - // Adding digital signature + // Adding digital signature. if (isset($element['#digital_signature']) && $element['#digital_signature']) { $file_path = $print_builder->savePrintableDigitalSignature([$webform_submission], $print_engine, $scheme, $file_name); } diff --git a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php index e9d96dd7..a394da3f 100644 --- a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php +++ b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php @@ -33,8 +33,8 @@ public function printHtml(EntityInterface $entity, $use_default_css = TRUE, $opt /** * Modified version of the original savePrintable() function. * - * The only difference is modified call to prepareRenderer with digitalPost flag - * TRUE. + * The only difference is modified call to prepareRenderer with digitalPost + * flag TRUE. * * @see PrintBuilder::savePrintable() * @@ -65,8 +65,6 @@ public function savePrintableDigitalSignature(array $entities, PrintEngineInterf /** * 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 @@ -78,8 +76,10 @@ public function savePrintableDigitalSignature(array $entities, PrintEngineInterf * * @return \Drupal\entity_print\Renderer\RendererInterface * A print renderer. + * + * @see PrintBuilder::prepareRenderer */ - protected function prepareRenderer(array $entities, PrintEngineInterface $print_engine, $use_default_css, $digitalSignature = false) { + 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'); } diff --git a/modules/os2forms_digital_signature/README.md b/modules/os2forms_digital_signature/README.md index f9d0a090..242fa541 100644 --- a/modules/os2forms_digital_signature/README.md +++ b/modules/os2forms_digital_signature/README.md @@ -29,7 +29,7 @@ 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? + The URL of the service providing digital signature. This is the example of a known service [https://signering.bellcom.dk/sign.php?](https://signering.bellcom.dk/sign.php?) - **Hash Salt used for signature** diff --git a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml index f30f3501..33848830 100644 --- a/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml +++ b/modules/os2forms_digital_signature/os2forms_digital_signature.services.yml @@ -7,6 +7,7 @@ services: class: Drupal\os2forms_digital_signature\Service\SigningService arguments: - '@http_client' + - '@datetime.time' - '@config.factory' - '@entity_type.manager' - '@logger.channel.os2forms_digital_signature' diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index 17073f2a..9eafa80d 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -2,6 +2,7 @@ namespace Drupal\os2forms_digital_signature\Service; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Entity\EntityStorageInterface; @@ -39,6 +40,7 @@ class SigningService { public function __construct( private readonly ClientInterface $httpClient, + private readonly TimeInterface $time, ConfigFactoryInterface $configFactory, EntityTypeManager $entityTypeManager, private readonly LoggerChannelInterface $logger, @@ -184,7 +186,7 @@ public function deleteStalledSubmissions() : void { $digitalSignatureWebforms = []; // Finding webforms that have any handler. - $query = \Drupal::entityQuery('webform') + $query = $this->webformStorage->getQuery() ->exists('handlers'); $handler_webform_ids = $query->execute(); @@ -217,7 +219,7 @@ public function deleteStalledSubmissions() : void { // 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; + $timestamp_threshold = $this->time->getRequestTime() - $retention_period; $query = $this->webformSubmissionStorage->getQuery() ->accessCheck(FALSE) ->condition('webform_id', $digitalSignatureWebforms, 'IN') From 9ae977f98fe71065860c639b62e04cff9d2edaf7 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 2 Jun 2025 16:46:51 +0300 Subject: [PATCH 16/20] OS-110 injecting dependency --- .../os2forms_attachment.services.yml | 2 +- .../src/Os2formsAttachmentPrintBuilder.php | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/os2forms_attachment/os2forms_attachment.services.yml b/modules/os2forms_attachment/os2forms_attachment.services.yml index 34d2d676..e477d46f 100644 --- a/modules/os2forms_attachment/os2forms_attachment.services.yml +++ b/modules/os2forms_attachment/os2forms_attachment.services.yml @@ -1,4 +1,4 @@ services: os2forms_attachment.print_builder: class: Drupal\os2forms_attachment\Os2formsAttachmentPrintBuilder - arguments: ['@entity_print.renderer_factory', '@event_dispatcher', '@string_translation'] + arguments: ['@entity_print.renderer_factory', '@event_dispatcher', '@string_translation', '@file_system'] diff --git a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php index a394da3f..1f8a9f38 100644 --- a/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php +++ b/modules/os2forms_attachment/src/Os2formsAttachmentPrintBuilder.php @@ -4,16 +4,27 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\File\FileExists; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\entity_print\Event\PreSendPrintEvent; use Drupal\entity_print\Event\PrintEvents; use Drupal\entity_print\Plugin\PrintEngineInterface; use Drupal\entity_print\PrintBuilder; +use Drupal\entity_print\Renderer\RendererFactoryInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * The OS2Forms attachment print builder service. */ class Os2formsAttachmentPrintBuilder extends PrintBuilder { + /** + * {@inheritdoc} + */ + public function __construct(RendererFactoryInterface $renderer_factory, EventDispatcherInterface $event_dispatcher, TranslationInterface $string_translation, protected readonly FileSystemInterface $file_system) { + parent::__construct($renderer_factory, $event_dispatcher, $string_translation); + } + /** * {@inheritdoc} */ @@ -55,7 +66,7 @@ public function savePrintableDigitalSignature(array $entities, PrintEngineInterf $uri = "$scheme://$filename"; // Save the file. - return \Drupal::service('file_system')->saveData($print_engine->getBlob(), $uri, FileExists::Replace); + return $this->file_system->saveData($print_engine->getBlob(), $uri, FileExists::Replace); } /** From e01f0c1c1261495dd735f3a11a4626274d2d4c06 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 9 Jun 2025 12:36:52 +0300 Subject: [PATCH 17/20] OS-110 refactoring --- .../src/Form/SettingsForm.php | 4 +- .../DigitalSignatureWebformHandler.php | 74 ++++++++++++++----- .../src/Service/SigningService.php | 27 +++++-- 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Form/SettingsForm.php b/modules/os2forms_digital_signature/src/Form/SettingsForm.php index cc21436a..a05ec886 100644 --- a/modules/os2forms_digital_signature/src/Form/SettingsForm.php +++ b/modules/os2forms_digital_signature/src/Form/SettingsForm.php @@ -37,10 +37,10 @@ protected function getEditableConfigNames() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { - $form['os2forms_digital_signature_remove_service_url'] = [ + $form['os2forms_digital_signature_remote_service_url'] = [ '#type' => 'textfield', '#title' => $this->t('Signature server URL'), - '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_remove_service_url'), + '#default_value' => $this->config(self::$configName)->get('os2forms_digital_signature_remote_service_url'), '#description' => $this->t('E.g. https://signering.bellcom.dk/sign.php?'), ]; $form['os2forms_digital_signature_sign_hash_salt'] = [ diff --git a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php index b67cfa98..9a616bfc 100644 --- a/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php +++ b/modules/os2forms_digital_signature/src/Plugin/WebformHandler/DigitalSignatureWebformHandler.php @@ -3,11 +3,18 @@ namespace Drupal\os2forms_digital_signature\Plugin\WebformHandler; use Drupal\Component\Utility\Crypt; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\File\FileUrlGeneratorInterface; +use Drupal\Core\Site\Settings; use Drupal\Core\Url; +use Drupal\file\FileRepositoryInterface; +use Drupal\os2forms_digital_signature\Service\SigningService; +use Drupal\webform\Plugin\WebformElementManagerInterface; use Drupal\webform\Plugin\WebformHandlerBase; use Drupal\webform\WebformSubmissionInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -30,29 +37,56 @@ class DigitalSignatureWebformHandler extends WebformHandlerBase { * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ - protected $moduleHandler; + private readonly ModuleHandlerInterface $moduleHandler; /** * The webform element plugin manager. * * @var \Drupal\webform\Plugin\WebformElementManagerInterface */ - protected $elementManager; + private readonly WebformElementManagerInterface $elementManager; /** * Logger for channel - os2forms_digital_signature. * - * @var \Drupal\Core\Logger\LoggerChannelInterface + * @var \Psr\Log\LoggerInterface */ - protected $logger; + private readonly LoggerInterface $logger; /** - * {@inheritdoc} + * File system interface. + * + * @var \Drupal\Core\File\FileSystemInterface */ - public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->logger = \Drupal::logger('os2forms_digital_signature'); - } + private readonly FileSystemInterface $fileSystem; + + /** + * File repository. + * + * @var \Drupal\file\FileRepositoryInterface + */ + private readonly FileRepositoryInterface $fileRepository; + + /** + * File URL generator. + * + * @var \Drupal\Core\File\FileUrlGeneratorInterface + */ + private readonly FileUrlGeneratorInterface $fileUrlGenerator; + + /** + * OS2Forms signing service. + * + * @var \Drupal\os2forms_digital_signature\Service\SigningService + */ + private readonly SigningService $signingService; + + /** + * Settings service. + * + * @var \Drupal\Core\Site\Settings + */ + private readonly Settings $settings; /** * {@inheritdoc} @@ -61,6 +95,12 @@ public static function create(ContainerInterface $container, array $configuratio $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); $instance->moduleHandler = $container->get('module_handler'); $instance->elementManager = $container->get('plugin.manager.webform.element'); + $instance->logger = $container->get('logger.channel.os2forms_digital_signature'); + $instance->fileSystem = $container->get('file_system'); + $instance->fileRepository = $container->get('file.repository'); + $instance->fileUrlGenerator = $container->get('file_url_generator'); + $instance->signingService = $container->get('os2forms_digital_signature.signing_service'); + $instance->settings = $container->get('settings'); return $instance; } @@ -87,7 +127,7 @@ public function preSave(WebformSubmissionInterface $webform_submission) { } $destinationDir = 'private://signing'; - if (!\Drupal::service('file_system')->prepareDirectory($destinationDir, FileSystemInterface::CREATE_DIRECTORY)) { + if (!$this->fileSystem->prepareDirectory($destinationDir, FileSystemInterface::CREATE_DIRECTORY)) { $this->logger->error('File directory cannot be created: %filedirectory', ['%filedirectory' => $destinationDir]); return; } @@ -96,8 +136,7 @@ public function preSave(WebformSubmissionInterface $webform_submission) { // Save the file data. try { - /** @var \Drupal\file\FileInterface $fileToSign */ - $fileToSign = \Drupal::service('file.repository')->writeData($attachment['filecontent'], $fileUri, FileExists::Replace); + $fileToSign = $this->fileRepository->writeData($attachment['filecontent'], $fileUri, FileExists::Replace); } catch (\Exception $e) { $this->logger->error('File cannot be saved: %fileUri, error: %error', @@ -109,19 +148,16 @@ public function preSave(WebformSubmissionInterface $webform_submission) { } $fileToSign->save(); - $fileToSignPublicUrl = \Drupal::service('file_url_generator')->generateAbsoluteString($fileToSign->getFileUri()); - - /** @var \Drupal\os2forms_digital_signature\Service\SigningService $signingService */ - $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); + $fileToSignPublicUrl = $this->fileUrlGenerator->generateAbsoluteString($fileToSign->getFileUri()); - $cid = $signingService->getCid(); + $cid = $this->signingService->getCid(); if (empty($cid)) { $this->logger->error('Failed to obtain cid. Is server running?'); return; } // Creating hash. - $salt = \Drupal::service('settings')->get('hash_salt'); + $salt = $this->settings->get('hash_salt'); $hash = Crypt::hashBase64($webform_submission->uuid() . $webform->id() . $salt); $attachmentFid = $attachment['fid'] ?? NULL; @@ -135,7 +171,7 @@ public function preSave(WebformSubmissionInterface $webform_submission) { // Starting signing, if everything is correct - this funcition will start // redirect. - $signingService->sign($fileToSignPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); + $this->signingService->sign($fileToSignPublicUrl, $cid, $signatureCallbackUrl->setAbsolute()->toString()); } /** diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index 9eafa80d..6a305446 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -29,14 +29,14 @@ class SigningService { * * @var \Drupal\Core\Entity\EntityStorageInterface */ - protected EntityStorageInterface $webformStorage; + private readonly EntityStorageInterface $webformStorage; /** * WebformSubmission storage. * * @var \Drupal\Core\Entity\EntityStorageInterface */ - protected EntityStorageInterface $webformSubmissionStorage; + private readonly EntityStorageInterface $webformSubmissionStorage; public function __construct( private readonly ClientInterface $httpClient, @@ -57,7 +57,7 @@ public function __construct( * The correlation id. */ public function getCid() : ?string { - $url = $this->config->get('os2forms_digital_signature_remove_service_url') . 'action=getcid'; + $url = $this->getServiceUrl() . http_build_query(['action' => 'getcid']); $response = $this->httpClient->request('GET', $url); $result = $response->getBody()->getContents(); @@ -104,7 +104,7 @@ public function sign(string $document_uri, string $cid, string $forward_url):voi '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); + $url = $this->getServiceUrl() . http_build_query($params); $response = new RedirectResponse($url); $response->send(); @@ -145,7 +145,7 @@ public function download(string $filename, $leave = FALSE, $annotate = FALSE, $a 'annotate' => $annotate, 'attributes' => $attributes, ]; - $url = $this->config->get('os2forms_digital_signature_remove_service_url') . http_build_query($params); + $url = $this->getServiceUrl() . http_build_query($params); $response = $this->httpClient->request('GET', $url); $return = $response->getBody()->getContents(); @@ -239,4 +239,21 @@ public function deleteStalledSubmissions() : void { } } + /** + * Returns Remove service URL. + * + * @return string + * Remote Service URL, if missing '?' or '&', '?' will be added + * automatically. + */ + public function getServiceUrl() : string { + $url = $this->config->get('os2forms_digital_signature_remote_service_url'); + // Handling URL, if it does not end with '?' or '&'. + if (!str_ends_with($url, '?') && !str_ends_with($url, '&')) { + return $url . '?'; + } + + return $url; + } + } From 23fa488b2cd94a9c3f126c28ac1f30be1cb3aa53 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 9 Jun 2025 12:55:13 +0300 Subject: [PATCH 18/20] OS-110 refactoring, service injections --- .../Controller/DigitalSignatureController.php | 46 ++++++++++++------- .../src/Service/SigningService.php | 2 +- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 8fddc8b9..54a5d30c 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -3,26 +3,44 @@ namespace Drupal\os2forms_digital_signature\Controller; use Drupal\Component\Utility\Crypt; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Site\Settings; use Drupal\Core\Url; use Drupal\file\Entity\File; +use Drupal\os2forms_digital_signature\Service\SigningService; +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Digital Signature Controller. */ -class DigitalSignatureController { +class DigitalSignatureController extends ControllerBase { /** - * Logger for channel - os2forms_digital_signature. - * - * @var \Drupal\Core\Logger\LoggerChannelInterface + * Constructor. */ - protected $logger; + public function __construct( + private readonly LoggerInterface $logger, + private readonly Settings $settings, + private readonly SigningService $signingService, + private readonly FileSystemInterface $fileSystem, + ) { + } - public function __construct() { - $this->logger = \Drupal::logger('os2forms_digital_signature'); + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('logger.channel.os2forms_digital_signature'), + $container->get('settings'), + $container->get('os2forms_digital_signature.signing_service'), + $container->get('file_system'), + ); } /** @@ -70,26 +88,20 @@ public function signCallback($uuid, $hash, $fid = NULL) { } // Checking hash. - $salt = \Drupal::service('settings')->get('hash_salt'); + $salt = $this->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 \Drupal\os2forms_digital_signature\Service\SigningService $signingService */ - $signingService = \Drupal::service('os2forms_digital_signature.signing_service'); - $signedFilename = \Drupal::request()->get('file'); - $signedFileContent = $signingService->download($signedFilename); + $signedFileContent = $this->signingService->download($signedFilename); if (!$signedFileContent) { $this->logger->warning('Missing file on remote server %file.', ['%file' => $signedFilename]); throw new NotFoundHttpException(); } - /** @var \Drupal\Core\File\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) { @@ -101,14 +113,14 @@ public function signCallback($uuid, $hash, $fid = NULL) { $expectedFileUri = "private://webform/$webformId/digital_signature/$uuid.pdf"; $directory = dirname($expectedFileUri); - if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { + if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) { $this->logger->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); + $this->fileSystem->saveData($signedFileContent, $expectedFileUri, FileExists::Replace); // Updating webform submission. $webformSubmission->setLocked(TRUE); diff --git a/modules/os2forms_digital_signature/src/Service/SigningService.php b/modules/os2forms_digital_signature/src/Service/SigningService.php index 6a305446..47505243 100644 --- a/modules/os2forms_digital_signature/src/Service/SigningService.php +++ b/modules/os2forms_digital_signature/src/Service/SigningService.php @@ -240,7 +240,7 @@ public function deleteStalledSubmissions() : void { } /** - * Returns Remove service URL. + * Returns Remote signature service URL. * * @return string * Remote Service URL, if missing '?' or '&', '?' will be added From 4681c85dd64bfe118764a2bb1c4d79399bd175d9 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 9 Jun 2025 13:11:48 +0300 Subject: [PATCH 19/20] OS-110 refactoring, service injections --- .../Controller/DigitalSignatureController.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 54a5d30c..7a85318c 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -4,6 +4,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Site\Settings; @@ -13,6 +14,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -20,6 +22,13 @@ */ class DigitalSignatureController extends ControllerBase { + /** + * File Storage. + * + * @var EntityStorageInterface + */ + protected EntityStorageInterface $fileStorage; + /** * Constructor. */ @@ -28,7 +37,9 @@ public function __construct( private readonly Settings $settings, private readonly SigningService $signingService, private readonly FileSystemInterface $fileSystem, + private readonly RequestStack $requestStack, ) { + $this->fileStorage = $this->entityTypeManager()->getStorage('file'); } /** @@ -40,6 +51,7 @@ public static function create(ContainerInterface $container) { $container->get('settings'), $container->get('os2forms_digital_signature.signing_service'), $container->get('file_system'), + $container->get('request_stack'), ); } @@ -63,7 +75,7 @@ public static function create(ContainerInterface $container) { */ public function signCallback($uuid, $hash, $fid = NULL) { // Load the webform submission entity by UUID. - $submissions = \Drupal::entityTypeManager() + $submissions = $this->entityTypeManager() ->getStorage('webform_submission') ->loadByProperties(['uuid' => $uuid]); @@ -78,7 +90,9 @@ public function signCallback($uuid, $hash, $fid = NULL) { $webformId = $webformSubmission->getWebform()->id(); // Checking the action. - $action = \Drupal::request()->query->get('action'); + $request = $this->requestStack->getCurrentRequest(); + + $action = $request->query->get('action'); if ($action == 'cancel') { $cancelUrl = $webformSubmission->getWebform()->toUrl()->toString(); @@ -95,7 +109,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { throw new NotFoundHttpException(); } - $signedFilename = \Drupal::request()->get('file'); + $signedFilename = $request->get('file'); $signedFileContent = $this->signingService->download($signedFilename); if (!$signedFileContent) { $this->logger->warning('Missing file on remote server %file.', ['%file' => $signedFilename]); @@ -128,7 +142,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { // If file existing, resave the file to update the size and etc. if ($fid) { - File::load($fid)->save(); + $this->fileStorage->load($fid)?->save(); } } catch (\Exception $e) { From 2482846016d421db8bbb17622cf29d3295eb1484 Mon Sep 17 00:00:00 2001 From: Stanislav Kutasevits Date: Mon, 9 Jun 2025 13:14:29 +0300 Subject: [PATCH 20/20] OS-110 refactoring, phpcs --- .../src/Controller/DigitalSignatureController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php index 7a85318c..656bf7d4 100644 --- a/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php +++ b/modules/os2forms_digital_signature/src/Controller/DigitalSignatureController.php @@ -9,7 +9,6 @@ use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Site\Settings; use Drupal\Core\Url; -use Drupal\file\Entity\File; use Drupal\os2forms_digital_signature\Service\SigningService; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -25,7 +24,7 @@ class DigitalSignatureController extends ControllerBase { /** * File Storage. * - * @var EntityStorageInterface + * @var \Drupal\Core\Entity\EntityStorageInterface */ protected EntityStorageInterface $fileStorage; @@ -119,7 +118,7 @@ public function signCallback($uuid, $hash, $fid = NULL) { // If $fid is present - we are replacing uploaded/managed file, otherwise // creating a new one. if ($fid) { - $file = File::load($fid); + $file = $this->fileStorage->load($fid); $expectedFileUri = $file->getFileUri(); } else {