diff --git a/.db_requirements b/.db_requirements index 45c7a584..03ac6403 100644 --- a/.db_requirements +++ b/.db_requirements @@ -1 +1 @@ -v0.0.1 +v0.0.13 diff --git a/.env b/.env index 3d1e57b3..68cdeb6c 100644 --- a/.env +++ b/.env @@ -13,6 +13,9 @@ APP_SECRET=32f3c49be690d4c5f499093ae7dd3a7d # Database at-rest encryption key (generated with "php bin/console generate:database-key") DATABASE_ENCRYPTION_KEY= +# The name of the site. Used only for displaying purposes. +SITE_NAME=open.minvws.nl + # ----------------------------------------------------- # External service configuration # ----------------------------------------------------- @@ -45,6 +48,9 @@ TIKA_HOST=http://127.0.0.1:9998 # Redis instance that is used for document content caching REDIS_URL=redis://localhost:6379 +REDIS_TLS_CAFILE= +REDIS_TLS_LOCAL_CERT= +REDIS_TLS_LOCAL_PK= # The name of cookie. Should start with __Host- , but cannot be prefixed # with __Host- when running on non-TLS connections @@ -52,3 +58,31 @@ COOKIE_NAME=WOOPID # Issuer of the TOTP tokens, used in 2fa for the totp URI TOTP_ISSUER=localhost + +# Application mode. Could be only for balie (backend), frontend, or both +APP_MODE=both + +# Base URL of the application frontend (which could different from backend when APP_MODE is not BOTH) +PUBLIC_BASE_URL=http://localhost:8000 + +# ----------------------------------------------------- +# Storage adapter to use +# Choose between aws or local +STORAGE_DOCUMENT_ADAPTER=local +STORAGE_THUMBNAIL_ADAPTER=local +STORAGE_BATCH_ADAPTER=local + +# Storage adapter configuration for AWS S3/Minio +STORAGE_MINIO_REGION=eu-west-1 +STORAGE_MINIO_ENDPOINT= +STORAGE_MINIO_ACCESS_KEY= +STORAGE_MINIO_SECRET_KEY= + +# Bucket definitions for AWS S3/Minio +STORAGE_MINIO_DOCUMENT_BUCKET=doc_bucket +STORAGE_MINIO_THUMBNAIL_BUCKET=thumb_bucket +STORAGE_MINIO_BATCH_BUCKET=batch_bucket + +# ----------------------------------------------------- +# Identification number for Piwik analytics +PIWIK_ANALYTICS_ID=0 diff --git a/.env.ci b/.env.ci index 64a563bf..2c974b90 100644 --- a/.env.ci +++ b/.env.ci @@ -3,6 +3,8 @@ APP_ENV=dev APP_SECRET=32f3c49be690d4c5f499093ae7dd3a7d +SITE_NAME=open.minvws.nl + DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?serverVersion=15&charset=utf8" HIGH_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/high @@ -20,7 +22,20 @@ ELASTICSEARCH_MTLS_CA_PATH= TIKA_HOST=http://localhost:9998 REDIS_URL=redis://localhost:6379 +REDIS_TLS_CAFILE= +REDIS_TLS_LOCAL_CERT= +REDIS_TLS_LOCAL_PK= COOKIE_NAME=WOOPID TOTP_ISSUER=localhost + +APP_MODE=both +PUBLIC_BASE_URL=http://localhost:8000 + + +STORAGE_DOCUMENT_ADAPTER=local +STORAGE_THUMBNAIL_ADAPTER=local +STORAGE_BATCH_ADAPTER=local + +PIWIK_ANALYTICS_ID=0 diff --git a/.env.development b/.env.development index 202f708e..5a9848af 100644 --- a/.env.development +++ b/.env.development @@ -3,6 +3,8 @@ APP_ENV=dev APP_SECRET=32f3c49be690d4c5f499093ae7dd3a7d +SITE_NAME=open.minvws.nl + DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres?serverVersion=15&charset=utf8" HIGH_TRANSPORT_DSN=amqp://guest:guest@rabbitmq:5672/%2f/high @@ -26,8 +28,19 @@ TIKA_HOST=http://tika:9998 DATABASE_ENCRYPTION_KEY= REDIS_URL=redis://redis:6379 +REDIS_TLS_CAFILE= +REDIS_TLS_LOCAL_CERT= +REDIS_TLS_LOCAL_PK= COOKIE_NAME=WOOPID TOTP_ISSUER=localhost +APP_MODE=both +PUBLIC_BASE_URL=http://localhost:8000 + +STORAGE_DOCUMENT_ADAPTER=local +STORAGE_THUMBNAIL_ADAPTER=local +STORAGE_BATCH_ADAPTER=local + +PIWIK_ANALYTICS_ID=0 diff --git a/.github/workflows/documentation-linter.yml b/.github/workflows/documentation-linter.yml index 1a6815f8..0d9c53b8 100644 --- a/.github/workflows/documentation-linter.yml +++ b/.github/workflows/documentation-linter.yml @@ -8,8 +8,8 @@ jobs: name: lint markDown file runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: DavidAnson/markdownlint-cli2-action@v11 + - uses: actions/checkout@v3 + - uses: DavidAnson/markdownlint-cli2-action@v12 with: globs: '**/*.md' diff --git a/.github/workflows/php-tests.yml b/.github/workflows/php-tests.yml index 5f0aaf69..d558663e 100644 --- a/.github/workflows/php-tests.yml +++ b/.github/workflows/php-tests.yml @@ -148,3 +148,36 @@ jobs: max-parallel: 3 matrix: php-versions: [ '8.1', '8.2' ] + + php-linting-twig: + needs: + - composer-install + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: vendor/ + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install PHP + uses: shivammathur/setup-php@master + with: + php-version: ${{ matrix.php-versions }} + - name: copy env file + run: | + cp .env.ci .env.local + # change database url to sqlite + sed -i 's|^DATABASE_URL=.*|DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db|' .env.local + - name: Twig linter + run: | + bin/console cache:clear + bin/console cache:warmup + bin/console lint:twig templates + env: + APP_ENV: prod + APP_DEBUG: false + strategy: + max-parallel: 3 + matrix: + php-versions: [ '8.1', '8.2' ] diff --git a/.github/workflows/robotframeworkci.yml b/.github/workflows/robotframeworkci.yml index f94b1480..60e07fb7 100644 --- a/.github/workflows/robotframeworkci.yml +++ b/.github/workflows/robotframeworkci.yml @@ -1,8 +1,6 @@ name: Robot Framework tests on: - push: - branches: [ main ] pull_request: branches: [ main ] diff --git a/.gitignore b/.gitignore index 77369483..a07108b9 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ yarn-error.log # /database +/public/sitemap* diff --git a/.npmrc b/.npmrc index b822692d..4d644ad0 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -@minvws:registry=https://npm.pkg.github.com +@minvws:registry=https://npm.pkg.github.com \ No newline at end of file diff --git a/Makefile b/Makefile index b00b5f5c..e75ca103 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SHELL=/usr/bin/env bash -O globstar all: help -test: test_phpcs test_phpstan test_phpcsfixer test_phpmd test_unit test_psalm ## Runs tests +test: test_phpcs test_phpstan test_phpcsfixer test_phpmd test_unit test_psalm test_twig test_markdown ## Runs tests test_phpcs: source test-utils.sh ;\ @@ -36,6 +36,18 @@ test_unit: ## Run unit tests section "PHPUNIT" ;\ vendor/bin/phpunit --testsuite "Woopie Unit Test Suite" +test_twig: ## Run twig linter + source test-utils.sh ;\ + section "TWIG-LINT" ;\ + APP_DEBUG=false APP_ENV=prod php bin/console cache:clear + APP_DEBUG=false APP_ENV=prod php bin/console cache:warmup + APP_DEBUG=false APP_ENV=prod php bin/console lint:twig templates + +test_markdown: ## Lint markdown files + source test-utils.sh ;\ + section "MARKDOWN-LINT" ;\ + npm run mdlint + fix: ## Fixes coding style vendor/bin/php-cs-fixer fix vendor/bin/phpcbf @@ -71,3 +83,11 @@ test-rf/%: ## Run Robot Framework tests with matching tag test-rf-head/%: ## Run Robot Framework with browser visible, with matching tag env/bin/python -m robot -d tests/robot_framework/results -x outputxunit.xml -i $* -v headless:false tests/robot_framework + +update: ## Update code / db/ assets, for instance after git pull + composer install + bin/console doctrine:migrations:migrate --no-interaction + npm install + npm run build + vendor/bin/phpdotenvsync --opt=sync --src=.env.development --dest=.env.local --no-interaction + diff --git a/assets/admin.js b/assets/admin.js index 87f53eb0..92db4320 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -1 +1,5 @@ -import './styles/admin/admin.scss'; \ No newline at end of file +// Styling +import './styles/admin/admin.scss'; + +// JS +import '@minvws/manon/collapsible.js'; diff --git a/assets/app.js b/assets/app.js index 4dffd788..3f3d7a01 100644 --- a/assets/app.js +++ b/assets/app.js @@ -3,3 +3,6 @@ import './styles/login.css'; import './init.js'; import '@minvws/manon/collapsible.js'; +import "./navigation.js" +import './search/init.js'; +import './tabs.js'; diff --git a/assets/dropzone.js b/assets/dropzone.js index a70bb199..27d3bd79 100644 --- a/assets/dropzone.js +++ b/assets/dropzone.js @@ -7,13 +7,13 @@ Dropzone.options.uploadform = { autoProcessQueue: true, uploadMultiple: false, addRemoveLinks: true, - maxFiles: 100, + maxFiles: 2000, maxFilesize: 4096, // MB chunking: true, parallelChunkUploads: true, retryChunks: true, retryChunksLimit: 3, - chunkSize: 50 * 1024 * 1024, // Bytes + chunkSize: 16 * 1024 * 1024, // Bytes timeout: 0, dictDefaultMessage: "Drop PDF or ZIP files here to upload your documents", acceptedFiles: "application/pdf,application/x-pdf,.zip", diff --git a/assets/init.js b/assets/init.js index 03a97b2a..bc6a990a 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1,5 +1,6 @@ import { onDomReady } from '@minvws/manon/utils.js'; onDomReady(function () { + document.documentElement.classList.remove('no-js'); document.documentElement.classList.add('js'); }); \ No newline at end of file diff --git a/assets/styles/admin/admin-base.scss b/assets/styles/admin/admin-base.scss index 7d468358..aa312653 100644 --- a/assets/styles/admin/admin-base.scss +++ b/assets/styles/admin/admin-base.scss @@ -1,204 +1,299 @@ +@use "@minvws/manon/collapsible"; +@use "@minvws/manon/layout-centered"; + @import "@minvws/manon/footer.scss"; @import "@minvws/manon/link.scss"; @import "@minvws/nl-rdo-rijksoverheid-ui-theme/scss/fonts/ro-icons/sets/base.scss"; @import "@minvws/manon/icon.scss"; +@import "@minvws/manon/button-icon.scss"; +@import "@minvws/manon/visually-hidden.scss"; html * { - box-sizing: border-box; + box-sizing: border-box; } body { - background: $gray-main-bg; - font-family: $font-sans; - line-height: $base-line-height; - margin: 0; - - > header { - align-items: center; - background: $gray-dark; - color: $white; + background: $gray-main-bg; + font-family: $font-sans; + line-height: $base-line-height; + margin: 0; display: flex; flex-direction: column; - flex-shrink: 0; - font-family: $font-sans-italic; - font-size: 22px; - height: 80px; - justify-content: center; - } + min-height: 100vh; - footer { - position: fixed; - top: 100vh; - width: 100%; - transform: translateY(-100%); + > header > nav { + background: $gray-dark; + color: $white; + font-family: $font-sans-italic; + font-size: 22px; + height: 80px; + } + + > header > nav > .nav__container { + width: 100%; + height: 100%; + max-width: 1152px; + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + margin: 0 auto; + } + + > header .header__profile { + position: relative; + + ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + align-items: center; + height: 80px; + } + li { + height: 100%; + &:hover, + &:focus { + background: $gray-535353; + } + &:first-child a { + font-family: $font-sans; + } + } + a, span { + text-decoration: none; + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + color: $white; + font-family: $font-sans-bold; + padding: 0 16px; + } + span { + font-family: $font-sans; + } + } + + > header > .header__content { + @include default-big-container; + @include center; + display: flex; + justify-content: space-between; + margin-top: 64px; + margin-bottom: 24px; + } + + > header h1 { + margin: 0; + } + + main { + flex: 1; + + > .content { + margin: 4rem auto 0; + padding: $main-padding; + @include default-big-container; + + &__small { + max-width: $small-content-container; + } + } + } + + footer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + margin-bottom: 32px; + + svg { + margin-bottom: 18px; + + path { + fill: #999; + } + } - ul { - display: flex; - list-style: none; - margin: 0; + ul { + display: flex; + list-style: none; + margin: 0; - li { - margin: 0 9px; - line-height: 1.5; + li { + margin: 0 9px; + line-height: 1.5; - a { - font-size: 0.875rem; - line-height: 1; + a { + font-size: 0.875rem; + line-height: 1; + } + } } - } } - } +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: $font-sans-bold; + font-weight: normal; } strong { - font-family: $font-sans-bold; - font-weight: normal; - font-style: normal; + font-family: $font-sans-bold; + font-weight: normal; + font-style: normal; +} + +input, +textarea { + font-size: 1em; } /* Input */ input[type="text"], input[type="email"], +input[type="date"], input[type="password"], input[type="submit"], -button[type="submit"] { - border-radius: 8px; - color: $color-input; - display: flex; - font-size: 1rem; - line-height: 24px; - padding: 10px 14px; +button[type="submit"], +select, +textarea { + border-radius: 8px; + color: $color-input; + display: flex; + font-family: $font-sans; + line-height: 24px; + padding: 10px 14px; } /* Buttons */ input[type="submit"], button[type="submit"] { - justify-content: center; - color: $white; - font-family: $font-sans; - font-weight: 700; - margin-top: 32px; - margin-bottom: 16px; - line-height: 1.25rem; + justify-content: center; + color: $white; + font-family: $font-sans; + font-weight: 700; + margin-top: 32px; + margin-bottom: 16px; + line-height: 1.25rem; - border: 1px solid $color-button-border; - background: var(--notification-explanation-intense-default, #007BC7); - box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); + border: 1px solid $color-button-border; + background: var(--notification-explanation-intense-default, #007bc7); + box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); } /* Text fields */ +input[type="date"], input[type="text"], input[type="email"], -input[type="password"] { - border-radius: 8px; - border: 1px solid var(--form-input-border-color); - background: $white; +input[type="password"], +select, +textarea { + border-radius: 8px; + border: 1px solid var(--form-input-border-color); + background: $white; - /* Shadow/xs */ - box-shadow: $shadow-xs; + /* Shadow/xs */ + box-shadow: $shadow-xs; } label { - font-size: 0.875rem; - margin-top: 16px; -} - -main { - margin: 4rem auto 0; - background: $white; - padding: $main-padding; + font-size: 0.875rem; + margin-top: 16px; } table { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } th { - padding: 0.75rem 1.5rem; - border: 1px solid $color-th-border; - background: $color-th-background; - color: $color-th-text; - font-family: $font-sans-italic; - font-size: 1rem; - font-style: italic; - font-weight: 400; - line-height: 18px; - white-space: nowrap; - text-align: left; - - &:first-child { - border-left: none; - } - &:last-child { - border-right: none; - } - - a { - align-items: center; - color: inherit; - display: flex; - gap: $default-gap; - text-decoration: none; - height: 20px; - line-height: 20px; + padding: 0.75rem 1.5rem; + border: 1px solid $color-th-border; + background: $color-th-background; + color: $color-th-text; + font-family: $font-sans-italic; + font-size: 1rem; + font-style: italic; + font-weight: 400; + line-height: 18px; + white-space: nowrap; + text-align: left; - &:hover { - color: $color-th-text-hover; + &:first-child { + border-left: none; + } + &:last-child, + &:nth-last-child(2) { + border-right: none; + border-left: none; + } + + a { + align-items: center; + color: inherit; + display: flex; + gap: $default-gap; + text-decoration: none; + height: 20px; + line-height: 20px; + + &:hover { + color: $color-th-text-hover; + } } - } } tr { - border-bottom: 1px solid $color-tr-border; - - &:hover { - background: $color-tr-hover; - } + border-bottom: 1px solid $color-tr-border; + + &:hover { + background: $color-tr-hover; + } } td { - color: $color-td; - font-size: 0.875rem; - padding: 1rem 1.5rem; - line-height: 1em; + color: $color-td; + font-size: 0.875rem; + padding: 1rem 1.5rem; + line-height: 1em; - span { - min-height: 40px; - display: flex; - align-items: center; - } + span { + min-height: 40px; + display: flex; + align-items: center; + } - height: 72px; + height: 72px; } .content { - background: $white; + background: $white; } -.icon { - font-family: $font-icon; - font-size: 1.125rem; - color: inherit; - position: static; - display: inline-flex; - justify-content: center; - align-items: center; - margin-right: 0.5rem; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; /* Firefox */ - font-smoothing: antialiased; +::placeholder { + /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: $gray-999; + opacity: 1; + /* Firefox */ } -::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ - color: $gray-999; - opacity: 1; /* Firefox */ +::-ms-input-placeholder { + /* Internet Explorer 10-11 */ + color: $gray-999; } -::-ms-input-placeholder { /* Internet Explorer 10-11 */ - color: $gray-999; +::-ms-input-placeholder { + /* Microsoft Edge */ + color: $gray-999; } - -::-ms-input-placeholder { /* Microsoft Edge */ - color: $gray-999; -} \ No newline at end of file diff --git a/assets/styles/admin/admin-decision-link.scss b/assets/styles/admin/admin-decision-link.scss index d2c645e6..8b018985 100644 --- a/assets/styles/admin/admin-decision-link.scss +++ b/assets/styles/admin/admin-decision-link.scss @@ -1,24 +1,27 @@ .decision { - &__case-number { - color: #535353; - font-size: 16px; - white-space: nowrap; - padding-right: 85px; - } + &__case-number { + color: #535353; + font-size: 16px; + white-space: nowrap; + padding-right: 85px; - &__detail-link { - text-decoration: none; - display: flex; - justify-content: center; - align-items: center; + &:after { + color: #999999; + width: 8px; + height: 14px; + padding: 0; + content: ''; + background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjE0IiB2aWV3Qm94PSIwIDAgOCAxNCIgZmlsbD0ibm9uZSI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wLjI5Mjg5MyAwLjI5Mjg5M0MwLjY4MzQxNyAtMC4wOTc2MzExIDEuMzE2NTggLTAuMDk3NjMxMSAxLjcwNzExIDAuMjkyODkzTDcuNzA3MTEgNi4yOTI4OUM4LjA5NzYzIDYuNjgzNDIgOC4wOTc2MyA3LjMxNjU4IDcuNzA3MTEgNy43MDcxMUwxLjcwNzExIDEzLjcwNzFDMS4zMTY1OCAxNC4wOTc2IDAuNjgzNDE3IDE0LjA5NzYgMC4yOTI4OTMgMTMuNzA3MUMtMC4wOTc2MzExIDEzLjMxNjYgLTAuMDk3NjMxMSAxMi42ODM0IDAuMjkyODkzIDEyLjI5MjlMNS41ODU3OSA3TDAuMjkyODkzIDEuNzA3MTFDLTAuMDk3NjMxMSAxLjMxNjU4IC0wLjA5NzYzMTEgMC42ODM0MTcgMC4yOTI4OTMgMC4yOTI4OTNaIiBmaWxsPSIjOTk5OTk5Ii8+Cjwvc3ZnPg==) no-repeat; + position: absolute; + right: 30px; + } + } - &:after { - color: #999999; - width: 8px; - height: 14px; - padding: 0; - content: ''; - background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjE0IiB2aWV3Qm94PSIwIDAgOCAxNCIgZmlsbD0ibm9uZSI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wLjI5Mjg5MyAwLjI5Mjg5M0MwLjY4MzQxNyAtMC4wOTc2MzExIDEuMzE2NTggLTAuMDk3NjMxMSAxLjcwNzExIDAuMjkyODkzTDcuNzA3MTEgNi4yOTI4OUM4LjA5NzYzIDYuNjgzNDIgOC4wOTc2MyA3LjMxNjU4IDcuNzA3MTEgNy43MDcxMUwxLjcwNzExIDEzLjcwNzFDMS4zMTY1OCAxNC4wOTc2IDAuNjgzNDE3IDE0LjA5NzYgMC4yOTI4OTMgMTMuNzA3MUMtMC4wOTc2MzExIDEzLjMxNjYgLTAuMDk3NjMxMSAxMi42ODM0IDAuMjkyODkzIDEyLjI5MjlMNS41ODU3OSA3TDAuMjkyODkzIDEuNzA3MTFDLTAuMDk3NjMxMSAxLjMxNjU4IC0wLjA5NzYzMTEgMC42ODM0MTcgMC4yOTI4OTMgMC4yOTI4OTNaIiBmaWxsPSIjOTk5OTk5Ii8+Cjwvc3ZnPg==) no-repeat; + &__detail-link { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; } - } -} \ No newline at end of file +} diff --git a/assets/styles/admin/admin-decisions.scss b/assets/styles/admin/admin-decisions.scss index 83055c6c..341cd83c 100644 --- a/assets/styles/admin/admin-decisions.scss +++ b/assets/styles/admin/admin-decisions.scss @@ -1,44 +1,22 @@ @import "admin-decision-link"; .admin-decisions { - main { - width: 100%; - max-width: 1280px; + > .content { + margin-top: 0; } - .form-search-decisions { - padding: 12px 16px; - display: flex; - justify-content: flex-end; + + footer { + margin-top: 160px; } +} - .search-decisions { - display: flex; - - &__input { - display: flex; - width: 424px; - padding: 10px 14px; - align-items: center; - gap: 8px; - - &:placeholder-shown { - text-overflow: ellipsis; - } +.decisions { + tr { + position: relative; } - &__button { - border: none; - color: #999; - background: none; - margin-left: -39px; + td:first-child { + display: flex; + align-items: center; } - } -} - -.decisions { - td:first-child { - display: flex; - align-items: center; - } } diff --git a/assets/styles/admin/admin-signin.scss b/assets/styles/admin/admin-signin.scss index eb9c959f..e3200146 100644 --- a/assets/styles/admin/admin-signin.scss +++ b/assets/styles/admin/admin-signin.scss @@ -1,46 +1,60 @@ .form-signin { - max-width: 360px; - background: $white; - padding: 24px; - margin: 64px auto; - display: flex; - flex-direction: column; - - h1 { - margin-top: 0; - margin-bottom: 16px; - font-size: 28px; - font-family: $font-sans; - } - - [type="submit"] { - border-radius: 8px; - width: 100%; - - /* Shadow/xs */ - box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); - } - - label { - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; - color: $gray-neutral; - margin-bottom: 6px; - } - - p { - line-height: 28px; - margin: 0.5rem 0; - } - - .text-help-primary { - color: $color-helptext-primary; - font-size: 1.125rem; - } - - .text-help-secondary { - color: $color-helptext-secondary; - } -} \ No newline at end of file + max-width: 360px; + background: $white; + padding: 24px; + margin: 64px auto; + display: flex; + flex-direction: column; + + #_auth_code { + margin-bottom: 30px; + } + + h1 { + margin-top: 0; + margin-bottom: 16px; + font-size: 28px; + font-family: $font-sans; + } + + [type="submit"] { + background: var(--notification-explanation-intense-default, #007bc7); + border: none; + color: white; + margin-bottom: 20px; + border-radius: 8px; + width: 100%; + + /* Shadow/xs */ + box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05); + + &:hover, + &:focus { + background: #01689b; + color: white; + } + } + + label { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + color: $gray-neutral; + margin-bottom: 6px; + } + + p { + line-height: 28px; + margin: 0.5rem 0; + } + + .text-help-primary { + color: $color-helptext-primary; + font-size: 1.125rem; + } + + .text-help-secondary { + color: $color-helptext-secondary; + } +} diff --git a/assets/styles/admin/admin-variables.scss b/assets/styles/admin/admin-variables.scss index 45075067..3c958900 100644 --- a/assets/styles/admin/admin-variables.scss +++ b/assets/styles/admin/admin-variables.scss @@ -2,64 +2,72 @@ $ro-font-path: "~@minvws/nl-rdo-rijksoverheid-ui-theme/fonts"; /* Regular */ @font-face { - font-family: "RO Sans Web"; - font-weight: normal; - font-style: normal; - font-display: swap; - src: url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Regular.woff2") format("woff2"), - url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Regular.woff") format("woff"), - url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Regular.ttf") format("truetype"); + font-family: "RO Sans Web"; + font-weight: normal; + font-style: normal; + font-display: swap; + src: url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Regular.woff2") + format("woff2"), + url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Regular.woff") + format("woff"), + url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Regular.ttf") + format("truetype"); } /* Bold */ @font-face { - font-family: "RO Sans Web Bold"; - font-weight: bold; - font-style: normal; - font-display: swap; - src: url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Bold.woff2") format("woff2"), - url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Bold.woff") format("woff"), - url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Bold.ttf") format("truetype"); + font-family: "RO Sans Web Bold"; + font-weight: bold; + font-style: normal; + font-display: swap; + src: url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Bold.woff2") + format("woff2"), + url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Bold.woff") + format("woff"), + url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Bold.ttf") + format("truetype"); } /* Italic */ @font-face { - font-family: "RO Sans Web Italic"; - font-weight: normal; - font-style: italic; - font-display: swap; - src: url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Italic.woff2") format("woff2"), - url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Italic.woff") format("woff"), - url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Italic.ttf") format("truetype"); + font-family: "RO Sans Web Italic"; + font-weight: normal; + font-style: italic; + font-display: swap; + src: url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Italic.woff2") + format("woff2"), + url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Italic.woff") + format("woff"), + url("#{$ro-font-path}/ro-sans-web/RO-SansWebText-Italic.ttf") + format("truetype"); } /* Icon font */ @font-face { - font-family: "RO Icons"; - font-weight: normal; - font-style: normal; - src: url("#{$ro-font-path}/ro-icons/ro-icons-3.6.woff2") format("woff2"), - url("#{$ro-font-path}/ro-icons/ro-icons-3.6.woff") format("woff"), - url("#{$ro-font-path}/ro-icons/ro-icons-3.6.ttf") format("truetype"); + font-family: "RO Icons"; + font-weight: normal; + font-style: normal; + src: url("#{$ro-font-path}/ro-icons/ro-icons-3.6.woff2") format("woff2"), + url("#{$ro-font-path}/ro-icons/ro-icons-3.6.woff") format("woff"), + url("#{$ro-font-path}/ro-icons/ro-icons-3.6.ttf") format("truetype"); } /* Admin colors */ -$gray-light: #CCCCCC; -$gray-blue-x-light: #D0D5DD; +$gray-light: #cccccc; +$gray-blue-x-light: #d0d5dd; $gray-blue-semi-transparent: #08558566; -$gray-dark: #1D1D1D; -$gray-main-bg: #F3F3F3; +$gray-dark: #1d1d1d; +$gray-main-bg: #f3f3f3; $gray-neutral: #696969; $gray-535353: #535353; $gray-667085: #667085; -$gray-F9FAFB: #F9FAFB; +$gray-F9FAFB: #f9fafb; $gray-999: #999999; -$white: #FFFFFF; +$white: #ffffff; $black: #000000; - /* links */ -$link-default: #01689B; +$link-default: #01689b; $link-hover: #004161; /* fonts */ @@ -78,24 +86,47 @@ $color-input: $gray-667085; $color-td: $gray-535353; $color-input-border: $gray-blue-x-light; $color-button-border: $gray-blue-semi-transparent; -$color-th-border: #E6E6E6; -$color-th-background: #F3F3F3; +$color-th-border: var(--gray-2); +$color-th-background: #f3f3f3; $color-th-text: #475467; $color-th-text-hover: #344054; -$color-tr-border: #E6E6E6; -$color-tr-hover: #E5F0F9; +$color-tr-border: var(--gray-2); +$color-tr-hover: #e5f0f9; $color-status-concept: var(--notification-warning-intense-default); $color-status-ready: var(--notification-explanation-intense-default); -$color-status-online: var(--ro-basic-set-orange); +$color-status-online: var(--succes-700); $color-status-public: var(--succes-700); $color-status-retracted: var(--notification-error-intense-default); $color-chevron-right: #999999; +$color-tab-active: $gray-535353; /* metrics */ $base-line-height: calc(26 / 16) * 1em; $input-line-height: calc(24 / 16) * 1em; $main-padding: 1.5rem; $default-gap: 8px; +$default-container-padding: 24px 16px; +$default-container-padding-nth-1: 0 16px 24px 16px; +$small-content-container: 808px; /* shadows */ -$shadow-xs: 0 1px 2px 0 rgba(16, 24, 40, 0.05); \ No newline at end of file +$shadow-xs: 0 1px 2px 0 rgba(16, 24, 40, 0.05); + +@mixin default-big-container { + width: 100%; + max-width: 1280px; +} + +@mixin default-small-container { + width: 100%; + max-width: $small-content-container; +} + +@mixin center { + margin-left: auto; + margin-right: auto; +} + +@mixin clear-padding { + padding: 0; +} diff --git a/assets/styles/admin/admin.scss b/assets/styles/admin/admin.scss index e8dc4045..691b5b70 100644 --- a/assets/styles/admin/admin.scss +++ b/assets/styles/admin/admin.scss @@ -1,23 +1,31 @@ +@import "admin-tailwind"; + //@import "@minvws/nl-rdo-rijksoverheid-ui-theme/scss/main"; -@import "admin-variables.scss"; -@import "admin-base.scss"; -@import "admin-badge.scss"; -@import "admin-signin.scss"; -@import "admin-decisions.scss"; +@import "admin-variables"; +@import "admin-base"; +@import "admin-components/all"; +@import "admin-badge"; +@import "admin-signin"; +@import "admin-search-form"; +@import "admin-decisions"; +@import "admin-decisions-new"; /* Manon overrides */ :root { - --footer-flex-direction: row; - --footer-min-height: 56px; - --link-text-color: #01689B; - --link-hover-text-color: #004161; - --link-text-decoration: underline; - --form-input-border-color: #ccc; - --icon-font-family: #{$font-icon}; - --button-icon-only-font-family: #{$font-icon}; - --succes-700: #027A48; - --notification-error-intense-default: #D52A1E; - --notification-warning-intense-default: #FFB71A; - --notification-explanation-intense-default: #007BC7; - --ro-basic-set-orange: #E17000; + --gray-2: #E6E6E6; + --ro-basic-set-orange: #E17000; + --footer-flex-direction: row; + --footer-min-height: 56px; + --link-text-color: #01689B; + --link-hover-text-color: #004161; + --link-text-decoration: underline; + --form-input-border-color: #ccc; + --icon-font-family: #{$font-icon}; + --icon-font-size: 1rem; + --succes-700: #027A48; + --notification-error-intense-default: #D52A1E; + --notification-warning-intense-default: #814081; + --notification-explanation-intense-default: #007BC7; + --color-step-default: var(--gray-2); + --color-step-active: var(--notification-explanation-intense-default); } diff --git a/assets/styles/components/all.scss b/assets/styles/components/all.scss index c873041b..5a918224 100644 --- a/assets/styles/components/all.scss +++ b/assets/styles/components/all.scss @@ -1,11 +1,15 @@ +@import "./colors.scss"; @import "./arrowed-link"; @import "./block-link"; -@import "./breadcrumbs"; @import "./button"; +@import "./content-container"; @import "./footer"; @import "./form-filters"; @import "./form"; @import "./gallery"; +@import "./grid"; +@import "./headings-variables"; +@import "./headings"; @import "./hero"; @import "./link"; @import "./list-densed"; @@ -16,11 +20,30 @@ @import "./logo"; @import "./main-navigation"; @import "./navigation-link"; +@import "./notification"; @import "./pagination"; +@import "./pipe-after"; @import "./pill"; +@import "./result-highlight"; @import "./reverse-underline"; @import "./search-form"; @import "./search-results"; @import "./section"; @import "./split-link"; @import "./table"; +@import "./tabs-variables"; +@import "./tabs"; +@import "./header"; +@import "./icons"; +@import "./link-visited"; +@import "./link-hover"; +@import "./link-focus"; +@import "./link"; +@import "./link-variables"; +@import "./breadcrumb-bar"; +@import "./button-base"; +@import "./form-base"; +@import "./form-select"; +@import "./header-navigation"; +@import "./header-navigation-link"; +@import "./form-select"; diff --git a/assets/styles/components/arrowed-link.scss b/assets/styles/components/arrowed-link.scss index 96d1dff7..5cf53f78 100644 --- a/assets/styles/components/arrowed-link.scss +++ b/assets/styles/components/arrowed-link.scss @@ -1,16 +1,26 @@ .arrowed-link { - padding-left: 1.25rem; position: relative; - &:before { - background: url('../../img/chevron-right.svg') no-repeat center center; - bottom: 0; - content: ''; - display: block; - left: 0; - padding: 0; - position: absolute; - top: 0; - width: 12px; + &:before, + &:hover:before { + content: var(--icon-chevron-right); + align-items: flex-start; + display: inline-flex; + height: 100%; + justify-self: center; + line-height: 1; + text-decoration-color: transparent; + font-size: 0.8rem; + transform: translateY(-2px); } -} \ No newline at end of file + + &.split-link { + &:before, + &:hover:before { + position: absolute; + left: -1.5rem; + top: 0.2rem; + transform: none; + } + } +} diff --git a/assets/styles/components/block-link.scss b/assets/styles/components/block-link.scss index 28de17d7..b5c2604e 100644 --- a/assets/styles/components/block-link.scss +++ b/assets/styles/components/block-link.scss @@ -1,39 +1,31 @@ -.block-link { +a.block-link { display: block; + color: var(--text-set-text-color); + text-decoration: none; - h2, h3 { - margin-bottom: .5rem; - text-decoration: none; + margin-bottom: 0.5rem; + color: var(--headings-text-color); + font-size: var(--h3-font-size); + font-weight: var(--text-set-strong-font-weight); } - &, &:focus, &:hover { color: var(--text-set-text-color); text-decoration: none; - h2 { - font-size: var(--h2-font-size, inherit); - font-weight: var(--text-set-strong-font-weight); - } - - h3 { - font-size: var(--h3-font-size, inherit); - font-weight: var(--text-set-strong-font-weight); - } - } - - &:focus, - &:hover { - - h2, h3 { + color: var(--headings-hover-text-color); text-decoration: underline; } } &:visited { color: var(--text-set-text-color); + + h3 { + color: var(--headings-visited-text-color, inherit); + } } -} \ No newline at end of file +} diff --git a/assets/styles/components/button.scss b/assets/styles/components/button.scss index e030ca14..69b0b8c3 100644 --- a/assets/styles/components/button.scss +++ b/assets/styles/components/button.scss @@ -1,12 +1,63 @@ .button.ghost:visited { - .icon::before { color: var(--button-ghost-text-color); } &:hover { + text-decoration: none; + .icon:before { - color: #fff; + color: white; + } + } +} + +.button.download-all { + background: transparent; + border-width: 3px; + font-weight: bold; + width: 210px; + + span { + color: var(--button-base-background-color); + + &:before { + filter: invert(16%) sepia(100%) saturate(3748%) hue-rotate(321deg) brightness(76%) contrast(112%); + } + } + + &:focus, + &:hover { + background: var(--button-base-background-color); + border-color: var(--button-base-background-color); + + span { + color: white; + + &:before { + filter: invert(100%) sepia(0%) saturate(7470%) hue-rotate(116deg) brightness(109%) contrast(109%); + } + } + } +} + +.button { + &.button-pink { + .svg-icon:before { + filter: invert(100%) sepia(0%) saturate(7500%) hue-rotate(106deg) brightness(106%) contrast(100%); + } + + &:hover { + background: transparent; + color: var(--ro-blue); + + span { + color: var(--ro-blue); + + &:before { + filter: invert(21%) sepia(97%) saturate(2139%) hue-rotate(182deg) brightness(92%) contrast(99%); + } + } } } } \ No newline at end of file diff --git a/assets/styles/components/form-filters.scss b/assets/styles/components/form-filters.scss index 1be770e3..d8dea363 100644 --- a/assets/styles/components/form-filters.scss +++ b/assets/styles/components/form-filters.scss @@ -1,22 +1,90 @@ .form-filters { background-color: transparent; + display: block; line-height: 1.25rem; padding: 0; + fieldset { + margin-bottom: 1rem; + } + legend { - margin-bottom: .25rem; + margin-bottom: 0.25rem; padding-left: 0; + width: 100%; + } + + input { + font-size: 1rem; } .checkbox { - margin: .3rem 0; + margin: 0.3rem 0; - input[type=checkbox] { - margin-top: 0; + input[type="checkbox"] { + border-color: #696969; + border-radius: 2px; + border-width: 1px; + margin-top: 0.125rem; } } label { font-size: 1rem; } -} \ No newline at end of file +} + +.toggle-filters-group-button { + padding: 0.5rem 0; + position: relative; + text-align: left; + width: 100%; + + &, + &:hover, + &:focus { + background: transparent; + border-color: #d7d6d6; + border-width: 0 0 1px; + color: var(--form-base-text-color); + } + + img { + margin-top: -0.25rem; + position: absolute; + right: 0; + transform: rotate(0deg); + } + + &.toggle-button--with-animation { + img { + transition: 0.25s ease-in-out transform; + } + } + + &.toggle-button--collapsed { + img { + transform: rotate(180deg); + } + } +} + +.toggle-filter-items-button { + font-size: 14px; + padding-left: 0; + padding-right: 0; + text-align: left; + + &, + &:hover, + &:focus { + background: transparent; + border: none; + color: var(--link-text-color); + } +} + +.filters-collapsible { + height: auto; + transition: 0.25s ease-in-out height; +} diff --git a/assets/styles/components/form.scss b/assets/styles/components/form.scss index b3e46816..fa92a173 100644 --- a/assets/styles/components/form.scss +++ b/assets/styles/components/form.scss @@ -1,3 +1,5 @@ :root { --form-base-background-color: transparent; + --form-fieldset-checkbox-height: 1rem; + --form-fieldset-checkbox-width: 1rem; } \ No newline at end of file diff --git a/assets/styles/components/gallery.scss b/assets/styles/components/gallery.scss index 6cab2b3d..e78f37c2 100644 --- a/assets/styles/components/gallery.scss +++ b/assets/styles/components/gallery.scss @@ -1,20 +1,52 @@ .gallery { background: #f3f3f3; - border-radius: .75rem; + display: block; padding: 1rem; } .gallery__container { display: flex; flex-direction: row; + gap: 1.5rem; overflow-x: auto; } -.gallery__item { +.gallery__image { display: block; + max-width: none; + margin-block-start: 2px; /* to fix focus */ + margin-block-end: 1rem; + width: auto; + + box-shadow: + 0px 4px 16px 0px rgba(0, 0, 0, 0.25), + 0px 2px 4px 0px rgba(0, 0, 0, 0.25); } -.gallery__image { +.gallery__image--no-thumbnail { + box-shadow: none; +} + +.skiplink-carousel-down, +.skiplink-carousel-up { display: block; - max-width: none; -} \ No newline at end of file + min-height: 0; + font-size: 0; + padding: 0; + border: 0; + max-width: 100%; + + &:focus { + height: auto; + font-size: var(--application-font-size); + min-height: var(--skip-to-content-min-height); + } +} + +#above-carousel:target .skiplink-carousel-up { + display: none; +} + +#below-carousel:target .skiplink-carousel-down { + display: none; +} diff --git a/assets/styles/components/link.scss b/assets/styles/components/link.scss index c67ce00f..be3f0665 100644 --- a/assets/styles/components/link.scss +++ b/assets/styles/components/link.scss @@ -1,26 +1,17 @@ -:root { - --link-focus-font-size: inherit; - --link-focus-font-weight: inherit; - --link-focus-line-height: inherit; +/*---------------------------------------------------------------*/ +/*--------------------------- link.scss -------------------------*/ +/*---------------------------------------------------------------*/ +@use "link-variables"; +@use "mixins/link"; +@use "mixins/outline"; - --link-hover-font-size: inherit; - --link-hover-font-weight: inherit; - --link-hover-line-height: inherit; - --link-hover-text-decoration: none; +a { + cursor: pointer; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; - --link-visited-line-height: inherit; + @include link.link("link-"); + @include link.link-elements-styling("link-"); + @include outline.outline("link-"); } - -h2>a { - color: var(--link-text-color); -} - -.link-green { - - &, - &:focus, - &:hover, - &:visited { - color: #39870c; - } -} \ No newline at end of file diff --git a/assets/styles/components/list-densed.scss b/assets/styles/components/list-densed.scss index 17ef92d0..6928ccb5 100644 --- a/assets/styles/components/list-densed.scss +++ b/assets/styles/components/list-densed.scss @@ -4,7 +4,9 @@ } li { - padding-bottom: 0; - padding-top: 0; + padding-bottom: .25rem; + padding-left: 1.5rem; + padding-top: .25rem; + line-height: 1.25; } } diff --git a/assets/styles/components/logo.scss b/assets/styles/components/logo.scss index 2f2c079f..8041a9dd 100644 --- a/assets/styles/components/logo.scss +++ b/assets/styles/components/logo.scss @@ -2,17 +2,23 @@ align-items: end; &, - &:hover { + &:hover, + &:focus, + &:active { line-height: 1; } + &:focus { + outline: var(--navigation-link-focus-outline); + outline-offset: -2px; + } + @media (width >=35rem) { max-width: 35rem; - } } .logo__text { display: block; - margin-bottom: .75rem; -} \ No newline at end of file + margin-bottom: 0.75rem; +} diff --git a/assets/styles/components/main-navigation.scss b/assets/styles/components/main-navigation.scss index 807ddb0b..d4736d5c 100644 --- a/assets/styles/components/main-navigation.scss +++ b/assets/styles/components/main-navigation.scss @@ -7,6 +7,9 @@ --breadcrumb-bar-link-active-text-color: #000; --breadcrumb-bar-link-focus-text-color: #000; --breadcrumb-bar-link-text-color: #000; + --breadcrumb-bar-content-wrapper-padding-left: 0; + --breadcrumb-bar-content-wrapper-padding-right: 0; + --breadcrumb-bar-icon: var(--icon-chevron-light-right); --div-content-wrapper-max-width: var(--content-max-width); --div-content-wrapper-padding-bottom: var(--content-padding-bottom); @@ -27,4 +30,26 @@ max-width: var(--div-content-wrapper-max-width); padding-left: var(--div-content-wrapper-padding-left); padding-right: var(--div-content-wrapper-padding-right); + + .one-third-two-thirds { + gap: inherit; + flex-direction: column; + + @media (max-width: $breakpoint-3) { + > div { + width: 100%; + &:first-of-type { + padding-top: 20px; + } + &:last-of-type { + padding-bottom: 20px; + } + } + } + + @media (min-width: $breakpoint-3) { + flex-direction: row; + gap: var(--layout-one-third-two-thirds-breakpoint-gap); + } + } } diff --git a/assets/styles/components/pagination.scss b/assets/styles/components/pagination.scss index 4412d63e..8890e7a1 100644 --- a/assets/styles/components/pagination.scss +++ b/assets/styles/components/pagination.scss @@ -1,13 +1,71 @@ +/*---------------------------------------------------------------*/ +/*---------------------- pagination.scss ------------------------*/ +/*---------------------------------------------------------------*/ + :root { + --pagination-link-border-width: 0 0 3px 0; --pagination-border-color: #e6e6e6; --pagination-border-width: 1px 0 0; } +%pagination-list-styling { + li { + span.svg-icon { + width: 30px; + height: 30px; + &:before { + display: inline-block; + content: ""; + width: 100%; + height: 100%; + } + } + } +} + +nav.pagination, +.pagination { + display: flex; + justify-content: var(--pagination-justify-content); + align-items: var(--pagination-align-items); + width: 100%; + border-width: var(--pagination-border-width); + border-style: var(--pagination-border-style); + border-color: var(--pagination-border-color); + padding-top: var(--pagination-padding-top); + gap: var(--pagination-gap); + + ul { + @extend %pagination-list-styling; + } + + .adjacent { + @extend %pagination-adjacent-styling; + } +} + +ul.pagination { + @extend %pagination-list-styling; +} + .pagination { ul { justify-content: center; padding-top: 1rem; width: 100%; + + span:hover { + background-color: transparent !important; + } + } + + @media (max-width: 42rem) { + li:nth-child(3), + li:nth-child(4), + li:nth-child(5), + li:nth-child(6) { + display: none; + } } .disabled:hover { @@ -20,7 +78,11 @@ &:hover { background-color: transparent; - text-decoration: underline; + // text-decoration: underline; } } -} \ No newline at end of file + + .active:not(:hover, :focus) { + outline: 0; + } +} diff --git a/assets/styles/components/pill.scss b/assets/styles/components/pill.scss index 0db26c91..f0567774 100644 --- a/assets/styles/components/pill.scss +++ b/assets/styles/components/pill.scss @@ -1,16 +1,25 @@ %pill { background-color: #f3f3f3; + border: none; color: #344054; display: inline-block; - padding: 0.25rem 1rem; + padding: 0.5rem 1rem; border-radius: 2rem; font-size: 1rem; text-decoration: none; } -.pill, -a.pill:focus, -a.pill:visited, -a.pill:hover { - @extend %pill; -} \ No newline at end of file +.pill { + + &, + &:hover, + &:focus, + &:visited { + @extend %pill; + } + + &:hover, + &:focus { + text-decoration: line-through; + } +} diff --git a/assets/styles/components/search-form.scss b/assets/styles/components/search-form.scss index 78bbdf18..e97e6cfd 100644 --- a/assets/styles/components/search-form.scss +++ b/assets/styles/components/search-form.scss @@ -2,7 +2,8 @@ form.search-form { flex-direction: row; margin: 0 auto; padding: 1rem 0; - z-index: 0; + position: relative; + z-index: 1; button { align-self: auto; @@ -40,6 +41,24 @@ form.search-form { position: static !important; } } + + .search-form__suggestions { + background-color: #fff; + border: 1px solid #eee; + padding: 0 1rem; + left: 0; + position: absolute; + right: 0; + top: 70px; + + &:empty { + display: none; + } + + .list-results { + padding-bottom: 0; + } + } } form.search-form--hero { @@ -53,4 +72,11 @@ form.search-form--hero { margin-top: -56px; padding: var(--form-base-padding-top) var(--form-base-padding-right); } -} \ No newline at end of file +} + +[role="search"].half-width { + margin-inline-start: auto; + margin-inline-end: auto; + width: 100%; + max-width: 30rem; +} diff --git a/assets/styles/components/split-link.scss b/assets/styles/components/split-link.scss index dc37228c..e882e78c 100644 --- a/assets/styles/components/split-link.scss +++ b/assets/styles/components/split-link.scss @@ -1,20 +1,26 @@ -.split-link__underline { - text-decoration: underline; -} - .split-link { - text-decoration: none; - @media (min-width: 42rem) { - align-items: baseline; - display: flex; + &, + &:hover, + &:focus, + &:visited { + text-decoration: none; + } - .truncate { - max-width: 80%; + &:hover, + &:focus { + .split-link__underline { + text-decoration: underline; } + } + @media (min-width: 42rem) { .split-link__suffix { - margin-left: 0.5rem; + margin-left: 0.25rem; } } -} \ No newline at end of file + + span { + hyphens: none; + } +} diff --git a/assets/styles/components/table.scss b/assets/styles/components/table.scss index 70c0bd8c..65a84c9a 100644 --- a/assets/styles/components/table.scss +++ b/assets/styles/components/table.scss @@ -16,7 +16,31 @@ table { } } - tbody th { + th:first-of-type { + padding-inline-start: 0; + } + + tbody th, + tbody td { vertical-align: top; + white-space: normal; + } + + tbody td.icon-type:has(.svg-icon) { + padding-top: 13px; } + + .th-date { + min-width: 25ch; + } +} + +.table-default__thead { + background: unset; + color: black; + border: 0 solid var(--table-cell-border-color); + border-width: 0 0 1px; + font-style: italic; + font-weight: 300; + vertical-align: top; } \ No newline at end of file diff --git a/assets/styles/helpers.scss b/assets/styles/helpers.scss index 4213865f..da1a60aa 100644 --- a/assets/styles/helpers.scss +++ b/assets/styles/helpers.scss @@ -20,21 +20,85 @@ a.ro-font-bold:hover { gap: 0; } -@media (min-width: 42rem) { - .ro-truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - .ro-width-full { width: 100%; } .js { - // .results in selector: js. .js\:visually-hidden + + // .results in selector: .js\:visually-hidden & &\:visually-hidden { @extend .visually-hidden; } + + & &\:hidden { + @extend .hidden; + } +} + +.no-js { + + // .results in selector: .js\:visually-hidden + & &\:hidden { + @extend .hidden; + } +} + +.width-delimiter { + max-width: 70ch; + -webkit-hyphens: none; + hyphens: none; + word-break: none; +} + +.nota-bene:is(dl) { + >div { + background: none !important; + display: block !important; + } + + :is(dd, dt) { + display: inline; + color: var(--grey-6); + } +} + +a[href^="mailto"], +.content-container a[href^="https://"] { + + &, + &:focus, + &:hover { + &::before { + box-sizing: border-box; + font-size: 0; + height: 24px; + width: 24px; + } + } + + &::before { + background: no-repeat left -5px; + background-size: contain; + margin-right: 4px; + } +} + +a[href^="mailto"]::before { + background-image: url("../svg/sendusmail.svg"); + content: "E-mail us \00a0 "; + + [lang="nl"] & { + content: "E-mail ons \00a0 "; + } +} + +.content-container a[href^="https://"]::before { + background-image: url("../svg/externallink.svg"); + background-position: left -4px; + content: " (external website) \00a0"; + + [lang="nl"] & { + content: "(externe website) \00a0 "; + } } diff --git a/assets/woopie.js b/assets/woopie.js index 0724c0ee..51b2f706 100644 --- a/assets/woopie.js +++ b/assets/woopie.js @@ -5,6 +5,5 @@ require('bootstrap/dist/js/bootstrap.bundle'); require('@fortawesome/fontawesome-free/css/all.min.css'); import "./alpine.js" // Alpine -import "./facet.js" // Faceted views -import "./carousel.js" // Carousel import "./dropzone.js" // Dropzone uploads +import "./inquiry.js" diff --git a/composer.json b/composer.json index 238d02cb..1e30af7d 100644 --- a/composer.json +++ b/composer.json @@ -7,19 +7,23 @@ "php": ">=8.1", "ext-ctype": "*", "ext-iconv": "*", - "ext-zip": "*", "ext-intl": "*", + "ext-zip": "*", + "aws/aws-sdk-php": "^3.279", "doctrine/annotations": "^2.0", - "doctrine/doctrine-bundle": "^2.9", + "doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-fixtures-bundle": "^3.4", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.14", "elasticsearch/elasticsearch": "^8.7", "endroid/qr-code": "^4.8", + "erichard/elasticsearch-query-builder": "^3.0@beta", "fakerphp/faker": "^1.21", + "indiehd/filename-sanitizer": "^0.1.0", "jaytaph/typearray": "^0.0", "knplabs/knp-paginator-bundle": "^6.2", "league/flysystem": "^3.0", + "league/flysystem-aws-s3-v3": "^3.15", "league/flysystem-bundle": "^3.1", "mhujer/breadcrumbs-bundle": "^1.5", "minvws/horsebattery": "^1.1", @@ -29,6 +33,7 @@ "phpoffice/phpspreadsheet": "^1.28", "phpstan/phpdoc-parser": "^1.20", "predis/predis": "^2.2", + "presta/sitemap-bundle": "^3.3", "scheb/2fa-backup-code": "^6.8", "scheb/2fa-bundle": "^6.8", "scheb/2fa-email": "^6.8", @@ -66,6 +71,7 @@ "symfony/webpack-encore-bundle": "^2.0", "symfony/yaml": "6.3.*", "twig/extra-bundle": "^2.12|^3.0", + "twig/intl-extra": "^3.7", "twig/string-extra": "^3.6", "twig/twig": "^2.12|^3.0" }, @@ -134,12 +140,13 @@ "psalm/plugin-symfony": "^5.0", "slevomat/coding-standard": "^8.11", "squizlabs/php_codesniffer": "^3.7", + "stefanocbt/phpdotenv-sync": "^1.2", "symfony/browser-kit": "6.3.*", "symfony/css-selector": "6.3.*", "symfony/debug-bundle": "6.3.*", "symfony/maker-bundle": "^1.0", "symfony/phpunit-bridge": "^6.3", "symfony/web-profiler-bundle": "6.3.*", - "vimeo/psalm": "^5.10" + "vimeo/psalm": "5.9" } } diff --git a/composer.lock b/composer.lock index a20ddffa..c18d306c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,157 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b5b68ed5284da7997a704aefb0edade0", + "content-hash": "8474b12f4b91e6e492cf4ecc104df214", "packages": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "2f1dc7b7eda080498be96a4a6d683a41583030e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/2f1dc7b7eda080498be96a4a6d683a41583030e9", + "reference": "2f1dc7b7eda080498be96a4a6d683a41583030e9", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.2" + }, + "time": "2023-07-20T16:49:55+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.280.2", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "d68b83b3bc39b70bf33e9b8b5166facbe3e4fe9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d68b83b3bc39b70bf33e9b8b5166facbe3e4fe9b", + "reference": "d68b83b3bc39b70bf33e9b8b5166facbe3e4fe9b", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.0.4", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.280.2" + }, + "time": "2023-09-01T18:06:10+00:00" + }, { "name": "bacon/bacon-qr-code", "version": "2.0.8", @@ -62,16 +211,16 @@ }, { "name": "dasprid/enum", - "version": "1.0.4", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f" + "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", - "reference": "8e6b6ea76eabbf19ea2bf5b67b98e1860474012f", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016", + "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016", "shasum": "" }, "require": { @@ -106,9 +255,9 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.4" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" }, - "time": "2023-03-01T18:44:03+00:00" + "time": "2023-08-25T16:18:39+00:00" }, { "name": "doctrine/annotations", @@ -281,16 +430,16 @@ }, { "name": "doctrine/collections", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "db8cda536a034337f7dd63febecc713d4957f9ee" + "reference": "3023e150f90a38843856147b58190aa8b46cc155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/db8cda536a034337f7dd63febecc713d4957f9ee", - "reference": "db8cda536a034337f7dd63febecc713d4957f9ee", + "url": "https://api.github.com/repos/doctrine/collections/zipball/3023e150f90a38843856147b58190aa8b46cc155", + "reference": "3023e150f90a38843856147b58190aa8b46cc155", "shasum": "" }, "require": { @@ -303,7 +452,7 @@ "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^4.22" + "vimeo/psalm": "^5.11" }, "type": "library", "autoload": { @@ -347,7 +496,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.1.2" + "source": "https://github.com/doctrine/collections/tree/2.1.3" }, "funding": [ { @@ -363,7 +512,7 @@ "type": "tidelift" } ], - "time": "2022-12-27T23:41:38+00:00" + "time": "2023-07-06T15:15:36+00:00" }, { "name": "doctrine/common", @@ -458,16 +607,16 @@ }, { "name": "doctrine/data-fixtures", - "version": "1.6.6", + "version": "1.6.7", "source": { "type": "git", "url": "https://github.com/doctrine/data-fixtures.git", - "reference": "4af35dadbfcf4b00abb2a217c4c8c8800cf5fcf4" + "reference": "ae4e845decbe177348fdbecd04331f4fb96aa301" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/4af35dadbfcf4b00abb2a217c4c8c8800cf5fcf4", - "reference": "4af35dadbfcf4b00abb2a217c4c8c8800cf5fcf4", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/ae4e845decbe177348fdbecd04331f4fb96aa301", + "reference": "ae4e845decbe177348fdbecd04331f4fb96aa301", "shasum": "" }, "require": { @@ -477,14 +626,14 @@ }, "conflict": { "doctrine/dbal": "<2.13", - "doctrine/orm": "<2.12", + "doctrine/orm": "<2.14", "doctrine/phpcr-odm": "<1.3.0" }, "require-dev": { "doctrine/coding-standard": "^11.0", "doctrine/dbal": "^2.13 || ^3.0", "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", - "doctrine/orm": "^2.12", + "doctrine/orm": "^2.14", "ext-sqlite3": "*", "phpstan/phpstan": "^1.5", "phpunit/phpunit": "^8.5 || ^9.5 || ^10.0", @@ -520,7 +669,7 @@ ], "support": { "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/1.6.6" + "source": "https://github.com/doctrine/data-fixtures/tree/1.6.7" }, "funding": [ { @@ -536,20 +685,20 @@ "type": "tidelift" } ], - "time": "2023-04-20T13:08:54+00:00" + "time": "2023-08-17T21:15:33+00:00" }, { "name": "doctrine/dbal", - "version": "3.6.5", + "version": "3.6.6", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "96d5a70fd91efdcec81fc46316efc5bf3da17ddf" + "reference": "63646ffd71d1676d2f747f871be31b7e921c7864" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/96d5a70fd91efdcec81fc46316efc5bf3da17ddf", - "reference": "96d5a70fd91efdcec81fc46316efc5bf3da17ddf", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/63646ffd71d1676d2f747f871be31b7e921c7864", + "reference": "63646ffd71d1676d2f747f871be31b7e921c7864", "shasum": "" }, "require": { @@ -565,10 +714,11 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.21", + "phpstan/phpstan": "1.10.29", "phpstan/phpstan-strict-rules": "^1.5", "phpunit/phpunit": "9.6.9", "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^5.4|^6.0", "symfony/console": "^4.4|^5.4|^6.0", @@ -632,7 +782,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.5" + "source": "https://github.com/doctrine/dbal/tree/3.6.6" }, "funding": [ { @@ -648,7 +798,7 @@ "type": "tidelift" } ], - "time": "2023-07-17T09:15:50+00:00" + "time": "2023-08-17T05:38:17+00:00" }, { "name": "doctrine/deprecations", @@ -699,16 +849,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.10.1", + "version": "2.10.2", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "f9d59c90b6f525dfc2a2064a695cb56e0ab40311" + "reference": "f28b1f78de3a2938ff05cfe751233097624cc756" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/f9d59c90b6f525dfc2a2064a695cb56e0ab40311", - "reference": "f9d59c90b6f525dfc2a2064a695cb56e0ab40311", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/f28b1f78de3a2938ff05cfe751233097624cc756", + "reference": "f28b1f78de3a2938ff05cfe751233097624cc756", "shasum": "" }, "require": { @@ -795,7 +945,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.10.1" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.10.2" }, "funding": [ { @@ -811,7 +961,7 @@ "type": "tidelift" } ], - "time": "2023-06-28T07:47:41+00:00" + "time": "2023-08-06T09:31:40+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", @@ -1415,16 +1565,16 @@ }, { "name": "doctrine/orm", - "version": "2.15.4", + "version": "2.16.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "f7e4b61459692f9b747f40696e6bf72080390f2d" + "reference": "17500f56eaa930f5cd14d765bc2cd851c7d37cc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/f7e4b61459692f9b747f40696e6bf72080390f2d", - "reference": "f7e4b61459692f9b747f40696e6bf72080390f2d", + "url": "https://api.github.com/repos/doctrine/orm/zipball/17500f56eaa930f5cd14d765bc2cd851c7d37cc0", + "reference": "17500f56eaa930f5cd14d765bc2cd851c7d37cc0", "shasum": "" }, "require": { @@ -1453,14 +1603,14 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.10.25", + "phpstan/phpstan": "~1.4.10 || 1.10.28", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.13.1" + "vimeo/psalm": "4.30.0 || 5.14.1" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1510,9 +1660,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.15.4" + "source": "https://github.com/doctrine/orm/tree/2.16.2" }, - "time": "2023-07-18T07:50:04+00:00" + "time": "2023-08-27T18:21:56+00:00" }, { "name": "doctrine/persistence", @@ -1784,16 +1934,16 @@ }, { "name": "elasticsearch/elasticsearch", - "version": "v8.8.2", + "version": "v8.9.0", "source": { "type": "git", "url": "git@github.com:elastic/elasticsearch-php.git", - "reference": "d249dcd6b6740ed39d89d7f16d59fadbefceef49" + "reference": "cbde0731140e1d6c4453fe8c41888fcdac426ddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/d249dcd6b6740ed39d89d7f16d59fadbefceef49", - "reference": "d249dcd6b6740ed39d89d7f16d59fadbefceef49", + "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/cbde0731140e1d6c4453fe8c41888fcdac426ddd", + "reference": "cbde0731140e1d6c4453fe8c41888fcdac426ddd", "shasum": "" }, "require": { @@ -1801,7 +1951,7 @@ "guzzlehttp/guzzle": "^7.0", "php": "^7.4 || ^8.0", "psr/http-client": "^1.0", - "psr/http-message": "^1.0 || ^2.0", + "psr/http-message": "^1.1 || ^2.0", "psr/log": "^1|^2|^3" }, "require-dev": { @@ -1833,25 +1983,25 @@ "elasticsearch", "search" ], - "time": "2023-06-08T07:57:43+00:00" + "time": "2023-08-07T14:53:59+00:00" }, { "name": "endroid/qr-code", - "version": "4.8.2", + "version": "4.8.4", "source": { "type": "git", "url": "https://github.com/endroid/qr-code.git", - "reference": "2436c2333a3931c95e2b96eb82f16f53143d6bba" + "reference": "a122b85d4a5a3257d471257a43ac3e5676a27ffe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/endroid/qr-code/zipball/2436c2333a3931c95e2b96eb82f16f53143d6bba", - "reference": "2436c2333a3931c95e2b96eb82f16f53143d6bba", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/a122b85d4a5a3257d471257a43ac3e5676a27ffe", + "reference": "a122b85d4a5a3257d471257a43ac3e5676a27ffe", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^2.0.5", - "php": "^8.0" + "php": "^8.1" }, "conflict": { "khanamiryan/qrcode-detector-decoder": "^1.0.6" @@ -1900,7 +2050,7 @@ ], "support": { "issues": "https://github.com/endroid/qr-code/issues", - "source": "https://github.com/endroid/qr-code/tree/4.8.2" + "source": "https://github.com/endroid/qr-code/tree/4.8.4" }, "funding": [ { @@ -1908,7 +2058,60 @@ "type": "github" } ], - "time": "2023-03-30T18:46:02+00:00" + "time": "2023-08-28T18:12:07+00:00" + }, + { + "name": "erichard/elasticsearch-query-builder", + "version": "3.0.2-beta", + "source": { + "type": "git", + "url": "https://github.com/erichard/elasticsearch-query-builder.git", + "reference": "100a28e18a1af7749662871e4547d49e90f02909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erichard/elasticsearch-query-builder/zipball/100a28e18a1af7749662871e4547d49e90f02909", + "reference": "100a28e18a1af7749662871e4547d49e90f02909", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4.10", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpunit/phpunit": "^9.5.19", + "rector/rector": "^0.12.17", + "symplify/easy-coding-standard": "^10.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Erichard\\ElasticQueryBuilder\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Erwan Richard", + "email": "erwan.richard@protonmail.com" + } + ], + "description": "Create elastic search query with a fluent interface", + "support": { + "issues": "https://github.com/erichard/elasticsearch-query-builder/issues", + "source": "https://github.com/erichard/elasticsearch-query-builder/tree/3.0.2-beta" + }, + "time": "2022-10-12T14:57:58+00:00" }, { "name": "ezyang/htmlpurifier", @@ -2041,22 +2244,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.7.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -2147,7 +2350,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.0" }, "funding": [ { @@ -2163,20 +2366,20 @@ "type": "tidelift" } ], - "time": "2023-05-21T14:04:53+00:00" + "time": "2023-08-27T10:20:53+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6" + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6", - "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", "shasum": "" }, "require": { @@ -2230,7 +2433,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.0" + "source": "https://github.com/guzzle/promises/tree/2.0.1" }, "funding": [ { @@ -2246,20 +2449,20 @@ "type": "tidelift" } ], - "time": "2023-05-21T13:50:22+00:00" + "time": "2023-08-03T15:11:55+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.5.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", "shasum": "" }, "require": { @@ -2346,7 +2549,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.5.0" + "source": "https://github.com/guzzle/psr7/tree/2.6.1" }, "funding": [ { @@ -2362,7 +2565,57 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:11:26+00:00" + "time": "2023-08-27T10:13:57+00:00" + }, + { + "name": "indiehd/filename-sanitizer", + "version": "v0.1.0", + "source": { + "type": "git", + "url": "https://github.com/indiehd/filename-sanitizer.git", + "reference": "e3e3dd75ba318792bda2d167b823133c5def773e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indiehd/filename-sanitizer/zipball/e3e3dd75ba318792bda2d167b823133c5def773e", + "reference": "e3e3dd75ba318792bda2d167b823133c5def773e", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpro/grumphp": "^0.14.3", + "phpunit/phpunit": "^7" + }, + "type": "library", + "autoload": { + "psr-4": { + "IndieHD\\FilenameSanitizer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "indieHD, LLC", + "email": "webmaster@indiehd.com", + "homepage": "https://indiehd.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/indiehd/filename-sanitizer/graphs/contributors" + } + ], + "description": "A lightweight library for sanitizing strings to be used as filenames.", + "support": { + "issues": "https://github.com/indiehd/filename-sanitizer/issues", + "source": "https://github.com/indiehd/filename-sanitizer/tree/master" + }, + "time": "2019-02-07T14:37:30+00:00" }, { "name": "jaytaph/typearray", @@ -2415,16 +2668,16 @@ }, { "name": "knplabs/knp-components", - "version": "v4.1.0", + "version": "v4.2.0", "source": { "type": "git", "url": "https://github.com/KnpLabs/knp-components.git", - "reference": "6b6efa81ee894e325744bf785d50dc962937b1f2" + "reference": "1f6560cc1247c8fe7ba75fe4f80f16ffcc9379a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KnpLabs/knp-components/zipball/6b6efa81ee894e325744bf785d50dc962937b1f2", - "reference": "6b6efa81ee894e325744bf785d50dc962937b1f2", + "url": "https://api.github.com/repos/KnpLabs/knp-components/zipball/1f6560cc1247c8fe7ba75fe4f80f16ffcc9379a0", + "reference": "1f6560cc1247c8fe7ba75fe4f80f16ffcc9379a0", "shasum": "" }, "require": { @@ -2432,7 +2685,7 @@ "symfony/event-dispatcher-contracts": "^3.0" }, "conflict": { - "doctrine/dbal": "<2.10" + "doctrine/dbal": "<3.1" }, "require-dev": { "doctrine/mongodb-odm": "^2.4", @@ -2485,7 +2738,7 @@ } ], "description": "Knplabs component library", - "homepage": "http://github.com/KnpLabs/knp-components", + "homepage": "https://github.com/KnpLabs/knp-components", "keywords": [ "components", "knp", @@ -2495,9 +2748,9 @@ ], "support": { "issues": "https://github.com/KnpLabs/knp-components/issues", - "source": "https://github.com/KnpLabs/knp-components/tree/v4.1.0" + "source": "https://github.com/KnpLabs/knp-components/tree/v4.2.0" }, - "time": "2022-12-19T09:36:54+00:00" + "time": "2023-05-09T10:21:13+00:00" }, { "name": "knplabs/knp-paginator-bundle", @@ -2661,18 +2914,84 @@ ], "time": "2023-05-04T09:04:26+00:00" }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a", + "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.220.0", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-05-02T20:02:14+00:00" + }, { "name": "league/flysystem-bundle", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-bundle.git", - "reference": "4b6e8095dbb9bed9971b4a5d8158cc6d8e720a26" + "reference": "c056bef0e8e0cdfb349e568d69e8337ce17ef6e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/4b6e8095dbb9bed9971b4a5d8158cc6d8e720a26", - "reference": "4b6e8095dbb9bed9971b4a5d8158cc6d8e720a26", + "url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/c056bef0e8e0cdfb349e568d69e8337ce17ef6e1", + "reference": "c056bef0e8e0cdfb349e568d69e8337ce17ef6e1", "shasum": "" }, "require": { @@ -2717,9 +3036,9 @@ "description": "Symfony bundle integrating Flysystem into Symfony 5.4+ applications", "support": { "issues": "https://github.com/thephpleague/flysystem-bundle/issues", - "source": "https://github.com/thephpleague/flysystem-bundle/tree/3.1.0" + "source": "https://github.com/thephpleague/flysystem-bundle/tree/3.2.0" }, - "time": "2022-12-26T19:09:49+00:00" + "time": "2023-08-21T15:19:15+00:00" }, { "name": "league/flysystem-local", @@ -2783,26 +3102,26 @@ }, { "name": "league/mime-type-detection", - "version": "1.11.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd" + "reference": "a6dfb1194a2946fcdc1f38219445234f65b35c96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ff6248ea87a9f116e78edd6002e39e5128a0d4dd", - "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/a6dfb1194a2946fcdc1f38219445234f65b35c96", + "reference": "a6dfb1194a2946fcdc1f38219445234f65b35c96", "shasum": "" }, "require": { "ext-fileinfo": "*", - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3" + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" }, "type": "library", "autoload": { @@ -2823,7 +3142,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.11.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.13.0" }, "funding": [ { @@ -2835,7 +3154,7 @@ "type": "tidelift" } ], - "time": "2022-04-17T13:12:02+00:00" + "time": "2023-08-05T12:09:49+00:00" }, { "name": "maennchen/zipstream-php", @@ -3240,27 +3559,97 @@ ], "time": "2023-06-21T08:46:11+00:00" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0" + }, + "time": "2023-08-25T10:54:48+00:00" + }, { "name": "nesbot/carbon", - "version": "2.68.1", + "version": "2.69.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da" + "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da", - "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4308217830e4ca445583a37d1bf4aff4153fa81c", + "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c", "shasum": "" }, "require": { "ext-json": "*", "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", "symfony/polyfill-mbstring": "^1.0", "symfony/polyfill-php80": "^1.16", "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" }, + "provide": { + "psr/clock-implementation": "1.0" + }, "require-dev": { "doctrine/dbal": "^2.0 || ^3.1.4", "doctrine/orm": "^2.7", @@ -3340,7 +3729,7 @@ "type": "tidelift" } ], - "time": "2023-06-20T18:29:04+00:00" + "time": "2023-08-03T09:00:52+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -3671,16 +4060,16 @@ }, { "name": "php-http/discovery", - "version": "1.19.0", + "version": "1.19.1", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "1856a119a0b0ba8da8b5c33c080aa7af8fac25b4" + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/1856a119a0b0ba8da8b5c33c080aa7af8fac25b4", - "reference": "1856a119a0b0ba8da8b5c33c080aa7af8fac25b4", + "url": "https://api.github.com/repos/php-http/discovery/zipball/57f3de01d32085fea20865f9b16fb0e69347c39e", + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e", "shasum": "" }, "require": { @@ -3743,9 +4132,9 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.19.0" + "source": "https://github.com/php-http/discovery/tree/1.19.1" }, - "time": "2023-06-19T08:45:36+00:00" + "time": "2023-07-11T07:02:26+00:00" }, { "name": "php-http/httplug", @@ -3973,16 +4362,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.7.2", + "version": "1.7.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d" + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b2fe4d22a5426f38e014855322200b97b5362c0d", - "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", "shasum": "" }, "require": { @@ -4025,9 +4414,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" }, - "time": "2023-05-30T18:13:47+00:00" + "time": "2023-08-12T11:01:26+00:00" }, { "name": "phpoffice/phpspreadsheet", @@ -4136,16 +4525,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.23.0", + "version": "1.23.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "a2b24135c35852b348894320d47b3902a94bc494" + "reference": "846ae76eef31c6d7790fac9bc399ecee45160b26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a2b24135c35852b348894320d47b3902a94bc494", - "reference": "a2b24135c35852b348894320d47b3902a94bc494", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/846ae76eef31c6d7790fac9bc399ecee45160b26", + "reference": "846ae76eef31c6d7790fac9bc399ecee45160b26", "shasum": "" }, "require": { @@ -4177,22 +4566,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.1" }, - "time": "2023-07-23T22:17:56+00:00" + "time": "2023-08-03T16:32:59+00:00" }, { "name": "predis/predis", - "version": "v2.2.0", + "version": "v2.2.1", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d" + "reference": "5f2b410a74afaff296a87a494e4c5488cf9fab57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/33b70b971a32b0d28b4f748b0547593dce316e0d", - "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d", + "url": "https://api.github.com/repos/predis/predis/zipball/5f2b410a74afaff296a87a494e4c5488cf9fab57", + "reference": "5f2b410a74afaff296a87a494e4c5488cf9fab57", "shasum": "" }, "require": { @@ -4232,7 +4621,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.2.0" + "source": "https://github.com/predis/predis/tree/v2.2.1" }, "funding": [ { @@ -4240,7 +4629,75 @@ "type": "github" } ], - "time": "2023-06-14T10:37:31+00:00" + "time": "2023-08-15T23:01:46+00:00" + }, + { + "name": "presta/sitemap-bundle", + "version": "v3.3.1", + "source": { + "type": "git", + "url": "https://github.com/prestaconcept/PrestaSitemapBundle.git", + "reference": "29f14e0fb97ae4cb1b37dce7cee828871ad9be19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/prestaconcept/PrestaSitemapBundle/zipball/29f14e0fb97ae4cb1b37dce7cee828871ad9be19", + "reference": "29f14e0fb97ae4cb1b37dce7cee828871ad9be19", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": ">=7.1.3", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/framework-bundle": "^4.4|^5.0|^6.0" + }, + "require-dev": { + "doctrine/annotations": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.5|^8.0", + "sensio/framework-extra-bundle": "^5.5|^6.1", + "squizlabs/php_codesniffer": "^3.5", + "symfony/browser-kit": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/phpunit-bridge": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "3.x": "3.x-dev", + "2.x": "2.x-dev", + "1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Presta\\SitemapBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Prestaconcept", + "homepage": "http://www.prestaconcept.net/" + } + ], + "description": "A Symfony bundle that provides tools to build your application sitemap.", + "keywords": [ + "Sitemap", + "bundle", + "prestaconcept", + "symfony", + "xml" + ], + "support": { + "issues": "https://github.com/prestaconcept/PrestaSitemapBundle/issues", + "source": "https://github.com/prestaconcept/PrestaSitemapBundle/tree/v3.3.1" + }, + "time": "2023-04-24T07:19:47+00:00" }, { "name": "psr/cache", @@ -4805,7 +5262,7 @@ }, { "name": "scheb/2fa-backup-code", - "version": "v6.8.0", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-backup-code.git", @@ -4848,22 +5305,22 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-backup-code/tree/v6.8.0" + "source": "https://github.com/scheb/2fa-backup-code/tree/v6.9.0" }, "time": "2022-12-10T15:20:09+00:00" }, { "name": "scheb/2fa-bundle", - "version": "v6.8.0", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-bundle.git", - "reference": "4f8e9e87f90cf50c72b0857ea2b88453cf1d2446" + "reference": "98fee6bf6ce17514d8f3772d4c7f86e6f7595a85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/4f8e9e87f90cf50c72b0857ea2b88453cf1d2446", - "reference": "4f8e9e87f90cf50c72b0857ea2b88453cf1d2446", + "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/98fee6bf6ce17514d8f3772d4c7f86e6f7595a85", + "reference": "98fee6bf6ce17514d8f3772d4c7f86e6f7595a85", "shasum": "" }, "require": { @@ -4915,13 +5372,13 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-bundle/tree/v6.8.0" + "source": "https://github.com/scheb/2fa-bundle/tree/v6.9.0" }, - "time": "2023-01-26T18:47:22+00:00" + "time": "2023-08-05T11:13:58+00:00" }, { "name": "scheb/2fa-email", - "version": "v6.8.0", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-email.git", @@ -4964,13 +5421,13 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-email/tree/v6.8.0" + "source": "https://github.com/scheb/2fa-email/tree/v6.9.0" }, "time": "2022-12-10T15:20:09+00:00" }, { "name": "scheb/2fa-totp", - "version": "v6.8.0", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/scheb/2fa-totp.git", @@ -5015,7 +5472,7 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-totp/tree/v6.8.0" + "source": "https://github.com/scheb/2fa-totp/tree/v6.9.0" }, "time": "2022-12-10T15:20:09+00:00" }, @@ -5258,16 +5715,16 @@ }, { "name": "symfony/amqp-messenger", - "version": "v6.3.0", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/amqp-messenger.git", - "reference": "54a04a295f52e8c5567e11748b4d5c06724cadb5" + "reference": "0391200eb277d16d1a4ccad1aea05f0a23ee90ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/amqp-messenger/zipball/54a04a295f52e8c5567e11748b4d5c06724cadb5", - "reference": "54a04a295f52e8c5567e11748b4d5c06724cadb5", + "url": "https://api.github.com/repos/symfony/amqp-messenger/zipball/0391200eb277d16d1a4ccad1aea05f0a23ee90ac", + "reference": "0391200eb277d16d1a4ccad1aea05f0a23ee90ac", "shasum": "" }, "require": { @@ -5307,7 +5764,7 @@ "description": "Symfony AMQP extension Messenger Bridge", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/amqp-messenger/tree/v6.3.0" + "source": "https://github.com/symfony/amqp-messenger/tree/v6.3.4" }, "funding": [ { @@ -5323,7 +5780,7 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:08:00+00:00" + "time": "2023-08-14T14:06:04+00:00" }, { "name": "symfony/asset", @@ -5396,16 +5853,16 @@ }, { "name": "symfony/cache", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "d176b97600860df13e851538c2df2ad88e9e5ca9" + "reference": "e60d00b4f633efa4c1ef54e77c12762d9073e7b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/d176b97600860df13e851538c2df2ad88e9e5ca9", - "reference": "d176b97600860df13e851538c2df2ad88e9e5ca9", + "url": "https://api.github.com/repos/symfony/cache/zipball/e60d00b4f633efa4c1ef54e77c12762d9073e7b3", + "reference": "e60d00b4f633efa4c1ef54e77c12762d9073e7b3", "shasum": "" }, "require": { @@ -5472,7 +5929,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.3.2" + "source": "https://github.com/symfony/cache/tree/v6.3.4" }, "funding": [ { @@ -5488,7 +5945,7 @@ "type": "tidelift" } ], - "time": "2023-07-27T16:19:14+00:00" + "time": "2023-08-05T09:10:27+00:00" }, { "name": "symfony/cache-contracts", @@ -5568,16 +6025,16 @@ }, { "name": "symfony/clock", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "2c72817f85cbdd0ae4e49643514a889004934296" + "reference": "a74086d3db70d0f06ffd84480daa556248706e98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/2c72817f85cbdd0ae4e49643514a889004934296", - "reference": "2c72817f85cbdd0ae4e49643514a889004934296", + "url": "https://api.github.com/repos/symfony/clock/zipball/a74086d3db70d0f06ffd84480daa556248706e98", + "reference": "a74086d3db70d0f06ffd84480daa556248706e98", "shasum": "" }, "require": { @@ -5621,7 +6078,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v6.3.1" + "source": "https://github.com/symfony/clock/tree/v6.3.4" }, "funding": [ { @@ -5637,7 +6094,7 @@ "type": "tidelift" } ], - "time": "2023-06-08T23:46:55+00:00" + "time": "2023-07-31T11:35:03+00:00" }, { "name": "symfony/config", @@ -5716,16 +6173,16 @@ }, { "name": "symfony/console", - "version": "v6.3.0", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7" + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", + "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", "shasum": "" }, "require": { @@ -5786,7 +6243,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.0" + "source": "https://github.com/symfony/console/tree/v6.3.4" }, "funding": [ { @@ -5802,20 +6259,20 @@ "type": "tidelift" } ], - "time": "2023-05-29T12:49:39+00:00" + "time": "2023-08-16T10:10:12+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "474cfbc46aba85a1ca11a27db684480d0db64ba7" + "reference": "68a5a9570806a087982f383f6109c5e925892a49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/474cfbc46aba85a1ca11a27db684480d0db64ba7", - "reference": "474cfbc46aba85a1ca11a27db684480d0db64ba7", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/68a5a9570806a087982f383f6109c5e925892a49", + "reference": "68a5a9570806a087982f383f6109c5e925892a49", "shasum": "" }, "require": { @@ -5867,7 +6324,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.3.2" + "source": "https://github.com/symfony/dependency-injection/tree/v6.3.4" }, "funding": [ { @@ -5883,7 +6340,7 @@ "type": "tidelift" } ], - "time": "2023-07-19T20:17:28+00:00" + "time": "2023-08-16T17:55:17+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5954,16 +6411,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "594263c7d2677022a16e4f39d20070463ba03888" + "reference": "589eeeb93669739ec1d8bd4593e4972d94e0981d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/594263c7d2677022a16e4f39d20070463ba03888", - "reference": "594263c7d2677022a16e4f39d20070463ba03888", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/589eeeb93669739ec1d8bd4593e4972d94e0981d", + "reference": "589eeeb93669739ec1d8bd4593e4972d94e0981d", "shasum": "" }, "require": { @@ -6044,7 +6501,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.3.1" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.3.4" }, "funding": [ { @@ -6060,7 +6517,7 @@ "type": "tidelift" } ], - "time": "2023-06-18T20:33:34+00:00" + "time": "2023-08-08T10:40:25+00:00" }, { "name": "symfony/doctrine-messenger", @@ -6567,16 +7024,16 @@ }, { "name": "symfony/finder", - "version": "v6.3.2", + "version": "v6.3.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "78ce4c29757d657d2b41a91c328923b9a0d6b43d" + "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/78ce4c29757d657d2b41a91c328923b9a0d6b43d", - "reference": "78ce4c29757d657d2b41a91c328923b9a0d6b43d", + "url": "https://api.github.com/repos/symfony/finder/zipball/9915db259f67d21eefee768c1abcf1cc61b1fc9e", + "reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e", "shasum": "" }, "require": { @@ -6611,7 +7068,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.3.2" + "source": "https://github.com/symfony/finder/tree/v6.3.3" }, "funding": [ { @@ -6627,20 +7084,20 @@ "type": "tidelift" } ], - "time": "2023-07-13T14:29:38+00:00" + "time": "2023-07-31T08:31:44+00:00" }, { "name": "symfony/flex", - "version": "v2.3.1", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "3c9c3424efdafe33e0e3cfb5e87e50b34711fedf" + "reference": "9c402af768c6c9f8126a9ffa192ecf7c16581e35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/3c9c3424efdafe33e0e3cfb5e87e50b34711fedf", - "reference": "3c9c3424efdafe33e0e3cfb5e87e50b34711fedf", + "url": "https://api.github.com/repos/symfony/flex/zipball/9c402af768c6c9f8126a9ffa192ecf7c16581e35", + "reference": "9c402af768c6c9f8126a9ffa192ecf7c16581e35", "shasum": "" }, "require": { @@ -6676,7 +7133,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.3.1" + "source": "https://github.com/symfony/flex/tree/v2.3.3" }, "funding": [ { @@ -6692,20 +7149,20 @@ "type": "tidelift" } ], - "time": "2023-05-27T07:38:25+00:00" + "time": "2023-08-04T09:02:35+00:00" }, { "name": "symfony/form", - "version": "v6.3.0", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "59e7c5afef32b9ff735e83e5fc74d63044833a2b" + "reference": "afdadf511e08bc6d4752afb869ce084276aca4e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/59e7c5afef32b9ff735e83e5fc74d63044833a2b", - "reference": "59e7c5afef32b9ff735e83e5fc74d63044833a2b", + "url": "https://api.github.com/repos/symfony/form/zipball/afdadf511e08bc6d4752afb869ce084276aca4e2", + "reference": "afdadf511e08bc6d4752afb869ce084276aca4e2", "shasum": "" }, "require": { @@ -6773,7 +7230,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v6.3.0" + "source": "https://github.com/symfony/form/tree/v6.3.2" }, "funding": [ { @@ -6789,20 +7246,20 @@ "type": "tidelift" } ], - "time": "2023-05-25T13:09:35+00:00" + "time": "2023-07-26T17:39:03+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "930fe7ee25a928b9b3627d717873ddd171430a82" + "reference": "f822f54ff05cd88878910b4559f66c12176d952c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/930fe7ee25a928b9b3627d717873ddd171430a82", - "reference": "930fe7ee25a928b9b3627d717873ddd171430a82", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/f822f54ff05cd88878910b4559f66c12176d952c", + "reference": "f822f54ff05cd88878910b4559f66c12176d952c", "shasum": "" }, "require": { @@ -6917,7 +7374,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.3.2" + "source": "https://github.com/symfony/framework-bundle/tree/v6.3.4" }, "funding": [ { @@ -6933,20 +7390,20 @@ "type": "tidelift" } ], - "time": "2023-07-26T17:39:03+00:00" + "time": "2023-08-16T18:04:38+00:00" }, { "name": "symfony/http-client", - "version": "v6.3.1", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "1c828a06aef2f5eeba42026dfc532d4fc5406123" + "reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/1c828a06aef2f5eeba42026dfc532d4fc5406123", - "reference": "1c828a06aef2f5eeba42026dfc532d4fc5406123", + "url": "https://api.github.com/repos/symfony/http-client/zipball/15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00", + "reference": "15f9f4bad62bfcbe48b5dedd866f04a08fc7ff00", "shasum": "" }, "require": { @@ -7009,7 +7466,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.3.1" + "source": "https://github.com/symfony/http-client/tree/v6.3.2" }, "funding": [ { @@ -7025,7 +7482,7 @@ "type": "tidelift" } ], - "time": "2023-06-24T11:51:27+00:00" + "time": "2023-07-05T08:41:27+00:00" }, { "name": "symfony/http-client-contracts", @@ -7107,16 +7564,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "43ed99d30f5f466ffa00bdac3f5f7aa9cd7617c3" + "reference": "cac1556fdfdf6719668181974104e6fcfa60e844" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/43ed99d30f5f466ffa00bdac3f5f7aa9cd7617c3", - "reference": "43ed99d30f5f466ffa00bdac3f5f7aa9cd7617c3", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/cac1556fdfdf6719668181974104e6fcfa60e844", + "reference": "cac1556fdfdf6719668181974104e6fcfa60e844", "shasum": "" }, "require": { @@ -7164,7 +7621,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.3.2" + "source": "https://github.com/symfony/http-foundation/tree/v6.3.4" }, "funding": [ { @@ -7180,20 +7637,20 @@ "type": "tidelift" } ], - "time": "2023-07-23T21:58:39+00:00" + "time": "2023-08-22T08:20:46+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "51daa1e14a4b5cc7260c47d5a10a11ab32c88b63" + "reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/51daa1e14a4b5cc7260c47d5a10a11ab32c88b63", - "reference": "51daa1e14a4b5cc7260c47d5a10a11ab32c88b63", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb", + "reference": "36abb425b4af863ae1fe54d8a8b8b4c76a2bccdb", "shasum": "" }, "require": { @@ -7202,7 +7659,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.3", "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-foundation": "^6.2.7", + "symfony/http-foundation": "^6.3.4", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -7210,7 +7667,7 @@ "symfony/cache": "<5.4", "symfony/config": "<6.1", "symfony/console": "<5.4", - "symfony/dependency-injection": "<6.3", + "symfony/dependency-injection": "<6.3.4", "symfony/doctrine-bridge": "<5.4", "symfony/form": "<5.4", "symfony/http-client": "<5.4", @@ -7234,7 +7691,7 @@ "symfony/config": "^6.1", "symfony/console": "^5.4|^6.0", "symfony/css-selector": "^5.4|^6.0", - "symfony/dependency-injection": "^6.3", + "symfony/dependency-injection": "^6.3.4", "symfony/dom-crawler": "^5.4|^6.0", "symfony/expression-language": "^5.4|^6.0", "symfony/finder": "^5.4|^6.0", @@ -7277,7 +7734,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.3.2" + "source": "https://github.com/symfony/http-kernel/tree/v6.3.4" }, "funding": [ { @@ -7293,20 +7750,20 @@ "type": "tidelift" } ], - "time": "2023-07-30T09:04:05+00:00" + "time": "2023-08-26T13:54:49+00:00" }, { "name": "symfony/intl", - "version": "v6.3.1", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "fdf4aff85fff2cc681cc45936b6b2a52731acc23" + "reference": "1f8cb145c869ed089a8531c51a6a4b31ed0b3c69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/fdf4aff85fff2cc681cc45936b6b2a52731acc23", - "reference": "fdf4aff85fff2cc681cc45936b6b2a52731acc23", + "url": "https://api.github.com/repos/symfony/intl/zipball/1f8cb145c869ed089a8531c51a6a4b31ed0b3c69", + "reference": "1f8cb145c869ed089a8531c51a6a4b31ed0b3c69", "shasum": "" }, "require": { @@ -7314,7 +7771,8 @@ }, "require-dev": { "symfony/filesystem": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0" + "symfony/finder": "^5.4|^6.0", + "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", "autoload": { @@ -7358,7 +7816,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.3.1" + "source": "https://github.com/symfony/intl/tree/v6.3.2" }, "funding": [ { @@ -7374,7 +7832,7 @@ "type": "tidelift" } ], - "time": "2023-06-21T12:08:28+00:00" + "time": "2023-07-20T07:43:09+00:00" }, { "name": "symfony/mailer", @@ -7458,16 +7916,16 @@ }, { "name": "symfony/messenger", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "e92ae9997f36e1189ff8251636adc21b8c9a6bea" + "reference": "bf460982736a4b99d11a3a90005ef438c3780df7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/e92ae9997f36e1189ff8251636adc21b8c9a6bea", - "reference": "e92ae9997f36e1189ff8251636adc21b8c9a6bea", + "url": "https://api.github.com/repos/symfony/messenger/zipball/bf460982736a4b99d11a3a90005ef438c3780df7", + "reference": "bf460982736a4b99d11a3a90005ef438c3780df7", "shasum": "" }, "require": { @@ -7486,6 +7944,7 @@ "psr/cache": "^1.0|^2.0|^3.0", "symfony/console": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher": "^5.4|^6.0", "symfony/http-kernel": "^5.4|^6.0", "symfony/process": "^5.4|^6.0", @@ -7523,7 +7982,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v6.3.1" + "source": "https://github.com/symfony/messenger/tree/v6.3.4" }, "funding": [ { @@ -7539,24 +7998,25 @@ "type": "tidelift" } ], - "time": "2023-06-21T12:08:28+00:00" + "time": "2023-08-14T14:06:04+00:00" }, { "name": "symfony/mime", - "version": "v6.3.0", + "version": "v6.3.3", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad" + "reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/7b5d2121858cd6efbed778abce9cfdd7ab1f62ad", - "reference": "7b5d2121858cd6efbed778abce9cfdd7ab1f62ad", + "url": "https://api.github.com/repos/symfony/mime/zipball/9a0cbd52baa5ba5a5b1f0cacc59466f194730f98", + "reference": "9a0cbd52baa5ba5a5b1f0cacc59466f194730f98", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -7565,7 +8025,7 @@ "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<5.4", - "symfony/serializer": "<6.2" + "symfony/serializer": "<6.2.13|>=6.3,<6.3.2" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", @@ -7574,7 +8034,7 @@ "symfony/dependency-injection": "^5.4|^6.0", "symfony/property-access": "^5.4|^6.0", "symfony/property-info": "^5.4|^6.0", - "symfony/serializer": "^6.2" + "symfony/serializer": "~6.2.13|^6.3.2" }, "type": "library", "autoload": { @@ -7606,7 +8066,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.3.0" + "source": "https://github.com/symfony/mime/tree/v6.3.3" }, "funding": [ { @@ -7622,7 +8082,7 @@ "type": "tidelift" } ], - "time": "2023-04-28T15:57:00+00:00" + "time": "2023-07-31T07:08:24+00:00" }, { "name": "symfony/monolog-bridge", @@ -8002,16 +8462,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -8023,7 +8483,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8063,7 +8523,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -8079,20 +8539,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "a3d9148e2c363588e05abbdd4ee4f971f0a5330c" + "reference": "e46b4da57951a16053cd751f63f4a24292788157" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/a3d9148e2c363588e05abbdd4ee4f971f0a5330c", - "reference": "a3d9148e2c363588e05abbdd4ee4f971f0a5330c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e46b4da57951a16053cd751f63f4a24292788157", + "reference": "e46b4da57951a16053cd751f63f4a24292788157", "shasum": "" }, "require": { @@ -8104,7 +8564,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8150,7 +8610,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.28.0" }, "funding": [ { @@ -8166,20 +8626,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-03-21T17:27:24+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", + "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", "shasum": "" }, "require": { @@ -8193,7 +8653,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8237,7 +8697,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0" }, "funding": [ { @@ -8253,20 +8713,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:30:37+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -8278,7 +8738,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8321,7 +8781,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -8337,20 +8797,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -8365,7 +8825,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8404,7 +8864,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -8420,20 +8880,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "508c652ba3ccf69f8c97f251534f229791b52a57" + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/508c652ba3ccf69f8c97f251534f229791b52a57", - "reference": "508c652ba3ccf69f8c97f251534f229791b52a57", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", "shasum": "" }, "require": { @@ -8443,7 +8903,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8456,7 +8916,10 @@ ], "psr-4": { "Symfony\\Polyfill\\Php83\\": "" - } + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8481,7 +8944,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" }, "funding": [ { @@ -8497,20 +8960,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-08-16T06:22:46+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166" + "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/f3cf1a645c2734236ed1e2e671e273eeb3586166", - "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9c44518a5aff8da565c8a55dbe85d2769e6f630e", + "reference": "9c44518a5aff8da565c8a55dbe85d2769e6f630e", "shasum": "" }, "require": { @@ -8525,7 +8988,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8563,7 +9026,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.28.0" }, "funding": [ { @@ -8579,20 +9042,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", - "version": "v6.3.0", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628" + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8741e3ed7fe2e91ec099e02446fb86667a0f1628", - "reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", "shasum": "" }, "require": { @@ -8624,7 +9087,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.3.0" + "source": "https://github.com/symfony/process/tree/v6.3.4" }, "funding": [ { @@ -8640,7 +9103,7 @@ "type": "tidelift" } ], - "time": "2023-05-19T08:06:44+00:00" + "time": "2023-08-07T10:39:22+00:00" }, { "name": "symfony/property-access", @@ -8804,20 +9267,21 @@ }, { "name": "symfony/routing", - "version": "v6.3.2", + "version": "v6.3.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "9874c77e1746c7be68ae67e79433cbb202648a8d" + "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/9874c77e1746c7be68ae67e79433cbb202648a8d", - "reference": "9874c77e1746c7be68ae67e79433cbb202648a8d", + "url": "https://api.github.com/repos/symfony/routing/zipball/e7243039ab663822ff134fbc46099b5fdfa16f6a", + "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "doctrine/annotations": "<1.12", @@ -8866,7 +9330,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.3.2" + "source": "https://github.com/symfony/routing/tree/v6.3.3" }, "funding": [ { @@ -8882,20 +9346,20 @@ "type": "tidelift" } ], - "time": "2023-07-24T13:52:02+00:00" + "time": "2023-07-31T07:08:24+00:00" }, { "name": "symfony/runtime", - "version": "v6.3.1", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "8e83b5d8e0ace903e1a91dedfe08a84ed2a54b0d" + "reference": "d5c09493647a0c1a16e6c8da308098e840d1164f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/8e83b5d8e0ace903e1a91dedfe08a84ed2a54b0d", - "reference": "8e83b5d8e0ace903e1a91dedfe08a84ed2a54b0d", + "url": "https://api.github.com/repos/symfony/runtime/zipball/d5c09493647a0c1a16e6c8da308098e840d1164f", + "reference": "d5c09493647a0c1a16e6c8da308098e840d1164f", "shasum": "" }, "require": { @@ -8945,7 +9409,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.3.1" + "source": "https://github.com/symfony/runtime/tree/v6.3.2" }, "funding": [ { @@ -8961,20 +9425,20 @@ "type": "tidelift" } ], - "time": "2023-06-21T12:08:28+00:00" + "time": "2023-07-16T17:05:46+00:00" }, { "name": "symfony/security-bundle", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "f4fe79d7ebafd406e1a6f646839bfbbed641d8b2" + "reference": "31957477b289220a47880ead3727bf5cc059fa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/f4fe79d7ebafd406e1a6f646839bfbbed641d8b2", - "reference": "f4fe79d7ebafd406e1a6f646839bfbbed641d8b2", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/31957477b289220a47880ead3727bf5cc059fa08", + "reference": "31957477b289220a47880ead3727bf5cc059fa08", "shasum": "" }, "require": { @@ -8984,6 +9448,7 @@ "symfony/clock": "^6.3", "symfony/config": "^6.1", "symfony/dependency-injection": "^6.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher": "^5.4|^6.0", "symfony/http-foundation": "^6.2", "symfony/http-kernel": "^6.2", @@ -9054,7 +9519,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.3.1" + "source": "https://github.com/symfony/security-bundle/tree/v6.3.4" }, "funding": [ { @@ -9070,24 +9535,25 @@ "type": "tidelift" } ], - "time": "2023-06-21T12:08:28+00:00" + "time": "2023-08-25T08:46:23+00:00" }, { "name": "symfony/security-core", - "version": "v6.3.0", + "version": "v6.3.3", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "9cb74232e978be1440d2bb7daf91eb40a9363890" + "reference": "b86ce012cc9a62a15ed43af5037eebc3e6de4d7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/9cb74232e978be1440d2bb7daf91eb40a9363890", - "reference": "9cb74232e978be1440d2bb7daf91eb40a9363890", + "url": "https://api.github.com/repos/symfony/security-core/zipball/b86ce012cc9a62a15ed43af5037eebc3e6de4d7f", + "reference": "b86ce012cc9a62a15ed43af5037eebc3e6de4d7f", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher-contracts": "^2.5|^3", "symfony/password-hasher": "^5.4|^6.0", "symfony/service-contracts": "^2.5|^3" @@ -9138,7 +9604,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.3.0" + "source": "https://github.com/symfony/security-core/tree/v6.3.3" }, "funding": [ { @@ -9154,20 +9620,20 @@ "type": "tidelift" } ], - "time": "2023-04-28T15:57:00+00:00" + "time": "2023-07-31T07:08:24+00:00" }, { "name": "symfony/security-csrf", - "version": "v6.3.0", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/security-csrf.git", - "reference": "1f505c9060bde692eb37718c78a91d95d9abeeec" + "reference": "63d7b098c448cbddb46ea5eda33b68c1ece6eb5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-csrf/zipball/1f505c9060bde692eb37718c78a91d95d9abeeec", - "reference": "1f505c9060bde692eb37718c78a91d95d9abeeec", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/63d7b098c448cbddb46ea5eda33b68c1ece6eb5b", + "reference": "63d7b098c448cbddb46ea5eda33b68c1ece6eb5b", "shasum": "" }, "require": { @@ -9206,7 +9672,7 @@ "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-csrf/tree/v6.3.0" + "source": "https://github.com/symfony/security-csrf/tree/v6.3.2" }, "funding": [ { @@ -9222,20 +9688,20 @@ "type": "tidelift" } ], - "time": "2023-04-21T14:41:17+00:00" + "time": "2023-07-05T08:41:27+00:00" }, { "name": "symfony/security-http", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "36d2bdd09c33f63014dc65f164a77ff099d256c6" + "reference": "0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/36d2bdd09c33f63014dc65f164a77ff099d256c6", - "reference": "36d2bdd09c33f63014dc65f164a77ff099d256c6", + "url": "https://api.github.com/repos/symfony/security-http/zipball/0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d", + "reference": "0afb37c1120c1c46219bdbd1dd912fb4d48eaf7d", "shasum": "" }, "require": { @@ -9293,7 +9759,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.3.1" + "source": "https://github.com/symfony/security-http/tree/v6.3.4" }, "funding": [ { @@ -9309,24 +9775,25 @@ "type": "tidelift" } ], - "time": "2023-06-18T15:50:12+00:00" + "time": "2023-08-25T19:43:09+00:00" }, { "name": "symfony/serializer", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "1d238ee3180bc047f8ab713bfb73848d553f4407" + "reference": "96d28a58d5a128bf77c54534b380eb7c92c8f846" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/1d238ee3180bc047f8ab713bfb73848d553f4407", - "reference": "1d238ee3180bc047f8ab713bfb73848d553f4407", + "url": "https://api.github.com/repos/symfony/serializer/zipball/96d28a58d5a128bf77c54534b380eb7c92c8f846", + "reference": "96d28a58d5a128bf77c54534b380eb7c92c8f846", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -9335,7 +9802,7 @@ "phpdocumentor/type-resolver": "<1.4.0", "symfony/dependency-injection": "<5.4", "symfony/property-access": "<5.4", - "symfony/property-info": "<5.4", + "symfony/property-info": "<5.4.24|>=6,<6.2.11", "symfony/uid": "<5.4", "symfony/yaml": "<5.4" }, @@ -9353,7 +9820,7 @@ "symfony/http-kernel": "^5.4|^6.0", "symfony/mime": "^5.4|^6.0", "symfony/property-access": "^5.4|^6.0", - "symfony/property-info": "^5.4|^6.0", + "symfony/property-info": "^5.4.24|^6.2.11", "symfony/uid": "^5.4|^6.0", "symfony/validator": "^5.4|^6.0", "symfony/var-dumper": "^5.4|^6.0", @@ -9386,7 +9853,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.3.1" + "source": "https://github.com/symfony/serializer/tree/v6.3.4" }, "funding": [ { @@ -9402,7 +9869,7 @@ "type": "tidelift" } ], - "time": "2023-06-21T19:54:33+00:00" + "time": "2023-08-24T14:35:28+00:00" }, { "name": "symfony/service-contracts", @@ -9636,20 +10103,21 @@ }, { "name": "symfony/translation", - "version": "v6.3.0", + "version": "v6.3.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f72b2cba8f79dd9d536f534f76874b58ad37876f" + "reference": "3ed078c54bc98bbe4414e1e9b2d5e85ed5a5c8bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f72b2cba8f79dd9d536f534f76874b58ad37876f", - "reference": "f72b2cba8f79dd9d536f534f76874b58ad37876f", + "url": "https://api.github.com/repos/symfony/translation/zipball/3ed078c54bc98bbe4414e1e9b2d5e85ed5a5c8bd", + "reference": "3ed078c54bc98bbe4414e1e9b2d5e85ed5a5c8bd", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.5|^3.0" }, @@ -9710,7 +10178,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.3.0" + "source": "https://github.com/symfony/translation/tree/v6.3.3" }, "funding": [ { @@ -9726,7 +10194,7 @@ "type": "tidelift" } ], - "time": "2023-05-19T12:46:45+00:00" + "time": "2023-07-31T07:08:24+00:00" }, { "name": "symfony/translation-contracts", @@ -10075,16 +10543,16 @@ }, { "name": "symfony/validator", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "1b71f43c62ee867ab08195ba6039a1bc3e6654dc" + "reference": "0c8435154920b9bbe93bece675234c244cadf73b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/1b71f43c62ee867ab08195ba6039a1bc3e6654dc", - "reference": "1b71f43c62ee867ab08195ba6039a1bc3e6654dc", + "url": "https://api.github.com/repos/symfony/validator/zipball/0c8435154920b9bbe93bece675234c244cadf73b", + "reference": "0c8435154920b9bbe93bece675234c244cadf73b", "shasum": "" }, "require": { @@ -10151,7 +10619,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.3.1" + "source": "https://github.com/symfony/validator/tree/v6.3.4" }, "funding": [ { @@ -10167,24 +10635,25 @@ "type": "tidelift" } ], - "time": "2023-06-21T12:08:28+00:00" + "time": "2023-08-17T15:49:05+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "34e5ca671222670ae00749d1f554713021f8ef63" + "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34e5ca671222670ae00749d1f554713021f8ef63", - "reference": "34e5ca671222670ae00749d1f554713021f8ef63", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2027be14f8ae8eae999ceadebcda5b4909b81d45", + "reference": "2027be14f8ae8eae999ceadebcda5b4909b81d45", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -10234,7 +10703,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.3.2" + "source": "https://github.com/symfony/var-dumper/tree/v6.3.4" }, "funding": [ { @@ -10250,20 +10719,20 @@ "type": "tidelift" } ], - "time": "2023-07-21T07:05:52+00:00" + "time": "2023-08-24T14:51:05+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "3400949782c0cb5b3e73aa64cfd71dde000beccc" + "reference": "df1f8aac5751871b83d30bf3e2c355770f8f0691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/3400949782c0cb5b3e73aa64cfd71dde000beccc", - "reference": "3400949782c0cb5b3e73aa64cfd71dde000beccc", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/df1f8aac5751871b83d30bf3e2c355770f8f0691", + "reference": "df1f8aac5751871b83d30bf3e2c355770f8f0691", "shasum": "" }, "require": { @@ -10308,7 +10777,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.3.2" + "source": "https://github.com/symfony/var-exporter/tree/v6.3.4" }, "funding": [ { @@ -10324,7 +10793,7 @@ "type": "tidelift" } ], - "time": "2023-07-26T17:39:03+00:00" + "time": "2023-08-16T18:14:47+00:00" }, { "name": "symfony/web-link", @@ -10482,20 +10951,21 @@ }, { "name": "symfony/yaml", - "version": "v6.3.0", + "version": "v6.3.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "a9a8337aa641ef2aa39c3e028f9107ec391e5927" + "reference": "e23292e8c07c85b971b44c1c4b87af52133e2add" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a9a8337aa641ef2aa39c3e028f9107ec391e5927", - "reference": "a9a8337aa641ef2aa39c3e028f9107ec391e5927", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e23292e8c07c85b971b44c1c4b87af52133e2add", + "reference": "e23292e8c07c85b971b44c1c4b87af52133e2add", "shasum": "" }, "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -10530,62 +11000,127 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Loads and dumps YAML files", - "homepage": "https://symfony.com", + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-31T07:08:24+00:00" + }, + { + "name": "twig/extra-bundle", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/twig-extra-bundle.git", + "reference": "f10baafe6eb0ecd615d52d5cbfb713a39f68e8f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/f10baafe6eb0ecd615d52d5cbfb713a39f68e8f3", + "reference": "f10baafe6eb0ecd615d52d5cbfb713a39f68e8f3", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "league/commonmark": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4|^6.3", + "twig/cache-extra": "^3.0", + "twig/cssinliner-extra": "^2.12|^3.0", + "twig/html-extra": "^2.12|^3.0", + "twig/inky-extra": "^2.12|^3.0", + "twig/intl-extra": "^2.12|^3.0", + "twig/markdown-extra": "^2.12|^3.0", + "twig/string-extra": "^2.12|^3.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Twig\\Extra\\TwigExtraBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Symfony bundle for extra Twig extensions", + "homepage": "https://twig.symfony.com", + "keywords": [ + "bundle", + "extra", + "twig" + ], "support": { - "source": "https://github.com/symfony/yaml/tree/v6.3.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.7.1" }, "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, { "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/twig/twig", "type": "tidelift" } ], - "time": "2023-04-28T13:28:14+00:00" + "time": "2023-07-29T15:34:56+00:00" }, { - "name": "twig/extra-bundle", - "version": "v3.6.1", + "name": "twig/intl-extra", + "version": "v3.7.1", "source": { "type": "git", - "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "802cc2dd46ec88285d6c7fa85c26ab7f2cd5bc49" + "url": "https://github.com/twigphp/intl-extra.git", + "reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/802cc2dd46ec88285d6c7fa85c26ab7f2cd5bc49", - "reference": "802cc2dd46ec88285d6c7fa85c26ab7f2cd5bc49", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/4f4fe572f635534649cc069e1dafe4a8ad63774d", + "reference": "4f4fe572f635534649cc069e1dafe4a8ad63774d", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/framework-bundle": "^4.4|^5.0|^6.0", - "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "php": ">=7.1.3", + "symfony/intl": "^5.4|^6.0", "twig/twig": "^2.7|^3.0" }, "require-dev": { - "league/commonmark": "^1.0|^2.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", - "twig/cache-extra": "^3.0", - "twig/cssinliner-extra": "^2.12|^3.0", - "twig/html-extra": "^2.12|^3.0", - "twig/inky-extra": "^2.12|^3.0", - "twig/intl-extra": "^2.12|^3.0", - "twig/markdown-extra": "^2.12|^3.0", - "twig/string-extra": "^2.12|^3.0" + "symfony/phpunit-bridge": "^5.4|^6.3" }, - "type": "symfony-bundle", + "type": "library", "autoload": { "psr-4": { - "Twig\\Extra\\TwigExtraBundle\\": "" + "Twig\\Extra\\Intl\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -10603,15 +11138,14 @@ "role": "Lead Developer" } ], - "description": "A Symfony bundle for extra Twig extensions", + "description": "A Twig extension for Intl", "homepage": "https://twig.symfony.com", "keywords": [ - "bundle", - "extra", + "intl", "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.6.1" + "source": "https://github.com/twigphp/intl-extra/tree/v3.7.1" }, "funding": [ { @@ -10623,30 +11157,30 @@ "type": "tidelift" } ], - "time": "2023-05-06T11:11:46+00:00" + "time": "2023-07-29T15:34:56+00:00" }, { "name": "twig/string-extra", - "version": "v3.6.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", - "reference": "fab682645b3f8730fbdb7bf9ec8fe668d6f76638" + "reference": "7230d630a25e91cd91a2bd8e2f0e872962507eab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/string-extra/zipball/fab682645b3f8730fbdb7bf9ec8fe668d6f76638", - "reference": "fab682645b3f8730fbdb7bf9ec8fe668d6f76638", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/7230d630a25e91cd91a2bd8e2f0e872962507eab", + "reference": "7230d630a25e91cd91a2bd8e2f0e872962507eab", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/string": "^5.0|^6.0", + "symfony/string": "^5.4|^6.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^5.4|^6.3" }, "type": "library", "autoload": { @@ -10678,7 +11212,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.6.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.7.1" }, "funding": [ { @@ -10690,20 +11224,20 @@ "type": "tidelift" } ], - "time": "2023-02-09T06:45:16+00:00" + "time": "2023-07-29T15:34:56+00:00" }, { "name": "twig/twig", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "5cf942bbab3df42afa918caeba947f1b690af64b" + "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/5cf942bbab3df42afa918caeba947f1b690af64b", - "reference": "5cf942bbab3df42afa918caeba947f1b690af64b", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", + "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", "shasum": "" }, "require": { @@ -10713,7 +11247,7 @@ }, "require-dev": { "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^5.4.9|^6.3" }, "type": "library", "autoload": { @@ -10749,7 +11283,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.7.0" + "source": "https://github.com/twigphp/Twig/tree/v3.7.1" }, "funding": [ { @@ -10761,7 +11295,7 @@ "type": "tidelift" } ], - "time": "2023-07-26T07:16:09+00:00" + "time": "2023-08-28T11:09:02+00:00" }, { "name": "webmozart/assert", @@ -10991,16 +11525,16 @@ }, { "name": "cmgmyr/phploc", - "version": "8.0.2", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/cmgmyr/phploc.git", - "reference": "35e308033e02264a59cb1b56cc2abb1a22483ca8" + "reference": "e61d4729df46c5920ab61973bfa3f70f81a70b5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cmgmyr/phploc/zipball/35e308033e02264a59cb1b56cc2abb1a22483ca8", - "reference": "35e308033e02264a59cb1b56cc2abb1a22483ca8", + "url": "https://api.github.com/repos/cmgmyr/phploc/zipball/e61d4729df46c5920ab61973bfa3f70f81a70b5f", + "reference": "e61d4729df46c5920ab61973bfa3f70f81a70b5f", "shasum": "" }, "require": { @@ -11008,8 +11542,7 @@ "ext-json": "*", "php": "^7.4 || ^8.0", "phpunit/php-file-iterator": "^3.0|^4.0", - "sebastian/cli-parser": "^1.0|^2.0", - "sebastian/version": "^3.0|^4.0" + "sebastian/cli-parser": "^1.0|^2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", @@ -11045,7 +11578,7 @@ "homepage": "https://github.com/cmgmyr/phploc", "support": { "issues": "https://github.com/cmgmyr/phploc/issues", - "source": "https://github.com/cmgmyr/phploc/tree/8.0.2" + "source": "https://github.com/cmgmyr/phploc/tree/8.0.3" }, "funding": [ { @@ -11053,7 +11586,7 @@ "type": "github" } ], - "time": "2023-03-19T10:37:20+00:00" + "time": "2023-08-05T16:49:39+00:00" }, { "name": "composer/pcre", @@ -11128,16 +11661,16 @@ }, { "name": "composer/semver", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { @@ -11187,9 +11720,9 @@ "versioning" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.2" + "source": "https://github.com/composer/semver/tree/3.4.0" }, "funding": [ { @@ -11205,7 +11738,7 @@ "type": "tidelift" } ], - "time": "2022-04-01T19:23:25+00:00" + "time": "2023-08-31T09:50:34+00:00" }, { "name": "composer/xdebug-handler", @@ -11595,26 +12128,24 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.21.1", + "version": "v3.25.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "229b55b3eae4729a8e2a321441ba40fcb3720b86" + "reference": "8e21d69801de6b5ecb0dbe0bcdf967b335b1260b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/229b55b3eae4729a8e2a321441ba40fcb3720b86", - "reference": "229b55b3eae4729a8e2a321441ba40fcb3720b86", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/8e21d69801de6b5ecb0dbe0bcdf967b335b1260b", + "reference": "8e21d69801de6b5ecb0dbe0bcdf967b335b1260b", "shasum": "" }, "require": { "composer/semver": "^3.3", "composer/xdebug-handler": "^3.0.3", - "doctrine/annotations": "^2", - "doctrine/lexer": "^2 || ^3", "ext-json": "*", "ext-tokenizer": "*", - "php": "^8.0.1", + "php": "^7.4 || ^8.0", "sebastian/diff": "^4.0 || ^5.0", "symfony/console": "^5.4 || ^6.0", "symfony/event-dispatcher": "^5.4 || ^6.0", @@ -11628,6 +12159,7 @@ "symfony/stopwatch": "^5.4 || ^6.0" }, "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.0", "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^2.0", "mikey179/vfsstream": "^1.6.11", @@ -11679,7 +12211,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.21.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.25.1" }, "funding": [ { @@ -11687,7 +12219,69 @@ "type": "github" } ], - "time": "2023-07-05T21:50:25+00:00" + "time": "2023-09-04T01:22:52+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2023-02-25T20:23:15+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -11953,16 +12547,16 @@ }, { "name": "masterminds/html5", - "version": "2.8.0", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3" + "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", - "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf", + "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf", "shasum": "" }, "require": { @@ -12014,9 +12608,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.8.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.8.1" }, - "time": "2023-04-26T07:27:39+00:00" + "time": "2023-05-10T11:58:31+00:00" }, { "name": "mikey179/vfsstream", @@ -12071,31 +12665,31 @@ }, { "name": "mockery/mockery", - "version": "1.6.4", + "version": "1.6.6", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "d1413755e26fe56a63455f7753221c86cbb88f66" + "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/d1413755e26fe56a63455f7753221c86cbb88f66", - "reference": "d1413755e26fe56a63455f7753221c86cbb88f66", + "url": "https://api.github.com/repos/mockery/mockery/zipball/b8e0bb7d8c604046539c1115994632c74dcb361e", + "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e", "shasum": "" }, "require": { "hamcrest/hamcrest-php": "^2.0.1", "lib-pcre": ">=7.0", - "php": ">=7.4,<8.3" + "php": ">=7.3" }, "conflict": { "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.3", + "phpunit/phpunit": "^8.5 || ^9.6.10", "psalm/plugin-phpunit": "^0.18.4", "symplify/easy-coding-standard": "^11.5.0", - "vimeo/psalm": "^5.13.1" + "vimeo/psalm": "^4.30" }, "type": "library", "autoload": { @@ -12152,7 +12746,7 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2023-07-19T15:51:02+00:00" + "time": "2023-08-09T00:03:52+00:00" }, { "name": "myclabs/deep-copy", @@ -12266,16 +12860,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.16.0", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -12316,9 +12910,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2023-06-25T14:52:30+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "nunomaduro/phpinsights", @@ -12802,18 +13396,93 @@ ], "time": "2022-09-10T08:44:15+00:00" }, + { + "name": "phpoption/phpoption", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2023-02-25T19:38:58+00:00" + }, { "name": "phpstan/phpstan", - "version": "1.10.26", + "version": "1.10.32", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "5d660cbb7e1b89253a47147ae44044f49832351f" + "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d660cbb7e1b89253a47147ae44044f49832351f", - "reference": "5d660cbb7e1b89253a47147ae44044f49832351f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c47e47d3ab03137c0e121e77c4d2cb58672f6d44", + "reference": "c47e47d3ab03137c0e121e77c4d2cb58672f6d44", "shasum": "" }, "require": { @@ -12862,20 +13531,20 @@ "type": "tidelift" } ], - "time": "2023-07-19T12:44:37+00:00" + "time": "2023-08-24T21:54:50+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.40", + "version": "1.3.43", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "f741919a720af6f84249abc62befeb15eee7bc88" + "reference": "c5015035755ad2d5013bd6bf98ff423ca6150822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f741919a720af6f84249abc62befeb15eee7bc88", - "reference": "f741919a720af6f84249abc62befeb15eee7bc88", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/c5015035755ad2d5013bd6bf98ff423ca6150822", + "reference": "c5015035755ad2d5013bd6bf98ff423ca6150822", "shasum": "" }, "require": { @@ -12903,8 +13572,8 @@ "nesbot/carbon": "^2.49", "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", "phpunit/phpunit": "^9.5.10", "ramsey/uuid-doctrine": "^1.5.0", "symfony/cache": "^4.4.35" @@ -12930,9 +13599,9 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.40" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.43" }, - "time": "2023-05-11T11:26:04+00:00" + "time": "2023-09-01T15:01:13+00:00" }, { "name": "phpstan/phpstan-mockery", @@ -13057,16 +13726,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.2", + "version": "10.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e" + "reference": "cd59bb34756a16ca8253ce9b2909039c227fff71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/db1497ec8dd382e82c962f7abbe0320e4882ee4e", - "reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/cd59bb34756a16ca8253ce9b2909039c227fff71", + "reference": "cd59bb34756a16ca8253ce9b2909039c227fff71", "shasum": "" }, "require": { @@ -13123,7 +13792,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.2" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.4" }, "funding": [ { @@ -13131,20 +13800,20 @@ "type": "github" } ], - "time": "2023-05-22T09:04:27+00:00" + "time": "2023-08-31T14:04:38+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "4.0.2", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "5647d65443818959172645e7ed999217360654b6" + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/5647d65443818959172645e7ed999217360654b6", - "reference": "5647d65443818959172645e7ed999217360654b6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { @@ -13184,7 +13853,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.0.2" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, "funding": [ { @@ -13192,7 +13861,7 @@ "type": "github" } ], - "time": "2023-05-07T09:13:23+00:00" + "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", @@ -13259,16 +13928,16 @@ }, { "name": "phpunit/php-text-template", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d" + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/9f3d3709577a527025f55bcf0f7ab8052c8bb37d", - "reference": "9f3d3709577a527025f55bcf0f7ab8052c8bb37d", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { @@ -13306,7 +13975,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.0" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, "funding": [ { @@ -13314,7 +13984,7 @@ "type": "github" } ], - "time": "2023-02-03T06:56:46+00:00" + "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", @@ -13377,16 +14047,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.2.6", + "version": "10.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1c17815c129f133f3019cc18e8d0c8622e6d9bcd" + "reference": "0dafb1175c366dd274eaa9a625e914451506bcd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c17815c129f133f3019cc18e8d0c8622e6d9bcd", - "reference": "1c17815c129f133f3019cc18e8d0c8622e6d9bcd", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0dafb1175c366dd274eaa9a625e914451506bcd1", + "reference": "0dafb1175c366dd274eaa9a625e914451506bcd1", "shasum": "" }, "require": { @@ -13411,7 +14081,7 @@ "sebastian/diff": "^5.0", "sebastian/environment": "^6.0", "sebastian/exporter": "^5.0", - "sebastian/global-state": "^6.0", + "sebastian/global-state": "^6.0.1", "sebastian/object-enumerator": "^5.0", "sebastian/recursion-context": "^5.0", "sebastian/type": "^4.0", @@ -13426,7 +14096,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.2-dev" + "dev-main": "10.3-dev" } }, "autoload": { @@ -13458,7 +14128,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.2.6" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.3.2" }, "funding": [ { @@ -13474,7 +14144,7 @@ "type": "tidelift" } ], - "time": "2023-07-17T12:08:28+00:00" + "time": "2023-08-15T05:34:23+00:00" }, { "name": "psalm/plugin-symfony", @@ -13710,16 +14380,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c" + "reference": "2db5010a484d53ebf536087a70b4a5423c102372" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/72f01e6586e0caf6af81297897bd112eb7e9627c", - "reference": "72f01e6586e0caf6af81297897bd112eb7e9627c", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372", "shasum": "" }, "require": { @@ -13730,7 +14400,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.3" }, "type": "library", "extra": { @@ -13774,7 +14444,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" }, "funding": [ { @@ -13782,20 +14453,20 @@ "type": "github" } ], - "time": "2023-02-03T07:07:16+00:00" + "time": "2023-08-14T13:18:12+00:00" }, { "name": "sebastian/complexity", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6" + "reference": "c70b73893e10757af9c6a48929fa6a333b56a97a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/e67d240970c9dc7ea7b2123a6d520e334dd61dc6", - "reference": "e67d240970c9dc7ea7b2123a6d520e334dd61dc6", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c70b73893e10757af9c6a48929fa6a333b56a97a", + "reference": "c70b73893e10757af9c6a48929fa6a333b56a97a", "shasum": "" }, "require": { @@ -13831,7 +14502,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/3.0.0" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.0.1" }, "funding": [ { @@ -13839,7 +14511,7 @@ "type": "github" } ], - "time": "2023-02-03T06:59:47+00:00" + "time": "2023-08-31T09:55:53+00:00" }, { "name": "sebastian/diff", @@ -14113,16 +14785,16 @@ }, { "name": "sebastian/lines-of-code", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130" + "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/17c4d940ecafb3d15d2cf916f4108f664e28b130", - "reference": "17c4d940ecafb3d15d2cf916f4108f664e28b130", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/649e40d279e243d985aa8fb6e74dd5bb28dc185d", + "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d", "shasum": "" }, "require": { @@ -14158,7 +14830,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.1" }, "funding": [ { @@ -14166,7 +14839,7 @@ "type": "github" } ], - "time": "2023-02-03T07:08:02+00:00" + "time": "2023-08-31T09:25:50+00:00" }, { "name": "sebastian/object-enumerator", @@ -14519,16 +15192,16 @@ }, { "name": "spatie/array-to-xml", - "version": "3.1.6", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/spatie/array-to-xml.git", - "reference": "e210b98957987c755372465be105d32113f339a4" + "reference": "f9ab39c808500c347d5a8b6b13310bd5221e39e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/e210b98957987c755372465be105d32113f339a4", - "reference": "e210b98957987c755372465be105d32113f339a4", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f9ab39c808500c347d5a8b6b13310bd5221e39e7", + "reference": "f9ab39c808500c347d5a8b6b13310bd5221e39e7", "shasum": "" }, "require": { @@ -14566,7 +15239,7 @@ "xml" ], "support": { - "source": "https://github.com/spatie/array-to-xml/tree/3.1.6" + "source": "https://github.com/spatie/array-to-xml/tree/3.2.0" }, "funding": [ { @@ -14578,7 +15251,7 @@ "type": "github" } ], - "time": "2023-05-11T14:04:07+00:00" + "time": "2023-07-19T18:30:26+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -14637,6 +15310,58 @@ }, "time": "2023-02-22T23:07:41+00:00" }, + { + "name": "stefanocbt/phpdotenv-sync", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/StefanoCbt/phpdotenv-sync.git", + "reference": "bf0d3c3904411be8a73b80c3e53b1fa80d97239e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/StefanoCbt/phpdotenv-sync/zipball/bf0d3c3904411be8a73b80c3e53b1fa80d97239e", + "reference": "bf0d3c3904411be8a73b80c3e53b1fa80d97239e", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "vlucas/phpdotenv": "^5.2" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.4", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "wapmorgan/php-deprecation-detector": "^2.0" + }, + "bin": [ + "bin/phpdotenvsync" + ], + "type": "library", + "autoload": { + "psr-4": { + "Stefanocbt\\PhpdotenvSync\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stefano Ciabatta", + "email": "stefanocbt.github@gmail.com" + } + ], + "description": "A package that makes sure that your .env file is in sync with your .env.example", + "support": { + "issues": "https://github.com/StefanoCbt/phpdotenv-sync/issues", + "source": "https://github.com/StefanoCbt/phpdotenv-sync/tree/1.2.0" + }, + "time": "2022-11-27T12:47:16+00:00" + }, { "name": "symfony/browser-kit", "version": "v6.3.2", @@ -14707,16 +15432,16 @@ }, { "name": "symfony/css-selector", - "version": "v6.3.0", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf" + "reference": "883d961421ab1709877c10ac99451632a3d6fa57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", - "reference": "88453e64cd86c5b60e8d2fb2c6f953bbc353ffbf", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/883d961421ab1709877c10ac99451632a3d6fa57", + "reference": "883d961421ab1709877c10ac99451632a3d6fa57", "shasum": "" }, "require": { @@ -14752,7 +15477,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.3.0" + "source": "https://github.com/symfony/css-selector/tree/v6.3.2" }, "funding": [ { @@ -14768,20 +15493,20 @@ "type": "tidelift" } ], - "time": "2023-03-20T16:43:42+00:00" + "time": "2023-07-12T16:00:22+00:00" }, { "name": "symfony/debug-bundle", - "version": "v6.3.0", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", - "reference": "02fe831f7cdd472c561116189bcc30d0759665e7" + "reference": "3f04a578e1a9f1d7da84a87b690c03123e5d8c31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/02fe831f7cdd472c561116189bcc30d0759665e7", - "reference": "02fe831f7cdd472c561116189bcc30d0759665e7", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/3f04a578e1a9f1d7da84a87b690c03123e5d8c31", + "reference": "3f04a578e1a9f1d7da84a87b690c03123e5d8c31", "shasum": "" }, "require": { @@ -14826,7 +15551,7 @@ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug-bundle/tree/v6.3.0" + "source": "https://github.com/symfony/debug-bundle/tree/v6.3.2" }, "funding": [ { @@ -14842,20 +15567,20 @@ "type": "tidelift" } ], - "time": "2023-05-25T12:58:06+00:00" + "time": "2023-07-13T14:29:38+00:00" }, { "name": "symfony/dom-crawler", - "version": "v6.3.1", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "8aa333f41f05afc7fc285a976b58272fd90fc212" + "reference": "3fdd2a3d5fdc363b2e8dbf817f9726a4d013cbd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/8aa333f41f05afc7fc285a976b58272fd90fc212", - "reference": "8aa333f41f05afc7fc285a976b58272fd90fc212", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/3fdd2a3d5fdc363b2e8dbf817f9726a4d013cbd1", + "reference": "3fdd2a3d5fdc363b2e8dbf817f9726a4d013cbd1", "shasum": "" }, "require": { @@ -14893,7 +15618,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.3.1" + "source": "https://github.com/symfony/dom-crawler/tree/v6.3.4" }, "funding": [ { @@ -14909,7 +15634,7 @@ "type": "tidelift" } ], - "time": "2023-06-05T15:30:22+00:00" + "time": "2023-08-01T07:43:40+00:00" }, { "name": "symfony/maker-bundle", @@ -15007,16 +15732,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v6.3.1", + "version": "v6.3.2", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "0b0bf59b0d9bd1422145a123a67fb12af546ef0d" + "reference": "e020e1efbd1b42cb670fcd7d19a25abbddba035d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/0b0bf59b0d9bd1422145a123a67fb12af546ef0d", - "reference": "0b0bf59b0d9bd1422145a123a67fb12af546ef0d", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e020e1efbd1b42cb670fcd7d19a25abbddba035d", + "reference": "e020e1efbd1b42cb670fcd7d19a25abbddba035d", "shasum": "" }, "require": { @@ -15068,7 +15793,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.1" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.2" }, "funding": [ { @@ -15084,7 +15809,7 @@ "type": "tidelift" } ], - "time": "2023-06-23T13:25:16+00:00" + "time": "2023-07-12T16:00:22+00:00" }, { "name": "symfony/web-profiler-bundle", @@ -15219,16 +15944,16 @@ }, { "name": "vimeo/psalm", - "version": "5.13.1", + "version": "5.9.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "086b94371304750d1c673315321a55d15fc59015" + "reference": "8b9ad1eb9e8b7d3101f949291da2b9f7767cd163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/086b94371304750d1c673315321a55d15fc59015", - "reference": "086b94371304750d1c673315321a55d15fc59015", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/8b9ad1eb9e8b7d3101f949291da2b9f7767cd163", + "reference": "8b9ad1eb9e8b7d3101f949291da2b9f7767cd163", "shasum": "" }, "require": { @@ -15319,23 +16044,109 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/5.13.1" + "source": "https://github.com/vimeo/psalm/tree/5.9.0" + }, + "time": "2023-03-29T21:38:21+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.0.2", + "php": "^7.1.3 || ^8.0", + "phpoption/phpoption": "^1.8", + "symfony/polyfill-ctype": "^1.23", + "symfony/polyfill-mbstring": "^1.23.1", + "symfony/polyfill-php80": "^1.23.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-filter": "*", + "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "5.5-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" }, - "time": "2023-06-27T16:39:49+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2022-10-16T01:01:54+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "erichard/elasticsearch-query-builder": 10 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.1", "ext-ctype": "*", "ext-iconv": "*", - "ext-zip": "*", - "ext-intl": "*" + "ext-intl": "*", + "ext-zip": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } diff --git a/config/bundles.php b/config/bundles.php index cc5ae002..67b4211c 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -18,4 +18,5 @@ League\FlysystemBundle\FlysystemBundle::class => ['all' => true], Snc\RedisBundle\SncRedisBundle::class => ['all' => true], WhiteOctober\BreadcrumbsBundle\WhiteOctoberBreadcrumbsBundle::class => ['all' => true], + Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true], ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index f4ce11ea..43d8668c 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -19,6 +19,7 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App + report_fields_where_declared: true when@test: doctrine: diff --git a/config/packages/flysystem.yaml b/config/packages/flysystem.yaml index a46104f3..e15def54 100644 --- a/config/packages/flysystem.yaml +++ b/config/packages/flysystem.yaml @@ -1,6 +1,50 @@ flysystem: storages: document.storage: + adapter: 'lazy' + options: + source: 'document.storage.%env(STORAGE_DOCUMENT_ADAPTER)%' + thumbnail.storage: + adapter: 'lazy' + options: + source: 'thumbnail.storage.%env(STORAGE_THUMBNAIL_ADAPTER)%' + batch.storage: + adapter: 'lazy' + options: + source: 'batch.storage.%env(STORAGE_BATCH_ADAPTER)%' + + + # + # Minio storage definitions + # + document.storage.aws: + adapter: 'aws' + visibility: 'public' + directory_visibility: 'public' + options: + client: 'Aws\S3\S3Client' + bucket: '%env(STORAGE_MINIO_DOCUMENT_BUCKET)%' + + thumbnail.storage.aws: + adapter: 'aws' + visibility: 'public' + directory_visibility: 'public' + options: + client: 'Aws\S3\S3Client' + bucket: '%env(STORAGE_MINIO_THUMBNAIL_BUCKET)%' + + batch.storage.aws: + adapter: 'aws' + visibility: 'public' + directory_visibility: 'public' + options: + client: 'Aws\S3\S3Client' + bucket: '%env(STORAGE_MINIO_BATCH_BUCKET)%' + + # + # Local storage definitions + # + document.storage.local: adapter: 'local' visibility: 'public' directory_visibility: 'public' @@ -14,7 +58,7 @@ flysystem: public: 0o775 private: 0o700 - thumbnail.storage: + thumbnail.storage.local: adapter: 'local' visibility: 'public' directory_visibility: 'public' @@ -28,7 +72,7 @@ flysystem: public: 0o775 private: 0o700 - batch.storage: + batch.storage.local: adapter: 'local' visibility: 'public' directory_visibility: 'public' diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index d198eff9..ef4d22c6 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -9,7 +9,7 @@ framework: # Remove or comment this section to explicitly disable session support. session: enabled: true - handler_id: null + handler_id: App\Session\EncryptedSessionProxy cookie_secure: true cookie_samesite: lax storage_factory_id: session.storage.factory.native @@ -26,3 +26,9 @@ when@test: test: true session: storage_factory_id: session.storage.factory.mock_file + +when@dev: + framework: + session: + cookie_lifetime: 604800 + gc_maxlifetime: 604800 diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index aefb5249..5cab0d18 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -48,7 +48,8 @@ framework: App\Message\InitializeElasticRolloverMessage: high App\Message\SetElasticAliasMessage: high - App\Message\IngestDossiersMessage: ingestor # Ingests an audio file + App\Message\IngestDossiersMessage: ingestor # Ingests all dossiers + App\Message\IngestDossierMessage: ingestor # Ingests a single dossier App\Message\IngestAudioMessage: ingestor # Ingests an audio file App\Message\IngestPdfMessage: ingestor # Ingests a complete PDF document and fires a message for each page App\Message\IngestPdfPageMessage: ingestor # Ingests a single PDF page from a PDF document @@ -57,5 +58,6 @@ framework: App\Message\UpdateDossierMessage: esupdater # Updates the dossier in the elastic index App\Message\UpdateDepartmentMessage: esupdater # Updates the dossier in the elastic index App\Message\UpdateOfficialMessage: esupdater # Updates the dossier in the elastic index + App\Message\RemoveDossierMessage: esupdater # Removes a dossier in the elastic index App\Message\GenerateArchiveMessage: global # Generates a ZIP archive of the dossier diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index eaba0fa0..d163d999 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -45,22 +45,8 @@ when@prod: monolog: handlers: main: - type: fingers_crossed - action_level: error - handler: nested - excluded_http_codes: [404, 405] - buffer_size: 50 # How many messages should be saved? Prevent memory leaks - nested: - type: rotating_file - max_files: 3 - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug + type: syslog + ident: woopie formatter: monolog.formatter.json - console: - type: console - process_psr_3_messages: false - channels: ["!event", "!doctrine"] - deprecation: - type: stream - channels: [deprecation] - path: php://stderr + level: info + channels: ["!request", "!doctrine", "!event", "!deprecation"] diff --git a/config/packages/security.yaml b/config/packages/security.yaml index bb3ccfd9..22d8961b 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -12,9 +12,8 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - main: - pattern: ^/ - lazy: true + balie: + pattern: ^/balie form_login: provider: app_user_provider login_path: app_login @@ -27,22 +26,27 @@ security: two_factor: auth_form_path: 2fa_login check_path: 2fa_login_check + main: + pattern: ^/ + security: false role_hierarchy: - ROLE_ADMIN: ROLE_USER, ROLE_ADMIN_DOSSIERS, ROLE_ADMIN_USERS, ROLE_ADMIN_REQUESTS + ROLE_ADMIN_USERS: ROLE_BALIE + ROLE_ADMIN_REQUESTS: ROLE_BALIE + ROLE_ADMIN_DOSSIERS: ROLE_BALIE + ROLE_ADMIN: ROLE_USER, ROLE_ADMIN_DOSSIERS, ROLE_ADMIN_USERS, ROLE_ADMIN_REQUESTS ROLE_SUPER_ADMIN: ROLE_ADMIN # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - - { path: ^/login, roles: PUBLIC_ACCESS } - - { path: ^/logout, roles: PUBLIC_ACCESS } - - { path: ^/change-password, roles: ROLE_USER } - - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } - - { path: ^/2fa_check, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } + - { path: ^/balie/login, roles: PUBLIC_ACCESS } + - { path: ^/balie/logout, roles: PUBLIC_ACCESS } + - { path: ^/balie/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } + - { path: ^/balie/2fa_check, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } - { path: ^/balie/elastic, roles: ROLE_SUPER_ADMIN } - - { path: ^/balie, roles: ROLE_ADMIN } - - { path: ^/, roles: PUBLIC_ACCESS } + - { path: ^/balie/change-password, roles: ROLE_BALIE } + - { path: ^/balie, roles: ROLE_BALIE } when@test: security: diff --git a/config/packages/snc_redis.yaml b/config/packages/snc_redis.yaml index 2ac1a32b..3bb4c40c 100644 --- a/config/packages/snc_redis.yaml +++ b/config/packages/snc_redis.yaml @@ -4,13 +4,28 @@ snc_redis: type: predis alias: ingest dsn: '%env(REDIS_URL)%' + options: + parameters: + ssl_context: { + 'verify_peer': true, + 'allow_self_signed': false, + 'verify_peer_name': true, + 'cafile': '%env(REDIS_TLS_CAFILE)%', + 'local_cert': '%env(REDIS_TLS_LOCAL_CERT)%', + 'local_pk': '%env(REDIS_TLS_LOCAL_PK)%' + } -# Define your clients here. The example below connects to database 0 of the default Redis server. -# -# See https://github.com/snc/SncRedisBundle/blob/master/docs/README.md for instructions on -# how to configure the bundle. -# -# default: -# type: phpredis -# alias: default -# dsn: "%env(REDIS_URL)%" + session: + type: predis + alias: session + dsn: '%env(REDIS_URL)%' + options: + parameters: + ssl_context: { + 'verify_peer': true, + 'allow_self_signed': false, + 'verify_peer_name': true, + 'cafile': '%env(REDIS_TLS_CAFILE)%', + 'local_cert': '%env(REDIS_TLS_LOCAL_CERT)%', + 'local_pk': '%env(REDIS_TLS_LOCAL_PK)%' + } diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index c9b5c133..63cb4e91 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,8 +1,11 @@ twig: default_path: '%kernel.project_dir%/templates' form_themes: -# - 'bootstrap_5_horizontal_layout.html.twig' - - 'bootstrap_5_form_theme.html.twig' + - 'woo_form_theme.html.twig' + globals: + PUBLIC_BASE_URL: '%env(PUBLIC_BASE_URL)%' + PIWIK_ANALYTICS_ID: '%env(PIWIK_ANALYTICS_ID)%' + SITE_NAME: '%env(default:default_site_name:SITE_NAME)%' when@test: twig: diff --git a/config/routes.yaml b/config/routes.yaml index 4ec2df86..ac6fe83b 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -8,34 +8,71 @@ app_contact: path: /contact controller: App\Controller\LocalizedTemplateController defaults: - template: 'static/contact.{locale}.html.twig' + template: 'static/contact.html.twig' + options: + sitemap: + priority: 0.7 + changefreq: monthly app_privacy: path: /privacy controller: App\Controller\LocalizedTemplateController defaults: - template: 'static/privacy.{locale}.html.twig' + template: 'static/privacy.html.twig' + options: + sitemap: + priority: 0.7 + changefreq: monthly + section: legal -app_about: +app_about: &app_about path: /about controller: App\Controller\LocalizedTemplateController defaults: - template: 'static/about.{locale}.html.twig' + template: 'static/about.html.twig' + options: + sitemap: + priority: 0.7 + changefreq: monthly + section: legal + +app_about_dutch: + <<: *app_about + path: /over-deze-website app_copyright: path: /copyright controller: App\Controller\LocalizedTemplateController defaults: - template: 'static/copyright.{locale}.html.twig' + template: 'static/copyright.html.twig' + options: + sitemap: + priority: 0.7 + changefreq: monthly + section: legal app_cookies: path: /cookies controller: App\Controller\LocalizedTemplateController defaults: - template: 'static/cookies.{locale}.html.twig' + template: 'static/cookies.html.twig' + options: + sitemap: + priority: 0.7 + changefreq: monthly + section: legal -app_accessibility: +app_accessibility: &app_accessibility path: /accessibility controller: App\Controller\LocalizedTemplateController defaults: - template: 'static/accessibility.{locale}.html.twig' + template: 'static/accessibility.html.twig' + options: + sitemap: + priority: 0.7 + changefreq: monthly + section: legal + +app_accessibility_dutch: + <<: *app_accessibility + path: /toegankelijkheid diff --git a/config/routes/scheb_2fa.yaml b/config/routes/scheb_2fa.yaml index 9a8ca667..9a733504 100644 --- a/config/routes/scheb_2fa.yaml +++ b/config/routes/scheb_2fa.yaml @@ -1,7 +1,7 @@ 2fa_login: - path: /2fa + path: /balie/2fa defaults: _controller: "scheb_two_factor.form_controller::form" 2fa_login_check: - path: /2fa_check + path: /balie/2fa_check diff --git a/config/services.yaml b/config/services.yaml index 36332ccb..d70963d3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -9,6 +9,8 @@ parameters: available_locales: ['en', 'nl'] + default_site_name: open.minvws.nl + services: _defaults: autowire: true # Automatically injects dependencies in your services. @@ -16,6 +18,7 @@ services: bind: $configPageLimit: '%configPageLimit%' + Psr\Log\LoggerInterface $logger: '@App\Service\Logging\EnrichedPsrLogger' App\: resource: '../src/' @@ -68,7 +71,7 @@ services: App\Service\Storage\DocumentStorageService: arguments: $storage: '@document.storage' - $isLocal: true + $isLocal: "@=env('STORAGE_DOCUMENT_ADAPTER')=='local' ? true : false" $documentRoot: '%document_path%' App\Service\Storage\ThumbnailStorageService: @@ -102,6 +105,9 @@ services: arguments: $redis: '@snc_redis.ingest' + App\Service\Worker\Pdf\Extractor\DecisionContentExtractor: + arguments: + $redis: '@snc_redis.ingest' App\Service\Worker\Pdf\Tools\Tika: arguments: @@ -140,4 +146,41 @@ services: App\Service\Logging\LoggingHelper: arguments: - - !tagged_iterator app.logging.type \ No newline at end of file + - !tagged_iterator app.logging.type + + App\Controller\StatsController: + arguments: + $redis: '@snc_redis.ingest' + $rabbitMqStatUrl: '%rabbitmq_stats_url%' + + + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + $redis: '@snc_redis.session' + + App\EventSubscriber\AppModeListener: + arguments: + $appMode: '%env(APP_MODE)%' + + App\Session\EncryptedSessionProxy: + arguments: + - '@Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler' + - '%env(APP_SECRET)%' + + App\Service\Logging\EnrichedPsrLogger: + arguments: + $logger: '@monolog.logger' + + Aws\S3\S3Client: + arguments: + - version: '2006-03-01' + region: '%env(STORAGE_MINIO_REGION)%' + endpoint: '%env(STORAGE_MINIO_ENDPOINT)%' + use_path_style_endpoint: true + credentials: + key: '%env(STORAGE_MINIO_ACCESS_KEY)%' + secret: '%env(STORAGE_MINIO_SECRET_KEY)%' + + App\EventSubscriber\SecurityHeaderSubscriber: + arguments: + $appMode: '%env(APP_MODE)%' diff --git a/docker/Dockerfile b/docker/Dockerfile index 8535eb59..af2d63a0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,7 +13,9 @@ RUN pecl install amqp && docker-php-ext-enable amqp RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer COPY woopie.conf /etc/apache2/sites-available/woopie.conf -RUN a2dissite 000-default && a2ensite woopie && service apache2 restart +COPY open-woopie.conf /etc/apache2/sites-available/open-woopie.conf +COPY balie-woopie.conf /etc/apache2/sites-available/balie-woopie.conf +RUN a2dissite 000-default && a2ensite woopie && a2ensite balie-woopie && a2ensite open-woopie && service apache2 restart # Install NodeJS RUN apt-get update -qq && \ diff --git a/docker/woopie.conf b/docker/woopie.conf index b8bc2116..db16b8e4 100644 --- a/docker/woopie.conf +++ b/docker/woopie.conf @@ -1,6 +1,8 @@ ServerName localhost + SetEnv APP_MODE both + DocumentRoot /var/www/html/public DirectoryIndex /index.php diff --git a/docs/install.md b/docs/install.md index a51e8519..182227ce 100644 --- a/docs/install.md +++ b/docs/install.md @@ -111,7 +111,7 @@ This will generate a password and 2fa token with which you can log into the webs ## Step 10: Browse to the site -When this is all done, you can goto the website at `http://localhost:8000/login`. You can log in with your +When this is all done, you can goto the website at `http://localhost:8000/balie/login`. You can log in with your generated credentials. See [usage](usage.md) for more information on how to use the application. diff --git a/docs/usage.md b/docs/usage.md index 337a6771..3e66bafb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -15,13 +15,13 @@ command: ``` Press ctrl-C when you feel you have enough documents. - + ### Create and upload real data On the Google drive (woo), you will find a folder called `Proof-of-concept-Woopie/test bestanden`. This folder contains a ZIP file with a number of PDF files and a few inventory XLS files. -You can create a dossier after logging in at `localhost:8000/balue/dossiers`. Fill in the dossier +You can create a dossier after logging in at `localhost:8000/balie/dossiers`. Fill in the dossier you want, and add a XLS file as inventory file. After this, you can upload the PDF files to the dossier. You can do this by clicking on the dossier diff --git a/package-lock.json b/package-lock.json index 32fc5708..6e78ed83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,28 +5,31 @@ "packages": { "": { "name": "woo", - "license": "UNLICENSED", + "license": "EUPL-1.2", "dependencies": { "@minvws/nl-rdo-rijksoverheid-ui-theme": "^0.0.14", "@popperjs/core": "^2.11.7", - "alpinejs": "^3.12.3", + "alpinejs": "^3.13.0", "dropzone": "^6.0.0-beta.2", "latte-carousel": "^1.6.1" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.18.6", - "@fortawesome/fontawesome-free": "^6.1.2", - "@hotwired/stimulus": "^3.1.0", + "@fortawesome/fontawesome-free": "^6.4.2", + "@hotwired/stimulus": "^3.2.2", "@symfony/stimulus-bridge": "^3.2.1", "@symfony/webpack-encore": "^4.4.0", - "autoprefixer": "^10.4.14", + "autoprefixer": "^10.4.15", "bootstrap": "^5.3.1", - "core-js": "^3.32.0", - "postcss": "^8.4.27", + "core-js": "^3.32.1", + "file-loader": "^6.2.0", + "lodash": "^4.17.21", + "markdownlint-cli2": "^0.9.2", + "postcss": "^8.4.29", "postcss-loader": "^7.3.3", "quoted-printable": "^1.0.1", - "regenerator-runtime": "^0.13.2", - "sass": "^1.64.1", + "regenerator-runtime": "^0.14.0", + "sass": "^1.66.1", "sass-loader": "^13.3.2", "tailwindcss": "^3.3.3", "webpack-notifier": "^1.6.0" @@ -1777,6 +1780,13 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "peer": true + }, "node_modules/@babel/template": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", @@ -1837,9 +1847,9 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz", - "integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", "dev": true, "hasInstallScript": true, "engines": { @@ -1847,9 +1857,9 @@ } }, "node_modules/@hotwired/stimulus": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.1.tgz", - "integrity": "sha512-HGlzDcf9vv/EQrMJ5ZG6VWNs8Z/xMN+1o2OhV1gKiSG6CqZt5MCBB1gRg5ILiN3U0jEAxuDTNPRfBcnZBDmupQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz", + "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==", "dev": true }, "node_modules/@hotwired/stimulus-webpack-helpers": { @@ -3016,9 +3026,9 @@ } }, "node_modules/alpinejs": { - "version": "3.12.3", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.12.3.tgz", - "integrity": "sha512-fLz2dfYQ3xCk7Ip8LiIpV2W+9brUyex2TAE7Z0BCvZdUDklJE+n+a8gCgLWzfZ0GzZNZu7HUP8Z0z6Xbm6fsSA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.0.tgz", + "integrity": "sha512-7FYR1Yz3evIjlJD1mZ3SYWSw+jlOmQGeQ1QiSufSQ6J84XMQFkzxm6OobiZ928SfqhGdoIp2SsABNsS4rXMMJw==", "dependencies": { "@vue/reactivity": "~3.1.1" } @@ -3132,9 +3142,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", + "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", "dev": true, "funding": [ { @@ -3144,11 +3154,15 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001520", "fraction.js": "^4.2.0", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -3381,9 +3395,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.9", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", - "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "funding": [ { @@ -3400,9 +3414,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001503", - "electron-to-chromium": "^1.4.431", - "node-releases": "^2.0.12", + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", "update-browserslist-db": "^1.0.11" }, "bin": { @@ -3483,9 +3497,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001508", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001508.tgz", - "integrity": "sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw==", + "version": "1.0.30001520", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz", + "integrity": "sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA==", "dev": true, "funding": [ { @@ -3764,9 +3778,9 @@ "dev": true }, "node_modules/core-js": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.0.tgz", - "integrity": "sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==", + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.1.tgz", + "integrity": "sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ==", "dev": true, "hasInstallScript": true, "funding": { @@ -4242,6 +4256,18 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4346,9 +4372,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.440", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.440.tgz", - "integrity": "sha512-r6dCgNpRhPwiWlxbHzZQ/d9swfPaEJGi8ekqRBwQYaR3WmA5VkqQfBWSDDjuJU1ntO+W9tHx8OHV/96Q8e0dVw==", + "version": "1.4.490", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz", + "integrity": "sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==", "dev": true }, "node_modules/emoji-regex": { @@ -4638,9 +4664,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4698,6 +4724,26 @@ "node": ">=0.8.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -5214,6 +5260,15 @@ "postcss": "^8.1.0" } }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -5713,6 +5768,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -5799,12 +5863,118 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdownlint": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.30.0.tgz", + "integrity": "sha512-nInuFvI/rEzanAOArW5490Ez4EYpB5ODqVM0mcDYCPx9DKJWCQqCgejjiCvbSeE7sjbDscVtZmwr665qpF5xGA==", + "dev": true, + "dependencies": { + "markdown-it": "13.0.1", + "markdownlint-micromark": "0.1.7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/markdownlint-cli2": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.9.2.tgz", + "integrity": "sha512-ndijEHIOikcs29W8068exHXlfkFviGFT/mPhREia7zSfQzHvTDkL2s+tWizvELjLHiKRO4KGTkkJyR3oeR8A5g==", + "dev": true, + "dependencies": { + "globby": "13.2.2", + "markdownlint": "0.30.0", + "markdownlint-cli2-formatter-default": "0.0.4", + "micromatch": "4.0.5", + "strip-json-comments": "5.0.1", + "yaml": "2.3.1" + }, + "bin": { + "markdownlint-cli2": "markdownlint-cli2.js", + "markdownlint-cli2-config": "markdownlint-cli2-config.js", + "markdownlint-cli2-fix": "markdownlint-cli2-fix.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/markdownlint-cli2-formatter-default": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.4.tgz", + "integrity": "sha512-xm2rM0E+sWgjpPn1EesPXx5hIyrN2ddUnUwnbCsD/ONxYtw3PX6LydvdH6dciWAoFDpwzbHM1TO7uHfcMd6IYg==", + "dev": true, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" + } + }, + "node_modules/markdownlint-cli2/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-micromark": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.7.tgz", + "integrity": "sha512-BbRPTC72fl5vlSKv37v/xIENSRDYL/7X/XoFzZ740FGEbs9vZerLrIkFRY0rv7slQKxDczToYuMmqQFN61fi4Q==", + "dev": true, + "engines": { + "node": ">=16" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -6121,9 +6291,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "node_modules/normalize-path": { @@ -6542,9 +6712,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", "dev": true, "funding": [ { @@ -7378,9 +7548,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", "dev": true }, "node_modules/regenerator-transform": { @@ -7614,9 +7784,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.64.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz", - "integrity": "sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==", + "version": "1.66.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz", + "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -7934,6 +8104,18 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -8062,6 +8244,18 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", + "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -8351,15 +8545,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -8568,6 +8753,12 @@ "node": ">= 0.6" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -9156,6 +9347,15 @@ "dev": true, "peer": true }, + "node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/package.json b/package.json index d8e73d99..44ae7ebf 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,21 @@ }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.18.6", - "@fortawesome/fontawesome-free": "^6.1.2", - "@hotwired/stimulus": "^3.1.0", + "@fortawesome/fontawesome-free": "^6.4.2", + "@hotwired/stimulus": "^3.2.2", "@symfony/stimulus-bridge": "^3.2.1", "@symfony/webpack-encore": "^4.4.0", - "autoprefixer": "^10.4.14", + "autoprefixer": "^10.4.15", "bootstrap": "^5.3.1", - "core-js": "^3.32.0", - "postcss": "^8.4.27", + "core-js": "^3.32.1", + "file-loader": "^6.2.0", + "lodash": "^4.17.21", + "markdownlint-cli2": "^0.9.2", + "postcss": "^8.4.29", "postcss-loader": "^7.3.3", "quoted-printable": "^1.0.1", - "regenerator-runtime": "^0.13.2", - "sass": "^1.64.1", + "regenerator-runtime": "^0.14.0", + "sass": "^1.66.1", "sass-loader": "^13.3.2", "tailwindcss": "^3.3.3", "webpack-notifier": "^1.6.0" @@ -28,12 +31,13 @@ "dev-server": "encore dev-server", "n": "encore dev", "watch": "encore dev --watch", - "build": "encore production --progress" + "build": "encore production --progress", + "mdlint": "markdownlint-cli2 '**/*.md' '!vendor/**' '!node_modules/**'" }, "dependencies": { "@minvws/nl-rdo-rijksoverheid-ui-theme": "^0.0.14", "@popperjs/core": "^2.11.7", - "alpinejs": "^3.12.3", + "alpinejs": "^3.13.0", "dropzone": "^6.0.0-beta.2", "latte-carousel": "^1.6.1" } diff --git a/public/placeholder.png b/public/placeholder.png index 21f7f2cd..6f4af848 100644 Binary files a/public/placeholder.png and b/public/placeholder.png differ diff --git a/ruleset.phpmd.xml b/ruleset.phpmd.xml index c2306016..6104c084 100644 --- a/ruleset.phpmd.xml +++ b/ruleset.phpmd.xml @@ -28,7 +28,7 @@ - + diff --git a/src/Citation.php b/src/Citation.php index e7aedf87..1f97f221 100644 --- a/src/Citation.php +++ b/src/Citation.php @@ -9,33 +9,80 @@ */ class Citation { + public const DUBBEL = 'dubbel'; + /** @var array|string[] */ - public static array $citations = [ + public static array $wooCitations = [ '5.1.1a' => 'Eenheid van de Kroon', '5.1.1b' => 'Veiligheid van de Staat', - '5.1.1c' => 'Vertrouwelijke bedrijfs- of fabricagegegevens', - '5.1.1d' => 'Persoonsgegevens', - '5.1.1e' => 'Persoonlijke identificatienummers', + '5.1.1c' => 'Vertrouwelijk verstrekte bedrijfs- of fabricagegegevens', + '5.1.1d' => 'Bijzondere persoonsgegevens', + '5.1.1e' => 'Nationale identificatienummers', '5.1.2a' => 'Internationale betrekkingen', - '5.1.2b' => 'Economische of financiële belangen van de overheid', + '5.1.2b' => 'Economische of financiële belangen van de Staat', '5.1.2c' => 'Opsporing en vervolging van strafbare feiten', - '5.1.2d' => 'Inspectie, controle en toezicht', + '5.1.2d' => 'Inspectie, controle en toezicht van bestuursorganen', '5.1.2e' => 'Eerbiediging van de persoonlijke levenssfeer', - '5.1.2f' => 'Concurrentiegevoelige bedrijfs- en fabricagegegevens', - '5.1.2g' => 'Bescherming van het milieu', - '5.1.2h' => 'Beveiliging van personen en bedrijven', - '5.1.2i' => 'Goed functioneren van de overheid', + '5.1.2f' => 'Bescherming van andere dan vertrouwelijk aan de overheid verstrekte concurrentiegevoelige bedrijfs- en fabricagegevens', + '5.1.2g' => 'De bescherming van het milieu waarop deze informatie betrekking heeft', + '5.1.2h' => 'De beveiliging van personen of bedrijven en het voorkomen van sabotage', + '5.1.2i' => 'Het goed functioneren van de staat, andere publiekrechtelijke lichamen of bestuursorganen', + '5.1.5' => 'Het voorkomen van onevenredige benadeling', + '5.2' => 'Persoonlijke beleidsopvattingen', + self::DUBBEL => 'Dubbel: inhoud is in een ander document al beoordeeld', + ]; + + /** @var array|string[] */ + public static array $wobCitations = [ + '10.1.a' => 'Eenheid van de Kroon', + '10.1.b' => 'Veiligheid van de Staat', + '10.1.c' => 'Vertrouwelijk verstrekte bedrijfs- en fabricagegegevens', + '10.1.d' => 'Bijzondere persoonsgegevens', + '10.2.a' => 'Internationale betrekkingen', + '10.2.b' => 'Economische of financiële belangen van de Staat', + '10.2.c' => 'Opsporing en vervolging van strafbare feiten', + '10.2.d' => 'Inspectie, controle en toezicht door bestuursorganen', + '10.2.e' => 'Eerbiediging van de persoonlijke levenssfeer', + '10.2.g' => 'Het voorkomen van onevenredige bevoordeling of benadeling', + '11.1' => 'Persoonlijke beleidsopvattingen', + self::DUBBEL => 'Dubbel: inhoud is in een ander document al beoordeeld', ]; + /** + * Converts a given citation to a human-readable classification. + */ public static function toClassification(string $citation): string + { + $canonical = str_replace(' ', '', $citation); + $canonical = strtolower($canonical); + + if (isset(self::$wobCitations[$canonical])) { + return self::$wobCitations[$canonical]; + } + + if (isset(self::$wooCitations[$canonical])) { + return self::$wooCitations[$canonical]; + } + + // Unknown citations get no classification intentionally + return ''; + } + + /** + * Returns citation type: woo, wob or unknown. + */ + public static function getCitationType(string $citation): string { $citation = str_replace(' ', '', $citation); $citation = strtolower($citation); - if (isset(self::$citations[$citation])) { - return self::$citations[$citation]; + if (isset(self::$wooCitations[$citation])) { + return 'woo'; + } + if (isset(self::$wobCitations[$citation])) { + return 'wob'; } - return "Onbekende reden ($citation)"; + return 'unknown'; } } diff --git a/src/Command/CleanSheet.php b/src/Command/CleanSheet.php index a274bf21..69a994d4 100644 --- a/src/Command/CleanSheet.php +++ b/src/Command/CleanSheet.php @@ -5,6 +5,7 @@ namespace App\Command; use App\Entity\Document; +use App\Entity\DocumentPrefix; use App\Entity\Dossier; use App\Entity\IngestLog; use App\Entity\Inquiry; @@ -44,6 +45,7 @@ protected function configure(): void ->setDefinition([ new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'), new InputOption('users', 'u', InputOption::VALUE_NONE, 'Reset users'), + new InputOption('prefixes', 'p', InputOption::VALUE_NONE, 'Reset prefixes'), new InputOption('index', 'i', InputOption::VALUE_REQUIRED, 'ES index name', 'woopie'), ]) ; @@ -80,6 +82,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->deleteAllEntities(User::class, $output); } + if ($input->getOption('prefixes')) { + $this->deleteAllEntities(DocumentPrefix::class, $output); + } + return 0; } diff --git a/src/Command/GenerateDocuments.php b/src/Command/GenerateDocuments.php index e4127188..83a046db 100644 --- a/src/Command/GenerateDocuments.php +++ b/src/Command/GenerateDocuments.php @@ -57,7 +57,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $dossierInquiries = $this->pickInquiries($inquiries, $i, 3); print "Creating dossier $i / $numberOfDossiers\n"; - $dossier = $this->createDossier('MINVWS-' . random_int(1000, 9999) . '-' . random_int(10000, 99999), $dossierInquiries); + $dossier = $this->createDossier('DOSSIER-' . random_int(1000, 9999) . '-' . random_int(10000, 99999), $dossierInquiries); $docCount = random_int(10, 100); for ($j = 0; $j !== $docCount; $j++) { diff --git a/src/Command/LoadFixture.php b/src/Command/LoadFixture.php index cd0746b7..04ccf83c 100644 --- a/src/Command/LoadFixture.php +++ b/src/Command/LoadFixture.php @@ -37,7 +37,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $file = strval($input->getArgument('file')); if (! file_exists($file)) { $output->writeln("File $file does not exist"); - $output->writeln("File $file does not exist"); return 1; } diff --git a/src/Command/UploadDocument.php b/src/Command/UploadDocument.php index 5fa509f7..ddd51171 100644 --- a/src/Command/UploadDocument.php +++ b/src/Command/UploadDocument.php @@ -5,7 +5,7 @@ namespace App\Command; use App\Entity\Dossier; -use App\Service\DocumentService; +use App\Service\FileProcessService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -14,14 +14,14 @@ class UploadDocument extends Command { - protected DocumentService $documentService; + protected FileProcessService $fileProcessService; protected EntityManagerInterface $doctrine; - public function __construct(DocumentService $documentService, EntityManagerInterface $doctrine) + public function __construct(FileProcessService $fileProcessService, EntityManagerInterface $doctrine) { parent::__construct(); - $this->documentService = $documentService; + $this->fileProcessService = $fileProcessService; $this->doctrine = $doctrine; } @@ -50,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $path = strval($input->getArgument('path')); $file = new \SplFileInfo($path); - $this->documentService->processDocument($file, $dossier, $path); + $this->fileProcessService->processFile($file, $dossier, $path); return 0; } diff --git a/src/Command/User/Create.php b/src/Command/User/Create.php index d6225b81..257c03f4 100644 --- a/src/Command/User/Create.php +++ b/src/Command/User/Create.php @@ -51,10 +51,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif ($input->getOption('admin')) { $role = ['ROLE_ADMIN']; } else { - $role = ['ROLE_USER']; + $role = ['ROLE_USER', 'ROLE_BALIE']; } - ['plainPassword' => $plainPassword, 'user' => $user ] = $this->userService->createUser( + ['plainPassword' => $plainPassword, 'user' => $user] = $this->userService->createUser( strval($input->getArgument('name')), strval($input->getArgument('email')), $role, diff --git a/src/Command/Where.php b/src/Command/Where.php index 734b9bea..17a48a3a 100644 --- a/src/Command/Where.php +++ b/src/Command/Where.php @@ -76,8 +76,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } $output->writeln("Document : {$document->getId()}"); - $output->writeln("Filename : {$document->getFilename()}"); - $output->writeln("Path : {$document->getFilepath()}"); + $output->writeln("Filename : {$document->getFileInfo()->getName()}"); + $output->writeln("Path : {$document->getFileInfo()->getPath()}"); $output->writeln(''); } diff --git a/src/Controller/Admin/DepartmentController.php b/src/Controller/Admin/DepartmentController.php index 2ce95f8d..1d080ce0 100644 --- a/src/Controller/Admin/DepartmentController.php +++ b/src/Controller/Admin/DepartmentController.php @@ -67,7 +67,7 @@ public function create(Breadcrumbs $breadcrumbs, Request $request): Response $this->doctrine->persist($department); $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Department created')); + $this->addFlash('backend', ['success' => $this->translator->trans('Department created')]); return $this->redirectToRoute('app_admin_departments'); } @@ -93,7 +93,7 @@ public function createHead(Breadcrumbs $breadcrumbs, Request $request): Response $this->doctrine->persist($governmentOfficial); $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Official created')); + $this->addFlash('backend', ['success' => $this->translator->trans('Official created')]); return $this->redirectToRoute('app_admin_departments'); } @@ -118,7 +118,7 @@ public function modifyHead(Breadcrumbs $breadcrumbs, Request $request, Governmen $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Official modified')); + $this->addFlash('backend', ['success' => $this->translator->trans('Official modified')]); $this->messageBus->dispatch(new UpdateOfficialMessage($oldHead, $head)); @@ -146,7 +146,7 @@ public function modify(Breadcrumbs $breadcrumbs, Request $request, Department $d $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Department modified')); + $this->addFlash('backend', ['success' => $this->translator->trans('Department modified')]); $this->messageBus->dispatch(new UpdateDepartmentMessage($oldDepartment, $department)); diff --git a/src/Controller/Admin/Dossier/DocumentController.php b/src/Controller/Admin/Dossier/DocumentController.php index e49ea20f..690dcf00 100644 --- a/src/Controller/Admin/Dossier/DocumentController.php +++ b/src/Controller/Admin/Dossier/DocumentController.php @@ -6,8 +6,10 @@ use App\Entity\Document; use App\Entity\Dossier; +use App\Entity\WithdrawReason; use App\Form\Document\IngestFormType; use App\Form\Document\RemoveFormType; +use App\Form\Document\WithdrawFormType; use App\Form\Dossier\DocumentUploadType; use App\Service\DocumentService; use App\Service\FileUploader; @@ -27,21 +29,12 @@ */ class DocumentController extends AbstractController { - protected DocumentService $documentService; - protected IngestService $ingester; - protected FileUploader $fileUploader; - protected TranslatorInterface $translator; - public function __construct( - DocumentService $documentService, - IngestService $ingester, - FileUploader $fileUploader, - TranslatorInterface $translator + private readonly DocumentService $documentService, + private readonly IngestService $ingester, + private readonly FileUploader $fileUploader, + private readonly TranslatorInterface $translator ) { - $this->documentService = $documentService; - $this->ingester = $ingester; - $this->fileUploader = $fileUploader; - $this->translator = $translator; } #[Route('/balie/dossier/{dossierId}/documents', name: 'app_admin_dossier_documents_edit', methods: ['GET'])] @@ -80,6 +73,7 @@ public function upload( return $this->render('admin/dossier/document-status.html.twig', [ 'dossier' => $dossier, + 'uploadStatus' => $dossier->getUploadStatus(), ]); } @@ -106,7 +100,7 @@ public function details( if ($removeForm->isSubmitted() && $removeForm->isValid()) { $this->documentService->removeDocumentFromDossier($dossier, $document); - $this->addFlash('success', $this->translator->trans('Document has been removed')); + $this->addFlash('backend', ['success' => $this->translator->trans('Document has been removed')]); return $this->redirectToRoute('app_admin_dossier_edit', ['dossierId' => $dossier->getDossierNr()]); } @@ -115,7 +109,7 @@ public function details( if ($ingestForm->isSubmitted() && $ingestForm->isValid()) { $this->ingester->ingest($document, new Options()); - $this->addFlash('success', $this->translator->trans('Document is scheduled for ingestion')); + $this->addFlash('backend', ['success' => $this->translator->trans('Document is scheduled for ingestion')]); return $this->redirectToRoute('app_admin_dossier_edit', ['dossierId' => $dossier->getDossierNr()]); } @@ -127,4 +121,59 @@ public function details( 'ingestForm' => $ingestForm->createView(), ]); } + + #[Route('/balie/dossier/{dossierId}/document/{documentId}/withdraw', name: 'app_admin_dossier_document_withdraw', methods: ['GET', 'POST'])] + public function docWithdraw( + Breadcrumbs $breadcrumbs, + #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier, + #[MapEntity(mapping: ['documentId' => 'documentNr'])] Document $document, + Request $request, + ): Response { + if (! $dossier->getDocuments()->contains($document)) { + throw new NotFoundHttpException('Document not found'); + } + + if ($document->isWithdrawn()) { + $this->addFlash('backend', ['error' => $this->translator->trans('Document is already withdrawn')]); + + return $this->redirectToRoute( + 'app_admin_dossier_document_details', + ['dossierId' => $dossier->getDossierNr(), 'documentId' => $document->getDocumentNr()] + ); + } + + $form = $this->createForm(WithdrawFormType::class); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var WithdrawReason $reason */ + $reason = $form->get('reason')->getData(); + + /** @var string $explanation */ + $explanation = $form->get('explanation')->getData(); + + $this->documentService->withdraw($document, $reason, $explanation); + $this->addFlash('backend', ['success' => $this->translator->trans('Document has been withdrawn')]); + + return $this->redirectToRoute( + 'app_admin_dossier_document_details', + ['dossierId' => $dossier->getDossierNr(), 'documentId' => $document->getDocumentNr()] + ); + } + + $breadcrumbs->addRouteItem('Home', 'app_home'); + $breadcrumbs->addRouteItem('Admin', 'app_admin'); + $breadcrumbs->addRouteItem('Dossier management', 'app_admin_dossiers'); + $breadcrumbs->addRouteItem( + 'Document', + 'app_admin_dossier_document_details', + ['dossierId' => $dossier->getDossierNr(), 'documentId' => $document->getDocumentNr()] + ); + $breadcrumbs->addItem('Intrekken'); + + return $this->render('admin/dossier/document-withdraw.html.twig', [ + 'dossier' => $dossier, + 'document' => $document, + 'form' => $form->createView(), + ]); + } } diff --git a/src/Controller/Admin/Dossier/DossierController.php b/src/Controller/Admin/Dossier/DossierController.php index bb30bcf5..7b1c3fa2 100644 --- a/src/Controller/Admin/Dossier/DossierController.php +++ b/src/Controller/Admin/Dossier/DossierController.php @@ -4,6 +4,7 @@ namespace App\Controller\Admin\Dossier; +use App\Entity\Document; use App\Entity\Dossier; use App\Form\Document\IngestFormType; use App\Form\Document\RemoveFormType; @@ -11,18 +12,18 @@ use App\Form\Dossier\SearchFormType; use App\Form\Dossier\StateChangeFormType; use App\Service\DossierService; -use App\Service\Elastic\ElasticService; -use App\Service\Ingest\IngestService; -use App\Service\Ingest\Options; +use App\Service\Inventory\ProcessInventoryResult; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Knp\Component\Pager\PaginatorInterface; +use Psr\Log\LoggerInterface; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -33,24 +34,14 @@ */ class DossierController extends AbstractController { - protected EntityManagerInterface $doctrine; - protected PaginatorInterface $paginator; - protected DossierService $dossierService; - protected IngestService $ingester; - protected ElasticService $elasticService; + protected const MAX_ITEMS_PER_PAGE = 100; public function __construct( - EntityManagerInterface $doctrine, - PaginatorInterface $paginator, - DossierService $dossierService, - IngestService $ingester, - ElasticService $elasticService, + private readonly EntityManagerInterface $doctrine, + private readonly PaginatorInterface $paginator, + private readonly DossierService $dossierService, + private readonly LoggerInterface $logger, ) { - $this->doctrine = $doctrine; - $this->paginator = $paginator; - $this->dossierService = $dossierService; - $this->ingester = $ingester; - $this->elasticService = $elasticService; } #[Route('/balie/dossiers', name: 'app_admin_dossiers', methods: ['GET'])] @@ -72,7 +63,7 @@ public function index(Breadcrumbs $breadcrumbs, Request $request): Response $pagination = $this->paginator->paginate( $query, $request->query->getInt('page', 1), - 10 + self::MAX_ITEMS_PER_PAGE, ); return $this->render('admin/dossier/index.html.twig', [ @@ -81,6 +72,30 @@ public function index(Breadcrumbs $breadcrumbs, Request $request): Response ]); } + #[Route('/balie/dossiers/search', name: 'app_admin_dossiers_search', methods: ['POST'])] + public function search(Request $request): Response + { + $searchTerm = urldecode(strval($request->query->get('q', ''))); + + $dossiers = $this->doctrine->getRepository(Dossier::class)->findBySearchTerm($searchTerm, 4); + $documents = $this->doctrine->getRepository(Document::class)->findBySearchTerm($searchTerm, 4); + + $ret = [ + 'results' => json_encode( + $this->renderView( + 'admin/dossier/search.html.twig', + [ + 'dossiers' => $dossiers, + 'documents' => $documents, + ], + ), + JSON_THROW_ON_ERROR, + ), + ]; + + return new JsonResponse($ret); + } + #[Route('/balie/dossier/new', name: 'app_admin_dossier_new', methods: ['GET', 'POST'])] public function new(Breadcrumbs $breadcrumbs, Request $request): Response { @@ -94,18 +109,38 @@ public function new(Breadcrumbs $breadcrumbs, Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - /** @var UploadedFile $file */ - $file = $form->get('inventory')->getData(); - $errors = $this->dossierService->create($dossier, $file); + /** @var UploadedFile $inventoryUpload */ + $inventoryUpload = $form->get('inventory')->getData(); + /** @var UploadedFile $decisionUpload */ + $decisionUpload = $form->get('decision_document')->getData(); + + if ($inventoryUpload) { + $this->logger->info('uploaded inventory file', [ + 'path' => $inventoryUpload->getRealPath(), + 'original_file' => $inventoryUpload->getClientOriginalName(), + 'size' => $inventoryUpload->getSize(), + 'file_hash' => hash_file('sha256', $inventoryUpload->getRealPath()), + ]); + } - if (! count($errors)) { + if ($decisionUpload) { + $this->logger->info('uploaded decision file', [ + 'path' => $decisionUpload->getRealPath(), + 'original_file' => $decisionUpload->getClientOriginalName(), + 'size' => $decisionUpload->getSize(), + 'file_hash' => hash_file('sha256', $decisionUpload->getRealPath()), + ]); + } + + $result = $this->dossierService->create($dossier, $inventoryUpload, $decisionUpload); + if ($result->isSuccessful()) { // All is good, we can safely return to dossier list - $this->addFlash('success', 'Dossier has been created successfully'); + $this->addFlash('backend', ['success' => 'Dossier has been created successfully']); return $this->redirectToRoute('app_admin_dossiers'); } - $this->addFormErrors($form, $errors); + $this->addFormErrors($form, $result); } return $this->render('admin/dossier/new.html.twig', [ @@ -113,7 +148,22 @@ public function new(Breadcrumbs $breadcrumbs, Request $request): Response ]); } - #[Route('/balie/dossier/{dossierId}', name: 'app_admin_dossier_edit', methods: ['GET', 'POST'])] + #[Route('/balie/dossier/{dossierId}', name: 'app_admin_dossier', methods: ['GET'])] + public function dossier( + Breadcrumbs $breadcrumbs, + #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier + ): Response { + $breadcrumbs->addRouteItem('Home', 'app_home'); + $breadcrumbs->addRouteItem('Balie', 'app_admin'); + $breadcrumbs->addRouteItem('Dossier management', 'app_admin_dossiers'); + $breadcrumbs->addItem('View dossier'); + + return $this->render('admin/dossier/view.html.twig', [ + 'dossier' => $dossier, + ]); + } + + #[Route('/balie/dossier/{dossierId}/edit', name: 'app_admin_dossier_edit', methods: ['GET', 'POST'])] public function edit( Breadcrumbs $breadcrumbs, Request $request, @@ -148,15 +198,6 @@ public function edit( protected function applyFilter(FormInterface $form, QueryBuilder $queryBuilder): void { - $searchTerm = strval($form->get('searchterm')->getData()); - if (! empty($searchTerm)) { - $queryBuilder->andWhere('LOWER(dos.title) LIKE :filter - OR LOWER(dos.status) LIKE :filter - OR LOWER(dos.dossierNr) LIKE :filter - OR inq.casenr LIKE :filter') - ->setParameter('filter', '%' . strtolower($searchTerm) . '%'); - } - /** @var string[] $statusFilters */ $statusFilters = $form->get('status')->getData(); if (is_array($statusFilters) && count($statusFilters) > 0) { @@ -187,12 +228,12 @@ protected function handleStateForm(Request $request, Dossier $dossier): ?Respons try { $this->dossierService->changeState($dossier, strval($stateForm->get('state')->getData())); } catch (\Exception $e) { - $this->addFlash('danger', 'Dossier status could not be changed due to incorrect state: ' . $e->getMessage()); + $this->addFlash('backend', ['success' => 'Dossier status could not be changed due to incorrect state: ' . $e->getMessage()]); return $this->redirectToRoute('app_admin_dossiers'); } - $this->addFlash('success', 'Dossier status has been changed'); + $this->addFlash('backend', ['success' => 'Dossier status has been changed']); return $this->redirectToRoute('app_admin_dossiers'); } @@ -207,7 +248,7 @@ protected function handleRemoveForm(Request $request, Dossier $dossier): ?Respon } $this->dossierService->remove($dossier); - $this->addFlash('success', 'Dossier has been removed'); + $this->addFlash('backend', ['success' => 'Dossier has been removed']); return $this->redirectToRoute('app_admin_dossiers'); } @@ -221,12 +262,9 @@ protected function handleIngestForm(Request $request, Dossier $dossier): ?Respon return null; } - $this->elasticService->updateDossier($dossier, false); - foreach ($dossier->getDocuments() as $document) { - $this->ingester->ingest($document, new Options()); - } + $this->dossierService->dispatchIngest($dossier); - $this->addFlash('success', 'Dossier is scheduled for ingestion'); + $this->addFlash('backend', ['success' => 'Dossier is scheduled for ingestion']); return $this->redirectToRoute('app_admin_dossiers'); } @@ -240,36 +278,53 @@ protected function handleUpdateForm(Request $request, Dossier $dossier): ?Respon return null; } - /** @var UploadedFile $file */ - $file = $form->get('inventory')->getData(); - $errors = $this->dossierService->update($dossier, $file); + /** @var UploadedFile $inventoryUpload */ + $inventoryUpload = $form->get('inventory')->getData(); + /** @var UploadedFile $decisionUpload */ + $decisionUpload = $form->get('decision_document')->getData(); + + if ($decisionUpload) { + $this->logger->info('uploaded decision file', [ + 'path' => $decisionUpload->getRealPath(), + 'original_file' => $decisionUpload->getClientOriginalName(), + 'size' => $decisionUpload->getSize(), + 'file_hash' => hash_file('sha256', $decisionUpload->getRealPath()), + ]); + } + + if ($inventoryUpload) { + $this->logger->info('uploaded inventory file', [ + 'path' => $inventoryUpload->getRealPath(), + 'original_file' => $inventoryUpload->getClientOriginalName(), + 'size' => $inventoryUpload->getSize(), + 'file_hash' => hash_file('sha256', $inventoryUpload->getRealPath()), + ]); + } - if (! count($errors)) { + $result = $this->dossierService->update($dossier, $inventoryUpload, $decisionUpload); + + if ($result->isSuccessful()) { // All is good, we can safely return to dossier list - $this->addFlash('success', 'Dossier has been updated successfully'); + $this->addFlash('backend', ['success' => 'Dossier has been updated successfully']); return $this->redirectToRoute('app_admin_dossiers'); } // Add errors to form - $this->addFormErrors($form->get('inventory'), $errors); + $this->addFormErrors($form->get('inventory'), $result); return null; } - /** - * @param array $errors - */ - protected function addFormErrors(FormInterface $form, array $errors): void + protected function addFormErrors(FormInterface $form, ProcessInventoryResult $result): void { - // Add all errors to the form - foreach ($errors as $lineNum => $lineErrors) { + foreach ($result->getGenericErrors() as $errorMessage) { + $form->addError(new FormError($errorMessage)); + } + + foreach ($result->getRowErrors() as $lineNum => $lineErrors) { foreach ($lineErrors as $error) { - if ($lineNum == 0) { - $form->addError(new FormError($error)); - } else { - $form->addError(new FormError(sprintf('Line %d: %s', $lineNum, $error))); - } + $form->addError(new FormError(sprintf('Line %d: %s', $lineNum, $error))); } } } diff --git a/src/Controller/Admin/ElasticController.php b/src/Controller/Admin/ElasticController.php index 05fd5b98..b1a6d231 100644 --- a/src/Controller/Admin/ElasticController.php +++ b/src/Controller/Admin/ElasticController.php @@ -77,7 +77,7 @@ public function create(Breadcrumbs $breadcrumbs, Request $request): Response ); $this->bus->dispatch($message); - $this->addFlash('success', $this->translator->trans('Elasticsearch rollover initiated')); + $this->addFlash('backend', ['success' => $this->translator->trans('Elasticsearch rollover initiated')]); return $this->redirectToRoute('app_admin_elastic'); } @@ -97,7 +97,7 @@ public function details(Breadcrumbs $breadcrumbs, string $indexName): Response $indices = $this->indexService->find($indexName); if (empty($indices)) { - $this->addFlash('error', 'Invalid elasticsearch index'); + $this->addFlash('backend', ['error' => 'Invalid elasticsearch index']); return $this->redirectToRoute('app_admin_elastic'); } @@ -121,7 +121,7 @@ public function makeLive(Breadcrumbs $breadcrumbs, Request $request, string $ind $indices = $this->indexService->find($indexName); if (empty($indices)) { - $this->addFlash('error', 'Invalid elasticsearch index'); + $this->addFlash('backend', ['error' => 'Invalid elasticsearch index']); return $this->redirectToRoute('app_admin_elastic'); } @@ -144,7 +144,7 @@ public function makeLive(Breadcrumbs $breadcrumbs, Request $request, string $ind ); $this->bus->dispatch($message); - $this->addFlash('success', $this->translator->trans('Elasticsearch index switch initiated')); + $this->addFlash('backend', ['success' => $this->translator->trans('Elasticsearch index switch initiated')]); return $this->redirectToRoute('app_admin_elastic'); } diff --git a/src/Controller/Admin/PrefixController.php b/src/Controller/Admin/PrefixController.php index 3b13fcff..9f493243 100644 --- a/src/Controller/Admin/PrefixController.php +++ b/src/Controller/Admin/PrefixController.php @@ -55,7 +55,7 @@ public function create(Breadcrumbs $breadcrumbs, Request $request): Response $this->doctrine->persist($prefix); $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Prefix created')); + $this->addFlash('backend', ['success' => $this->translator->trans('Prefix created')]); return $this->redirectToRoute('app_admin_prefixes'); } @@ -78,7 +78,7 @@ public function editPrefix(Breadcrumbs $breadcrumbs, Request $request, DocumentP $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Prefix modified')); + $this->addFlash('backend', ['success' => $this->translator->trans('Prefix modified')]); return $this->redirectToRoute('app_admin_prefixes'); } diff --git a/src/Controller/Admin/RequestController.php b/src/Controller/Admin/RequestController.php index 214b856a..8e1743eb 100644 --- a/src/Controller/Admin/RequestController.php +++ b/src/Controller/Admin/RequestController.php @@ -18,6 +18,8 @@ class RequestController extends AbstractController { + protected const MAX_ITEMS_PER_PAGE = 100; + protected EntityManagerInterface $doctrine; protected UserService $userService; protected PaginatorInterface $paginator; @@ -41,7 +43,7 @@ public function index(Breadcrumbs $breadcrumbs, Request $request): Response $pagination = $this->paginator->paginate( $query, $request->query->getInt('page', 1), - 10 + self::MAX_ITEMS_PER_PAGE ); return $this->render('admin/request/index.html.twig', [ diff --git a/src/Controller/Admin/TokenController.php b/src/Controller/Admin/TokenController.php index cfa35157..a67a542d 100644 --- a/src/Controller/Admin/TokenController.php +++ b/src/Controller/Admin/TokenController.php @@ -56,7 +56,7 @@ public function create(Breadcrumbs $breadcrumbs, Request $request): Response $this->doctrine->persist($token); $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Token created')); + $this->addFlash('backend', ['success' => $this->translator->trans('Token created')]); return $this->redirectToRoute('app_admin_tokens'); } @@ -79,7 +79,7 @@ public function modify(Breadcrumbs $breadcrumbs, Request $request, Token $token) $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('Token modified')); + $this->addFlash('backend', ['success' => $this->translator->trans('Token modified')]); return $this->redirectToRoute('app_admin_tokens'); } diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php index d74eacf4..5e152017 100644 --- a/src/Controller/Admin/UserController.php +++ b/src/Controller/Admin/UserController.php @@ -64,7 +64,7 @@ public function create(Breadcrumbs $breadcrumbs, Request $request): Response if ($userForm->isSubmitted() && $userForm->isValid()) { /** @var array{name: string, email: string, roles: string[]} $data */ $data = $userForm->getData(); - ['plainPassword' => $plainPassword, 'user' => $user ] = $this->userService->createUser( + ['plainPassword' => $plainPassword, 'user' => $user] = $this->userService->createUser( name: $data['name'], email: $data['email'], roles: $data['roles'] @@ -91,7 +91,7 @@ public function modify(Breadcrumbs $breadcrumbs, Request $request, User $user): $breadcrumbs->addItem('Edit user'); if ($user === $this->getUser()) { - $this->addFlash('danger', $this->translator->trans('Modifying your own account is not allowed')); + $this->addFlash('backend', ['warning' => $this->translator->trans('Modifying your own account is not allowed')]); return $this->redirectToRoute('app_admin_users'); } @@ -133,7 +133,7 @@ protected function handleInfoForm(Request $request, User $user): ?Response } $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('The user has been modified')); + $this->addFlash('backend', ['success' => $this->translator->trans('The user has been modified')]); return $this->redirectToRoute('app_admin'); } @@ -148,7 +148,7 @@ protected function handleRoleForm(Request $request, User $user): ?Response } $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('User roles have been modified')); + $this->addFlash('backend', ['success' => $this->translator->trans('User roles have been modified')]); return $this->redirectToRoute('app_admin'); } @@ -164,7 +164,7 @@ protected function handleDisableForm(Request $request, User $user): ?Response $user->setEnabled(false); $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('User has been disabled')); + $this->addFlash('backend', ['success' => $this->translator->trans('User has been disabled')]); return $this->redirectToRoute('app_admin'); } @@ -180,7 +180,7 @@ protected function handleEnableForm(Request $request, User $user): ?Response $user->setEnabled(true); $this->doctrine->flush(); - $this->addFlash('success', $this->translator->trans('User has been enabled')); + $this->addFlash('backend', ['success' => $this->translator->trans('User has been enabled')]); return $this->redirectToRoute('app_admin'); } diff --git a/src/Controller/DocumentController.php b/src/Controller/DocumentController.php index 848788a7..062783fb 100644 --- a/src/Controller/DocumentController.php +++ b/src/Controller/DocumentController.php @@ -6,10 +6,11 @@ use App\Entity\Document; use App\Entity\Dossier; +use App\Repository\DocumentRepository; +use App\Service\DossierService; use App\Service\Search\SearchService; use App\Service\Storage\DocumentStorageService; use App\Service\Storage\ThumbnailStorageService; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -21,24 +22,14 @@ class DocumentController extends AbstractController { - protected EntityManagerInterface $doctrine; - protected DocumentStorageService $documentStorage; - protected ThumbnailStorageService $thumbnailStorage; - protected SearchService $searchService; - protected TranslatorInterface $translator; - public function __construct( - EntityManagerInterface $doctrine, - DocumentStorageService $documentStorage, - ThumbnailStorageService $thumbnailStorage, - SearchService $searchService, - TranslatorInterface $translator + private readonly DocumentStorageService $documentStorage, + private readonly ThumbnailStorageService $thumbnailStorage, + private readonly SearchService $searchService, + private readonly TranslatorInterface $translator, + private readonly DocumentRepository $documentRepository, + private readonly DossierService $dossierService, ) { - $this->doctrine = $doctrine; - $this->documentStorage = $documentStorage; - $this->thumbnailStorage = $thumbnailStorage; - $this->searchService = $searchService; - $this->translator = $translator; } #[Route('/dossier/{dossierId}/document/{documentId}', name: 'app_document_detail', methods: ['GET'])] @@ -51,24 +42,20 @@ public function detail( $breadcrumbs->addRouteItem('Dossier', 'app_dossier_detail', ['dossierId' => $dossier->getDossierNr()]); $breadcrumbs->addItem('Document'); - if (! $dossier->getDocuments()->contains($document)) { - throw new NotFoundHttpException('Document not found'); + if (! $this->dossierService->isViewingAllowed($dossier, $document)) { + throw $this->createNotFoundException('Document or dossier not found'); } - $thread = $this->doctrine->getRepository(Document::class)->findBy(['threadId' => $document->getThreadId()], ['documentDate' => 'ASC']); - $family = $this->doctrine->getRepository(Document::class)->findBy(['familyId' => $document->getFamilyId()], ['documentDate' => 'ASC']); - - // This could be easier with a criteria - $family = array_filter($family, function (Document $doc) use ($document) { - return $doc->getDocumentNr() !== $document->getDocumentNr(); - }); + if (! $dossier->getDocuments()->contains($document)) { + throw new NotFoundHttpException('Document not found in dossier'); + } return $this->render('document/details.html.twig', [ 'ingested' => $this->searchService->isIngested($document), 'dossier' => $dossier, 'document' => $document, - 'thread' => $thread, - 'family' => $family, + 'thread' => $this->documentRepository->getRelatedDocumentsByThread($document), + 'family' => $this->documentRepository->getRelatedDocumentsByFamily($document), ]); } @@ -77,8 +64,12 @@ public function download( #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier, #[MapEntity(mapping: ['documentId' => 'documentNr'])] Document $document ): StreamedResponse { + if (! $this->dossierService->isViewingAllowed($dossier, $document)) { + throw $this->createNotFoundException('Document or dossier not found'); + } + if (! $dossier->getDocuments()->contains($document)) { - throw new NotFoundHttpException('Document not found'); + throw new NotFoundHttpException('Document not found in dossier'); } $stream = $this->documentStorage->retrieveResourceDocument($document); @@ -86,10 +77,10 @@ public function download( throw new NotFoundHttpException(); } - // @todo: caching headers et al $response = new StreamedResponse(); - $response->headers->set('Content-Type', $document->getMimetype()); - + $response->headers->set('Content-Type', $document->getFileInfo()->getMimetype()); + $response->headers->set('Content-Length', (string) $document->getFileInfo()->getSize()); + $response->headers->set('Last-Modified', $document->getUpdatedAt()->format('D, d M Y H:i:s') . ' GMT'); $response->setCallback(function () use ($stream) { fpassthru($stream); }); @@ -108,8 +99,12 @@ public function debugPage( #[MapEntity(mapping: ['documentId' => 'documentNr'])] Document $document, string $pageNr ): Response { + if (! $this->dossierService->isViewingAllowed($dossier, $document)) { + throw $this->createNotFoundException('Document or dossier not found'); + } + if (! $dossier->getDocuments()->contains($document)) { - throw new NotFoundHttpException('Document not found'); + throw new NotFoundHttpException('Document not found in dossier'); } $content = $this->searchService->getPageContent($document, intval($pageNr)); @@ -131,12 +126,17 @@ public function downloadPage( #[MapEntity(mapping: ['documentId' => 'documentNr'])] Document $document, string $pageNr ): StreamedResponse { + if (! $this->dossierService->isViewingAllowed($dossier, $document)) { + throw $this->createNotFoundException('Document or dossier not found'); + } + if (! $dossier->getDocuments()->contains($document)) { - throw new NotFoundHttpException('Document not found'); + throw new NotFoundHttpException('Document not found in dossier'); } // No file found (yet), just the document record - if ($document->getFilepath() == null || ! $document->isUploaded()) { + $file = $document->getFileInfo(); + if ($file->getPath() === null || ! $file->isUploaded()) { throw new NotFoundHttpException(); } @@ -145,11 +145,13 @@ public function downloadPage( throw new NotFoundHttpException(); } - // @todo: caching headers et al - $response = new StreamedResponse(function () use ($stream) { + $response = new StreamedResponse(); + $response->headers->set('Content-Type', $file->getMimetype()); + $response->headers->set('Content-Length', (string) $file->getSize()); + $response->headers->set('Last-Modified', $document->getUpdatedAt()->format('D, d M Y H:i:s') . ' GMT'); + $response->setCallback(function () use ($stream) { fpassthru($stream); }); - $response->headers->set('Content-Type', $document->getMimetype()); return $response; } @@ -165,24 +167,35 @@ public function thumbnailPage( #[MapEntity(mapping: ['documentId' => 'documentNr'])] Document $document, string $pageNr ): StreamedResponse { + if (! $this->dossierService->isViewingAllowed($dossier, $document)) { + throw $this->createNotFoundException('Document or dossier not found'); + } + if (! $dossier->getDocuments()->contains($document)) { - throw new NotFoundHttpException('Document not found'); + throw new NotFoundHttpException('Document not found in dossier'); } + $fileSize = $this->thumbnailStorage->fileSize($document, intval($pageNr)); $stream = $this->thumbnailStorage->retrieveResource($document, intval($pageNr)); if (! $stream) { // Display default placeholder thumbnail if we haven't found a thumbnail for given document/pageNr $path = sprintf('%s/%s', $this->getParameter('kernel.project_dir') . '/public', 'placeholder.png'); + $fileSize = filesize($path); $stream = fopen($path, 'rb'); if (! $stream) { throw new NotFoundHttpException(); } } - // @todo: caching headers et al $response = new StreamedResponse(); $response->headers->set('Content-Type', 'image/png'); - + $response->headers->set('Content-Length', (string) $fileSize); + $response->setCache([ + 'public' => true, + 'max_age' => 3600, + 's_maxage' => 3600, + 'immutable' => true, + ]); $response->setCallback(function () use ($stream) { fpassthru($stream); }); diff --git a/src/Controller/DossierController.php b/src/Controller/DossierController.php index 3dbfd3b6..cff46433 100644 --- a/src/Controller/DossierController.php +++ b/src/Controller/DossierController.php @@ -7,28 +7,37 @@ use App\Entity\BatchDownload; use App\Entity\Dossier; use App\Message\GenerateArchiveMessage; +use App\Service\ArchiveService; +use App\Service\DossierService; use App\Service\Search\Model\Config; +use App\Service\Storage\DocumentStorageService; use Doctrine\ORM\EntityManagerInterface; +use Knp\Component\Pager\PaginatorInterface; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Contracts\Translation\TranslatorInterface; use WhiteOctober\BreadcrumbsBundle\Model\Breadcrumbs; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class DossierController extends AbstractController { - protected EntityManagerInterface $doctrine; - protected MessageBusInterface $messageBus; - protected TranslatorInterface $translator; + protected const MAX_ITEMS_PER_PAGE = 100; - public function __construct(EntityManagerInterface $doctrine, MessageBusInterface $messageBus, TranslatorInterface $translator) - { - $this->doctrine = $doctrine; - $this->messageBus = $messageBus; - $this->translator = $translator; + public function __construct( + private readonly EntityManagerInterface $doctrine, + private readonly MessageBusInterface $messageBus, + private readonly DocumentStorageService $documentStorage, + private readonly DossierService $dossierService, + private readonly PaginatorInterface $paginator, + private readonly ArchiveService $archiveService, + ) { } #[Route('/dossiers', name: 'app_dossier_index', methods: ['GET'])] @@ -40,17 +49,44 @@ public function index(): Response #[Route('/dossier/{dossierId}', name: 'app_dossier_detail', methods: ['GET'])] public function detail( #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier, - Breadcrumbs $breadcrumbs + Breadcrumbs $breadcrumbs, + Request $request, ): Response { $breadcrumbs->addRouteItem('Home', 'app_home'); $breadcrumbs->addItem('Dossier'); - if (! $dossier->isVisible()) { + if (! $this->dossierService->isViewingAllowed($dossier)) { throw $this->createNotFoundException('Dossier not found'); } + // Split the documents by judgement for display purposes + $publicDocs = []; + $notPublicDocs = []; + foreach ($dossier->getDocuments() as $document) { + if ($document->getJudgement()?->isAtLeastPartialPublic()) { + $publicDocs[] = $document; + } else { + $notPublicDocs[] = $document; + } + } + + $publicPagination = $this->paginator->paginate( + $publicDocs, + $request->query->getInt('pu', 1), + self::MAX_ITEMS_PER_PAGE, + ['pageParameterName' => 'pu'], + ); + $notPublicPagination = $this->paginator->paginate( + $notPublicDocs, + $request->query->getInt('pn', 1), + self::MAX_ITEMS_PER_PAGE, + ['pageParameterName' => 'pn'], + ); + return $this->render('dossier/details.html.twig', [ 'dossier' => $dossier, + 'public_docs' => $publicPagination, + 'not_public_docs' => $notPublicPagination, ]); } @@ -59,7 +95,7 @@ public function createBatch( Request $request, #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier, ): Response { - if (! $dossier->isVisible()) { + if (! $this->dossierService->isViewingAllowed($dossier)) { throw $this->createNotFoundException('Dossier not found'); } @@ -78,10 +114,13 @@ public function createBatch( } } - if (count($documents) === 0) { - $this->addFlash('warning', $this->translator->trans('No documents selected')); - - return $this->redirectToRoute('app_dossier_detail', ['dossierId' => $dossier->getDossierNr()]); + // If a batch already exists with the given documents, redirect to that batch. + $batch = $this->archiveService->archiveExists($dossier, $documents); + if ($batch) { + return $this->redirectToRoute('app_dossier_batch_detail', [ + 'dossierId' => $dossier->getDossierNr(), + 'batchId' => $batch->getId(), + ]); } $batch = new BatchDownload(); @@ -113,7 +152,7 @@ public function batch( $breadcrumbs->addRouteItem('Dossier', 'app_dossier_detail', ['dossierId' => $dossier->getDossierNr()]); $breadcrumbs->addItem('Download'); - if (! $dossier->isVisible()) { + if (! $this->dossierService->isViewingAllowed($dossier)) { throw $this->createNotFoundException('Dossier not found'); } @@ -122,4 +161,99 @@ public function batch( 'batch' => $batch, ]); } + + #[Route('/dossier/{dossierId}/batch/{batchId}/download', name: 'app_dossier_batch_download', methods: ['GET'])] + public function batchDownload( + #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier, + #[MapEntity(mapping: ['batchId' => 'id'])] BatchDownload $batch, + ): Response { + if (! $this->dossierService->isViewingAllowed($dossier)) { + throw $this->createNotFoundException('Dossier not found'); + } + + if ($batch->getStatus() !== BatchDownload::STATUS_COMPLETED) { + return $this->redirectToRoute('app_dossier_batch_detail', [ + 'dossierId' => $dossier->getDossierNr(), + 'batchId' => $batch->getId(), + ]); + } + + $stream = $this->archiveService->getZipStream($batch); + if (! $stream) { + throw new NotFoundHttpException(); + } + + $response = new StreamedResponse(); + $response->headers->set('Content-Type', 'application/zip'); + $response->headers->set('Content-Length', $batch->getSize()); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $batch->getFilename() . '"'); + // Since the batch is immutable, we can cache it for a while + $response->setCache([ + 'public' => true, + 'max_age' => 48 * 3600, + 's_maxage' => 48 * 3600, + 'immutable' => true, + ]); + $response->setCallback(function () use ($stream) { + fpassthru($stream); + }); + + return $response; + } + + #[Route('/dossier/{dossierId}/inventory/download', name: 'app_dossier_inventory_download', methods: ['GET'])] + public function downloadInventory( + #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier + ): StreamedResponse { + if (! $this->dossierService->isViewingAllowed($dossier)) { + throw $this->createNotFoundException('Dossier not found'); + } + + $inventory = $dossier->getInventory(); + if (! $inventory) { + throw $this->createNotFoundException('Dossier inventory not found'); + } + + $stream = $this->documentStorage->retrieveResourceDocument($inventory); + if (! $stream) { + throw new NotFoundHttpException(); + } + + $response = new StreamedResponse(); + $response->headers->set('Content-Type', $inventory->getFileInfo()->getMimetype()); + $response->headers->set('Content-Length', (string) $inventory->getFileInfo()->getSize()); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $inventory->getFileInfo()->getName() . '"'); + $response->headers->set('Last-Modified', $inventory->getUpdatedAt()->format('D, d M Y H:i:s') . ' GMT'); + $response->setCallback(function () use ($stream) { + fpassthru($stream); + }); + + return $response; + } + + #[Route('/dossier/{dossierId}/decision/download', name: 'app_dossier_decision_download', methods: ['GET'])] + public function downloadDecision( + #[MapEntity(mapping: ['dossierId' => 'dossierNr'])] Dossier $dossier + ): StreamedResponse { + $decisionDocument = $dossier->getDecisionDocument(); + if (! $decisionDocument) { + throw $this->createNotFoundException('Dossier decision document not found'); + } + + $stream = $this->documentStorage->retrieveResourceDocument($decisionDocument); + if (! $stream) { + throw new NotFoundHttpException(); + } + + $response = new StreamedResponse(); + $response->headers->set('Content-Type', $decisionDocument->getFileInfo()->getMimetype()); + $response->headers->set('Content-Length', (string) $decisionDocument->getFileInfo()->getSize()); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $decisionDocument->getFileInfo()->getName() . '"'); + $response->headers->set('Last-Modified', $decisionDocument->getUpdatedAt()->format('D, d M Y H:i:s') . ' GMT'); + $response->setCallback(function () use ($stream) { + fpassthru($stream); + }); + + return $response; + } } diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 7d59d027..502f437f 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -47,8 +47,9 @@ public function index(Request $request, Breadcrumbs $breadcrumbs): Response } // From here we always have a 'q' from the query string - $q = strval($request->query->get('q')); - if (! empty($q)) { + if ($request->query->has('q')) { + $q = strval($request->query->get('q')); + return new RedirectResponse($this->generateUrl('app_search', ['q' => $q])); } diff --git a/src/Controller/InquiryController.php b/src/Controller/InquiryController.php index 609c76b0..0bf7e165 100644 --- a/src/Controller/InquiryController.php +++ b/src/Controller/InquiryController.php @@ -7,27 +7,58 @@ use App\Entity\Inquiry; use App\Service\InquiryService; use Doctrine\ORM\EntityManagerInterface; +use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class InquiryController extends AbstractController { + protected const MAX_ITEMS_PER_PAGE = 100; + public function __construct( protected EntityManagerInterface $doctrine, protected Security $security, - protected InquiryService $inquiryService + protected InquiryService $inquiryService, + private readonly PaginatorInterface $paginator, ) { } #[Route('/inquiry/{token}', name: 'app_inquiry_detail', methods: ['GET'])] - public function detail(Inquiry $inquiry): Response + public function detail(Inquiry $inquiry, Request $request): Response { $this->inquiryService->saveInquiry($inquiry); + // Split the documents by judgement for display purposes + $publicDocs = []; + $notPublicDocs = []; + foreach ($inquiry->getDocuments() as $document) { + if ($document->getJudgement()?->isAtLeastPartialPublic()) { + $publicDocs[] = $document; + } else { + $notPublicDocs[] = $document; + } + } + + $publicPagination = $this->paginator->paginate( + $publicDocs, + $request->query->getInt('pu', 1), + self::MAX_ITEMS_PER_PAGE, + ['pageParameterName' => 'pu'], + ); + $notPublicPagination = $this->paginator->paginate( + $notPublicDocs, + $request->query->getInt('pn', 1), + self::MAX_ITEMS_PER_PAGE, + ['pageParameterName' => 'pn'], + ); + return $this->render('inquiry/index.html.twig', [ 'inquiry' => $inquiry, + 'public_docs' => $publicPagination, + 'not_public_docs' => $notPublicPagination, ]); } } diff --git a/src/Controller/SearchController.php b/src/Controller/SearchController.php index 0132d169..9379bc9d 100644 --- a/src/Controller/SearchController.php +++ b/src/Controller/SearchController.php @@ -5,9 +5,11 @@ namespace App\Controller; use App\Service\Search\ConfigFactory; +use App\Service\Search\Model\Config; use App\Service\Search\SearchService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -84,9 +86,6 @@ public function ajaxResultMinimalistic(Request $request): Response #[Route('/search', name: 'app_search')] public function search(Request $request, Breadcrumbs $breadcrumbs): Response { - $breadcrumbs->addRouteItem('Home', 'app_home'); - $breadcrumbs->addItem('Search results'); - // If we have a POST request, we have a search query in the body. Redirect to GET request // so we have the q in the query string. if ($request->isMethod('POST')) { @@ -99,10 +98,15 @@ public function search(Request $request, Breadcrumbs $breadcrumbs): Response )); } - // From here we always have a 'q' from the query string - $q = strval($request->query->get('q')); - $config = $this->configFactory->createFromRequest($request); + + $breadcrumbs->addRouteItem('Home', 'app_home'); + if ($config->searchType === Config::TYPE_DOSSIER) { + $breadcrumbs->addItem('All published dossiers'); + } else { + $breadcrumbs->addItem('Search'); + } + $result = $this->searchService->search($config); if ($result->hasFailed()) { return $this->render('search/result-failure.html.twig', [ @@ -116,8 +120,24 @@ public function search(Request $request, Breadcrumbs $breadcrumbs): Response } #[Route('/browse', name: 'app_browse')] - public function browse(Breadcrumbs $breadcrumbs): Response + public function browse(Request $request, Breadcrumbs $breadcrumbs): Response { + // If we have a POST request, we have a search query in the body. Redirect to GET request + // so we have the q in the query string. + if ($request->isMethod('POST')) { + $q = strval($request->request->get('q')); + + // Redirect to GET request, so we have the q in the query string. + return $this->redirect($this->generateUrl('app_browse', ['q' => $q])); + } + + // From here we always have a 'q' from the query string + if ($request->query->has('q')) { + $q = strval($request->query->get('q')); + + return new RedirectResponse($this->generateUrl('app_search', ['q' => $q])); + } + $breadcrumbs->addRouteItem('Home', 'app_home'); $breadcrumbs->addItem('Browse'); diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 5cac923a..abb32fe8 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -25,7 +25,7 @@ public function __construct(EntityManagerInterface $doctrine, UserPasswordHasher $this->passwordEncoder = $passwordEncoder; } - #[Route(path: '/login', name: 'app_login')] + #[Route(path: '/balie/login', name: 'app_login')] public function login(AuthenticationUtils $authenticationUtils): Response { // get the login error if there is one @@ -36,13 +36,13 @@ public function login(AuthenticationUtils $authenticationUtils): Response return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); } - #[Route(path: '/logout', name: 'app_logout')] + #[Route(path: '/balie/logout', name: 'app_logout')] public function logout(): void { throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); } - #[Route(path: '/change-password', name: 'app_change_password')] + #[Route(path: '/balie/change-password', name: 'app_change_password')] public function changePassword(Request $request): Response { $form = $this->createForm(ChangePasswordType::class); diff --git a/src/Controller/StatsController.php b/src/Controller/StatsController.php index 5bac1750..38f645a5 100644 --- a/src/Controller/StatsController.php +++ b/src/Controller/StatsController.php @@ -7,18 +7,23 @@ use App\Entity\Document; use App\Entity\Dossier; use App\Entity\WorkerStats; +use App\Service\Search\Model\Config; +use App\Service\Search\SearchService; use Doctrine\ORM\EntityManagerInterface; +use Predis\Client; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class StatsController extends AbstractController { - protected EntityManagerInterface $doctrine; - - public function __construct(EntityManagerInterface $doctrine) - { - $this->doctrine = $doctrine; + public function __construct( + private readonly EntityManagerInterface $doctrine, + private readonly Client $redis, + private readonly SearchService $searchService, + private readonly string $rabbitMqStatUrl + ) { } #[Route('/prometheus', name: 'app_prometheus', methods: ['GET'])] @@ -47,4 +52,98 @@ public function prometheus(): Response return $response; } + + #[Route('/health', name: 'app_health', methods: ['GET'])] + public function health(): JsonResponse + { + $services = [ + 'postgres' => $this->isPostgresAlive(), + 'redis' => $this->isRedisAlive(), + 'elastic' => $this->isElasticAlive(), + 'rabbitmq' => $this->isRabbitMqAlive(), + ]; + + $statusCode = Response::HTTP_OK; + foreach ($services as $status) { + if ($status === false) { + $statusCode = Response::HTTP_SERVICE_UNAVAILABLE; + } + } + + $healthy = $services['postgres'] && $services['redis'] && $services['elastic'] && $services['rabbitmq']; + $response = new JsonResponse([ + 'healthy' => $healthy, + 'externals' => [ + 'postgres' => $services['postgres'], + 'redis' => $services['redis'], + 'elastic' => $services['elastic'], + 'rabbitmq' => $services['rabbitmq'], + ], + ], $statusCode); + + return $response->setPrivate(); + } + + protected function isRedisAlive(): bool + { + try { + $this->redis->connect(); + $result = $this->redis->isConnected(); + if ($result !== true) { + return false; + } + + $result = $this->redis->ping('ping'); + + return $result === 'ping'; + } catch (\Throwable) { + // ignore + } + + return false; + } + + protected function isPostgresAlive(): bool + { + try { + $result = $this->doctrine->getConnection()->fetchOne('SELECT 1'); + + return $result === 1; + } catch (\Throwable) { + // ignore + } + + return false; + } + + protected function isElasticAlive(): bool + { + try { + $result = $this->searchService->search(new Config()); + + return $result->hasFailed() === false; + } catch (\Throwable) { + // ignore + } + + return false; + } + + protected function isRabbitMqAlive(): bool + { + try { + $client = new \GuzzleHttp\Client([ + 'base_uri' => $this->rabbitMqStatUrl, + 'timeout' => 2.0, + 'connect_timeout' => 2.0, + ]); + $response = $client->get('/api/overview'); + + return $response->getStatusCode() === Response::HTTP_OK; + } catch (\Exception) { + // ignore + } + + return false; + } } diff --git a/src/DataCollector/ElasticCollector.php b/src/DataCollector/ElasticCollector.php index 5721bf08..16d3ef77 100644 --- a/src/DataCollector/ElasticCollector.php +++ b/src/DataCollector/ElasticCollector.php @@ -9,6 +9,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * A data collector for elasticsearch calls so we can display them in the debug profiler toolbar. + */ class ElasticCollector extends AbstractDataCollector { protected bool $enabled = true; diff --git a/src/DataFixtures/DepartmentFixtures.php b/src/DataFixtures/DepartmentFixtures.php index 3fdb0688..ddb80926 100644 --- a/src/DataFixtures/DepartmentFixtures.php +++ b/src/DataFixtures/DepartmentFixtures.php @@ -9,28 +9,32 @@ use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; +/** + * This is a set of fixtures for the Department and officials entities. It is not meant to be used in production. + */ class DepartmentFixtures extends Fixture { public function load(ObjectManager $manager): void { $departments = [ - 'Ministerie van Algemene Zaken', - 'Ministerie van Binnenlandse Zaken en Koninkrijksrelaties', - 'Ministerie van Buitenlandse Zaken', - 'Ministerie van Defensie', - 'Ministerie van Economische Zaken en Klimaat', - 'Ministerie van Financiën', - 'Ministerie van Infrastructuur en Waterstaat', - 'Ministerie van Justitie en Veiligheid', - 'Ministerie van Landbouw, Natuur en Voedselkwaliteit', - 'Ministerie van Onderwijs, Cultuur en Wetenschap', - 'Ministerie van Sociale Zaken en Werkgelegenheid', - 'Ministerie van Volksgezondheid, Welzijn en Sport', + 'Ministerie van Algemene Zaken' => 'AZ', + 'Ministerie van Binnenlandse Zaken en Koninkrijksrelaties' => 'BZK', + 'Ministerie van Buitenlandse Zaken' => 'BZ', + 'Ministerie van Defensie' => 'Def', + 'Ministerie van Economische Zaken en Klimaat' => 'EZK', + 'Ministerie van Financiën' => 'Fin', + 'Ministerie van Infrastructuur en Waterstaat' => 'I&W', + 'Ministerie van Justitie en Veiligheid' => 'J&V', + 'Ministerie van Landbouw, Natuur en Voedselkwaliteit' => 'LNV', + 'Ministerie van Onderwijs, Cultuur en Wetenschap' => 'OCW', + 'Ministerie van Sociale Zaken en Werkgelegenheid' => 'SZW', + 'Ministerie van Volksgezondheid, Welzijn en Sport' => 'VWS', ]; - foreach ($departments as $department) { + foreach ($departments as $department => $short) { $entity = new Department(); $entity->setName($department); + $entity->setShortTag($short); $manager->persist($entity); } diff --git a/src/Entity/BatchDownload.php b/src/Entity/BatchDownload.php index bf00ba10..e36a7417 100644 --- a/src/Entity/BatchDownload.php +++ b/src/Entity/BatchDownload.php @@ -127,4 +127,9 @@ public function setSize(string $size): static return $this; } + + public function getFilename(): string + { + return sprintf('dossier-%s-%s.zip', $this->getDossier()->getDossierNr(), $this->getId()->toBase58()); + } } diff --git a/src/Entity/Department.php b/src/Entity/Department.php index 413ac723..a9e94a35 100644 --- a/src/Entity/Department.php +++ b/src/Entity/Department.php @@ -21,6 +21,9 @@ class Department #[ORM\Column(length: 255)] private string $name; + #[ORM\Column(length: 20, nullable: true)] + private ?string $shortTag = null; + public function getId(): Uuid { return $this->id; @@ -37,4 +40,21 @@ public function setName(string $name): self return $this; } + + public function getShortTag(): ?string + { + return $this->shortTag; + } + + public function setShortTag(?string $shortTag): static + { + $this->shortTag = $shortTag; + + return $this; + } + + public function nameAndShort(): string + { + return $this->name . ' (' . $this->shortTag . ')'; + } } diff --git a/src/Entity/Document.php b/src/Entity/Document.php index bec0f3f8..2d44deb7 100644 --- a/src/Entity/Document.php +++ b/src/Entity/Document.php @@ -4,12 +4,14 @@ namespace App\Entity; +use App\Doctrine\TimestampableTrait; use App\Repository\DocumentRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Doctrine\ORM\Mapping\Embedded; use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Component\Uid\Uuid; @@ -19,18 +21,10 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ #[ORM\Entity(repositoryClass: DocumentRepository::class)] -#[ORM\InheritanceType('SINGLE_TABLE')] -#[ORM\DiscriminatorColumn(name: 'class', type: 'string')] -#[ORM\DiscriminatorMap([ - self::CLASS_INVENTORY => Inventory::class, - self::CLASS_DOCUMENT => Document::class, - self::CLASS_DECISION => Decision::class, -])] -class Document +#[ORM\HasLifecycleCallbacks] +class Document implements EntityWithFileInfo { - public const CLASS_INVENTORY = 'inventory'; - public const CLASS_DOCUMENT = 'document'; - public const CLASS_DECISION = 'decision'; + use TimestampableTrait; #[ORM\Id] #[ORM\Column(type: 'uuid', unique: true, nullable: false)] @@ -38,21 +32,6 @@ class Document #[ORM\CustomIdGenerator(class: UuidGenerator::class)] private Uuid $id; - #[ORM\Column(nullable: false)] - private \DateTimeImmutable $createdAt; - - #[ORM\Column(nullable: false)] - private \DateTimeImmutable $updatedAt; - - #[ORM\Column(length: 100, nullable: true)] - private ?string $mimetype; - - #[ORM\Column(length: 1024, nullable: true)] - private ?string $filepath; - - #[ORM\Column(nullable: false)] - private int $filesize = 0; - // Number of pages for word based documents #[ORM\Column(nullable: false)] private int $pageCount = 0; @@ -71,23 +50,12 @@ class Document #[ORM\ManyToMany(targetEntity: Dossier::class, inversedBy: 'documents')] private Collection $dossiers; - /* The type of the local file on disk. This is mostly a PDF. These are the types that can be ingested by the workers */ - #[ORM\Column(length: 255, nullable: false)] - private string $fileType; - - /* The type of the original file. This could be a spreadsheet, word document or email */ - #[ORM\Column(length: 255, nullable: false)] - private string $sourceType; - #[ORM\Column(length: 255, nullable: false)] private string $documentNr; #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: false)] private \DateTimeInterface $documentDate; - #[ORM\Column(length: 255, nullable: false)] - private string $filename; - #[ORM\Column(nullable: true)] private ?int $familyId = null; @@ -97,8 +65,8 @@ class Document #[ORM\Column(nullable: true)] private ?int $threadId = null; - #[ORM\Column(length: 255, nullable: true)] - private ?string $judgement = null; + #[ORM\Column(length: 255, nullable: true, enumType: Judgement::class)] + private ?Judgement $judgement = null; /** @var array */ #[ORM\Column(type: Types::JSON, nullable: false)] @@ -111,28 +79,44 @@ class Document #[ORM\Column(length: 255, nullable: true)] private ?string $period = null; - #[ORM\Column(nullable: false)] - private bool $uploaded = false; - /** @var Collection|IngestLog[] */ #[ORM\OneToMany(mappedBy: 'document', targetEntity: IngestLog::class, orphanRemoval: true)] private Collection $ingestLogs; #[ORM\Column] - private bool $suspended; + private bool $suspended = false; #[ORM\Column] - private bool $withdrawn; + private bool $withdrawn = false; + + #[ORM\Column(length: 255, nullable: true, enumType: WithdrawReason::class)] + private ?WithdrawReason $withdrawReason = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $withdrawExplanation = null; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeImmutable $withdrawDate = null; /** @var Collection|Inquiry[] */ #[ORM\ManyToMany(targetEntity: Inquiry::class, mappedBy: 'documents')] private Collection $inquiries; + #[Embedded(class: FileInfo::class, columnPrefix: 'file_')] + private FileInfo $fileInfo; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $link = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $remark = null; + public function __construct() { $this->dossiers = new ArrayCollection(); $this->ingestLogs = new ArrayCollection(); $this->inquiries = new ArrayCollection(); + $this->fileInfo = new FileInfo(); } public function getId(): Uuid @@ -145,66 +129,6 @@ public function setId(UUid $uuid): void $this->id = $uuid; } - public function getCreatedAt(): \DateTimeImmutable - { - return $this->createdAt; - } - - public function setCreatedAt(\DateTimeImmutable $createdAt): self - { - $this->createdAt = $createdAt; - - return $this; - } - - public function getUpdatedAt(): \DateTimeImmutable - { - return $this->updatedAt; - } - - public function setUpdatedAt(\DateTimeImmutable $updatedAt): self - { - $this->updatedAt = $updatedAt; - - return $this; - } - - public function getMimetype(): ?string - { - return $this->mimetype; - } - - public function setMimetype(?string $mimetype): self - { - $this->mimetype = $mimetype; - - return $this; - } - - public function getFilepath(): ?string - { - return $this->filepath; - } - - public function setFilepath(?string $filepath): self - { - $this->filepath = $filepath; - - return $this; - } - - public function getFilesize(): int - { - return $this->filesize; - } - - public function setFilesize(int $filesize): self - { - $this->filesize = $filesize; - - return $this; - } - public function getPageCount(): int { return $this->pageCount; @@ -277,18 +201,6 @@ public function setDocumentDate(\DateTimeInterface $documentDate): self return $this; } - public function getFilename(): string - { - return $this->filename; - } - - public function setFilename(string $filename): self - { - $this->filename = $filename; - - return $this; - } - public function getFamilyId(): ?int { return $this->familyId; @@ -325,12 +237,12 @@ public function setThreadId(int $threadId): self return $this; } - public function getJudgement(): ?string + public function getJudgement(): ?Judgement { return $this->judgement; } - public function setJudgement(string $judgement): self + public function setJudgement(Judgement $judgement): self { $this->judgement = $judgement; @@ -389,18 +301,6 @@ public function setPeriod(string $period): self return $this; } - public function isUploaded(): bool - { - return $this->uploaded; - } - - public function setUploaded(bool $uploaded): self - { - $this->uploaded = $uploaded; - - return $this; - } - /** * @return Collection|Dossier[] */ @@ -486,77 +386,133 @@ public function groupedIngestLogs(): array return $grouped; } - public function getFileType(): ?string + public function isSuspended(): bool { - return $this->fileType; + return $this->suspended; } - public function setFileType(string $fileType): self + public function setSuspended(bool $suspended): self { - $this->fileType = $fileType; + $this->suspended = $suspended; return $this; } - public function getSourceType(): ?string + public function isWithdrawn(): bool + { + return $this->withdrawn; + } + + /** + * @return Collection|Inquiry[] + */ + public function getInquiries(): Collection + { + return $this->inquiries; + } + + public function addInquiry(Inquiry $inquiry): static { - return $this->sourceType; + if (! $this->inquiries->contains($inquiry)) { + $this->inquiries->add($inquiry); + } + + return $this; } - public function setSourceType(string $sourceType): self + public function removeInquiry(Inquiry $inquiry): static { - $this->sourceType = $sourceType; + if ($this->inquiries->removeElement($inquiry)) { + $inquiry->removeDocument($this); + } return $this; } - public function isSuspended(): bool + public function getFileInfo(): FileInfo { - return $this->suspended; + return $this->fileInfo; } - public function setSuspended(bool $suspended): self + public function setFileInfo(FileInfo $fileInfo): self { - $this->suspended = $suspended; + $this->fileInfo = $fileInfo; return $this; } - public function isWithdrawn(): bool + public function getLink(): ?string { - return $this->withdrawn; + return $this->link; } - public function setWithdrawn(bool $withdrawn): self + public function setLink(?string $link): static { - $this->withdrawn = $withdrawn; + $this->link = $link; return $this; } - /** - * @return Collection|Inquiry[] - */ - public function getInquiries(): Collection + public function isUploaded(): bool { - return $this->inquiries; + return $this->fileInfo->isUploaded(); } - public function addInquiry(Inquiry $inquiry): static + public function shouldBeUploaded(): bool { - if (! $this->inquiries->contains($inquiry)) { - $this->inquiries->add($inquiry); + if ($this->suspended === true) { + return false; } - return $this; + if (! $this->judgement) { + return false; + } + + return $this->judgement->isAtLeastPartialPublic(); } - public function removeInquiry(Inquiry $inquiry): static + public function getRemark(): ?string { - if ($this->inquiries->removeElement($inquiry)) { - $inquiry->removeDocument($this); - } + return $this->remark; + } + + public function setRemark(?string $remark): static + { + $this->remark = $remark; return $this; } + + public function getFileCacheKey(): string + { + return $this->documentNr; + } + + public function getWithdrawReason(): ?WithdrawReason + { + return $this->withdrawReason; + } + + public function getWithdrawExplanation(): ?string + { + return $this->withdrawExplanation; + } + + public function getWithdrawDate(): ?\DateTimeImmutable + { + return $this->withdrawDate; + } + + public function withdraw(WithdrawReason $reason, string $explanation): void + { + $this->withdrawn = true; + $this->withdrawReason = $reason; + $this->withdrawExplanation = $explanation; + $this->withdrawDate = new \DateTimeImmutable(); + + $this->fileInfo->setMimetype(null); + $this->fileInfo->setUploaded(false); + $this->fileInfo->setSize(0); + $this->fileInfo->setPath(null); + } } diff --git a/src/Entity/Dossier.php b/src/Entity/Dossier.php index 510ed881..559828fb 100644 --- a/src/Entity/Dossier.php +++ b/src/Entity/Dossier.php @@ -4,23 +4,30 @@ namespace App\Entity; +use App\Doctrine\TimestampableTrait; use App\Repository\DossierRepository; +use App\ValueObject\DossierUploadStatus; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use Doctrine\Common\Collections\Criteria; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\JoinTable; use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Uid\Uuid; /** * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ #[ORM\Entity(repositoryClass: DossierRepository::class)] +#[UniqueEntity('dossierNr')] +#[ORM\HasLifecycleCallbacks] class Dossier implements EntityWithId { + use TimestampableTrait; + public const STATUS_CONCEPT = 'concept'; // Dossier is just uploaded and does not have (all) the documents present yet public const STATUS_COMPLETED = 'completed'; // Dossier has all the uploaded documents and is ready for publication public const STATUS_PREVIEW = 'preview'; // Dossier is in preview mode and can only be viewed with specific tokens @@ -52,20 +59,14 @@ class Dossier implements EntityWithId #[ORM\Column(type: 'uuid', unique: true)] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: UuidGenerator::class)] - private Uuid $id; - - #[ORM\Column] - private \DateTimeImmutable $createdAt; - - #[ORM\Column] - private \DateTimeImmutable $updatedAt; + private ?Uuid $id = null; /** @var Collection|Document[] */ #[ORM\ManyToMany(targetEntity: Document::class, mappedBy: 'dossiers')] #[ORM\OrderBy(['documentNr' => 'ASC'])] private Collection $documents; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, unique: true)] private string $dossierNr; #[ORM\Column(length: 500)] @@ -101,13 +102,28 @@ class Dossier implements EntityWithId private string $decision; /** @var Collection|Inquiry[] */ - #[ORM\ManyToMany(targetEntity: Inquiry::class, inversedBy: 'dossiers')] + #[ORM\ManyToMany(targetEntity: Inquiry::class, mappedBy: 'dossiers')] #[JoinTable(name: 'inquiry_dossier')] private Collection $inquiries; #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] private ?\DateTimeImmutable $publicationDate; + #[ORM\OneToOne(mappedBy: 'dossier', targetEntity: Inventory::class)] + private ?Inventory $inventory = null; + + #[ORM\OneToOne(mappedBy: 'dossier', targetEntity: RawInventory::class)] + private ?RawInventory $rawInventory = null; + + #[ORM\OneToOne(mappedBy: 'dossier', targetEntity: DecisionDocument::class)] + private ?DecisionDocument $decisionDocument = null; + + /** + * @var string[]|null + */ + #[ORM\Column(nullable: true)] + private ?array $defaultSubjects = null; + public function __construct() { $this->documents = new ArrayCollection(); @@ -116,35 +132,11 @@ public function __construct() $this->inquiries = new ArrayCollection(); } - public function getId(): Uuid + public function getId(): ?Uuid { return $this->id; } - public function getCreatedAt(): \DateTimeImmutable - { - return $this->createdAt; - } - - public function setCreatedAt(\DateTimeImmutable $createdAt): self - { - $this->createdAt = $createdAt; - - return $this; - } - - public function getUpdatedAt(): \DateTimeImmutable - { - return $this->updatedAt; - } - - public function setUpdatedAt(\DateTimeImmutable $updatedAt): self - { - $this->updatedAt = $updatedAt; - - return $this; - } - public function getDossierNr(): string { return $this->dossierNr; @@ -189,12 +181,9 @@ public function setStatus(string $status): self return $this; } - public function uploadCount(): int + public function getUploadStatus(): DossierUploadStatus { - $crit = new Criteria(); - $crit->where(Criteria::expr()->eq('uploaded', true)); - - return $this->documents->matching($crit)->count(); + return new DossierUploadStatus($this); } /** @@ -245,14 +234,6 @@ public function removeGovernmentOfficial(GovernmentOfficial $governmentOfficial) return $this; } - public function isVisible(): bool - { - return - $this->status === self::STATUS_PUBLISHED - || $this->status === self::STATUS_PREVIEW - ; - } - public function getSummary(): string { return $this->summary; @@ -278,26 +259,23 @@ public function setDocumentPrefix(string $documentPrefix): self } /** - * When $allDocuments is true, it will return all documents, including inventory and decision documents, otherwise only - * "documents" are returned. - * * @return Collection|Document[] */ - public function getDocuments(bool $allDocuments = false): Collection + public function getDocuments(): Collection { - if ($allDocuments) { - return $this->documents; - } + return $this->documents; + } - // We should be able to use filter() here, but it doesn't work for phpstan (https://github.com/doctrine/collections/issues/364) - $documents = []; - foreach ($this->documents as $element) { - if (get_class($element) === Document::class) { - $documents[] = $element; - } - } + public function getInventory(): ?Inventory + { + return $this->inventory; + } + + public function setInventory(?Inventory $inventory): self + { + $this->inventory = $inventory; - return new ArrayCollection($documents); + return $this; } public function addDocument(Document $document): self @@ -413,4 +391,62 @@ public function setPublicationDate(?\DateTimeImmutable $publicationDate): void { $this->publicationDate = $publicationDate; } + + public function getDecisionDocument(): ?DecisionDocument + { + return $this->decisionDocument; + } + + public function setDecisionDocument(?DecisionDocument $decisionDocument): self + { + $this->decisionDocument = $decisionDocument; + + return $this; + } + + /** + * @return string[]|null + */ + public function getDefaultSubjects(): ?array + { + return $this->defaultSubjects; + } + + /** + * @param string[]|null $defaultSubjects + * + * @return $this + */ + public function setDefaultSubjects(?array $defaultSubjects): static + { + $this->defaultSubjects = $defaultSubjects; + + return $this; + } + + public function removeInquiry(Inquiry $inquiry): static + { + if ($this->inquiries->removeElement($inquiry)) { + $inquiry->removeDossier($this); + } + + return $this; + } + + public function getRawInventory(): ?RawInventory + { + return $this->rawInventory; + } + + public function setRawInventory(?RawInventory $rawInventory): static + { + // set the owning side of the relation if necessary + if ($rawInventory !== null && $rawInventory->getDossier() !== $this) { + $rawInventory->setDossier($this); + } + + $this->rawInventory = $rawInventory; + + return $this; + } } diff --git a/src/Entity/EntityWithId.php b/src/Entity/EntityWithId.php index f81ebc6c..65fb43b0 100644 --- a/src/Entity/EntityWithId.php +++ b/src/Entity/EntityWithId.php @@ -8,5 +8,5 @@ interface EntityWithId { - public function getId(): Uuid; + public function getId(): ?Uuid; } diff --git a/src/Entity/Inquiry.php b/src/Entity/Inquiry.php index da67e272..80090144 100644 --- a/src/Entity/Inquiry.php +++ b/src/Entity/Inquiry.php @@ -30,11 +30,11 @@ class Inquiry private \DateTimeImmutable $updatedAt; /** @var Collection|Document[] */ - #[ORM\ManyToMany(targetEntity: Document::class, inversedBy: 'inquiries')] + #[ORM\ManyToMany(targetEntity: Document::class, inversedBy: 'inquiries', cascade: ['persist'])] private Collection $documents; /** @var Collection|Dossier[] */ - #[ORM\ManyToMany(targetEntity: Dossier::class, mappedBy: 'inquiries')] + #[ORM\ManyToMany(targetEntity: Dossier::class, inversedBy: 'inquiries', cascade: ['persist'])] private Collection $dossiers; #[ORM\Column(length: 255)] diff --git a/src/Entity/Inventory.php b/src/Entity/Inventory.php index dfbaaf02..a1f9dfc9 100644 --- a/src/Entity/Inventory.php +++ b/src/Entity/Inventory.php @@ -4,9 +4,67 @@ namespace App\Entity; +use App\Doctrine\TimestampableTrait; use Doctrine\ORM\Mapping as ORM; +use Doctrine\ORM\Mapping\Embedded; +use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; +use Symfony\Component\Uid\Uuid; #[ORM\Entity] -class Inventory extends Document +#[ORM\HasLifecycleCallbacks] +class Inventory implements EntityWithFileInfo { + use TimestampableTrait; + + #[ORM\Id] + #[ORM\Column(type: 'uuid', unique: true, nullable: false)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: UuidGenerator::class)] + private Uuid $id; + + #[ORM\OneToOne(inversedBy: 'inventory', targetEntity: Dossier::class)] + #[ORM\JoinColumn(name: 'dossier_id', referencedColumnName: 'id', nullable: false, onDelete: 'cascade')] + private Dossier $dossier; + + #[Embedded(class: FileInfo::class, columnPrefix: 'file_')] + private FileInfo $file; + + public function getId(): Uuid + { + return $this->id; + } + + public function __construct() + { + $this->file = new FileInfo(); + } + + public function setDossier(Dossier $dossier): self + { + $this->dossier = $dossier; + + return $this; + } + + public function getDossier(): Dossier + { + return $this->dossier; + } + + public function getFileInfo(): FileInfo + { + return $this->file; + } + + public function setFileInfo(FileInfo $fileInfo): self + { + $this->file = $fileInfo; + + return $this; + } + + public function getFileCacheKey(): string + { + return 'inventory-' . $this->id->toBase58(); + } } diff --git a/src/EventSubscriber/ChangePasswordSubscriber.php b/src/EventSubscriber/ChangePasswordSubscriber.php index 3c8a84e6..735a61f4 100644 --- a/src/EventSubscriber/ChangePasswordSubscriber.php +++ b/src/EventSubscriber/ChangePasswordSubscriber.php @@ -21,7 +21,7 @@ class ChangePasswordSubscriber implements EventSubscriberInterface protected UrlGeneratorInterface $urlGenerator; protected Security $security; - // Skip the redirector when we are on these routes + // Skip the redirector when we are on these routes, otherwise we end up in a redirect loop /** @var array|string[] */ protected array $skipRoutes = [ '2fa_check', diff --git a/src/EventSubscriber/PaginationCountSubscriber.php b/src/EventSubscriber/PaginationCountSubscriber.php index 07f38b39..a93f21c4 100644 --- a/src/EventSubscriber/PaginationCountSubscriber.php +++ b/src/EventSubscriber/PaginationCountSubscriber.php @@ -32,7 +32,7 @@ public function itemCount(ItemsEvent $event): void return; } - $event->count = $event->target->getDocumentCount(); + $event->count = $event->target->getResultCount(); $event->items = $event->target->getEntries(); $event->stopPropagation(); diff --git a/src/EventSubscriber/SecurityHeaderSubscriber.php b/src/EventSubscriber/SecurityHeaderSubscriber.php index f88ba145..d4ed5ece 100644 --- a/src/EventSubscriber/SecurityHeaderSubscriber.php +++ b/src/EventSubscriber/SecurityHeaderSubscriber.php @@ -5,6 +5,7 @@ namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -13,6 +14,13 @@ */ class SecurityHeaderSubscriber implements EventSubscriberInterface { + protected string $appMode; + + protected const CSP_SELF = "'self'"; + protected const CSP_UNSAFE_INLINE = "'unsafe-inline'"; + protected const CSP_UNSAFE_EVAL = "'unsafe-eval'"; + protected const CSP_DATA = 'data:'; + /** @var array|string[] */ protected array $fields = [ 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload', @@ -29,12 +37,43 @@ class SecurityHeaderSubscriber implements EventSubscriberInterface 'X-Download-Options' => 'noopen', 'X-Permitted-Cross-Domain-Policies' => 'off', 'X-XSS-Protection' => '1; mode=block', - 'Content-Security-Policy' => "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " . - "style-src 'self' 'unsafe-inline'; " . - "img-src 'self' data: ; " . - "font-src 'self';", ]; + /** @var array|string[][][] */ + protected array $csp = [ + 'FRONTEND' => [ + 'script-src' => [self::CSP_SELF, 'https://statistiek.rijksoverheid.nl'], + 'style-src' => [self::CSP_SELF], + 'img-src' => [self::CSP_SELF, self::CSP_DATA, 'https://statistiek.rijksoverheid.nl'], + 'font-src' => [self::CSP_SELF], + ], + 'BALIE' => [ + 'script-src' => [self::CSP_SELF, self::CSP_UNSAFE_INLINE, self::CSP_UNSAFE_EVAL, 'https://statistiek.rijksoverheid.nl'], + 'style-src' => [self::CSP_SELF, self::CSP_UNSAFE_INLINE], + 'img-src' => [self::CSP_SELF, self::CSP_DATA, 'https://statistiek.rijksoverheid.nl'], + 'font-src' => [self::CSP_SELF], + ], + 'BOTH' => [ + 'script-src' => [self::CSP_SELF, self::CSP_UNSAFE_INLINE, self::CSP_UNSAFE_EVAL, 'https://statistiek.rijksoverheid.nl'], + 'style-src' => [self::CSP_SELF, self::CSP_UNSAFE_INLINE], + 'img-src' => [self::CSP_SELF, self::CSP_DATA], + 'font-src' => [self::CSP_SELF], + ], + ]; + + public function __construct(string $appMode) + { + $this->appMode = $appMode; + } + + public function onKernelRequest(RequestEvent $event): void + { + // Add random nonce that can be used in CSP for this request only + $nonce = bin2hex(random_bytes(16)); + + $event->getRequest()->attributes->set('csp_nonce', $nonce); + } + public function onKernelResponse(ResponseEvent $event): void { $response = $event->getResponse(); @@ -44,12 +83,35 @@ public function onKernelResponse(ResponseEvent $event): void $response->headers->set($key, $value); } } + + // Add nonce to CSP + $nonce = $event->getRequest()->attributes->get('csp_nonce'); + + $csp = $this->csp[$this->appMode] ?? $this->csp['both']; + $csp['script-src'][] = "'nonce-" . $nonce . "'"; + $csp['style-src'][] = "'nonce-" . $nonce . "'"; + + $response->headers->set('Content-Security-Policy', $this->buildCsp($csp)); } public static function getSubscribedEvents(): array { return [ + KernelEvents::REQUEST => 'onKernelRequest', KernelEvents::RESPONSE => 'onKernelResponse', ]; } + + /** + * @param string[][] $csp + */ + protected function buildCsp(array $csp): string + { + $result = []; + foreach ($csp as $key => $value) { + $result[] = $key . ' ' . join(' ', $value); + } + + return implode('; ', $result); + } } diff --git a/src/Exception/FixtureInventoryException.php b/src/Exception/FixtureInventoryException.php index eaa50f5b..12b0f347 100644 --- a/src/Exception/FixtureInventoryException.php +++ b/src/Exception/FixtureInventoryException.php @@ -7,7 +7,7 @@ class FixtureInventoryException extends \RuntimeException { /** - * @param array> $errors + * @param array> $errors */ public function __construct( string $message, @@ -15,7 +15,7 @@ public function __construct( ) { foreach ($errors as $error) { foreach ($error as $rowIndex => $errorDescription) { - $message .= "\n- [row $rowIndex] $errorDescription"; + $message .= "\n- [$rowIndex] $errorDescription"; } } @@ -23,7 +23,7 @@ public function __construct( } /** - * @param array> $errors + * @param array> $errors */ public static function forProcessingErrors(array $errors): self { diff --git a/src/Form/ChoiceTypeWithHelp.php b/src/Form/ChoiceTypeWithHelp.php index 9dd6b29b..d355fc99 100644 --- a/src/Form/ChoiceTypeWithHelp.php +++ b/src/Form/ChoiceTypeWithHelp.php @@ -24,7 +24,7 @@ public function configureOptions(OptionsResolver $resolver): void */ public function finishView(FormView $view, FormInterface $form, array $options): void { - parent::finishView($view, $form, $options); // TODO: Change the autogenerated stub + parent::finishView($view, $form, $options); foreach ($view->children as $child) { $child->vars['help'] = $options['choice_help_labels'][$child->vars['value']] ?? []; diff --git a/src/Form/DepartmentType.php b/src/Form/DepartmentType.php index c131e131..f965979f 100644 --- a/src/Form/DepartmentType.php +++ b/src/Form/DepartmentType.php @@ -8,6 +8,8 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; /** * @template-extends AbstractType @@ -20,10 +22,23 @@ class DepartmentType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder + ->add('short_tag', TextType::class, [ + 'label' => 'Shortname', + 'required' => true, + 'help' => 'Short name of the agency or ministry', + 'constraints' => [ + new NotBlank(), + new Length(['min' => 2, 'max' => 10]), + ], + ]) ->add('name', TextType::class, [ - 'label' => 'Naam', + 'label' => 'Name', 'required' => true, - 'help' => 'Naam van ministerie of organisatie', + 'help' => 'Name of the agency or ministry', + 'constraints' => [ + new NotBlank(), + new Length(['min' => 2, 'max' => 100]), + ], ]) ->add('submit', SubmitType::class, [ 'label' => 'Opslaan', diff --git a/src/Form/Document/IngestFormType.php b/src/Form/Document/IngestFormType.php index 8eb1c2c9..e6fd05ec 100644 --- a/src/Form/Document/IngestFormType.php +++ b/src/Form/Document/IngestFormType.php @@ -25,7 +25,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'mapped' => false, ]) ->add('submit', SubmitType::class, [ - 'label' => 'Ingest document', + 'label' => 'Ingest dossier', 'attr' => [ 'class' => 'btn btn-primary', ], diff --git a/src/Form/Dossier/DossierType.php b/src/Form/Dossier/DossierType.php index f8226424..695f2f19 100644 --- a/src/Form/Dossier/DossierType.php +++ b/src/Form/Dossier/DossierType.php @@ -10,7 +10,9 @@ use App\Entity\GovernmentOfficial; use App\Form\Transformer\DocumentPrefixTransformer; use App\Form\Transformer\EntityToArrayTransformer; +use App\Form\Transformer\TextToArrayTransformer; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -20,9 +22,13 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\ReversedTransformer; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\File; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; /** * @template-extends AbstractType @@ -33,13 +39,17 @@ class DossierType extends AbstractType { protected EntityManagerInterface $doctrine; - protected const ACCEPTED_MIMETYPES = [ + protected const SPREADSHEET_MIMETYPES = [ 'application/xls', 'application/vnd.ms-excel', 'application/vnd.oasis.opendocument.spreadsheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ]; + protected const DOCUMENT_MIMETYPES = [ + 'application/pdf', + ]; + public function __construct(EntityManagerInterface $doctrine) { $this->doctrine = $doctrine; @@ -47,20 +57,33 @@ public function __construct(EntityManagerInterface $doctrine) /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder + ->add('dossier_nr', TextType::class, [ + 'label' => 'Dossier nummer', + 'required' => true, + 'help' => 'Verplicht dossier nummer. Let op: het dossier nummer moet uniek zijn en kan na aanmaken niet meer gewijzigd worden.', + 'attr' => ['class' => 'w-full'], + 'constraints' => [ + new NotBlank(), + new Length(['min' => 3, 'max' => 255]), + ], + ]) ->add('title', TextType::class, [ 'label' => 'Titel', 'required' => true, 'help' => 'Geef een korte titel voor het dossier', + 'attr' => ['class' => 'w-full'], ]) - ->add('summary', TextAreaType::class, [ + ->add('summary', TextareaType::class, [ 'label' => 'Omschrijving', 'required' => true, 'help' => 'Geef een korte omschrijving voor het dossier', 'constraints' => [], + 'attr' => ['class' => 'w-full'], ]) ->add('departments', EntityType::class, [ 'class' => Department::class, @@ -68,7 +91,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'multiple' => false, 'help' => 'Het departement waar het dossier onder hoort', - 'choice_label' => 'name', + 'choice_label' => 'name_and_short', + 'query_builder' => function (EntityRepository $er) { + return $er->createQueryBuilder('d') + ->orderBy('d.name', 'ASC'); + }, ]) ->add('governmentofficials', EntityType::class, [ 'class' => GovernmentOfficial::class, @@ -83,7 +110,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'choice_label' => 'prefix', 'label' => 'Document prefix', 'required' => true, - 'help' => 'Het document prefix bepaald onder welk domein de documenten van dit dossier vallen', + 'help' => 'Het document prefix bepaalt onder welk domein de documenten van dit dossier vallen', 'placeholder' => 'Selecteer een prefix', ]) ->add('publication_reason', ChoiceType::class, [ @@ -91,23 +118,40 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => true, 'help' => 'De reden waarom dit dossier gepubliceerd wordt', 'choices' => [ - 'Wob: verzoek' => Dossier::REASON_WOB_REQUEST, - 'Woo: verzoek' => Dossier::REASON_WOO_REQUEST, - 'Woo: actieve openbaarmaking' => Dossier::REASON_WOO_ACTIVE, + 'Wob-verzoek' => Dossier::REASON_WOB_REQUEST, + 'Woo-verzoek' => Dossier::REASON_WOO_REQUEST, + 'Woo-actieve openbaarmaking' => Dossier::REASON_WOO_ACTIVE, ], ]) ->add('decision', ChoiceType::class, [ - 'label' => 'Genomen besluit', + 'label' => 'Soort besluit', 'required' => true, 'help' => 'Het besluit omtrent dit dossier', 'choices' => [ 'Reeds openbaar' => Dossier::DECISION_ALREADY_PUBLIC, 'Openbaar' => Dossier::DECISION_PUBLIC, - 'Gedeeltelijk openbaar' => Dossier::DECISION_PARTIAL_PUBLIC, + 'Deels openbaar' => Dossier::DECISION_PARTIAL_PUBLIC, 'Niet openbaar' => Dossier::DECISION_NOT_PUBLIC, 'Niets aangetroffen' => Dossier::DECISION_NOTHING_FOUND, ], ]) + ->add('decision_document', FileType::class, [ + 'label' => 'Besluit document', + 'required' => false, + 'help' => 'Het document met een motivatie van het besluit', + 'mapped' => false, + 'constraints' => [ + new File([ + 'mimeTypes' => self::DOCUMENT_MIMETYPES, + 'mimeTypesMessage' => 'Please upload a valid decision document (pdf)', + ]), + ], + ]) + ->add('default_subjects', TextType::class, [ + 'label' => 'Default subject', + 'required' => false, + 'help' => 'Onderwerp dat standaard aan documenten binnen dit dossier worden toegevoegd indien er geen onderwerp is meegeven', + ]) ->add('date_from', DateType::class, [ 'label' => 'Periode van', 'required' => false, @@ -131,7 +175,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'constraints' => [ new File([ 'maxSize' => '1024k', - 'mimeTypes' => self::ACCEPTED_MIMETYPES, + 'mimeTypes' => self::SPREADSHEET_MIMETYPES, 'mimeTypesMessage' => 'Please upload a valid spreadsheet', ]), ], @@ -141,10 +185,23 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); $this->addTransformers($builder); + + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { + /** @var Dossier|null $dossier */ + $dossier = $event->getData(); + $form = $event->getForm(); + + if ($dossier && $dossier->getId() !== null) { + $form->remove('dossier_nr'); + } + }); } protected function addTransformers(FormBuilderInterface $builder): void { + // Default subjects is a text field, but holds semicolon separated files + $builder->get('default_subjects')->addModelTransformer(new ReversedTransformer(new TextToArrayTransformer(';')), forceAppend: true); + // If we are editing an entity, we need to transform the entity to an array if the choice is not multiple. This is because the dossier // entity always expects an array of entities, even if the choice is not multiple. if ($builder->get('departments')->getOption('multiple') == false) { @@ -162,7 +219,7 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Dossier::class, - 'edit_mode' => false, // Set to true if we are editting an entity. + 'edit_mode' => false, // Set to true if we are editing an entity. ]); } } diff --git a/src/Form/Dossier/SearchFormType.php b/src/Form/Dossier/SearchFormType.php index 875b649e..69904d9f 100644 --- a/src/Form/Dossier/SearchFormType.php +++ b/src/Form/Dossier/SearchFormType.php @@ -10,7 +10,6 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; /** @@ -43,12 +42,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'expanded' => true, 'multiple' => true, ]) - ->add('searchterm', TextType::class, [ - 'required' => false, - ]) + ->add('submit', SubmitType::class, [ 'attr' => [ - 'class' => 'btn btn-outline-dark btn-rounded waves-effect', + 'class' => 'icon icon-search', ], ]) ->setMethod('GET') diff --git a/src/Form/GovernmentOfficialType.php b/src/Form/GovernmentOfficialType.php index 046bb7a6..c42b2fb5 100644 --- a/src/Form/GovernmentOfficialType.php +++ b/src/Form/GovernmentOfficialType.php @@ -23,7 +23,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('name', TextType::class, [ 'label' => 'Naam', 'required' => true, - 'help' => 'Naam van bewindvoerder', + 'help' => 'Naam van bewindspersoon', ]) ->add('submit', SubmitType::class, [ 'label' => 'Opslaan', diff --git a/src/Form/User/UserCreateFormType.php b/src/Form/User/UserCreateFormType.php index fea2959e..0a472494 100644 --- a/src/Form/User/UserCreateFormType.php +++ b/src/Form/User/UserCreateFormType.php @@ -25,20 +25,20 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('name', TextType::class, [ - 'label' => 'Naam', + 'label' => 'Name', ]) ->add('email', TextType::class, [ - 'label' => 'E-mailadres', + 'label' => 'E-mail address', ]) ->add('roles', ChoiceTypeWithHelp::class, [ 'choices' => $this->createChoices(Roles::roleDetails()), 'choice_help_labels' => $this->createHelp(Roles::roleDetails()), 'multiple' => true, 'expanded' => true, - 'label' => 'Rollen', + 'label' => 'Roles', ]) ->add('submit', SubmitType::class, [ - 'label' => 'Gebruiker aanmaken', + 'label' => 'Create user', ]) ; diff --git a/src/Form/User/UserRoleFormType.php b/src/Form/User/UserRoleFormType.php index 93824c80..e28bb3c3 100644 --- a/src/Form/User/UserRoleFormType.php +++ b/src/Form/User/UserRoleFormType.php @@ -30,7 +30,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'expanded' => true, ]) ->add('submit', SubmitType::class, [ - 'label' => 'Opslaan', + 'label' => 'Save', ]) ; diff --git a/src/Message/ProcessDocumentMessage.php b/src/Message/ProcessDocumentMessage.php index 84cdcd02..6ba07027 100644 --- a/src/Message/ProcessDocumentMessage.php +++ b/src/Message/ProcessDocumentMessage.php @@ -8,8 +8,7 @@ class ProcessDocumentMessage { - // @todo: Rename to $dossierUuid, because this suggest the document uuid - protected Uuid $uuid; + protected Uuid $dossierUuid; protected string $remotePath; protected bool $chunked; protected string $chunkUuid; @@ -17,14 +16,14 @@ class ProcessDocumentMessage protected string $originalFilename; public function __construct( - Uuid $uuid, + Uuid $dossierUuid, string $remotePath, string $originalFilename, bool $chunked = false, string $chunkUuid = '', int $chunkCount = 0 ) { - $this->uuid = $uuid; + $this->dossierUuid = $dossierUuid; $this->remotePath = $remotePath; $this->chunked = $chunked; $this->chunkUuid = $chunkUuid; @@ -32,9 +31,9 @@ public function __construct( $this->originalFilename = $originalFilename; } - public function getUuid(): Uuid + public function getDossierUuid(): Uuid { - return $this->uuid; + return $this->dossierUuid; } public function isChunked(): bool diff --git a/src/MessageHandler/GenerateArchiveHandler.php b/src/MessageHandler/GenerateArchiveHandler.php index f64a86cf..770a957d 100644 --- a/src/MessageHandler/GenerateArchiveHandler.php +++ b/src/MessageHandler/GenerateArchiveHandler.php @@ -11,6 +11,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +/** + * This handler will process a generate archive message. Its task is to generate a ZIP archive file for the given documents in the message. + */ #[AsMessageHandler] class GenerateArchiveHandler { diff --git a/src/MessageHandler/IngestAudioHandler.php b/src/MessageHandler/IngestAudioHandler.php index a5a2c09c..01f18f95 100644 --- a/src/MessageHandler/IngestAudioHandler.php +++ b/src/MessageHandler/IngestAudioHandler.php @@ -11,6 +11,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +/** + * Ingest an audio file into the system. + */ #[AsMessageHandler] class IngestAudioHandler { diff --git a/src/MessageHandler/IngestDossiersHandler.php b/src/MessageHandler/IngestDossiersHandler.php index 0250e614..a64734df 100644 --- a/src/MessageHandler/IngestDossiersHandler.php +++ b/src/MessageHandler/IngestDossiersHandler.php @@ -13,6 +13,9 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\MessageBusInterface; +/** + * Ingest multiple dossiers that are publishable into the system. Used primarily for reindexing. + */ #[AsMessageHandler] class IngestDossiersHandler { diff --git a/src/MessageHandler/IngestPdfHandler.php b/src/MessageHandler/IngestPdfHandler.php index 1e6e60e5..36d34dc4 100644 --- a/src/MessageHandler/IngestPdfHandler.php +++ b/src/MessageHandler/IngestPdfHandler.php @@ -14,6 +14,9 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\MessageBusInterface; +/** + * Ingest a PDF file into the system. It will extract all pages fromm the pdf and emits a message for each page. + */ #[AsMessageHandler] class IngestPdfHandler { diff --git a/src/MessageHandler/IngestPdfPageHandler.php b/src/MessageHandler/IngestPdfPageHandler.php index 2e82f100..9688c2e0 100644 --- a/src/MessageHandler/IngestPdfPageHandler.php +++ b/src/MessageHandler/IngestPdfPageHandler.php @@ -11,6 +11,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +/** + * Ingest a single PDF page into the system. + */ #[AsMessageHandler] class IngestPdfPageHandler { diff --git a/src/MessageHandler/InitializeElasticRolloverHandler.php b/src/MessageHandler/InitializeElasticRolloverHandler.php index 26a52164..e46f4591 100644 --- a/src/MessageHandler/InitializeElasticRolloverHandler.php +++ b/src/MessageHandler/InitializeElasticRolloverHandler.php @@ -12,6 +12,9 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\MessageBusInterface; +/** + * Initialize the elasticsearch rollover and does a dossier ingestion of all dossiers. + */ #[AsMessageHandler] class InitializeElasticRolloverHandler { diff --git a/src/MessageHandler/ProcessDocumentHandler.php b/src/MessageHandler/ProcessDocumentHandler.php index 30428efa..fa4ea364 100644 --- a/src/MessageHandler/ProcessDocumentHandler.php +++ b/src/MessageHandler/ProcessDocumentHandler.php @@ -6,39 +6,33 @@ use App\Entity\Dossier; use App\Message\ProcessDocumentMessage; -use App\Service\DocumentService; +use App\Service\FileProcessService; use App\Service\Storage\DocumentStorageService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +/** + * Process a document (archive, pdf) that is uploaded to the system. If the upload has been chunked, it will be stitched together first. + */ #[AsMessageHandler] class ProcessDocumentHandler { - protected EntityManagerInterface $doctrine; - protected LoggerInterface $logger; - protected DocumentService $documentService; - protected DocumentStorageService $storageService; - public function __construct( - DocumentService $documentService, - DocumentStorageService $storageService, - EntityManagerInterface $doctrine, - LoggerInterface $logger + private readonly FileProcessService $fileProcessService, + private readonly DocumentStorageService $storageService, + private readonly EntityManagerInterface $doctrine, + private readonly LoggerInterface $logger ) { - $this->documentService = $documentService; - $this->storageService = $storageService; - $this->doctrine = $doctrine; - $this->logger = $logger; } public function __invoke(ProcessDocumentMessage $message): void { - $dossier = $this->doctrine->getRepository(Dossier::class)->find($message->getUuid()); + $dossier = $this->doctrine->getRepository(Dossier::class)->find($message->getDossierUuid()); if (! $dossier) { // No dossier found for this message $this->logger->warning('No dossier found for this message', [ - 'dossier_uuid' => $message->getUuid(), + 'dossier_uuid' => $message->getDossierUuid(), ]); return; @@ -49,7 +43,7 @@ public function __invoke(ProcessDocumentMessage $message): void $localFile = $this->assembleChunks($message->getChunkUuid(), $message->getChunkCount()); if (! $localFile) { $this->logger->error('Could not assemble chunks', [ - 'dossier_uuid' => $message->getUuid(), + 'dossier_uuid' => $message->getDossierUuid(), 'chunk_uuid' => $message->getChunkUuid(), 'chunk_count' => $message->getChunkCount(), ]); @@ -57,7 +51,7 @@ public function __invoke(ProcessDocumentMessage $message): void return; } - $this->documentService->processDocument($localFile, $dossier, $message->getOriginalFilename()); + $this->fileProcessService->processFile($localFile, $dossier, $message->getOriginalFilename()); unlink($localFile->getPathname()); return; @@ -67,7 +61,7 @@ public function __invoke(ProcessDocumentMessage $message): void $localFilePath = $this->storageService->download($message->getRemotePath()); if (! $localFilePath) { $this->logger->error('File could not be downloaded', [ - 'dossier_uuid' => $message->getUuid(), + 'dossier_uuid' => $message->getDossierUuid(), 'file_path' => $message->getRemotePath(), ]); @@ -75,23 +69,28 @@ public function __invoke(ProcessDocumentMessage $message): void } $localFile = new \SplFileObject($localFilePath); - $this->documentService->processDocument($localFile, $dossier, $message->getOriginalFilename()); - $this->storageService->removeDownload($localFilePath); + try { + $this->fileProcessService->processFile($localFile, $dossier, $message->getOriginalFilename()); + } catch (\Throwable $e) { + throw $e; + } finally { + $this->storageService->removeDownload($localFilePath, true); + } } - protected function assembleChunks(string $uuid, int $chunkCount): ?\SplFileInfo + protected function assembleChunks(string $chunkUuid, int $chunkCount): ?\SplFileInfo { - $path = sprintf('%s/assembled-%s', sys_get_temp_dir(), $uuid); + $path = sprintf('%s/assembled-%s', sys_get_temp_dir(), $chunkUuid); $stitchedFile = new \SplFileObject($path, 'w'); for ($i = 0; $i < $chunkCount; $i++) { // Check if the chunk exists - $remoteChunkPath = '/uploads/chunks/' . $uuid . '/' . $i; + $remoteChunkPath = '/uploads/chunks/' . $chunkUuid . '/' . $i; $localChunkFile = $this->storageService->download($remoteChunkPath); if (! $localChunkFile) { $this->logger->error('Chunk is not readable', [ - 'uuid' => $uuid, + 'uuid' => $chunkUuid, 'chunk' => $i, ]); diff --git a/src/MessageHandler/SetElasticAliasHandler.php b/src/MessageHandler/SetElasticAliasHandler.php index d5a39ed2..9e3fb7c5 100644 --- a/src/MessageHandler/SetElasticAliasHandler.php +++ b/src/MessageHandler/SetElasticAliasHandler.php @@ -10,6 +10,9 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\MessageBusInterface; +/** + * Set the elasticsearch alias to the given index. + */ #[AsMessageHandler] class SetElasticAliasHandler { diff --git a/src/MessageHandler/UpdateDepartmentHandler.php b/src/MessageHandler/UpdateDepartmentHandler.php index dc6db976..789a7c1f 100644 --- a/src/MessageHandler/UpdateDepartmentHandler.php +++ b/src/MessageHandler/UpdateDepartmentHandler.php @@ -10,6 +10,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +/** + * Update a department in elasticsearch. + */ #[AsMessageHandler] class UpdateDepartmentHandler { diff --git a/src/MessageHandler/UpdateDossierHandler.php b/src/MessageHandler/UpdateDossierHandler.php index 73a9c926..4a9505c6 100644 --- a/src/MessageHandler/UpdateDossierHandler.php +++ b/src/MessageHandler/UpdateDossierHandler.php @@ -11,6 +11,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +/** + * Update a dossier data based on info in the database into elasticsearch. + */ #[AsMessageHandler] class UpdateDossierHandler { diff --git a/src/MessageHandler/UpdateOfficialHandler.php b/src/MessageHandler/UpdateOfficialHandler.php index babe37ab..ccfd1be4 100644 --- a/src/MessageHandler/UpdateOfficialHandler.php +++ b/src/MessageHandler/UpdateOfficialHandler.php @@ -10,6 +10,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +/** + * Update an official in elasticsearch. + */ #[AsMessageHandler] class UpdateOfficialHandler { diff --git a/src/Repository/BatchDownloadRepository.php b/src/Repository/BatchDownloadRepository.php index aac99c74..2405688d 100644 --- a/src/Repository/BatchDownloadRepository.php +++ b/src/Repository/BatchDownloadRepository.php @@ -77,4 +77,14 @@ public function findExpiredArchives(): array ->getQuery() ->getResult(); } + + public function pruneExpired(): void + { + $this->createQueryBuilder('b') + ->delete() + ->andWhere('b.expiration < :now') + ->setParameter('now', new \DateTimeImmutable()) + ->getQuery() + ->execute(); + } } diff --git a/src/Repository/DocumentRepository.php b/src/Repository/DocumentRepository.php index 5f9b513a..d1059d75 100644 --- a/src/Repository/DocumentRepository.php +++ b/src/Repository/DocumentRepository.php @@ -5,8 +5,10 @@ namespace App\Repository; use App\Entity\Document; +use App\Entity\Dossier; use App\Service\Elastic\Model\DocumentCounts; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Persistence\ManagerRegistry; /** @@ -42,30 +44,49 @@ public function remove(Document $entity, bool $flush = false): void } } - // /** - // * @return Document[] Returns an array of Document objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('s') - // ->andWhere('s.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('s.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?Document - // { - // return $this->createQueryBuilder('s') - // ->andWhere('s.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + /** + * @return Document[] + */ + public function findByThreadId(int $threadId, bool $onlyPublished = true): array + { + $qb = $this->createQueryBuilder('d') + ->innerJoin('d.dossiers', 'ds') + ->where('d.threadId = :threadId') + ->orderBy('d.documentDate', 'ASC') + ->setParameter('threadId', $threadId) + ; + + if ($onlyPublished) { + $qb + ->andWhere('ds.status = :status') + ->setParameter('status', Dossier::STATUS_PUBLISHED) + ; + } + + return $qb->getQuery()->getResult(); + } + + /** + * @return Document[] + */ + public function findByFamilyId(int $familyId, bool $onlyPublished = true): array + { + $qb = $this->createQueryBuilder('d') + ->innerJoin('d.dossiers', 'ds') + ->where('d.familyId = :familyId') + ->orderBy('d.documentDate', 'ASC') + ->setParameter('familyId', $familyId) + ; + + if ($onlyPublished) { + $qb + ->andWhere('ds.status = :status') + ->setParameter('status', Dossier::STATUS_PUBLISHED) + ; + } + + return $qb->getQuery()->getResult(); + } public function pagecount(): int { @@ -88,8 +109,6 @@ public function getCountAndPageSumForStatuses(array $dossierStatuses = []): Docu ->addSelect('SUM(d.pageCount) as totalPageCount') ->innerJoin('d.dossiers', 'ds') ->where($qb->expr()->in('ds.status', ':statuses')) - ->andWhere('d NOT INSTANCE OF App\Entity\Inventory') - ->andWhere('d NOT INSTANCE OF App\Entity\Decision') ->setParameters([ 'statuses' => $dossierStatuses, ]); @@ -103,26 +122,70 @@ public function getCountAndPageSumForStatuses(array $dossierStatuses = []): Docu ); } - // /** - // * @return Document[] - // */ - // public function findLatests(int $limit): array - // { - // return $this->createQueryBuilder('d') - // ->orderBy('d.createdAt', 'DESC') - // ->setMaxResults($limit) - // ->getQuery() - // ->getResult(); - // } - - // public function findByDossierAndDocument(string $dossierId, string $documentId) - // { - // return $this->createQueryBuilder('d') - // ->andWhere(':dossierId MEMBER OF d.dossiers') - // ->andWhere('d.id = :documentId') - // ->setParameter('dossierId', $dossierId) - // ->setParameter('documentId', $documentId) - // ->getQuery() - // ->getOneOrNullResult(); - // } + public function getRelatedDocumentsByThread(Document $document): ArrayCollection + { + $threadId = $document->getThreadId(); + if ($threadId < 1) { + return new ArrayCollection(); + } + + $threadDocuments = new ArrayCollection( + $this->findByThreadId($threadId) + ); + + return $threadDocuments->filter( + fn (Document $threadDocument): bool => $threadDocument->getId() !== $document->getId() + ); + } + + public function getRelatedDocumentsByFamily(Document $document): ArrayCollection + { + $familyId = $document->getFamilyId(); + if ($familyId < 1) { + return new ArrayCollection(); + } + + $familyDocuments = new ArrayCollection( + $this->findByFamilyId($familyId) + ); + + return $familyDocuments->filter( + fn (Document $familyDocument): bool => $familyDocument->getId() !== $document->getId() + ); + } + + /** + * @return Document[] + */ + public function findBySearchTerm(string $searchTerm, int $limit): array + { + $qb = $this->createQueryBuilder('d') + ->innerJoin('d.dossiers', 'ds') + ->leftJoin('d.inquiries', 'i') + ->where('d.fileInfo.name LIKE :searchTerm') + ->orWhere('d.documentNr LIKE :searchTerm') + ->orWhere('i.casenr LIKE :searchTerm') + ->orderBy('d.updatedAt', 'DESC') + ->setMaxResults($limit) + ->setParameter('searchTerm', '%' . $searchTerm . '%') + ; + + return $qb->getQuery()->getResult(); + } + + public function findOneByDossierAndDocumentId(Dossier $dossier, string $documentId): ?Document + { + $qb = $this->createQueryBuilder('d') + ->innerJoin('d.dossiers', 'ds') + ->where('d.documentId = :documentId') + ->andWhere('ds.id = :dossierId') + ->setParameter('documentId', $documentId) + ->setParameter('dossierId', $dossier->getId()) + ; + + /** @var ?Document $document */ + $document = $qb->getQuery()->getOneOrNullResult(); + + return $document; + } } diff --git a/src/Repository/DossierRepository.php b/src/Repository/DossierRepository.php index 86c40a6c..05311d5a 100644 --- a/src/Repository/DossierRepository.php +++ b/src/Repository/DossierRepository.php @@ -58,13 +58,21 @@ public function findAllPublishable(): array return $dossiers; } - // public function findOneBySomeField($value): ?Dossier - // { - // return $this->createQueryBuilder('s') - // ->andWhere('s.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + /** + * @return Dossier[] + */ + public function findBySearchTerm(string $searchTerm, int $limit): array + { + $qb = $this->createQueryBuilder('d') + ->leftJoin('d.inquiries', 'i') + ->where('d.title LIKE :searchTerm') + ->orWhere('d.dossierNr LIKE :searchTerm') + ->orWhere('i.casenr LIKE :searchTerm') + ->orderBy('d.updatedAt', 'DESC') + ->setMaxResults($limit) + ->setParameter('searchTerm', '%' . $searchTerm . '%') + ; + + return $qb->getQuery()->getResult(); + } } diff --git a/src/Roles.php b/src/Roles.php index 56c7c4e8..1e1fcecf 100644 --- a/src/Roles.php +++ b/src/Roles.php @@ -6,6 +6,7 @@ class Roles { + public const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN'; public const ROLE_ADMIN = 'ROLE_ADMIN'; public const ROLE_ADMIN_USERS = 'ROLE_ADMIN_USERS'; public const ROLE_ADMIN_DOSSIERS = 'ROLE_ADMIN_DOSSIERS'; @@ -13,6 +14,11 @@ class Roles /** @var array|array{role: string, description: string, help: string}[] */ protected static array $roleInfo = [ + [ + 'role' => self::ROLE_SUPER_ADMIN, + 'description' => 'Super administrator', + 'help' => 'This user is allowed system wide operations.', + ], [ 'role' => self::ROLE_ADMIN, 'description' => 'Global administrator', @@ -36,6 +42,8 @@ class Roles ]; /** + * Returns a list of all role details that can be used in the administration system. + * * @return array{role: string, description: string, help: string}[] */ public static function roleDetails(): array diff --git a/src/Service/ArchiveService.php b/src/Service/ArchiveService.php index a89fdea8..b471f11b 100644 --- a/src/Service/ArchiveService.php +++ b/src/Service/ArchiveService.php @@ -38,6 +38,9 @@ public function __construct( /** * Generates a ZIP archive for the given batch download. Returns true on success (or already created), false otherwise. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function generateArchive(BatchDownload $batch): bool { @@ -51,11 +54,19 @@ public function generateArchive(BatchDownload $batch): bool $zip = new \ZipArchive(); $zip->open($zipArchivePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); + $documents = $batch->getDocuments(); + if (count($documents) === 0) { + foreach ($batch->getDossier()->getDocuments() as $document) { + $documents[] = $document->getDocumentNr(); + } + } + // Add all document files - foreach ($batch->getDocuments() as $documentNr) { + $localPaths = []; + foreach ($documents as $documentNr) { // Check (again) if the document exists in the dossier. $document = $this->findInDossier($batch->getDossier(), $documentNr); - if (! $document) { + if (! $document || ! $document->isUploaded()) { continue; } @@ -69,20 +80,41 @@ public function generateArchive(BatchDownload $batch): bool continue; } - $zip->addFile($localPath, $document->getDocumentNr() . '-' . $document->getFilename()); - $this->storageService->removeDownload($localPath); + + // Generate correct filename for this document + $fileName = $document->getDocumentNr() . '-' . $document->getFileInfo()->getName(); + if (! str_ends_with(strtolower($fileName), '.pdf')) { + $fileName .= '.pdf'; + } + + $sanitizer = new FilenameSanitizer($fileName); + $sanitizer->stripAdditionalCharacters(); + $sanitizer->stripIllegalFilesystemCharacters(); + $sanitizer->stripRiskyCharacters(); + $fileName = $sanitizer->getFilename(); + + $zip->addFile($localPath, $fileName); + + $localPaths[] = $localPath; } // Finished processing $zip->close(); - $destinationPath = sprintf('batch-%s.zip', $batch->getId()->toBase58()); - if ($this->saveZip($zipArchivePath, $destinationPath, $batch)) { + // Remove all local files + foreach ($localPaths as $localPath) { + $this->storageService->removeDownload($localPath); + } + + if ($this->saveZip($zipArchivePath, $batch->getFilename(), $batch)) { $batch->setStatus(BatchDownload::STATUS_COMPLETED); } else { $batch->setStatus(BatchDownload::STATUS_FAILED); } + // Store the documents in the batch + $batch->setDocuments($documents); + // Save new status and size $fileSize = filesize($zipArchivePath); $batch->setSize(is_int($fileSize) ? strval($fileSize) : '0'); // size == bigint == string @@ -140,4 +172,51 @@ protected function saveZip(string $zipArchivePath, string $destinationPath, Batc return true; } + + /** + * @return false|resource + */ + public function getZipStream(BatchDownload $batch) + { + try { + return $this->storage->readStream($batch->getFilename()); + } catch (FilesystemException $e) { + $this->logger->error('Failed open ZIP archive ', [ + 'batch' => $batch->getId()->toBase58(), + 'path' => $batch->getFilename(), + 'exception' => $e->getMessage(), + ]); + + return false; + } + } + + /** + * @param string[] $documents + */ + public function archiveExists(Dossier $dossier, array $documents): ?BatchDownload + { + // No documents mean all documents of the dossier + if (count($documents) === 0) { + foreach ($dossier->getDocuments() as $document) { + $documents[] = $document->getDocumentNr(); + } + } + + // Prune all expired documents (garbage collection in case cron doesn't work) + $this->doctrine->getRepository(BatchDownload::class)->pruneExpired(); + + $batches = $this->doctrine->getRepository(BatchDownload::class)->findBy([ + 'status' => BatchDownload::STATUS_COMPLETED, + 'dossier' => $dossier, + ]); + + foreach ($batches as $batch) { + if ($batch->getDocuments() === $documents) { + return $batch; + } + } + + return null; + } } diff --git a/src/Service/DocumentService.php b/src/Service/DocumentService.php index a3db778f..ea44adb7 100644 --- a/src/Service/DocumentService.php +++ b/src/Service/DocumentService.php @@ -6,130 +6,51 @@ use App\Entity\Document; use App\Entity\Dossier; +use App\Entity\WithdrawReason; +use App\Service\Ingest\IngestLogger; +use App\Service\Ingest\IngestService; +use App\Service\Ingest\Options; use App\Service\Storage\DocumentStorageService; +use App\Service\Storage\ThumbnailStorageService; use Doctrine\ORM\EntityManagerInterface; -use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\File\File; +use Symfony\Contracts\Translation\TranslatorInterface; /** - * This class will manage documents that are uploaded to the system. It can process either a PDF file and add it to a dossier, or a ZIP where - * it will find PDFs and add them to the dossier. Note that only PDFs are added when the filename of the PDF matches a document number in - * the dossier. + * This class handles Document entity management. Not to be confused with 'ES documents' or 'upload document' (files)! */ class DocumentService { public function __construct( private readonly EntityManagerInterface $doctrine, - private readonly DocumentStorageService $storage, - private readonly LoggerInterface $logger, + private readonly IngestLogger $ingestLogger, + private readonly TranslatorInterface $translator, + private readonly IngestService $ingester, + private readonly DocumentStorageService $documentStorage, + private readonly ThumbnailStorageService $thumbStorage, ) { } - public function processDocument(\SplFileInfo $file, Dossier $dossier, string $originalFile): bool + public function withdraw(Document $document, WithdrawReason $reason, string $explanation): void { - $parts = pathinfo($originalFile); - $ext = $parts['extension'] ?? ''; + $this->removeAllFilesForDocument($document); - switch ($ext) { - case 'mp3': - return $this->processFile($file, $dossier, $originalFile, 'audio'); - case 'zip': - return $this->processZip($file, $dossier); - case 'pdf': - return $this->processFile($file, $dossier, $originalFile, 'pdf'); - default: - $this->logger->error('Unsupported filetype detected', [ - 'extension' => $ext, - 'originalFile' => $originalFile, - 'dossierId' => $dossier->getId(), - ]); - throw new \RuntimeException('Unsupported filetype detected'); - } - } - - protected function processFile(\SplFileInfo $file, Dossier $dossier, string $originalFile, string $type): bool - { - // Fetch document number from the beginning of the filename. Only use digits - $originalFile = basename($originalFile); - preg_match('/^(\d+)/', $originalFile, $matches); - $documentId = $matches[1] ?? null; - - if (is_null($documentId)) { - $this->logger->error('Cannot extract document ID from the filename', [ - 'filename' => $originalFile, - 'matches' => $matches, - 'dossierId' => $dossier->getId(), - ]); - - throw new \RuntimeException('Cannot extract document id from file'); - } - - $documentNr = $dossier->getDocumentPrefix() . '-' . $documentId; - - // Find matching document entity in the database - $document = $this->doctrine->getRepository(Document::class)->findOneBy(['documentNr' => $documentNr]); - - if (! $document || $document->getDossiers()->contains($dossier) === false) { - $this->logger->error("Document with id $documentId not found", [ - 'documentId' => $documentId, - 'dossierId' => $dossier->getId(), - ]); - - throw new \RuntimeException("Document with id $documentId not found"); - } - - // Store document in storage - if (! $this->storage->storeDocument($file, $document)) { - $this->logger->error('Failed to store document', [ - 'documentId' => $documentId, - 'path' => $file->getRealPath(), - ]); - - throw new \RuntimeException("Failed to store document with id $documentId"); - } - - $document->setFileType($type); + $document->withdraw($reason, $explanation); $this->doctrine->persist($document); $this->doctrine->flush(); - return true; - } - - protected function processZip(\SplFileInfo $file, Dossier $dossier): bool - { - $zip = new \ZipArchive(); - $zip->open($file->getPathname()); - - for ($i = 0; $i != $zip->numFiles; $i++) { - $filename = $zip->getNameIndex($i); - if (! $filename) { - continue; - } - $ext = pathinfo($filename, PATHINFO_EXTENSION); - if ($ext != 'pdf') { - continue; - } - - // Extract file to tmp dir - $zip->extractTo(sys_get_temp_dir(), $filename); - - try { - $tmpPath = sprintf('%s/%s', sys_get_temp_dir(), $filename); - $this->processFile(new File($tmpPath), $dossier, $filename, 'pdf'); - } catch (\Exception) { - // do nothing. Seems like an extra file in the zip - } - - // Cleanup tmp file if needed - if (file_exists($tmpPath)) { - unlink($tmpPath); - } - } - - $zip->close(); - - return true; + // Re-ingest the document, this will update all file metadata and overwrite (with an empty set) any existing page content. + $this->ingester->ingest($document, new Options()); + + $this->ingestLogger->success( + $document, + 'withdraw', + sprintf( + 'Withdrawn with reason %s. Explanation: %s', + $this->translator->trans($reason->value), + $explanation + ) + ); } public function removeDocumentFromDossier(Dossier $dossier, Document $document): void @@ -141,11 +62,19 @@ public function removeDocumentFromDossier(Dossier $dossier, Document $document): $dossier->removeDocument($document); if ($document->getDossiers()->count() === 0) { - // Remove whole document as there are no links left + // Remove whole document including all files, as there are no links left. + $this->removeAllFilesForDocument($document); $this->doctrine->remove($document); } $this->doctrine->persist($dossier); $this->doctrine->flush(); } + + private function removeAllFilesForDocument(Document $document): void + { + $this->documentStorage->deleteAllFilesForDocument($document); + + $this->thumbStorage->deleteAllThumbsForDocument($document); + } } diff --git a/src/Service/DossierService.php b/src/Service/DossierService.php index 8627465a..5e7203b3 100644 --- a/src/Service/DossierService.php +++ b/src/Service/DossierService.php @@ -4,8 +4,16 @@ namespace App\Service; +use App\Entity\DecisionDocument; +use App\Entity\Document; use App\Entity\Dossier; +use App\Message\IngestDossierMessage; +use App\Message\RemoveDossierMessage; use App\Message\UpdateDossierMessage; +use App\Service\Inventory\InventoryService; +use App\Service\Inventory\ProcessInventoryResult; +use App\Service\Storage\DocumentStorageService; +use App\SourceType; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -13,147 +21,133 @@ /** * This class handles dossier management. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DossierService { - protected EntityManagerInterface $doctrine; - protected InventoryService $inventoryService; - protected LoggerInterface $logger; - protected MessageBusInterface $messageBus; - public function __construct( - EntityManagerInterface $doctrine, - InventoryService $inventoryService, - MessageBusInterface $messageBus, - LoggerInterface $logger + private readonly EntityManagerInterface $doctrine, + private readonly InventoryService $inventoryService, + private readonly MessageBusInterface $messageBus, + private readonly LoggerInterface $logger, + private readonly InquiryService $inquiryService, + private readonly DocumentStorageService $documentStorage, ) { - $this->doctrine = $doctrine; - $this->inventoryService = $inventoryService; - $this->logger = $logger; - $this->messageBus = $messageBus; } /** - * Creates a new dossier with inventory file. Returns an array of errors, if any. - * - * There can be multiple errors per row/line number. Note that line number 0 means - * a generic error - * - * [ - * 0 => [ - * "incorrect column count", - * ] - * 4 => [ - * "invalid date format", - * "invalid other error", - * ] - * ] - * - * @return array + * Creates a new dossier with an inventory file and decision document. */ - public function create(Dossier $dossier, UploadedFile $file = null): array - { - $dossier->setCreatedAt(new \DateTimeImmutable()); - $dossier->setUpdatedAt(new \DateTimeImmutable()); + public function create( + Dossier $dossier, + ?UploadedFile $inventoryUpload, + ?UploadedFile $decisionUpload + ): ProcessInventoryResult { $dossier->setStatus(Dossier::STATUS_CONCEPT); - // @TODO: Hardcoded dossier prefix! - $dossierNr = 'VWS-' . random_int(100, 999) . '-' . random_int(1000, 9999); - $dossier->setDossierNr($dossierNr); + $this->doctrine->persist($dossier); + if ($dossier->getId() === null) { + $this->logger->error('Dossier has an empty ID. This should not happen'); - // Wrap in transaction, so we can rollback if inventory processing fails - $this->doctrine->beginTransaction(); + return new ProcessInventoryResult(); + } - $this->doctrine->persist($dossier); + if ($decisionUpload instanceof UploadedFile) { + $this->storeDecisionDocument($decisionUpload, $dossier); + } + + // Wrap in transaction, so we can roll back if inventory processing fails + $this->doctrine->beginTransaction(); $this->doctrine->flush(); - if ($file) { - $errors = $this->inventoryService->processInventory($file, $dossier); - } else { - $errors = []; - } + $result = $this->inventoryService->processInventory($inventoryUpload, $dossier); + if ($result->isSuccessful()) { + // Commit inventory and dossier changes + $this->doctrine->commit(); - if (count($errors)) { + if ($dossier->getId()) { + $this->messageBus->dispatch(new UpdateDossierMessage($dossier->getId())); + } + + $this->logger->info('Dossier created', [ + 'dossier' => $dossier->getId(), + ]); + } else { // Rollback inventory and dossier changes $this->doctrine->rollback(); $this->logger->info('Dossier creation failed', [ 'dossier' => $dossier->getId(), - 'errors' => $errors, + 'errors' => $result->getAllErrors(), ]); - - return $errors; } - // Commit inventory and dossier changes - $this->doctrine->commit(); - - $this->logger->info('Dossier created', [ - 'dossier' => $dossier->getId(), - ]); - - return []; + return $result; } - /** - * @return array - */ - public function update(Dossier $dossier, UploadedFile $file = null): array - { - $dossier->setUpdatedAt(new \DateTimeImmutable()); + public function update( + Dossier $dossier, + ?UploadedFile $inventoryUpload, + ?UploadedFile $decisionUpload + ): ProcessInventoryResult { + if ($decisionUpload instanceof UploadedFile) { + $this->storeDecisionDocument($decisionUpload, $dossier); + } // Wrap in transaction $this->doctrine->beginTransaction(); - $this->doctrine->persist($dossier); $this->doctrine->flush(); - $errors = []; - if ($file) { - $errors = $this->inventoryService->processInventory($file, $dossier); + if ($dossier->getId() === null) { + return new ProcessInventoryResult(); } - if ($errors) { + if ($inventoryUpload instanceof UploadedFile) { + $result = $this->inventoryService->processInventory($inventoryUpload, $dossier); + } else { + $result = new ProcessInventoryResult(); + } + + if ($result->isSuccessful()) { + // Commit inventory and dossier changes + $this->doctrine->commit(); + + $this->messageBus->dispatch(new UpdateDossierMessage($dossier->getId())); + + $this->logger->info('Dossier updated', [ + 'dossier' => $dossier->getId(), + ]); + } else { // Rollback everything mutated in this transaction $this->doctrine->rollback(); $this->logger->info('Dossier update failed', [ 'dossier' => $dossier->getId(), - 'errors' => $errors, + 'errors' => $result->getAllErrors(), ]); - - return $errors; } - // Commit inventory and dossier changes - $this->doctrine->commit(); - - $this->messageBus->dispatch(new UpdateDossierMessage($dossier->getId())); - - $this->logger->info('Dossier updated', [ - 'dossier' => $dossier->getId(), - ]); - - return []; + return $result; } public function remove(Dossier $dossier): void { - // Remove documents that are only attached to this dossier - foreach ($dossier->getDocuments() as $document) { - if ($document->getDossiers()->count() == 1) { - $this->doctrine->remove($document); - } + if ($dossier->getId() === null) { + return; } - // @TODO: remove from elasticsearch - - $this->doctrine->remove($dossier); - $this->doctrine->flush(); + // Remove from elasticsearch + $this->messageBus->dispatch(new RemoveDossierMessage($dossier->getId())); } public function changeState(Dossier $dossier, string $newState): void { + if ($dossier->getId() === null) { + return; + } + if (! $dossier->isAllowedState($newState)) { $this->logger->error('Invalid state change', [ 'dossier' => $dossier->getId(), @@ -169,7 +163,7 @@ public function changeState(Dossier $dossier, string $newState): void case Dossier::STATUS_COMPLETED: // Check all documents present foreach ($dossier->getDocuments() as $document) { - if (! $document->isUploaded()) { + if ($document->shouldBeUploaded() && ! $document->isUploaded()) { $this->logger->error('Invalid state change', [ 'dossier' => $dossier->getId(), 'oldState' => $dossier->getStatus(), @@ -180,6 +174,11 @@ public function changeState(Dossier $dossier, string $newState): void throw new \InvalidArgumentException('Not all documents are uploaded in this dossier'); } } + + if ($dossier->getDecisionDocument()?->getFileInfo()->isUploaded() !== true) { + throw new \InvalidArgumentException('Decision document is missing'); + } + break; } @@ -195,4 +194,86 @@ public function changeState(Dossier $dossier, string $newState): void 'newState' => $newState, ]); } + + /** + * Store the decision document to disk and add it to the dossier. + */ + protected function storeDecisionDocument(UploadedFile $upload, Dossier $dossier): void + { + if ($dossier->getId() === null) { + return; + } + + $decisionDocument = $dossier->getDecisionDocument(); + if (! $decisionDocument) { + // Create inventory if not exists yet + $decisionDocument = new DecisionDocument(); + $dossier->setDecisionDocument($decisionDocument); + $decisionDocument->setDossier($dossier); + } + + $file = $decisionDocument->getFileInfo(); + $file->setSourceType(SourceType::SOURCE_PDF); + $file->setType('pdf'); + + // Set original filename + $filename = 'decision-' . $dossier->getDossierNr() . '.' . $upload->getClientOriginalExtension(); + $file->setName($filename); + + $this->doctrine->persist($decisionDocument); + + if (! $this->documentStorage->storeDocument($upload, $decisionDocument)) { + throw new \RuntimeException('Could not store decision document'); + } + } + + public function dispatchIngest(Dossier $dossier): void + { + if ($dossier->getId() === null) { + return; + } + + $message = new IngestDossierMessage($dossier->getId()); + $this->messageBus->dispatch($message); + } + + // Returns true when the dossier (and/or document) is allowed to be viewed. This will also + // consider documents and dossiers which are marked as preview and that are allowed by the session. + public function isViewingAllowed(Dossier $dossier, Document $document = null): bool + { + // If dossier is published, allow viewing + if ($dossier->getStatus() == Dossier::STATUS_PUBLISHED) { + return true; + } + + // If dossier is not preview, deny access + if ($dossier->getStatus() != Dossier::STATUS_PREVIEW) { + return false; + } + + $inquiryIds = $this->inquiryService->getInquiries(); + + // Check if any inquiry id from the dossier is in the session inquiry ids. + foreach ($dossier->getInquiries() as $inquiry) { + if (in_array($inquiry->getId(), $inquiryIds)) { + // Inquiry id is set in the session, so allow viewing + return true; + } + } + + // If document is not visible, and no document is given, deny viewing + if (! $document) { + return false; + } + + // Check all inquiry ids from the document to see if we have one matching in our session. + foreach ($document->getInquiries() as $inquiry) { + if (in_array($inquiry->getId(), $inquiryIds)) { + // Inquiry id is set in the session, so allow viewing + return true; + } + } + + return false; + } } diff --git a/src/Service/Elastic/ElasticClientFactory.php b/src/Service/Elastic/ElasticClientFactory.php index ded0aa1c..12e06948 100644 --- a/src/Service/Elastic/ElasticClientFactory.php +++ b/src/Service/Elastic/ElasticClientFactory.php @@ -6,6 +6,7 @@ use Elastic\Elasticsearch\ClientBuilder; use Elastic\Elasticsearch\ClientInterface; +use GuzzleHttp\Client; /** * Creates a configured Elasticsearch client. @@ -21,6 +22,10 @@ public static function create( string $mtlsCAPath = null ): ClientInterface { $builder = new ClientBuilder(); + $builder->setHttpClient(new Client([ + 'timeout' => 15, + 'connect_timeout' => 5, + ])); $builder->setHosts(explode(',', $host)); if (! empty($username)) { diff --git a/src/Service/Elastic/ElasticService.php b/src/Service/Elastic/ElasticService.php index 8b29baaa..0d6cbb4b 100644 --- a/src/Service/Elastic/ElasticService.php +++ b/src/Service/Elastic/ElasticService.php @@ -21,6 +21,7 @@ * Service for interacting with Elasticsearch. Together with the SearchService, this should be the only entrypoint to elasticsearch. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class ElasticService { @@ -43,15 +44,13 @@ public function __construct(ElasticClientInterface $elastic, LoggerInterface $lo public function updatePage(Document $document, int $pageNr, string $content): void { $this->logger->debug('[Elasticsearch][Index Page] Inserting page'); - - for ($retryCount = 0; $retryCount <= self::$maxRetries; $retryCount++) { - try { - $this->elastic->update([ - 'index' => ElasticConfig::WRITE_INDEX, - 'id' => $document->getDocumentNr(), - 'body' => [ - 'script' => [ - 'source' => <<< EOF + $this->retry(function () use ($document, $pageNr, $content) { + $this->elastic->update([ + 'index' => ElasticConfig::WRITE_INDEX, + 'id' => $document->getDocumentNr(), + 'body' => [ + 'script' => [ + 'source' => <<< EOF if (ctx._source.pages == null) { ctx._source.pages = [params.page]; } else { @@ -68,41 +67,17 @@ public function updatePage(Document $document, int $pageNr, string $content): vo } } EOF, - 'lang' => 'painless', - 'params' => [ - 'page' => [ - 'page_nr' => $pageNr, - 'content' => $content, - ], + 'lang' => 'painless', + 'params' => [ + 'page' => [ + 'page_nr' => $pageNr, + 'content' => $content, ], ], ], - ]); - - return; - } catch (ClientResponseException $e) { - if ($retryCount == self::$maxRetries) { - $this->logger->error('[Elasticsearch][Index Page] Too many retries', [ - 'message' => $e->getMessage(), - 'code' => $e->getCode(), - ]); - throw $e; - } - if ($e->getCode() != 409) { - $this->logger->error('[Elasticsearch][Index Page] An error occurred: {message}', [ - 'message' => $e->getMessage(), - 'code' => $e->getCode(), - ]); - throw $e; - } - - $waitMs = (int) ceil(min(100000 * pow(1.5, $retryCount), 5000000)); - $this->logger->notice('[Elasticsearch][Index Page] Update document version mismatch. Retrying...', [ - 'waitMs' => $waitMs, - ]); - usleep($waitMs); - } - } + ], + ]); + }); } /** @@ -121,23 +96,24 @@ public function updateDocument(Document $document, array $metadata = [], array $ $inquiryIds[] = $inquiry->getId(); } + $file = $document->getFileInfo(); $documentDoc = [ 'type' => 'document', 'document_nr' => $document->getDocumentNr(), 'dossier_nr' => $dossierIds, - 'mime_type' => $document->getMimeType(), - 'file_size' => $document->getFileSize(), - 'file_type' => $document->getFileType(), - 'source_type' => $document->getSourceType(), + 'mime_type' => $file->getMimeType(), + 'file_size' => $file->getSize(), + 'file_type' => $file->getType(), + 'source_type' => $file->getSourceType(), 'date' => $document->getDocumentDate()->format(\DateTimeInterface::ATOM), - 'filename' => $document->getFilename(), + 'filename' => $file->getName(), 'family_id' => $document->getFamilyId() ?? 0, 'document_id' => $document->getDocumentId() ?? 0, 'thread_id' => $document->getThreadId() ?? 0, 'judgement' => $document->getJudgement(), 'grounds' => $document->getGrounds(), 'subjects' => $document->getSubjects(), - 'period' => $document->getPeriod(), + 'date_period' => $document->getPeriod(), 'audio_duration' => $document->getDuration(), 'document_pages' => $document->getPageCount(), 'dossiers' => $dossiers, @@ -229,6 +205,22 @@ public function updateDossier(Dossier $dossier, bool $updateDocuments = true): v } } + public function updateDossierDecisionContent(Dossier $dossier, string $content): void + { + $dossierDoc = [ + 'decision_content' => $content, + ]; + + $this->elastic->update([ + 'index' => ElasticConfig::WRITE_INDEX, + 'id' => $dossier->getDossierNr(), + 'body' => [ + 'doc' => $dossierDoc, + 'doc_as_upsert' => true, + ], + ]); + } + public function updateAudio(Document $document, Metadata $metadata): void { $ids = []; @@ -257,11 +249,11 @@ public function updateAudio(Document $document, Metadata $metadata): void ]); } - public function documentExists(Document $document): bool + public function documentExists(string $documentNr): bool { $result = $this->elastic->exists([ 'index' => ElasticConfig::WRITE_INDEX, - 'id' => $document->getDocumentNr(), + 'id' => $documentNr, ]); /** @var Elasticsearch $result */ @@ -341,7 +333,7 @@ public function updateOfficial(GovernmentOfficial $old, GovernmentOfficial $new) } } } - + if (ctx._source.dossiers != null) { for (int i = 0; i < ctx._source.dossiers.length; i++) { if (ctx._source.dossiers[i].government_official != null) { @@ -352,7 +344,7 @@ public function updateOfficial(GovernmentOfficial $old, GovernmentOfficial $new) } } } - } + } EOF, 'lang' => 'painless', 'params' => [ @@ -393,7 +385,7 @@ public function updateDepartment(Department $old, Department $new): void } } } - + if (ctx._source.dossiers != null) { for (int i = 0; i < ctx._source.dossiers.length; i++) { if (ctx._source.dossiers[i].departments != null) { @@ -404,7 +396,7 @@ public function updateDepartment(Department $old, Department $new): void } } } - } + } EOF, 'lang' => 'painless', 'params' => [ @@ -464,46 +456,120 @@ public function getLogger(): LoggerInterface private function updateAllDocumentsForDossier(Dossier $dossier, array $dossierDoc): void { $this->logger->debug('[Elasticsearch][Update Dossier] Updating dossier in document'); - for ($retryCount = 0; $retryCount <= self::$maxRetries; $retryCount++) { - try { - $this->elastic->updateByQuery([ - 'index' => ElasticConfig::WRITE_INDEX, - 'body' => [ - 'query' => [ - 'bool' => [ - 'must' => [ - ['match' => ['type' => Config::TYPE_DOCUMENT]], - ['match' => ['dossier_nr' => $dossier->getDossierNr()]], - ], + $this->retry(function () use ($dossier, $dossierDoc) { + $this->elastic->updateByQuery([ + 'index' => ElasticConfig::WRITE_INDEX, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [ + ['match' => ['type' => Config::TYPE_DOCUMENT]], + ['match' => ['dossier_nr' => $dossier->getDossierNr()]], ], ], - 'script' => [ - 'source' => <<< EOF - for (int i = 0; i < ctx._source.dossiers.length; i++) { - if (ctx._source.dossiers[i].dossier_nr == params.dossier.dossier_nr) { - ctx._source.dossiers[i] = params.dossier; - } + ], + 'script' => [ + 'source' => <<< EOF + for (int i = 0; i < ctx._source.dossiers.length; i++) { + if (ctx._source.dossiers[i].dossier_nr == params.dossier.dossier_nr) { + ctx._source.dossiers[i] = params.dossier; } + } EOF, - 'lang' => 'painless', - 'params' => [ - 'dossier' => $dossierDoc, + 'lang' => 'painless', + 'params' => [ + 'dossier' => $dossierDoc, + ], + ], + ], + ]); + }); + } + + // Removes the nested dossier entry from all documents that have this dossier. Note that this can + // leave orphaned documents (documents that do not have any dossiers as nested entities). We assume + // that these are cleaned up BEFORE running this function. + private function removeAllDocumentsForDossier(Dossier $dossier): void + { + $this->retry(function () use ($dossier) { + $this->elastic->updateByQuery([ + 'index' => ElasticConfig::WRITE_INDEX, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [ + ['match' => ['type' => Config::TYPE_DOCUMENT]], + ['match' => ['dossier_nr' => $dossier->getDossierNr()]], ], ], ], - ]); + 'script' => [ + 'source' => <<< EOF + for (int i = ctx._source.dossiers.length-1; i>=0; i-) { + if (ctx._source.dossiers[i].dossier_nr == params.dossier_nr) { + ctx._source.dossiers.remove(i); + } + } +EOF, + 'lang' => 'painless', + 'params' => [ + 'dossier_nr' => $dossier->getDossierNr(), + ], + ], + ], + ]); + }); + } + + // Removes a given document + public function removeDocument(string $documentNr): void + { + if (! $this->documentExists($documentNr)) { + return; + } + + // @Note: it's possible that the document is removed in between checking for existence and deleting. + + // Delete document + $this->elastic->delete([ + 'index' => ElasticConfig::WRITE_INDEX, + 'id' => $documentNr, + ]); + } + + // Removes a dossier and all references inside documents that have this dossier as nested object. + public function removeDossier(Dossier $dossier): void + { + // Remove all dossier entries found in documents + $this->removeAllDocumentsForDossier($dossier); + + // Delete dossier document + $this->elastic->delete([ + 'index' => ElasticConfig::WRITE_INDEX, + 'id' => $dossier->getDossierNr(), + ]); + } + + // Will retry a callable for a specified number of times. If the callable throws a ClientResponseException with a 409 code, it will + // retry the callable. If the callable throws a ClientResponseException with a different code, it will throw the exception. + // If the callable throws any other exception, it will throw the exception. + protected function retry(callable $fn): void + { + for ($retryCount = 0; $retryCount <= self::$maxRetries; $retryCount++) { + try { + $fn(); return; } catch (ClientResponseException $e) { if ($retryCount == self::$maxRetries) { - $this->logger->error('[Elasticsearch][Update Dossier] Too many retries', [ + $this->logger->error('[Elasticsearch] Too many retries', [ 'message' => $e->getMessage(), 'code' => $e->getCode(), ]); throw $e; } if ($e->getCode() != 409) { - $this->logger->error('[Elasticsearch][Update Dossier] An error occurred: {message}', [ + $this->logger->error('[Elasticsearch] An error occurred: {message}', [ 'message' => $e->getMessage(), 'code' => $e->getCode(), ]); @@ -511,7 +577,7 @@ private function updateAllDocumentsForDossier(Dossier $dossier, array $dossierDo } $waitMs = (int) ceil(min(100000 * pow(1.4, $retryCount), 5000000)); - $this->logger->notice('[Elasticsearch][Update Dossier] Update dossier version mismatch. Retrying...', [ + $this->logger->notice('[Elasticsearch] Update dossier version mismatch. Retrying...', [ 'waitMs' => $waitMs, ]); usleep($waitMs); diff --git a/src/Service/Elastic/IndexService.php b/src/Service/Elastic/IndexService.php index 7eb824f7..6fa4e3d1 100644 --- a/src/Service/Elastic/IndexService.php +++ b/src/Service/Elastic/IndexService.php @@ -150,18 +150,24 @@ public function find(string $name = null): array $params['index'] = $name; } - /** @var Elasticsearch $response */ - $response = $this->elastic->cat()->indices($params); + /** @var Elasticsearch $indicesResponse */ + $indicesResponse = $this->elastic->cat()->indices($params); + + /** @var Elasticsearch $mappingResponse */ + $mappingResponse = $this->elastic->indices()->getMapping(); + $mappingData = $mappingResponse->asArray(); $indices = []; - foreach ($response->asArray() as $index) { + foreach ($indicesResponse->asArray() as $index) { $indexAliases = array_keys($aliases[$index['index']]['aliases'] ?? []); + $indices[] = new Index( name: $index['index'], health: $index['health'], status: $index['status'], docsCount: $index['docs.count'] ?? '??', storeSize: $index['store.size'] ?? '??', + mappingVersion: strval($mappingData[$index['index']]['mappings']['_meta']['version'] ?? 'unknown'), aliases: $indexAliases, ); } diff --git a/src/Service/Elastic/Model/Index.php b/src/Service/Elastic/Model/Index.php index 5b7ec1cf..d18aad5e 100644 --- a/src/Service/Elastic/Model/Index.php +++ b/src/Service/Elastic/Model/Index.php @@ -15,6 +15,7 @@ public function __construct( public readonly string $status, public readonly string $docsCount, public readonly string $storeSize, + public readonly string $mappingVersion, public readonly array $aliases = [], ) { } diff --git a/src/Service/FakeDataGenerator.php b/src/Service/FakeDataGenerator.php index 13b92272..daf3fa72 100644 --- a/src/Service/FakeDataGenerator.php +++ b/src/Service/FakeDataGenerator.php @@ -8,6 +8,7 @@ use App\Entity\Document; use App\Entity\Dossier; use App\Entity\GovernmentOfficial; +use App\Entity\Judgement; use App\SourceType; use Doctrine\ORM\EntityManagerInterface; use Faker\Factory; @@ -64,8 +65,6 @@ public function generateDossier(string $dossierNr): Dossier ]); $dossier = new Dossier(); - $dossier->setCreatedAt(new \DateTimeImmutable()); - $dossier->setUpdatedAt(new \DateTimeImmutable()); $dossier->setDossierNr($dossierNr); $dossier->setTitle($this->faker->sentence()); $dossier->setSummary($sentences); @@ -99,24 +98,48 @@ public function generateDocument(): Document $documentId = random_int(100000, 999999); $documentNr = 'PREF-' . $documentId; $document = new Document(); - $document->setCreatedAt(new \DateTimeImmutable()); - $document->setUpdatedAt(new \DateTimeImmutable()); $document->setDocumentDate(new \DateTimeImmutable()); $document->setDocumentNr($documentNr); - $document->setSourceType($sourceType); $document->setDuration(0); $document->setFamilyId($documentId); $document->setDocumentid($documentId); $document->setThreadId(0); $document->setPageCount(random_int(1, 20)); $document->setSummary('summary of the document'); - $document->setUploaded(false); - $document->setFilename('document-' . $documentNr . '.pdf'); - $document->setMimetype('application/pdf'); - $document->setFileType('pdf'); $document->setSubjects($this->generateSubjects()); - $document->setSuspended(false); - $document->setWithdrawn(false); + + $file = $document->getFileInfo(); + $file->setSourceType($sourceType); + $file->setName('document-' . $documentNr . '.pdf'); + $file->setMimetype('application/pdf'); + $file->setType('pdf'); + + switch ($randomInt = random_int(0, 10)) { + case $randomInt <= 5: + $document->setJudgement(Judgement::PUBLIC); + $file->setUploaded(true); + break; + case $randomInt <= 7: + $document->setJudgement(Judgement::PARTIAL_PUBLIC); + $file->setUploaded(true); + break; + case $randomInt <= 8: + $document->setJudgement(Judgement::ALREADY_PUBLIC); + $file->setUploaded(false); + break; + default: + $document->setJudgement(Judgement::NOT_PUBLIC); + $file->setUploaded(false); + break; + } + + if (random_int(0, 1) === 1) { + $document->setLink($this->faker->url()); + } + + if (random_int(0, 1) === 1) { + $document->setRemark($this->faker->text()); + } return $document; } diff --git a/src/Service/FileUploader.php b/src/Service/FileUploader.php index 948afd2b..4162292c 100644 --- a/src/Service/FileUploader.php +++ b/src/Service/FileUploader.php @@ -8,6 +8,7 @@ use App\Form\Dossier\DocumentUploadType; use App\Message\ProcessDocumentMessage; use App\Service\Storage\DocumentStorageService; +use Psr\Log\LoggerInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; @@ -22,6 +23,7 @@ class FileUploader protected MessageBusInterface $messageBus; protected FormFactoryInterface $formFactory; protected DocumentStorageService $documentStorage; + protected LoggerInterface $logger; /** @var array|string[] */ protected array $mandatoryParams = [ @@ -32,11 +34,16 @@ class FileUploader 'dzuuid', ]; - public function __construct(MessageBusInterface $messageBus, FormFactoryInterface $formFactory, DocumentStorageService $documentStorage) - { + public function __construct( + MessageBusInterface $messageBus, + FormFactoryInterface $formFactory, + DocumentStorageService $documentStorage, + LoggerInterface $logger, + ) { $this->messageBus = $messageBus; $this->formFactory = $formFactory; $this->documentStorage = $documentStorage; + $this->logger = $logger; } /** @@ -55,6 +62,10 @@ public function handleUpload(Request $request, Dossier $dossier): bool protected function handleCompleteFiles(Request $request, Dossier $dossier): bool { + if ($dossier->getId() == null) { + return false; + } + // Check if document uploaded is valid (e.g. not too large). This is done through the validation of // a form (@TODO: we should use direct validation for this, not through a form) $form = $this->formFactory->create(DocumentUploadType::class, $dossier, ['csrf_protection' => false]); @@ -66,15 +77,21 @@ protected function handleCompleteFiles(Request $request, Dossier $dossier): bool // Dispatch message for each file uploaded to process the files /** @var UploadedFile[] $uploadedFiles */ $uploadedFiles = $request->files->get('document_upload'); - // $uploadedFiles = $form->get('upload')->getData(); foreach ($uploadedFiles as $uploadedFile) { + $this->logger->info('uploaded document file', [ + 'path' => $uploadedFile->getRealPath(), + 'original_file' => $uploadedFile->getClientOriginalName(), + 'size' => $uploadedFile->getSize(), + 'file_hash' => hash_file('sha256', $uploadedFile->getRealPath()), + ]); + $remotePath = '/uploads/' . (string) $dossier->getId() . '/' . $uploadedFile->getClientOriginalName(); if (! $this->documentStorage->store($uploadedFile, $remotePath)) { continue; } $message = new ProcessDocumentMessage( - uuid: $dossier->getId(), + dossierUuid: $dossier->getId(), remotePath: $remotePath, originalFilename: $uploadedFile->getClientOriginalName(), chunked: false, @@ -94,6 +111,10 @@ protected function handleCompleteFiles(Request $request, Dossier $dossier): bool */ protected function handleChunkedUpload(Request $request, Dossier $dossier): bool { + if ($dossier->getId() == null) { + return false; + } + foreach ($this->mandatoryParams as $param) { if (! $request->request->has($param)) { throw new \Exception('Missing parameter: ' . $param); @@ -110,6 +131,15 @@ protected function handleChunkedUpload(Request $request, Dossier $dossier): bool $upload = $request->files->get('document_upload'); /** @var UploadedFile $uploadedFile */ $uploadedFile = $upload['upload']; + + $this->logger->info('uploaded document chunk file', [ + 'path' => $uploadedFile->getRealPath(), + 'original_file' => $uploadedFile->getClientOriginalName(), + 'size' => $uploadedFile->getSize(), + 'chunk_index' => $chunkIndex, + 'file_hash' => hash_file('sha256', $uploadedFile->getRealPath()), + ]); + $this->documentStorage->store($uploadedFile, $remoteChunkFile); // Check if all parts have been uploaded (ie: all chunks are found in the chunk upload dir) @@ -121,7 +151,7 @@ protected function handleChunkedUpload(Request $request, Dossier $dossier): bool // Dispatch a message to process the uploaded file $message = new ProcessDocumentMessage( - uuid: $dossier->getId(), + dossierUuid: $dossier->getId(), remotePath: $remoteChunkPath, originalFilename: $uploadedFile->getClientOriginalName(), chunked: true, diff --git a/src/Service/FixtureService.php b/src/Service/FixtureService.php index ab3ec627..3c5d6f85 100644 --- a/src/Service/FixtureService.php +++ b/src/Service/FixtureService.php @@ -13,6 +13,8 @@ use App\Message\ProcessDocumentMessage; use App\Service\Elastic\ElasticService; use App\Service\Ingest\IngestService; +use App\Service\Ingest\Options; +use App\Service\Inventory\InventoryService; use App\Service\Storage\DocumentStorageService; use App\SourceType; use Doctrine\ORM\EntityManagerInterface; @@ -79,11 +81,9 @@ public function create(array $dossier, string $path): void */ protected function createDossier(array $data, string $inventoryPath, ?string $documentsPath): Dossier { - $this->validatePrefix($data['document_prefix']); + $this->ensurePrefixExists($data['document_prefix']); $dossier = new Dossier(); - $dossier->setCreatedAt(new \DateTimeImmutable()); - $dossier->setUpdatedAt(new \DateTimeImmutable()); $dossier->setDateFrom(new \DateTimeImmutable($data['period_from'])); $dossier->setDateTo(new \DateTimeImmutable($data['period_to'])); $dossier->setDecision($data['decision']); @@ -101,10 +101,10 @@ protected function createDossier(array $data, string $inventoryPath, ?string $do $this->doctrine->flush(); $file = new UploadedFile($inventoryPath, 'inventory.pdf', 'application/pdf', null, true); - $errors = $this->inventoryService->processInventory($file, $dossier); + $result = $this->inventoryService->processInventory($file, $dossier); - if (count($errors) > 0) { - throw FixtureInventoryException::forProcessingErrors($errors); + if (! $result->isSuccessful()) { + throw FixtureInventoryException::forProcessingErrors($result->getAllErrors()); } if ($documentsPath) { @@ -115,13 +115,19 @@ protected function createDossier(array $data, string $inventoryPath, ?string $do } $message = new ProcessDocumentMessage( - uuid: $dossier->getId(), + dossierUuid: $dossier->getId(), remotePath: $remotePath, originalFilename: $documentPathFileInfo->getBasename(), chunked: false, ); $this->messageBus->dispatch($message); + } else { + // If no 'real' documents are provided: still index the metadata-only documents + $options = new Options(); + foreach ($dossier->getDocuments() as $document) { + $this->ingester->ingest($document, $options); + } } if (isset($data['fake_documents']) && is_array($data['fake_documents'])) { @@ -166,19 +172,20 @@ protected function createDocument(Dossier $dossier, array $data): void $document->setCreatedAt($data['created_at']); $document->setUpdatedAt($data['updated_at']); $document->setDocumentDate($data['document_date']); - $document->setSourceType($data['source_type']); $document->setDuration($data['duration']); $document->setFamilyId($data['family_id']); $document->setThreadId($data['thread_id']); $document->setPageCount(count($data['pages'])); $document->setSummary($data['summary']); - $document->setUploaded($data['uploaded']); - $document->setFilename($data['filename']); - $document->setMimetype($data['mime_type']); - $document->setFileType($data['file_type']); $document->setSubjects($data['subjects']); $document->setSuspended($data['suspended']); - $document->setWithdrawn($data['withdrawn']); + + $file = $document->getFileInfo(); + $file->setUploaded($data['uploaded']); + $file->setName($data['filename']); + $file->setMimetype($data['mime_type']); + $file->setType($data['file_type']); + $file->setSourceType($data['source_type']); $this->doctrine->persist($document); $dossier->addDocument($document); @@ -190,10 +197,15 @@ protected function createDocument(Dossier $dossier, array $data): void $this->elasticService->setPages($document, $pages); } - private function validatePrefix(string $prefix): void + private function ensurePrefixExists(string $prefix): void { if ($this->doctrine->getRepository(DocumentPrefix::class)->count(['prefix' => $prefix]) === 0) { - throw new \RuntimeException("Prefix $prefix does not exist"); + $documentPrefix = new DocumentPrefix(); + $documentPrefix->setPrefix($prefix); + $documentPrefix->setDescription($prefix); + + $this->doctrine->persist($documentPrefix); + $this->doctrine->flush(); } } diff --git a/src/Service/Ingest/Handler.php b/src/Service/Ingest/Handler.php index 13235136..96375027 100644 --- a/src/Service/Ingest/Handler.php +++ b/src/Service/Ingest/Handler.php @@ -5,6 +5,7 @@ namespace App\Service\Ingest; use App\Entity\Document; +use App\Entity\FileInfo; interface Handler { @@ -14,7 +15,7 @@ interface Handler public function handle(Document $document, Options $options): void; /** - * Returns true when this handler can handle the given mimetype. + * Returns true when this handler can handle the given FileInfo. */ - public function canHandle(string $mimeType): bool; + public function canHandle(FileInfo $fileInfo): bool; } diff --git a/src/Service/Ingest/Handler/AudioHandler.php b/src/Service/Ingest/Handler/AudioHandler.php index ec6e9791..02d22e4d 100644 --- a/src/Service/Ingest/Handler/AudioHandler.php +++ b/src/Service/Ingest/Handler/AudioHandler.php @@ -5,15 +5,24 @@ namespace App\Service\Ingest\Handler; use App\Entity\Document; +use App\Entity\FileInfo; use App\Message\IngestAudioMessage; use App\Service\Ingest\Handler; use App\Service\Ingest\Options; +use Psr\Log\LoggerInterface; +use Symfony\Component\Messenger\MessageBusInterface; -class AudioHandler extends BaseHandler implements Handler +class AudioHandler implements Handler { + public function __construct( + private readonly MessageBusInterface $bus, + private readonly LoggerInterface $logger, + ) { + } + public function handle(Document $document, Options $options): void { - $this->logger->info('Ingesting AUDIO into document', [ + $this->logger->info('Dispatching ingest for audio document', [ 'document' => $document->getId(), ]); @@ -21,8 +30,8 @@ public function handle(Document $document, Options $options): void $this->bus->dispatch($message); } - public function canHandle(string $mimeType): bool + public function canHandle(FileInfo $fileInfo): bool { - return $mimeType === 'audio/mpeg'; + return $fileInfo->getMimetype() === 'audio/mpeg'; } } diff --git a/src/Service/Ingest/Handler/PdfHandler.php b/src/Service/Ingest/Handler/PdfHandler.php index 8e38b0ef..2c62f96f 100644 --- a/src/Service/Ingest/Handler/PdfHandler.php +++ b/src/Service/Ingest/Handler/PdfHandler.php @@ -5,43 +5,24 @@ namespace App\Service\Ingest\Handler; use App\Entity\Document; +use App\Entity\FileInfo; use App\Message\IngestPdfMessage; -use App\Service\DocumentService; use App\Service\Ingest\Handler; use App\Service\Ingest\Options; -use App\Service\Storage\DocumentStorageService; -use App\Service\Worker\Pdf\Extractor\PagecountExtractor; -use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\MessageBusInterface; -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class PdfHandler extends BaseHandler implements Handler +class PdfHandler implements Handler { - protected DocumentStorageService $storageService; - protected DocumentService $documentService; - protected PagecountExtractor $extractor; - public function __construct( - MessageBusInterface $bus, - EntityManagerInterface $doctrine, - LoggerInterface $logger, - DocumentStorageService $storageService, - DocumentService $documentService, - PagecountExtractor $extractor, + private readonly MessageBusInterface $bus, + private readonly LoggerInterface $logger, ) { - parent::__construct($bus, $doctrine, $logger); - - $this->storageService = $storageService; - $this->documentService = $documentService; - $this->extractor = $extractor; } public function handle(Document $document, Options $options): void { - $this->logger->info('Ingesting PDF for document', [ + $this->logger->info('Dispatching ingest for PDF document', [ 'document' => $document->getId(), ]); @@ -49,8 +30,8 @@ public function handle(Document $document, Options $options): void $this->bus->dispatch($message); } - public function canHandle(string $mimeType): bool + public function canHandle(FileInfo $fileInfo): bool { - return $mimeType === 'application/pdf'; + return $fileInfo->getMimetype() === 'application/pdf'; } } diff --git a/src/Service/Ingest/IngestLogger.php b/src/Service/Ingest/IngestLogger.php index a0a65f95..f4e927c6 100644 --- a/src/Service/Ingest/IngestLogger.php +++ b/src/Service/Ingest/IngestLogger.php @@ -12,6 +12,7 @@ class IngestLogger implements LoggingTypeInterface { private bool $enabled = true; + private bool $flush = true; public function __construct( private readonly EntityManagerInterface $doctrine, @@ -33,6 +34,11 @@ public function restore(): void $this->enabled = true; } + public function setFlush(bool $flush): void + { + $this->flush = $flush; + } + public function success(Document $document, string $event, string $message): void { $this->writeLogToDatabase($document, $event, $message, true); @@ -58,6 +64,9 @@ private function writeLogToDatabase(Document $document, string $event, string $m $log->setSuccess($succes); $this->doctrine->persist($log); - $this->doctrine->flush(); + + if ($this->flush) { + $this->doctrine->flush(); + } } } diff --git a/src/Service/Ingest/IngestService.php b/src/Service/Ingest/IngestService.php index a59a5845..f6af4112 100644 --- a/src/Service/Ingest/IngestService.php +++ b/src/Service/Ingest/IngestService.php @@ -28,8 +28,8 @@ public function __construct( public function ingest(Document $document, Options $options): void { foreach ($this->handlers as $handler) { - if ($handler->canHandle($document->getMimetype() ?? '')) { - $this->ingestLogger->success($document, 'ingest', 'Starting ingest on ' . $document->getFilename()); + if ($handler->canHandle($document->getFileInfo())) { + $this->ingestLogger->success($document, 'ingest', 'Starting ingest on ' . $document->getFileInfo()->getName()); $handler->handle($document, $options); diff --git a/src/Service/Search/ConfigFactory.php b/src/Service/Search/ConfigFactory.php index 32819636..6124ad82 100644 --- a/src/Service/Search/ConfigFactory.php +++ b/src/Service/Search/ConfigFactory.php @@ -6,7 +6,7 @@ use App\Service\InquiryService; use App\Service\Search\Model\Config; -use App\Service\Search\Model\Facet; +use App\Service\Search\Query\Facet\FacetMappingService; use Symfony\Component\HttpFoundation\Request; class ConfigFactory @@ -15,8 +15,10 @@ class ConfigFactory public const MIN_PAGE_SIZE = 1; public const MAX_PAGE_SIZE = 100; - public function __construct(protected InquiryService $inquiryService) - { + public function __construct( + private readonly InquiryService $inquiryService, + private readonly FacetMappingService $facetMapping, + ) { } /** @@ -32,14 +34,16 @@ public function createFromRequest( $pageNum = max($request->query->getInt('page', 1) - 1, 0); $facets = []; - foreach (Facet::getQueryMapping() as $facetKey => $queryKey) { - if (! $request->query->has($queryKey)) { + foreach ($this->facetMapping->getAll() as $facet) { + if (! $request->query->has($facet->getQueryParam())) { continue; } // Make sure that $items is always an array - $items = $request->query->all()[$queryKey]; - $items = is_array($items) ? array_values($items) : [$items]; + $items = $request->query->all()[$facet->getQueryParam()]; + if (! is_array($items)) { + $items = [$items]; + } // Url decode the strings but not numbers etc foreach ($items as $index => $item) { @@ -48,7 +52,7 @@ public function createFromRequest( } } - $facets[$facetKey] = $items; + $facets[$facet->getFacetKey()] = $items; } // Type is not a facet but must be set directly in the config diff --git a/src/Service/Search/Model/AggregationBucketEntry.php b/src/Service/Search/Model/AggregationBucketEntry.php index ea100634..a21a2e8b 100644 --- a/src/Service/Search/Model/AggregationBucketEntry.php +++ b/src/Service/Search/Model/AggregationBucketEntry.php @@ -6,13 +6,11 @@ class AggregationBucketEntry { - protected string $key; - protected int $count; - - public function __construct(string $key, int $count) - { - $this->key = $key; - $this->count = $count; + public function __construct( + private readonly string $key, + private readonly int $count, + private readonly string $displayValue, + ) { } public function getKey(): string @@ -24,4 +22,9 @@ public function getCount(): int { return $this->count; } + + public function getDisplayValue(): string + { + return $this->displayValue; + } } diff --git a/src/Service/Search/Model/Config.php b/src/Service/Search/Model/Config.php index 06688eb8..d83bcb61 100644 --- a/src/Service/Search/Model/Config.php +++ b/src/Service/Search/Model/Config.php @@ -4,6 +4,8 @@ namespace App\Service\Search\Model; +use App\Service\Search\Query\Facet\FacetDefinition; + class Config { public const OPERATOR_PHRASE = 'phrase'; @@ -34,4 +36,17 @@ public function __construct( public readonly array $dossierInquiries = [], ) { } + + public function hasFacetValues(FacetDefinition $facet): bool + { + return array_key_exists($facet->getFacetKey(), $this->facets) && count($this->facets[$facet->getFacetKey()]) > 0; + } + + /** + * @return array|mixed[] + */ + public function getFacetValues(FacetDefinition $facet): array + { + return $this->facets[$facet->getFacetKey()] ?? []; + } } diff --git a/src/Service/Search/Object/ObjectHandler.php b/src/Service/Search/Object/ObjectHandler.php index 422fedaa..543f53f1 100644 --- a/src/Service/Search/Object/ObjectHandler.php +++ b/src/Service/Search/Object/ObjectHandler.php @@ -98,19 +98,29 @@ public function getPageContent(Document $document, int $pageNr): string { $params = [ 'index' => ElasticConfig::READ_INDEX, - 'id' => $document->getDocumentNr(), 'body' => [ '_source' => false, 'query' => [ - 'nested' => [ - 'path' => 'pages', - 'query' => [ - 'term' => [ - 'pages.page_nr' => $pageNr, + 'bool' => [ + 'must' => [ + [ + 'term' => [ + '_id' => $document->getDocumentNr(), + ], + ], + [ + 'nested' => [ + 'path' => 'pages', + 'query' => [ + 'term' => [ + 'pages.page_nr' => $pageNr, + ], + ], + 'inner_hits' => [ + '_source' => 'pages.content', + ], + ], ], - ], - 'inner_hits' => [ - '_source' => 'pages.content', ], ], ], @@ -122,10 +132,10 @@ public function getPageContent(Document $document, int $pageNr): string $response = $this->elastic->search($params); $response = new TypeArray($response->asArray()); - $ontent = $response->getString('[hits][hits][0][inner_hits][pages][hits][hits][0][_source][content]', ''); + $content = $response->getString('[hits][hits][0][inner_hits][pages][hits][hits][0][_source][content]', ''); - return $ontent; - } catch (\Throwable) { + return $content; + } catch (\Throwable $e) { return ''; } } diff --git a/src/Service/Search/Query/Aggregation/AggregationStrategyInterface.php b/src/Service/Search/Query/Aggregation/AggregationStrategyInterface.php index 93d13ee6..0ad13bfd 100644 --- a/src/Service/Search/Query/Aggregation/AggregationStrategyInterface.php +++ b/src/Service/Search/Query/Aggregation/AggregationStrategyInterface.php @@ -4,10 +4,13 @@ namespace App\Service\Search\Query\Aggregation; +use App\Service\Search\Model\Config; +use App\Service\Search\Query\Facet\FacetDefinition; +use Erichard\ElasticQueryBuilder\Aggregation\AbstractAggregation; + interface AggregationStrategyInterface { - /** - * @return array - */ - public function getQuery(): array; + public function excludeOwnFilters(): bool; + + public function getAggregation(FacetDefinition $facet, Config $config, int $maxCount): AbstractAggregation; } diff --git a/src/Service/Search/Query/Aggregation/TermsAggregationStrategy.php b/src/Service/Search/Query/Aggregation/TermsAggregationStrategy.php index d13be476..9000c61f 100644 --- a/src/Service/Search/Query/Aggregation/TermsAggregationStrategy.php +++ b/src/Service/Search/Query/Aggregation/TermsAggregationStrategy.php @@ -4,31 +4,34 @@ namespace App\Service\Search\Query\Aggregation; +use App\Service\Search\Model\Config; +use App\Service\Search\Query\Dsl\TermsAggregationWithMinDocCount; +use App\Service\Search\Query\Facet\FacetDefinition; +use Erichard\ElasticQueryBuilder\Aggregation\AbstractAggregation; +use Erichard\ElasticQueryBuilder\Constants\SortDirections; + class TermsAggregationStrategy implements AggregationStrategyInterface { public function __construct( - protected string $tagName, - protected string $fieldName, - protected int $maxCount, + // Set to false for AND behaviour in facet counts. + private readonly bool $excludeOwnFilters = true, ) { } - /** - * @return array - */ - public function getQuery(): array + public function getAggregation(FacetDefinition $facet, Config $config, int $maxCount): AbstractAggregation + { + return new TermsAggregationWithMinDocCount( + name: $facet->getFacetKey(), + fieldOrSource: $facet->getPath(), + minDocCount: 1, + orderField: '_count', + orderValue: SortDirections::DESC, + size: $maxCount, + ); + } + + public function excludeOwnFilters(): bool { - return [ - $this->tagName => [ - 'terms' => [ - 'field' => $this->fieldName, - 'size' => $this->maxCount, - 'order' => [ - '_count' => 'desc', - ], - 'min_doc_count' => 0, - ], - ], - ]; + return $this->excludeOwnFilters; } } diff --git a/src/Service/Search/Query/Filter/AndTermFilter.php b/src/Service/Search/Query/Filter/AndTermFilter.php index 98307b94..7b60fc95 100644 --- a/src/Service/Search/Query/Filter/AndTermFilter.php +++ b/src/Service/Search/Query/Filter/AndTermFilter.php @@ -4,26 +4,31 @@ namespace App\Service\Search\Query\Filter; +use App\Service\Search\Model\Config; +use App\Service\Search\Query\Facet\FacetDefinition; +use Erichard\ElasticQueryBuilder\Query\BoolQuery; +use Erichard\ElasticQueryBuilder\Query\TermQuery; + /** * Meaning that all values must match. */ class AndTermFilter implements FilterInterface { - public function __construct(protected string $field) + public function addToQuery(FacetDefinition $facet, BoolQuery $query, Config $config, string $prefix = ''): void { - } + $values = $config->getFacetValues($facet); + if (count($values) === 0) { + return; + } - public function getQuery(array $values): ?array - { - $filters = []; foreach ($values as $value) { - $filters[] = ['term' => [$this->field => $value]]; + /** @var string $value */ + $query->addFilter( + new TermQuery( + field: $prefix . $facet->getPath(), + value: $value + ) + ); } - - return [ - 'bool' => [ - 'filter' => $filters, - ], - ]; } } diff --git a/src/Service/Search/Query/Filter/DateRangeFilter.php b/src/Service/Search/Query/Filter/DateRangeFilter.php index 7a86999a..5d523383 100644 --- a/src/Service/Search/Query/Filter/DateRangeFilter.php +++ b/src/Service/Search/Query/Filter/DateRangeFilter.php @@ -4,30 +4,43 @@ namespace App\Service\Search\Query\Filter; +use App\Service\Search\Model\Config; +use App\Service\Search\Query\Facet\FacetDefinition; +use Erichard\ElasticQueryBuilder\Query\BoolQuery; +use Erichard\ElasticQueryBuilder\Query\RangeQuery; + class DateRangeFilter implements FilterInterface { - public function __construct(protected string $field, protected string $comparisonOperator) - { + public function __construct( + private readonly string $comparisonOperator + ) { } - public function getQuery(array $values): ?array + public function addToQuery(FacetDefinition $facet, BoolQuery $query, Config $config, string $prefix = ''): void { + $values = $config->getFacetValues($facet); if (count($values) !== 1) { - return null; + return; } $date = $this->asDate(array_shift($values)); if ($date === null) { - return null; + return; + } + + $rangeQuery = new RangeQuery($prefix . $facet->getPath()); + switch ($this->comparisonOperator) { + case 'lte': + $rangeQuery->lte($date->format('Y-m-d')); + break; + case 'gte': + $rangeQuery->gte($date->format('Y-m-d')); + break; + default: + throw new \RuntimeException('Unknown DateRangeFilter comparison operator: ' . $this->comparisonOperator); } - return [ - 'range' => [ - $this->field => [ - $this->comparisonOperator => $date->format('Y-m-d'), - ], - ], - ]; + $query->addFilter($rangeQuery); } protected function asDate(mixed $value): ?\DateTimeImmutable @@ -38,7 +51,7 @@ protected function asDate(mixed $value): ?\DateTimeImmutable try { return new \DateTimeImmutable($value); - } catch (\Exception $e) { + } catch (\Exception) { return null; } } diff --git a/src/Service/Search/Query/Filter/FilterInterface.php b/src/Service/Search/Query/Filter/FilterInterface.php index 77c1de9e..3ca060ce 100644 --- a/src/Service/Search/Query/Filter/FilterInterface.php +++ b/src/Service/Search/Query/Filter/FilterInterface.php @@ -4,12 +4,11 @@ namespace App\Service\Search\Query\Filter; +use App\Service\Search\Model\Config; +use App\Service\Search\Query\Facet\FacetDefinition; +use Erichard\ElasticQueryBuilder\Query\BoolQuery; + interface FilterInterface { - /** - * @param mixed[] $values - * - * @return ?array - */ - public function getQuery(array $values): ?array; + public function addToQuery(FacetDefinition $facet, BoolQuery $query, Config $config, string $prefix = ''): void; } diff --git a/src/Service/Search/Query/Filter/OrTermFilter.php b/src/Service/Search/Query/Filter/OrTermFilter.php index 2929beb9..ec7d6276 100644 --- a/src/Service/Search/Query/Filter/OrTermFilter.php +++ b/src/Service/Search/Query/Filter/OrTermFilter.php @@ -4,26 +4,29 @@ namespace App\Service\Search\Query\Filter; +use App\Service\Search\Model\Config; +use App\Service\Search\Query\Facet\FacetDefinition; +use Erichard\ElasticQueryBuilder\Query\BoolQuery; +use Erichard\ElasticQueryBuilder\Query\TermsQuery; + /** * Meaning that at least one value must match. */ class OrTermFilter implements FilterInterface { - public function __construct(protected string $field) + public function addToQuery(FacetDefinition $facet, BoolQuery $query, Config $config, string $prefix = ''): void { - } + /** @var string[] $values */ + $values = $config->getFacetValues($facet); + if (count($values) === 0) { + return; + } - /** - * @param mixed[] $values - * - * @return array - */ - public function getQuery(array $values): array - { - return [ - 'terms' => [ - $this->field => $values, - ], - ]; + $query->addFilter( + new TermsQuery( + field: $prefix . $facet->getPath(), + values: $values + ) + ); } } diff --git a/src/Service/Search/Query/QueryGenerator.php b/src/Service/Search/Query/QueryGenerator.php index 9b245dbc..cfecd243 100644 --- a/src/Service/Search/Query/QueryGenerator.php +++ b/src/Service/Search/Query/QueryGenerator.php @@ -6,65 +6,59 @@ use App\ElasticConfig; use App\Service\Search\Model\Config; -use App\Service\Search\Model\Facet; -use App\Service\Search\Query\Aggregation\NestedAggregationStrategy; -use App\Service\Search\Query\Aggregation\TermsAggregationStrategy; -use App\Service\Search\Query\DossierStrategy\TopLevelDossierStrategy; +use App\Service\Search\Query\Condition\ContentAccessConditions; +use App\Service\Search\Query\Condition\FacetConditions; +use App\Service\Search\Query\Condition\SearchTermConditions; +use Erichard\ElasticQueryBuilder\Query\BoolQuery; +use Erichard\ElasticQueryBuilder\Query\QueryStringQuery; +use Erichard\ElasticQueryBuilder\QueryBuilder; class QueryGenerator { public function __construct( - protected DocumentQueryGenerator $docQueryGen, - protected DossierQueryGenerator $dosQueryGen, - protected Config $config + private readonly AggregationGenerator $aggregationGenerator, + private readonly ContentAccessConditions $accessConditions, + private readonly FacetConditions $facetConditions, + private readonly SearchTermConditions $searchTermConditions, ) { } - /** - * @return array - */ - public function createFacetsQuery(): array + public function createFacetsQuery(Config $config): QueryBuilder { - $query = [ - 'index' => ElasticConfig::READ_INDEX, - 'body' => [ - 'size' => 0, - '_source' => false, - 'aggs' => $this->addAggregations(5), - 'query' => $this->addQuery(), - ], - ]; + $queryBuilder = new QueryBuilder(); + $queryBuilder->setIndex(ElasticConfig::READ_INDEX); + $queryBuilder->setSize(0); + $queryBuilder->setSource(false); - return $query; + $this->addQuery($queryBuilder, $config); + + $this->aggregationGenerator->addAggregations($queryBuilder, $config, 5); + + return $queryBuilder; } - /** - * @return array - */ - public function createExtendedFacetsQuery(): array + public function createExtendedFacetsQuery(Config $config): QueryBuilder { - $query = [ - 'index' => ElasticConfig::READ_INDEX, - 'body' => [ - 'size' => 0, - '_source' => false, - 'aggs' => $this->addAggregations(25), - ], - ]; + $queryBuilder = new QueryBuilder(); + $queryBuilder->setIndex(ElasticConfig::READ_INDEX); + $queryBuilder->setSize(0); + $queryBuilder->setSource(false); - return $query; + $this->addQuery($queryBuilder, $config); + $this->aggregationGenerator->addAggregations($queryBuilder, $config, 25); + + return $queryBuilder; } - /** - * @return array - */ - public function createQuery(): array + public function createQuery(Config $config): QueryBuilder { - $query = [ - 'index' => ElasticConfig::READ_INDEX, + $queryBuilder = new QueryBuilder(); + $queryBuilder->setIndex(ElasticConfig::READ_INDEX); + $queryBuilder->setSize($config->limit); + $queryBuilder->setFrom($config->offset); + + $params = [ 'body' => [ - 'size' => $this->config->limit, - 'from' => $this->config->offset, '_source' => [ 'excludes' => [ 'content', @@ -73,154 +67,69 @@ public function createQuery(): array 'dossiers.inquiry_ids', ], ], - 'query' => $this->addQuery(), - 'highlight' => $this->addHighlighting(), - 'aggs' => $this->addAggregations(25), - 'suggest' => $this->addSuggestions(), - ], - ]; - - return $query; - } - - /** - * @return array - */ - protected function addSuggestions(): array - { - $suggestions = [ - ElasticConfig::SUGGESTIONS_SEARCH_INPUT => [ - 'text' => $this->config->query, - 'term' => [ - 'field' => 'content_for_suggestions', - 'size' => 3, - 'sort' => 'frequency', - 'suggest_mode' => 'popular', - 'string_distance' => 'jaro_winkler', - ], ], ]; - return $suggestions; - } - - /** - * @return array - */ - protected function addQuery(): array - { - $documentQuery = $this->getDocumentQuery(); - $dossierQuery = $this->getDossierQuery(); - $combinedQuery = $this->combineQueries([$documentQuery, $dossierQuery]); - - return $combinedQuery; - } - - /** - * @return array - */ - protected function getDocumentQuery(): array - { - if (! in_array($this->config->searchType, [Config::TYPE_DOCUMENT, Config::TYPE_ALL])) { - return []; + if ($config->query !== '') { + $params['body']['suggest'] = $this->getSuggestParams($config); } - $dossierConditions = $this->docQueryGen->getConditions($this->config); + $queryBuilder->setParams($params); + + $this->addQuery($queryBuilder, $config); + $this->aggregationGenerator->addAggregations($queryBuilder, $config, 25); + $this->aggregationGenerator->addDocTypeAggregations($queryBuilder); + $this->addHighlight($queryBuilder, $config); - return $dossierConditions; + return $queryBuilder; } - /** - * @return array - */ - protected function getDossierQuery(): array + private function addQuery(QueryBuilder $queryBuilder, Config $config): void { - if (! in_array($this->config->searchType, [Config::TYPE_DOSSIER, Config::TYPE_ALL])) { - return []; - } + $query = new BoolQuery(); - $dossierConditions = $this->dosQueryGen->getConditions($this->config, new TopLevelDossierStrategy()); + $this->accessConditions->applyToQuery($config, $query); + $this->facetConditions->applyToQuery($config, $query); + $this->searchTermConditions->applyToQuery($config, $query); - return $dossierConditions; + $queryBuilder->setQuery($query); } /** - * @param array> $queries - * * @return array */ - protected function combineQueries(array $queries): array + private function getSuggestParams(Config $config): array { - $queries = array_values(array_filter($queries)); - $count = count($queries); - - return match ($count) { - 0 => [], - 1 => $queries[0], - default => [ - 'bool' => [ - 'should' => $queries, - 'minimum_should_match' => 1, + return [ + ElasticConfig::SUGGESTIONS_SEARCH_INPUT => [ + 'text' => $config->query, + 'term' => [ + 'field' => 'content_for_suggestions', + 'size' => 3, + 'sort' => 'frequency', + 'suggest_mode' => 'popular', + 'string_distance' => 'jaro_winkler', ], ], - }; + ]; } - protected function addAggregations(int $maxCount = 5): \stdClass + private function addHighlight(QueryBuilder $queryBuilder, Config $config): void { - if (! $this->config->aggregations) { - return (object) []; + if ($config->query === '') { + return; } - // Based on what type we are searching for, we need to aggregate on different fields - // When searching on dossiers only, we never find aggregations for 'dossiers.departments.name' for instance, but only for 'department.name' - $aggregationConfig = [ - Config::TYPE_DOCUMENT => [ - new NestedAggregationStrategy('dossiers', 'dossiers', [ - new TermsAggregationStrategy(Facet::FACET_DEPARTMENT, 'dossiers.departments.name', $maxCount), - new TermsAggregationStrategy(Facet::FACET_OFFICIAL, 'dossiers.government_officials.name', $maxCount), - new TermsAggregationStrategy(Facet::FACET_PERIOD, 'dossiers.date_period', $maxCount), - ]), - new TermsAggregationStrategy(Facet::FACET_SUBJECT, 'subjects', $maxCount), - new TermsAggregationStrategy(Facet::FACET_SOURCE, 'source_type', $maxCount), - new TermsAggregationStrategy(Facet::FACET_GROUNDS, 'grounds', $maxCount), - new TermsAggregationStrategy(Facet::FACET_JUDGEMENT, 'judgement', $maxCount), - ], - Config::TYPE_DOSSIER => [ - new TermsAggregationStrategy(Facet::FACET_DEPARTMENT, 'departments.name', $maxCount), - new TermsAggregationStrategy(Facet::FACET_OFFICIAL, 'government_officials.name', $maxCount), - new TermsAggregationStrategy(Facet::FACET_PERIOD, 'date_period', $maxCount), - new TermsAggregationStrategy(Facet::FACET_SUBJECT, 'subjects', $maxCount), - new TermsAggregationStrategy(Facet::FACET_SOURCE, 'source_type', $maxCount), - new TermsAggregationStrategy(Facet::FACET_GROUNDS, 'grounds', $maxCount), - new TermsAggregationStrategy(Facet::FACET_JUDGEMENT, 'judgement', $maxCount), - ], - ]; - $aggregationConfig[Config::TYPE_ALL] = $aggregationConfig[Config::TYPE_DOCUMENT]; - - $aggregations = []; - foreach ($aggregationConfig[$this->config->searchType] as $strategy) { - $queryPart = $strategy->getQuery(); - $aggregations = array_merge_recursive($aggregations, $queryPart); - } - - $aggregations['unique_dossiers'] = [ - 'cardinality' => [ - 'field' => 'dossier_nr', - ], - ]; - - return (object) $aggregations; - } + // Hightlighting uses a 'clean' query with additional filters like status. + // This is very important, otherwise filter values like 'document' and statuses will be highlighted in content. + $query = new QueryStringQuery( + query: $config->query, + fields: ['title', 'summary', 'decision_content', 'dossiers.summary', 'dossiers.title', 'pages.content'], + ); - /** - * @return array - */ - protected function addHighlighting(): array - { - return [ + $queryBuilder->setHighlight([ 'max_analyzed_offset' => 1000000, - 'pre_tags' => [''], + 'pre_tags' => [''], 'post_tags' => [''], 'fields' => [ // Document object @@ -250,8 +159,14 @@ protected function addHighlighting(): array 'number_of_fragments' => 5, 'type' => 'unified', ], + 'decision_content' => [ + 'fragment_size' => 50, + 'number_of_fragments' => 5, + 'type' => 'unified', + ], ], - 'require_field_match' => false, - ]; + 'require_field_match' => true, + 'highlight_query' => $query->build(), + ]); } } diff --git a/src/Service/Search/Result/Result.php b/src/Service/Search/Result/Result.php index 96e68d98..25d7bece 100644 --- a/src/Service/Search/Result/Result.php +++ b/src/Service/Search/Result/Result.php @@ -6,6 +6,7 @@ use App\Service\Search\Model\Aggregation; use App\Service\Search\Model\Suggestion; +use App\ValueObject\FilterDetails; use Knp\Component\Pager\Pagination\AbstractPagination; use Knp\Component\Pager\Pagination\PaginationInterface; @@ -38,11 +39,27 @@ class Result /** @var array|mixed[] */ protected array $query; // Actual query used to search + protected FilterDetails $filterDetails; // Details about additional filters (non-facet) + + private int $resultCount; // Total number of result items + public static function create(): self { return new self(); } + public function getResultCount(): int + { + return $this->resultCount; + } + + public function setResultCount(int $resultCount): Result + { + $this->resultCount = $resultCount; + + return $this; + } + public function getDocumentCount(): int { return $this->documentCount; @@ -258,4 +275,16 @@ public function setType(string $type): Result return $this; } + + public function getFilterDetails(): FilterDetails + { + return $this->filterDetails; + } + + public function setFilterDetails(FilterDetails $filterDetails): self + { + $this->filterDetails = $filterDetails; + + return $this; + } } diff --git a/src/Service/Search/Result/ResultTransformer.php b/src/Service/Search/Result/ResultTransformer.php index 4bcb285d..3ca9be95 100644 --- a/src/Service/Search/Result/ResultTransformer.php +++ b/src/Service/Search/Result/ResultTransformer.php @@ -6,11 +6,13 @@ use App\Entity\Document; use App\Entity\Dossier; +use App\Entity\Inquiry; use App\Service\Search\Model\Aggregation; -use App\Service\Search\Model\AggregationBucketEntry; use App\Service\Search\Model\Config; use App\Service\Search\Model\Suggestion; use App\Service\Search\Model\SuggestionEntry; +use App\ValueObject\FilterDetails; +use App\ValueObject\InquiryDescription; use Doctrine\ORM\EntityManagerInterface; use Elastic\Elasticsearch\Response\Elasticsearch; use Jaytaph\TypeArray\TypeArray; @@ -25,7 +27,8 @@ class ResultTransformer public function __construct( protected EntityManagerInterface $doctrine, protected LoggerInterface $logger, - protected PaginatorInterface $paginator + protected PaginatorInterface $paginator, + private readonly AggregationMapper $aggregationMapper, ) { } @@ -63,6 +66,8 @@ public function transform(array $query, Config $config, ?Elasticsearch $response $result->setPagination($pagination); } + $result->setFilterDetails($this->getFilterDetails($config)); + return $result; } @@ -82,10 +87,9 @@ protected function transformResults(Config $config, Elasticsearch $response): Re return $result; } - $result->setDocumentCount($typedResponse->getInt('[hits][total][value]', 0)); - if ($config->aggregations) { - $result->setDossierCount($typedResponse->getInt('[aggregations][unique_dossiers][value]', 0)); - } + $result->setResultCount($typedResponse->getInt('[hits][total][value]', 0)); + $result->setDossierCount($typedResponse->getInt('[aggregations][unique_dossiers][value]', 0)); + $result->setDocumentCount($typedResponse->getInt('[aggregations][unique_documents][value]', 0)); $suggestions = $this->transformSuggestions($typedResponse); if ($suggestions) { @@ -165,13 +169,10 @@ protected function transformAggregations(TypeArray $response): array continue; } - // Regular aggregation, iterate over the buckets and create entries - $entries = []; - foreach ($aggregation->getIterable('[buckets]') as $bucket) { - $entries[] = new AggregationBucketEntry($bucket->getString('[key]'), $bucket->getInt('[doc_count]')); - } - - $ret[] = new Aggregation(strval($name), $entries); + $ret[] = $this->aggregationMapper->map( + strval($name), + $aggregation->getIterable('[buckets]') + ); } return $ret; @@ -201,7 +202,7 @@ protected function extractDocumentEntry(TypeArray $hit): ?ResultEntry $highlightPaths = [ '[highlight][pages.content]', '[highlight][dossiers.title]', - '[highlight][dossiers.title]', + '[highlight][dossiers.summary]', ]; $highlightData = $this->getHighlightData($hit, $highlightPaths); @@ -230,6 +231,7 @@ protected function extractDossierEntry(TypeArray $hit): ?ResultEntry $highlightPaths = [ '[highlight][title]', '[highlight][summary]', + '[highlight][decision_content]', ]; $highlightData = $this->getHighlightData($hit, $highlightPaths); @@ -260,4 +262,33 @@ protected function getHighlightData(TypeArray $hit, array $paths): array /** @var string[] $highlightData */ return $highlightData; } + + /** + * @param string[] $inquiryIds + * + * @return InquiryDescription[] + */ + private function getInquiryDescriptions(array $inquiryIds): array + { + if (count($inquiryIds) === 0) { + return []; + } + + return array_map( + static fn (Inquiry $inquiry): InquiryDescription => InquiryDescription::fromEntity($inquiry), + $this->doctrine->getRepository(Inquiry::class)->findBy(['id' => $inquiryIds]) + ); + } + + private function getFilterDetails(Config $config): FilterDetails + { + /** @var string[] $dossierNrs */ + $dossierNrs = $config->facets['dnr'] ?? []; + + return new FilterDetails( + $this->getInquiryDescriptions($config->dossierInquiries), + $this->getInquiryDescriptions($config->documentInquiries), + $dossierNrs, + ); + } } diff --git a/src/Service/Search/SearchService.php b/src/Service/Search/SearchService.php index e9dd5cc9..8c0c2868 100644 --- a/src/Service/Search/SearchService.php +++ b/src/Service/Search/SearchService.php @@ -8,7 +8,7 @@ use App\Service\Elastic\ElasticClientInterface; use App\Service\Search\Model\Config; use App\Service\Search\Object\ObjectHandler; -use App\Service\Search\Query\QueryGeneratorFactory; +use App\Service\Search\Query\QueryGenerator; use App\Service\Search\Result\Result; use App\Service\Search\Result\ResultTransformer; use Elastic\Elasticsearch\Response\Elasticsearch; @@ -19,7 +19,7 @@ class SearchService public function __construct( protected ElasticClientInterface $elastic, protected LoggerInterface $logger, - protected QueryGeneratorFactory $queryGenFactory, + protected QueryGenerator $queryGenerator, protected ObjectHandler $objectHandler, protected ResultTransformer $resultTransformer ) { @@ -27,18 +27,16 @@ public function __construct( public function searchFacets(Config $config): Result { - $queryGenerator = $this->queryGenFactory->create($config); - $query = $queryGenerator->createFacetsQuery(); + $query = $this->queryGenerator->createFacetsQuery($config); - return $this->doSearch($query, $config); + return $this->doSearch($query->build(), $config); } public function search(Config $config): Result { - $queryGenerator = $this->queryGenFactory->create($config); - $query = $queryGenerator->createQuery(); + $query = $this->queryGenerator->createQuery($config); - return $this->doSearch($query, $config); + return $this->doSearch($query->build(), $config); } public function isIngested(Document $document): bool @@ -53,12 +51,10 @@ public function getPageContent(Document $document, int $pageNr): string public function retrieveExtendedFacets(): Result { - $queryGenerator = $this->queryGenFactory->create(); - $query = $queryGenerator->createExtendedFacetsQuery(); - $config = new Config(limit: 0); + $query = $this->queryGenerator->createExtendedFacetsQuery($config); - return $this->doSearch($query, $config); + return $this->doSearch($query->build(), $config); } /** diff --git a/src/Service/SqlDump/NodeVisitor.php b/src/Service/SqlDump/NodeVisitor.php index 94de3706..5507a559 100644 --- a/src/Service/SqlDump/NodeVisitor.php +++ b/src/Service/SqlDump/NodeVisitor.php @@ -10,6 +10,10 @@ use PhpParser\NodeVisitorAbstract; use Symfony\Component\Console\Output\OutputInterface; +/** + * This class is a node visitor for the sql dump tool. It will traverse the AST and find the up method, and then + * process the statements in that method. If there is an addSql statement, it will write the SQL to the output. + */ class NodeVisitor extends NodeVisitorAbstract { protected OutputInterface $output; diff --git a/src/Service/Storage/DocumentStorageService.php b/src/Service/Storage/DocumentStorageService.php index 7c21f0b6..34aaae22 100644 --- a/src/Service/Storage/DocumentStorageService.php +++ b/src/Service/Storage/DocumentStorageService.php @@ -5,6 +5,7 @@ namespace App\Service\Storage; use App\Entity\Document; +use App\Entity\EntityWithFileInfo; use Doctrine\ORM\EntityManagerInterface; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemOperator; @@ -154,7 +155,11 @@ public function store(\SplFileInfo $localFile, string $remotePath): bool // An exception occurred when trying to store the file. return false; } finally { - fclose($stream); + // Close the stream if not already closed + // @phpstan-ignore-next-line + if (is_resource($stream)) { + @fclose($stream); + } } return true; @@ -215,7 +220,7 @@ public function existsPage(Document $document, int $pageNr): bool public function retrieveDocument(Document $document, string $localPath): bool { - $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFilepath() ?? '')); + $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFileInfo()->getPath() ?? '')); return $this->retrieve($remotePath, $localPath); } @@ -223,14 +228,14 @@ public function retrieveDocument(Document $document, string $localPath): bool /** * @return resource|null */ - public function retrieveResourceDocument(Document $document) + public function retrieveResourceDocument(EntityWithFileInfo $document) { - $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFilepath() ?? '')); + $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFileInfo()->getPath() ?? '')); return $this->retrieveResource($remotePath); } - public function storeDocument(\SplFileInfo $localFile, Document $document): bool + public function storeDocument(\SplFileInfo $localFile, EntityWithFileInfo $document): bool { $remotePath = $this->generateDocumentPath($document, $localFile); @@ -240,12 +245,13 @@ public function storeDocument(\SplFileInfo $localFile, Document $document): bool } // Store file information in document record - $document->setFilepath($remotePath); - $document->setFilesize($localFile->getSize()); + $file = $document->getFileInfo(); + $file->setPath($remotePath); + $file->setSize($localFile->getSize()); $foundationFile = new File($localFile->getPathname()); - $document->setMimetype($foundationFile->getMimeType() ?? ''); - $document->setUploaded(true); + $file->setMimetype($foundationFile->getMimeType() ?? ''); + $file->setUploaded(true); $this->doctrine->persist($document); $this->doctrine->flush(); @@ -255,7 +261,7 @@ public function storeDocument(\SplFileInfo $localFile, Document $document): bool public function existsDocument(Document $document): bool { - $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFilepath() ?? '')); + $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFileInfo()->getPath() ?? '')); return $this->exists($remotePath); } @@ -286,6 +292,11 @@ public function download(string $remotePath): string|false return $localPath; } + // Remove the temporary file when retrieval fails + if (file_exists($localPath)) { + unlink($localPath); + } + return false; } @@ -296,9 +307,9 @@ public function downloadPage(Document $document, int $pageNr): string|false return $this->download($remotePath); } - public function downloadDocument(Document $document): string|false + public function downloadDocument(EntityWithFileInfo $document): string|false { - $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFilepath() ?? '')); + $remotePath = $this->generateDocumentPath($document, new \SplFileInfo($document->getFileInfo()->getPath() ?? '')); return $this->download($remotePath); } @@ -309,10 +320,10 @@ public function downloadDocument(Document $document): string|false * Since download*() does not copy the file but actually points to the given file when * the filesystem is local, this function will NOT delete the file in that case. */ - public function removeDownload(string $localPath): void + public function removeDownload(string $localPath, bool $forceLocalDelete = false): void { // Don't remove when the storage is local. It would point to the actual stored file - if ($this->isLocal) { + if ($this->isLocal && ! $forceLocalDelete) { return; } @@ -326,7 +337,7 @@ public function removeDownload(string $localPath): void * Returns the root path of a document. Normally, this is /{prefix}/{suffix}, where prefix are the first two characters of the * SHA256 hash, and suffix is the rest of the SHA256 hash. */ - protected function getRootPathForDocument(Document $document): string + protected function getRootPathForDocument(EntityWithFileInfo $document): string { $documentId = (string) $document->getId(); $hash = hash('sha256', $documentId); @@ -340,7 +351,7 @@ protected function getRootPathForDocument(Document $document): string /** * Generates the path to a document. It will use the original filename of the file object if it's an uploaded file. */ - protected function generateDocumentPath(Document $document, \SplFileInfo $file): string + protected function generateDocumentPath(EntityWithFileInfo $document, \SplFileInfo $file): string { $rootPath = $this->getRootPathForDocument($document); @@ -390,4 +401,26 @@ public function list(string $remotePath, string $filter = '*'): array return $ret; } + + public function deleteAllFilesForDocument(Document $document): bool + { + try { + $path = $this->generateDocumentPath($document, new \SplFileInfo($document->getFileInfo()->getPath() ?? '')); + $this->storage->delete($path); + + for ($pageNr = 1; $pageNr <= $document->getPageCount(); $pageNr++) { + $path = $this->generatePagePath($document, $pageNr); + $this->storage->delete($path); + } + } catch (\Throwable $e) { + $this->logger->error('Could not delete files from storage', [ + 'exception' => $e->getMessage(), + 'path' => $path ?? '', + ]); + + return false; + } + + return true; + } } diff --git a/src/Service/Storage/ThumbnailStorageService.php b/src/Service/Storage/ThumbnailStorageService.php index 241168d3..328d863e 100644 --- a/src/Service/Storage/ThumbnailStorageService.php +++ b/src/Service/Storage/ThumbnailStorageService.php @@ -161,6 +161,56 @@ public function exists(Document $document, int $pageNr = null): bool return false; } + /** + * Returns the filesize in bytes, or 0 when file is not found (or empty, not readable etc). + */ + public function fileSize(Document $document, int $pageNr = null): int + { + if ($pageNr) { + $path = $this->generatePagePath($document, $pageNr); + } else { + $path = $this->generateDocumentPath($document); + } + + $this->logger->info('Path: ' . $path); + + // Create path if not exists + try { + return $this->storage->fileSize($path); + } catch (FilesystemException $e) { + // Could not create directory + $this->logger->error('Could not check file size', [ + 'document' => $document->getId(), + 'path' => $path, + 'exception' => $e->getMessage(), + ]); + } + + return 0; + } + + public function deleteAllThumbsForDocument(Document $document): bool + { + try { + $path = $this->generateDocumentPath($document); + $this->storage->delete($path); + + for ($pageNr = 1; $pageNr <= $document->getPageCount(); $pageNr++) { + $path = $this->generatePagePath($document, $pageNr); + $this->storage->delete($path); + } + } catch (\Throwable $e) { + $this->logger->error('Could not delete thumbnails from storage', [ + 'exception' => $e->getMessage(), + 'path' => $path ?? '', + ]); + + return false; + } + + return true; + } + /** * Returns the root path of a document. Normally, this is /{prefix}/{suffix}, where prefix are the first two characters of the * SHA256 hash, and suffix is the rest of the SHA256 hash. diff --git a/src/Service/Worker/Audio/Extractor/AudioExtractor.php b/src/Service/Worker/Audio/Extractor/AudioExtractor.php index 5f0aa2c2..38348ea1 100644 --- a/src/Service/Worker/Audio/Extractor/AudioExtractor.php +++ b/src/Service/Worker/Audio/Extractor/AudioExtractor.php @@ -72,6 +72,8 @@ protected function extractContentFromAudio(Document $document): array $content = $this->extractText($localAudioPath); + $this->documentStorage->removeDownload($localAudioPath); + return [$content, $metaData]; } diff --git a/src/Service/Worker/Audio/Extractor/WaveImageExtractor.php b/src/Service/Worker/Audio/Extractor/WaveImageExtractor.php index bfeb0811..d6c632c0 100644 --- a/src/Service/Worker/Audio/Extractor/WaveImageExtractor.php +++ b/src/Service/Worker/Audio/Extractor/WaveImageExtractor.php @@ -75,6 +75,8 @@ public function extract(Document $document, bool $forceRefresh): void $process = new Process($params); $process->run(); + $this->documentStorage->removeDownload($localPath); + if (! $process->isSuccessful()) { $this->logger->error('Failed to create wave image for audio', [ 'document' => $document->getId(), @@ -84,7 +86,6 @@ public function extract(Document $document, bool $forceRefresh): void ]); $this->fileUtils->deleteTempDirectory($tempDir); - $this->documentStorage->removeDownload($localPath); return; } @@ -92,6 +93,5 @@ public function extract(Document $document, bool $forceRefresh): void $this->thumbnailStorage->store($document, new File($targetPath), 0); $this->fileUtils->deleteTempDirectory($tempDir); - $this->documentStorage->removeDownload($localPath); } } diff --git a/src/Service/Worker/Pdf/Extractor/DocumentContentExtractor.php b/src/Service/Worker/Pdf/Extractor/DocumentContentExtractor.php index 161b7201..d47c7232 100644 --- a/src/Service/Worker/Pdf/Extractor/DocumentContentExtractor.php +++ b/src/Service/Worker/Pdf/Extractor/DocumentContentExtractor.php @@ -69,6 +69,8 @@ protected function extractContentFromPdf(Document $document): array $tikaData = $this->tika->extract($localPdfPath); $documentContent = $tikaData['X-TIKA:content'] ?? ''; + $this->documentStorage->removeDownload($localPdfPath); + return [$documentContent, $tikaData]; } diff --git a/src/Service/Worker/Pdf/Extractor/PageContentExtractor.php b/src/Service/Worker/Pdf/Extractor/PageContentExtractor.php index 7e13128a..437a751d 100644 --- a/src/Service/Worker/Pdf/Extractor/PageContentExtractor.php +++ b/src/Service/Worker/Pdf/Extractor/PageContentExtractor.php @@ -75,6 +75,8 @@ protected function extractContentFromPdf(Document $document, int $pageNr): array $pageContent[] = $tikaData['X-TIKA:content'] ?? ''; $pageContent[] = $this->tesseract->extract($localPdfPath); + $this->documentStorage->removeDownload($localPdfPath); + return [join("\n", $pageContent), $tikaData]; } diff --git a/src/Service/Worker/Pdf/Extractor/PageExtractor.php b/src/Service/Worker/Pdf/Extractor/PageExtractor.php index f54089ed..8dc88d9e 100644 --- a/src/Service/Worker/Pdf/Extractor/PageExtractor.php +++ b/src/Service/Worker/Pdf/Extractor/PageExtractor.php @@ -38,6 +38,13 @@ public function extract(Document $document, int $pageNr, bool $forceRefresh): vo } $localPath = $this->documentStorage->downloadDocument($document); + if (! $localPath) { + $this->logger->error('cannot download document from storage', [ + 'document' => $document->getId(), + ]); + + return; + } $tempDir = $this->fileUtils->createTempDir(); $targetPath = $tempDir . '/page.pdf'; @@ -47,6 +54,9 @@ public function extract(Document $document, int $pageNr, bool $forceRefresh): vo $process = new Process($params); $process->run(); + // Remove local file + $this->documentStorage->removeDownload($localPath); + if (! $process->isSuccessful()) { $this->logger->error('Failed to fetch PDF page: ', [ 'document' => $document->getId(), diff --git a/src/Service/Worker/Pdf/Extractor/PagecountExtractor.php b/src/Service/Worker/Pdf/Extractor/PagecountExtractor.php index 3ca3d276..40ce33d9 100644 --- a/src/Service/Worker/Pdf/Extractor/PagecountExtractor.php +++ b/src/Service/Worker/Pdf/Extractor/PagecountExtractor.php @@ -60,11 +60,20 @@ protected function setCachedPageCount(Document $document, int $count): void protected function extractPageCountFromPdf(Document $document): int { $localPdfPath = $this->documentStorage->downloadDocument($document); + if (! $localPdfPath) { + $this->logger->error('Failed to download document for page count extraction', [ + 'document' => $document->getDocumentNr(), + ]); + + return 0; + } $params = ['/usr/bin/pdftk', $localPdfPath, 'dump_data']; $process = new Process($params); $process->run(); + $this->documentStorage->removeDownload($localPdfPath); + if (! $process->isSuccessful()) { $this->logger->error('Failed to get page count: ', [ 'sourcePath' => $localPdfPath, diff --git a/src/Service/Worker/Pdf/Extractor/ThumbnailExtractor.php b/src/Service/Worker/Pdf/Extractor/ThumbnailExtractor.php index 411b783c..8f4c2358 100644 --- a/src/Service/Worker/Pdf/Extractor/ThumbnailExtractor.php +++ b/src/Service/Worker/Pdf/Extractor/ThumbnailExtractor.php @@ -58,6 +58,8 @@ public function extract(Document $document, int $pageNr, bool $forceRefresh): vo $process = new Process($params); $process->run(); + $this->documentStorage->removeDownload($localPath); + if (! $process->isSuccessful()) { $this->logger->error('Failed to create thumbnail for document', [ 'document' => $document->getId(), @@ -68,7 +70,6 @@ public function extract(Document $document, int $pageNr, bool $forceRefresh): vo ]); $this->fileUtils->deleteTempDirectory($tempDir); - $this->documentStorage->removeDownload($localPath); return; } @@ -77,6 +78,5 @@ public function extract(Document $document, int $pageNr, bool $forceRefresh): vo $this->thumbnailStorage->store($document, $file, $pageNr); $this->fileUtils->deleteTempDirectory($tempDir); - $this->documentStorage->removeDownload($localPath); } } diff --git a/src/SourceType.php b/src/SourceType.php index 34b02598..2e88c182 100644 --- a/src/SourceType.php +++ b/src/SourceType.php @@ -70,6 +70,7 @@ class SourceType ], ]; + // Finds the given source type in the list of known types public static function getType(string $target): string { $target = strtolower($target); @@ -84,6 +85,8 @@ public static function getType(string $target): string } /** + * Returns a list of all known source types. + * * @return array|string[] */ public static function getAllSourceTypes(): array diff --git a/src/Twig/Extension/AppExtension.php b/src/Twig/Extension/AppExtension.php index 629b4539..1a252a6b 100644 --- a/src/Twig/Extension/AppExtension.php +++ b/src/Twig/Extension/AppExtension.php @@ -44,6 +44,7 @@ public function getFunctions(): array new TwigFunction('choice_attr', [$this->runtime, 'getChoiceAttribute']), new TwigFunction('app_version', [$this->runtime, 'appVersion']), new TwigFunction('die', [$this->runtime, 'dieTwig']), + new TwigFunction('is_backend', [$this->runtime, 'isBackend']), ]; } } diff --git a/src/Twig/Extension/DateExtension.php b/src/Twig/Extension/DateExtension.php index 088bf232..e0ee62f0 100644 --- a/src/Twig/Extension/DateExtension.php +++ b/src/Twig/Extension/DateExtension.php @@ -19,7 +19,7 @@ public function getFunctions(): array /** * Validates whether a date string conforms to a specified format. */ - public function isValidDate(?string $date, string $format = 'd-m-Y'): bool + public function isValidDate(?string $date, string $format = 'Y-m-d'): bool { if (! $date) { return false; diff --git a/src/Twig/Extension/WooExtension.php b/src/Twig/Extension/WooExtension.php index 4c5622e2..0d4e6bb6 100644 --- a/src/Twig/Extension/WooExtension.php +++ b/src/Twig/Extension/WooExtension.php @@ -39,6 +39,10 @@ public function getFunctions(): array new TwigFunction('status_badge', [$this->runtime, 'statusBadge'], ['is_safe' => ['html']]), new TwigFunction('period', [$this->runtime, 'period']), new TwigFunction('has_thumbnail', [$this->runtime, 'hasThumbnail']), + new TwigFunction('is_document_id', [$this->runtime, 'isDocumentLink']), + new TwigFunction('generate_document_link', [$this->runtime, 'generateDocumentLink']), + new TwigFunction('get_citation_type', [$this->runtime, 'getCitationType']), + new TwigFunction('query_string_without_param', [$this->runtime, 'queryStringWithoutParam']), ]; } } diff --git a/src/Twig/Runtime/AppExtensionRuntime.php b/src/Twig/Runtime/AppExtensionRuntime.php index a23e2d37..51a3e98a 100644 --- a/src/Twig/Runtime/AppExtensionRuntime.php +++ b/src/Twig/Runtime/AppExtensionRuntime.php @@ -6,15 +6,19 @@ use Carbon\Carbon; use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Twig\Extension\RuntimeExtensionInterface; class AppExtensionRuntime implements RuntimeExtensionInterface { protected string $projectPath; + protected RequestStack $requestStack; - public function __construct(string $projectPath) + public function __construct(string $projectPath, RequestStack $requestStack) { $this->projectPath = $projectPath; + $this->requestStack = $requestStack; } public function basename(string $value): string @@ -77,4 +81,11 @@ public function isInstanceOf(mixed $var, string $instance): bool { return $var instanceof $instance; } + + public function isBackend(): bool + { + $request = $this->requestStack->getCurrentRequest() ?? new Request(); + + return str_starts_with($request->getPathInfo(), '/balie'); + } } diff --git a/src/Twig/Runtime/WooExtensionRuntime.php b/src/Twig/Runtime/WooExtensionRuntime.php index 863ef3ab..4fcda66e 100644 --- a/src/Twig/Runtime/WooExtensionRuntime.php +++ b/src/Twig/Runtime/WooExtensionRuntime.php @@ -7,27 +7,41 @@ use App\Citation; use App\Entity\Document; use App\Entity\Dossier; +use App\Repository\DocumentRepository; use App\Service\DateRangeConverter; -use App\Service\Search\Model\Facet; +use App\Service\Search\Query\Facet\FacetMappingService; use App\Service\Storage\ThumbnailStorageService; use App\SourceType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\RuntimeExtensionInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class WooExtensionRuntime implements RuntimeExtensionInterface { protected RequestStack $requestStack; protected ThumbnailStorageService $storageService; + protected DocumentRepository $documentRepository; + protected UrlGeneratorInterface $urlGenerator; + protected TranslatorInterface $translator; - private TranslatorInterface $translator; - - public function __construct(RequestStack $requestStack, ThumbnailStorageService $storageService, TranslatorInterface $translator) - { + public function __construct( + RequestStack $requestStack, + ThumbnailStorageService $storageService, + TranslatorInterface $translator, + DocumentRepository $documentRepository, + UrlGeneratorInterface $urlGenerator, + private readonly FacetMappingService $facetMapping, + ) { $this->requestStack = $requestStack; $this->storageService = $storageService; $this->translator = $translator; + $this->documentRepository = $documentRepository; + $this->urlGenerator = $urlGenerator; } /** @@ -104,7 +118,7 @@ public function decision(string $value): string case Dossier::DECISION_NOTHING_FOUND: return 'Niets gevonden'; case Dossier::DECISION_PARTIAL_PUBLIC: - return 'Gedeeltelijk gepubliceerd'; + return 'Deels openbaar'; default: return 'Onbekend'; @@ -140,8 +154,8 @@ public function period(?\DateTimeImmutable $from, ?\DateTimeImmutable $to): stri */ public function hasFacets(Request $request): bool { - foreach (Facet::getQueryMapping() as $queryKey) { - if ($request->query->has($queryKey)) { + foreach ($this->facetMapping->getAll() as $defition) { + if ($request->query->has($defition->getQueryParam())) { return true; } } @@ -162,6 +176,98 @@ public function hasThumbnail(Document $document, int $pageNr): bool */ public function facet2query(string $facet): string { - return Facet::getQueryVarForFacet($facet); + return $this->facetMapping->getFacetByKey($facet)->getQueryParam(); + } + + /** + * Returns true if the given link is actually a document ID (ie: PREFIX-12345) and the given dossier is set to published. + */ + public function isDocumentLink(string $link): bool + { + /** @var Document|null $document */ + $document = $this->documentRepository->findOneBy(['documentNr' => $link]); + if (is_null($document)) { + return false; + } + + // If we find a dossier with status published, we can return true + foreach ($document->getDossiers() as $dossier) { + if ($dossier->getStatus() == Dossier::STATUS_PUBLISHED) { + return true; + } + } + + return false; + } + + /** + * Generate the link from the given document ID found in the link. If the link is not an existing document id, it will be returned as is. + */ + public function generateDocumentLink(string $link): string + { + /** @var Document|null $document */ + $document = $this->documentRepository->findOneBy(['documentNr' => $link]); + if (is_null($document)) { + return $link; + } + + // If we find a dossier with status published, we can return true + foreach ($document->getDossiers() as $dossier) { + if ($dossier->getStatus() == Dossier::STATUS_PUBLISHED) { + return $this->urlGenerator->generate('app_document_detail', [ + 'dossierId' => $dossier->getDossierNr(), + 'documentId' => $document->getDocumentNr(), + ]); + } + } + + return $link; + } + + public function getCitationType(string $citation): string + { + return Citation::getCitationType($citation); + } + + public function queryStringWithoutParam(string $queryParam, string $value): string + { + $request = $this->requestStack->getCurrentRequest(); + if (! $request) { + return ''; + } + + $queryString = strval($request->getQueryString()); + parse_str($queryString, $currentParams); + parse_str($queryParam, $paramToRemove); + $paramKeyToRemove = key($paramToRemove); + + $currentParamValue = $currentParams[$paramKeyToRemove] ?? null; + if ($currentParamValue === null) { + return $queryString; + } + + if (is_array($currentParamValue)) { + if (array_is_list($currentParamValue)) { + foreach ($currentParamValue as $paramSubKey => $paramSubValue) { + if ($paramSubValue === $value) { + unset($currentParams[$paramKeyToRemove][$paramSubKey]); + break; + } + } + } else { + /** @var array> $paramToRemove */ + $paramSubKey = key($paramToRemove[$paramKeyToRemove]); + unset($currentParams[$paramKeyToRemove][$paramSubKey]); + } + } else { + unset($currentParams[$paramKeyToRemove]); + } + + $currentParams = array_filter($currentParams); + + $query = http_build_query($currentParams); + $query = preg_replace('/%5B\d+%5D/imU', '%5B%5D', $query); + + return strval($query); } } diff --git a/symfony.lock b/symfony.lock index c504d6f5..9ea87014 100644 --- a/symfony.lock +++ b/symfony.lock @@ -116,6 +116,9 @@ "tests/bootstrap.php" ] }, + "presta/sitemap-bundle": { + "version": "v3.3.1" + }, "scheb/2fa-bundle": { "version": "6.8", "recipe": { diff --git a/tailwind.config.js b/tailwind.config.js index b311805c..76abbd6e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,11 +1,18 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ - "./templates/**/*.html.twig", - ], - theme: { - extend: {}, - }, - plugins: [], + content: [ + "./templates/**/*.html.twig", + ], + theme: { + extend: { + // https://www.color-name.com/hex/f3f3f3 + colors: { + 'anti-flash-white': '#f3f3f3', + 'dim-gray': '#696969', + independence: '#475467' + }, + }, + }, + plugins: [], } diff --git a/templates/admin.html.twig b/templates/admin.html.twig index b9fca817..7bf15197 100644 --- a/templates/admin.html.twig +++ b/templates/admin.html.twig @@ -5,7 +5,7 @@ {% block meta %}{% endblock %} - Admin + Beheer {{ SITE_NAME }} @@ -14,15 +14,62 @@ {{ encore_entry_script_tags('woopie') }} {{ encore_entry_script_tags('admin') }} + + {% include "piwik.html.twig" %}
- WobCovid Publicatie/document management + + {%- block header_content -%} +
+ {% block page_title %}

{{ 'Admin' | trans() }}

{% endblock %} + {% block actions %}{% endblock %} +
+ {%- endblock -%}
+ {% block body %}{% endblock %}