From 7410949bca15239dc7e0b903af59b64288c5193f Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Tue, 7 May 2024 14:26:01 +0200 Subject: [PATCH] Moved digital post settings into config. Added support for os2web_key to get certificate. --- .github/workflows/pr.yml | 31 +-- .gitignore | 2 - .markdownlint.jsonc | 13 + .markdownlintrc | 18 -- CHANGELOG.md | 3 + README.md | 20 +- composer.json | 90 ++++--- modules/os2forms_digital_post/README.md | 4 + .../os2forms_digital_post.info.yml | 1 + .../os2forms_digital_post.install | 9 + .../os2forms_digital_post.services.yml | 6 +- .../src/Exception/InvalidSettingException.php | 10 - .../src/Form/SettingsForm.php | 238 +++++++----------- .../src/Helper/CertificateLocatorHelper.php | 79 ------ .../src/Helper/DigitalPostHelper.php | 8 +- .../src/Helper/KeyCertificateLocator.php | 59 +++++ .../src/Helper/MemoryCertificateLocator.php | 49 ++++ .../src/Helper/Settings.php | 146 +++++++---- package.json | 13 - scripts/code-analysis | 18 +- 20 files changed, 419 insertions(+), 398 deletions(-) create mode 100644 .markdownlint.jsonc delete mode 100644 .markdownlintrc delete mode 100644 modules/os2forms_digital_post/src/Exception/InvalidSettingException.php delete mode 100644 modules/os2forms_digital_post/src/Helper/CertificateLocatorHelper.php create mode 100644 modules/os2forms_digital_post/src/Helper/KeyCertificateLocator.php create mode 100644 modules/os2forms_digital_post/src/Helper/MemoryCertificateLocator.php delete mode 100644 package.json diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 42565efe..ca98f738 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -49,6 +49,9 @@ jobs: composer validate --strict composer.json # Check that dependencies resolve. composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction + - name: Check that composer file is normalized + run: | + composer normalize --dry-run php-coding-standards: name: PHP coding standards @@ -111,27 +114,13 @@ jobs: run: | ./scripts/code-analysis - markdownlint: + coding-standards-markdown: + name: Markdown coding standards runs-on: ubuntu-latest - name: markdownlint steps: - name: Checkout - uses: actions/checkout@v2 - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: Cache yarn packages - uses: actions/cache@v2 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Yarn install - uses: actions/setup-node@v2 - with: - node-version: '20' - - run: yarn install - - name: markdownlint - run: yarn coding-standards-check/markdownlint + uses: actions/checkout@master + + - name: Coding standards + run: | + docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' diff --git a/.gitignore b/.gitignore index 1cc8643c..1a6c2523 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,3 @@ composer.lock vendor -node_modules/ -yarn.lock diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 00000000..a28c5809 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,13 @@ +{ + "default": true, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md + "line-length": { + "line_length": 120, + "code_blocks": false, + "tables": false + }, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md + "no-duplicate-heading": { + "siblings_only": true + } +} diff --git a/.markdownlintrc b/.markdownlintrc deleted file mode 100644 index 75637156..00000000 --- a/.markdownlintrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - // @see https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.jsonc - // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md - "MD013": { - // Exclude code blocks - "code_blocks": false - }, - - // Prevent complaining on duplicated headings in CHANGELOG.md - // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md - "MD024": { - "siblings_only": true - } -} - -// Local Variables: -// mode: json -// End: diff --git a/CHANGELOG.md b/CHANGELOG.md index d15a292c..37dd9ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [#101](https://github.com/OS2Forms/os2forms/pull/101) + Added support for os2web_key + ## [3.15.0] 2024-05-03 - Added webform encryption modules diff --git a/README.md b/README.md index 12e4ad05..90245a21 100644 --- a/README.md +++ b/README.md @@ -122,29 +122,27 @@ run the checks locally. ```sh docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.1-fpm composer install -docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.1-fpm composer coding-standards-check - # Fix (some) coding standards issues. docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.1-fpm composer coding-standards-apply +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.1-fpm composer coding-standards-check ``` ### Markdown ```sh -docker run --rm --volume ${PWD}:/app --workdir /app node:20 yarn install -docker run --rm --volume ${PWD}:/app --workdir /app node:20 yarn coding-standards-check/markdownlint - -# Fix (some) coding standards issues. -docker run --rm --volume ${PWD}:/app --workdir /app node:20 yarn coding-standards-apply/markdownlint +docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' --fix +docker run --rm --volume $PWD:/md peterdavehello/markdownlint markdownlint --ignore vendor --ignore LICENSE.md '**/*.md' ``` ## Code analysis We use [PHPStan](https://phpstan.org/) for static code analysis. -Running statis code analysis on a standalone Drupal module is a bit tricky, so -we use a helper script to run the analysis: +Running statis code analysis on a standalone Drupal module is a bit tricky, so we use a helper script to run the +analysis: -```sh -./scripts/code-analysis +```shell +docker run --rm --volume ${PWD}:/app --workdir /app itkdev/php8.1-fpm ./scripts/code-analysis ``` + +**Note**: Currently the code analysis is only run on the `os2forms_digital_post` sub-module (cf. [`phpstan.neon`](./phpstan.neon)). diff --git a/composer.json b/composer.json index beee4327..f9e1809c 100644 --- a/composer.json +++ b/composer.json @@ -1,20 +1,8 @@ { "name": "os2forms/os2forms", - "type": "drupal-module", "description": "Drupal 8 OS2Form module provides advanced webform functionality for Danish Municipalities", - "minimum-stability": "dev", - "prefer-stable": true, "license": "EUPL-1.2", - "repositories": { - "drupal": { - "type": "composer", - "url": "https://packages.drupal.org/8" - }, - "assets": { - "type": "composer", - "url": "https://asset-packagist.org" - } - }, + "type": "drupal-module", "require": { "php": "^8.1", "ext-dom": "*", @@ -47,7 +35,7 @@ "drupal/mailsystem": "^4.1", "drupal/masquerade": "^2.0@RC", "drupal/pathauto": "^1.5", - "drupal/permissions_by_term": "^3.1 || ^2.25", + "drupal/permissions_by_term": "^2.25 || ^3.1", "drupal/queue_mail": "^1.4", "drupal/queue_ui": "^2.1", "drupal/r4032login": "^2.1", @@ -77,11 +65,12 @@ "itk-dev/beskedfordeler-drupal": "^1.0", "itk-dev/serviceplatformen": "dev-feature/guzzle6-adapter as 1.5", "os2web/os2web_datalookup": "^1.5", + "os2web/os2web_key": "dev-os2web_key", "os2web/os2web_nemlogin": "^1.0", "php-http/guzzle6-adapter": "^2.0", "phpoffice/phpword": "^0.18.2", "symfony/options-resolver": "^5.4 || ^6.0", - "tecnickcom/tcpdf": "~6", + "tecnickcom/tcpdf": "^6", "webmozart/path-util": "^2.3", "wsdltophp/packagebase": "^5.0", "zaporylie/composer-drupal-optimizations": "^1.2" @@ -90,25 +79,45 @@ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", "drupal/coder": "^8.3", "drupal/maillog": "^1.0", + "ergebnis/composer-normalize": "^2.42", "mglaman/phpstan-drupal": "^1.1", "phpstan/extension-installer": "^1.3", "phpstan/phpstan-deprecation-rules": "^1.1", "phpunit/phpunit": "^9.5", "wsdltophp/packagegenerator": "^4.0" }, - "extra" : { + "repositories": { + "os2web/os2web_key": { + "type": "vcs", + "url": "https://github.com/itk-dev/os2web_key" + }, + "drupal": { + "type": "composer", + "url": "https://packages.drupal.org/8" + }, + "assets": { + "type": "composer", + "url": "https://asset-packagist.org" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "cweagans/composer-patches": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true, + "simplesamlphp/composer-module-installer": true, + "vaimo/composer-patches": true, + "zaporylie/composer-drupal-optimizations": true + }, + "sort-packages": true + }, + "extra": { "composer-exit-on-patch-failure": false, - "enable-patching" : true, + "enable-patching": true, "patches": { - "drupal/entity_print": { - "2733781 - Add Export to Word Support": "https://www.drupal.org/files/issues/2019-11-22/2733781-47.patch" - }, - "drupal/webform": { - "Unlock possibility of using Entity print module export to Word": "https://www.drupal.org/files/issues/2020-02-29/3096552-6.patch" - }, - "drupal/user_default_page": { - "Warning: in_array() expects parameter 2 to be array, null given in user_default_page_user_logout() (https://www.drupal.org/node/3246986)": "https://www.drupal.org/files/issues/2021-11-01/user_default_page-3246986-2.patch" - }, "drupal/coc_forms_auto_export": { "3240592 - Problem with phpseclib requirement in 2.x (https://www.drupal.org/project/coc_forms_auto_export/issues/3240592)": "https://www.drupal.org/files/issues/2021-10-04/requirement-namespace-3240592-1.patch" }, @@ -121,34 +130,23 @@ } }, "scripts": { - "code-analysis/phpstan": [ - "phpstan analyse" - ], "code-analysis": [ "@code-analysis/phpstan" ], - "coding-standards-check/phpcs": [ - "phpcs --standard=phpcs.xml.dist" + "code-analysis/phpstan": [ + "phpstan analyse" ], - "coding-standards-check": [ - "@coding-standards-check/phpcs" + "coding-standards-apply": [ + "@coding-standards-apply/phpcs" ], "coding-standards-apply/phpcs": [ "phpcbf --standard=phpcs.xml.dist" ], - "coding-standards-apply": [ - "@coding-standards-apply/phpcs" + "coding-standards-check": [ + "@coding-standards-check/phpcs" + ], + "coding-standards-check/phpcs": [ + "phpcs --standard=phpcs.xml.dist" ] - }, - "config": { - "sort-packages": true, - "allow-plugins": { - "cweagans/composer-patches": true, - "dealerdirect/phpcodesniffer-composer-installer": true, - "phpstan/extension-installer": true, - "simplesamlphp/composer-module-installer": true, - "vaimo/composer-patches": true, - "zaporylie/composer-drupal-optimizations": true - } } } diff --git a/modules/os2forms_digital_post/README.md b/modules/os2forms_digital_post/README.md index a487b13e..c36e9f48 100644 --- a/modules/os2forms_digital_post/README.md +++ b/modules/os2forms_digital_post/README.md @@ -31,6 +31,10 @@ examples](modules/os2forms_digital_post_examples/README.md). Go to `/admin/os2forms_digital_post/settings` to set up global settings for digital post. +### Key + +We use [os2web_key](https://github.com/OS2web/os2web_key) to provide the certificate for sending digital post. + ### Queue The actual sending of digital post is handled by jobs in an [Advanced diff --git a/modules/os2forms_digital_post/os2forms_digital_post.info.yml b/modules/os2forms_digital_post/os2forms_digital_post.info.yml index 266ecf78..8660bf80 100644 --- a/modules/os2forms_digital_post/os2forms_digital_post.info.yml +++ b/modules/os2forms_digital_post/os2forms_digital_post.info.yml @@ -7,6 +7,7 @@ dependencies: - 'beskedfordeler:beskedfordeler' - 'drupal:advancedqueue' - 'os2web_datalookup:os2web_datalookup' + - 'os2web_key:os2web_key' - 'webform:webform' - 'webform:webform_submission_log' diff --git a/modules/os2forms_digital_post/os2forms_digital_post.install b/modules/os2forms_digital_post/os2forms_digital_post.install index 760743cb..80b756ac 100644 --- a/modules/os2forms_digital_post/os2forms_digital_post.install +++ b/modules/os2forms_digital_post/os2forms_digital_post.install @@ -17,3 +17,12 @@ use Drupal\os2forms_digital_post\Helper\BeskedfordelerHelper; function os2forms_digital_post_schema() { return Drupal::service(BeskedfordelerHelper::class)->schema(); } + +/** + * Implements hook_update_N(). + */ +function os2forms_digital_post_update_9001() { + \Drupal::service('module_installer')->install([ + 'os2web_key', + ], TRUE); +} diff --git a/modules/os2forms_digital_post/os2forms_digital_post.services.yml b/modules/os2forms_digital_post/os2forms_digital_post.services.yml index a40d88a4..f1793774 100644 --- a/modules/os2forms_digital_post/os2forms_digital_post.services.yml +++ b/modules/os2forms_digital_post/os2forms_digital_post.services.yml @@ -9,11 +9,13 @@ services: Drupal\os2forms_digital_post\Helper\Settings: arguments: - - "@keyvalue" + - "@config.factory" + - "@key.repository" Drupal\os2forms_digital_post\Helper\CertificateLocatorHelper: arguments: - "@Drupal\\os2forms_digital_post\\Helper\\Settings" + - "@key.repository" Drupal\os2forms_digital_post\Helper\MeMoHelper: arguments: @@ -30,7 +32,7 @@ services: Drupal\os2forms_digital_post\Helper\DigitalPostHelper: arguments: - "@Drupal\\os2forms_digital_post\\Helper\\Settings" - - "@Drupal\\os2forms_digital_post\\Helper\\CertificateLocatorHelper" + - "@Drupal\\os2web_key\\CertificateHelper" - "@plugin.manager.os2web_datalookup" - "@Drupal\\os2forms_digital_post\\Helper\\MeMoHelper" - "@Drupal\\os2forms_digital_post\\Helper\\ForsendelseHelper" diff --git a/modules/os2forms_digital_post/src/Exception/InvalidSettingException.php b/modules/os2forms_digital_post/src/Exception/InvalidSettingException.php deleted file mode 100644 index c3d34af6..00000000 --- a/modules/os2forms_digital_post/src/Exception/InvalidSettingException.php +++ /dev/null @@ -1,10 +0,0 @@ -queueStorage = $entityTypeManager->getStorage('advancedqueue_queue'); } @@ -44,12 +46,23 @@ public function __construct( */ public static function create(ContainerInterface $container) { return new static( + $container->get('config.factory'), + $container->get('entity_type.manager'), $container->get(Settings::class), - $container->get(CertificateLocatorHelper::class), - $container->get('entity_type.manager') ); } + /** + * {@inheritdoc} + * + * @phpstan-return array + */ + protected function getEditableConfigNames() { + return [ + Settings::CONFIG_NAME, + ]; + } + /** * {@inheritdoc} */ @@ -63,15 +76,26 @@ public function getFormId() { * @phpstan-param array $form * @phpstan-return array */ - public function buildForm(array $form, FormStateInterface $form_state) { - $form['test_mode'] = [ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form = parent::buildForm($form, $form_state); + + $form['message'] = [ + '#theme' => 'status_messages', + '#message_list' => [ + 'status' => [ + $this->t('Use drush os2forms-digital-post:test:send to test sending digital post.'), + ], + ], + ]; + + $form[Settings::TEST_MODE] = [ '#type' => 'checkbox', '#title' => $this->t('Test mode'), - '#default_value' => $this->settings->getTestMode(), + '#default_value' => $this->settings->getEditableValue(Settings::TEST_MODE), + '#description' => $this->createDescription(Settings::TEST_MODE), ]; - $sender = $this->settings->getSender(); - $form['sender'] = [ + $form[Settings::SENDER] = [ '#type' => 'fieldset', '#title' => $this->t('Sender'), '#tree' => TRUE, @@ -82,126 +106,74 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#options' => [ 'CVR' => $this->t('CVR'), ], - '#default_value' => $sender[Settings::SENDER_IDENTIFIER_TYPE] ?? 'CVR', + '#default_value' => $this->settings->getEditableValue([Settings::SENDER, Settings::SENDER_IDENTIFIER_TYPE]) ?? 'CVR', '#required' => TRUE, + '#description' => $this->createDescription([Settings::SENDER, Settings::SENDER_IDENTIFIER_TYPE]), ], Settings::SENDER_IDENTIFIER => [ '#type' => 'textfield', '#title' => $this->t('Identifier'), - '#default_value' => $sender[Settings::SENDER_IDENTIFIER] ?? NULL, + '#default_value' => $this->settings->getEditableValue([Settings::SENDER, Settings::SENDER_IDENTIFIER]), '#required' => TRUE, + '#description' => $this->createDescription([Settings::SENDER, Settings::SENDER_IDENTIFIER]), ], Settings::FORSENDELSES_TYPE_IDENTIFIKATOR => [ '#type' => 'textfield', '#title' => $this->t('Forsendelsestypeidentifikator'), - '#default_value' => $sender[Settings::FORSENDELSES_TYPE_IDENTIFIKATOR] ?? NULL, + '#default_value' => $this->settings->getEditableValue([ + Settings::SENDER, Settings::FORSENDELSES_TYPE_IDENTIFIKATOR, + ]), '#required' => TRUE, + '#description' => $this->createDescription([Settings::SENDER, Settings::FORSENDELSES_TYPE_IDENTIFIKATOR]), ], ]; - $certificate = $this->settings->getCertificate(); - $form['certificate'] = [ + $form[Settings::CERTIFICATE] = [ '#type' => 'fieldset', '#title' => $this->t('Certificate'), '#tree' => TRUE, - 'locator_type' => [ - '#type' => 'select', - '#title' => $this->t('Certificate locator type'), - '#options' => [ - 'azure_key_vault' => $this->t('Azure key vault'), - 'file_system' => $this->t('File system'), - ], - '#default_value' => $certificate['locator_type'] ?? NULL, - ], - ]; - - $form['certificate'][CertificateLocatorHelper::LOCATOR_TYPE_AZURE_KEY_VAULT] = [ - '#type' => 'fieldset', - '#title' => $this->t('Azure key vault'), - '#states' => [ - 'visible' => [':input[name="certificate[locator_type]"]' => ['value' => CertificateLocatorHelper::LOCATOR_TYPE_AZURE_KEY_VAULT]], - ], - ]; - - $settings = [ - 'tenant_id' => ['title' => $this->t('Tenant id')], - 'application_id' => ['title' => $this->t('Application id')], - 'client_secret' => ['title' => $this->t('Client secret')], - 'name' => ['title' => $this->t('Name')], - 'secret' => ['title' => $this->t('Secret')], - 'version' => ['title' => $this->t('Version')], - ]; - - foreach ($settings as $key => $info) { - $form['certificate'][CertificateLocatorHelper::LOCATOR_TYPE_AZURE_KEY_VAULT][$key] = [ - '#type' => 'textfield', - '#title' => $info['title'], - '#default_value' => $certificate[CertificateLocatorHelper::LOCATOR_TYPE_AZURE_KEY_VAULT][$key] ?? NULL, - '#states' => [ - 'required' => [':input[name="certificate[locator_type]"]' => ['value' => CertificateLocatorHelper::LOCATOR_TYPE_AZURE_KEY_VAULT]], - ], - ]; - } - - $form['certificate'][CertificateLocatorHelper::LOCATOR_TYPE_FILE_SYSTEM] = [ - '#type' => 'fieldset', - '#title' => $this->t('File system'), - '#states' => [ - 'visible' => [':input[name="certificate[locator_type]"]' => ['value' => CertificateLocatorHelper::LOCATOR_TYPE_FILE_SYSTEM]], - ], - - 'path' => [ - '#type' => 'textfield', - '#title' => $this->t('Path'), - '#default_value' => $certificate[CertificateLocatorHelper::LOCATOR_TYPE_FILE_SYSTEM]['path'] ?? NULL, - '#states' => [ - 'required' => [':input[name="certificate[locator_type]"]' => ['value' => CertificateLocatorHelper::LOCATOR_TYPE_FILE_SYSTEM]], + Settings::KEY => [ + '#type' => 'key_select', + '#key_filters' => [ + 'type' => 'os2web_certificate', ], + '#key_description' => FALSE, + '#title' => $this->t('Key'), + '#default_value' => $this->settings->getEditableValue([Settings::CERTIFICATE, Settings::KEY]), + '#required' => TRUE, + '#description' => $this->createDescription([Settings::CERTIFICATE, Settings::KEY]), ], ]; - $form['certificate']['passphrase'] = [ - '#type' => 'textfield', - '#title' => $this->t('Passphrase'), - '#default_value' => $certificate['passphrase'] ?? NULL, - ]; - - $processing = $this->settings->getProcessing(); - $form['processing'] = [ + $form[Settings::PROCESSING] = [ '#type' => 'fieldset', '#title' => $this->t('Processing'), '#tree' => TRUE, ]; - $defaultValue = $processing['queue'] ?? 'os2forms_digital_post'; - $form['processing']['queue'] = [ + $queue = $this->settings->getEditableValue([Settings::PROCESSING, Settings::QUEUE]); + $form[Settings::PROCESSING][Settings::QUEUE] = [ '#type' => 'select', '#title' => $this->t('Queue'), '#options' => array_map( static fn(EntityInterface $queue) => $queue->label(), $this->queueStorage->loadMultiple() ), - '#default_value' => $defaultValue, - '#description' => $this->t("Queue for digital post jobs. The queue must be run via Drupal's cron or via drush advancedqueue:queue:process @queue(in a cron job).", [ - '@queue' => $defaultValue, - ':queue_url' => '/admin/config/system/queues/jobs/' . urlencode($defaultValue), - ]), - ]; - - $form['actions']['#type'] = 'actions'; - - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Save settings'), - ]; - - $form['actions']['testCertificate'] = [ - '#type' => 'submit', - '#name' => 'testCertificate', - '#value' => $this->t('Test certificate'), + '#required' => TRUE, + '#default_value' => $queue, + '#description' => $this->createDescription([Settings::PROCESSING, Settings::QUEUE], + $queue + ? $this->t("Queue for digital post jobs. The queue must be run via Drupal's cron or via drush advancedqueue:queue:process @queue (in a cron job).", [ + '@queue' => $queue, + ':queue_url' => Url::fromRoute('view.advancedqueue_jobs.page_1', [ + 'arg_0' => $queue, + ])->toString(TRUE)->getGeneratedUrl(), + ]) + : $this->t("Queue for digital post jobs. The queue must be processed via Drupal's cron or drush advancedqueue:queue:process (in a cron job)."), + ), ]; return $form; @@ -212,59 +184,41 @@ public function buildForm(array $form, FormStateInterface $form_state) { * * @phpstan-param array $form */ - public function validateForm(array &$form, FormStateInterface $formState): void { - $triggeringElement = $formState->getTriggeringElement(); - if ('testCertificate' === ($triggeringElement['#name'] ?? NULL)) { - return; + public function submitForm(array &$form, FormStateInterface $form_state): void { + $config = $this->config(Settings::CONFIG_NAME); + foreach ([ + Settings::TEST_MODE, + Settings::SENDER, + Settings::CERTIFICATE, + Settings::PROCESSING, + ] as $key) { + $config->set($key, $form_state->getValue($key)); } + $config->save(); - $values = $formState->getValues(); - if (CertificateLocatorHelper::LOCATOR_TYPE_FILE_SYSTEM === $values['certificate']['locator_type']) { - $path = $values['certificate'][CertificateLocatorHelper::LOCATOR_TYPE_FILE_SYSTEM]['path'] ?? NULL; - if (!file_exists($path)) { - $formState->setErrorByName('certificate][file_system][path', $this->t('Invalid certificate path: %path', ['%path' => $path])); - } - } + parent::submitForm($form, $form_state); } /** - * {@inheritdoc} + * Create form field description with information on any runtime override. * - * @phpstan-param array $form + * @param string|array $key + * The key. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description + * The actual field description. + * + * @return string + * The full description. */ - public function submitForm(array &$form, FormStateInterface $formState): void { - $triggeringElement = $formState->getTriggeringElement(); - if ('testCertificate' === ($triggeringElement['#name'] ?? NULL)) { - $this->testCertificate(); - return; - } - - try { - $settings['test_mode'] = (bool) $formState->getValue('test_mode'); - $settings['sender'] = $formState->getValue('sender'); - $settings['certificate'] = $formState->getValue('certificate'); - $settings['processing'] = $formState->getValue('processing'); - $this->settings->setSettings($settings); - $this->messenger()->addStatus($this->t('Settings saved')); - } - catch (OptionsResolverException $exception) { - $this->messenger()->addError($this->t('Settings not saved (@message)', ['@message' => $exception->getMessage()])); + private function createDescription(string|array $key, ?TranslatableMarkup $description = NULL): string { + if ($value = $this->settings->getOverride($key)) { + if (!empty($description)) { + $description .= '
'; + } + $description .= $this->t('Note: overridden on runtime with the value @value.', ['@value' => var_export($value['runtime'], TRUE)]); } - } - /** - * Test certificate. - */ - private function testCertificate(): void { - try { - $certificateLocator = $this->certificateLocatorHelper->getCertificateLocator(); - $certificateLocator->getCertificates(); - $this->messenger()->addStatus($this->t('Certificate succesfully tested')); - } - catch (\Throwable $throwable) { - $message = $this->t('Error testing certificate: %message', ['%message' => $throwable->getMessage()]); - $this->messenger()->addError($message); - } + return (string) $description; } } diff --git a/modules/os2forms_digital_post/src/Helper/CertificateLocatorHelper.php b/modules/os2forms_digital_post/src/Helper/CertificateLocatorHelper.php deleted file mode 100644 index 7c4d4117..00000000 --- a/modules/os2forms_digital_post/src/Helper/CertificateLocatorHelper.php +++ /dev/null @@ -1,79 +0,0 @@ -settings->getCertificate(); - - $locatorType = $certificateSettings['locator_type']; - $options = $certificateSettings[$locatorType]; - $options += [ - 'passphrase' => $certificateSettings['passphrase'] ?: '', - ]; - - if (self::LOCATOR_TYPE_AZURE_KEY_VAULT === $locatorType) { - $httpClient = new GuzzleAdapter(new Client()); - $requestFactory = new RequestFactory(); - - $vaultToken = new VaultToken($httpClient, $requestFactory); - - $token = $vaultToken->getToken( - $options['tenant_id'], - $options['application_id'], - $options['client_secret'], - ); - - $vault = new VaultSecret( - $httpClient, - $requestFactory, - $options['name'], - $token->getAccessToken() - ); - - return new AzureKeyVaultCertificateLocator( - $vault, - $options['secret'], - $options['version'], - $options['passphrase'], - ); - } - elseif (self::LOCATOR_TYPE_FILE_SYSTEM === $locatorType) { - $certificatepath = realpath($options['path']) ?: NULL; - if (NULL === $certificatepath) { - throw new CertificateLocatorException(sprintf('Invalid certificate path %s', $options['path'])); - } - return new FilesystemCertificateLocator($certificatepath, $options['passphrase']); - } - - throw new CertificateLocatorException(sprintf('Invalid certificate locator type: %s', $locatorType)); - } - -} diff --git a/modules/os2forms_digital_post/src/Helper/DigitalPostHelper.php b/modules/os2forms_digital_post/src/Helper/DigitalPostHelper.php index c51f0dee..87d50c85 100644 --- a/modules/os2forms_digital_post/src/Helper/DigitalPostHelper.php +++ b/modules/os2forms_digital_post/src/Helper/DigitalPostHelper.php @@ -10,6 +10,7 @@ use Drupal\os2web_datalookup\Plugin\DataLookupManager; use Drupal\os2web_datalookup\Plugin\os2web\DataLookup\DataLookupInterfaceCompany; use Drupal\os2web_datalookup\Plugin\os2web\DataLookup\DataLookupInterfaceCpr; +use Drupal\os2web_key\CertificateHelper; use Drupal\webform\WebformSubmissionInterface; use ItkDev\Serviceplatformen\Service\SF1601\Serializer; use ItkDev\Serviceplatformen\Service\SF1601\SF1601; @@ -28,7 +29,7 @@ final class DigitalPostHelper implements LoggerInterface { */ public function __construct( private readonly Settings $settings, - private readonly CertificateLocatorHelper $certificateLocatorHelper, + private readonly CertificateHelper $certificateHelper, private readonly DataLookupManager $dataLookupManager, private readonly MeMoHelper $meMoHelper, private readonly ForsendelseHelper $forsendelseHelper, @@ -60,7 +61,10 @@ public function sendDigitalPost(string $type, Message $message, ?ForsendelseI $f $options = [ 'test_mode' => (bool) $this->settings->getTestMode(), 'authority_cvr' => $senderSettings[Settings::SENDER_IDENTIFIER], - 'certificate_locator' => $this->certificateLocatorHelper->getCertificateLocator(), + 'certificate_locator' => new KeyCertificateLocator( + $this->settings->getCertificateKey(), + $this->certificateHelper + ), ]; $service = new SF1601($options); $transactionId = Serializer::createUuid(); diff --git a/modules/os2forms_digital_post/src/Helper/KeyCertificateLocator.php b/modules/os2forms_digital_post/src/Helper/KeyCertificateLocator.php new file mode 100644 index 00000000..832bc19f --- /dev/null +++ b/modules/os2forms_digital_post/src/Helper/KeyCertificateLocator.php @@ -0,0 +1,59 @@ + + */ + public function getCertificates(): array { + if (!isset($this->certificates)) { + $this->certificates = $this->certificateHelper->getCertificates($this->key); + } + + return $this->certificates; + } + + /** + * {@inheritdoc} + */ + public function getCertificate(): string { + return $this->key->getKeyValue(); + } + + /** + * {@inheritdoc} + */ + public function getAbsolutePathToCertificate(): string { + throw new CertificateLocatorException(__METHOD__ . ' should not be used.'); + } + +} diff --git a/modules/os2forms_digital_post/src/Helper/MemoryCertificateLocator.php b/modules/os2forms_digital_post/src/Helper/MemoryCertificateLocator.php new file mode 100644 index 00000000..69d791d2 --- /dev/null +++ b/modules/os2forms_digital_post/src/Helper/MemoryCertificateLocator.php @@ -0,0 +1,49 @@ + + */ + public function getCertificates(): array { + $certificates = []; + $this->passphrase = 'P5bISuw?s:u4'; + if (!openssl_pkcs12_read($this->certificate, $certificates, $this->passphrase)) { + throw new CertificateLocatorException(sprintf('Could not read certificate: %s', openssl_error_string() ?: '')); + } + + return $certificates; + } + + /** + * {@inheritdoc} + */ + public function getCertificate(): string { + return $this->certificate; + } + + /** + * {@inheritdoc} + */ + public function getAbsolutePathToCertificate(): string { + throw new CertificateLocatorException(__METHOD__ . ' should not be used.'); + } + +} diff --git a/modules/os2forms_digital_post/src/Helper/Settings.php b/modules/os2forms_digital_post/src/Helper/Settings.php index e64be738..c0d5384d 100644 --- a/modules/os2forms_digital_post/src/Helper/Settings.php +++ b/modules/os2forms_digital_post/src/Helper/Settings.php @@ -2,45 +2,61 @@ namespace Drupal\os2forms_digital_post\Helper; -use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueStoreInterface; -use Drupal\os2forms_digital_post\Exception\InvalidSettingException; -use Symfony\Component\OptionsResolver\OptionsResolver; +use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\ImmutableConfig; +use Drupal\key\KeyInterface; +use Drupal\key\KeyRepositoryInterface; /** * General settings for os2forms_digital_post. */ final class Settings { + public const CONFIG_NAME = 'os2forms_digital_post.settings'; + + public const TEST_MODE = 'test_mode'; + + public const SENDER = 'sender'; public const SENDER_IDENTIFIER_TYPE = 'sender_identifier_type'; public const SENDER_IDENTIFIER = 'sender_identifier'; public const FORSENDELSES_TYPE_IDENTIFIKATOR = 'forsendelses_type_identifikator'; + public const CERTIFICATE = 'certificate'; + public const KEY = 'key'; + + public const PROCESSING = 'processing'; + public const QUEUE = 'queue'; + /** - * The store. + * The runtime (immutable) config. * - * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + * @var \Drupal\Core\Config\ImmutableConfig */ - private KeyValueStoreInterface $store; + private ImmutableConfig $runtimeConfig; /** - * The key prefix. + * The (mutable) config. * - * @var string + * @var \Drupal\Core\Config\Config */ - private $collection = 'os2forms_digital_post.'; + private Config $editableConfig; /** - * Constructor. + * The constructor. */ - public function __construct(KeyValueFactoryInterface $keyValueFactory) { - $this->store = $keyValueFactory->get($this->collection); + public function __construct( + ConfigFactoryInterface $configFactory, + private readonly KeyRepositoryInterface $keyRepository, + ) { + $this->runtimeConfig = $configFactory->get(self::CONFIG_NAME); + $this->editableConfig = $configFactory->getEditable(self::CONFIG_NAME); } /** * Get test mode. */ public function getTestMode(): bool { - return (bool) $this->get('test_mode', TRUE); + return (bool) $this->get(self::TEST_MODE, TRUE); } /** @@ -49,18 +65,25 @@ public function getTestMode(): bool { * @phpstan-return array */ public function getSender(): array { - $value = $this->get('sender'); + $value = $this->get(self::SENDER); + return is_array($value) ? $value : []; } + /** + * Get key. + */ + public function getKey(): ?string { + return $this->get([self::CERTIFICATE, self::KEY]); + } + /** * Get certificate. - * - * @phpstan-return array */ - public function getCertificate(): array { - $value = $this->get('certificate'); - return is_array($value) ? $value : []; + public function getCertificateKey(): ?KeyInterface { + return $this->keyRepository->getKey( + $this->getKey(), + ); } /** @@ -69,57 +92,82 @@ public function getCertificate(): array { * @phpstan-return array */ public function getProcessing(): array { - $value = $this->get('processing'); + $value = $this->get(self::PROCESSING); + return is_array($value) ? $value : []; } /** - * Get a setting value. + * Get editable value. * - * @param string $key + * @param string|array $key * The key. - * @param mixed|null $default - * The default value. * * @return mixed - * The setting value. + * The editable value. */ - private function get(string $key, $default = NULL) { - $resolver = $this->getSettingsResolver(); - if (!$resolver->isDefined($key)) { - throw new InvalidSettingException(sprintf('Setting %s is not defined', $key)); + public function getEditableValue(string|array $key): mixed { + if (is_array($key)) { + $key = implode('.', $key); } - - return $this->store->get($key, $default); + return $this->editableConfig->get($key); } /** - * Set settings. + * Get runtime value override if any. * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface + * @param string|array $key + * The key. * - * @phpstan-param array $settings + * @return array|null + * - 'runtime': the runtime value + * - 'editable': the editable (raw) value */ - public function setSettings(array $settings): self { - $settings = $this->getSettingsResolver()->resolve($settings); - foreach ($settings as $key => $value) { - $this->store->set($key, $value); + public function getOverride(string|array $key): ?array { + $runtimeValue = $this->getRuntimeValue($key); + $editableValue = $this->getEditableValue($key); + + // Note: We deliberately use "Equal" (==) rather than "Identical" (===) + // to compare values (cf. https://www.php.net/manual/en/language.operators.comparison.php#language.operators.comparison). + if ($runtimeValue == $editableValue) { + return NULL; } - return $this; + return [ + 'runtime' => $runtimeValue, + 'editable' => $editableValue, + ]; } /** - * Get settings resolver. + * Get a setting value. + * + * @param string|array $key + * The key. + * @param mixed $default + * The default value. + * + * @return mixed + * The setting value. */ - private function getSettingsResolver(): OptionsResolver { - return (new OptionsResolver()) - ->setDefaults([ - 'test_mode' => TRUE, - 'sender' => [], - 'certificate' => [], - 'processing' => [], - ]); + private function get(string|array $key, mixed $default = NULL) { + return $this->getRuntimeValue($key) ?? $default; + } + + /** + * Get runtime value with any overrides applied. + * + * @param string|array $key + * The key. + * + * @return mixed + * The runtime value. + */ + public function getRuntimeValue(string|array $key): mixed { + if (is_array($key)) { + $key = implode('.', $key); + } + return $this->runtimeConfig->get($key); } } diff --git a/package.json b/package.json deleted file mode 100644 index 52fcd34c..00000000 --- a/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "license": "UNLICENSED", - "private": true, - "devDependencies": { - "markdownlint-cli": "^0.32.2" - }, - "scripts": { - "coding-standards-check/markdownlint": "yarn markdownlint --ignore LICENSE.md --ignore vendor --ignore node_modules '*.md' 'modules/os2forms_digital_post/**/*.md'", - "coding-standards-check": "yarn coding-standards-check/markdownlint", - "coding-standards-apply/markdownlint": "yarn markdownlint --ignore LICENSE.md --ignore vendor --ignore node_modules '*.md' 'modules/os2forms_digital_post/**/*.md' --fix", - "coding-standards-apply": "yarn coding-standards-apply/markdownlint" - } -} diff --git a/scripts/code-analysis b/scripts/code-analysis index f8ace6c7..5a3c1e25 100755 --- a/scripts/code-analysis +++ b/scripts/code-analysis @@ -16,9 +16,21 @@ if [ ! -f "$drupal_dir/composer.json" ]; then composer --no-interaction create-project drupal/recommended-project:^9 "$drupal_dir" fi # Copy our code into the modules folder -mkdir -p "$drupal_dir/$module_path" + +# Clean up +rm -fr "${drupal_dir:?}/$module_path" + # https://stackoverflow.com/a/15373763 -rsync --archive --compress . --filter=':- .gitignore' --exclude "$drupal_dir" --exclude .git "$drupal_dir/$module_path" +# rsync --archive --compress . --filter=':- .gitignore' --exclude "$drupal_dir" --exclude .git "$drupal_dir/$module_path" + +# The rsync command in not available in itkdev/php8.1-fpm + +git config --global --add safe.directory /app +# Copy module files into module path +for f in $(git ls-files); do + mkdir -p "$drupal_dir/$module_path/$(dirname "$f")" + cp "$f" "$drupal_dir/$module_path/$f" +done drupal_composer config minimum-stability dev @@ -32,4 +44,4 @@ drupal_composer config extra.merge-plugin.include "$module_path/composer.json" drupal_composer require --dev symfony/phpunit-bridge # Run PHPStan -(cd "$drupal_dir" && vendor/bin/phpstan --configuration="$module_path/phpstan.neon") +(cd "$drupal_dir/$module_path" && ../../../../vendor/bin/phpstan)