diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..96c12d6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# This file is copied from config/drupal-module/.editorconfig in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = LF +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,css,scss}] +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 + +[config/sync/**/*.{yml,yaml}] +indent_size = 2 + +[*.{php,install,module,theme}] +indent_size = 2 diff --git a/.env b/.env new file mode 100644 index 0000000..20ab01d --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +COMPOSE_PROJECT_NAME=os2forms_fordelingskomponent +ITKDEV_TEMPLATE=drupal-module diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..483da6e --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,29 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/changelog.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Changelog +### +### Checks that changelog has been updated + +name: Changelog + +on: + pull_request: + +jobs: + changelog: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Git fetch + run: git fetch + + - name: Check that changelog has been updated. + run: git diff --exit-code origin/${{ github.base_ref }} -- CHANGELOG.md && exit 1 || exit 0 diff --git a/.github/workflows/composer.yaml b/.github/workflows/composer.yaml new file mode 100644 index 0000000..b7f05e9 --- /dev/null +++ b/.github/workflows/composer.yaml @@ -0,0 +1,85 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/composer.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Composer +### +### Validates composer.json and checks that it's normalized. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [ergebnis/composer-normalize](https://github.com/ergebnis/composer-normalize) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev ergebnis/composer-normalize +### ``` +### +### Normalize `composer.json` by running +### +### ``` shell +### docker compose run --rm phpfpm composer normalize +### ``` + +name: Composer + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + composer-validate: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer validate --strict + + composer-normalized: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer init --no-interaction + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer config --no-plugins allow-plugins.mglaman/composer-drupal-lenient true + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer require mglaman/composer-drupal-lenient + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm rm composer.lenient.* + + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm composer normalize --dry-run + + composer-audit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer audit diff --git a/.github/workflows/markdown.yaml b/.github/workflows/markdown.yaml new file mode 100644 index 0000000..f8bcf09 --- /dev/null +++ b/.github/workflows/markdown.yaml @@ -0,0 +1,44 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/markdown.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Markdown +### +### Lints Markdown files (`**/*.md`) in the project. +### +### [markdownlint-cli configuration +### files](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration), +### `.markdownlint.jsonc` and `.markdownlintignore`, control what is actually +### linted and how. +### +### #### Assumptions +### +### 1. A docker compose service named `markdownlint` for running `markdownlint` +### (from +### [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli)) +### exists. + +name: Markdown + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + markdown-lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm markdownlint markdownlint '**/*.md' diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml new file mode 100644 index 0000000..c4e5269 --- /dev/null +++ b/.github/workflows/php.yaml @@ -0,0 +1,64 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/drupal-module/php.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Drupal module PHP +### +### Checks that PHP code adheres to the [Drupal coding +### standards](https://www.drupal.org/docs/develop/standards). +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [drupal/coder](https://www.drupal.org/project/coder) is a dev requirement +### in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev drupal/coder +### ``` +### +### Clean up and check code by running +### +### ``` shell +### docker compose run --rm phpfpm vendor/bin/phpcbf +### docker compose run --rm phpfpm vendor/bin/phpcs +### ``` +### +### > [!NOTE] +### > The template adds `.phpcs.xml.dist` as [a configuration file for +### > PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file) +### > and this makes it possible to override the actual configuration used in a +### > project by adding a more important configuration file, e.g. `.phpcs.xml`. + +name: PHP + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + coding-standards: + name: PHP - Check Coding Standards + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer init --no-interaction + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer config --no-plugins allow-plugins.mglaman/composer-drupal-lenient true + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer require mglaman/composer-drupal-lenient + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm rm composer.lenient.* + + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm vendor/bin/phpcs diff --git a/.github/workflows/project.yaml b/.github/workflows/project.yaml new file mode 100644 index 0000000..da5ae3d --- /dev/null +++ b/.github/workflows/project.yaml @@ -0,0 +1,30 @@ +name: Project + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + code-analysis: + name: PHP - Code analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - run: | + ./scripts/code-analysis + + rector: + name: PHP - Rector + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - run: | + ./scripts/rector diff --git a/.github/workflows/twig.yaml b/.github/workflows/twig.yaml new file mode 100644 index 0000000..1e050de --- /dev/null +++ b/.github/workflows/twig.yaml @@ -0,0 +1,54 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/twig.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Twig +### +### Validates Twig files +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [vincentlanglet/twig-cs-fixer](https://github.com/VincentLanglet/Twig-CS-Fixer) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev vincentlanglet/twig-cs-fixer +### ``` +### +### 3. A [Configuration +### file](https://github.com/VincentLanglet/Twig-CS-Fixer/blob/main/docs/configuration.md#configuration-file) +### in the root of the project defines which files to check and rules to use. + +name: Twig + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + twig-lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - run: | + docker network create frontend + + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer init --no-interaction + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer config --no-plugins allow-plugins.mglaman/composer-drupal-lenient true + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer require mglaman/composer-drupal-lenient + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm rm composer.lenient.* + + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm vendor/bin/twig-cs-fixer lint diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml new file mode 100644 index 0000000..8c60963 --- /dev/null +++ b/.github/workflows/yaml.yaml @@ -0,0 +1,41 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/yaml.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### YAML +### +### Validates YAML files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. +### +### #### Symfony YAML +### +### Symfony's YAML config files use 4 spaces for indentation and single quotes. +### Therefore we use a [Prettier configuration +### file](https://prettier.io/docs/configuration), `.prettierrc.yaml`, to make +### Prettier format YAML files in the `config/` folder like Symfony expects. + +name: YAML + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + yaml-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier '**/*.{yml,yaml}' --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722dce6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor +composer.lock +*.cache diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..0253096 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,22 @@ +// This file is copied from config/markdown/.markdownlint.jsonc in https://github.com/itk-dev/devops_itkdev-docker. +// Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +// markdownlint-cli configuration file (cf. https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration) +{ + "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 + }, + // https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections#creating-a-collapsed-section + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md + "no-inline-html": { + "allowed_elements": ["details", "summary"] + } +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..d143ace --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,12 @@ +# This file is copied from config/markdown/.markdownlintignore in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#ignoring-files +vendor/ +node_modules/ +LICENSE.md +# Drupal +web/*.md +web/core/ +web/libraries/ +web/*/contrib/ diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..083aa41 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,33 @@ + + + + + + The coding standard. + + . + + + vendor + rector.php + + + + + + + + + + + + + + + + + + + src/Settings/ + + diff --git a/.twig-cs-fixer.dist.php b/.twig-cs-fixer.dist.php new file mode 100644 index 0000000..0a0f295 --- /dev/null +++ b/.twig-cs-fixer.dist.php @@ -0,0 +1,16 @@ +in(__DIR__); +// … that are not ignored by VCS +$finder->ignoreVCSIgnored(true); + +$config = new TwigCsFixer\Config\Config(); +$config->setFinder($finder); + +return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ecc79af --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/OS2Forms/os2forms_fordelingskomponent diff --git a/README.md b/README.md index 0c82f6c..d0297d8 100644 --- a/README.md +++ b/README.md @@ -1 +1,141 @@ -# Fordelingskomponent for Drupal +# Fordelingskomponent for OS2Forms + +## Installation + +1. Create two keys (on `/admin/config/system/keys`) + + | Key | Type | Provider | + |-------------------------|----------------|----------| + | SF2900 Certificate | Certificate | File | + | SF2900 SFTP private key | Authentication | File | + + Note: The "SFTP private key" key must be passwordless[^1]. + + You can use `ssh-keygen` to remove the password from a certificate: + + ``` shell + cp cert/sf2900-sftp cert/sf2900-sftp-nopass + ssh-keygen -p -N "" -f cert/sf2900-sftp-nopass + ``` + +2. Create a queue (on `/admin/config/system/queues`) for Fordelingskomponent handler jobs. +3. Go to `/admin/os2forms_fordelingskomponent/settings` and configure the Fordelingskomponent module. + +[^1] It takes a very long time to read a key with a password (reference?) + +## Console commands + +``` shell +drush os2forms-fordelingskomponent:sftp:ls +``` + +--- + +``` shell name=key-create-sf2900_certificate +drush config:set --input-format=yaml key.key.sf2900_certificate '?' - <<'EOF' +langcode: da +status: true +dependencies: + module: + - os2web_key +id: sf2900_certificate +label: 'SF2900 Certificate' +description: '' +key_type: os2web_key_certificate +key_type_settings: + passphrase: … + input_format: pfx + output_format: pem +key_provider: file +key_provider_settings: + file_location: /app/cert/OS2Forms_FordelingUdvikling.p12 + strip_line_breaks: false +key_input: none +key_input_settings: { } +EOF +``` + +``` shell name=key-create-sf2900_sftp_private_key +drush config:set --input-format=yaml key.key.sf2900_sftp_private_key '?' - <<'EOF' +langcode: da +status: true +dependencies: { } +id: sf2900_sftp_private_key +label: 'SF2900 SFTP private key' +description: '' +key_type: authentication +key_type_settings: { } +key_provider: file +key_provider_settings: + file_location: /app/cert/OS2Forms_FordelingUdvikling-sftp-nopass + strip_line_breaks: false +key_input: none +key_input_settings: { } +``` + +## Development + +``` shell +task composer:install +``` + +### Composer install hacks + +``` shell name=composer-install-hack +# Create a temporary composer file to install https://github.com/mglaman/composer-drupal-lenient before the real install needs it. +docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer init --no-interaction +docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer config --no-plugins allow-plugins.mglaman/composer-drupal-lenient true +docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer require mglaman/composer-drupal-lenient +docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm rm composer.lenient.* + +# Now we can install what we actually need. +docker compose run --rm phpfpm composer install +``` + +### Kvittering + +[Kom godt i gang - Fordelingskomponenten, side +27](https://digitaliseringskataloget.dk/files/integration-files/150620210923/Kom%20godt%20i%20gang%20-%20Fordelingskomponenten.pdf#page=27) + +``` shell name=test-anvender-FordelingskvitteringModtag substitutions='«base url»: http://selvbetjening.local.itkdev.dk' +# curl --verbose --insecure --location «base url»/os2forms-fordelingskomponent/sf2900/2.4/FordelingskvitteringModtag --head + +curl --verbose --insecure --location «base url»/os2forms-fordelingskomponent/sf2900/2.4/FordelingskvitteringModtag --header 'content-type: application/soap+xml' --data @- <<'XML' + + + + + + ACCEPTERET + Forretning + + + 752bc93a-37cb-46db-9fb1-d5f4f7e3964e + d8101c99-0262-4a97-ac75-5685a6c6494a + + + + + +XML +``` + +## References + +* [Fælleskommunal Filudveksling](https://digitaliseringskataloget.dk/l%C3%B8sninger/filudveksling) + * [Vejledning til Serviceplatformens SFTP-service](https://docs.kombit.dk/latest/d312b273) + +## KP-formularer + +* + +## Debugging + +``` php +# settings.local.php +$settings['os2forms_fordelingskomponent']['log_level'] = \Drupal\Core\Logger\RfcLogLevel::DEBUG; +``` + +``` shell +drush sql:query --extra='--table' "SELECT (SELECT COUNT(*) FROM os2forms_fordelingskomponent_anvender_forsendelse) AS os2forms_fordelingskomponent_anvender_forsendelse, (SELECT COUNT(*) FROM os2forms_fordelingskomponent_anvender_kvittering) AS os2forms_fordelingskomponent_anvender_kvittering;" +``` diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..4a8d6a0 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,141 @@ +# https://taskfile.dev + +version: "3" + +tasks: + compose: + cmds: + - docker compose {{.TASK_ARGS}} {{.CLI_ARGS}} + internal: true + + composer: + desc: Run composer inside docker compose setup, e.g. `task {{.TASK}} -- install` + cmds: + - task: compose + vars: + TASK_ARGS: run --rm phpfpm composer {{.TASK_ARGS}} + + composer:install: + desc: Run composer inside docker compose setup, e.g. `task {{.TASK}} -- install` + cmds: + - rm -fr composer.lock vendor + - | + # Create a temporary composer file to install https://github.com/mglaman/composer-drupal-lenient before the real install needs it. + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer init --no-interaction + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer config --no-plugins allow-plugins.mglaman/composer-drupal-lenient true + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm composer require mglaman/composer-drupal-lenient + docker compose run --rm --env COMPOSER=composer.lenient.json phpfpm rm composer.lenient.* + + - task: composer + vars: + TASK_ARGS: install + + coding-standards:apply: + desc: "Apply coding standards" + cmds: + # - task: coding-standards:javascript:apply + - task: coding-standards:markdown:apply + - task: coding-standards:php:apply + # - task: coding-standards:styles:apply + - task: coding-standards:twig:apply + - task: coding-standards:yaml:apply + silent: true + + coding-standards:check: + desc: "Apply coding standards" + cmds: + # - task: coding-standards:javascript:check + - task: coding-standards:markdown:check + - task: coding-standards:php:check + # - task: coding-standards:styles:check + - task: coding-standards:twig:check + - task: coding-standards:yaml:check + silent: true + + coding-standards:javascript:apply: + desc: "Apply coding standards for javascript" + cmds: + # Cf. .github/workflows/javascript.yaml + - docker compose run --rm prettier 'web/themes/custom/**/js/**/*.js' --write + + coding-standards:javascript:check: + desc: "Apply coding standards for javascript" + cmds: + - task: coding-standards:javascript:apply + # Cf. .github/workflows/javascript.yaml + - docker compose run --rm prettier 'web/themes/custom/**/js/**/*.js' --check + + coding-standards:markdown:apply: + desc: "Apply coding standards for Markdown" + cmds: + # Cf. .github/workflows/markdown.yaml + - docker compose run --rm markdownlint markdownlint '**/*.md' --fix + + coding-standards:markdown:check: + desc: "Apply and check coding standards for Markdown" + cmds: + - task: coding-standards:markdown:apply + # Cf. .github/workflows/markdown.yaml + - docker compose run --rm markdownlint markdownlint '**/*.md' + + coding-standards:php:apply: + desc: "Apply coding standards for PHP" + cmds: + # Cf. .github/workflows/php.yaml + - docker compose run --rm phpfpm vendor/bin/phpcbf + silent: true + + coding-standards:php:check: + desc: "Apply and check coding standards for PHP" + cmds: + - task: coding-standards:php:apply + # Cf. .github/workflows/php.yaml + - docker compose run --rm phpfpm vendor/bin/phpcs + silent: true + + coding-standards:styles:apply: + desc: "Apply coding standards for styles" + cmds: + # Cf. .github/workflows/styles.yaml + - docker compose run --rm prettier 'web/themes/custom/**/css/**/*.{css,scss}' --write + + coding-standards:styles:check: + desc: "Apply coding standards for styles" + cmds: + - task: coding-standards:styles:apply + # Cf. .github/workflows/styles.yaml + - docker compose run --rm prettier 'web/themes/custom/**/css/**/*.{css,scss}' --check + + coding-standards:twig:apply: + desc: "Apply coding standards for Twig" + cmds: + - docker compose run --rm phpfpm vendor/bin/twig-cs-fixer fix + silent: true + + coding-standards:twig:check: + desc: "Apply and check coding standards for Twig" + cmds: + - task: coding-standards:twig:apply + - docker compose run --rm phpfpm vendor/bin/twig-cs-fixer lint + silent: true + + coding-standards:yaml:apply: + desc: "Apply coding standards for YAML" + cmds: + # Cf. .github/workflows/yaml.yaml + - docker compose run --rm prettier '**/*.{yml,yaml}' --write + + coding-standards:yaml:check: + desc: "Apply coding standards for YAML" + cmds: + - task: coding-standards:yaml:apply + # Cf. .github/workflows/yaml.yaml + - docker compose run --rm prettier '**/*.{yml,yaml}' --check + + test: + cmds: + - docker compose run --env PHP_XDEBUG_MODE --env PHP_XDEBUG_WITH_REQUEST --env PHP_IDE_CONFIG --rm phpfpm vendor/bin/phpunit {{.CLI_ARGS}} + + xdebug:test: + cmds: + - PHP_XDEBUG_MODE=debug PHP_XDEBUG_WITH_REQUEST=yes PHP_IDE_CONFIG=serverName=localhost docker compose run --env PHP_XDEBUG_MODE --env PHP_XDEBUG_WITH_REQUEST --env PHP_IDE_CONFIG --rm phpfpm vendor/bin/phpunit {{.CLI_ARGS}} diff --git a/compose.override.yaml b/compose.override.yaml new file mode 100644 index 0000000..e81a3bd --- /dev/null +++ b/compose.override.yaml @@ -0,0 +1,4 @@ +services: + phpfpm: + # OS2Forms is not yet ready for PHP 8.4 (!) + image: itkdev/php8.3-fpm:latest diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..77994c4 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,32 @@ +# itk-version: 3.2.4 + +services: + phpfpm: + image: itkdev/php8.4-fpm:latest + user: ${COMPOSE_USER:-deploy} + volumes: + - .:/app-os2forms_fordelingskomponent + working_dir: /app-os2forms_fordelingskomponent + environment: + # https://getcomposer.org/doc/03-cli.md#composer-no-security-blocking + # @see https://github.com/OS2Forms/os2forms/issues/245 + COMPOSER_NO_SECURITY_BLOCKING: 1 + + # Code checks tools + markdownlint: + image: itkdev/markdownlint + profiles: + - dev + volumes: + - ./:/md + + prettier: + # Prettier does not (yet, fcf. + # https://github.com/prettier/prettier/issues/15206) have an official + # docker image. + # https://hub.docker.com/r/jauderho/prettier is good candidate (cf. https://hub.docker.com/search?q=prettier&sort=updated_at&order=desc) + image: jauderho/prettier + profiles: + - dev + volumes: + - ./:/work diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..79acdaa --- /dev/null +++ b/composer.json @@ -0,0 +1,63 @@ +{ + "name": "os2forms/os2forms_fordelingskomponent", + "description": "Fordelingskomponent integration for OS2Forms", + "license": "EUPL-1.2", + "type": "drupal-module", + "authors": [ + { + "name": "Mikkel Ricky", + "email": "rimi@aarhus.dk" + } + ], + "require": { + "ext-dom": "*", + "ext-soap": "*", + "ext-xsl": "*", + "drupal/system_stream_wrapper": "^2.1", + "drush/drush": "^12 || ^13", + "itk-dev/serviceplatformen": "dev-feature/SF2900-Fordelingskomponenten as 1.7.9999", + "os2forms/os2forms": "^5.0" + }, + "require-dev": { + "drupal/coder": "^8.3", + "ergebnis/composer-normalize": "^2.50", + "mglaman/phpstan-drupal": "^2.0", + "phpstan/extension-installer": "^1.4", + "phpunit/phpunit": "^9.6", + "vincentlanglet/twig-cs-fixer": "^3.13" + }, + "repositories": [ + { + "type": "composer", + "url": "https://packages.drupal.org/8" + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "autoload-dev": { + "psr-4": { + "Drupal\\os2forms_fordelingskomponent\\": "src/", + "Drupal\\os2forms_fordelingskomponent\\Test\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "cweagans/composer-patches": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "ergebnis/composer-normalize": true, + "mglaman/composer-drupal-lenient": true, + "phpstan/extension-installer": true, + "simplesamlphp/composer-module-installer": false, + "simplesamlphp/composer-xmlprovider-installer": false, + "zaporylie/composer-drupal-optimizations": true + } + }, + "extra": { + "drupal-lenient": { + "allowed-list": [ + "drupal/coc_forms_auto_export", + "drupal/webform_node_element" + ] + } + } +} diff --git a/drush.services.yml b/drush.services.yml new file mode 100644 index 0000000..395470d --- /dev/null +++ b/drush.services.yml @@ -0,0 +1,28 @@ +services: + os2forms_fordelingskomponent.sftp.ls: + class: Drupal\os2forms_fordelingskomponent\Drush\Commands\SftpLsCommand + arguments: + - '@Drupal\os2forms_fordelingskomponent\Helper\FordelingskomponentHelper' + tags: + - { name: console.command } + + os2forms_fordelingskomponent.sftp.put: + class: Drupal\os2forms_fordelingskomponent\Drush\Commands\SftpPutCommand + arguments: + - '@Drupal\os2forms_fordelingskomponent\Helper\FordelingskomponentHelper' + tags: + - { name: console.command } + + os2forms_fordelingskomponent.sftp.get: + class: Drupal\os2forms_fordelingskomponent\Drush\Commands\SftpGetCommand + arguments: + - '@Drupal\os2forms_fordelingskomponent\Helper\FordelingskomponentHelper' + tags: + - { name: console.command } + + os2forms_fordelingskomponent.sned.journalnotat: + class: Drupal\os2forms_fordelingskomponent\Drush\Commands\SendJournalnotatCommand + arguments: + - '@Drupal\os2forms_fordelingskomponent\Helper\FordelingskomponentHelper' + tags: + - { name: console.command } diff --git a/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.info.yml b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.info.yml new file mode 100644 index 0000000..ed88670 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.info.yml @@ -0,0 +1,8 @@ +name: "os2forms_fordelingskomponent_debug" +type: module +description: "os2forms_fordelingskomponent_debug" +package: OS2Forms +core_version_requirement: ^10 || ^11 + +dependencies: + - os2forms_fordelingskomponent:os2forms_fordelingskomponent diff --git a/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.links.task.yml b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.links.task.yml new file mode 100644 index 0000000..034eede --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.links.task.yml @@ -0,0 +1,5 @@ +os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_forsendelse: + title: "Fordelingskomponentforsendelser" + route_name: os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_forsendelse + parent_id: entity.webform_submission.canonical + weight: 999 diff --git a/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.routing.yml b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.routing.yml new file mode 100644 index 0000000..9b3296e --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.routing.yml @@ -0,0 +1,45 @@ +# Drupal does not seem to support using / in route parameters (cf. https://drupal.stackexchange.com/questions/175758/slashes-in-single-route-parameter-or-other-ways-to-handle-a-menu-tail-with-dynam) +os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_sftp: + path: "/admin/os2forms-fordelingskomponent-debug/os2forms-fordelingskomponent-debug-sftp/{dir}" + defaults: &defaults + _title: "Fordelingskomponent SFTP" + _controller: '\Drupal\os2forms_fordelingskomponent_debug\Controller\Os2formsFordelingskomponentDebugSftpController' + dir: ~ + methods: &methods + - GET + requirements: &requirements + _permission: "administer site configuration" + +os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_sftp_filename: + path: "/admin/os2forms-fordelingskomponent-debug/os2forms-fordelingskomponent-debug-sftp/{dir}/{filename}" + defaults: *defaults + methods: *methods + requirements: *requirements + +os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_forsendelse: + path: "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/os2forms-fordelingskomponent-debug-forsendelse" + defaults: + _title: "Fordelingskomponentforsendelser" + _controller: '\Drupal\os2forms_fordelingskomponent_debug\Controller\Os2formsFordelingskomponentDebugForsendelseController' + methods: *methods + requirements: *requirements + options: + parameters: + webform: + type: "entity:webform" + webform_submission: + type: "entity:webform_submission" + +os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_kvittering: + path: "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/os2forms-fordelingskomponent-debug-forsendelse/kvittering/{anvender_transaktions_id}" + defaults: + _title: "Fordelingskomponentkvitteringer" + _controller: '\Drupal\os2forms_fordelingskomponent_debug\Controller\Os2formsFordelingskomponentDebugKvitteringController' + methods: *methods + requirements: *requirements + options: + parameters: + webform: + type: "entity:webform" + webform_submission: + type: "entity:webform_submission" diff --git a/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.services.yml b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.services.yml new file mode 100644 index 0000000..420ed1b --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/os2forms_fordelingskomponent_debug.services.yml @@ -0,0 +1,5 @@ +services: + _defaults: + autowire: true + + Drupal\os2forms_fordelingskomponent_debug\Hook\ThemeHooks: diff --git a/modules/os2forms_fordelingskomponent_debug/src/Controller/AbstractController.php b/modules/os2forms_fordelingskomponent_debug/src/Controller/AbstractController.php new file mode 100644 index 0000000..a4ba3f6 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/src/Controller/AbstractController.php @@ -0,0 +1,29 @@ +format(\DateTimeImmutable::ATOM) : NULL; + } + + /** + * Render YAML. + */ + protected function renderYaml(?\JsonSerializable $value): string { + return '
' . Yaml::dump(json_decode(json_encode($value), TRUE),
+        inline: PHP_INT_MAX) . '
'; + } + +} diff --git a/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugForsendelseController.php b/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugForsendelseController.php new file mode 100644 index 0000000..edaba65 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugForsendelseController.php @@ -0,0 +1,152 @@ +query->get('anvender_transaktions_id')) { + if ($item = $this->repository->loadByAnvenderTransaktionsId($anvenderTransaktionsId)) { + return $this->itemDetails($item); + } + + throw new NotFoundHttpException(); + } + $items = $this->repository->loadBySubmission($webform_submission); + + // https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21Table.php/class/Table/10 + $header = [ + 'anvenderTransaktionsId' => $this->t('anvenderTransaktionsId'), + 'distributionTransaktionsId' => $this->t('distributionTransaktionsId'), + 'receipts' => $this->t('Receipts'), + 'webform handlers' => $this->t('Webform handlers'), + 'createdAt' => $this->t('Created at'), + 'updatedAt' => $this->t('Updated at'), + 'deliveredAt' => $this->t('Delivered at'), + ]; + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'anvenderTransaktionsId' => [ + 'data' => [ + // https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21Link.php/class/Link/10 + '#title' => $item->anvenderTransaktionsId, + '#type' => 'link', + '#url' => Url::fromRoute('os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_forsendelse', [ + 'webform' => $webform->id(), + 'webform_submission' => $webform_submission->id(), + 'anvender_transaktions_id' => $item->anvenderTransaktionsId, + ]), + ], + ], + 'distributionTransaktionsId' => [ + 'data' => [ + '#markup' => $item->distributionTransaktionsId, + ], + ], + 'receipts' => [ + 'data' => [ + '#title' => $this->t('Receipts'), + '#type' => 'link', + '#url' => Url::fromRoute('os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_kvittering', [ + 'webform' => $webform->id(), + 'webform_submission' => $webform_submission->id(), + 'anvender_transaktions_id' => $item->anvenderTransaktionsId, + ]), + ], + ], + 'webform handlers' => [ + 'data' => [ + '#title' => $this->t('Webform handlers'), + '#type' => 'link', + '#url' => Url::fromRoute('entity.webform.handlers', [ + 'webform' => $item->webformId, + ]), + ], + ], + 'createdAt' => [ + 'data' => [ + '#markup' => $this->formatDatetime($item->createdAt), + ], + ], + 'updatedAt' => [ + 'data' => [ + '#markup' => $this->formatDatetime($item->updatedAt), + ], + ], + 'deliveredAt' => [ + 'data' => [ + '#markup' => $this->formatDatetime($item->deliveredAt), + ], + ], + ]; + } + + return [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No entries available.'), + ]; + } + + /** + * Build item details. + */ + private function itemDetails(AnvenderForsendelse $item) { + return [ + [ + '#type' => 'item', + '#title' => $this->t('anvenderTransaktionsId'), + '#markup' => $item->anvenderTransaktionsId, + ], + [ + '#type' => 'item', + '#title' => $this->t('distributionTransaktionsId'), + '#markup' => $item->distributionTransaktionsId, + ], + [ + '#type' => 'item', + '#title' => $this->t('Created at'), + '#markup' => $this->formatDatetime($item->createdAt), + ], + [ + '#type' => 'item', + '#title' => $this->t('Updated at'), + '#markup' => $this->formatDatetime($item->updatedAt), + ], + [ + '#type' => 'item', + '#title' => $this->t('Request'), + '#markup' => $this->renderYaml($item->request), + ], + [ + '#type' => 'item', + '#title' => $this->t('Response'), + '#markup' => $this->renderYaml($item->response), + ], + ]; + } + +} diff --git a/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugKvitteringController.php b/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugKvitteringController.php new file mode 100644 index 0000000..ebb0463 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugKvitteringController.php @@ -0,0 +1,134 @@ +query->get('id')) { + if ($item = $this->repository->load($id)) { + return $this->itemDetails($item); + } + + throw new NotFoundHttpException(); + } + + $items = $this->repository->loadByAnvenderTransaktionsId($anvender_transaktions_id); + + // https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21Table.php/class/Table/10 + $header = [ + 'id' => $this->t('ID'), + 'anvenderTransaktionsId' => $this->t('anvenderTransaktionsId'), + 'distributionTransaktionsId' => $this->t('distributionTransaktionsId'), + 'createdAt' => $this->t('Created at'), + 'updatedAt' => $this->t('Updated at'), + ]; + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'id' => [ + 'data' => [ + '#title' => $item->id, + '#type' => 'link', + '#url' => Url::fromRoute('os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_kvittering', + [ + 'webform' => $webform->id(), + 'webform_submission' => $webform_submission->id(), + 'anvender_transaktions_id' => $item->anvenderTransaktionsId, + 'id' => $item->id, + ]), + ], + ], + 'anvenderTransaktionsId' => [ + 'data' => [ + '#title' => $item->anvenderTransaktionsId, + '#type' => 'link', + '#url' => Url::fromRoute('os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_forsendelse', + [ + 'webform' => $webform->id(), + 'webform_submission' => $webform_submission->id(), + 'anvender_transaktions_id' => $item->anvenderTransaktionsId, + ]), + ], + ], + 'distributionTransaktionsId' => [ + 'data' => [ + '#markup' => $item->distributionTransaktionsId, + ], + ], + 'createdAt' => [ + 'data' => [ + '#markup' => $this->formatDatetime($item->createdAt), + ], + ], + 'updatedAt' => [ + 'data' => [ + '#markup' => $this->formatDatetime($item->updatedAt), + ], + ], + ]; + } + + return [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No entries available.'), + ]; + } + + /** + * Build item details. + */ + private function itemDetails(AnvenderKvittering $item) { + return [ + [ + '#type' => 'item', + '#title' => $this->t('anvenderTransaktionsId'), + '#markup' => $item->anvenderTransaktionsId, + ], + [ + '#type' => 'item', + '#title' => $this->t('distributionTransaktionsId'), + '#markup' => $item->distributionTransaktionsId, + ], + [ + '#type' => 'item', + '#title' => $this->t('Created at'), + '#markup' => $this->formatDatetime($item->createdAt), + ], + [ + '#type' => 'item', + '#title' => $this->t('Updated at'), + '#markup' => $this->formatDatetime($item->updatedAt), + ], + [ + '#type' => 'item', + '#title' => $this->t('Request'), + '#markup' => $this->renderYaml($item->request), + ], + ]; + } + +} diff --git a/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugSftpController.php b/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugSftpController.php new file mode 100644 index 0000000..83cb46e --- /dev/null +++ b/modules/os2forms_fordelingskomponent_debug/src/Controller/Os2formsFordelingskomponentDebugSftpController.php @@ -0,0 +1,90 @@ +helper->sf2900()->sftp(); + + if (NULL !== $filename && preg_match('/\.[^.]+$/', $filename)) { + $content = $sftp->getContents($filename, $dir); + + $contentType = match(pathinfo($filename, PATHINFO_EXTENSION)) { + 'sftpreceipt', 'trigger' => $this->mimeTypeGuesser->guessMimeType('name.xml'), + default => $this->mimeTypeGuesser->guessMimeType($filename) + }; + + return new Response($content, Response::HTTP_OK, [ + 'Content-Type' => $contentType, + ]); + } + else { + $files = $sftp->getFiles($dir ?? '.', recursive: TRUE, raw: TRUE); + + // Filter out . and .. + $files = array_filter($files, static fn (string $name) => !preg_match('/^\.+$/', $name), ARRAY_FILTER_USE_KEY); + + if ($filter = ($request->query->all()['filter'] ?? NULL)) { + $files = array_filter($files, static fn (string $filename) => array_all($filter, fn (string $value) => str_contains($filename, $value)), ARRAY_FILTER_USE_KEY); + } + + $header = [ + 'filepath' => $this->t('Path'), + 'atime' => $this->t('Last accessed at'), + 'mtime' => $this->t('Last modified at'), + ]; + $rows = []; + foreach ($files as $stat) { + $rows[] = [ + 'filepath' => [ + 'data' => [ + '#title' => '/' . ($dir ? $dir . '/' : '') . $stat->filename, + '#type' => 'link', + '#url' => $dir + ? Url::fromRoute('os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_sftp_filename', [ + 'dir' => $dir, + 'filename' => $stat->filename, + ]) + : Url::fromRoute('os2forms_fordelingskomponent_debug.os2forms_fordelingskomponent_debug_sftp', [ + 'dir' => $stat->filename, + ]), + ], + ], + 'atime' => $this->formatDatetime($stat->atime ?? NULL), + 'mtime' => $this->formatDatetime($stat->mtime ?? NULL), + ]; + } + + return [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No entries available.'), + ]; + } + } + +} diff --git a/modules/os2forms_fordelingskomponent_examples/README.md b/modules/os2forms_fordelingskomponent_examples/README.md new file mode 100644 index 0000000..5f29176 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/README.md @@ -0,0 +1,49 @@ +# OS2Forms Fordelingskomponent examples + +Example forms for OS2Forms Fordelingskomponent + +## Installation + +``` shell +drush pm:enable os2forms_fordelingskomponent_examples +``` + +Go to `/admin/structure/webform?category=Fordelingskomponent` to see the example forms. + +## Updating the examples + +All example webforms have IDs that match the regular expressions `/^os2forms_fdk_/` or `/^o2f_fdk_/`, i.e. if you want +to create a new example webform it must be have an ID like `os2forms_fdk_my_example`, say. + +Run + +``` shell +drush os2forms-fordelingskomponent:examples:export +``` + +to export all example webforms. + +Test the newly exported config by reinstalling the `os2forms_fordelingskomponent_examples` module + +``` shell +drush pm:uninstall os2forms_fordelingskomponent_examples +drush pm:install os2forms_fordelingskomponent_examples +``` + +Alternatively, import all examples: + +``` shell +drush config:import --partial --source module://os2forms_fordelingskomponent_examples/config/install +``` + +Or a single webform, e.g. + +``` shell +drush config:set --input-format=yaml webform.webform.os2forms_fdk_kp_anmoding '?' - < config/install/webform.webform.os2forms_fdk_kp_anmoding.yml +# drush config:get webform.webform.os2forms_fdk_kp_anmoding +``` + +## Testing + +Fill out an example webform with test data and pre-populate some fields: +`/webform/os2forms_fdk_kp_sp241/test?ansoeger_personnummer=1234567890&ansoeger_telefonnummer=12345678&fuldmagt_fuldmagthaverspersonnummer=0987654321` diff --git a/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_dokument.yml b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_dokument.yml new file mode 100644 index 0000000..1dc56a1 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_dokument.yml @@ -0,0 +1,304 @@ +langcode: da +status: open +dependencies: + module: + - os2forms + - os2forms_fordelingskomponent + - webform_encrypt + - webform_entity_print + - webform_revisions + enforced: + module: + - os2forms_fordelingskomponent_examples +third_party_settings: + webform_encrypt: + element: + message: + encrypt: true + encrypt_profile: webform + attachment: + encrypt: true + encrypt_profile: webform + os2forms: + os2forms_email_handler: + enabled: 0 + email_recipients: "" + os2forms_nemid: + session_type: "" + webform_type: "" + nemlogin_auto_redirect: 0 + os2forms_nemlogin_openid_connect: + authentication_settings: + user_claim: "" + element_key: "" + error_message: "" + os2forms_nemid_address_protection: + nemlogin_hide_form: os2forms_nemlogin_address_protection_default_behaviour + nemlogin_hide_message: "" + os2forms_rest_api: + allowed_users: null + os2forms_sync: + publish: 0 + os2forms_webform_submission_log: + emails: "" + webform_entity_print: + template: + header: "" + footer: "" + css: "" + os2form_header: "" + os2form_colophon: "" + os2form_footer: "" + export_types: + pdf: + enabled: true + link_text: "" + link_attributes: {} + word_docx: + enabled: false + link_text: "" + link_attributes: {} +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_fdk_dokument +title: 'OS2Forms Fordelingskomponent example with "dokument"' +description: "" +categories: {} +elements: |- + message: + '#type': textarea + '#title': Message + '#required': true + '#default_value': |- + [current-date:long] + + [random:hash:sha512] + attachment: + '#type': os2forms_attachment + '#title': attachment + '#export_type': pdf + '#digital_signature': 0 + '#excluded_elements': { } + '#exclude_empty': 0 + '#exclude_empty_checkbox': 0 +css: "" +javascript: "" +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: "" + ajax_effect: "" + ajax_speed: null + page: true + page_submit_path: "" + page_confirm_path: "" + page_theme_name: "" + form_title: both + form_submit_once: false + form_open_message: "" + form_close_message: "" + form_exception_message: "" + form_previous_submissions: true + form_confidential: false + form_confidential_message: "" + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: "" + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: "" + form_access_denied_message: "" + form_access_denied_attributes: {} + form_file_limit: "" + form_attributes: {} + form_method: "" + form_action: "" + share: false + share_node: false + share_theme_name: "" + share_title: true + share_page_body_attributes: {} + submission_label: "" + submission_exception_message: "" + submission_locked_message: "" + submission_log: false + submission_excluded_elements: {} + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: {} + submission_views_replace: {} + submission_user_columns: {} + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: "" + submission_access_denied_message: "" + submission_access_denied_attributes: {} + previous_submission_message: "" + previous_submissions_message: "" + autofill: false + autofill_message: "" + autofill_excluded_elements: {} + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: "" + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: "" + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: "" + wizard_prev_button_label: "" + wizard_next_button_label: "" + wizard_toggle: false + wizard_toggle_show_label: "" + wizard_toggle_hide_label: "" + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: "" + preview_title: "" + preview_message: "" + preview_attributes: {} + preview_excluded_elements: {} + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: "" + draft_loaded_message: "" + draft_pending_single_message: "" + draft_pending_multiple_message: "" + confirmation_type: message + confirmation_url: "" + confirmation_title: "" + confirmation_message: "" + confirmation_attributes: {} + confirmation_back: true + confirmation_back_label: "" + confirmation_back_attributes: {} + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: "" + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: "" + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: {} + permissions: {} + view_any: + roles: {} + users: {} + permissions: {} + update_any: + roles: {} + users: {} + permissions: {} + delete_any: + roles: {} + users: {} + permissions: {} + purge_any: + roles: {} + users: {} + permissions: {} + view_own: + roles: {} + users: {} + permissions: {} + update_own: + roles: {} + users: {} + permissions: {} + delete_own: + roles: {} + users: {} + permissions: {} + administer: + roles: {} + users: {} + permissions: {} + test: + roles: {} + users: {} + permissions: {} + configuration: + roles: {} + users: {} + permissions: {} +handlers: + os2forms_fordelingskomponent_sf2900: + id: os2forms_fordelingskomponent_sf2900 + handler_id: os2forms_fordelingskomponent_sf2900 + label: "Fordelingskomponent (sf2900)" + notes: "" + status: true + conditions: {} + weight: 0 + settings: + sf2900: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "What is “Brugervendt nøgle”?" + titel: "Your document" + beskrivelse: "This is a very important document" + distribution_type: JOURNALPOST + attachment_element: afsend_content_pdf + xml_template: "" + xsd_url: "" + journalpost_message: "Dette er et fordelingsobjekt fra [site:base-url]." + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: DOKUMENT + journalpost_message: "" + attachment_element: attachment + xml_template: "" + xsd_url: "" +variants: {} diff --git a/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_formular.yml b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_formular.yml new file mode 100644 index 0000000..dc71d33 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_formular.yml @@ -0,0 +1,305 @@ +langcode: da +status: open +dependencies: + module: + - os2forms + - os2forms_fordelingskomponent + - webform_encrypt + - webform_entity_print + - webform_revisions + enforced: + module: + - os2forms_fordelingskomponent_examples +third_party_settings: + webform_encrypt: + element: + message: + encrypt: true + encrypt_profile: webform + attachment: + encrypt: true + encrypt_profile: webform + os2forms: + os2forms_email_handler: + enabled: 0 + email_recipients: "" + os2forms_nemid: + session_type: "" + webform_type: "" + nemlogin_auto_redirect: 0 + os2forms_nemlogin_openid_connect: + authentication_settings: + user_claim: "" + element_key: "" + error_message: "" + os2forms_nemid_address_protection: + nemlogin_hide_form: os2forms_nemlogin_address_protection_default_behaviour + nemlogin_hide_message: "" + os2forms_rest_api: + allowed_users: null + os2forms_sync: + publish: 0 + os2forms_webform_submission_log: + emails: "" + webform_entity_print: + template: + header: "" + footer: "" + css: "" + os2form_header: "" + os2form_colophon: "" + os2form_footer: "" + export_types: + pdf: + enabled: true + link_text: "" + link_attributes: {} + word_docx: + enabled: false + link_text: "" + link_attributes: {} +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_fdk_formular +title: 'OS2Forms Fordelingskomponent example with "formular"' +description: "" +categories: {} +elements: |- + message: + '#type': textarea + '#title': Message + '#required': true + '#default_value': |- + [current-date:long] + + [random:hash:sha512] + attachment: + '#type': os2forms_attachment + '#title': attachment + '#export_type': pdf + '#digital_signature': 0 + '#excluded_elements': { } + '#exclude_empty': 0 + '#exclude_empty_checkbox': 0 +css: "" +javascript: "" +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: "" + ajax_effect: "" + ajax_speed: null + page: true + page_submit_path: "" + page_confirm_path: "" + page_theme_name: "" + form_title: both + form_submit_once: false + form_open_message: "" + form_close_message: "" + form_exception_message: "" + form_previous_submissions: true + form_confidential: false + form_confidential_message: "" + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: "" + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: "" + form_access_denied_message: "" + form_access_denied_attributes: {} + form_file_limit: "" + form_attributes: {} + form_method: "" + form_action: "" + share: false + share_node: false + share_theme_name: "" + share_title: true + share_page_body_attributes: {} + submission_label: "" + submission_exception_message: "" + submission_locked_message: "" + submission_log: false + submission_excluded_elements: {} + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: {} + submission_views_replace: {} + submission_user_columns: {} + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: "" + submission_access_denied_message: "" + submission_access_denied_attributes: {} + previous_submission_message: "" + previous_submissions_message: "" + autofill: false + autofill_message: "" + autofill_excluded_elements: {} + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: "" + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: "" + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: "" + wizard_prev_button_label: "" + wizard_next_button_label: "" + wizard_toggle: false + wizard_toggle_show_label: "" + wizard_toggle_hide_label: "" + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: "" + preview_title: "" + preview_message: "" + preview_attributes: {} + preview_excluded_elements: {} + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: "" + draft_loaded_message: "" + draft_pending_single_message: "" + draft_pending_multiple_message: "" + confirmation_type: message + confirmation_url: "" + confirmation_title: "" + confirmation_message: "" + confirmation_attributes: {} + confirmation_back: true + confirmation_back_label: "" + confirmation_back_attributes: {} + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: "" + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: "" + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: {} + permissions: {} + view_any: + roles: {} + users: {} + permissions: {} + update_any: + roles: {} + users: {} + permissions: {} + delete_any: + roles: {} + users: {} + permissions: {} + purge_any: + roles: {} + users: {} + permissions: {} + view_own: + roles: {} + users: {} + permissions: {} + update_own: + roles: {} + users: {} + permissions: {} + delete_own: + roles: {} + users: {} + permissions: {} + administer: + roles: {} + users: {} + permissions: {} + test: + roles: {} + users: {} + permissions: {} + configuration: + roles: {} + users: {} + permissions: {} +handlers: + os2forms_fordelingskomponent_sf2900: + id: os2forms_fordelingskomponent_sf2900 + handler_id: os2forms_fordelingskomponent_sf2900 + label: "Fordelingskomponent (sf2900)" + notes: "" + status: true + conditions: {} + weight: 0 + settings: + sf2900: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "What is “Brugervendt nøgle”?" + titel: "Your document" + beskrivelse: "This is a very important document" + distribution_type: JOURNALPOST + attachment_element: afsend_content_pdf + xml_template: "" + xsd_url: "" + journalpost_message: "Dette er et fordelingsobjekt fra [site:base-url]." + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: attachment + formular_type: Hest + xml_template: "\r\n\r\n {{ webform_submission.data.message }}\r\n" + xsd_url: "" +variants: {} diff --git a/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_journalpost.yml b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_journalpost.yml new file mode 100644 index 0000000..d7ad58d --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_journalpost.yml @@ -0,0 +1,294 @@ +langcode: da +status: open +dependencies: + module: + - os2forms + - os2forms_fordelingskomponent + - webform_encrypt + - webform_entity_print + - webform_revisions + enforced: + module: + - os2forms_fordelingskomponent_examples +third_party_settings: + webform_encrypt: + element: + message: + encrypt: true + encrypt_profile: webform + os2forms: + os2forms_email_handler: + enabled: 0 + email_recipients: "" + os2forms_nemid: + session_type: "" + webform_type: "" + nemlogin_auto_redirect: 0 + os2forms_nemlogin_openid_connect: + authentication_settings: + user_claim: "" + element_key: "" + error_message: "" + os2forms_nemid_address_protection: + nemlogin_hide_form: os2forms_nemlogin_address_protection_default_behaviour + nemlogin_hide_message: "" + os2forms_rest_api: + allowed_users: null + os2forms_sync: + publish: 0 + os2forms_webform_submission_log: + emails: "" + webform_entity_print: + template: + header: "" + footer: "" + css: "" + os2form_header: "" + os2form_colophon: "" + os2form_footer: "" + export_types: + pdf: + enabled: true + link_text: "" + link_attributes: {} + word_docx: + enabled: false + link_text: "" + link_attributes: {} +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_fdk_journalpost +title: 'OS2Forms Fordelingskomponent example with "journalpost"' +description: "" +categories: {} +elements: |- + message: + '#type': textarea + '#title': Message + '#required': true + '#default_value': |- + [current-date:long] + + [random:hash:sha512] +css: "" +javascript: "" +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: "" + ajax_effect: "" + ajax_speed: null + page: true + page_submit_path: "" + page_confirm_path: "" + page_theme_name: "" + form_title: both + form_submit_once: false + form_open_message: "" + form_close_message: "" + form_exception_message: "" + form_previous_submissions: true + form_confidential: false + form_confidential_message: "" + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: "" + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: "" + form_access_denied_message: "" + form_access_denied_attributes: {} + form_file_limit: "" + form_attributes: {} + form_method: "" + form_action: "" + share: false + share_node: false + share_theme_name: "" + share_title: true + share_page_body_attributes: {} + submission_label: "" + submission_exception_message: "" + submission_locked_message: "" + submission_log: false + submission_excluded_elements: {} + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: {} + submission_views_replace: {} + submission_user_columns: {} + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: "" + submission_access_denied_message: "" + submission_access_denied_attributes: {} + previous_submission_message: "" + previous_submissions_message: "" + autofill: false + autofill_message: "" + autofill_excluded_elements: {} + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: "" + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: "" + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: "" + wizard_prev_button_label: "" + wizard_next_button_label: "" + wizard_toggle: false + wizard_toggle_show_label: "" + wizard_toggle_hide_label: "" + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: "" + preview_title: "" + preview_message: "" + preview_attributes: {} + preview_excluded_elements: {} + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: "" + draft_loaded_message: "" + draft_pending_single_message: "" + draft_pending_multiple_message: "" + confirmation_type: message + confirmation_url: "" + confirmation_title: "" + confirmation_message: "" + confirmation_attributes: {} + confirmation_back: true + confirmation_back_label: "" + confirmation_back_attributes: {} + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: "" + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: "" + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: {} + permissions: {} + view_any: + roles: {} + users: {} + permissions: {} + update_any: + roles: {} + users: {} + permissions: {} + delete_any: + roles: {} + users: {} + permissions: {} + purge_any: + roles: {} + users: {} + permissions: {} + view_own: + roles: {} + users: {} + permissions: {} + update_own: + roles: {} + users: {} + permissions: {} + delete_own: + roles: {} + users: {} + permissions: {} + administer: + roles: {} + users: {} + permissions: {} + test: + roles: {} + users: {} + permissions: {} + configuration: + roles: {} + users: {} + permissions: {} +handlers: + os2forms_fordelingskomponent_sf2900: + id: os2forms_fordelingskomponent_sf2900 + handler_id: os2forms_fordelingskomponent_sf2900 + label: "Fordelingskomponent (sf2900)" + notes: "" + status: true + conditions: {} + weight: 0 + settings: + sf2900: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "What is “Brugervendt nøgle”?" + titel: "Your document" + beskrivelse: "This is a very important document" + distribution_type: JOURNALPOST + attachment_element: afsend_content_pdf + xml_template: "" + xsd_url: "" + journalpost_message: "Dette er et fordelingsobjekt fra [site:base-url]." + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: JOURNALPOST + journalpost_message: "Besked: [webform_submission:values:message]\r\n\r\n[current-date:html_datetime]" + attachment_element: "" + formular_type: "" + xml_template: "" + xsd_url: "" +variants: {} diff --git a/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_anmoding.yml b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_anmoding.yml new file mode 100644 index 0000000..9d413f2 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_anmoding.yml @@ -0,0 +1,494 @@ +langcode: da +status: open +dependencies: + module: + - os2forms + - os2forms_fordelingskomponent + - webform_encrypt + - webform_entity_print + - webform_revisions + enforced: + module: + - os2forms_fordelingskomponent_examples +third_party_settings: + webform_encrypt: + element: + ansoeger_oplysninger: + encrypt: true + encrypt_profile: webform + ansoeger: + encrypt: true + encrypt_profile: webform + fornavn: + encrypt: true + encrypt_profile: webform + mellemnavn: + encrypt: true + encrypt_profile: webform + efternavn: + encrypt: true + encrypt_profile: webform + personnummer: + encrypt: true + encrypt_profile: webform + telefonnummer: + encrypt: true + encrypt_profile: webform + sagstype: + encrypt: true + encrypt_profile: webform + almindeligt_helbredstillaeg: + encrypt: true + encrypt_profile: webform + afsend_content_pdf: + encrypt: true + encrypt_profile: webform + attachment: + encrypt: true + encrypt_profile: webform + dokumenter_overslag: + encrypt: true + encrypt_profile: webform + dokumenter_faktura: + encrypt: true + encrypt_profile: webform + dokumenter_bilag: + encrypt: true + encrypt_profile: webform + os2forms: + os2forms_email_handler: + enabled: 0 + email_recipients: "" + os2forms_nemid: + session_type: "" + webform_type: "" + nemlogin_auto_redirect: 0 + os2forms_nemlogin_openid_connect: + authentication_settings: + user_claim: "" + element_key: "" + error_message: "" + os2forms_nemid_address_protection: + nemlogin_hide_form: os2forms_nemlogin_address_protection_default_behaviour + nemlogin_hide_message: "" + os2forms_rest_api: + allowed_users: null + os2forms_sync: + publish: 0 + os2forms_webform_submission_log: + emails: "" + webform_entity_print: + template: + header: "" + footer: "" + css: "" + os2form_header: "" + os2form_colophon: "" + os2form_footer: "" + export_types: + pdf: + enabled: true + link_text: "" + link_attributes: {} + word_docx: + enabled: false + link_text: "" + link_attributes: {} +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_fdk_kp_anmoding +title: "OS2Forms Fordelingskomponent: Anmodning (KP)" +description: "" +categories: + - Example + - Fordelingskomponent +elements: |- + ansoeger_oplysninger: + '#type': fieldset + '#title': AnsoegerOplysninger + ansoeger: + '#type': fieldset + '#title': Ansøger + fornavn: + '#type': textfield + '#title': Fornavn + mellemnavn: + '#type': textfield + '#title': Mellemnavn + efternavn: + '#type': textfield + '#title': Efternavn + personnummer: + '#type': textfield + '#title': Personnummer + telefonnummer: + '#type': textfield + '#title': Telefonnummer + sagstype: + '#type': fieldset + '#title': Sagstype + almindeligt_helbredstillaeg: + '#type': select + '#title': 'Almindeligt helbredstillaeg' + '#options': + Medicin: Medicin + Tandbehandling: Tandbehandling + Fodbehandling: Fodbehandling + Fysioterapi: Fysioterapi + Kiropraktik: Kiropraktik + Psykologhjaelp: Psykologhjaelp + Hoereappartbehandling: Hoereapparatbehandling + afsend_content_pdf: + '#type': 'webform_entity_print_attachment:pdf' + '#title': 'Fordelingskomponent (PDF) hest' + '#display_on': view + '#filename': hat-og-briller.pdf + attachment: + '#type': os2forms_attachment + '#title': attachment + '#export_type': pdf + '#digital_signature': 0 + '#excluded_elements': { } + '#exclude_empty': 0 + '#exclude_empty_checkbox': 0 + dokumenter_overslag: + '#type': webform_document_file + '#title': Overslag + '#multiple': 3 + dokumenter_faktura: + '#type': webform_document_file + '#title': Faktura + '#multiple': 3 + dokumenter_bilag: + '#type': webform_document_file + '#title': Bilag + '#multiple': 3 +css: "" +javascript: "" +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: "" + ajax_effect: "" + ajax_speed: null + page: true + page_submit_path: "" + page_confirm_path: "" + page_theme_name: "" + form_title: both + form_submit_once: false + form_open_message: "" + form_close_message: "" + form_exception_message: "" + form_previous_submissions: true + form_confidential: false + form_confidential_message: "" + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: "" + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: "" + form_access_denied_message: "" + form_access_denied_attributes: {} + form_file_limit: "" + form_attributes: {} + form_method: "" + form_action: "" + share: false + share_node: false + share_theme_name: "" + share_title: true + share_page_body_attributes: {} + submission_label: "" + submission_exception_message: "" + submission_locked_message: "" + submission_log: false + submission_excluded_elements: {} + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: {} + submission_views_replace: {} + submission_user_columns: {} + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: "" + submission_access_denied_message: "" + submission_access_denied_attributes: {} + previous_submission_message: "" + previous_submissions_message: "" + autofill: false + autofill_message: "" + autofill_excluded_elements: {} + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: "" + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: "" + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: "" + wizard_prev_button_label: "" + wizard_next_button_label: "" + wizard_toggle: false + wizard_toggle_show_label: "" + wizard_toggle_hide_label: "" + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: "" + preview_title: "" + preview_message: "" + preview_attributes: {} + preview_excluded_elements: {} + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: "" + draft_loaded_message: "" + draft_pending_single_message: "" + draft_pending_multiple_message: "" + confirmation_type: message + confirmation_url: "" + confirmation_title: "" + confirmation_message: "" + confirmation_attributes: {} + confirmation_back: true + confirmation_back_label: "" + confirmation_back_attributes: {} + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: "" + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: "" + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: {} + permissions: {} + view_any: + roles: {} + users: {} + permissions: {} + update_any: + roles: {} + users: {} + permissions: {} + delete_any: + roles: {} + users: {} + permissions: {} + purge_any: + roles: {} + users: {} + permissions: {} + view_own: + roles: {} + users: {} + permissions: {} + update_own: + roles: {} + users: {} + permissions: {} + delete_own: + roles: {} + users: {} + permissions: {} + administer: + roles: {} + users: {} + permissions: {} + test: + roles: {} + users: {} + permissions: {} + configuration: + roles: {} + users: {} + permissions: {} +handlers: + fordelingskomponent_sf2900: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900 + label: Fordelingskomponent + notes: "" + status: true + conditions: {} + weight: -50 + settings: + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: attachment + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_authority: "55133018" + xml_template: | + + +
+ urn:oio:cvr-nr:{{ handler.settings.sender.senderId }} + {{ submission.completed.value|date("Y-m-d") }} + {{ handler.settings.distributionContext.kleEmne }} + {% for file in files.dokumenter_overslag|default([]) %} + + {{ file.sftp_filename }} + Overslag + + {% endfor %} + {% for file in files.dokumenter_faktura|default([]) %} + + {{ file.sftp_filename }} + Faktura + + {% endfor %} + {% for file in files.dokumenter_bilag|default([]) %} + + {{ file.sftp_filename }} + Bilag + + {% endfor %} +
+ + + {{ submission.data.fornavn }} + {{ submission.data.efternavn }} + urn:oio:cpr:0000000000 + + + + Medicin + + + {{ submission.data.ansoeger_fornavn }}{% if submission.data.ansoeger_mellemnavn|default(false) %} {{ submission.data.ansoeger_mellemnavn }}{% endif %} {{ submission.data.ansoeger_efternavn }} + {{ submission.completed.value|date("Y-m-d") }} + +
+ xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" + fordelingskomponent_sf2900_malformed_xml: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900_malformed_xml + label: "Fordelingskomponent (malformed XML)" + notes: "" + status: false + conditions: {} + weight: -49 + settings: + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: "" + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_authority: "55133018" + xml_template: "\r\n\r\n
\r\n urn:oio:cvr-nr:{{ handler.settings.sender.senderId }}\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n {{ handler.settings.distributionContext.kleEmne }}\r\n \r\n \r\n {{ submission.data.fornavn }}\r\n {{ submission.data.efternavn }}\r\n urn:oio:cpr:0000000000\r\n \r\n \r\n \r\n Medicin\r\n \r\n \r\n Underskrift0\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n \r\n\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" + fordelingskomponent_sf2900_invalid_twig: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900_invalid_twig + label: "Fordelingskomponent (invalid Twig)" + notes: "" + status: false + conditions: {} + weight: -48 + settings: + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: "" + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_authority: "55133018" + xml_template: "\r\n\r\n
\r\n urn:oio:cvr-nr:{{ handler.settings.sender.senderId\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n {{ handler.settings.distributionContext.kleEmne }}\r\n
\r\n \r\n \r\n {{ submission.data.fornavn }}\r\n {{ submission.data.efternavn }}\r\n urn:oio:cpr:0000000000\r\n \r\n \r\n \r\n Medicin\r\n \r\n \r\n Underskrift0\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n \r\n
\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" + fordelingskomponent_sf2900_invalid_xml: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900_invalid_xml + label: "Fordelingskomponent (invalid XML)" + notes: "" + status: false + conditions: {} + weight: -47 + settings: + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: "" + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_authority: "55133018" + xml_template: "\r\n\r\n \r\n \r\n {{ submission.data.fornavn }}\r\n {{ submission.data.efternavn }}\r\n urn:oio:cpr:0000000000\r\n \r\n \r\n \r\n Medicin\r\n \r\n \r\n Underskrift0\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n \r\n\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" +variants: {} diff --git a/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_anmoding_dev.yml b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_anmoding_dev.yml new file mode 100644 index 0000000..5b35337 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_anmoding_dev.yml @@ -0,0 +1,442 @@ +langcode: da +status: open +dependencies: + module: + - os2forms + - os2forms_fordelingskomponent + - webform_encrypt + - webform_entity_print + - webform_revisions + enforced: + module: + - os2forms_fordelingskomponent_examples +third_party_settings: + webform_encrypt: + element: + ansoeger_oplysninger: + encrypt: true + encrypt_profile: webform + ansoeger: + encrypt: true + encrypt_profile: webform + fornavn: + encrypt: true + encrypt_profile: webform + mellemnavn: + encrypt: true + encrypt_profile: webform + efternavn: + encrypt: true + encrypt_profile: webform + personnummer: + encrypt: true + encrypt_profile: webform + telefonnummer: + encrypt: true + encrypt_profile: webform + sagstype: + encrypt: true + encrypt_profile: webform + almindeligt_helbredstillaeg: + encrypt: true + encrypt_profile: webform + afsend_content_pdf: + encrypt: true + encrypt_profile: webform + attachment: + encrypt: true + encrypt_profile: webform + dokumenter_bilag: + encrypt: true + encrypt_profile: webform + os2forms: + os2forms_email_handler: + enabled: 0 + email_recipients: "" + os2forms_nemid: + session_type: "" + webform_type: "" + nemlogin_auto_redirect: 0 + os2forms_nemlogin_openid_connect: + authentication_settings: + user_claim: "" + element_key: "" + error_message: "" + os2forms_nemid_address_protection: + nemlogin_hide_form: os2forms_nemlogin_address_protection_default_behaviour + nemlogin_hide_message: "" + os2forms_rest_api: + allowed_users: null + os2forms_sync: + publish: 0 + os2forms_webform_submission_log: + emails: "" + webform_entity_print: + template: + header: "" + footer: "" + css: "" + os2form_header: "" + os2form_colophon: "" + os2form_footer: "" + export_types: + pdf: + enabled: true + link_text: "" + link_attributes: {} + word_docx: + enabled: false + link_text: "" + link_attributes: {} +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_fdk_kp_anmoding_dev +title: "OS2Forms Fordelingskomponent: Anmodning (KP) – dev" +description: "" +categories: + - Example + - Fordelingskomponent +elements: |- + ansoeger_oplysninger: + '#type': fieldset + '#title': AnsoegerOplysninger + ansoeger: + '#type': fieldset + '#title': Ansøger + fornavn: + '#type': textfield + '#title': Fornavn + mellemnavn: + '#type': textfield + '#title': Mellemnavn + efternavn: + '#type': textfield + '#title': Efternavn + personnummer: + '#type': textfield + '#title': Personnummer + telefonnummer: + '#type': textfield + '#title': Telefonnummer + sagstype: + '#type': fieldset + '#title': Sagstype + almindeligt_helbredstillaeg: + '#type': select + '#title': 'Almindeligt helbredstillaeg' + '#options': + Medicin: Medicin + Tandbehandling: Tandbehandling + Fodbehandling: Fodbehandling + Fysioterapi: Fysioterapi + Kiropraktik: Kiropraktik + Psykologhjaelp: Psykologhjaelp + Hoereappartbehandling: Hoereapparatbehandling + afsend_content_pdf: + '#type': 'webform_entity_print_attachment:pdf' + '#title': 'Fordelingskomponent (PDF) hest' + '#display_on': view + '#filename': hat-og-briller.pdf + attachment: + '#type': os2forms_attachment + '#title': attachment + '#export_type': pdf + '#digital_signature': 0 + '#excluded_elements': { } + '#exclude_empty': 0 + '#exclude_empty_checkbox': 0 + dokumenter_bilag: + '#type': webform_document_file + '#title': Bilag + '#multiple': 3 +css: "" +javascript: "" +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: "" + ajax_effect: "" + ajax_speed: null + page: true + page_submit_path: "" + page_confirm_path: "" + page_theme_name: "" + form_title: both + form_submit_once: false + form_open_message: "" + form_close_message: "" + form_exception_message: "" + form_previous_submissions: true + form_confidential: false + form_confidential_message: "" + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: "" + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: "" + form_access_denied_message: "" + form_access_denied_attributes: {} + form_file_limit: "" + form_attributes: {} + form_method: "" + form_action: "" + share: false + share_node: false + share_theme_name: "" + share_title: true + share_page_body_attributes: {} + submission_label: "" + submission_exception_message: "" + submission_locked_message: "" + submission_log: false + submission_excluded_elements: {} + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: {} + submission_views_replace: {} + submission_user_columns: {} + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: "" + submission_access_denied_message: "" + submission_access_denied_attributes: {} + previous_submission_message: "" + previous_submissions_message: "" + autofill: false + autofill_message: "" + autofill_excluded_elements: {} + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: "" + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: "" + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: "" + wizard_prev_button_label: "" + wizard_next_button_label: "" + wizard_toggle: false + wizard_toggle_show_label: "" + wizard_toggle_hide_label: "" + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: "" + preview_title: "" + preview_message: "" + preview_attributes: {} + preview_excluded_elements: {} + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: "" + draft_loaded_message: "" + draft_pending_single_message: "" + draft_pending_multiple_message: "" + confirmation_type: message + confirmation_url: "" + confirmation_title: "" + confirmation_message: "" + confirmation_attributes: {} + confirmation_back: true + confirmation_back_label: "" + confirmation_back_attributes: {} + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: "" + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: "" + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: {} + permissions: {} + view_any: + roles: {} + users: {} + permissions: {} + update_any: + roles: {} + users: {} + permissions: {} + delete_any: + roles: {} + users: {} + permissions: {} + purge_any: + roles: {} + users: {} + permissions: {} + view_own: + roles: {} + users: {} + permissions: {} + update_own: + roles: {} + users: {} + permissions: {} + delete_own: + roles: {} + users: {} + permissions: {} + administer: + roles: {} + users: {} + permissions: {} + test: + roles: {} + users: {} + permissions: {} + configuration: + roles: {} + users: {} + permissions: {} +handlers: + fordelingskomponent_sf2900: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900 + label: Fordelingskomponent + notes: "" + status: true + conditions: {} + weight: -50 + settings: + distribution_context: + routing_modtager_aktoer: "" + kle_emne: 32.03.13 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: attachment + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_it_system: 6b6afca0-e4fb-4c04-b2d4-bce99dedbcad + recipient_authority: "55133018" + xml_template: "\r\n\r\n
\r\n urn:oio:cvr-nr:{{ handler.settings.sender.senderId }}\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n {{ handler.settings.distributionContext.kleEmne }}\r\n {% for file in files.dokumenter_overslag|default([]) %}\r\n \r\n {{ file.sftp_filename }}\r\n Overslag\r\n \r\n {% endfor %}\r\n {% for file in files.dokumenter_faktura|default([]) %}\r\n \r\n {{ file.sftp_filename }}\r\n Faktura\r\n \r\n {% endfor %}\r\n {% for file in files.dokumenter_bilag|default([]) %}\r\n \r\n {{ file.sftp_filename }}\r\n Bilag\r\n \r\n {% endfor %}\r\n
\r\n \r\n \r\n {{ submission.data.fornavn }}\r\n {{ submission.data.efternavn }}\r\n urn:oio:cpr:0000000000\r\n \r\n \r\n \r\n Medicin\r\n \r\n \r\n Underskrift0\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n \r\n
\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" + fordelingskomponent_sf2900_malformed_xml: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900_malformed_xml + label: "Fordelingskomponent (malformed XML)" + notes: "" + status: false + conditions: {} + weight: -49 + settings: + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: "" + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_authority: "55133018" + xml_template: "\r\n\r\n
\r\n urn:oio:cvr-nr:{{ handler.settings.sender.senderId }}\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n {{ handler.settings.distributionContext.kleEmne }}\r\n \r\n \r\n {{ submission.data.fornavn }}\r\n {{ submission.data.efternavn }}\r\n urn:oio:cpr:0000000000\r\n \r\n \r\n \r\n Medicin\r\n \r\n \r\n Underskrift0\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n \r\n\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" + fordelingskomponent_sf2900_invalid_twig: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900_invalid_twig + label: "Fordelingskomponent (invalid Twig)" + notes: "" + status: false + conditions: {} + weight: -48 + settings: + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: "" + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_authority: "55133018" + xml_template: "\r\n\r\n
\r\n urn:oio:cvr-nr:{{ handler.settings.sender.senderId\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n {{ handler.settings.distributionContext.kleEmne }}\r\n
\r\n \r\n \r\n {{ submission.data.fornavn }}\r\n {{ submission.data.efternavn }}\r\n urn:oio:cpr:0000000000\r\n \r\n \r\n \r\n Medicin\r\n \r\n \r\n Underskrift0\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n \r\n
\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" + fordelingskomponent_sf2900_invalid_xml: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900_invalid_xml + label: "Fordelingskomponent (invalid XML)" + notes: "" + status: false + conditions: {} + weight: -47 + settings: + distribution_context: + kle_emne: 01.01.01 + handling_facet: "" + brugervendt_noegle: "Brugervendt nøgle" + titel: Titel + beskrivelse: Beskrivelse + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: "" + formular_type: BehandlingsudgifterRefusionsanmodningFormular_1 + files: + filspecifikation: BehandlingsudgifterRefusionsanmodningBilag_1 + recipient_authority: "55133018" + xml_template: "\r\n\r\n \r\n \r\n {{ submission.data.fornavn }}\r\n {{ submission.data.efternavn }}\r\n urn:oio:cpr:0000000000\r\n \r\n \r\n \r\n Medicin\r\n \r\n \r\n Underskrift0\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n \r\n\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/Anmodning.xsd" +variants: {} diff --git a/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_sp241.yml b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_sp241.yml new file mode 100644 index 0000000..2392d95 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/config/install/webform.webform.os2forms_fdk_kp_sp241.yml @@ -0,0 +1,429 @@ +langcode: da +status: open +dependencies: + module: + - os2forms + - os2forms_fordelingskomponent + - os2forms_permissions_by_term + - webform_encrypt + - webform_entity_print + - webform_revisions +third_party_settings: + os2forms: + os2forms_email_handler: + enabled: 0 + email_recipients: "" + os2forms_nemid: + session_type: "" + webform_type: personal + nemlogin_auto_redirect: 1 + os2forms_nemlogin_openid_connect: + authentication_settings: + user_claim: "" + element_key: "" + error_message: "" + os2forms_nemid_address_protection: + nemlogin_hide_form: os2forms_nemlogin_address_protection_default_behaviour + nemlogin_hide_message: "" + os2forms_rest_api: + allowed_users: null + os2forms_sync: + publish: 0 + os2forms_webform_submission_log: + emails: "" + webform_entity_print: + template: + header: "" + footer: "" + css: "" + os2form_header: "" + os2form_colophon: "" + os2form_footer: "" + export_types: + pdf: + enabled: true + link_text: "" + link_attributes: {} + word_docx: + enabled: false + link_text: "" + link_attributes: {} + webform_encrypt: + element: + ansoeger_fornavn: + encrypt: true + encrypt_profile: webform + ansoeger_mellemnavn: + encrypt: true + encrypt_profile: webform + ansoeger_efternavn: + encrypt: true + encrypt_profile: webform + ansoeger_personnummer: + encrypt: true + encrypt_profile: webform + ansoeger_telefonnummer: + encrypt: true + encrypt_profile: webform + sygeforsikring: + encrypt: true + encrypt_profile: webform + sygeforsikring_gruppe: + encrypt: true + encrypt_profile: webform + erklaering: + encrypt: true + encrypt_profile: webform + underskriftsoplysninger_underskriftsdato: + encrypt: true + encrypt_profile: webform + fuldmagt: + encrypt: true + encrypt_profile: webform + fuldmagt_fuldmagtdokumentnavn: + encrypt: true + encrypt_profile: webform + fuldmagt_fuldmagthaverspersonnummer: + encrypt: true + encrypt_profile: webform + dokumenter_overslag: + encrypt: true + encrypt_profile: webform + dokumenter_faktura: + encrypt: true + encrypt_profile: webform + dokumenter_bilag: + encrypt: true + encrypt_profile: webform + kvittering: + encrypt: true + encrypt_profile: webform +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_fdk_kp_sp241 +title: "SP241 (eksempel): Ansøgning om helbredstillæg" +description: "

/webform/os2forms_fdk_kp_sp241/test?ansoeger_personnummer=1234567890&ansoeger_telefonnummer=12345678

" +categories: + - Eksempel + - KP + - SP +elements: |- + ansoeger_fornavn: + '#type': textfield + '#title': Fornavn + '#required': true + '#prepopulate': true + ansoeger_mellemnavn: + '#type': textfield + '#title': Mellemnavn + '#prepopulate': true + ansoeger_efternavn: + '#type': textfield + '#title': Efternavn + '#required': true + '#prepopulate': true + ansoeger_personnummer: + '#type': textfield + '#title': Personnummer + '#required': true + '#pattern': '\d{10}' + '#prepopulate': true + ansoeger_telefonnummer: + '#type': textfield + '#title': Telefonnummer + '#pattern': \d+ + '#prepopulate': true + sygeforsikring: + '#type': checkbox + '#title': Sygeforsikring + '#prepopulate': true + sygeforsikring_gruppe: + '#type': select + '#title': Gruppe + '#options': + GRUPPE_1: 'Gruppe 1' + GRUPPE_2: 'Gruppe 2' + GRUPPE_5: 'Gruppe 5' + GRUPPE_E: 'Gruppe E' + GRUPPE_N: 'Gruppe N' + GRUPPE_S: 'Gruppe S' + GRUPPE_BASIS: Basis + '#prepopulate': true + '#states': + _visible: + ':input[name="sygeforsikring"]': + checked: true + required: + ':input[name="sygeforsikring"]': + checked: true + erklaering: + '#type': checkbox + '#title': Erklæring + '#required': true + '#prepopulate': true + underskriftsoplysninger_underskriftsdato: + '#type': date + '#title': Underskriftsdato + '#disabled': true + '#default_value': today + fuldmagt: + '#type': checkbox + '#title': Fuldmagt + '#prepopulate': true + fuldmagt_fuldmagtdokumentnavn: + '#type': textfield + '#title': Navn + '#prepopulate': true + '#states': + _visible: + ':input[name="fuldmagt"]': + checked: true + required: + ':input[name="fuldmagt"]': + checked: true + fuldmagt_fuldmagthaverspersonnummer: + '#type': textfield + '#title': Personnummer + '#pattern': '\d{10}' + '#prepopulate': true + '#states': + _visible: + ':input[name="fuldmagt"]': + checked: true + required: + ':input[name="fuldmagt"]': + checked: true + dokumenter_overslag: + '#type': webform_document_file + '#title': Overslag + dokumenter_faktura: + '#type': webform_document_file + '#title': Faktura + dokumenter_bilag: + '#type': webform_document_file + '#title': Bilag + kvittering: + '#type': os2forms_attachment + '#title': kvittering + '#export_type': pdf + '#digital_signature': 0 + '#excluded_elements': { } + '#exclude_empty': 0 + '#exclude_empty_checkbox': 0 +css: "" +javascript: "" +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: "" + ajax_effect: "" + ajax_speed: null + page: true + page_submit_path: "" + page_confirm_path: "" + page_theme_name: "" + form_title: both + form_submit_once: false + form_open_message: "" + form_close_message: "" + form_exception_message: "" + form_previous_submissions: true + form_confidential: false + form_confidential_message: "" + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: "" + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: "" + form_access_denied_message: "" + form_access_denied_attributes: {} + form_file_limit: "" + form_attributes: {} + form_method: "" + form_action: "" + share: false + share_node: false + share_theme_name: "" + share_title: true + share_page_body_attributes: {} + submission_label: "" + submission_exception_message: "" + submission_locked_message: "" + submission_log: false + submission_excluded_elements: {} + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: {} + submission_views_replace: {} + submission_user_columns: {} + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: "" + submission_access_denied_message: "" + submission_access_denied_attributes: {} + previous_submission_message: "" + previous_submissions_message: "" + autofill: false + autofill_message: "" + autofill_excluded_elements: {} + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: "" + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: "" + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: "" + wizard_prev_button_label: "" + wizard_next_button_label: "" + wizard_toggle: false + wizard_toggle_show_label: "" + wizard_toggle_hide_label: "" + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: "" + preview_title: "" + preview_message: "" + preview_attributes: {} + preview_excluded_elements: {} + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: "" + draft_loaded_message: "" + draft_pending_single_message: "" + draft_pending_multiple_message: "" + confirmation_type: page + confirmation_url: "" + confirmation_title: "" + confirmation_message: "" + confirmation_attributes: {} + confirmation_back: true + confirmation_back_label: "" + confirmation_back_attributes: {} + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: "" + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: "" + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: {} + permissions: {} + view_any: + roles: {} + users: {} + permissions: {} + update_any: + roles: {} + users: {} + permissions: {} + delete_any: + roles: {} + users: {} + permissions: {} + purge_any: + roles: {} + users: {} + permissions: {} + view_own: + roles: {} + users: {} + permissions: {} + update_own: + roles: {} + users: {} + permissions: {} + delete_own: + roles: {} + users: {} + permissions: {} + administer: + roles: {} + users: {} + permissions: {} + test: + roles: {} + users: {} + permissions: {} + configuration: + roles: {} + users: {} + permissions: {} +handlers: + fordelingskomponent_sf2900: + id: os2forms_fordelingskomponent_sf2900 + handler_id: fordelingskomponent_sf2900 + label: "Fordelingskomponent (sf2900)" + notes: "" + status: true + conditions: {} + weight: 0 + settings: + distribution_context: + routing_modtager_aktoer: "" + kle_emne: 32.03.12 + handling_facet: G01 + brugervendt_noegle: "@todo Skal skjules for “formular“" + titel: "@todo Titel SP241" + beskrivelse: "@todo Beskrivelse\r\n\r\nSe https://rimi-itk.github.io/digitaliseringskataloget.dk/digitaliseringskataloget.dk/sf1415/0.6/Integrationsbeskrivelse_SF1415.pdf#page=14 for at finde formulartype mm." + distribution_object: + distribution_type: FORMULAR + journalpost_message: "" + attachment_element: kvittering + formular_type: HelbredstillægAnsøgningFormular_1 + files: + filspecifikation: HelbredstillægAnsøgningBilag_1 + recipient_it_system_look_up: 1 + recipient_it_system: "" + recipient_authority: "55133018" + xml_template: "\r\n\r\n
\r\n urn:oio:cvr-nr:{{ handler.settings.sender.sender_id }}\r\n {{ submission.completed.value|date(\"Y-m-d\") }}\r\n {{ handler.settings.distribution_context.kle_emne }}\r\n {% for file in files.dokumenter_overslag|default([]) %}\r\n \r\n {{ file.sftp_filename }}\r\n Overslag\r\n \r\n {% endfor %}\r\n {% for file in files.dokumenter_faktura|default([]) %}\r\n \r\n {{ file.sftp_filename }}\r\n Faktura\r\n \r\n {% endfor %}\r\n {% for file in files.dokumenter_bilag|default([]) %}\r\n \r\n {{ file.sftp_filename }}\r\n Bilag\r\n \r\n {% endfor %}\r\n
\r\n \r\n \r\n {{ submission.data.ansoeger_fornavn }}\r\n {% if submission.data.ansoeger_mellemnavn|default(false) %}\r\n {{ submission.data.ansoeger_mellemnavn }}\r\n {% endif %}\r\n {{ submission.data.ansoeger_efternavn }}\r\n urn:oio:cpr:{{ submission.data.ansoeger_personnummer }}\r\n \r\n \r\n Accepteret\r\n \r\n {{ submission.data.ansoeger_fornavn }}{% if submission.data.ansoeger_mellemnavn|default(false) %} {{ submission.data.ansoeger_mellemnavn }}{% endif %} {{ submission.data.ansoeger_efternavn }}\r\n {{ submission.data.underskriftsoplysninger_underskriftsdato|date(\"Y-m-d\") }}\r\n \r\n
\r\n" + xsd_url: "module://os2forms_fordelingskomponent/resources/SP/SF2900_XSD/SP241.xsd" +variants: {} diff --git a/modules/os2forms_fordelingskomponent_examples/os2forms_fordelingskomponent_examples.info.yml b/modules/os2forms_fordelingskomponent_examples/os2forms_fordelingskomponent_examples.info.yml new file mode 100644 index 0000000..9a964bb --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/os2forms_fordelingskomponent_examples.info.yml @@ -0,0 +1,8 @@ +name: "OS2Forms Fordelingskomponent examples" +type: module +description: "Example forms for OS2Forms Fordelingskomponent." +package: "OS2Forms" + +core_version_requirement: ^9 || ^10 +dependencies: + - "os2forms_fordelingskomponent:os2forms_fordelingskomponent" diff --git a/modules/os2forms_fordelingskomponent_examples/resources/templates/os2_fdk_kp_anmoding.xml.twig b/modules/os2forms_fordelingskomponent_examples/resources/templates/os2_fdk_kp_anmoding.xml.twig new file mode 100644 index 0000000..1c585f5 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/resources/templates/os2_fdk_kp_anmoding.xml.twig @@ -0,0 +1,22 @@ + + +
+ urn:oio:cvr-nr:{{ handlers.settings.cvr }} + {{ webform_submission.complete|date(Y - m - d) }} + {{ handlers.settings.kle }} +
+ + + {{ webform_submission.values.fornavn }} + {{ webform_submission.values.efternavn }} + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + {{ webform_submission.complete|date('Y-m-d') }} + +
diff --git a/modules/os2forms_fordelingskomponent_examples/src/Drush/Commands/Os2formsFordelingskomponentExamplesCommands.php b/modules/os2forms_fordelingskomponent_examples/src/Drush/Commands/Os2formsFordelingskomponentExamplesCommands.php new file mode 100644 index 0000000..9768ac9 --- /dev/null +++ b/modules/os2forms_fordelingskomponent_examples/src/Drush/Commands/Os2formsFordelingskomponentExamplesCommands.php @@ -0,0 +1,88 @@ +io(); + + $io->info(array_merge([ + 'Exporting webforms with IDs matching one of', + ], self::CONFIG_NAME_PATTERNS) + ); + + $configFactory = $this->configManager->getConfigFactory(); + $configNames = array_values( + array_filter( + $configFactory->listAll(), + static fn (string $name): bool => !empty(array_filter( + array_map( + static fn (string $pattern) => preg_match($pattern, $name), + self::CONFIG_NAME_PATTERNS + ) + )), + ) + ); + + $moduleDir = $this->moduleExtensionList->getPath('os2forms_fordelingskomponent_examples'); + $targetDir = $moduleDir . '/config/install'; + + foreach ($configNames as $name) { + $targetName = $targetDir . '/' . $name . '.yml'; + + $io->section($name); + $config = $configFactory->getEditable($name); + foreach (static::$configKeysToClear as $key) { + $io->writeln(dt('Clearing key %key', ['%key' => $key])); + $config->clear($key); + } + // @todo (How) Can we use the config manager (or factory) to do this? + file_put_contents($targetName, Yaml::encode($config->get())); + $io->success(dt('Config written to %file', ['%file' => $targetName])); + } + } + +} diff --git a/os2forms_fordelingskomponent.info.yml b/os2forms_fordelingskomponent.info.yml new file mode 100644 index 0000000..599c6d7 --- /dev/null +++ b/os2forms_fordelingskomponent.info.yml @@ -0,0 +1,15 @@ +name: "Fordelingskomponent" +type: module +description: "Fordelingsskomponent integration for OS2Forms" +package: OS2Forms +core_version_requirement: ^10 || ^11 +dependencies: + - advancedqueue:advancedqueue + - "os2web_key:os2web_key" + - "os2web_audit:os2web_audit" + # Why don't we get this dependency implicitly from os2web_key? + - "key:key" + - "system_stream_wrapper:system_stream_wrapper" + - "webform:webform" + +configure: os2forms_fordelingskomponent.admin.settings diff --git a/os2forms_fordelingskomponent.install b/os2forms_fordelingskomponent.install new file mode 100644 index 0000000..50f01f9 --- /dev/null +++ b/os2forms_fordelingskomponent.install @@ -0,0 +1,17 @@ +schema(); +} diff --git a/os2forms_fordelingskomponent.links.menu.yml b/os2forms_fordelingskomponent.links.menu.yml new file mode 100644 index 0000000..bc29479 --- /dev/null +++ b/os2forms_fordelingskomponent.links.menu.yml @@ -0,0 +1,5 @@ +os2forms_fordelingskomponent.admin.settings: + title: OS2Forms Fordelingskomponent + description: Configure the OS2Forms Fordelingskomponent module + parent: system.admin_config_system + route_name: os2forms_fordelingskomponent.admin.settings diff --git a/os2forms_fordelingskomponent.module b/os2forms_fordelingskomponent.module new file mode 100644 index 0000000..5fc1293 --- /dev/null +++ b/os2forms_fordelingskomponent.module @@ -0,0 +1,15 @@ +theme($existing, $type, $theme, $path); +} diff --git a/os2forms_fordelingskomponent.routing.yml b/os2forms_fordelingskomponent.routing.yml new file mode 100644 index 0000000..98c171a --- /dev/null +++ b/os2forms_fordelingskomponent.routing.yml @@ -0,0 +1,68 @@ +os2forms_fordelingskomponent.admin.settings: + path: "/admin/os2forms_fordelingskomponent/settings" + defaults: + _title: "Fordelingskomponent settings" + _form: 'Drupal\os2forms_fordelingskomponent\Form\SettingsForm' + requirements: + _permission: "administer site configuration" + +os2forms_fordelingskomponent.routing_info: + path: "/admin/structure/webform/manage/{webform}/os2forms_fordelingskomponent/routing_info/{webform_handler}" + defaults: + _title: "Fordelingskomponent routing info" + _controller: '\Drupal\os2forms_fordelingskomponent\Controller\Os2formsFordelingskomponentRoutingInfoController' + options: + parameters: + webform: + type: "entity:webform" + requirements: + _entity_access: "webform.update" + +os2forms_fordelingskomponent.fordelingskomponent_distribution_object.preview: + path: "/admin/structure/webform/manage/{webform}/os2forms_fordelingskomponent/fordelingsobjekt/{webform_handler}/preview/{webform_submission}" + defaults: + _controller: '\Drupal\os2forms_fordelingskomponent\Controller\Os2formsFordelingskomponentDistributionObjectPreviewController' + _title: "Fordelingskomponent fordelingsobjekt preview" + options: + parameters: + webform: + type: "entity:webform" + webform_submission: + type: "entity:webform_submission" + requirements: + _entity_access: "webform_submission.view" + +os2forms_fordelingskomponent.distribution_object.index: + path: "/admin/structure/webform/manage/{webform}/os2forms_fordelingskomponent/{webform_handler}/fordelingsobject" + defaults: + _title: "Fordelingskomponent fordelingsobject index" + _controller: '\Drupal\os2forms_fordelingskomponent\Controller\Os2formsFordelingskomponentFordelingsobjectController::index' + options: + parameters: + webform: + type: "entity:webform" + requirements: + _entity_access: "webform.update" + +os2forms_fordelingskomponent.distribution_object.show: + path: "/admin/structure/webform/manage/{webform}/os2forms_fordelingskomponent/{webform_handler}/fordelingsobject/{anvender_transaktions_id}" + defaults: + _title: "Fordelingskomponent fordelingsobject show" + _controller: '\Drupal\os2forms_fordelingskomponent\Controller\Os2formsFordelingskomponentFordelingsobjectController::show' + options: + parameters: + webform: + type: "entity:webform" + requirements: + _entity_access: "webform.update" + +os2forms_fordelingskomponent.sf2900_2_4_FordelingskvitteringModtag: + path: "/os2forms-fordelingskomponent/sf2900/2.4/FordelingskvitteringModtag" + defaults: + _title: "SF2900 2.4 FordelingskvitteringModtag" + _controller: '\Drupal\os2forms_fordelingskomponent\Controller\Fordelingskomponent\FordelingskvitteringModtagController' + methods: [HEAD, POST] + requirements: + _permission: "access content" + options: + no_cache: "TRUE" diff --git a/os2forms_fordelingskomponent.services.yml b/os2forms_fordelingskomponent.services.yml new file mode 100644 index 0000000..4c20c62 --- /dev/null +++ b/os2forms_fordelingskomponent.services.yml @@ -0,0 +1,31 @@ +services: + _defaults: + autowire: true + + logger.channel.os2forms_fordelingskomponent: + parent: logger.channel_base + # @todo This does not work as expected. + # class: Drupal\os2forms_fordelingskomponent\Logger\Os2FormsFordelingskomponentLoggerChannel + arguments: ["os2forms_fordelingskomponent"] + + logger.channel.os2forms_fordelingskomponent_submission: + parent: logger.channel_base + arguments: ["webform_submission"] + + Drupal\os2forms_fordelingskomponent\Helper\FordelingskomponentHelper: + tags: + - { name: "event_subscriber" } + + Drupal\os2forms_fordelingskomponent\Helper\WebformHelperSF2900: + + Drupal\os2forms_fordelingskomponent\Helper\XmlHelper: + + Drupal\os2forms_fordelingskomponent\Hook\InstallHooks: + + Drupal\os2forms_fordelingskomponent\Hook\ThemeHooks: + + Drupal\os2forms_fordelingskomponent\Repository\AnvenderForsendelseRepository: + + Drupal\os2forms_fordelingskomponent\Repository\AnvenderKvitteringRepository: + + Drupal\os2forms_fordelingskomponent\Settings: diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..716b6c6 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,14 @@ +parameters: + paths: + - . + level: 5 + customRulesetUsed: true + reportUnmatchedIgnoredErrors: false + excludePaths: + - rector.php + # Ignore any vendor folder (https://phpstan.org/user-guide/ignoring-errors#excluding-whole-files) + - vendor (?) + + ignoreErrors: + - '#Call to method Drupal\\Core\\Entity\\Query\\QueryInterface::accessCheck\(\) will always evaluate to true.#' + - '#Call to method PHPUnit\\Framework\\Assert::assertTrue\(\) with true will always evaluate to true.#' diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..766495c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..44fe530 --- /dev/null +++ b/rector.php @@ -0,0 +1,21 @@ +withPaths([ + __DIR__ . '/src', + // __DIR__ . '/tests', + ]) + ->withSets([ + Drupal10SetList::DRUPAL_10, + ]) + ->withPhpSets(php83: TRUE) + ->withTypeCoverageLevel(0); diff --git a/resources/.gitignore b/resources/.gitignore new file mode 100644 index 0000000..859b8db --- /dev/null +++ b/resources/.gitignore @@ -0,0 +1,2 @@ +.task + diff --git a/resources/README.md b/resources/README.md new file mode 100644 index 0000000..53a8d4e --- /dev/null +++ b/resources/README.md @@ -0,0 +1,3 @@ +# Resources + +* [`ServiceContract-SFTP-20230926/`](./ServiceContract-SFTP-20230926) () diff --git a/resources/SP/SF2900_XSD/Anmodning.xsd b/resources/SP/SF2900_XSD/Anmodning.xsd new file mode 100644 index 0000000..05b0c90 --- /dev/null +++ b/resources/SP/SF2900_XSD/Anmodning.xsd @@ -0,0 +1,180 @@ + + + + + + Anmodning om refusion + + + + + + + + + + + + + + + + + - Overslag +- Faktura +- Bilag + + + + + + + + + + + + + + + Ved ansoegning med login + + + + + + + + + + + + + + Ved ansoegning uden login + + + + + + + + + + + + + + + + + + + + - Medicin +- Tandbehandling +- Fodbehandling +- Fysioterapi +- Kiropraktik +- Psykologhjaelp +- Hoereapparatbehandling + + + + + - Tandprotese +- Fodbehandling +- Briller + + + + + + + + + + + + + + + + + + + + + + + + + + + + Personnummer + + + + + + + + + Telefonnummer + + + + + + + + Angiver dokumenttype + + + + + + + + + + Angiver typen af almindeligt helbredstillaeg + + + + + + + + + + + + + + Angiver typen af udvidet helbredstillaeg + + + + + + + + + + Angiver generell max længde for strænger + + + + + + + + Myndighedstype + + + + + + diff --git a/resources/SP/SF2900_XSD/SP241.xsd b/resources/SP/SF2900_XSD/SP241.xsd new file mode 100644 index 0000000..5891e6f --- /dev/null +++ b/resources/SP/SF2900_XSD/SP241.xsd @@ -0,0 +1,180 @@ + + + + + + Ansoegning om helbredstillaeg + + + + + + + + + + + + + + + + - Overslag +- Faktura +- Bilag + + + + + + + + + + + + + + + Ved ansoegning med login + + + + + + + + + + + + + + Ved ansoegning uden login + + + + + + + + + + + + + + + + + Medlem af Sygeforsikringen »danmark« + + + + + + - Gruppe 1 +- Gruppe 2 +- Gruppe 5 +- Gruppe E +- Gruppe N +- Gruppe S +- Gruppe Basis + + + + + + + + + + + Kender rettigheder og pligter om behandling af personoplysninger + + + + + + + + + + + + + + + + + + + + + + + + Personnummer + + + + + + + + + Telefonnummer + + + + + + + + Tilkendegiver accept + + + + + + + + Angiver dokumenttype + + + + + + + + + + Angiver sygesikringstype + + + + + + + + + + + + + + Angiver generell max længde for strænger + + + + + + + + Myndighedstype + + + + + + diff --git a/resources/SP/SF2900_XSD/SP242.xsd b/resources/SP/SF2900_XSD/SP242.xsd new file mode 100644 index 0000000..fe79ec8 --- /dev/null +++ b/resources/SP/SF2900_XSD/SP242.xsd @@ -0,0 +1,206 @@ + + + + + + Ansoegning om udvidet helbredstillaeg + + + + + + + + + + + + + + + + + - Overslag +- Faktura +- Bilag + + + + + + + + + + + + + + + Ved ansoegning med login + + + + + + + + + + + + + + Ved ansoegning uden login + + + + + + + + + + + + + + + + + - Tandprotese +Tilbud (VT 701, VT 704 eller tilsvarende) fra tandtekniker/tandlaege afgives via digital loesning på virk.dk +- Briller +Specificeret overslag (SP 246 eller tilsvarende) fra optiker/oejenlæge skal medunderskrives og afgives via digital +loesning på virk.dk +- Fodbehandling +Ansoegning (SP 247 eller tilsvarende) skal medunderskrives af fodterapeut/fodplejer og afgives via +digital loesning på virk.dk + + + + + Medlem af Sygeforsikringen »danmark« + + + + + + - Gruppe 1 +- Gruppe 2 +- Gruppe 5 +- Gruppe E +- Gruppe N +- Gruppe S +- Gruppe Basis + + + + + + + + + + + + + + + + Samtykke til at kommunen kan tage kontakt til den konkrete behandler (tandlaege/tandtekniker/ +fodplejer/fodterapeut eller optiker) for eventuel afklaring + + + + + Kender rettigheder og pligter om behandling af personoplysninger + + + + + + + + + + + + + + + + Personnummer + + + + + + + + + Telefonnummer + + + + + + + + Tilkendegiver accept + + + + + + + + Angiver dokumenttype + + + + + + + + + + Angiver typen af udvidet helbredstillaeg + + + + + + + + + + Angiver sygesikringstype + + + + + + + + + + + + + + Angiver generell max længde for strænger + + + + + + + + Myndighedstype + + + + + + diff --git a/resources/SP/SF2900_XSD/SP246.xsd b/resources/SP/SF2900_XSD/SP246.xsd new file mode 100644 index 0000000..ad73b94 --- /dev/null +++ b/resources/SP/SF2900_XSD/SP246.xsd @@ -0,0 +1,275 @@ + + + + + + Specificeret overslag fra optiker/oejenlaege + + + + + + + + + + + + + + + + - Overslag + - Faktura + - Bilag + + + + + + + + + + + + + + + Har briller/kontaktlinser + + + + + + Brillens/kontaktlinsernes alder + + + + + + + + Anden/yderligere/mere specifik begrundelse + + + + + Specielle forhold, fx allergi, der goer sig gaeldende for valg af stel/glas (husk laegeerklaering) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + I alt + + + + + + + + Optiker/oejenlaege maa videregive overslaget til kommunen til brug for behandling af ansoegning om udvidet helbredstillaeg og kender rettigheder og pligter om behandling af personoplysninger + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Personnummer + + + + + + + + + Telefonnummer + + + + + + + + E-mail address + + + + + + + + Oeje specifikation + + + + + + + + + + + Tilkendegiver accept + + + + + + + + Angiver dokumenttype + + + + + + + + + + Angiver begrundelsen for ny brille + + + + + + + + + + + Myndighedstype + + + + + + + + Angiver generell max længde for strænger + + + + + + + + Angiver max længde for decimaler + + + + + + + + + Angiver max længde for decimaler + + + + + + + + + + Angiver max længde for positive heltal + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/SP/SF2900_XSD/SP501.xsd b/resources/SP/SF2900_XSD/SP501.xsd new file mode 100644 index 0000000..7d93503 --- /dev/null +++ b/resources/SP/SF2900_XSD/SP501.xsd @@ -0,0 +1,457 @@ + + + + + + Ansoegning om personligt tillaeg + + + + + + + + + + + + + + + + - Overslag + - Faktura + - Bilag + + + + + + + + + + + + + + + Ved ansoegning med login + + + + + + + + + + + + + + + Ved ansoegning uden login + + + + + + + + + + + + + + + + + Oplysninger fra ansoenging som samliv, formaal mm. + + + + + + + + + + + + + + + + + + Oplysninger om borger og evt aegtefaelles formue + + + + + + Oplysninger om borgers formue + + + + + + + + + + + + + + + + + + Oplysninger om aegtefaelle/samlevers formue + + + + + + + + + + + + + + + + + + + + + Oplysninger om borger og evt aegtefaelles indtaegter per maaned + + + + + + Oplysninger om borgers indtaegter per maaned + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Oplysninger om aegtefaelle/samlevers indtaegter per maaned + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Oplysninger om borger og evt aegtefaelles udgifter per maaned + + + + + + Oplysninger om borgers udgifter per maaned + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Oplysninger om aegtefaelle/samlevers udgifter per maaned + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Medlem af Sygeforsikringen »danmark« + + + + + + + + + + Kender rettigheder og pligter om behandling af personoplysninger + + + + + + + + + + + + + + + + + + + + + + + + Personnummer + + + + + + + + + Telefonnummer + + + + + + + + Tilkendegiver accept + + + + + + + + Angiver dokumenttype + + + + + + + + + + E-mail address + + + + + + + + Angiver civilstand + + + + + + + + + + + Angiver boform + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Angiver sygesikringstype + + + + + + + + + + + + + + Myndighedstype + + + + + + + + KLE format for personligt- og helbredstillæg + + + + + + diff --git a/resources/SP/examples/SF2900_XSD/Anmodning.xml b/resources/SP/examples/SF2900_XSD/Anmodning.xml new file mode 100644 index 0000000..0e0a92d --- /dev/null +++ b/resources/SP/examples/SF2900_XSD/Anmodning.xml @@ -0,0 +1,40 @@ + + +
+ urn:oio:cvr-nr:12345678 + 2026-04-24 + {{ handler.settings.distributionContext.kleEmne }} + + + {{ file.sftp_filename }} + Overslag + + + + + {{ file.sftp_filename }} + Faktura + + + + + {{ file.sftp_filename }} + Bilag + + +
+ + + {{ submission.data.fornavn }} + {{ submission.data.efternavn }} + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + 2026-04-24 + +
diff --git a/resources/SP/examples/SF2900_XSD/SP241-Fuldmagt.xml b/resources/SP/examples/SF2900_XSD/SP241-Fuldmagt.xml new file mode 100644 index 0000000..111f2da --- /dev/null +++ b/resources/SP/examples/SF2900_XSD/SP241-Fuldmagt.xml @@ -0,0 +1,45 @@ + + +
+ urn:oio:cvr-nr:12345678 + 2026-04-24 + {{ handler.settings.distributionContext.kleEmne }} + + + {{ file.sftp_filename }} + Overslag + + + + + {{ file.sftp_filename }} + Faktura + + + + + {{ file.sftp_filename }} + Bilag + + +
+ + + {{ submission.data.fornavn }} + {{ submission.data.efternavn }} + urn:oio:cpr:0000000000 + + + + GRUPPE_BASIS + + Accepteret + + Underskrift0 + 2026-04-26 + + + + urn:oio:cpr:1111111111 + +
diff --git a/resources/SP/examples/SF2900_XSD/SP241-Sygeforsikring.xml b/resources/SP/examples/SF2900_XSD/SP241-Sygeforsikring.xml new file mode 100644 index 0000000..c718c5e --- /dev/null +++ b/resources/SP/examples/SF2900_XSD/SP241-Sygeforsikring.xml @@ -0,0 +1,41 @@ + + +
+ urn:oio:cvr-nr:12345678 + 2026-04-24 + {{ handler.settings.distributionContext.kleEmne }} + + + {{ file.sftp_filename }} + Overslag + + + + + {{ file.sftp_filename }} + Faktura + + + + + {{ file.sftp_filename }} + Bilag + + +
+ + + {{ submission.data.fornavn }} + {{ submission.data.efternavn }} + urn:oio:cpr:0000000000 + + + + GRUPPE_BASIS + + Accepteret + + Underskrift0 + 2026-04-26 + +
diff --git a/resources/SP/examples/SF2900_XSD/SP241.xml b/resources/SP/examples/SF2900_XSD/SP241.xml new file mode 100644 index 0000000..63c9b56 --- /dev/null +++ b/resources/SP/examples/SF2900_XSD/SP241.xml @@ -0,0 +1,38 @@ + + +
+ urn:oio:cvr-nr:12345678 + 2026-04-24 + {{ handler.settings.distributionContext.kleEmne }} + + + {{ file.sftp_filename }} + Overslag + + + + + {{ file.sftp_filename }} + Faktura + + + + + {{ file.sftp_filename }} + Bilag + + +
+ + + {{ submission.data.fornavn }} + {{ submission.data.efternavn }} + urn:oio:cpr:0000000000 + + + Accepteret + + Underskrift0 + 2026-04-26 + +
diff --git a/resources/SP/twig/SF2900_XSD/Anmodning.twig.xml b/resources/SP/twig/SF2900_XSD/Anmodning.twig.xml new file mode 100644 index 0000000..5281bdb --- /dev/null +++ b/resources/SP/twig/SF2900_XSD/Anmodning.twig.xml @@ -0,0 +1,40 @@ + + +
+ urn:oio:cvr-nr:{{ handler.settings.sender.senderId }} + {{ submission.completed.value|date("Y-m-d") }} + {{ handler.settings.distributionContext.kleEmne }} + {% for file in files.dokumenter_overslag|default([]) %} + + {{ file.sftp_filename }} + Overslag + + {% endfor %} + {% for file in files.dokumenter_faktura|default([]) %} + + {{ file.sftp_filename }} + Faktura + + {% endfor %} + {% for file in files.dokumenter_bilag|default([]) %} + + {{ file.sftp_filename }} + Bilag + + {% endfor %} +
+ + + {{ submission.data.fornavn }} + {{ submission.data.efternavn }} + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + {{ submission.completed.value|date("Y-m-d") }} + +
diff --git a/resources/ServiceContract-SFTP-20230926/xsd/RouteParameters.xsd b/resources/ServiceContract-SFTP-20230926/xsd/RouteParameters.xsd new file mode 100644 index 0000000..434a3ab --- /dev/null +++ b/resources/ServiceContract-SFTP-20230926/xsd/RouteParameters.xsd @@ -0,0 +1,12 @@ + + + + + + + + + A rule must be valid for this date to be applicable for routing. + + + diff --git a/resources/ServiceContract-SFTP-20230926/xsd/SFTPDynamicRoutingInfo.xsd b/resources/ServiceContract-SFTP-20230926/xsd/SFTPDynamicRoutingInfo.xsd new file mode 100644 index 0000000..2296b5c --- /dev/null +++ b/resources/ServiceContract-SFTP-20230926/xsd/SFTPDynamicRoutingInfo.xsd @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Supplerende parameter der anvendes i routing, når routing er konfigureret til det. +Hvis der skal anvendes en RouteParameter, er det defineret specifikt for den enkelte integration. + + + + + + + + + + + + + diff --git a/resources/ServiceContract-SFTP-20230926/xsd/SFTPTypes.xsd b/resources/ServiceContract-SFTP-20230926/xsd/SFTPTypes.xsd new file mode 100644 index 0000000..6bdd911 --- /dev/null +++ b/resources/ServiceContract-SFTP-20230926/xsd/SFTPTypes.xsd @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/Taskfile.yml b/resources/Taskfile.yml new file mode 100644 index 0000000..fbe2a95 --- /dev/null +++ b/resources/Taskfile.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json + +version: "3" + +tasks: + xml:validate: + desc: Validate all XML files using their XSD file + sources: + - "SP/examples/**/*.xml" + cmds: + - for: sources + # https://taskfile.dev/docs/reference/templating#regular-expressions + # cmd: echo xmlstarlet val --err --xsd {{ regexReplaceAll "\\.xml" (.ITEM | replace "/examples/" "/" | "\\.xml" ".xsd" }} {{.ITEM}} + cmd: xmlstarlet val --err --xsd {{ regexReplaceAll "(-[^-]+)?\\.xml$" (.ITEM | replace "/examples/" "/") ".xsd" }} {{.ITEM}} + # Force the task to always run + # (cf. https://taskfile.dev/docs/guide#using-programmatic-checks-to-indicate-a-task-is-up-to-date) + status: + - false diff --git a/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceAnvenderV2.wsdl b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceAnvenderV2.wsdl new file mode 100644 index 0000000..0aece94 --- /dev/null +++ b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceAnvenderV2.wsdl @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceModtagerV2.wsdl b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceModtagerV2.wsdl new file mode 100644 index 0000000..820feea --- /dev/null +++ b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceModtagerV2.wsdl @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceMsgV2.xsd b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceMsgV2.xsd new file mode 100644 index 0000000..b650b15 --- /dev/null +++ b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceMsgV2.xsd @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceTypesV2.xsd b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceTypesV2.xsd new file mode 100644 index 0000000..480238e --- /dev/null +++ b/resources/sf2900/SF2900_EP_MS1-2/DistributionServiceTypesV2.xsd @@ -0,0 +1,550 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/.env b/scripts/.env new file mode 100644 index 0000000..4ce8811 --- /dev/null +++ b/scripts/.env @@ -0,0 +1,2 @@ +COMPOSE_PROJECT_NAME=drupal-module +MODULE_NAME=os2forms_fordelingskomponent diff --git a/scripts/base b/scripts/base new file mode 100644 index 0000000..def2c9d --- /dev/null +++ b/scripts/base @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -o errexit -o errtrace -o noclobber -o nounset -o pipefail +IFS=$'\n\t' + +execute_name=execute + +usage() { + (cat >&2 </dev/null); then + (cat >&2 <&2 <getHandler($handlerID); + } + catch (\Exception) { + $handler = NULL; + } + + if (!$handler instanceof WebformHandlerSF2900) { + throw new NotFoundHttpException(); + } + + return $handler; + } + +} diff --git a/src/Controller/Fordelingskomponent/AbstractSoapController.php b/src/Controller/Fordelingskomponent/AbstractSoapController.php new file mode 100644 index 0000000..673bc0c --- /dev/null +++ b/src/Controller/Fordelingskomponent/AbstractSoapController.php @@ -0,0 +1,79 @@ +wsdl, [ + 'classmap' => ClassMap::get(), + 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, + ]); + + $server->setObject($this); + + $response = new Response(); + $response->headers->set('content-type', 'application/soap+xml'); + + ob_start(); + try { + $server->handle(); + } + catch (\Throwable $e) { + $this->logger->error('SOAP server error: @message', [ + '@message' => $e->getMessage(), + 'exception' => $e, + ]); + $server->fault($e->getCode(), $e->getMessage()); + } + $response->setContent(ob_get_clean()); + + // Returning the response will result in + // + // nginx-1 | 2026/03/02 12:46:21 [error] 36#36: *13 upstream sent + // duplicate header line: "Content-Length: 302", previous value: + // "Content-Length: 302" while reading response header from upstream … + // . + $this->sendResponse($response); + + // Ensure nginx and proxy do not cache. + $response->headers->set('x-accel-buffering', 'no'); + // Ensure browser do not cache. + $response->headers->set('cache-control', 'no-cache, no-store, private'); + + return $response; + } + + /** + * Send response. + */ + private function sendResponse(Response $response): void { + foreach ($response->headers->all() as $name => $value) { + header($name . ': ' . reset($value)); + } + echo $response->getContent(); + exit(); + } + +} diff --git a/src/Controller/Fordelingskomponent/FordelingskvitteringModtagController.php b/src/Controller/Fordelingskomponent/FordelingskvitteringModtagController.php new file mode 100644 index 0000000..a45ff64 --- /dev/null +++ b/src/Controller/Fordelingskomponent/FordelingskvitteringModtagController.php @@ -0,0 +1,62 @@ +getForretningskvittering(); + // @todo Do something with the kvittering. + $context = $request->getDistributionContext(); + + $response = new FordelingskvitteringModtagAnvenderResponseType(); + + // We may receive multiple receipts. + $kvittering = new AnvenderKvittering( + id: NULL, + anvenderTransaktionsId: $context->getAnvenderTransaktionsID(), + distributionTransaktionsId: $context->getDistributionTransktionsID(), + request: $request, + response: $response, + ); + $this->kvitteringRepository->save($kvittering); + + return $response; + } + +} diff --git a/src/Controller/Os2formsFordelingskomponentDistributionObjectPreviewController.php b/src/Controller/Os2formsFordelingskomponentDistributionObjectPreviewController.php new file mode 100644 index 0000000..1e63080 --- /dev/null +++ b/src/Controller/Os2formsFordelingskomponentDistributionObjectPreviewController.php @@ -0,0 +1,102 @@ +getHandler($webform, $webform_handler); + $handlerSettings = $this->settings->getHandlerSettings($handler); + + // Get previous, self and next submission IDs. + $submissionIds = array_values($this->helper->loadSubmissionIds($webform)); + $currentSubmissionId = $webform_submission->id(); + $index = array_search($currentSubmissionId, $submissionIds); + if (FALSE === $index) { + throw new NotFoundHttpException(); + } + + $routeName = $request->attributes->get('_route'); + $links = array_map( + static fn($submissionId) => Url::fromRoute($routeName, [ + 'webform' => $webform->id(), + 'webform_handler' => $handler->getHandlerId(), + 'webform_submission' => $submissionId, + ]), + array_filter([ + 'prev' => $submissionIds[$index + 1] ?? NULL, + 'self' => $currentSubmissionId, + 'next' => $submissionIds[$index - 1] ?? NULL, + ]) + ); + + return [ + '#theme' => ThemeHooks::DISTRIBUTION_OBJECT_PREVIEW, + '#webform' => $webform, + '#submission' => $webform_submission, + '#handler' => $handler, + '#handler_settings' => $handlerSettings, + '#preview' => $this->renderPreview($handler, $handlerSettings, $webform_submission), + '#links' => $links, + ]; + } + + /** + * Render preview of distribution object. + */ + public function renderPreview(WebformHandlerSF2900 $handler, HandlerSettings $handlerSettings, WebformSubmissionInterface $submission): array { + $exceptions = []; + $warnings = []; + + $distributionObject = NULL; + $xml = []; + try { + $attachment = new Attachment('preview', Attachment::MIME_TYPE_PDF, 'preview.pdf'); + $distributionObject = $this->helper->buildDistributionObject($handlerSettings, $submission, $attachment); + } + catch (\Exception $exception) { + $exceptions[] = $exception; + } + + try { + $xml = $this->helper->renderXml($handlerSettings, $submission, validateXml: FALSE); + } + catch (\Throwable) { + // Silently ignore any errors. + } + + return [ + 'exceptions' => $exceptions, + 'warnings' => $warnings, + 'distribution_object' => $distributionObject, + 'xml' => $xml->withContextAsArray(), + ]; + } + +} diff --git a/src/Controller/Os2formsFordelingskomponentFordelingsobjectController.php b/src/Controller/Os2formsFordelingskomponentFordelingsobjectController.php new file mode 100644 index 0000000..41bb5d4 --- /dev/null +++ b/src/Controller/Os2formsFordelingskomponentFordelingsobjectController.php @@ -0,0 +1,50 @@ +getHandler($webform, $webform_handler); + $items = $this->repository->loadByWebformAndHandler($webform, $handler); + + return new JsonResponse($items); + } + + /** + * Builds the response. + */ + public function show(WebformInterface $webform, string $webform_handler, string $anvender_transaktions_id): Response { + $handler = $this->getHandler($webform, $webform_handler); + + $item = $this->repository->loadByAnvenderTransaktionsId($anvender_transaktions_id); + if (NULL === $item || $item->webformId !== $webform->id() || $item->webformHandlerId != $handler->getHandlerId()) { + throw new NotFoundHttpException(); + } + + return new JsonResponse($item); + } + +} diff --git a/src/Controller/Os2formsFordelingskomponentRoutingInfoController.php b/src/Controller/Os2formsFordelingskomponentRoutingInfoController.php new file mode 100644 index 0000000..1940065 --- /dev/null +++ b/src/Controller/Os2formsFordelingskomponentRoutingInfoController.php @@ -0,0 +1,44 @@ +getHandler($webform, $webform_handler); + $handlerSettings = $this->settings->getHandlerSettings($handler); + $info = $this->helper->getRoutingInfo($handlerSettings); + + return [ + '#theme' => ThemeHooks::ROUTING_INFO, + '#webform' => $webform, + '#handler' => $handler, + '#handler_settings' => $handlerSettings, + '#info' => $info, + '#return_url' => $webform->toUrl('handlers'), + ]; + } + +} diff --git a/src/Drush/Commands/AbstractCommand.php b/src/Drush/Commands/AbstractCommand.php new file mode 100644 index 0000000..ec48176 --- /dev/null +++ b/src/Drush/Commands/AbstractCommand.php @@ -0,0 +1,23 @@ +addArgument('routingMyndighed', InputArgument::REQUIRED, 'The routing myndighed') + ->addArgument('routingKleEmne', InputArgument::REQUIRED, 'The KLE-emne') + ->addArgument('routingHandlingFacet', InputArgument::OPTIONAL, 'The routingHandlingFacet'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); + + $routingMyndighed = $input->getArgument('routingMyndighed'); + $routingKLEEmne = $input->getArgument('routingKleEmne'); + $routingHandlingFacet = $input->getArgument('routingHandlingFacet'); + + $info = $this->helper->sf2900()->getModtagerList( + routingMyndighed: $routingMyndighed, + routingKLEEmne: $routingKLEEmne, + routingHandlingFacet: $routingHandlingFacet, + ); + $io->writeln(json_encode($info, JSON_PRETTY_PRINT)); + + return self::SUCCESS; + } + +} diff --git a/src/Drush/Commands/SendJournalnotatCommand.php b/src/Drush/Commands/SendJournalnotatCommand.php new file mode 100644 index 0000000..21ff359 --- /dev/null +++ b/src/Drush/Commands/SendJournalnotatCommand.php @@ -0,0 +1,38 @@ +warning('This command is a no-op'); + + return self::SUCCESS; + } + +} diff --git a/src/Drush/Commands/SftpGetCommand.php b/src/Drush/Commands/SftpGetCommand.php new file mode 100644 index 0000000..0fc8141 --- /dev/null +++ b/src/Drush/Commands/SftpGetCommand.php @@ -0,0 +1,56 @@ +addArgument('filename', InputArgument::REQUIRED, 'Name of file to get') + ->addArgument('dir', InputArgument::OPTIONAL, 'Directory', SftpHelper::OUTGOING_FOLDER); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); + $sftp = $this->helper->sf2900()->sftp(); + + $filename = $input->getArgument('filename'); + $dir = $input->getArgument('dir'); + + try { + $contents = $sftp->getContents($filename, $dir); + + echo $contents; + + return self::SUCCESS; + } + catch (\Exception $exception) { + $io->error($exception->getMessage()); + + return self::FAILURE; + } + } + +} diff --git a/src/Drush/Commands/SftpLsCommand.php b/src/Drush/Commands/SftpLsCommand.php new file mode 100644 index 0000000..967cfd7 --- /dev/null +++ b/src/Drush/Commands/SftpLsCommand.php @@ -0,0 +1,51 @@ +addArgument('dir', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'List of directory paths', [SftpHelper::OUTGOING_FOLDER]); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); + $sftp = $this->helper->sf2900()->sftp(); + $dirs = (array) $input->getArgument('dir'); + foreach ($dirs as $dir) { + // @todo getFiles does not complain when using an invalid directory … + $files = $sftp->getFiles($dir); + $files = array_filter($files, fn (string $file) => !preg_match('/^[.]+$/', $file)); + $io->section($dir); + foreach ($files as $file) { + $io->writeln($file); + } + } + + return self::SUCCESS; + } + +} diff --git a/src/Drush/Commands/SftpPutCommand.php b/src/Drush/Commands/SftpPutCommand.php new file mode 100644 index 0000000..b012f9b --- /dev/null +++ b/src/Drush/Commands/SftpPutCommand.php @@ -0,0 +1,54 @@ +addArgument('filename', InputArgument::REQUIRED, 'Name of file to put') + ->addArgument('dir', InputArgument::OPTIONAL, 'Target directory', [SftpHelper::OUTGOING_FOLDER]); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); + $sftp = $this->helper->sf2900()->sftp(); + + $filename = $input->getArgument('filename'); + + try { + $result = $sftp->putFile($filename); + $io->success(sprintf('File %s put on SFTP server as %s', $filename, $result)); + + return self::SUCCESS; + } + catch (\Exception $exception) { + $io->error($exception->getMessage()); + + return self::FAILURE; + } + } + +} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php new file mode 100644 index 0000000..4721e2d --- /dev/null +++ b/src/Exception/Exception.php @@ -0,0 +1,10 @@ +queueStorage = $entityTypeManager->getStorage('advancedqueue_queue'); + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'os2forms_fordelingskomponent_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return [Settings::CONFIG_NAME]; + } + + /** + * {@inheritdoc} + */ + #[\Override] + public function buildForm(array $form, FormStateInterface $form_state): array { + $form[SenderSettings::NAME] = [ + '#type' => 'fieldset', + '#title' => $this->t('Sender'), + '#tree' => TRUE, + ] + $this->buildFormSender(); + + $form[GeneralSettings::NAME] = [ + '#type' => 'fieldset', + '#title' => $this->t('General'), + '#tree' => TRUE, + ] + $this->buildFormGeneral(); + + return parent::buildForm($form, $form_state); + } + + /** + * Build form section "SF2900". + */ + private function buildFormSender(): array { + $settings = $this->settings->getSenderSettings(); + + $section[SenderSettings::SENDER_ID] = [ + '#type' => 'textfield', + '#title' => $this->t('Sender ID'), + '#required' => TRUE, + '#default_value' => $settings->senderId, + '#description' => $this->t('Sender ID (CVR).'), + ]; + + $section[SenderSettings::ROUTING_MYNDIGHED] = [ + '#type' => 'textfield', + '#title' => $this->t('Routing myndighed'), + // '#required' => TRUE, + '#default_value' => $settings->routingMyndighed, + '#description' => $this->t('Default routing myndighed (CVR). May be overwritten by handler settings.'), + ]; + + $section[SenderSettings::REGISTRERING_IT_SYSTEM] = [ + '#type' => 'textfield', + '#title' => $this->t('Registrering it-system'), + '#required' => TRUE, + '#default_value' => $settings->registreringItSystem, + ]; + + $section[SenderSettings::CERTIFICATE] = [ + '#type' => 'key_select', + '#key_filters' => [ + 'type' => 'os2web_key_certificate', + ], + '#title' => $this->t('Certificate'), + '#required' => TRUE, + '#default_value' => $settings->certificate, + '#description' => $this->t('Passwordless certificate.'), + ]; + + $section[SftpSettings::NAME] = [ + '#type' => 'fieldset', + '#title' => $this->t('SFTP'), + '#tree' => TRUE, + ]; + + $section[SftpSettings::NAME][SftpSettings::USERNAME] = [ + '#type' => 'textfield', + '#title' => $this->t('Username'), + '#required' => TRUE, + '#default_value' => $settings->sftp?->username, + '#description' => $this->t('SFTP username.'), + ]; + + $section[SftpSettings::NAME][SftpSettings::PRIVATE_KEY] = [ + '#type' => 'key_select', + '#title' => $this->t('Private key'), + '#required' => TRUE, + '#default_value' => $settings->sftp?->privateKey, + '#description' => $this->t('SFTP private key.'), + ]; + + return $section; + } + + /** + * Build form section "General". + */ + private function buildFormGeneral(): array { + $settings = $this->settings->getGeneralSettings(); + + $description = empty($settings->queue) + ? $this->t('Optional queue for fordelingskomponent jobs. If no queue is specified, all fordelingskomponent jobs are run immediately.') + : $this->t("Optional queue for fordelingskomponent jobs. If no queue is specified, all fordelingskomponent jobs are run immediately. The queue must be run via Drupal's cron or via drush advancedqueue:queue:process @queue (in a cron job).", + [ + '@queue' => $settings->queue, + ':queue_url' => '/admin/config/system/queues/jobs/' . urlencode((string) $settings->queue), + ]); + $section[GeneralSettings::QUEUE] = [ + '#type' => 'select', + '#title' => $this->t('Queue'), + '#options' => array_map( + static fn(EntityInterface $queue) => $queue->label(), + $this->queueStorage->loadMultiple() + ), + '#empty_option' => $this->t('No queue'), + '#default_value' => $settings->queue, + '#description' => $description, + ]; + + $section[GeneralSettings::TEST_MODE] = [ + '#type' => 'checkbox', + '#title' => $this->t('Test mode'), + '#default_value' => $settings->testMode, + ]; + + return $section; + } + + /** + * {@inheritdoc} + */ + #[\Override] + public function validateForm(array &$form, FormStateInterface $form_state): void { + $setError = static fn (string|array $path, TranslatableMarkup $message) => $form_state->setErrorByName(implode('][', (array) $path), $message); + + $value = $form_state->getValue(SenderSettings::NAME)[SenderSettings::SENDER_ID] ?? ''; + if (!FordelingskomponentHelper::isValidCvr($value)) { + $setError([SenderSettings::NAME, SenderSettings::SENDER_ID], $this->t('The sender ID is not a valid CVR.')); + } + + $value = $form_state->getValue(SenderSettings::NAME)[SenderSettings::ROUTING_MYNDIGHED] ?? ''; + if (!empty($value) && !FordelingskomponentHelper::isValidCvr($value)) { + $setError([SenderSettings::NAME, SenderSettings::ROUTING_MYNDIGHED], $this->t('The routing myndighed is not a valid CVR.')); + } + + $value = $form_state->getValue(SenderSettings::NAME)[SenderSettings::REGISTRERING_IT_SYSTEM] ?? ''; + if (!empty($value) && !FordelingskomponentHelper::isValidUuid($value)) { + $setError([SenderSettings::NAME, SenderSettings::REGISTRERING_IT_SYSTEM], $this->t('The registrering it system is not a valid UUID.')); + } + + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + #[\Override] + public function submitForm(array &$form, FormStateInterface $form_state): void { + $config = $this->config(Settings::CONFIG_NAME); + foreach ([ + SenderSettings::NAME, + GeneralSettings::NAME, + ] as $name) { + $config->set($name, $form_state->getValue($name)); + } + $config->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/src/Helper/FordelingskomponentHelper.php b/src/Helper/FordelingskomponentHelper.php new file mode 100644 index 0000000..6483277 --- /dev/null +++ b/src/Helper/FordelingskomponentHelper.php @@ -0,0 +1,808 @@ +fileStorage = $entityTypeManager->getStorage('file'); + } + + /** + * Get routing info. + */ + public function getRoutingInfo(HandlerSettings $handlerSettings): ?FordelingsmodtagerListResponseType { + return $this->sf2900()->getModtagerList( + routingMyndighed: (string) $handlerSettings->sender->routingMyndighed, + routingKLEEmne: (string) $handlerSettings->distributionContext->kleEmne, + routingHandlingFacet: $handlerSettings->distributionContext->handlingFacet, + ); + } + + /** + * Build distribution object. + */ + public function buildDistributionObject( + WebformSubmissionInterface $submission, + HandlerSettings $handlerSettings, + ?Attachment $attachment, + ): DistributionFormularType|DistributionDokumentType|DistributionJournalPostType { + $virkning = $this->buildVirkning($handlerSettings); + + $id = Serializer::createUuid(); + $fraTidsPunkt = new \DateTime(); + $brevDato = new \DateTime(); + + $type = $handlerSettings->distributionObject->distributionType; + $distributionObject = match ($type) { + DistributionObjectSettings::DISTRIBUTION_TYPE_JOURNALPOST => $this->buildDistributionJournalPostType( + id: $id, + fraTidsPunkt: $fraTidsPunkt, + virkning: $virkning, + handlerSettings: $handlerSettings, + ), + DistributionObjectSettings::DISTRIBUTION_TYPE_DOKUMENT => $this->buildDistributionDokumentType( + id: $id, + fraTidsPunkt: $fraTidsPunkt, + brevDato: $brevDato, + virkning: $virkning, + submission: $submission, + handlerSettings: $handlerSettings, + ), + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR => $this->buildDistributionFormularType( + id: $id, + submission: $submission, + handlerSettings: $handlerSettings, + attachment: $attachment, + ), + default => throw new Exception(sprintf('Invalid distribution type: %s', $type)), + }; + + return $distributionObject; + } + + /** + * Build distribution object for "Journalnotat". + */ + private function buildDistributionJournalPostType( + string $id, + \DateTimeInterface $fraTidsPunkt, + VirkningType $virkning, + HandlerSettings $handlerSettings, + ): DistributionJournalPostType { + // @todo DO we need a specific “titel" property? + $titel = $handlerSettings->distributionContext->titel; + $notat = $handlerSettings->distributionObject->journalpostMessage; + + return new DistributionJournalPostType( + iD: $id, + kLEEmneForslag: $handlerSettings->distributionContext->kleEmne, + registrering: new JournalPostRegistreringType( + fraTidsPunkt: SF2900::formatDateTime($fraTidsPunkt), + livscyklusKode: LivscyklusKodeType::VALUE_OPRETTET, + registreringItSystem: new UUID_URN($handlerSettings->sender->registreringItSystem), + relationListe: new JournalPostRelationsListeType([ + new JournalPostType( + virkning: $virkning, + rolle: JournalPostRolleType::VALUE_JOURNALPOST, + // @todo What is "indeks"? + indeks: '1', + journalnotatAttributter: new JournalNotatEgenskaberType( + notat: $notat, + titel: $titel, + ) + ), + ]) + ) + ); + } + + /** + * Build distribution object for "Dokument". + */ + private function buildDistributionDokumentType( + string $id, + \DateTimeInterface $fraTidsPunkt, + \DateTimeInterface $brevDato, + VirkningType $virkning, + WebformSubmissionInterface $submission, + HandlerSettings $handlerSettings, + ): DistributionDokumentType { + return new DistributionDokumentType( + iD: $id, + kLEEmneForslag: $handlerSettings->distributionContext->kleEmne, + registrering: new DokumentRegistreringType( + fraTidsPunkt: SF2900::formatDateTime($fraTidsPunkt), + livscyklusKode: LivscyklusKodeType::VALUE_OPRETTET, + registreringItSystem: new UUID_URN($handlerSettings->sender->registreringItSystem), + relationListe: new RelationsListe( + variantListe: new VariantListeType([ + new VariantType( + // If we don't clone the “virking", the XML serializer adds an + // ID and references which SF2900 does not handle. + virkning: $this->cloneVirkning($virkning), + rolle: VariantRolleType::VALUE_VARIANT, + indeks: '1', + variantAttributter: new VariantAttributterType( + // @todo What to use here? + variantType: Attachment::FORMAT_NAME_PDF, + ), + delAttributter: new DelAttributterType( + // @todo What to use here? + delTekst: 'Hele dokumentet', + ), + ), + ]), + ), + tilstandsListe: [ + new TilstandListeType( + tilstand: [ + new TilstandType( + // @todo Hvad er fremdrift? + fremdrift: FremdriftType::VALUE_ENDELIGT, + virkning: $this->cloneVirkning($virkning), + ), + ] + ), + ], + attributListe: new AttributterListeType([ + new AttributterType( + brugervendtNoegleTekst: $handlerSettings->distributionContext->brugervendtNoegle, + titelTekst: $handlerSettings->distributionContext->titel, + beskrivelseTekst: $handlerSettings->distributionContext->beskrivelse, + // @todo What to use here? + dokumenttype: DokumenttypeType::VALUE_ANDEN, + retning: RetningType::VALUE_UDGAAENDE, + brevdato: SF2900::formatDate($brevDato), + virkning: $this->cloneVirkning($virkning), + ), + ]), + // importTidspunkt: null, + // brugerRef: null,. + ), + handlingFacetForslag: $handlerSettings->distributionContext->handlingFacet + ); + } + + /** + * Build distribution object for "Formular". + */ + private function buildDistributionFormularType( + string $id, + WebformSubmissionInterface $submission, + HandlerSettings $handlerSettings, + Attachment $attachment, + ): DistributionFormular { + $files = $this->buildFileGroups($handlerSettings, $submission); + $renderResult = $this->renderXml($handlerSettings, $submission, $files); + if ($renderResult->exception) { + throw $renderResult->exception; + } + + $xml = (string) $renderResult->rendered; + $xsdUrl = $handlerSettings->distributionObject->xsdUrl; + + $this->xmlHelper->validateXml($xml); + if (!empty($xsdUrl)) { + $this->xmlHelper->validateXml($xml, $xsdUrl, loadXsdContent: TRUE); + } + + // The attachment must be a PDF. + $titelTekst = basename($attachment->filename); + $formatNavn = pathinfo($attachment->filename, PATHINFO_EXTENSION); + $formularIndhold = base64_encode($attachment->contents); + + // The XML will be embedded in an SOAP:Envelope element, so we have to + // make sure that the XML declaration is not included when embedding. + // Passing a DOMDocument to FormularXMLType takes care of this. + $dom = new \DOMDocument(); + $dom->loadXML($xml); + $formularXML = new FormularXMLType($dom); + + $meddelelse = new MeddelelseType( + formularType: $handlerSettings->distributionObject->formularType, + formular: new FormularType( + titelTekst: $titelTekst, + formatNavn: $formatNavn, + formularIndhold: $formularIndhold, + formularXML: $formularXML, + ), + ); + + return (new DistributionFormular( + iD: $id, + kLEEmneForslag: $handlerSettings->distributionContext->kleEmne, + meddelelse: $meddelelse, + handlingFacetForslag: $handlerSettings->distributionContext->handlingFacet, + )) + ->setFileGroups($files); + } + + /** + * Render XML. + */ + public function renderXml( + HandlerSettings $handlerSettings, + WebformSubmissionInterface $submission, + ?array $files, + bool $validateXml = TRUE, + ): XmlRenderResult { + $template = $handlerSettings->distributionObject->xmlTemplate; + if (empty(trim((string) $template))) { + throw new RuntimeException('Missing XML template'); + } + + $context = $this->xmlHelper->getRenderContext($handlerSettings, $submission, $files); + + $rendered = NULL; + $exception = NULL; + try { + $rendered = $this->xmlHelper->render($template, $context, validateXml: $validateXml); + } + catch (\Exception $e) { + $exception = $e; + } + + return new XmlRenderResult( + template: $template, + context: $context, + rendered: $rendered, + exception: $exception, + ); + } + + private const FILE_ELEMENT_TYPES = [ + 'managed_file', + 'webform_document_file', + 'webform_image_file', + ]; + + /** + * Build files for a distribution object. + * + * @return array> + * The file groups. + */ + public function buildFileGroups(HandlerSettings $handlerSettings, WebformSubmissionInterface $submission): array { + $groups = []; + $elements = $submission->getWebform()->getElementsDecodedAndFlattened(); + $fileElements = array_filter($elements, + static fn(array $element) => in_array($element['#type'] ?? NULL, self::FILE_ELEMENT_TYPES)); + foreach ($fileElements as $type => $_) { + $values = $submission->getData()[$type] ?? NULL; + if ($values) { + /** @var \Drupal\file\FileInterface[] $files */ + $files = $this->fileStorage->loadMultiple((array) $values); + foreach ($files as $file) { + $groups[$type][] = [ + 'sftp_filename' => $this->getSftpFilename($handlerSettings, $submission, $file->getFilename()), + 'file' => $file, + ]; + } + } + } + + return $groups; + } + + /** + * Get SFTP filename. + */ + private function getSftpFilename( + HandlerSettings $handlerSettings, + WebformSubmissionInterface $submission, + string $filename, + ): string { + return implode('_', [ + uniqid('os2forms_fordelingskomponent_'), + $handlerSettings->handlerId, + $submission->uuid(), + $filename, + ]); + } + + /** + * Send dokument. + * + * @return array + * [The response, The kombi post message]. + * + * @phpstan-return array + */ + public function uploadFiles( + DistributionFormularType|DistributionDokumentType|DistributionJournalPostType $distributionObject, + HandlerSettings $handlerSettings, + WebformSubmissionInterface $submission, + ): array { + $sf2900 = $this->sf2900(); + $transactionId = Serializer::createUuid(); + + $triggerObjects = []; + if ($distributionObject instanceof DistributionFormular) { + $files = $distributionObject->getFileGroups(); + $sftp = $sf2900->sftp(); + $recipientItSystem = NULL; + if ($handlerSettings->distributionObject->files->recipientItSystemLookUp) { + $routingInfo = $this->getRoutingInfo($handlerSettings); + $system = $routingInfo->getSystemer()->getSystem(); + if (1 !== count($system)) { + throw new \RuntimeException('Cannot find single recipient system'); + } + $recipientItSystem = $system[0]->getSystemUUID(); + } + foreach ($files as $items) { + foreach ($items as $item) { + /** @var \Drupal\file\Entity\File $file */ + $file = $item['file']; + $sftp->putFile($file->getFileUri(), $file->getFilename(), $item['sftp_filename']); + $triggerObject = $this->buildTriggerFile($file, $item['sftp_filename'], $handlerSettings, $submission, $transactionId, + recipientItSystem: $recipientItSystem); + $sftp->putContents($triggerObject, $item['sftp_filename'], $item['sftp_filename'] . '.trigger'); + $triggerObjects[] = $triggerObject; + } + } + } + + return $triggerObjects; + } + + /** + * Check if files are delivered. + * + * @todo Report back if delivery has failed, i.e. if receipts exist but + * report errors. + */ + public function checkFilesDelivered( + array $triggerObjects, + WebformSubmissionInterface $submission, + ): bool { + $context = [ + 'webform_submission' => $submission, + ]; + foreach ($triggerObjects as $triggerObject) { + try { + $sxe = new \SimpleXMLElement($triggerObject); + $filename = (string) $sxe->xpath('//FileDescriptor/FileName')[0]; + if (empty($filename)) { + throw new \RuntimeException('Cannot get file name'); + } + $this->debug('Checking file %filename', $context + [ + '%filename' => $filename, + ]); + + $receipt = $this->sf2900()->sftp()->getContents($filename . '.sftpreceipt', SftpHelper::INCOMING_FOLDER); + $receiptXse = new \SimpleXMLElement($receipt); + $status = (string) $receiptXse->xpath('//Receipt/Message')[0]; + + $this->debug('`Status for file %filename: %status', $context + [ + '%filename' => $filename, + '%status' => $status, + ]); + if ('SUCCESS' !== $status) { + throw new \RuntimeException(sprintf('Message for %s: %s', $filename, $status)); + } + } + catch (\Exception $exception) { + $this->logger->warning('Error checking file %filename: %message', $context + [ + '%filename' => $filename ?? NULL, + '%message' => $exception->getMessage(), + 'exception' => $exception, + ]); + return FALSE; + } + } + + return TRUE; + } + + /** + * Send dokument. + * + * @return array + * [The response, The kombi post message]. + * + * @phpstan-return array + */ + public function sendDokument( + WebformSubmissionInterface $submission, + DistributionFormularType|DistributionDokumentType|DistributionJournalPostType $dokument, + ?Attachment $attachment, + HandlerSettings $handlerSettings, + ) { + $sf2900 = $this->sf2900(); + $transactionId = Serializer::createUuid(); + + $dokumentFilNavn = NULL; + if ($dokument instanceof DistributionDokumentType) { + if (NULL === $attachment) { + throw new InvalidAttachmentElementException(sprintf('Missing attachment for %s', $dokument::class)); + } + $sftp = $sf2900->sftp(); + $sftpFilename = $this->getSftpFilename($handlerSettings, $submission, $attachment->filename); + $dokumentFilNavn = $sftp->putContents($attachment->contents, $attachment->filename, $sftpFilename); + // @todo Create trigger object? + } + + $this->setTransactionContext($transactionId, new TransactionContext( + transactionId: $transactionId, + handlerSettings: $handlerSettings, + submission: $submission, + )); + + $response = $sf2900->afsend( + transactionId: $transactionId, + document: $dokument, + routingMyndighed: $handlerSettings->sender->routingMyndighed, + routingKLEEmne: $handlerSettings->distributionContext->kleEmne, + routingHandlingFacet: $handlerSettings->distributionContext->handlingFacet, + routingModtagerAktoer: $handlerSettings->distributionContext->routingModtagerAktoer, + dokumentFilNavn: $dokumentFilNavn, + ); + + $msg = sprintf('Fordelingskomponent afsend dokument.'); + // If the cause is a submission, add webform id to audit logging message. + $msg .= sprintf(' Webform id %s.', $submission->getWebform()->id()); + $this->auditLogger->info('Fordelingskomponent', $msg); + + return [$response, $sf2900->getLastRequest()]; + } + + /** + * {@inheritdoc} + * + * @param mixed $level + * The level. + * @param string $message + * The message. + * @param array $context + * The context. + * + * @phpstan-param array $context + */ + public function log($level, $message, array $context = []): void { + $this->logger->log($level, $message, $context); + // @see https://www.drupal.org/node/3020595 + if (isset($context['webform_submission']) && $context['webform_submission'] instanceof WebformSubmissionInterface) { + $this->submissionLogger->log($level, $message, $context); + } + } + + /** + * Check if a string is a valid CVR. + */ + public static function isValidCvr(string $value): bool { + return (bool) preg_match('/^[0-9]{8}$/', $value); + } + + /** + * Check if a string is a valid UUID. + */ + public static function isValidUuid(string $value): bool { + return (bool) preg_match('/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/', $value); + } + + /** + * The SF2900 singleton. + */ + private SF2900 $sf2900; + + /** + * Get a singleton instance of SF2900. + */ + public function sf2900(): SF2900 { + if (!isset($this->sf2900)) { + $settings = $this->settings->getSenderSettings(); + $certificateKey = $this->keyRepository->getKey($settings->certificate); + // @todo Handle other key types? + $certificates = $this->keyHelper->getCertificates($certificateKey); + $certificate = implode(PHP_EOL, $certificates); + + $privateKeyKey = $this->keyRepository->getKey($settings->sftp->privateKey); + $privateKey = $privateKeyKey->getKeyValue(); + $sf2900options = [ + 'test_mode' => $this->settings->getGeneralSettings()->testMode, + 'authority_cvr' => $settings->senderId, + 'certificate' => $certificate, + 'sftp' => [ + 'private_key' => $privateKey, + // @todo Do we need to be able to handle a password here? + // 'private_key_password' => '', + 'username' => $settings->sftp->username, + ], + ]; + + $this->sf2900 = new SF2900($this->eventDispatcher, $sf2900options); + } + + return $this->sf2900; + } + + /** + * Build a Virkning object. + */ + private function buildVirkning(HandlerSettings $handlerSettings): VirkningType { + $aktoer = $handlerSettings->sender->registreringItSystem; + + return new VirkningType( + aktoer: new UUID_URN($aktoer), + aktoerType: AktoerTypeType::VALUE_IT_SYSTEM, + // fraTidsPunkt: null, + // tilTidspunkt: null, + // noteTekst: null,. + ); + } + + /** + * Deep clone a virking. + * + * @param \ItkDev\Serviceplatformen\SF2900\StructType\VirkningType $virkning + * The object to clone. + * + * @return \ItkDev\Serviceplatformen\SF2900\StructType\VirkningType + * The cloned object. + */ + private function cloneVirkning(VirkningType $virkning): VirkningType { + return unserialize(serialize($virkning)); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + // BeforeServiceCallEvent::class => 'beforeServiceCall',. + AfterServiceCallEvent::class => 'afterServiceCall', + ]; + } + + /** + * AfterServiceCallEvent event handler. + */ + public function afterServiceCall(AfterServiceCallEvent $event): void { + $request = $event->getRequest(); + $response = $event->getResponse(); + + if ($request instanceof FordelingsobjektAfsendRequestType) { + assert($response instanceof FordelingsobjektAfsendResponseType); + $anvenderTransaktionsId = $request->getAnmodning()->getDistributionContext()->getAnvenderTransaktionsID(); + $context = $this->getTransactionContext($anvenderTransaktionsId); + $this->anvenderForsendelseRepository->save( + new AnvenderForsendelse( + webformId: $context->submission->getWebform()->id(), + webformHandlerId: $context->handlerSettings->handlerId, + webformSubmissionId: $context->submission->id(), + anvenderTransaktionsId: $anvenderTransaktionsId, + request: $request, + distributionTransaktionsId: $response->getDistributionContext()->getDistributionTransktionsID(), + response: $response + ) + ); + } + } + + /** + * The transaction contexts. + * + * @var array + */ + private array $transactionContexts = []; + + /** + * Set transaction context. + */ + private function setTransactionContext( + string $transactionId, + TransactionContext $transactionContext, + ) { + $this->transactionContexts[$transactionId] = $transactionContext; + } + + /** + * Get transaction context. + */ + private function getTransactionContext( + string $transactionId, + ): TransactionContext { + return $this->transactionContexts[$transactionId]; + } + + // @see https://rimi-itk.github.io/digitaliseringskataloget.dk/digitaliseringskataloget.dk/sf1415/0.6/Integrationsbeskrivelse_SF1415.pdf#page=16 + // @todo Generate classes from resources/ServiceContract-SFTP-20230926/xsd/SFTPTypes.xsd. + private const string TRIGGER_FILE_TEMPLATE = <<<'XML' + + + + + + + + ROUTING_V1_0_0 + + + + + + + + + + + + + +XML; + + /** + * Build trigger file. + * + * @see https://rimi-itk.github.io/digitaliseringskataloget.dk/digitaliseringskataloget.dk/sf1415/0.6/Integrationsbeskrivelse_SF1415.pdf + */ + private function buildTriggerFile( + File $file, + string $sftpFilename, + HandlerSettings $handlerSettings, + WebformSubmissionInterface $submission, + string $transactionId, + ?string $recipientItSystem = NULL, + ): string { + $dom = new \DOMDocument(); + $dom->loadXML(self::TRIGGER_FILE_TEMPLATE); + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('ns2', 'http://serviceplatformen.dk/xml/wsdl/soap11/SFTP/1/types'); + $setValue = function (string $expression, mixed $value) use ($xpath) { + $nodes = $xpath->query($expression); + if (!$nodes || 1 !== $nodes->count()) { + throw new \RuntimeException(sprintf('No unique node found for expression %s', $expression)); + } + /** @var \DOMElement $node */ + $node = $nodes->item(0); + $node->nodeValue = $value; + }; + $removeElement = function (string $expression) use ($xpath) { + $nodes = $xpath->query($expression); + if (!$nodes || 1 !== $nodes->count()) { + throw new \RuntimeException(sprintf('No unique node found for expression %s', $expression)); + } + /** @var \DOMElement $node */ + $node = $nodes->item(0); + $node->parentNode->removeChild($node); + }; + + $setValue('//FileDescriptor/FileName', $sftpFilename); + $setValue('//FileDescriptor/SizeInBytes', $file->getSize()); + $setValue('//FileDescriptor/Sender', $handlerSettings->sender->sftp->username); + $setValue('//FileDescriptor/SendersFileId', $file->uuid()); + + $infRef = $handlerSettings->distributionObject->files->filspecifikation; + $senderItSystem = $handlerSettings->sender->registreringItSystem; + $senderAuthority = 'urn:oio:cvr-nr:' . $handlerSettings->sender->routingMyndighed; + $timestamp = SF2900::formatDateTime(new \DateTimeImmutable()); + + $recipientItSystem = trim((string) ($recipientItSystem ?? $handlerSettings->distributionObject->files->recipientItSystem)); + $recipientAuthority = 'urn:oio:cvr-nr:' . $handlerSettings->distributionObject->files->recipientAuthority; + + $setValue('//FileContentDescriptor/SFTPDynamicRoutingInfo/InfRef', $infRef); + $setValue('//FileContentDescriptor/SFTPDynamicRoutingInfo/SenderIt-system', $senderItSystem); + $setValue('//FileContentDescriptor/SFTPDynamicRoutingInfo/SenderAuthority', $senderAuthority); + $setValue('//FileContentDescriptor/SFTPDynamicRoutingInfo/TransactionId', $transactionId); + $setValue('//FileContentDescriptor/SFTPDynamicRoutingInfo/SenderTimestamp', $timestamp); + if (empty($recipientItSystem)) { + $removeElement('//FileContentDescriptor/SFTPDynamicRoutingInfo/RecipientIt-system'); + } + else { + $setValue('//FileContentDescriptor/SFTPDynamicRoutingInfo/RecipientIt-system', $recipientItSystem); + } + $setValue('//FileContentDescriptor/SFTPDynamicRoutingInfo/RecipientAuthority', $recipientAuthority); + + $xml = $dom->saveXML(); + + try { + $this->xmlHelper->validateXml($xml, + 'module://os2forms_fordelingskomponent/resources/ServiceContract-SFTP-20230926/xsd/SFTPTypes.xsd'); + } + catch (InvalidXmlException $e) { + $this->logger->error('Invalid XML in trigger file: %message.', [ + 'webform_submission' => $submission, + '%message' => $e->getMessage(), + 'exception' => $e, + ]); + throw new RuntimeException(sprintf('Invalid XML in trigger file: %s', $e->getMessage())); + } + + return $xml; + } + +} diff --git a/src/Helper/WebformHelperSF2900.php b/src/Helper/WebformHelperSF2900.php new file mode 100644 index 0000000..cb43f69 --- /dev/null +++ b/src/Helper/WebformHelperSF2900.php @@ -0,0 +1,414 @@ +webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission'); + $this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue'); + } + + /** + * Build distribution object. + */ + public function buildDistributionObject(HandlerSettings $handlerSettings, WebformSubmissionInterface $submission, ?Attachment $attachment): DistributionFormularType|DistributionDokumentType|DistributionJournalPostType { + $handlerSettings = $this->replaceTokens($handlerSettings, $submission); + + return $this->helper->buildDistributionObject( + $submission, + $handlerSettings, + $attachment, + ); + } + + /** + * Render XML. + */ + public function renderXml(HandlerSettings $handlerSettings, WebformSubmissionInterface $submission, bool $validateXml = TRUE): XmlRenderResult { + $files = $this->helper->buildFileGroups($handlerSettings, $submission); + + return $this->helper->renderXml($handlerSettings, $submission, files: $files, validateXml: $validateXml); + } + + /** + * Get main document. + * + * @see WebformAttachmentController::download() + */ + protected function getAttachment(WebformSubmissionInterface $submission, HandlerSettings $handlerSettings): ?Attachment { + if (!in_array($handlerSettings->distributionObject->distributionType, [ + DistributionObjectSettings::DISTRIBUTION_TYPE_DOKUMENT, + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ])) { + return NULL; + } + + // Lifted from Drupal\webform_attachment\Controller\WebformAttachmentController::download. + $element = $handlerSettings->distributionObject->attachmentElement; + $element = $submission->getWebform()->getElement($element) ?: []; + if (!isset($element['#type'])) { + throw new InvalidAttachmentElementException(sprintf('Cannot get attachment element %s', $handlerSettings->distributionObject->attachmentElement)); + } + [$type] = explode(':', $element['#type']); + $instance = $this->elementInfoManager->createInstance($type); + + if (!$instance instanceof WebformAttachmentBase) { + throw new InvalidAttachmentElementException(sprintf('Attachment element must be an instance of %s. Found %s.', WebformAttachmentBase::class, $instance::class)); + } + + $fileName = $instance::getFileName($element, $submission); + $mimeType = $instance::getFileMimeType($element, $submission); + $content = $instance::getFileContent($element, $submission); + + return new Attachment( + $content, + $mimeType, + $fileName + ); + } + + /** + * Load webform submission by id. + */ + public function loadSubmission(int $id): ?WebformSubmissionInterface { + return $this->webformSubmissionStorage->load($id); + } + + /** + * Load submission IDs for a webform. + */ + public function loadSubmissionIds(WebformInterface $webform): array { + return $this->webformSubmissionStorage->getQuery() + ->accessCheck() + ->condition('webform_id', $webform->id()) + ->sort('created', 'DESC') + ->sort('sid', 'DESC') + ->execute(); + } + + /** + * Load latest submission on a webform. + */ + public function loadLatestSubmission(WebformInterface $webform): ?WebformSubmissionInterface { + $submissionIds = $this->loadSubmissionIds($webform); + + $id = reset($submissionIds); + + return $id ? $this->loadSubmission($id) : NULL; + } + + /** + * Load queue. + */ + private function loadQueue(): QueueInterface { + $id = $this->settings->getGeneralSettings()->queue ?? NULL; + + /** @var ?\Drupal\advancedqueue\Entity\QueueInterface $queue */ + $queue = $this->queueStorage->load($id); + + if (NULL === $queue) { + throw new RuntimeException('Cannot load queue %queue_id', ['%queue_id' => $id]); + } + + return $queue; + } + + /** + * {@inheritdoc} + * + * @param mixed $level + * The level. + * @param string $message + * The message. + * @param array $context + * The context. + * + * @phpstan-param array $context + */ + public function log($level, $message, array $context = []): void { + $this->logger->log($level, $message, $context); + // @see https://www.drupal.org/node/3020595 + if (isset($context['webform_submission']) && $context['webform_submission'] instanceof WebformSubmissionInterface) { + $this->submissionLogger->log($level, $message, $context); + } + } + + /** + * Create a job. + * + * @see self::processJob() + */ + public function createJob(WebformSubmissionInterface $webformSubmission, WebformHandlerSF2900|HandlerSettings $handlerSettings, ?string $state = NULL, ?array $payload = []): ?Job { + $context = [ + 'handler_id' => WebformHandlerSF2900::ID, + 'webform_submission' => $webformSubmission, + ]; + + try { + if ($handlerSettings instanceof WebformHandlerSF2900) { + $handlerSettings = $this->settings->getHandlerSettings($handlerSettings); + } + + if (NULL === $state) { + // In initial job creating (right after submission), validate the + // submission and abort on error. + $this->validateSubmission($webformSubmission, $handlerSettings); + } + + $job = Job::create(FordelingskomponentSF2900::class, [ + self::PAYLOAD_KEY => [ + self::PAYLOAD_STATE => $state, + ] + $payload, + ] + [ + 'formId' => $webformSubmission->getWebform()->id(), + 'submissionId' => $webformSubmission->id(), + 'handlerSettings' => $handlerSettings->toArray(), + ]); + $queue = $this->loadQueue(); + $queue->enqueueJob($job); + $context['@queue'] = $queue->id(); + $this->notice('Fordelingskomponent job added to the queue @queue.', $context + [ + 'operation' => match ($state) { + self::STATE_UPLOAD_FILES => 'Fordelingskomponent upload files', + self::STATE_CHECK_FILES => 'Fordelingskomponent check files', + default => 'Fordelingskomponent afsend', + }, + ]); + + return $job; + } + catch (\Exception $exception) { + $this->error('Error creating job for fordelingskomponent: %message', $context + [ + '%message' => $exception->getMessage(), + 'operation' => 'Fordelingskomponent afsend failed', + 'exception' => $exception, + ]); + return NULL; + } + } + + /** + * Process a job. + * + * @see self::createJob() + */ + public function processJob(Job $job): JobResult { + $payload = $job->getPayload(); + $context = [ + 'handler_id' => WebformHandlerSF2900::ID, + 'operation' => 'fordelingskomponent afsend', + ]; + try { + $submissionId = $payload['submissionId']; + $submission = $this->loadSubmission($submissionId); + if (NULL === $submission) { + $message = 'Cannot load submission @submissionId'; + $context = [ + '@submissionId' => $submissionId, + ]; + $this->error($message, $context); + + throw new SubmissionNotFoundException(str_replace(array_keys($context), array_values($context), + $message)); + } + + $context['webform_submission'] = $submission; + $handlerSettings = new HandlerSettings($payload['handlerSettings']); + + $attachment = $this->getAttachment($submission, $handlerSettings); + $distributionObject = $this->buildDistributionObject($handlerSettings, $submission, $attachment); + + $sftpRoutingRequired = $distributionObject instanceof DistributionFormular + && !empty($distributionObject->getFileGroups()); + + if (!$sftpRoutingRequired) { + // No SFTP files uplead and awaiting delivery needed. + $this->helper->sendDokument($submission, $distributionObject, $attachment, $handlerSettings); + $this->notice('Fordelingskomponent afsendt', $context); + } + else { + // Start a sequence of jobs: + // + // 1. Upload files and trigger files. When done, create a job to + // 2. Check that all files have been delivered. Finally + // 3. Send distribution object. + [$state, $info] = $this->getJobState($job); + switch ($state) { + case self::STATE_UPLOAD_FILES: + $files = $this->helper->uploadFiles($distributionObject, $handlerSettings, $submission); + $this->notice('Fordelingskomponent files uploaded', $context); + $this->createJob($submission, $handlerSettings, self::STATE_CHECK_FILES, [self::PAYLOAD_FILES => $files]); + break; + + case self::STATE_CHECK_FILES: + $files = $info[self::PAYLOAD_FILES]; + if (!$this->helper->checkFilesDelivered($files, $submission)) { + $this->notice('Fordelingskomponent files not yet delivered', $context); + return JobResult::failure(sprintf('Files not yet delivered')); + } + else { + $this->notice('Fordelingskomponent files delivered', $context); + $this->createJob($submission, $handlerSettings, self::STATE_SEND_DISTRIBUTION_OBJECT, [self::PAYLOAD_FILES_DELIVERED => TRUE]); + } + break; + + case self::STATE_SEND_DISTRIBUTION_OBJECT: + $this->helper->sendDokument($submission, $distributionObject, $attachment, $handlerSettings); + $this->notice('Fordelingskomponent distribution object afsendt', $context); + break; + } + } + + return JobResult::success(); + } + catch (\Exception $e) { + $this->error('Error: @message', $context + [ + '@message' => $e->getMessage(), + 'exception' => $e, + ]); + + return JobResult::failure($e->getMessage()); + } + } + + /** + * Delete messages. + * + * @param \Drupal\os2forms_fordelingskomponent\Plugin\WebformHandler\WebformHandlerSF2900 $handler + * The handler. + * @param \Drupal\webform\WebformSubmissionInterface[] $webform_submissions + * The webform submissions. + */ + public function deleteMessages(WebformHandlerSF2900 $handler, array $webform_submissions) { + $this->anvenderForsendelseRepository->deleteBySubmissions($webform_submissions); + // @todo Clean up + } + + /** + * Replace tokens in handler settings supporting tokens. + */ + private function replaceTokens(HandlerSettings $handlerSettings, WebformSubmissionInterface $submission): HandlerSettings { + // @todo Should we clone the settings before making changes? + $handlerSettings->distributionContext->titel = $this->webformTokenManager->replace((string) $handlerSettings->distributionContext->titel, $submission); + $handlerSettings->distributionContext->beskrivelse = $this->webformTokenManager->replace((string) $handlerSettings->distributionContext->beskrivelse, $submission); + $handlerSettings->distributionContext->brugervendtNoegle = $this->webformTokenManager->replace((string) $handlerSettings->distributionContext->brugervendtNoegle, $submission); + + $handlerSettings->distributionObject->journalpostMessage = $this->webformTokenManager->replace((string) $handlerSettings->distributionObject->journalpostMessage, $submission); + + return $handlerSettings; + } + + private const string PAYLOAD_KEY = 'os2forms_fordelingskomponent'; + private const string PAYLOAD_STATE = 'state'; + private const string STATE_UPLOAD_FILES = 'upload_files'; + + private const string PAYLOAD_FILES = 'files'; + private const string STATE_CHECK_FILES = 'check_files'; + + private const string PAYLOAD_FILES_DELIVERED = 'files_delivered'; + private const string STATE_SEND_DISTRIBUTION_OBJECT = 'send_distribution_object'; + + /** + * Get state for a job. + * + * The state is computed based on data set in the job payload. + * + * This is only used when we must upload files (and check that they're ready + * before sending the actual distribution object) and hence the first state + * (and the default) is "upload files". + * + * @return array + * A job state and the job info. + */ + private function getJobState(Job $job): array { + $payload = $job->getPayload(); + + $info = $payload[self::PAYLOAD_KEY] ?? NULL; + $state = self::STATE_UPLOAD_FILES; + if (isset($info[self::PAYLOAD_FILES_DELIVERED])) { + $state = self::STATE_SEND_DISTRIBUTION_OBJECT; + } + elseif (isset($info[self::PAYLOAD_FILES])) { + $state = self::STATE_CHECK_FILES; + } + + return [$state, $info]; + } + + /** + * Validate a submission. + */ + public function validateSubmission(WebformSubmissionInterface $submission, HandlerSettings $handlerSettings): void { + $attachment = $this->getAttachment($submission, $handlerSettings); + + $this->helper->buildDistributionObject($submission, $handlerSettings, $attachment); + } + +} diff --git a/src/Helper/XmlHelper.php b/src/Helper/XmlHelper.php new file mode 100644 index 0000000..093b2a5 --- /dev/null +++ b/src/Helper/XmlHelper.php @@ -0,0 +1,200 @@ +twig->isStrictVariables(); + $this->twig->enableStrictVariables(); + return $callback(); + } finally { + if (isset($strictVariables) && $strictVariables) { + $this->twig->enableStrictVariables(); + } + else { + $this->twig->disableStrictVariables(); + } + } + } + + /** + * Render XML template. + */ + public function render(string $template, array $context, bool $validateXml = TRUE): string { + try { + if ($validateXml) { + $this->checkXml($template); + } + + return $this->useTwig( + fn () => $this->createTemplate($template)->render($context) + ); + } + catch (\Throwable $exception) { + throw new InvalidXmlException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + /** + * Get render context. + */ + public function getRenderContext(HandlerSettings $handlerSettings, WebformSubmissionInterface $submission, array $files) { + return [ + 'submission' => $submission->toArray(TRUE), + 'files' => $files, + 'handler' => ['settings' => $handlerSettings->toArray()], + ]; + } + + /** + * Check that Twig template is valid, i.e. has no syntax errors. + */ + public function validateTemplate(string $template): void { + try { + $this->createTemplate($template); + } + catch (\Throwable $exception) { + throw new InvalidXmlException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + /** + * Check that XML is valid. Optionally validate using an XSD. + * + * @throws \Drupal\os2forms_fordelingskomponent\Exception\InvalidXmlException + * An exception. + */ + public function validateXml(string $xml, ?string $xsdUrl = NULL, bool $loadXsdContent = FALSE): void { + $this->checkXml($xml); + + if (NULL === $xsdUrl) { + return; + } + + // https://www.php.net/manual/en/function.libxml-use-internal-errors.php + $useInternalErrors = libxml_use_internal_errors(TRUE); + + if ($loadXsdContent) { + $content = file_get_contents($xsdUrl); + if (FALSE === $content) { + throw new InvalidXmlException(sprintf('Error loading XSD: %s', $xsdUrl)); + } + } + + try { + $reader = new \XMLReader(); + $path = tempnam(sys_get_temp_dir(), 'os2forms_fordelingskomponent'); + file_put_contents($path, $xml); + $reader->open($path); + + $reader->setParserProperty(\XMLReader::VALIDATE, TRUE); + $reader->setSchema($xsdUrl); + + $errors = []; + + while ($reader->read()) { + if (!$reader->isValid()) { + $error = \libxml_get_last_error(); + if ($error instanceof \libXMLError) { + if (!str_contains($error->message, 'no DTD found')) { + $errors[] = $error; + } + } + } + } + + if ($errors) { + $message = $this->formatLibXmlErrors($errors); + + libxml_clear_errors(); + + throw new InvalidXmlException('Error validating XML:' . PHP_EOL . $message); + } + + } finally { + if (isset($path) && is_file($path)) { + unlink($path); + } + libxml_clear_errors(); + libxml_use_internal_errors($useInternalErrors); + } + } + + /** + * Check that XML is well-formed. + */ + private function checkXml(string $template): void { + // https://www.php.net/manual/en/function.libxml-use-internal-errors.php + $useInternalErrors = \libxml_use_internal_errors(TRUE); + try { + $doc = new \DomDocument(); + if (!$doc->loadXML($template)) { + $message = $this->formatLibXmlErrors(libxml_get_errors()); + + libxml_clear_errors(); + + throw new InvalidXmlException('Error loading XML:' . PHP_EOL . $message); + } + } + catch (\Throwable $exception) { + throw new InvalidXmlException($exception->getMessage(), $exception->getCode(), $exception); + } finally { + libxml_use_internal_errors($useInternalErrors); + } + } + + /** + * Create a Twig template. + */ + private function createTemplate(string $template): TemplateWrapper { + return $this->useTwig( + fn() => $this->twig->createTemplate($template) + ); + } + + /** + * Format a list of XML errors. + */ + private function formatLibXmlErrors(array $errors): string { + return implode( + PHP_EOL, + array_unique( + array_map( + static fn (\LibXMLError $error): string => sprintf('%d:%d: %s', $error->line, $error->column, $error->message), + $errors + ) + ) + ); + } + +} diff --git a/src/Hook/InstallHooks.php b/src/Hook/InstallHooks.php new file mode 100644 index 0000000..69f4ee8 --- /dev/null +++ b/src/Hook/InstallHooks.php @@ -0,0 +1,161 @@ + [ + 'anvender_transaktions_id' => [ + 'description' => 'UUID', + 'type' => 'varchar', + 'length' => 36, + 'not null' => TRUE, + ], + 'created_at' => [ + 'description' => 'The Unix timestamp when the item was created.', + 'type' => 'int', + 'size' => 'big', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'updated_at' => [ + 'description' => 'The Unix timestamp when the item was updated.', + 'type' => 'int', + 'size' => 'big', + 'unsigned' => TRUE, + ], + 'distribution_transaktions_id' => [ + 'description' => 'UUID', + 'type' => 'varchar', + 'length' => 36, + ], + 'request' => [ + 'description' => 'The request (serialized).', + 'type' => 'text', + 'size' => 'big', + 'not null' => TRUE, + ], + 'response' => [ + 'description' => 'The response (serialized).', + 'type' => 'text', + 'size' => 'big', + ], + ], + 'primary key' => [ + 'anvender_transaktions_id', + ], + 'indexes' => [ + 'anvender_transaktions_id' => [ + 'anvender_transaktions_id', + ], + 'distribution_transaktions_id' => [ + 'distribution_transaktions_id', + ], + ], + ]; + + $createTable = static function (string $description, array $fields = [], array $primaryKey = [], array $foreignKeys = [], array $indexes = []) use ($baseSchema): array { + $table = $baseSchema + [ + 'description' => $description, + ]; + + foreach ($fields as $name => $spec) { + $table['fields'][$name] = $spec; + } + + if ($primaryKey) { + $table['primary key'] = $primaryKey; + } + + foreach ($foreignKeys as $name => $spec) { + $table['foreign keys'][$name] = $spec; + } + + foreach ($indexes as $name => $spec) { + $table['indexes'][$name] = $spec; + } + + return $table; + }; + + $schema[self::TABLE_ANVENDER_FORSENDELSE] = $createTable( + description: 'Stores data on forsendelser.', + fields: [ + 'webform_id' => [ + 'description' => 'Webform ID.', + 'type' => 'varchar', + 'length' => '256', + ], + 'webform_handler_id' => [ + 'description' => 'Webform handler ID.', + 'type' => 'varchar', + 'length' => '256', + ], + 'webform_submission_id' => [ + 'description' => 'Webform submission ID. References {webform_submissions}.sid.', + 'type' => 'int', + 'size' => 'big', + 'unsigned' => TRUE, + ], + 'delivered_at' => [ + 'description' => 'The Unix timestamp when the item was delivered.', + 'type' => 'int', + 'size' => 'big', + 'unsigned' => TRUE, + ], + ], + foreignKeys: [ + 'webform_submission_id' => [ + 'table' => 'webform_submission', + 'columns' => [ + 'webform_submission_id' => 'sid', + ], + ], + ], + indexes: [ + 'webform_submission' => ['webform_id', 'webform_handler_id', 'webform_submission_id'], + ] + ); + + $schema[self::TABLE_ANVENDER_KVITTERING] = $createTable( + description: 'Stores data on kvitteringer.', + fields: [ + 'id' => [ + 'description' => 'Unique ID', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + ], + primaryKey: [ + 'id', + ], + ); + + $schema[self::TABLE_MODTAGER_FORSENDELSE] = $createTable( + description: 'Stores data on forsendelser.', + fields: [ + 'confirmed_at' => [ + 'description' => 'The Unix timestamp when the item was delivered.', + 'type' => 'int', + 'size' => 'big', + 'unsigned' => TRUE, + ], + ], + ); + + return $schema; + } + +} diff --git a/src/Hook/ThemeHooks.php b/src/Hook/ThemeHooks.php new file mode 100644 index 0000000..69b2bae --- /dev/null +++ b/src/Hook/ThemeHooks.php @@ -0,0 +1,60 @@ + [ + 'variables' => [ + 'webform' => NULL, + 'handler' => NULL, + 'handler_settings' => NULL, + 'info' => NULL, + 'return_url' => NULL, + ], + ], + + self::DISTRIBUTION_OBJECT_PREVIEW => [ + 'variables' => [ + 'webform' => NULL, + 'submission' => NULL, + 'handler' => NULL, + 'handler_settings' => NULL, + 'preview' => NULL, + 'links' => [ + 'prev' => NULL, + 'self' => NULL, + 'next' => NULL, + ], + ], + ], + + self::DISTRIBUTION_OBJECT_PREVIEW_RENDER => [ + 'variables' => [ + 'webform' => NULL, + 'handler' => NULL, + 'handler_settings' => NULL, + 'submission' => NULL, + 'exceptions' => NULL, + 'warnings' => NULL, + 'distribution_object' => NULL, + 'xml' => NULL, + ], + ], + ]; + } + } + +} diff --git a/src/Logger/Os2FormsFordelingskomponentLoggerChannel.php b/src/Logger/Os2FormsFordelingskomponentLoggerChannel.php new file mode 100644 index 0000000..dcc393f --- /dev/null +++ b/src/Logger/Os2FormsFordelingskomponentLoggerChannel.php @@ -0,0 +1,33 @@ +levelTranslation[$level] ?? RfcLogLevel::ERROR; + $logLevel = $this->getLogLevel(); + if ($logLevel >= $rfcLogLevel) { + parent::log($level, $message, $context); + } + } + + /** + * Get log level. + */ + private function getLogLevel(): int { + return (int) (Settings::get('os2forms_fordelingskomponent')['log_level'] ?? RfcLogLevel::ERROR); + } + +} diff --git a/src/Model/Attachment.php b/src/Model/Attachment.php new file mode 100644 index 0000000..6f05b88 --- /dev/null +++ b/src/Model/Attachment.php @@ -0,0 +1,29 @@ +mimeType; + } + +} diff --git a/src/Model/DistributionFormular.php b/src/Model/DistributionFormular.php new file mode 100644 index 0000000..995e557 --- /dev/null +++ b/src/Model/DistributionFormular.php @@ -0,0 +1,42 @@ +> + */ +final class DistributionFormular extends DistributionFormularType { + /** + * The files. + */ + protected array $fileGroups; + + /** + * Get file groups. + * + * @return FileGroups + * The file groups. + */ + public function getFileGroups(): array { + return $this->fileGroups; + } + + /** + * Set file groups. + * + * @param FileGroups $fileGroups + * The file groups. + */ + public function setFileGroups(array $fileGroups): static { + $this->fileGroups = $fileGroups; + + return $this; + } + +} diff --git a/src/Model/Fordelingskomponent/AnvenderForsendelse.php b/src/Model/Fordelingskomponent/AnvenderForsendelse.php new file mode 100644 index 0000000..2a4fe48 --- /dev/null +++ b/src/Model/Fordelingskomponent/AnvenderForsendelse.php @@ -0,0 +1,30 @@ +context; + if (($context['submission'] ?? NULL) instanceof WebformSubmissionInterface) { + $context['submission'] = $context['submission']->toArray(TRUE, TRUE); + } + // @todo Convert 'handler' and 'files' + return new self( + $this->template, + $context, + $this->rendered, + $this->exception, + ); + + } + +} diff --git a/src/Plugin/AdvancedQueue/JobType/FordelingskomponentSF2900.php b/src/Plugin/AdvancedQueue/JobType/FordelingskomponentSF2900.php new file mode 100644 index 0000000..bcd724b --- /dev/null +++ b/src/Plugin/AdvancedQueue/JobType/FordelingskomponentSF2900.php @@ -0,0 +1,67 @@ + $configuration + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get(WebformHelperSF2900::class) + ); + } + + /** + * {@inheritdoc} + * + * @phpstan-param array $configuration + */ + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + /** + * The webform helper. + */ + private readonly WebformHelperSF2900 $helper, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function process(Job $job): JobResult { + return $this->helper->processJob($job); + } + +} diff --git a/src/Plugin/WebformHandler/WebformHandlerSF2900.php b/src/Plugin/WebformHandler/WebformHandlerSF2900.php new file mode 100644 index 0000000..4d91c5f --- /dev/null +++ b/src/Plugin/WebformHandler/WebformHandlerSF2900.php @@ -0,0 +1,505 @@ +settingsService = $container->get(Settings::class); + $instance->helper = $container->get(WebformHelperSF2900::class); + $instance->xmlHelper = $container->get(XmlHelper::class); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getOffCanvasWidth(): string { + return WebformDialogHelper::DIALOG_NONE; + } + + /** + * {@inheritdoc} + */ + #[\Override] + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form[DistributionContextSettings::NAME] = [ + '#type' => 'fieldset', + '#title' => $this->t('Fordelingskomponent'), + '#tree' => TRUE, + ] + $this->buildConfigurationFormDistributionContext(); + + $form[DistributionObjectSettings::NAME] = [ + '#type' => 'fieldset', + '#title' => $this->t('Fordelingsobjekt'), + '#tree' => TRUE, + ] + $this->buildConfigurationFormDistributionObject(); + + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * Build distribution context form section. + */ + private function buildConfigurationFormDistributionContext(): array { + $settings = $this->settingsService->getDistributionContextSettings((array) ($this->getSettings()[DistributionContextSettings::NAME] ?? NULL)); + $globalSettings = $this->settingsService->getDistributionContextSettings(); + + $section[DistributionContextSettings::ROUTING_MODTAGER_AKTOER] = [ + '#title' => $this->t('Routing modtager aktoer'), + '#type' => 'textfield', + '#attributes' => [ + 'pattern' => DistributionContextSettings::ROUTING_MODTAGER_AKTOER_PATTERN, + ], + '#default_value' => $settings->routingModtagerAktoer, + '#description' => $this->t('Routing modtager aktoer (UUID). If set, any routing rules (using %kle_emne and %handling_facet) will be ignored.', [ + '%kle_emne' => $this->t('KLE-emne'), + '%handling_facet' => $this->t('Handling facet'), + ]), + ]; + + $section[DistributionContextSettings::KLE_EMNE] = [ + '#title' => $this->t('KLE-emne'), + '#type' => 'textfield', + '#default_value' => $settings->kleEmne, + // @todo Show default global value for all fields. + '#placeholder' => $globalSettings->kleEmne, + '#required' => TRUE, + '#attributes' => [ + 'pattern' => DistributionContextSettings::KLE_EMNE_PATTERN, + ], + '#description' => $this->t('KLE-emne (format: dd.dd.dd)'), + ]; + + $section[DistributionContextSettings::HANDLING_FACET] = [ + '#title' => $this->t('Handling-facet'), + '#type' => 'textfield', + '#default_value' => $settings->handlingFacet, + '#attributes' => [ + 'pattern' => DistributionContextSettings::HANDLING_FACET_PATTERN, + ], + '#description' => $this->t('handlingfacet (format: [A-Å]dd)'), + ]; + + $section[DistributionContextSettings::BRUGERVENDT_NOEGLE] = [ + '#title' => $this->t('Brugervendt nøgle'), + '#type' => 'textfield', + '#default_value' => $settings->brugervendtNoegle, + '#required' => TRUE, + '#description' => 'WHAT IS THIS?!', + ]; + + $section[DistributionContextSettings::TITEL] = [ + '#title' => $this->t('Titel'), + '#type' => 'textfield', + '#default_value' => $settings->titel, + '#required' => TRUE, + ]; + + $section[DistributionContextSettings::BESKRIVELSE] = [ + '#title' => $this->t('Beskrivelse'), + '#type' => 'textarea', + '#default_value' => $settings->beskrivelse, + '#required' => TRUE, + ]; + + return $section; + } + + /** + * Build distribution object form section. + */ + private function buildConfigurationFormDistributionObject(): array { + $settings = $this->settingsService->getDistributionObjectSettings((array) ($this->getSettings()[DistributionObjectSettings::NAME] ?? NULL)); + + $section[DistributionObjectSettings::DISTRIBUTION_TYPE] = [ + '#title' => $this->t('Distribution type'), + '#type' => 'select', + '#options' => [ + DistributionObjectSettings::DISTRIBUTION_TYPE_JOURNALPOST => $this->t('Journalpost'), + DistributionObjectSettings::DISTRIBUTION_TYPE_DOKUMENT => $this->t('Dokument'), + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR => $this->t('Formular'), + ], + '#default_value' => $settings->distributionType, + '#required' => TRUE, + ]; + + /* + * Set "visible" and "required" states on element depending on + * distribution types. + */ + $setStates = function ( + array &$element, + array $distributionTypes, + string $connector = 'or', + bool $require = TRUE, + ): void { + $conditions = array_map(static fn(string $type) => ['value' => $type], $distributionTypes); + // Insert connector between all conditions. + $numberOfValues = count($conditions); + for ($i = 0; $i < $numberOfValues - 1; $i++) { + array_splice($conditions, 2 * $i + 1, 0, [$connector]); + } + $state = [ + ':input[name="settings[' . DistributionObjectSettings::NAME . '][' . DistributionObjectSettings::DISTRIBUTION_TYPE . ']"]' => $conditions, + ]; + $element['#states']['visible'][] = $state; + if ($require) { + $element['#states']['required'][] = $state; + } + }; + + $section[DistributionObjectSettings::JOURNALPOST_MESSAGE] = [ + '#type' => 'textarea', + '#title' => $this->t('Message'), + '#default_value' => $settings->journalpostMessage, + '#description' => $this->t('Journal post message. Supports tokens.'), + ]; + $setStates($section[DistributionObjectSettings::JOURNALPOST_MESSAGE], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_JOURNALPOST, + ]); + + $attachmentElements = $this->getAttachmentElements(); + $section[DistributionObjectSettings::ATTACHMENT_ELEMENT] = [ + '#type' => 'select', + '#title' => $this->t('Element that contains the document to send'), + '#default_value' => $settings->attachmentElement, + '#options' => $attachmentElements, + ]; + $setStates($section[DistributionObjectSettings::ATTACHMENT_ELEMENT], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_DOKUMENT, + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ]); + + $section[DistributionObjectSettings::FORMULAR_TYPE] = [ + '#type' => 'textfield', + '#title' => $this->t('Formulartype'), + '#default_value' => $settings->formularType, + ]; + $setStates($section[DistributionObjectSettings::FORMULAR_TYPE], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ], require: FALSE); + + $section[DistributionObjectSettings::FILES] = [ + '#type' => 'fieldset', + '#title' => $this->t('Files'), + ]; + $setStates($section[DistributionObjectSettings::FILES], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ]); + + $section[DistributionObjectSettings::FILES][DistributionObjectFilesSettings::FILSPECIFIKATION] = [ + '#type' => 'textfield', + '#title' => $this->t('Filspecifikation (InfRef)'), + '#default_value' => $settings->files->filspecifikation, + '#description' => $this->t('Filspecifikation matching %formular_type', [ + '%formular_type' => $this->t('Formulartype'), + ]), + ]; + $setStates($section[DistributionObjectSettings::FILES][DistributionObjectFilesSettings::FILSPECIFIKATION], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ], require: FALSE); + + $section[DistributionObjectSettings::FILES][DistributionObjectFilesSettings::RECIPIENT_IT_SYSTEM_LOOK_UP] = [ + '#title' => $this->t('Look up %recipient_it_system based on distribution object routing', [ + '%recipient_it_system' => $this->t('Recipient IT system'), + ]), + '#type' => 'checkbox', + '#default_value' => $settings->files->recipientItSystemLookUp, + ]; + + $section[DistributionObjectSettings::FILES][DistributionObjectFilesSettings::RECIPIENT_IT_SYSTEM] = [ + '#title' => $this->t('Recipient IT system'), + '#type' => 'textfield', + '#attributes' => [ + 'pattern' => DistributionObjectFilesSettings::RECIPIENT_IT_SYSTEM_PATTERN, + ], + '#default_value' => $settings->files->recipientItSystem, + '#description' => $this->t('Recipient IT system (UUID). Leave empty for implicit file routing.'), + '#states' => [ + 'visible' => [ + ':input[name="settings[' . DistributionObjectSettings::NAME . '][' . DistributionObjectSettings::FILES . '][' . DistributionObjectFilesSettings::RECIPIENT_IT_SYSTEM_LOOK_UP . ']"]' => ['checked' => FALSE], + ], + ], + ]; + + $section[DistributionObjectSettings::FILES][DistributionObjectFilesSettings::RECIPIENT_AUTHORITY] = [ + '#title' => $this->t('Recipient authority'), + '#type' => 'textfield', + '#attributes' => [ + 'pattern' => DistributionObjectFilesSettings::RECIPIENT_AUTHORITY_PATTERN, + ], + '#default_value' => $settings->files->recipientAuthority, + '#description' => $this->t('CVR for recipient'), + ]; + $setStates($section[DistributionObjectSettings::FILES][DistributionObjectFilesSettings::RECIPIENT_AUTHORITY], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ], require: FALSE); + + $section[DistributionObjectSettings::XML_TEMPLATE] = [ + '#type' => 'textarea', + '#title' => $this->t('XML template'), + '#default_value' => $settings->xmlTemplate, + ]; + $setStates($section[DistributionObjectSettings::XML_TEMPLATE], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ]); + + $section[DistributionObjectSettings::XSD_URL] = [ + '#type' => 'textfield', + '#title' => $this->t('XSD URL'), + '#default_value' => $settings->xsdUrl, + ]; + $setStates($section[DistributionObjectSettings::XSD_URL], [ + DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR, + ], require: FALSE); + + return $section; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $setError = static fn(string|array $path, TranslatableMarkup $message) => $form_state->setErrorByName(implode('][', + (array) $path), $message); + + $value = $form_state->getValue(DistributionContextSettings::NAME)[DistributionContextSettings::ROUTING_MODTAGER_AKTOER] ?? ''; + if (!empty($value) + && !preg_match('/' . DistributionContextSettings::ROUTING_MODTAGER_AKTOER_PATTERN . '/', (string) $value)) { + $setError( + [DistributionContextSettings::NAME, DistributionContextSettings::ROUTING_MODTAGER_AKTOER], + $this->t('Invalid routing modtager aktør: %value.', ['%value' => $value]) + ); + } + + $value = $form_state->getValue(DistributionContextSettings::NAME)[DistributionContextSettings::KLE_EMNE] ?? ''; + if (!preg_match('/' . DistributionContextSettings::KLE_EMNE_PATTERN . '/', $value)) { + $setError( + [DistributionContextSettings::NAME, DistributionContextSettings::KLE_EMNE], + $this->t('Invalid KLE-emne: %value.', ['%value' => $value]) + ); + } + + $value = $form_state->getValue(DistributionContextSettings::NAME)[DistributionContextSettings::HANDLING_FACET] ?? ''; + if (!empty($value) + && !preg_match('/' . DistributionContextSettings::HANDLING_FACET_PATTERN . '/', (string) $value)) { + $setError( + [DistributionContextSettings::NAME, DistributionContextSettings::HANDLING_FACET], + $this->t('Invalid Handling-facet: %value.', ['%value' => $value]) + ); + } + + $value = $form_state->getValue(DistributionObjectSettings::NAME)[DistributionObjectFilesSettings::RECIPIENT_IT_SYSTEM] ?? ''; + if ($value && !preg_match('/' . DistributionObjectFilesSettings::RECIPIENT_IT_SYSTEM_PATTERN . '/', $value)) { + $setError( + [DistributionObjectSettings::NAME, DistributionObjectFilesSettings::RECIPIENT_IT_SYSTEM], + $this->t('Invalid recipient IT system: %value.', ['%value' => $value]) + ); + } + + $type = $form_state->getValue(DistributionObjectSettings::NAME)[DistributionObjectSettings::DISTRIBUTION_TYPE] ?? ''; + if (DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR === $type) { + $template = (string) ($form_state->getValue(DistributionObjectSettings::NAME)[DistributionObjectSettings::XML_TEMPLATE] ?? NULL); + try { + $this->xmlHelper->validateXml($template); + $this->xmlHelper->validateTemplate($template); + } + catch (InvalidXmlException $e) { + $form_state->setErrorByName(self::SECTION_SF2900 . '][' . DistributionObjectSettings::XML_TEMPLATE, + $this->t('Invalid XML template: %message.', ['%message' => $e->getMessage()])); + } + + $url = (string) ($form_state->getValue(DistributionObjectSettings::NAME)[DistributionObjectSettings::XSD_URL] ?? NULL); + if ($url) { + $contents = @file_get_contents($url); + if (FALSE === $contents) { + $form_state->setErrorByName(self::SECTION_SF2900 . '][' . DistributionObjectSettings::XSD_URL, + $this->t('Cannot read XSD URL %url.', ['%url' => $url])); + } + } + } + + parent::validateConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + foreach ([ + DistributionContextSettings::NAME, + DistributionObjectSettings::NAME, + // SenderSettings::NAME,. + ] as $name) { + $this->configuration[$name] = $form_state->getValue($name); + } + } + + /** + * {@inheritdoc} + */ + public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) { + // Run only when submission is completed. + // @todo Run on update? + if (!$webform_submission->isCompleted()) { + return; + } + + $this->helper->createJob($webform_submission, $this); + } + + /** + * {@inheritdoc} + * + * @phpstan-return void + */ + public function postDelete(WebformSubmissionInterface $webform_submission) { + $this->helper->deleteMessages($this, [$webform_submission]); + } + + /** + * {@inheritdoc} + * + * @phpstan-return void + */ + public function postPurge(array $webform_submissions) { + $this->helper->deleteMessages($this, $webform_submissions); + } + + /** + * {@inheritdoc} + */ + #[\Override] + public function getSummary() { + $settings = $this->settingsService->getHandlerSettings($this); + + $build = [ + 'info' => [ + '#prefix' => '
', + '#suffix' => '
', + '#markup' => $this->t('KLE-emne: %kle_emne; Handling-facet: %handling_facet', + [ + '%kle_emne' => $settings->distributionContext->kleEmne, + '%handling_facet' => $settings->distributionContext->handlingFacet, + ]), + ], + ]; + + $items = []; + + if ($settings->distributionContext->kleEmne) { + $items[] = Link::createFromRoute( + $this->t('Show routing info'), + 'os2forms_fordelingskomponent.routing_info', [ + 'webform' => $this->getWebform()->id(), + 'webform_handler' => $this->getHandlerId(), + ] + ); + } + + if ($submission = $this->helper->loadLatestSubmission($this->getWebform())) { + $items[] = Link::createFromRoute( + $this->t('Preview distribution object'), + 'os2forms_fordelingskomponent.fordelingskomponent_distribution_object.preview', [ + 'webform' => $this->getWebform()->id(), + 'webform_handler' => $this->getHandlerId(), + 'webform_submission' => $submission->id(), + ] + ); + + $items[] = Link::createFromRoute( + $this->t('Distribution objects'), + 'os2forms_fordelingskomponent.distribution_object.index', [ + 'webform' => $this->getWebform()->id(), + 'webform_handler' => $this->getHandlerId(), + ] + ); + } + + $build['links'] = [ + '#theme' => 'item_list', + '#items' => $items, + ]; + + return $build; + } + + /** + * Get attachment elements. + * + * @phpstan-return array + */ + private function getAttachmentElements(): array { + $elements = $this->getWebform()->getElementsDecodedAndFlattened(); + + $elementTypes = [ + 'webform_entity_print_attachment:pdf', + 'os2forms_attachment', + ]; + $elements = array_filter( + $elements, + static fn(array $element) => in_array($element['#type'], $elementTypes, TRUE) + ); + + return array_map(static fn(array $element) => $element['#title'], $elements); + } + +} diff --git a/src/Repository/AbstractRepository.php b/src/Repository/AbstractRepository.php new file mode 100644 index 0000000..e901666 --- /dev/null +++ b/src/Repository/AbstractRepository.php @@ -0,0 +1,23 @@ + $orderBy + * The order. + * + * @return \Drupal\os2forms_fordelingskomponent\Model\Fordelingskomponent\AnvenderForsendelse[] + * The list of forsendelser. + */ + private function loadBy(array $conditions = [], array $orderBy = ['created_at' => 'DESC']): array { + $query = $this->database + ->select(self::TABLE, 't') + ->fields('t'); + + foreach ($conditions as $condition) { + $query->condition(...$condition); + } + + foreach ($orderBy as $field => $direction) { + $query->orderBy($field, $direction); + } + + $statement = $query->execute(); + assert(NULL !== $statement); + $result = $statement->fetchAll(); + return array_map( + static fn(object $row) => new AnvenderForsendelse( + webformId: $row->webform_id, + webformHandlerId: $row->webform_handler_id, + webformSubmissionId: $row->webform_submission_id, + anvenderTransaktionsId: $row->anvender_transaktions_id, + request: unserialize($row->request, options: ['allowed_classes' => TRUE]), + distributionTransaktionsId: $row->distribution_transaktions_id, + response: unserialize($row->response, options: ['allowed_classes' => TRUE]), + createdAt: $row->created_at, + updatedAt: $row->updated_at, + deliveredAt: $row->delivered_at, + ), + array: $result + ); + } + + /** + * Load forsendelser for a submission. + * + * @return \Drupal\os2forms_fordelingskomponent\Model\Fordelingskomponent\AnvenderForsendelse[] + * The result. + */ + public function loadBySubmission(WebformSubmissionInterface $submission): array { + return $this->loadBy([ + ['webform_submission_id', $submission->id()], + ]); + } + + /** + * Load by ... + */ + public function loadByWebformAndHandler(WebformInterface $webform, WebformHandlerSF2900 $handler) { + return $this->loadBy([ + ['webform_id', $webform->id()], + ['webform_handler_id', $handler->gethandlerId()], + ]); + } + + /** + * Delete by submission. + * + * @param \Drupal\webform\WebformSubmissionInterface[] $submissions + * The submissions. + */ + public function deleteBySubmissions(array $submissions): int { + // @todo Delete kvitteringer. + if (empty($submissions)) { + return 0; + } + + try { + $ids = array_map(static fn(WebformSubmissionInterface $submission) => $submission->id(), $submissions); + return $this->database->delete(self::TABLE) + ->condition('webform_submission_id', $ids, 'IN') + ->execute(); + } + catch (\Exception $exception) { + $this->logger->error('Error deleting forsendelser for submissions @submissions: @message', [ + '@submission' => implode(', ', $ids), + '@message' => $exception->getMessage(), + 'exception' => $exception, + ]); + } + + return 0; + } + + /** + * Load forsendelse by transaktions-id. + */ + public function loadByAnvenderTransaktionsId(string $anvenderTransaktionsId, ?string $distributionTransaktionsId = NULL): ?AnvenderForsendelse { + $criteria = [ + ['anvender_transaktions_id', $anvenderTransaktionsId], + ]; + if (NULL !== $distributionTransaktionsId) { + $criteria[] = ['distribution_transaktions_id', $distributionTransaktionsId]; + } + $result = $this->loadBy($criteria); + + if (1 !== count($result)) { + return NULL; + } + + return reset($result); + } + + /** + * Save forsendelse. + */ + public function save(AnvenderForsendelse $forsendelse): bool { + try { + $now = $this->time->getRequestTime(); + $forsendelse->createdAt ??= $now; + $forsendelse->updatedAt = $now; + + $fields = [ + 'webform_id' => $forsendelse->webformId, + 'webform_handler_id' => $forsendelse->webformHandlerId, + 'webform_submission_id' => $forsendelse->webformSubmissionId, + 'anvender_transaktions_id' => $forsendelse->anvenderTransaktionsId, + 'request' => serialize($forsendelse->request), + 'distribution_transaktions_id' => $forsendelse->distributionTransaktionsId, + 'response' => serialize($forsendelse->response), + 'created_at' => $forsendelse->createdAt, + 'updated_at' => $forsendelse->updatedAt, + 'delivered_at' => $forsendelse->deliveredAt, + ]; + if (NULL === $this->loadByAnvenderTransaktionsId($forsendelse->anvenderTransaktionsId)) { + $this->database + ->insert(self::TABLE) + ->fields($fields) + ->execute(); + } + else { + $this->database + ->update(self::TABLE) + ->condition('anvender_transaktions_id', $forsendelse->anvenderTransaktionsId) + ->fields($fields) + ->execute(); + } + + return TRUE; + } + catch (\Exception $exception) { + $this->logger->error('Error saving forsendelse: @message', [ + '@message' => $exception->getMessage(), + 'exception' => $exception, + ]); + } + + return FALSE; + } + +} diff --git a/src/Repository/AnvenderKvitteringRepository.php b/src/Repository/AnvenderKvitteringRepository.php new file mode 100644 index 0000000..7e99db0 --- /dev/null +++ b/src/Repository/AnvenderKvitteringRepository.php @@ -0,0 +1,123 @@ + $orderBy + * The order. + * + * @return \Drupal\os2forms_fordelingskomponent\Model\Fordelingskomponent\AnvenderKvittering[] + * The list of kvittering. + */ + private function loadBy(array $conditions = [], array $orderBy = ['created_at' => 'DESC']): array { + $query = $this->database + ->select(self::TABLE, 't') + ->fields('t'); + + foreach ($conditions as $condition) { + $query->condition(...$condition); + } + + foreach ($orderBy as $field => $direction) { + $query->orderBy($field, $direction); + } + + $statement = $query->execute(); + assert(NULL !== $statement); + $result = $statement->fetchAll(); + return array_map( + static fn(object $row) => new AnvenderKvittering( + id: $row->id, + anvenderTransaktionsId: $row->anvender_transaktions_id, + distributionTransaktionsId: $row->distribution_transaktions_id, + request: unserialize($row->request, options: ['allowed_classes' => TRUE]), + response: unserialize($row->response, options: ['allowed_classes' => TRUE]), + createdAt: $row->created_at, + updatedAt: $row->updated_at, + ), + array: $result + ); + } + + /** + * Load kvittering by ID. + * + * @return ?\Drupal\os2forms_fordelingskomponent\Model\Fordelingskomponent\AnvenderKvittering + * The kvittering if any. + */ + public function load(int $id): ?AnvenderKvittering { + $criteria = [ + ['id', $id], + ]; + + $result = $this->loadBy($criteria); + + return 1 === count($result) ? reset($result) : NULL; + } + + /** + * Load kvittering by transaktions-id. + * + * @return \Drupal\os2forms_fordelingskomponent\Model\Fordelingskomponent\AnvenderKvittering[] + * The kvitteringer. + */ + public function loadByAnvenderTransaktionsId(string $anvenderTransaktionsId, ?string $distributionTransaktionsId = NULL): array { + $criteria = [ + ['anvender_transaktions_id', $anvenderTransaktionsId], + ]; + if (NULL !== $distributionTransaktionsId) { + $criteria[] = ['distribution_transaktions_id', $distributionTransaktionsId]; + } + + return $this->loadBy($criteria); + } + + /** + * Save kvittering. + */ + public function save(AnvenderKvittering $kvittering): bool { + try { + $now = $this->time->getRequestTime(); + $kvittering->createdAt ??= $now; + $kvittering->updatedAt = $now; + + $fields = [ + 'anvender_transaktions_id' => $kvittering->anvenderTransaktionsId, + 'request' => serialize($kvittering->request), + 'distribution_transaktions_id' => $kvittering->distributionTransaktionsId, + 'response' => serialize($kvittering->response), + 'created_at' => $kvittering->createdAt, + 'updated_at' => $kvittering->updatedAt, + ]; + + $this->database + ->insert(self::TABLE) + ->fields($fields) + ->execute(); + + return TRUE; + } + catch (\Exception $exception) { + $this->logger->error('Error saving kvittering: @message', [ + '@message' => $exception->getMessage(), + 'exception' => $exception, + ]); + } + + return FALSE; + } + +} diff --git a/src/Settings.php b/src/Settings.php new file mode 100644 index 0000000..2be3caf --- /dev/null +++ b/src/Settings.php @@ -0,0 +1,91 @@ +config = $configFactory->get(self::CONFIG_NAME); + } + + /** + * Get general settings. + */ + public function getGeneralSettings(): GeneralSettings { + return new GeneralSettings($this->getValue(GeneralSettings::NAME)); + } + + /** + * Get sender settings. + */ + public function getSenderSettings(array $values = []): SenderSettings { + return (new SenderSettings($this->getValue(SenderSettings::NAME))) + ->apply($values); + } + + /** + * Get distribution context settings. + */ + public function getDistributionContextSettings(array $values = []): DistributionContextSettings { + return (new DistributionContextSettings($this->getValue(DistributionContextSettings::NAME))) + ->apply($values); + } + + /** + * Get distribution object settings. + */ + public function getDistributionObjectSettings(array $values = []): DistributionObjectSettings { + return (new DistributionObjectSettings($this->getValue(DistributionObjectSettings::NAME))) + ->apply($values); + } + + /** + * Get handler settings. + * + * The settings are the global settings with handler specific settings on top. + */ + public function getHandlerSettings(WebformHandlerSF2900 $handler): HandlerSettings { + $handlerSettings = $handler->getSettings(); + $settings = new HandlerSettings([ + HandlerSettings::HANDLER_ID => $handler->gethandlerId(), + HandlerSettings::SENDER => $this->getSenderSettings($handlerSettings[SenderSettings::NAME] ?? []), + HandlerSettings::DISTRIBUTION_CONTEXT => $this->getDistributionContextSettings($handlerSettings[DistributionContextSettings::NAME] ?? []), + HandlerSettings::DISTRIBUTION_OBJECT => $this->getDistributionObjectSettings($handlerSettings[DistributionObjectSettings::NAME] ?? []), + ]); + + return $settings; + } + + /** + * Get settings value. + * + * @return array + * The settings values. + */ + private function getValue(string $section): array { + $values = $this->config->get($section); + + return is_array($values) ? $values : []; + } + +} diff --git a/src/Settings/AbstractSettings.php b/src/Settings/AbstractSettings.php new file mode 100644 index 0000000..08f85b5 --- /dev/null +++ b/src/Settings/AbstractSettings.php @@ -0,0 +1,154 @@ + + * $settingsProperties = [ + * 'items' => SomeNestedSettings::class, + * ]; + * + * + * @var array + */ + protected static array $settingsProperties = []; + + /** + * List properties. + * + * Map from name to AbstractSettings type, e.g. + * + * + * $listProperties = [ + * 'items' => SomeNestedSettings::class, + * ]; + * + * + * @var array + */ + protected static array $listProperties = []; + + /** + * Nullable properties. + * + * List of properties that must be set to null if the value is a blank string. + * + * @var array + */ + protected static array $nullableProperties = []; + + /** + * The values. + * + * @var array + */ + protected array $values; + + /** + * Constructor. + * + * @param array $values + * The values. + * @param bool $throwExceptionOnMissingProperty + * If set, an exception is thrown when setting an undefined property. + * If not set, undefined properties are silently ignored. + */ + public function __construct(array $values, bool $throwExceptionOnMissingProperty = FALSE) { + $this->values = []; + $this->apply($values, $throwExceptionOnMissingProperty); + } + + /** + * Apply values to settings. + */ + public function apply(array $values, bool $throwExceptionOnMissingProperty = FALSE): static { + foreach (static::$listProperties as $property => $class) { + if (isset($values[$property]) && is_array($values[$property])) { + $values[$property] = array_map(static fn(array $vals) => new $class($vals), $values[$property]); + } + } + + foreach (static::$settingsProperties as $property => $class) { + if (isset($values[$property]) && is_array($values[$property])) { + $values[$property] = new $class($values[$property]); + } + } + + foreach ($values as $key => $value) { + $name = self::kebab2camel($key); + if (!property_exists($this, $name)) { + if ($throwExceptionOnMissingProperty) { + throw new \RuntimeException( + $name !== $key + ? sprintf('Property "%s" ("%s") does not exist in class %s.', + $name, $key, static::class) + : sprintf('Property "%s" does not exist in class %s.', $name, + static::class) + ); + } + else { + continue; + } + } + if (isset(static::$nullableProperties[$key]) && empty(trim((string) $value))) { + $value = NULL; + } + $this->$name = $value; + $this->values[self::camel2kebab($name)] = $value; + } + + return $this; + } + + /** + * Convert settings to array. + */ + public function toArray(bool $recursive = TRUE): array { + $values = $this->values; + + if ($recursive) { + foreach ($values as &$value) { + if ($value instanceof self) { + $value = $value->toArray($recursive); + } + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize(): array { + return $this->toArray(); + } + + /** + * Convert kebab_case to camelCase. + */ + public static function kebab2camel(string $value): string { + return lcfirst(str_replace('_', '', ucwords($value, '_'))); + } + + /** + * Convert camelCase to kebab_case. + * + * @see https://stackoverflow.com/a/40514305/2502647 + */ + public static function camel2kebab(string $value): string { + return strtolower((string) preg_replace('/(?<=\d)(?=[A-Za-z])|(?<=[A-Za-z])(?=\d)|(?<=[a-z])(?=[A-Z])/', '_', $value)); + } + +} diff --git a/src/Settings/DistributionContextSettings.php b/src/Settings/DistributionContextSettings.php new file mode 100644 index 0000000..b18e4ec --- /dev/null +++ b/src/Settings/DistributionContextSettings.php @@ -0,0 +1,39 @@ + TRUE, + self::HANDLING_FACET => TRUE, + ]; + + public const string KLE_EMNE_PATTERN = '^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$'; + public const string KLE_EMNE = 'kle_emne'; + public ?string $kleEmne = NULL; + + public const string HANDLING_FACET_PATTERN = '^[A-Z,Æ,Ø,Å][0-9][0-9]$'; + public const string HANDLING_FACET = 'handling_facet'; + public ?string $handlingFacet = NULL; + + public const string BRUGERVENDT_NOEGLE = 'brugervendt_noegle'; + public ?string $brugervendtNoegle = NULL; + + public const string ROUTING_MODTAGER_AKTOER_PATTERN = self::UUID_PATTERN; + public const string ROUTING_MODTAGER_AKTOER = 'routing_modtager_aktoer'; + public ?string $routingModtagerAktoer = NULL; + + public const string TITEL = 'titel'; + public ?string $titel = NULL; + + public const string BESKRIVELSE = 'beskrivelse'; + public ?string $beskrivelse = NULL; + +} diff --git a/src/Settings/DistributionObjectSettings.php b/src/Settings/DistributionObjectSettings.php new file mode 100644 index 0000000..99b0a7d --- /dev/null +++ b/src/Settings/DistributionObjectSettings.php @@ -0,0 +1,50 @@ + FilesSettings::class, + ]; + + public const string DISTRIBUTION_TYPE = 'distribution_type'; + public ?string $distributionType = NULL; + + public const string DISTRIBUTION_TYPE_JOURNALPOST = ObjektTypeType::VALUE_JOURNALPOST; + public const string DISTRIBUTION_TYPE_DOKUMENT = ObjektTypeType::VALUE_DOKUMENT; + public const string DISTRIBUTION_TYPE_FORMULAR = ObjektTypeType::VALUE_FORMULAR; + + public const string JOURNALPOST_MESSAGE = 'journalpost_message'; + public ?string $journalpostMessage = NULL; + + public const string ATTACHMENT_ELEMENT = 'attachment_element'; + public ?string $attachmentElement = NULL; + + public const string FORMULAR_TYPE = 'formular_type'; + public ?string $formularType = ''; + + public const string XML_TEMPLATE = 'xml_template'; + public ?string $xmlTemplate = NULL; + + public const string XSD_URL = 'xsd_url'; + public ?string $xsdUrl = NULL; + + const string FILES = 'files'; + public ?FilesSettings $files = NULL; + + public function __construct(array $values, bool $throwExceptionOnMissingProperty = FALSE) { + $this->files = new FilesSettings([]); + parent::__construct($values, $throwExceptionOnMissingProperty); + } + +} diff --git a/src/Settings/DistributionObjectSettings/FilesSettings.php b/src/Settings/DistributionObjectSettings/FilesSettings.php new file mode 100644 index 0000000..fd72936 --- /dev/null +++ b/src/Settings/DistributionObjectSettings/FilesSettings.php @@ -0,0 +1,27 @@ + SenderSettings::class, + self::DISTRIBUTION_CONTEXT => DistributionContextSettings::class, + self::DISTRIBUTION_OBJECT => DistributionObjectSettings::class, + ]; + + const string HANDLER_ID = 'handler_id'; + public string $handlerId; + + const string SENDER = 'sender'; + public ?SenderSettings $sender = NULL; + + const string DISTRIBUTION_CONTEXT = 'distribution_context'; + public ?DistributionContextSettings $distributionContext = NULL; + + const string DISTRIBUTION_OBJECT = 'distribution_object'; + public ?DistributionObjectSettings $distributionObject = NULL; + +} diff --git a/src/Settings/SenderSettings.php b/src/Settings/SenderSettings.php new file mode 100644 index 0000000..9611cd2 --- /dev/null +++ b/src/Settings/SenderSettings.php @@ -0,0 +1,39 @@ + SftpSettings::class, + ]; + + const string ROUTING_MYNDIGHED = 'routing_myndighed'; + public ?string $routingMyndighed = NULL; + + const string SENDER_ID = 'sender_id'; + public ?string $senderId = NULL; + + const string REGISTRERING_IT_SYSTEM = 'registrering_it_system'; + public ?string $registreringItSystem = NULL; + + const string CERTIFICATE = 'certificate'; + public ?string $certificate = NULL; + + const string SFTP = 'sftp'; + public ?SftpSettings $sftp = NULL; + + public function __construct(array $values, bool $throwExceptionOnMissingProperty = FALSE) { + $this->sftp = new SftpSettings([]); + parent::__construct($values, $throwExceptionOnMissingProperty); + } + +} diff --git a/src/Settings/SenderSettings/SftpSettings.php b/src/Settings/SenderSettings/SftpSettings.php new file mode 100644 index 0000000..5f49ca7 --- /dev/null +++ b/src/Settings/SenderSettings/SftpSettings.php @@ -0,0 +1,19 @@ + +{% block styles %} + +{% endblock %} + +{% block navigation %} + +{% endblock %} + +{% block content %}{% endblock %} + +{% block handler_setting %} + {{ _self.render_handler_settings(handler_settings) }} +{% endblock %} + + +{% macro render_handler_settings(handler_settings) %} +
+ {{ 'Handler settings'|trans }} + {{ _self.render_json(handler_settings) }} +
+{% endmacro %} + +{% macro render_json(value, label) %} +
+ {{ label }} + + {# + {% set data_url = 'data:application/json,' ~ (value|json_encode|url_encode) %} + + {{ 'Open data in new tab'|trans }} + #} + +
{{ value|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_UNICODE')) }}
+
+{% endmacro %} + +{% macro render_xml(value, label) %} +
+ {{ label }} + + {# + {% set data_url = 'data:text/xml,' ~ (value|url_encode) %} + + {{ 'Open data in new tab'|trans }} + #} + +
{{ value }}
+
+{% endmacro %} diff --git a/templates/os2forms-fordelingskomponent-distribution-object-preview.html.twig b/templates/os2forms-fordelingskomponent-distribution-object-preview.html.twig new file mode 100644 index 0000000..2086548 --- /dev/null +++ b/templates/os2forms-fordelingskomponent-distribution-object-preview.html.twig @@ -0,0 +1,80 @@ +{% extends '@os2forms_fordelingskomponent/base.html.twig' %} + +{# +/** + * @file + * Template for Fordelingskomponent payload preview. + * + * Available variables: + * - preview_urls: The preview URLs + * - prev: Previous submission preview URL (if any) + * - self: The current preview URL (if any) + * - next: Next submission preview URL (if any) + * - webform: The webform + * - handler: The handler ID + * - submission: The submission ID + * - return_url: The return URL (to list of webform handlers) + * - render_url: The render URL to render the actual preview + */ +#} +{% block content %} + {% for exception in preview.exceptions %} + + {% endfor %} + + {% for warning in preview.warnings %} + + {% endfor %} + + {% set distribution_type = handler_settings.distributionObject.distributionType %} + {% set distribution_object = preview.distribution_object|default(null) %} + {% if distribution_type == constant('\\Drupal\\os2forms_fordelingskomponent\\Settings\\DistributionObjectSettings::DISTRIBUTION_TYPE_JOURNALPOST') %} + {{ distribution_type }} + {{ _self.render_json(distribution_object) }} + {% elseif distribution_type == constant('\\Drupal\\os2forms_fordelingskomponent\\Settings\\DistributionObjectSettings::DISTRIBUTION_TYPE_DOKUMENT') %} + {{ distribution_type }} + {{ _self.render_json(distribution_object) }} + {% elseif distribution_type == constant('\\Drupal\\os2forms_fordelingskomponent\\Settings\\DistributionObjectSettings::DISTRIBUTION_TYPE_FORMULAR') %} + {{ distribution_type }} + {% if distribution_object %} +
+ {{ 'Distribution object'|trans }} + {{ _self.render_json(distribution_object) }} +
+ {% endif %} + + {% set xml = distribution_object.meddelelse.formular.formularXml.any|default(preview.xml.rendered) %} + {% if xml %} +
+ {{ 'XML'|trans }} + + {{ _self.render_xml(xml) }} +
+ {% endif %} + + {% set xml_template = handler_settings.distributionObject.xmlTemplate|default(null) %} + {% if xml_template %} +
+ {{ 'XML template'|trans }} + + {{ _self.render_xml(xml_template) }} +
+ {% endif %} + + {% if preview.xml.context %} +
+ {{ 'XML render context'|trans }} + + {{ _self.render_json(preview.xml.context) }} +
+ {% endif %} + {% else %} + + {% endif %} +{% endblock %} diff --git a/templates/os2forms-fordelingskomponent-routing-info.html.twig b/templates/os2forms-fordelingskomponent-routing-info.html.twig new file mode 100644 index 0000000..327a95c --- /dev/null +++ b/templates/os2forms-fordelingskomponent-routing-info.html.twig @@ -0,0 +1,16 @@ +{% extends '@os2forms_fordelingskomponent/base.html.twig' %} + +{# +/** + * @file + * Template for Fordelingskomponent routing info. + * + * Available variables: + * - webform: The webform + * - handler: The handler + * - return_url: The return URL (to list of webform handlers) + */ +#} +{% block content %} + {{ _self.render_json(info) }} +{% endblock %} diff --git a/tests/Unit/AbstractTestCase.php b/tests/Unit/AbstractTestCase.php new file mode 100644 index 0000000..45e8bdf --- /dev/null +++ b/tests/Unit/AbstractTestCase.php @@ -0,0 +1,12 @@ +twig = new Environment(new ArrayLoader()); + $this->helper = new XmlHelper($this->twig); + } + + /** + * @covers \Drupal\os2forms_fordelingskomponent\Helper\XmlHelper + */ + public function testStrictVariables(): void { + $this->assertFalse($this->twig->isStrictVariables()); + $this->helper->validateTemplate(''); + $this->helper->validateXml(''); + $this->helper->render('', []); + $this->assertFalse($this->twig->isStrictVariables()); + } + + /** + * @covers \Drupal\os2forms_fordelingskomponent\Helper\XmlHelper + * @dataProvider \Drupal\os2forms_fordelingskomponent\Test\Unit\Helper\XmlHelperTestDataProvider::provideRenderData + */ + public function testRender( + string $template, + array $context, + string|InvalidXmlException $expected, + ) { + if ($expected instanceof InvalidXmlException) { + $this->expectException($expected::class); + } + + $actual = $this->helper->render($template, $context); + $this->assertXmlStringEqualsXmlString($expected, $actual); + } + + /** + * @covers \Drupal\os2forms_fordelingskomponent\Helper\XmlHelper + * @dataProvider \Drupal\os2forms_fordelingskomponent\Test\Unit\Helper\XmlHelperTestDataProvider::provideValidateTemplateData() + */ + public function testValidateTemplate( + string $template, + ?InvalidXmlException $expected = NULL, + ) { + if ($expected instanceof InvalidXmlException) { + $this->expectException($expected::class); + } + + $this->helper->validateTemplate($template); + $this->assertTrue(TRUE); + } + + /** + * @covers \Drupal\os2forms_fordelingskomponent\Helper\XmlHelper + * @dataProvider \Drupal\os2forms_fordelingskomponent\Test\Unit\Helper\XmlHelperTestDataProvider::provideValidateXmlData() + */ + public function testValidateXml( + string $template, + string $xsdUrl, + ?InvalidXmlException $expected = NULL, + ) { + if ($expected instanceof InvalidXmlException) { + $this->expectException($expected::class); + } + + $this->helper->validateXml($template, $xsdUrl); + $this->assertTrue(TRUE); + } + +} diff --git a/tests/Unit/Helper/XmlHelperTestDataProvider.php b/tests/Unit/Helper/XmlHelperTestDataProvider.php new file mode 100644 index 0000000..e2acd6c --- /dev/null +++ b/tests/Unit/Helper/XmlHelperTestDataProvider.php @@ -0,0 +1,266 @@ + + + {{ name }} + +XML, + [], + new InvalidXmlException(), + ]; + + yield [ + <<<'XML' + + +
+ urn:oio:cvr-nr:{{ handlers.settings.cvr }} + {{ webform_submission.completed|date('Y-m-d') }} + {{ handlers.settings.kle }} +
+ + + {{ webform_submission.data.fornavn }} + {{ webform_submission.data.efternavn }} + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + {{ webform_submission.completed|date('Y-m-d') }} + +
+XML, + [ + 'handlers' => [ + 'settings' => [ + 'cvr' => '12345678', + 'kle' => '01.02.03', + ], + ], + 'webform_submission' => self::createWebformSubmission([ + 'completed' => (new \DateTimeImmutable('2001-01-01T00:00:00Z'))->getTimestamp(), + 'data' => [ + 'fornavn' => 'Anders', + 'efternavn' => 'And', + ], + ]), + ], + <<<'XML' + + +
+ urn:oio:cvr-nr:12345678 + 2001-01-01 + 01.02.03 +
+ + + Anders + And + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + 2001-01-01 + +
+XML, + ]; + + yield [ + <<<'XML' + + +
+ urn:oio:cvr-nr:{{ handlers.settings.cvr }} + {{ webform_submission.completed|date('Y-m-d') }} + {{ handlers.settings.kle }} +
+ +{% if webform_submission.data.fornavn == "And" %} + + {{ webform_submission.data.fornavn }} + {{ webform_submission.data.efternavn }} + urn:oio:cpr:0000000000 + +{% else %} + + {{ webform_submission.data.fornavn }} + {{ webform_submission.data.efternavn }} + urn:oio:cpr:0000000000 + +{% endif %} + + + Medicin + + + Underskrift0 + {{ webform_submission.completed|date('Y-m-d') }} + +
+XML, + [ + 'handlers' => [ + 'settings' => [ + 'cvr' => '12345678', + 'kle' => '01.02.03', + ], + ], + 'webform_submission' => self::createWebformSubmission([ + 'completed' => (new \DateTimeImmutable('2001-01-01T00:00:00Z'))->getTimestamp(), + 'data' => [ + 'fornavn' => 'Anders', + 'efternavn' => 'And', + ], + ]), + ], + <<<'XML' + + +
+ urn:oio:cvr-nr:12345678 + 2001-01-01 + 01.02.03 +
+ + + Anders + And + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + 2001-01-01 + +
+XML, + ]; + } + + /** + * Create webform submission. + */ + private static function createWebformSubmission(array $values): array { + return $values; + } + + /** + * Data provider. + */ + public static function provideValidateTemplateData(): iterable { + yield [ + <<<'XML' + + + {{ name } + +XML, + new InvalidXmlException(), + ]; + + yield [ + <<<'XML' + + + {% if name == "And" %} + {{ name }} + +XML, + new InvalidXmlException(), + ]; + + } + + /** + * Data provider. + */ + public static function provideValidateXmlData(): iterable { + yield [ + <<<'XML' + + +
+ urn:oio:cvr-nr:12345678 + 2001-01-01 + 01.02.03 +
+ + + Anders + And + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + 2001-01-01 + +
+XML, + self::RESOURCE_PATH . '/xsd/Anmodning.xsd', + ]; + + yield [ + <<<'XML' + + + + + Anders + And + urn:oio:cpr:0000000000 + + + + Medicin + + + Underskrift0 + 2001-01-01 + + +XML, + self::RESOURCE_PATH . '/xsd/Anmodning.xsd', + new InvalidXmlException(), + ]; + } + +}